@north-light/crouter 0.3.162 → 0.3.164
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-memory/00-runtime-base.md +1 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -0
- package/dist/builtin-memory/init.md +38 -0
- package/dist/builtin-memory/internal/INDEX.md +1 -0
- package/dist/builtin-memory/internal/memory-loading.md +45 -0
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +34 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +14 -3
- package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +1 -1
- package/dist/clients/attach/__tests__/crtr-output.test.js +14 -9
- package/dist/clients/attach/__tests__/edit-diff.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/edit-diff.test.js +35 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/editor-frame-title.test.js +19 -0
- package/dist/clients/attach/render/chat-view.js +54 -38
- package/dist/clients/attach/render/crtr-output.d.ts +2 -1
- package/dist/clients/attach/render/crtr-output.js +33 -4
- package/dist/clients/attach/render/edit-diff.d.ts +21 -6
- package/dist/clients/attach/render/edit-diff.js +104 -81
- package/dist/clients/attach/render/tool-calls.d.ts +21 -20
- package/dist/clients/attach/render/tool-calls.js +89 -43
- package/dist/clients/attach/session/connection.d.ts +1 -1
- package/dist/clients/attach/session/editor-frame.d.ts +5 -1
- package/dist/clients/attach/session/editor-frame.js +21 -15
- package/dist/clients/attach/session/identity.d.ts +1 -1
- package/dist/clients/attach/viewer.js +628 -624
- package/dist/commands/__tests__/api-canvas-source.test.d.ts +1 -0
- package/dist/commands/__tests__/api-canvas-source.test.js +37 -0
- package/dist/commands/__tests__/human.test.js +2 -0
- package/dist/commands/__tests__/search-contents.test.d.ts +1 -0
- package/dist/commands/__tests__/search-contents.test.js +52 -0
- package/dist/commands/api-client.js +1 -0
- package/dist/commands/human/prompts.js +2 -2
- package/dist/commands/human/shared.d.ts +1 -0
- package/dist/commands/human/shared.js +7 -4
- package/dist/commands/memory/lint.d.ts +6 -0
- package/dist/commands/memory/lint.js +177 -35
- package/dist/commands/memory/read.js +22 -1
- package/dist/commands/memory/write.js +18 -4
- package/dist/commands/memory.js +1 -1
- package/dist/commands/search/contents.js +2 -2
- package/dist/commands/sys/__tests__/sync-deps.test.js +12 -21
- package/dist/commands/sys/__tests__/sync-import.test.js +30 -29
- package/dist/commands/sys/setup-wizard.d.ts +8 -1
- package/dist/commands/sys/setup-wizard.js +209 -58
- package/dist/commands/sys/sync-deps.js +14 -18
- package/dist/commands/sys/sync-project-guidance.js +16 -19
- package/dist/core/__tests__/migration.test.js +8 -3
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +7 -10
- package/dist/core/__tests__/on-read-dedup-resume.test.js +13 -25
- package/dist/core/__tests__/on-read-identity.test.js +8 -15
- package/dist/core/__tests__/revive.test.js +2 -2
- package/dist/core/__tests__/tmux-surface.test.js +10 -4
- package/dist/core/__tests__/worktree.test.js +3 -3
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +18 -11
- package/dist/core/canvas/labels.d.ts +4 -5
- package/dist/core/canvas/labels.js +9 -9
- package/dist/core/command-manifests/schema.js +18 -5
- package/dist/core/command.js +17 -7
- package/dist/core/configured-clis/invoker.js +2 -0
- package/dist/core/help.d.ts +3 -1
- package/dist/core/help.js +5 -2
- package/dist/core/keybindings/__tests__/resolve.test.js +5 -2
- package/dist/core/keybindings/catalog.d.ts +3 -2
- package/dist/core/keybindings/catalog.js +34 -25
- package/dist/core/keybindings/index.d.ts +1 -1
- package/dist/core/keybindings/resolve.js +8 -0
- package/dist/core/keybindings/types.d.ts +6 -1
- package/dist/core/memory/doc-link-grammar.d.ts +20 -0
- package/dist/core/memory/doc-link-grammar.js +110 -0
- package/dist/core/memory-resolver.d.ts +5 -0
- package/dist/core/memory-resolver.js +12 -1
- package/dist/core/runtime/bearings.d.ts +8 -8
- package/dist/core/runtime/bearings.js +20 -16
- package/dist/core/runtime/broker.js +3 -13
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/launch.d.ts +1 -1
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/tmux.js +136 -100
- package/dist/core/scope.js +4 -5
- package/dist/core/substrate/ceiling.d.ts +3 -6
- package/dist/core/substrate/ceiling.js +13 -15
- package/dist/core/substrate/index.d.ts +2 -3
- package/dist/core/substrate/index.js +2 -2
- package/dist/core/substrate/on-read.d.ts +4 -13
- package/dist/core/substrate/on-read.js +142 -262
- package/dist/core/substrate/render.js +4 -4
- package/dist/core/substrate/schema.d.ts +3 -2
- package/dist/core/substrate/schema.js +5 -5
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-tool-guide.test.js +96 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +7 -8
- package/dist/pi-extensions/canvas-tool-guide.d.ts +14 -0
- package/dist/pi-extensions/canvas-tool-guide.js +77 -0
- package/package.json +2 -2
- package/runtime.lock.json +6 -6
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crouter-help.ts +0 -95
|
@@ -1,75 +1,38 @@
|
|
|
1
|
-
// on-read.ts —
|
|
1
|
+
// on-read.ts — file-context delivery for the document substrate.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// system-prompt rung the boot render uses). Three triggers decide which docs
|
|
7
|
-
// surface (design §4/§6; Stream A native rules):
|
|
3
|
+
// A memory with file-read visibility participates only through an explicit
|
|
4
|
+
// `applies-to` path glob (authoring lint requires one). Two events use that one
|
|
5
|
+
// routing mechanism:
|
|
8
6
|
//
|
|
9
|
-
// •
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// reads through explicit `applies-to` / `read-when` triggers.
|
|
18
|
-
// • applies-to GLOB — any RESOLVED substrate doc (user/project/builtin scope)
|
|
19
|
-
// whose `appliesTo` glob matches the read file path surfaces, regardless of
|
|
20
|
-
// where the read file sits relative to the doc.
|
|
21
|
-
// • read-when FRONTMATTER (Stream A) — any RESOLVED doc whose `readWhen`
|
|
22
|
-
// condition matches the READ FILE's own parsed frontmatter (markdown only),
|
|
23
|
-
// via `evalCondition` against that YAML rather than the node subject.
|
|
7
|
+
// • A successful `read` tool call evaluates the read file against every
|
|
8
|
+
// visible substrate doc. Project stores encountered between that file and
|
|
9
|
+
// the filesystem root join the resolved cwd/profile corpus, so a nested
|
|
10
|
+
// workspace can contribute guidance exactly when a file beneath it is read.
|
|
11
|
+
// • Opening a workspace evaluates the reserved `applies-to: "."` target for
|
|
12
|
+
// every project store mounted by the cwd and selected profile. This happens
|
|
13
|
+
// while the first-message bearings are assembled, so workspace-wide context
|
|
14
|
+
// arrives before the task rather than waiting for an arbitrary first file.
|
|
24
15
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// scope. The two explicit triggers compose by OR.
|
|
16
|
+
// `read-when` remains an additional OR trigger over a read markdown file's own
|
|
17
|
+
// frontmatter. It does not replace `applies-to`: every non-none file-read rung
|
|
18
|
+
// still declares its path boundary explicitly.
|
|
29
19
|
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// cross-cutting applies-to/read-when trigger is the author deliberately opting
|
|
34
|
-
// ONE doc into surfacing when a specific file is read, so a `none`-capped
|
|
35
|
-
// ancestor INDEX (the default for a reference plugin pulled by name) must not
|
|
36
|
-
// veto it. Same stance as D5 — the explicit trigger is the more specific signal.
|
|
37
|
-
//
|
|
38
|
-
// Each candidate runs the substrate pipeline at its fileReadVisibility rung:
|
|
39
|
-
// parse → gatePasses(doc, assembleNodeSubject(nodeId)) → render
|
|
40
|
-
// (content → body, preview → previewLine, name → bare tag, none → skip).
|
|
41
|
-
// The result is the faithful envelope (verdict n1):
|
|
42
|
-
// <auto-loaded-context>
|
|
43
|
-
// <memory kind="…" name="…" src="…">…body/preview…</memory>
|
|
44
|
-
// </auto-loaded-context>
|
|
45
|
-
// Returns '' when nothing surfaces.
|
|
46
|
-
//
|
|
47
|
-
// Pure + defensive: reads disk + the resolver + canvas-db subject assembly; no
|
|
48
|
-
// writes, no side effects. Every disk/parse/glob step is wrapped so one bad doc
|
|
49
|
-
// can never throw the whole render (the extension is additionally inert on
|
|
50
|
-
// error). The CALLER owns the per-session `seen` realpath set (cleared on
|
|
51
|
-
// session_start) and threads it in, so a given doc is injected at most once per
|
|
52
|
-
// session across repeated reads.
|
|
20
|
+
// Every candidate renders at its own fileReadVisibility rung. Explicit routing
|
|
21
|
+
// is the whole on-read model, so directory INDEX ceilings remain a boot-catalog
|
|
22
|
+
// concern and do not cap a deliberately matched document.
|
|
53
23
|
import { realpathSync } from 'node:fs';
|
|
54
24
|
import { homedir } from 'node:os';
|
|
55
|
-
import { basename, dirname,
|
|
25
|
+
import { basename, dirname, matchesGlob, parse, relative, sep } from 'node:path';
|
|
56
26
|
import { CRTR_DIR_NAME } from '../../types.js';
|
|
57
27
|
import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
58
28
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
59
29
|
import { evalCondition } from '../predicate.js';
|
|
60
|
-
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
30
|
+
import { listAllMemoryDocs, listProjectMemoryDocs } from '../memory-resolver.js';
|
|
31
|
+
import { getNode } from '../canvas/index.js';
|
|
61
32
|
import { userScopeRoot } from '../scope.js';
|
|
62
|
-
import { assembleNodeSubject,
|
|
33
|
+
import { assembleNodeSubject, displayName, gatePasses, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, } from './index.js';
|
|
63
34
|
import { cachedSubstrateDocs } from './session-cache.js';
|
|
64
|
-
// Ancestor dirs we never look inside for a `.crouter/memory/` store (the read
|
|
65
|
-
// file may live under a build/dependency tree; `.crouter` is NOT junk here — it
|
|
66
|
-
// is the segment we explicitly join onto each surviving ancestor).
|
|
67
35
|
const JUNK_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', '.yalc']);
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
// Small path helpers (mirror the on-read precedent established by
|
|
70
|
-
// frontmatter-rules so the injected envelope keeps the faithful substrate
|
|
71
|
-
// shape).
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
36
|
function realpathOrSelf(p) {
|
|
74
37
|
try {
|
|
75
38
|
return realpathSync(p);
|
|
@@ -78,7 +41,6 @@ function realpathOrSelf(p) {
|
|
|
78
41
|
return p;
|
|
79
42
|
}
|
|
80
43
|
}
|
|
81
|
-
/** Escape a value for an XML-ish attribute in the injected envelope. */
|
|
82
44
|
function attr(s) {
|
|
83
45
|
return s
|
|
84
46
|
.replace(/&/g, '&')
|
|
@@ -86,12 +48,11 @@ function attr(s) {
|
|
|
86
48
|
.replace(/</g, '<')
|
|
87
49
|
.replace(/>/g, '>');
|
|
88
50
|
}
|
|
89
|
-
/** Nearest enclosing git repo root for a path (walk up looking for `.git`). */
|
|
90
51
|
function gitRootOf(p) {
|
|
91
52
|
let d = p;
|
|
92
53
|
const root = parse(d).root;
|
|
93
54
|
while (true) {
|
|
94
|
-
if (pathExists(
|
|
55
|
+
if (pathExists(`${d}${sep}.git`))
|
|
95
56
|
return d;
|
|
96
57
|
if (d === root)
|
|
97
58
|
return null;
|
|
@@ -101,7 +62,6 @@ function gitRootOf(p) {
|
|
|
101
62
|
d = parent;
|
|
102
63
|
}
|
|
103
64
|
}
|
|
104
|
-
/** Display a path relative to its nearest git repo root, else absolute. */
|
|
105
65
|
function disp(p) {
|
|
106
66
|
const root = gitRootOf(p);
|
|
107
67
|
if (!root)
|
|
@@ -110,14 +70,18 @@ function disp(p) {
|
|
|
110
70
|
return rel === '' ? '.' : rel;
|
|
111
71
|
}
|
|
112
72
|
function isJunkAncestor(dir) {
|
|
113
|
-
return dir.split(sep).some((
|
|
73
|
+
return dir.split(sep).some((segment) => JUNK_DIRS.has(segment));
|
|
114
74
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
75
|
+
function owningRootOf(doc) {
|
|
76
|
+
const parts = doc.path.split(sep);
|
|
77
|
+
const idx = parts.lastIndexOf(CRTR_DIR_NAME);
|
|
78
|
+
if (idx <= 0)
|
|
79
|
+
return null;
|
|
80
|
+
return parts.slice(0, idx).join(sep) || sep;
|
|
81
|
+
}
|
|
82
|
+
function loadProjectDoc(file, memoryDir) {
|
|
119
83
|
try {
|
|
120
|
-
const raw = relative(
|
|
84
|
+
const raw = relative(memoryDir, file)
|
|
121
85
|
.replace(/\.md$/i, '')
|
|
122
86
|
.split(sep)
|
|
123
87
|
.join('/');
|
|
@@ -128,67 +92,47 @@ function loadPositionalDoc(file, memDir, scope) {
|
|
|
128
92
|
const schema = parseSubstrateFrontmatter(data);
|
|
129
93
|
if (schema === null)
|
|
130
94
|
return null;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
95
|
+
return {
|
|
96
|
+
...schema,
|
|
97
|
+
name: resolveDocName(data, fallbackName),
|
|
98
|
+
scope: 'project',
|
|
99
|
+
path: file,
|
|
100
|
+
body,
|
|
101
|
+
};
|
|
137
102
|
}
|
|
138
103
|
catch {
|
|
139
104
|
return null;
|
|
140
105
|
}
|
|
141
106
|
}
|
|
142
|
-
function
|
|
107
|
+
function safeWalkMarkdown(dir) {
|
|
143
108
|
try {
|
|
144
|
-
return walkFiles(dir, (
|
|
109
|
+
return walkFiles(dir, (name) => name.toLowerCase().endsWith('.md'));
|
|
145
110
|
}
|
|
146
111
|
catch {
|
|
147
112
|
return [];
|
|
148
113
|
}
|
|
149
114
|
}
|
|
150
|
-
/**
|
|
151
|
-
*
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
/** POSITIONAL trigger: every substrate doc in an ancestor PROJECT dir's
|
|
157
|
-
* `.crouter/memory/`, walking from the read file up to $HOME (or the
|
|
158
|
-
* filesystem root for a read outside $HOME), skipping junk ancestors. The
|
|
159
|
-
* user-global `~/.crouter/memory/` store is deliberately skipped: otherwise
|
|
160
|
-
* every file read under $HOME would surface unrelated user-wide preferences.
|
|
161
|
-
*
|
|
162
|
-
* The crouter home (`~/.crouter`, the user scope root) is skipped for the same
|
|
163
|
-
* reason it is never a project scope root (scope.ts's `isProjectScopeDir`): it
|
|
164
|
-
* is the user scope, not a project ancestor. Without this fence, reading any
|
|
165
|
-
* canvas runtime artifact (`~/.crouter/canvas/nodes/.../reports|context/...`)
|
|
166
|
-
* walks up THROUGH `~/.crouter` and would inject whatever lives at
|
|
167
|
-
* `~/.crouter/.crouter/memory/` — e.g. a differently-scoped node whose cwd
|
|
168
|
-
* resolved its project store there — leaking one node's private memory into
|
|
169
|
-
* every unrelated node that reads a canvas file. */
|
|
170
|
-
function positionalCandidates(absReadFile) {
|
|
171
|
-
const out = [];
|
|
172
|
-
const seenDocPaths = new Set();
|
|
115
|
+
/** Project docs from every workspace store enclosing the read file. This is a
|
|
116
|
+
* discovery walk only: each returned doc must still match an explicit trigger. */
|
|
117
|
+
function enclosingProjectDocs(absReadFile) {
|
|
118
|
+
const docs = [];
|
|
119
|
+
const seen = new Set();
|
|
173
120
|
const home = realpathOrSelf(homedir());
|
|
174
121
|
const userRoot = realpathOrSelf(userScopeRoot());
|
|
175
122
|
const fsRoot = parse(absReadFile).root;
|
|
176
123
|
let dir = dirname(absReadFile);
|
|
177
|
-
let depth = 0;
|
|
178
124
|
while (true) {
|
|
179
125
|
if (dir !== home && dir !== userRoot && !isJunkAncestor(dir)) {
|
|
180
|
-
const
|
|
181
|
-
if (pathExists(
|
|
182
|
-
for (const file of
|
|
126
|
+
const memoryDir = `${dir}${sep}${CRTR_DIR_NAME}${sep}memory`;
|
|
127
|
+
if (pathExists(memoryDir)) {
|
|
128
|
+
for (const file of safeWalkMarkdown(memoryDir)) {
|
|
183
129
|
const real = realpathOrSelf(file);
|
|
184
|
-
if (
|
|
130
|
+
if (seen.has(real))
|
|
185
131
|
continue;
|
|
186
|
-
|
|
187
|
-
const doc =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (doc && !hasExplicitTrigger(doc))
|
|
191
|
-
out.push({ doc, realpath: real, order: depth, explicit: false });
|
|
132
|
+
seen.add(real);
|
|
133
|
+
const doc = loadProjectDoc(file, memoryDir);
|
|
134
|
+
if (doc !== null)
|
|
135
|
+
docs.push(doc);
|
|
192
136
|
}
|
|
193
137
|
}
|
|
194
138
|
}
|
|
@@ -198,65 +142,42 @@ function positionalCandidates(absReadFile) {
|
|
|
198
142
|
if (parent === dir)
|
|
199
143
|
break;
|
|
200
144
|
dir = parent;
|
|
201
|
-
depth += 1;
|
|
202
145
|
}
|
|
203
|
-
return
|
|
146
|
+
return docs;
|
|
204
147
|
}
|
|
205
|
-
|
|
206
|
-
* → `<root>`. Builtin docs (no `.crouter` segment) return null. Used to test an
|
|
207
|
-
* `appliesTo` glob against a read path RELATIVE to the doc's own project root. */
|
|
208
|
-
function owningRootOf(doc) {
|
|
209
|
-
const parts = doc.path.split(sep);
|
|
210
|
-
const idx = parts.lastIndexOf(CRTR_DIR_NAME);
|
|
211
|
-
if (idx <= 0)
|
|
212
|
-
return null;
|
|
213
|
-
return parts.slice(0, idx).join(sep) || sep;
|
|
214
|
-
}
|
|
215
|
-
function globMatches(glob, absReadFile, owningRoot) {
|
|
216
|
-
const targets = [absReadFile, basename(absReadFile)];
|
|
217
|
-
if (owningRoot)
|
|
218
|
-
targets.push(relative(owningRoot, absReadFile));
|
|
219
|
-
return targets.some((t) => {
|
|
220
|
-
try {
|
|
221
|
-
return matchesGlob(t, glob);
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
return false; // an invalid glob never matches
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
/** applies-to GLOB trigger: every RESOLVED substrate doc whose `appliesTo` glob
|
|
229
|
-
* matches the read path. `taken` carries the realpaths already claimed by the
|
|
230
|
-
* positional pass, so a doc found both ways is not double-counted.
|
|
231
|
-
* Uses the per-session cache so the corpus is not re-walked+re-parsed on every
|
|
232
|
-
* read tool call (O(reads × corpus) without the cache). */
|
|
233
|
-
function appliesToCandidates(absReadFile, taken) {
|
|
234
|
-
let docs;
|
|
148
|
+
function resolvedDocs() {
|
|
235
149
|
try {
|
|
236
|
-
|
|
150
|
+
return cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
|
|
237
151
|
}
|
|
238
152
|
catch {
|
|
239
153
|
return [];
|
|
240
154
|
}
|
|
155
|
+
}
|
|
156
|
+
function dedupeByPhysicalPath(docs) {
|
|
157
|
+
const seen = new Set();
|
|
241
158
|
const out = [];
|
|
242
159
|
for (const doc of docs) {
|
|
243
|
-
const globs = doc.appliesTo;
|
|
244
|
-
if (!globs || globs.length === 0)
|
|
245
|
-
continue;
|
|
246
160
|
const real = realpathOrSelf(doc.path);
|
|
247
|
-
if (
|
|
161
|
+
if (seen.has(real))
|
|
248
162
|
continue;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
taken.add(real);
|
|
252
|
-
out.push({ doc, realpath: real, order: -1, explicit: true });
|
|
163
|
+
seen.add(real);
|
|
164
|
+
out.push(doc);
|
|
253
165
|
}
|
|
254
166
|
return out;
|
|
255
167
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
168
|
+
function globMatches(glob, absReadFile, owningRoot) {
|
|
169
|
+
const targets = [absReadFile, basename(absReadFile)];
|
|
170
|
+
if (owningRoot !== null)
|
|
171
|
+
targets.push(relative(owningRoot, absReadFile));
|
|
172
|
+
return targets.some((target) => {
|
|
173
|
+
try {
|
|
174
|
+
return matchesGlob(target, glob);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
260
181
|
function readFileFrontmatter(absReadFile) {
|
|
261
182
|
if (!/\.(md|mdx|markdown)$/i.test(absReadFile))
|
|
262
183
|
return {};
|
|
@@ -267,41 +188,20 @@ function readFileFrontmatter(absReadFile) {
|
|
|
267
188
|
return {};
|
|
268
189
|
}
|
|
269
190
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return [];
|
|
284
|
-
}
|
|
285
|
-
const out = [];
|
|
286
|
-
for (const doc of docs) {
|
|
287
|
-
if (doc.readWhen === undefined)
|
|
288
|
-
continue;
|
|
289
|
-
const real = realpathOrSelf(doc.path);
|
|
290
|
-
if (taken.has(real))
|
|
291
|
-
continue;
|
|
292
|
-
if (!evalCondition(doc.readWhen, readFileFm))
|
|
293
|
-
continue;
|
|
294
|
-
taken.add(real);
|
|
295
|
-
out.push({ doc, realpath: real, order: -1, explicit: true });
|
|
296
|
-
}
|
|
297
|
-
return out;
|
|
191
|
+
function matchesReadEvent(doc, absReadFile, readFrontmatter) {
|
|
192
|
+
const pathMatch = doc.appliesTo?.some((glob) => globMatches(glob, absReadFile, owningRootOf(doc))) === true;
|
|
193
|
+
const frontmatterMatch = doc.readWhen !== undefined &&
|
|
194
|
+
Object.keys(readFrontmatter).length > 0 &&
|
|
195
|
+
evalCondition(doc.readWhen, readFrontmatter);
|
|
196
|
+
return pathMatch || frontmatterMatch;
|
|
197
|
+
}
|
|
198
|
+
function envelopeName(doc) {
|
|
199
|
+
const displayed = displayName(doc.name);
|
|
200
|
+
if (displayed !== '')
|
|
201
|
+
return displayed;
|
|
202
|
+
const root = owningRootOf(doc);
|
|
203
|
+
return root === null ? doc.name : basename(root);
|
|
298
204
|
}
|
|
299
|
-
// ---------------------------------------------------------------------------
|
|
300
|
-
// Per-doc envelope render.
|
|
301
|
-
// ---------------------------------------------------------------------------
|
|
302
|
-
/** One memory as a `<memory …>` element at its fileReadVisibility rung, or null
|
|
303
|
-
* when that rung is `none`. `content` → full body; `preview` → the routing
|
|
304
|
-
* line; `name` → a bare self-closed tag (the `name=` attribute IS the surface). */
|
|
305
205
|
function renderDocEnvelope(doc) {
|
|
306
206
|
const rung = doc.fileReadVisibility;
|
|
307
207
|
if (rung === 'none')
|
|
@@ -311,26 +211,33 @@ function renderDocEnvelope(doc) {
|
|
|
311
211
|
body = doc.body.trim();
|
|
312
212
|
else if (rung === 'preview')
|
|
313
213
|
body = previewLine(doc);
|
|
314
|
-
|
|
315
|
-
const attrs = `kind="${attr(doc.kind)}" name="${attr(doc.name)}" src="${attr(disp(doc.path))}"`;
|
|
214
|
+
const attrs = `kind="${attr(doc.kind)}" name="${attr(envelopeName(doc))}" src="${attr(disp(doc.path))}"`;
|
|
316
215
|
return body === '' ? `<memory ${attrs} />` : `<memory ${attrs}>\n${body}\n</memory>`;
|
|
317
216
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
217
|
+
function renderCandidates(subject, docs, seen) {
|
|
218
|
+
const rendered = [];
|
|
219
|
+
for (const doc of docs) {
|
|
220
|
+
const real = realpathOrSelf(doc.path);
|
|
221
|
+
if (seen.has(real))
|
|
222
|
+
continue;
|
|
223
|
+
try {
|
|
224
|
+
if (!gatePasses(doc, subject))
|
|
225
|
+
continue;
|
|
226
|
+
const block = renderDocEnvelope(doc);
|
|
227
|
+
if (block === null)
|
|
228
|
+
continue;
|
|
229
|
+
seen.add(real);
|
|
230
|
+
rendered.push(block);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return rendered.length === 0
|
|
237
|
+
? ''
|
|
238
|
+
: `<auto-loaded-context>\n${rendered.join('\n')}\n</auto-loaded-context>`;
|
|
239
|
+
}
|
|
240
|
+
/** Surface docs matched by a successful read tool call. */
|
|
334
241
|
export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
|
|
335
242
|
let subject;
|
|
336
243
|
try {
|
|
@@ -342,63 +249,36 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
|
|
|
342
249
|
if (subject === null)
|
|
343
250
|
return '';
|
|
344
251
|
const absReadFile = realpathOrSelf(readFilePath);
|
|
345
|
-
|
|
252
|
+
const readFrontmatter = readFileFrontmatter(absReadFile);
|
|
253
|
+
const docs = dedupeByPhysicalPath([...enclosingProjectDocs(absReadFile), ...resolvedDocs()])
|
|
254
|
+
.filter((doc) => realpathOrSelf(doc.path) !== absReadFile)
|
|
255
|
+
.filter((doc) => matchesReadEvent(doc, absReadFile, readFrontmatter));
|
|
256
|
+
return renderCandidates(subject, docs, seen);
|
|
257
|
+
}
|
|
258
|
+
/** Surface workspace-wide docs during first-message assembly. `.` is a reserved
|
|
259
|
+
* applies-to target meaning “when this project store is mounted by cwd/profile”. */
|
|
260
|
+
export function renderWorkspaceOpenDocs(nodeId) {
|
|
261
|
+
let subject;
|
|
262
|
+
let docs;
|
|
346
263
|
try {
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
264
|
+
subject = assembleNodeSubject(nodeId);
|
|
265
|
+
const node = getNode(nodeId);
|
|
266
|
+
if (subject === null || node === null)
|
|
267
|
+
return '';
|
|
268
|
+
docs = listProjectMemoryDocs(node.cwd, node.profile_id ?? null)
|
|
269
|
+
.map(parseSubstrateDoc)
|
|
270
|
+
.filter((doc) => doc !== null);
|
|
352
271
|
}
|
|
353
272
|
catch {
|
|
354
273
|
return '';
|
|
355
274
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const resolved = cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
|
|
366
|
-
ceil = buildCeilingIndex([...resolved, ...candidates.map((c) => c.doc)]);
|
|
367
|
-
}
|
|
368
|
-
catch {
|
|
369
|
-
ceil = buildCeilingIndex(candidates.map((c) => c.doc));
|
|
370
|
-
}
|
|
371
|
-
const rendered = [];
|
|
372
|
-
for (const c of candidates) {
|
|
373
|
-
// Never re-surface the doc the agent is literally reading.
|
|
374
|
-
if (c.realpath === absReadFile)
|
|
375
|
-
continue;
|
|
376
|
-
// Once-per-session dedup (caller-owned set).
|
|
377
|
-
if (seen.has(c.realpath))
|
|
378
|
-
continue;
|
|
379
|
-
let block;
|
|
380
|
-
try {
|
|
381
|
-
if (!gatePasses(c.doc, subject))
|
|
382
|
-
continue; // gated out for this node
|
|
383
|
-
// D6: an explicit applies-to/read-when trigger pierces the INDEX ceiling
|
|
384
|
-
// and renders at the doc's own rung; positional docs honor the ceiling.
|
|
385
|
-
const rung = c.explicit
|
|
386
|
-
? c.doc.fileReadVisibility
|
|
387
|
-
: effectiveRung(c.doc, ceil, 'fileReadVisibility');
|
|
388
|
-
const doc = rung === c.doc.fileReadVisibility && displayName(c.doc.name) === c.doc.name
|
|
389
|
-
? c.doc
|
|
390
|
-
: { ...c.doc, fileReadVisibility: rung, name: displayName(c.doc.name) };
|
|
391
|
-
block = renderDocEnvelope(doc);
|
|
392
|
-
}
|
|
393
|
-
catch {
|
|
394
|
-
continue; // a single bad doc never breaks the read
|
|
395
|
-
}
|
|
396
|
-
if (block === null)
|
|
397
|
-
continue; // fileReadVisibility 'none' — not a read surface
|
|
398
|
-
seen.add(c.realpath); // mark injected only once it actually surfaces
|
|
399
|
-
rendered.push(block);
|
|
400
|
-
}
|
|
401
|
-
if (rendered.length === 0)
|
|
402
|
-
return '';
|
|
403
|
-
return `<auto-loaded-context>\n${rendered.join('\n')}\n</auto-loaded-context>`;
|
|
275
|
+
docs = dedupeByPhysicalPath(docs)
|
|
276
|
+
.filter((doc) => doc.appliesTo?.some((glob) => glob.trim() === '.') === true)
|
|
277
|
+
.sort((a, b) => {
|
|
278
|
+
const aRoot = owningRootOf(a) ?? '';
|
|
279
|
+
const bRoot = owningRootOf(b) ?? '';
|
|
280
|
+
const depth = aRoot.split(sep).filter(Boolean).length - bRoot.split(sep).filter(Boolean).length;
|
|
281
|
+
return depth || aRoot.localeCompare(bRoot) || a.path.localeCompare(b.path);
|
|
282
|
+
});
|
|
283
|
+
return renderCandidates(subject, docs, new Set());
|
|
404
284
|
}
|
|
@@ -59,7 +59,7 @@ import { projectScopeRoots } from '../scope.js';
|
|
|
59
59
|
// pull subject.js (canvas-db) transitively, re-tainting every CLI consumer of
|
|
60
60
|
// the pure render fns. The canvas-db wrappers (renderPreferencesSection /
|
|
61
61
|
// renderKnowledgeBlock) live in render-node.js instead.
|
|
62
|
-
import { buildCeilingIndex,
|
|
62
|
+
import { buildCeilingIndex, effectiveSystemPromptRung, indexDirOf, isIndexName } from './ceiling.js';
|
|
63
63
|
import { gatePasses } from './gate.js';
|
|
64
64
|
import { normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, rungRank, } from './schema.js';
|
|
65
65
|
import { cachedSubstrateDocs } from './session-cache.js';
|
|
@@ -105,7 +105,7 @@ function selectWinners(subject, kind) {
|
|
|
105
105
|
const ceil = buildCeilingIndex(docs);
|
|
106
106
|
const eligible = docs
|
|
107
107
|
.map((d) => {
|
|
108
|
-
const rung =
|
|
108
|
+
const rung = effectiveSystemPromptRung(d, ceil);
|
|
109
109
|
return rung === d.systemPromptVisibility ? d : { ...d, systemPromptVisibility: rung };
|
|
110
110
|
})
|
|
111
111
|
.filter((d) => d.kind === kind)
|
|
@@ -224,8 +224,8 @@ function nodeLocalDocs(nodeId, subject) {
|
|
|
224
224
|
if (schema === null)
|
|
225
225
|
continue;
|
|
226
226
|
// ONE substrate-identity rule (schema.ts's resolveDocName), shared with
|
|
227
|
-
// the resolver and
|
|
228
|
-
//
|
|
227
|
+
// the resolver and explicit file-context loader: frontmatter `name` wins
|
|
228
|
+
// over the physical-path-derived fallback.
|
|
229
229
|
const name = resolveDocName(data, fallbackName);
|
|
230
230
|
// node-local is NOT a resolver scope; `scope` is a placeholder never read
|
|
231
231
|
// by gate eval (keyed off the NODE subject, not the doc) nor by the
|
|
@@ -54,8 +54,9 @@ export interface SubstrateSchema {
|
|
|
54
54
|
* eligible. An empty `{}` is carried as-is and is inert (never matches) — see
|
|
55
55
|
* `gatePasses`. */
|
|
56
56
|
gate?: GatePredicate;
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
57
|
+
/** Explicit file-context routes. `.` is the project workspace-open target;
|
|
58
|
+
* every other value is a glob matched after an actual file read. A single
|
|
59
|
+
* value is normalized to a 1-list. */
|
|
59
60
|
appliesTo?: string[];
|
|
60
61
|
/** Optional condition over the READ FILE's own frontmatter — the on-read
|
|
61
62
|
* frontmatter trigger (Stream A native rules). Same coercion + predicate
|
|
@@ -73,9 +73,9 @@ export function normalizeDocName(name) {
|
|
|
73
73
|
// Substrate identity — the ONE explicit-name-then-path-fallback rule. A doc's
|
|
74
74
|
// resolver identity is its explicit frontmatter `name` when present (normalized),
|
|
75
75
|
// otherwise the caller-supplied path-derived fallback. Every loader that turns a
|
|
76
|
-
// physical .md file into a named doc (the resolver's `listMemoryDocsInDir
|
|
77
|
-
//
|
|
78
|
-
// depends on which path loaded it.
|
|
76
|
+
// physical .md file into a named doc (the resolver's `listMemoryDocsInDir` and
|
|
77
|
+
// the explicit file-context loader) calls this ONE helper, so a doc's identity
|
|
78
|
+
// never depends on which path loaded it.
|
|
79
79
|
// ---------------------------------------------------------------------------
|
|
80
80
|
/** Resolve a doc's identity from its raw frontmatter record: an explicit
|
|
81
81
|
* `name` field wins (trimmed + normalized), else `fallbackName` (the
|
|
@@ -177,8 +177,8 @@ function parseGate(v) {
|
|
|
177
177
|
? v
|
|
178
178
|
: undefined;
|
|
179
179
|
}
|
|
180
|
-
/** Normalize `applies-to` to a non-empty
|
|
181
|
-
*
|
|
180
|
+
/** Normalize `applies-to` to a non-empty route list, or undefined when no
|
|
181
|
+
* file-context event is declared. Accepts a string or an array of strings. */
|
|
182
182
|
function parseAppliesTo(v) {
|
|
183
183
|
if (typeof v === 'string') {
|
|
184
184
|
return v.trim() === '' ? undefined : [v];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|