@crouton-kit/crouter 0.3.47 → 0.3.48

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.
@@ -13,10 +13,15 @@
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) — the
17
- // AGENTS.md/CLAUDE.md project instructions, moved OUT of the system prompt
18
- // (pi's copy is suppressed via noContextFiles in broker.ts) so they ride
19
- // this user message instead;
16
+ // carries the <project_context> block (buildProjectContextBlock) — now just
17
+ // the cwd/git <environment> snapshot. Project instructions (what used to be
18
+ // a re-rendered AGENTS.md/CLAUDE.md here) come from the document substrate
19
+ // instead: `crtr sys sync` migrates each CLAUDE.md/AGENTS.md into that
20
+ // dir's own `.crouter/memory/AGENTS.md` at content/content, so it rides the
21
+ // <memory kind="knowledge"> block below (renderKnowledgeBlock) via the
22
+ // profile-widened boot render, exactly like any other substrate doc — no
23
+ // parallel re-implementation of pi's project-context loader lives here
24
+ // anymore (see `crtr memory read taste/document-substrate`);
20
25
  // • promote.ts folds orchestratorContextNote() into the promotion guidance
21
26
  // dump, so a node that becomes an orchestrator MID-LIFE gets the
22
27
  // orchestrator framing it never received at spawn — it spawned as a base
@@ -36,78 +41,10 @@
36
41
  // This across-cycles note is the ONE thing a terminal worker's bearings drop.
37
42
  import { contextDir, getNode, fullName, subscriptionsOf, } from '../canvas/index.js';
38
43
  import { execSync } from 'node:child_process';
39
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
40
- import { homedir } from 'node:os';
41
- import { join, resolve } from 'node:path';
44
+ import { readdirSync } from 'node:fs';
42
45
  import { cadenceDisplay } from '../wake.js';
43
46
  import { renderKnowledgeBlock } from '../substrate/index.js';
44
47
  import { loadProfileManifest } from '../profiles/manifest.js';
45
- // ---------------------------------------------------------------------------
46
- // Project context (<project_context>) — the AGENTS.md/CLAUDE.md files pi would
47
- // otherwise inject into its SYSTEM PROMPT, rendered here so they ride the
48
- // first-message bearings instead (pi's copy is suppressed via
49
- // resourceLoaderOptions.noContextFiles in broker.ts). We re-implement pi's
50
- // loadProjectContextFiles + getAgentDir (resource-loader.js / config.js) rather
51
- // than IMPORT pi: bearings.ts is statically imported by the thin daemon (crtrd,
52
- // for wakeOriginFrom), so a static @earendil-works import would pull the whole
53
- // pi engine into the supervisor. The discovery is a stable convention + pure fs.
54
- // ---------------------------------------------------------------------------
55
- /** pi's getAgentDir(): the global agent config dir holding a user-wide context
56
- * file. Honors PI_CODING_AGENT_DIR (tilde-expanded), else ~/.pi/agent (pi's
57
- * piConfig.configDir is ".pi"). */
58
- function piAgentDir() {
59
- const env = process.env['PI_CODING_AGENT_DIR'];
60
- if (env !== undefined && env !== '') {
61
- return env.startsWith('~') ? join(homedir(), env.slice(1)) : env;
62
- }
63
- return join(homedir(), '.pi', 'agent');
64
- }
65
- /** Per-dir context-file candidates, in pi's precedence order (first match wins). */
66
- const CONTEXT_FILE_CANDIDATES = ['AGENTS.md', 'AGENTS.MD', 'CLAUDE.md', 'CLAUDE.MD'];
67
- function loadContextFileFromDir(dir) {
68
- for (const name of CONTEXT_FILE_CANDIDATES) {
69
- const p = join(dir, name);
70
- if (existsSync(p)) {
71
- try {
72
- return { path: p, content: readFileSync(p, 'utf8') };
73
- }
74
- catch {
75
- // Unreadable candidate — skip, mirroring pi (best-effort).
76
- }
77
- }
78
- }
79
- return null;
80
- }
81
- /** Mirror of pi's loadProjectContextFiles: the global agent-dir file first, then
82
- * every AGENTS.md/CLAUDE.md up the cwd's ancestry, ordered root→cwd (so the
83
- * most-specific project file reads last). Deduped by absolute path. */
84
- function loadProjectContextFiles(cwd) {
85
- const files = [];
86
- const seen = new Set();
87
- const global = loadContextFileFromDir(piAgentDir());
88
- if (global !== null) {
89
- files.push(global);
90
- seen.add(global.path);
91
- }
92
- const ancestors = [];
93
- let dir = resolve(cwd);
94
- const root = resolve('/');
95
- for (;;) {
96
- const f = loadContextFileFromDir(dir);
97
- if (f !== null && !seen.has(f.path)) {
98
- ancestors.unshift(f);
99
- seen.add(f.path);
100
- }
101
- if (dir === root)
102
- break;
103
- const parent = resolve(dir, '..');
104
- if (parent === dir)
105
- break;
106
- dir = parent;
107
- }
108
- files.push(...ancestors);
109
- return files;
110
- }
111
48
  function escapeAttribute(value) {
112
49
  return value.replaceAll('&', '&amp;').replaceAll('"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
113
50
  }
@@ -175,18 +112,19 @@ function parseWorktrees(lines) {
175
112
  }
176
113
  return worktrees;
177
114
  }
178
- /** The `<project_context>` block for the files discovered from `cwd`, plus the
179
- * cwd/git environment snapshot that now rides at the end of the wrapper.
180
- * Always emits the wrapper because the environment snapshot is always present
181
- * even when no project instructions exist. Exported for testing. */
115
+ /** The `<project_context>` block now just the cwd/git `<environment>`
116
+ * snapshot (dir listing + git status/worktrees). This USED to also carry a
117
+ * `<project_instructions>` block re-rendering the node's AGENTS.md/CLAUDE.md;
118
+ * that machinery is gone (hard cut, 2026-07-06) project guidance now lives
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. */
182
124
  export function buildProjectContextBlock(cwd) {
183
- const files = loadProjectContextFiles(cwd);
184
125
  const envLines = directoryListingLines(cwd);
185
126
  const git = gitSnapshot(cwd);
186
- let out = '<project_context>\n\nProject-specific instructions and guidelines:\n\n';
187
- for (const { path, content } of files) {
188
- out += `<project_instructions path="${path}">\n${content}\n</project_instructions>\n\n`;
189
- }
127
+ let out = '<project_context>\n\n';
190
128
  out += `<environment cwd="${escapeAttribute(cwd)}">\n`;
191
129
  out += `Directory:\n${envLines.map((line) => ` ${line}`).join('\n')}\n`;
192
130
  out += `${git.lines.map((line) => `${line}`).join('\n')}\n`;
@@ -432,9 +370,12 @@ export function buildWakeBearings(origin) {
432
370
  * from.
433
371
  * 2. `<memory kind="knowledge">` — the consultable catalog, a SIBLING of the
434
372
  * bearings (never nested under the context dir). Dropped when empty.
435
- * 3. `<project_context>` — the AGENTS.md/CLAUDE.md instructions + the cwd/git
436
- * environment snapshot, moved OUT of the system prompt (pi's copy is
437
- * suppressed via noContextFiles in broker.ts) so it rides this message. */
373
+ * 3. `<project_context>` — the cwd/git environment snapshot. pi's own
374
+ * AGENTS.md/CLAUDE.md system-prompt injection stays suppressed
375
+ * (noContextFiles in broker.ts); project guidance instead rides the
376
+ * `<memory kind="knowledge">` block above via the document substrate
377
+ * (`crtr sys sync` migrates each CLAUDE.md/AGENTS.md into that dir's own
378
+ * `.crouter/memory/AGENTS.md`). */
438
379
  export function buildContextBearings(nodeId) {
439
380
  const node = getNode(nodeId);
440
381
  const bearings = ['<crtr-bearings>', buildIdentityAssertion(nodeId)];
@@ -1706,10 +1706,11 @@ export async function buildBrokerSession(engine, cfg) {
1706
1706
  resourceLoaderOptions: {
1707
1707
  additionalExtensionPaths: cfg.extensionPaths,
1708
1708
  appendSystemPrompt: cfg.appendSystemPromptPath !== undefined ? [cfg.appendSystemPromptPath] : undefined,
1709
- // Suppress pi's <project_context> in the SYSTEM PROMPT — crouter
1710
- // re-renders the same AGENTS.md/CLAUDE.md files into the first-message
1711
- // <crtr-context> bearings instead (buildProjectContextBlock), so all
1712
- // project instructions ride the user message, not the system prompt.
1709
+ // Suppress pi's <project_context> AGENTS.md/CLAUDE.md injection in the
1710
+ // SYSTEM PROMPT unconditionally crouter reads project guidance through
1711
+ // the document substrate instead (a migrated AGENTS.md doc rides the
1712
+ // <memory kind="knowledge"> block in the first-message bearings; see
1713
+ // bearings.ts and `crtr sys sync`), never pi's own file-based loader.
1713
1714
  noContextFiles: true,
1714
1715
  },
1715
1716
  });
@@ -9,10 +9,12 @@
9
9
  // • POSITIONAL — walk the read file's ancestor dirs; any doc living in an
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
- // explicit read-trigger (see D5 below). This mirrors nested-context's
13
- // `.claude/rules` ancestor walk, but keyed on `.crouter/memory/`. The
14
- // user-global `~/.crouter/memory/` store is NOT positional; user docs only
15
- // fire on reads through explicit `applies-to` / `read-when` triggers.
12
+ // explicit read-trigger (see D5 below). This is the substrate's own
13
+ // ancestor walk keyed on `.crouter/memory/` — the same shape the retired
14
+ // nested-context pi-extension used for `.claude/rules` before that path
15
+ // was migrated into substrate docs (`crtr sys sync`). The user-global
16
+ // `~/.crouter/memory/` store is NOT positional; user docs only fire on
17
+ // reads through explicit `applies-to` / `read-when` triggers.
16
18
  // • applies-to GLOB — any RESOLVED substrate doc (user/project/builtin scope)
17
19
  // whose `appliesTo` glob matches the read file path surfaces, regardless of
18
20
  // where the read file sits relative to the doc.
@@ -63,8 +65,9 @@ import { cachedSubstrateDocs } from './session-cache.js';
63
65
  // is the segment we explicitly join onto each surviving ancestor).
64
66
  const JUNK_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', '.yalc']);
65
67
  // ---------------------------------------------------------------------------
66
- // Small path helpers (mirror the on-read precedent — nested-context /
67
- // frontmatter-rules — so the injected envelope matches their faithful shape).
68
+ // Small path helpers (mirror the on-read precedent — frontmatter-rules, and
69
+ // the retired nested-context extension — so the injected envelope matches
70
+ // their faithful shape).
68
71
  // ---------------------------------------------------------------------------
69
72
  function realpathOrSelf(p) {
70
73
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/crouter",
3
- "version": "0.3.47",
3
+ "version": "0.3.48",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "license": "MIT",
49
49
  "dependencies": {
50
- "@crouton-kit/humanloop": "^0.3.22",
50
+ "@crouton-kit/humanloop": "^0.3.25",
51
51
  "@earendil-works/pi-agent-core": "0.80.2",
52
52
  "@earendil-works/pi-ai": "0.80.3",
53
53
  "@earendil-works/pi-coding-agent": "0.80.2",
@@ -1,327 +0,0 @@
1
- /**
2
- * Nested Context Loader
3
- *
4
- * Pi only loads AGENTS.md / CLAUDE.md from the cwd's ancestor chain at startup.
5
- * Anything *below* the launch dir (subproject CLAUDE.md files, .claude/rules/)
6
- * is never pulled in. This extension fills that gap:
7
- *
8
- * When the `read` tool reads a file, we walk from that file's directory up to
9
- * the session cwd and surface, appended to the read result:
10
- * 1. Each directory's CLAUDE.md / AGENTS.md (first match per dir).
11
- * 2. Each directory's .claude/rules/*.md — unconditionally if the rule has no
12
- * `paths:` frontmatter, or only when the read file matches one of its
13
- * `paths:` globs.
14
- *
15
- * Guarantees:
16
- * - No duplicates. A `seen` set tracks every absolute path already loaded,
17
- * including the files pi loaded at startup (seeded in session_start) and any
18
- * context/rule file the agent reads directly.
19
- * - Bounded. Anchored to the file's own parent chain (not the session cwd),
20
- * stops at $HOME, and skips generated/dependency dirs (node_modules, ...).
21
- *
22
- * Install: drop in ~/.pi/agent/extensions/ (global) or .pi/extensions/ (project).
23
- */
24
-
25
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
26
- import * as fs from "node:fs";
27
- import * as os from "node:os";
28
- import * as path from "node:path";
29
-
30
- // First-match-per-dir order mirrors pi's own loader (AGENTS.md wins over CLAUDE.md).
31
- const CONTEXT_FILENAMES = ["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"];
32
-
33
- // Directory names we never descend into / load context from.
34
- const JUNK_DIRS = new Set([
35
- "node_modules",
36
- ".git",
37
- ".venv",
38
- "venv",
39
- "dist",
40
- "build",
41
- ".next",
42
- ".cache",
43
- ".yalc",
44
- ".sisyphus",
45
- ".crouter",
46
- ]);
47
-
48
- interface RuleFile {
49
- name: string;
50
- paths: string[];
51
- body: string;
52
- }
53
-
54
- function realpathOrSelf(p: string): string {
55
- try {
56
- return fs.realpathSync(p);
57
- } catch {
58
- return p;
59
- }
60
- }
61
-
62
- function isJunkPath(absDir: string): boolean {
63
- return absDir.split(path.sep).some((seg) => JUNK_DIRS.has(seg));
64
- }
65
-
66
- // Nearest enclosing git repo root for a path (walk up looking for `.git`).
67
- function gitRoot(p: string): string | null {
68
- let d = p;
69
- const root = path.parse(d).root;
70
- while (true) {
71
- if (fs.existsSync(path.join(d, ".git"))) return d;
72
- if (d === root) return null;
73
- const parent = path.dirname(d);
74
- if (parent === d) return null;
75
- d = parent;
76
- }
77
- }
78
-
79
- // Display paths relative to the nearest git repo root, else absolute.
80
- function disp(p: string): string {
81
- const root = gitRoot(p);
82
- if (!root) return p;
83
- const rel = path.relative(root, p);
84
- return rel === "" ? "." : rel;
85
- }
86
-
87
- // Escape a value for use inside an XML-ish attribute in the injected block.
88
- function attr(s: string): string {
89
- return s
90
- .replace(/&/g, "&amp;")
91
- .replace(/"/g, "&quot;")
92
- .replace(/</g, "&lt;")
93
- .replace(/>/g, "&gt;");
94
- }
95
-
96
- const AUTO_CONTEXT_RE = /<auto-loaded-context(?:\s[^>]*)?>\n([\s\S]*?)\n<\/auto-loaded-context>/;
97
-
98
- function mergeAutoLoadedContext(items: string[], content: Array<{ type: string; text?: string }>) {
99
- const existingText = content.map((block) => block.text ?? "").join("\n");
100
- const fresh = items.map((item) => item.trim()).filter((item) => item !== "" && !existingText.includes(item));
101
- if (fresh.length === 0) return content;
102
- const inner = fresh.join("\n");
103
-
104
- const existingIdx = content.findIndex((block) => typeof block.text === "string" && AUTO_CONTEXT_RE.test(block.text));
105
- if (existingIdx !== -1) {
106
- return content.map((block, idx) => {
107
- if (idx !== existingIdx || typeof block.text !== "string") return block;
108
- return {
109
- ...block,
110
- text: block.text.replace(AUTO_CONTEXT_RE, (_whole, current: string) => {
111
- const trimmed = String(current).trim();
112
- return `<auto-loaded-context>\n${inner}${trimmed === "" ? "" : `\n${trimmed}`}\n</auto-loaded-context>`;
113
- }),
114
- };
115
- });
116
- }
117
-
118
- return [{ type: "text" as const, text: `<auto-loaded-context>\n${inner}\n</auto-loaded-context>` }, ...content];
119
- }
120
-
121
- function firstContextFile(dir: string): string | null {
122
- for (const name of CONTEXT_FILENAMES) {
123
- const fp = path.join(dir, name);
124
- if (fs.existsSync(fp) && fs.statSync(fp).isFile()) return fp;
125
- }
126
- return null;
127
- }
128
-
129
- function findMarkdown(dir: string): string[] {
130
- const out: string[] = [];
131
- let entries: fs.Dirent[];
132
- try {
133
- entries = fs.readdirSync(dir, { withFileTypes: true });
134
- } catch {
135
- return out;
136
- }
137
- for (const e of entries) {
138
- const fp = path.join(dir, e.name);
139
- if (e.isDirectory()) out.push(...findMarkdown(fp));
140
- else if (e.isFile() && e.name.endsWith(".md")) out.push(fp);
141
- }
142
- return out;
143
- }
144
-
145
- // Minimal frontmatter parse: pulls `name` and the `paths:` list. Avoids a YAML dep.
146
- function parseRule(filePath: string): RuleFile {
147
- let raw = "";
148
- try {
149
- raw = fs.readFileSync(filePath, "utf-8");
150
- } catch {
151
- return { name: path.basename(filePath, ".md"), paths: [], body: "" };
152
- }
153
- const fm = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
154
- if (!fm) return { name: path.basename(filePath, ".md"), paths: [], body: raw };
155
-
156
- const [, front, body] = fm;
157
- const lines = front.split("\n");
158
- let name = path.basename(filePath, ".md");
159
- const paths: string[] = [];
160
- let inPaths = false;
161
-
162
- const clean = (s: string) => s.trim().replace(/^["']|["']$/g, "");
163
- for (const line of lines) {
164
- const nameMatch = line.match(/^name:\s*(.+)$/);
165
- if (nameMatch) {
166
- name = clean(nameMatch[1]);
167
- inPaths = false;
168
- continue;
169
- }
170
- const pathsInline = line.match(/^paths:\s*(.*)$/);
171
- if (pathsInline) {
172
- const rest = pathsInline[1].trim();
173
- if (rest === "" || rest === "[]") {
174
- inPaths = true; // YAML list form follows on subsequent lines
175
- } else {
176
- // Inline form: a single glob or a comma/space-separated list,
177
- // optionally wrapped in [ ... ].
178
- inPaths = false;
179
- for (const part of rest.replace(/^\[|\]$/g, "").split(/[,]/)) {
180
- const g = clean(part);
181
- if (g) paths.push(g);
182
- }
183
- }
184
- continue;
185
- }
186
- if (inPaths) {
187
- const item = line.match(/^\s*-\s*(.+)$/);
188
- if (item) {
189
- paths.push(clean(item[1]));
190
- continue;
191
- }
192
- if (/^\S/.test(line)) inPaths = false; // dedented to next key
193
- }
194
- }
195
- return { name, paths, body: body.trim() };
196
- }
197
-
198
- function ruleMatches(absFile: string, baseDir: string, globs: string[]): boolean {
199
- if (globs.length === 0) return true; // unconditional rule
200
- const rel = path.relative(baseDir, absFile);
201
- return globs.some((g) => {
202
- try {
203
- return path.matchesGlob(rel, g) || path.matchesGlob(absFile, g);
204
- } catch {
205
- return false;
206
- }
207
- });
208
- }
209
-
210
- export default function nestedContext(pi: ExtensionAPI) {
211
- const seen = new Set<string>();
212
-
213
- // Seed `seen` with everything pi already loaded at startup: the global
214
- // agent file plus every CLAUDE.md/AGENTS.md on the cwd ancestor chain.
215
- pi.on("session_start", async (_event, ctx) => {
216
- const globalDir = path.join(os.homedir(), ".pi", "agent");
217
- const globalCtx = firstContextFile(globalDir);
218
- if (globalCtx) seen.add(realpathOrSelf(globalCtx));
219
-
220
- let dir = path.resolve(ctx.cwd);
221
- const root = path.parse(dir).root;
222
- while (true) {
223
- const cf = firstContextFile(dir);
224
- if (cf) seen.add(realpathOrSelf(cf));
225
- if (dir === root) break;
226
- const parent = path.dirname(dir);
227
- if (parent === dir) break;
228
- dir = parent;
229
- }
230
- });
231
-
232
- pi.on("tool_result", async (event, ctx) => {
233
- if (event.toolName !== "read") return;
234
- if (event.isError) return;
235
-
236
- const rawPath = (event.input as { path?: string; file_path?: string })?.path
237
- ?? (event.input as { file_path?: string })?.file_path;
238
- if (!rawPath) return;
239
-
240
- const cwd = path.resolve(ctx.cwd);
241
- // The model often passes a leading ~ ; pi's read tool expands it but
242
- // event.input keeps the raw form, so expand it ourselves before resolving.
243
- const expanded = rawPath.startsWith("~")
244
- ? path.join(os.homedir(), rawPath.slice(1))
245
- : rawPath;
246
- const absFile = realpathOrSelf(path.resolve(cwd, expanded));
247
-
248
- // Anchor to the FILE's own location, not the session cwd: walk its parent
249
- // chain up to the home directory (or filesystem root if outside home).
250
- const home = os.homedir();
251
- const fsRoot = path.parse(absFile).root;
252
-
253
- // If the agent read a context/rule file directly, mark it loaded so we
254
- // never re-inject it later, and don't append it to its own read.
255
- seen.add(absFile);
256
-
257
- type Block = { order: number; text: string };
258
- const blocks: Block[] = [];
259
-
260
- let dir = path.dirname(absFile);
261
- let depth = 0;
262
- while (true) {
263
- if (!isJunkPath(dir)) {
264
- // 1) Directory context file.
265
- const cf = firstContextFile(dir);
266
- if (cf) {
267
- const real = realpathOrSelf(cf);
268
- if (!seen.has(real)) {
269
- seen.add(real);
270
- try {
271
- const content = fs.readFileSync(cf, "utf-8").trim();
272
- blocks.push({
273
- order: depth,
274
- text:
275
- `<project-guidance src="${attr(disp(cf))}" scope="${attr(`${disp(dir)}/`)}">\n` +
276
- `${content}\n` +
277
- `</project-guidance>`,
278
- });
279
- } catch {
280
- /* unreadable; skip */
281
- }
282
- }
283
- }
284
-
285
- // 2) .claude/rules for this directory.
286
- const rulesDir = path.join(dir, ".claude", "rules");
287
- for (const rf of findMarkdown(rulesDir)) {
288
- // Skip stray context files living inside a rules dir; they are not rules.
289
- if (CONTEXT_FILENAMES.includes(path.basename(rf))) continue;
290
- const real = realpathOrSelf(rf);
291
- if (seen.has(real)) continue;
292
- const rule = parseRule(rf);
293
- if (!ruleMatches(absFile, dir, rule.paths)) continue; // may match on a later read
294
- seen.add(real);
295
- blocks.push({
296
- order: depth,
297
- text:
298
- `<rule name="${attr(rule.name)}" src="${attr(disp(rf))}">\n` +
299
- `${rule.body}\n` +
300
- `</rule>`,
301
- });
302
- }
303
- }
304
-
305
- if (dir === home || dir === fsRoot) break;
306
- const parent = path.dirname(dir);
307
- if (parent === dir) break;
308
- dir = parent;
309
- depth += 1;
310
- }
311
-
312
- if (blocks.length === 0) return;
313
-
314
- // Outermost-first, so the most specific (nearest) guidance reads last/closest
315
- // to the file content that follows it.
316
- blocks.sort((a, b) => b.order - a.order);
317
- const merged = mergeAutoLoadedContext(blocks.map((b) => b.text), event.content);
318
- if (merged === event.content) return;
319
-
320
- if (ctx.hasUI) {
321
- ctx.ui.notify(`Loaded ${blocks.length} nested context file(s)`, "info");
322
- }
323
-
324
- // Prepend or merge, so the governing guidance appears before the file output.
325
- return { content: merged };
326
- });
327
- }