@nexrall/code-core 1.4.71 → 1.4.73

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 (126) hide show
  1. package/dist/agent/agentRegistry.d.ts.map +1 -1
  2. package/dist/agent/agentRegistry.js +28 -10
  3. package/dist/agent/agentTypes.d.ts.map +1 -1
  4. package/dist/agent/agentTypes.js +159 -48
  5. package/dist/agent/askOnce.d.ts.map +1 -1
  6. package/dist/agent/askOnce.js +14 -5
  7. package/dist/agent/backgroundAgents.d.ts.map +1 -1
  8. package/dist/agent/backgroundAgents.js +22 -8
  9. package/dist/agent/claimEvidence.d.ts.map +1 -1
  10. package/dist/agent/claimEvidence.js +2 -1
  11. package/dist/agent/compaction.d.ts.map +1 -1
  12. package/dist/agent/compaction.js +31 -10
  13. package/dist/agent/crossProcessLock.d.ts.map +1 -1
  14. package/dist/agent/crossProcessLock.js +14 -6
  15. package/dist/agent/editCompleteness.d.ts.map +1 -1
  16. package/dist/agent/fileLocks.d.ts.map +1 -1
  17. package/dist/agent/fileLocks.js +14 -5
  18. package/dist/agent/flaky.d.ts.map +1 -1
  19. package/dist/agent/hooks.d.ts.map +1 -1
  20. package/dist/agent/hooks.js +284 -75
  21. package/dist/agent/iterationPolicy.d.ts.map +1 -1
  22. package/dist/agent/iterationPolicy.js +26 -23
  23. package/dist/agent/lifecycleHost.d.ts.map +1 -1
  24. package/dist/agent/lifecycleHost.js +35 -11
  25. package/dist/agent/loop.d.ts +22 -4
  26. package/dist/agent/loop.d.ts.map +1 -1
  27. package/dist/agent/loop.js +238 -51
  28. package/dist/agent/memory.d.ts.map +1 -1
  29. package/dist/agent/memory.js +13 -7
  30. package/dist/agent/modelCatalogue.d.ts.map +1 -1
  31. package/dist/agent/modelCatalogue.js +4 -1
  32. package/dist/agent/nestedInstructions.d.ts +122 -0
  33. package/dist/agent/nestedInstructions.d.ts.map +1 -0
  34. package/dist/agent/nestedInstructions.js +696 -0
  35. package/dist/agent/peerRegistry.d.ts.map +1 -1
  36. package/dist/agent/peerRegistry.js +44 -6
  37. package/dist/agent/peerTransport.d.ts.map +1 -1
  38. package/dist/agent/peerTransport.js +18 -6
  39. package/dist/agent/planMode.d.ts.map +1 -1
  40. package/dist/agent/planMode.js +194 -41
  41. package/dist/agent/readDedupe.d.ts.map +1 -1
  42. package/dist/agent/readDedupe.js +3 -1
  43. package/dist/agent/securityLint.d.ts.map +1 -1
  44. package/dist/agent/sharedTasks.d.ts.map +1 -1
  45. package/dist/agent/sharedTasks.js +20 -5
  46. package/dist/agent/skills.d.ts +79 -0
  47. package/dist/agent/skills.d.ts.map +1 -1
  48. package/dist/agent/skills.js +403 -14
  49. package/dist/agent/subAgentBudget.d.ts.map +1 -1
  50. package/dist/agent/subTask.d.ts.map +1 -1
  51. package/dist/agent/subTask.js +87 -37
  52. package/dist/agent/subTaskSupport.d.ts.map +1 -1
  53. package/dist/agent/subTaskSupport.js +43 -14
  54. package/dist/agent/testIntegrity.d.ts.map +1 -1
  55. package/dist/agent/testIntegrity.js +4 -2
  56. package/dist/agent/toolDescriptions.d.ts.map +1 -1
  57. package/dist/agent/toolPrefetch.d.ts.map +1 -1
  58. package/dist/agent/toolPrefetch.js +15 -11
  59. package/dist/agent/trust.d.ts.map +1 -1
  60. package/dist/agent/trust.js +6 -2
  61. package/dist/agent/worktree.d.ts.map +1 -1
  62. package/dist/agent/worktree.js +70 -16
  63. package/dist/agent/worktreeEnforcement.d.ts.map +1 -1
  64. package/dist/agent/worktreeEnforcement.js +9 -4
  65. package/dist/api/client.d.ts +6 -0
  66. package/dist/api/client.d.ts.map +1 -1
  67. package/dist/api/client.js +64 -31
  68. package/dist/auth/index.d.ts.map +1 -1
  69. package/dist/auth/index.js +3 -1
  70. package/dist/checkpoint/manager.d.ts.map +1 -1
  71. package/dist/checkpoint/manager.js +22 -7
  72. package/dist/commands/loader.d.ts.map +1 -1
  73. package/dist/commands/loader.js +6 -1
  74. package/dist/index.d.ts +2 -0
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +2 -0
  77. package/dist/mcp/client.d.ts +42 -0
  78. package/dist/mcp/client.d.ts.map +1 -1
  79. package/dist/mcp/client.js +89 -24
  80. package/dist/mcp/httpClient.d.ts +24 -1
  81. package/dist/mcp/httpClient.d.ts.map +1 -1
  82. package/dist/mcp/httpClient.js +80 -14
  83. package/dist/mcp/instructions.d.ts +11 -0
  84. package/dist/mcp/instructions.d.ts.map +1 -0
  85. package/dist/mcp/instructions.js +42 -0
  86. package/dist/mcp/manager.d.ts +35 -0
  87. package/dist/mcp/manager.d.ts.map +1 -1
  88. package/dist/mcp/manager.js +135 -24
  89. package/dist/mcp/server.d.ts.map +1 -1
  90. package/dist/mcp/server.js +48 -14
  91. package/dist/mcp/sseClient.d.ts +9 -1
  92. package/dist/mcp/sseClient.d.ts.map +1 -1
  93. package/dist/mcp/sseClient.js +43 -10
  94. package/dist/mcp/stats.d.ts.map +1 -1
  95. package/dist/permissions/bashClassify.d.ts.map +1 -1
  96. package/dist/permissions/bashClassify.js +25 -3
  97. package/dist/permissions/destructive.d.ts.map +1 -1
  98. package/dist/permissions/destructive.js +7 -2
  99. package/dist/permissions/destructiveTokens.d.ts.map +1 -1
  100. package/dist/permissions/destructiveTokens.js +104 -24
  101. package/dist/permissions/modePolicy.d.ts.map +1 -1
  102. package/dist/permissions/modePolicy.js +40 -17
  103. package/dist/permissions/rules.d.ts.map +1 -1
  104. package/dist/permissions/rules.js +7 -5
  105. package/dist/plugins/data.d.ts.map +1 -1
  106. package/dist/plugins/data.js +6 -2
  107. package/dist/plugins/index.d.ts.map +1 -1
  108. package/dist/plugins/index.js +18 -7
  109. package/dist/plugins/installer.d.ts.map +1 -1
  110. package/dist/plugins/installer.js +17 -10
  111. package/dist/plugins/sources.d.ts.map +1 -1
  112. package/dist/plugins/sources.js +9 -3
  113. package/dist/tools/executor.d.ts.map +1 -1
  114. package/dist/tools/executor.js +454 -168
  115. package/dist/tools/sandbox.d.ts.map +1 -1
  116. package/dist/tools/sandbox.js +5 -1
  117. package/dist/tools/symbols.d.ts.map +1 -1
  118. package/dist/tools/symbols.js +30 -8
  119. package/dist/tools/tsLangService.d.ts.map +1 -1
  120. package/dist/tools/tsLangService.js +12 -4
  121. package/dist/types.d.ts +17 -0
  122. package/dist/types.d.ts.map +1 -1
  123. package/dist/util/frontmatter.d.ts.map +1 -1
  124. package/dist/util/miniYaml.d.ts.map +1 -1
  125. package/dist/util/miniYaml.js +3 -1
  126. package/package.json +1 -1
@@ -0,0 +1,696 @@
1
+ "use strict";
2
+ // Discovered instruction files: `<dir>/nexrall.md` and rule files under
3
+ // `.nexrall/rules/` — the instructions half of the same 2.1.x behaviour skills.ts
4
+ // implements for skills.
5
+ //
6
+ // Claude Code loads a subdirectory's CLAUDE.md once it works with files there, and
7
+ // the file then stays in context for the rest of the session. Same shape here, one
8
+ // level per ancestor: as a tool call touches `packages/cli/src/x.ts`, every directory
9
+ // from `packages/cli/src` up to (but excluding) the project root is checked for its
10
+ // own `nexrall.md`/`NEXRALL.md`. The ROOT file is the session brief's job — read by
11
+ // `readNexrallMd` (CLI) / `_readNexrallMd` (VS Code) at session start — so it is
12
+ // deliberately not re-read here.
13
+ //
14
+ // The second tier is `.nexrall/rules/**/*.md` (S18d), Claude Code's `.claude/rules/`
15
+ // shape: standalone instruction files that say what they scope to in frontmatter.
16
+ //
17
+ // • No `paths:` (or `globs:`) frontmatter → UNCONDITIONAL: the rule applies from
18
+ // the session's first request. Root-level unconditional rules load once, up
19
+ // front; an unconditional rule in a subdirectory's `.nexrall/rules/` loads when
20
+ // that subtree is first touched (the nested-CLAUDE.md timing).
21
+ // • `paths:` frontmatter → PATH-SCOPED: the rule loads only when a tool call
22
+ // touches a project-relative path matching one of its globs (shared with CC:
23
+ // `**`, `*`, `?`, `{a,b}`, `[abc]`; comma string or YAML list). It then stays
24
+ // for the session like everything in the ledger.
25
+ // • Frontmatter never reaches the prompt — only the body after the closing `---`.
26
+ // • Symlinks are never followed: a cloned repo must not be able to point a "rule"
27
+ // at, say, `~/.ssh/id_rsa` and have it injected as instructions.
28
+ //
29
+ // Two deliberate properties:
30
+ // • LAZY: only directories the agent has actually worked in are loaded; a repo full
31
+ // of subtree conventions costs nothing until the subtree is touched.
32
+ // • A LEDGER: once loaded, a file stays loaded. The host owns the scanner and passes
33
+ // it to every run of the session, because conventions that silently vanished
34
+ // between turns would be worse than not having them. `refresh()` re-reads files
35
+ // from disk (/reload), because content edits have no other way in.
36
+ //
37
+ // Directories are re-checked on every touch (a plain existsSync — the `loaded` map is
38
+ // what makes re-reads impossible), so a nexrall.md or a rules file CREATED mid-session
39
+ // — the agent writing one, /init-style — applies from the request after the write. The
40
+ // ignore policy is skills.ts's `isIgnoredNestedDir`: a dependency checkout is not where
41
+ // the user keeps briefs, and scanning node_modules would turn any dependency touch into
42
+ // a prompt-injection vector. (The ignore gate does not apply to the ROOT `.nexrall/`,
43
+ // which is the project's own configuration dir.)
44
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
45
+ if (k2 === undefined) k2 = k;
46
+ var desc = Object.getOwnPropertyDescriptor(m, k);
47
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
48
+ desc = { enumerable: true, get: function() { return m[k]; } };
49
+ }
50
+ Object.defineProperty(o, k2, desc);
51
+ }) : (function(o, m, k, k2) {
52
+ if (k2 === undefined) k2 = k;
53
+ o[k2] = m[k];
54
+ }));
55
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
56
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
57
+ }) : function(o, v) {
58
+ o["default"] = v;
59
+ });
60
+ var __importStar = (this && this.__importStar) || (function () {
61
+ var ownKeys = function(o) {
62
+ ownKeys = Object.getOwnPropertyNames || function (o) {
63
+ var ar = [];
64
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
65
+ return ar;
66
+ };
67
+ return ownKeys(o);
68
+ };
69
+ return function (mod) {
70
+ if (mod && mod.__esModule) return mod;
71
+ var result = {};
72
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
73
+ __setModuleDefault(result, mod);
74
+ return result;
75
+ };
76
+ })();
77
+ Object.defineProperty(exports, "__esModule", { value: true });
78
+ exports.NestedInstructionScanner = exports.NESTED_INSTRUCTIONS_TOTAL = exports.NESTED_INSTRUCTIONS_PER_FILE = void 0;
79
+ exports.pathGlobToRegExp = pathGlobToRegExp;
80
+ exports.extractPathGlobs = extractPathGlobs;
81
+ exports.formatNestedInstructions = formatNestedInstructions;
82
+ const fs = __importStar(require("fs"));
83
+ const path = __importStar(require("path"));
84
+ const skills_1 = require("./skills");
85
+ const frontmatter_1 = require("../util/frontmatter");
86
+ /**
87
+ * Caps: discovered files ride the system prompt on EVERY request of the session, so a
88
+ * pathological (or hostile, from a cloned repo) subdirectory file must not be able to
89
+ * crowd out the real project brief. Truncation is visible; the same pattern as the
90
+ * MCP-instructions caps.
91
+ */
92
+ exports.NESTED_INSTRUCTIONS_PER_FILE = 4000;
93
+ exports.NESTED_INSTRUCTIONS_TOTAL = 12000;
94
+ /**
95
+ * Nested candidate names — the root's own `nexrall.md`/`NEXRALL.md` family. The root's
96
+ * extra candidates (`.nexrall/instructions.md`, `.nexrall/config.md`) are a project-level
97
+ * indirection and stay out of the nested tier on purpose.
98
+ */
99
+ const NESTED_MD_CANDIDATES = ['nexrall.md', 'NEXRALL.md'];
100
+ /** The rules directory, relative to the project root or any subdirectory. */
101
+ const RULES_SUBPATH = ['.nexrall', 'rules'];
102
+ /** Parent-first (fewer segments first), then alphabetical — the order the prompt shows. */
103
+ function compareNestedInstructions(a, b) {
104
+ const da = a.relPath.split('/').length;
105
+ const db = b.relPath.split('/').length;
106
+ return da !== db ? da - db : a.relPath.localeCompare(b.relPath);
107
+ }
108
+ function tryReadFile(p) {
109
+ try {
110
+ return fs.readFileSync(p, 'utf-8').trim();
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ function relPosix(workDir, file) {
117
+ return path.relative(path.resolve(workDir), file).split(path.sep).join('/');
118
+ }
119
+ // ─── `paths:` globs (S18d) ─────────────────────────────────────────────────────
120
+ //
121
+ // The matcher is a real glob compiler, not the segment-and-boundary matcher
122
+ // `/exclude` uses (contextFiles.ts): CC's documented rule patterns look like
123
+ // `src/**/*.{ts,tsx}` / `lib/**/*.ts` / `**/*.test.ts`, so `**` must cross
124
+ // separators, `**/` must collapse to zero-or-more whole segments, and brace
125
+ // alternation and character classes must work. Patterns are anchored to the
126
+ // project root and matched against project-relative POSIX paths.
127
+ const compiledGlobs = new Map();
128
+ const MAX_BRACE_DEPTH = 8;
129
+ function escapeRe(s) {
130
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
131
+ }
132
+ /** Index of the brace matching the `{` at `start`, or -1. Backslash escapes are honoured. */
133
+ function findClosingBrace(s, start) {
134
+ let depth = 0;
135
+ for (let i = start; i < s.length; i++) {
136
+ const ch = s[i];
137
+ if (ch === '\\') {
138
+ i++;
139
+ continue;
140
+ }
141
+ if (ch === '{')
142
+ depth++;
143
+ else if (ch === '}') {
144
+ depth--;
145
+ if (depth === 0)
146
+ return i;
147
+ }
148
+ }
149
+ return -1;
150
+ }
151
+ /** Split brace alternatives on TOP-LEVEL commas only (`{a,b{c,d}}` → `a`, `b{c,d}`). */
152
+ function splitTopLevelCommas(inner) {
153
+ const out = [];
154
+ let depth = 0;
155
+ let cur = '';
156
+ for (const ch of inner) {
157
+ if (ch === '{')
158
+ depth++;
159
+ else if (ch === '}')
160
+ depth--;
161
+ if (ch === ',' && depth === 0) {
162
+ out.push(cur);
163
+ cur = '';
164
+ continue;
165
+ }
166
+ cur += ch;
167
+ }
168
+ out.push(cur);
169
+ return out;
170
+ }
171
+ function compileGlob(glob, depth) {
172
+ let re = '';
173
+ let i = 0;
174
+ while (i < glob.length) {
175
+ const ch = glob[i];
176
+ if (ch === '\\' && i + 1 < glob.length) {
177
+ re += escapeRe(glob[i + 1]);
178
+ i += 2;
179
+ continue;
180
+ }
181
+ if (ch === '*') {
182
+ if (glob[i + 1] === '*') {
183
+ // `**/` = zero or more whole segments — so `src/**/*.ts` matches `src/a.ts`
184
+ // too. A trailing/mid `**` not followed by `/` crosses separators as-is.
185
+ if (glob[i + 2] === '/') {
186
+ re += '(?:[^/]+/)*';
187
+ i += 3;
188
+ continue;
189
+ }
190
+ re += '.*';
191
+ i += 2;
192
+ continue;
193
+ }
194
+ re += '[^/]*';
195
+ i += 1;
196
+ continue;
197
+ }
198
+ if (ch === '?') {
199
+ re += '[^/]';
200
+ i += 1;
201
+ continue;
202
+ }
203
+ if (ch === '{' && depth < MAX_BRACE_DEPTH) {
204
+ const end = findClosingBrace(glob, i);
205
+ if (end !== -1) {
206
+ const alts = splitTopLevelCommas(glob.slice(i + 1, end));
207
+ re += `(?:${alts.map((a) => compileGlob(a, depth + 1)).join('|')})`;
208
+ i = end + 1;
209
+ continue;
210
+ }
211
+ re += '\\{';
212
+ i += 1;
213
+ continue;
214
+ }
215
+ if (ch === '[') {
216
+ // Character class. A `]` may appear literally right after `[` / `[!` / `[^`;
217
+ // otherwise the class ends at the first `]`. No escapes inside classes —
218
+ // deliberate simplification (real rule patterns do not use them).
219
+ let j = i + 1;
220
+ if (glob[j] === '!' || glob[j] === '^')
221
+ j++;
222
+ if (glob[j] === ']')
223
+ j++;
224
+ for (; j < glob.length && glob[j] !== ']'; j++) {
225
+ /* find the end */
226
+ }
227
+ if (j < glob.length) {
228
+ let cls = glob.slice(i + 1, j);
229
+ let neg = false;
230
+ if (cls[0] === '!' || cls[0] === '^') {
231
+ neg = true;
232
+ cls = cls.slice(1);
233
+ }
234
+ // `-` is kept for ranges; regex-metacharacters that mean themselves in a
235
+ // glob class are escaped.
236
+ cls = cls.replace(/[\\\]^]/g, '\\$&');
237
+ re += `[${neg ? '^' : ''}${cls}]`;
238
+ i = j + 1;
239
+ continue;
240
+ }
241
+ re += '\\[';
242
+ i += 1;
243
+ continue;
244
+ }
245
+ re += escapeRe(ch);
246
+ i += 1;
247
+ }
248
+ return re;
249
+ }
250
+ /**
251
+ * Compile one `paths:` glob to an anchored RegExp over project-relative POSIX paths,
252
+ * or null when the pattern is empty / fails to compile (callers treat null as
253
+ * non-matching). Compiled once per pattern string, cached for the process.
254
+ */
255
+ function pathGlobToRegExp(pattern) {
256
+ const cleaned = pattern.trim().replace(/^\.\//, '').replace(/^\/+/, '');
257
+ if (!cleaned)
258
+ return null;
259
+ const cached = compiledGlobs.get(cleaned);
260
+ if (cached !== undefined)
261
+ return cached;
262
+ let rx;
263
+ try {
264
+ rx = new RegExp(`^${compileGlob(cleaned, 0)}$`);
265
+ }
266
+ catch {
267
+ rx = null;
268
+ }
269
+ compiledGlobs.set(cleaned, rx);
270
+ return rx;
271
+ }
272
+ /** True when the project-relative path matches any of the rule's globs. */
273
+ function matchesAnyGlob(patterns, rel) {
274
+ return patterns.some((p) => {
275
+ const rx = pathGlobToRegExp(p);
276
+ return rx ? rx.test(rel) : false;
277
+ });
278
+ }
279
+ function leadingIndent(line) {
280
+ const m = /^[ \t]*/.exec(line);
281
+ return m ? m[0].length : 0;
282
+ }
283
+ function unquoteYaml(v) {
284
+ return v.trim().replace(/^["']|["']$/g, '');
285
+ }
286
+ /**
287
+ * Read the `paths:` (alias `globs:`) frontmatter of a rule file. Returns null when
288
+ * there is no frontmatter, the key is absent, or it lists nothing — all of which mean
289
+ * UNCONDITIONAL. Accepts the three shapes CC's docs and real files use: a block list
290
+ * (`paths:\n - glob`), a comma-separated string, and a flow list (`["a", "b"]`).
291
+ * Only top-level keys count — an indented `paths:` inside a nested map is that map's
292
+ * field, the same isolation util/frontmatter applies.
293
+ */
294
+ function extractPathGlobs(raw) {
295
+ const m = /^\s*---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw);
296
+ if (!m)
297
+ return null;
298
+ const lines = m[1].split(/\r?\n/);
299
+ for (let i = 0; i < lines.length; i++) {
300
+ if (leadingIndent(lines[i]) > 0)
301
+ continue;
302
+ const kv = /^(paths|globs)\s*:\s*(.*)$/i.exec(lines[i].trim());
303
+ if (!kv)
304
+ continue;
305
+ const inline = kv[2].trim();
306
+ if (inline)
307
+ return splitGlobList(inline);
308
+ const items = [];
309
+ for (let j = i + 1; j < lines.length; j++) {
310
+ if (!lines[j].trim())
311
+ continue;
312
+ if (leadingIndent(lines[j]) === 0)
313
+ break; // the next top-level key
314
+ const li = /^\s*-\s*(.*)$/.exec(lines[j]);
315
+ if (li) {
316
+ const v = unquoteYaml(li[1]);
317
+ if (v)
318
+ items.push(v);
319
+ }
320
+ }
321
+ return items.length ? items : null;
322
+ }
323
+ return null;
324
+ }
325
+ /** Comma-separated string or flow list → globs. Quoted items keep embedded commas. */
326
+ function splitGlobList(value) {
327
+ let s = value.trim();
328
+ if (s.startsWith('[') && s.endsWith(']'))
329
+ s = s.slice(1, -1);
330
+ const out = [];
331
+ let cur = '';
332
+ let quote = null;
333
+ for (const ch of s) {
334
+ if (quote) {
335
+ if (ch === quote)
336
+ quote = null;
337
+ else
338
+ cur += ch;
339
+ continue;
340
+ }
341
+ if (ch === '"' || ch === "'") {
342
+ quote = ch;
343
+ continue;
344
+ }
345
+ if (ch === ',') {
346
+ const v = cur.trim();
347
+ if (v)
348
+ out.push(v);
349
+ cur = '';
350
+ continue;
351
+ }
352
+ cur += ch;
353
+ }
354
+ const last = cur.trim();
355
+ if (last)
356
+ out.push(last);
357
+ return out.length ? out : null;
358
+ }
359
+ /**
360
+ * Every `.md` file under a rules directory, recursive, sorted, SYMLINKS SKIPPED —
361
+ * following one would let a cloned repo inject an arbitrary file (say
362
+ * `~/.aws/credentials`) as "instructions". A directory that does not exist is
363
+ * empty, not an error.
364
+ */
365
+ function walkRulesDir(base) {
366
+ const out = [];
367
+ const walk = (dir) => {
368
+ let entries;
369
+ try {
370
+ entries = fs.readdirSync(dir, { withFileTypes: true });
371
+ }
372
+ catch {
373
+ return;
374
+ }
375
+ entries.sort((a, b) => a.name.localeCompare(b.name));
376
+ for (const e of entries) {
377
+ if (e.isSymbolicLink())
378
+ continue;
379
+ const p = path.join(dir, e.name);
380
+ if (e.isDirectory())
381
+ walk(p);
382
+ else if (e.isFile() && e.name.toLowerCase().endsWith('.md'))
383
+ out.push(p);
384
+ }
385
+ };
386
+ walk(base);
387
+ return out;
388
+ }
389
+ /**
390
+ * Discovers `nexrall.md` briefs and `.nexrall/rules` files as the agent works (see the
391
+ * section comment above). Stateful across calls by design — the loaded set IS the
392
+ * feature (and the callback dedupe), so the host that owns a session keeps ONE
393
+ * instance and passes it to every run.
394
+ */
395
+ class NestedInstructionScanner {
396
+ constructor(workDir) {
397
+ this.workDir = workDir;
398
+ this.loaded = new Map();
399
+ this.ignored = new Map();
400
+ /** dir → the real on-disk path of its brief (casing resolved once, on the first hit). */
401
+ this.resolved = new Map();
402
+ /** rule file → parsed metadata: globs (null = unconditional) + frontmatter-stripped body. */
403
+ this.ruleMeta = new Map();
404
+ /** Root unconditional rules load once per session state; refresh() re-arms. */
405
+ this.rootInitialized = false;
406
+ }
407
+ /**
408
+ * Load the root's unconditional rules and return them — the HOST calls this at
409
+ * session start so they are announced with `session_start` alongside the brief and
410
+ * memory. A host that skips it still gets the rules from the first `list()`; it
411
+ * just has no announcement to make.
412
+ */
413
+ initialRules() {
414
+ return this.ensureRootRules();
415
+ }
416
+ /**
417
+ * Scan the tool input of one call for filesystem paths and return the instruction
418
+ * files this call loaded (usually none). A failed tool call still scans: intent
419
+ * marks the area even when the file itself was missing.
420
+ */
421
+ scanInput(input) {
422
+ const out = [];
423
+ for (const key of skills_1.NESTED_PATH_INPUT_KEYS) {
424
+ const v = input[key];
425
+ if (typeof v !== 'string' || !v || v.length > 4096)
426
+ continue;
427
+ out.push(...this.scanPath(v));
428
+ }
429
+ return out;
430
+ }
431
+ /** Scan the ancestor chain of one touched path; returns the files newly loaded by this call. */
432
+ scanPath(touched) {
433
+ let abs;
434
+ try {
435
+ abs = path.isAbsolute(touched) ? touched : path.join(this.workDir, touched);
436
+ }
437
+ catch {
438
+ return [];
439
+ }
440
+ let base = abs;
441
+ try {
442
+ if (!fs.statSync(abs).isDirectory())
443
+ base = path.dirname(abs);
444
+ }
445
+ catch {
446
+ base = path.dirname(abs); // a path being created: its directory is the claim
447
+ }
448
+ const work = path.resolve(this.workDir);
449
+ const relTouched = relPosix(work, abs);
450
+ const out = [];
451
+ let dir = base;
452
+ for (;;) {
453
+ const rel = path.relative(work, dir);
454
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
455
+ break;
456
+ if (!(0, skills_1.isIgnoredNestedDir)(this.workDir, dir, this.ignored)) {
457
+ const found = this.checkDir(dir);
458
+ if (found && !this.loaded.has(found)) {
459
+ const content = tryReadFile(found);
460
+ if (content !== null) {
461
+ const entry = {
462
+ filePath: found,
463
+ relPath: path.relative(work, found).split(path.sep).join('/'),
464
+ content,
465
+ kind: 'brief',
466
+ loadReason: 'nested_traversal',
467
+ };
468
+ this.loaded.set(found, entry);
469
+ out.push(entry);
470
+ }
471
+ }
472
+ out.push(...this.scanRulesDir(path.join(dir, ...RULES_SUBPATH), relTouched));
473
+ }
474
+ const parent = path.dirname(dir);
475
+ if (parent === dir)
476
+ break;
477
+ dir = parent;
478
+ }
479
+ // Path-scoped ROOT rules: matched against this touch, regardless of the ancestor
480
+ // chain (their scope is the glob, not a directory). Unconditional root rules are
481
+ // ensureRootRules()'s job.
482
+ if (!relTouched.startsWith('..'))
483
+ out.push(...this.scanRootRulesFor(relTouched));
484
+ return out;
485
+ }
486
+ /** Everything loaded so far, parent-first — what the prompt renders. */
487
+ list() {
488
+ this.ensureRootRules();
489
+ return [...this.loaded.values()].sort(compareNestedInstructions);
490
+ }
491
+ /**
492
+ * Re-read every loaded file from disk; files that no longer exist are dropped.
493
+ * The `/reload` escape hatch for content edits — returns how many entries changed
494
+ * or vanished, so a caller can report it (0 = nothing to say). Root rules are
495
+ * re-listed, so an unconditional rule file created since the session started (or
496
+ * one whose frontmatter lost its `paths:`) joins here; new path-scoped files load
497
+ * on their next matching touch.
498
+ */
499
+ refresh() {
500
+ let changed = 0;
501
+ this.ruleMeta.clear();
502
+ this.rootInitialized = false;
503
+ for (const [file, entry] of [...this.loaded]) {
504
+ let content;
505
+ if (entry.kind === 'rule') {
506
+ const meta = this.ruleMetaOf(file);
507
+ content = meta && meta.content ? meta.content : null;
508
+ }
509
+ else {
510
+ content = tryReadFile(file);
511
+ }
512
+ if (content === null) {
513
+ this.loaded.delete(file);
514
+ changed++;
515
+ }
516
+ else if (content !== entry.content) {
517
+ entry.content = content;
518
+ changed++;
519
+ }
520
+ }
521
+ changed += this.ensureRootRules().length;
522
+ return changed;
523
+ }
524
+ /** Root unconditional rules: root listing once per session state, `session_start`. */
525
+ ensureRootRules() {
526
+ const out = [];
527
+ if (this.rootInitialized)
528
+ return out;
529
+ this.rootInitialized = true;
530
+ for (const file of walkRulesDir(path.join(this.workDir, ...RULES_SUBPATH))) {
531
+ const entry = this.loadRule(file, 'session_start', null);
532
+ if (entry)
533
+ out.push(entry);
534
+ }
535
+ return out;
536
+ }
537
+ /** Root path-scoped rules matching this touch ('path_glob_match'). */
538
+ scanRootRulesFor(relTouched) {
539
+ const out = [];
540
+ for (const file of walkRulesDir(path.join(this.workDir, ...RULES_SUBPATH))) {
541
+ if (this.loaded.has(file))
542
+ continue;
543
+ const meta = this.ruleMetaOf(file);
544
+ if (!meta || meta.patterns === null)
545
+ continue; // unconditional → ensureRootRules
546
+ const entry = this.loadRule(file, 'path_glob_match', relTouched);
547
+ if (entry)
548
+ out.push(entry);
549
+ }
550
+ return out;
551
+ }
552
+ /**
553
+ * Rules a directory carries (`<dir>/.nexrall/rules`): unconditional ones load with
554
+ * the touch (`nested_traversal` — the same timing as that directory's brief);
555
+ * path-scoped ones load when this touch matches (`path_glob_match`).
556
+ */
557
+ scanRulesDir(base, relTouched) {
558
+ const out = [];
559
+ for (const file of walkRulesDir(base)) {
560
+ const entry = this.loadRule(file, 'nested_traversal', relTouched);
561
+ if (entry)
562
+ out.push(entry);
563
+ }
564
+ return out;
565
+ }
566
+ /**
567
+ * Load one rule file if it should be — dedupe, parse, scope decision. `fallbackReason`
568
+ * is the reason an UNCONDITIONAL rule loads with; `relTouched` (null = no touch yet)
569
+ * is what path-scoped globs are tested against. Returns the new entry or null.
570
+ */
571
+ loadRule(file, fallbackReason, relTouched) {
572
+ if (this.loaded.has(file))
573
+ return null;
574
+ const meta = this.ruleMetaOf(file);
575
+ if (!meta || !meta.content)
576
+ return null; // unreadable or empty body: nothing to inject
577
+ let reason;
578
+ if (meta.patterns === null)
579
+ reason = fallbackReason;
580
+ else if (relTouched !== null && matchesAnyGlob(meta.patterns, relTouched))
581
+ reason = 'path_glob_match';
582
+ else
583
+ reason = null; // scoped, and this touch (if any) is out of scope
584
+ if (!reason)
585
+ return null;
586
+ const entry = {
587
+ filePath: file,
588
+ relPath: relPosix(this.workDir, file),
589
+ content: meta.content,
590
+ kind: 'rule',
591
+ loadReason: reason,
592
+ };
593
+ this.loaded.set(file, entry);
594
+ return entry;
595
+ }
596
+ /** Parsed metadata for a rule file, cached until refresh(). */
597
+ ruleMetaOf(file) {
598
+ const cached = this.ruleMeta.get(file);
599
+ if (cached)
600
+ return cached;
601
+ let raw;
602
+ try {
603
+ raw = fs.readFileSync(file, 'utf-8');
604
+ }
605
+ catch {
606
+ return null;
607
+ }
608
+ const parsed = (0, frontmatter_1.parseFrontmatter)(raw);
609
+ const meta = {
610
+ patterns: parsed.ok ? extractPathGlobs(raw) : null,
611
+ content: (parsed.ok ? parsed.body : raw).trim(),
612
+ };
613
+ this.ruleMeta.set(file, meta);
614
+ return meta;
615
+ }
616
+ /**
617
+ * The instruction file a directory carries, or null. A directory WITHOUT one is
618
+ * re-checked on every touch (see the header), so a file created after the first scan
619
+ * is still found; a directory WITH one is resolved once — the ledger keeps its
620
+ * content, and deletion is `refresh()`'s business.
621
+ */
622
+ checkDir(dir) {
623
+ const known = this.resolved.get(dir);
624
+ if (known)
625
+ return known;
626
+ if ((0, skills_1.isIgnoredNestedDir)(this.workDir, dir, this.ignored))
627
+ return null;
628
+ for (const name of NESTED_MD_CANDIDATES) {
629
+ const p = path.join(dir, name);
630
+ try {
631
+ if (fs.statSync(p).isFile()) {
632
+ const real = resolveRealName(dir, name);
633
+ this.resolved.set(dir, real);
634
+ return real;
635
+ }
636
+ }
637
+ catch {
638
+ /* try the next candidate */
639
+ }
640
+ }
641
+ return null;
642
+ }
643
+ }
644
+ exports.NestedInstructionScanner = NestedInstructionScanner;
645
+ /**
646
+ * The path to REPORT for a probed candidate. On a case-insensitive filesystem the stat
647
+ * above can succeed for `NEXRALL.md` through a `nexrall.md` probe (macOS, Windows), and
648
+ * a ledger labelled with a path that does not exist verbatim is a small lie that breaks
649
+ * tests on exactly one OS. One readdir on the FIRST hit resolves the real name; exact
650
+ * spelling wins where both exist (Linux can carry both files).
651
+ */
652
+ function resolveRealName(dir, probedBase) {
653
+ try {
654
+ const entries = fs.readdirSync(dir);
655
+ const real = entries.includes(probedBase)
656
+ ? probedBase
657
+ : entries.find((e) => e.toLowerCase() === probedBase.toLowerCase());
658
+ return path.join(dir, real ?? probedBase);
659
+ }
660
+ catch {
661
+ return path.join(dir, probedBase);
662
+ }
663
+ }
664
+ /**
665
+ * Render the discovered-instructions block, or null when there is nothing to inject —
666
+ * callers join the result with the project brief, and a null must not leave an empty
667
+ * section behind. Parent-first order makes a deeper file (the more specific one) read
668
+ * as the refinement of its ancestors, which is also how Claude Code orders nested
669
+ * CLAUDE.md context. Rules carry their own label so a file like `.nexrall/rules/x.md`
670
+ * is not mistaken for a subdirectory brief.
671
+ */
672
+ function formatNestedInstructions(files) {
673
+ if (!files.length)
674
+ return null;
675
+ const parts = [];
676
+ const seen = new Set();
677
+ let used = 0;
678
+ for (const f of [...files].sort(compareNestedInstructions)) {
679
+ let text = f.content.trim();
680
+ if (!text || seen.has(f.relPath))
681
+ continue;
682
+ seen.add(f.relPath);
683
+ if (text.length > exports.NESTED_INSTRUCTIONS_PER_FILE) {
684
+ text = text.slice(0, exports.NESTED_INSTRUCTIONS_PER_FILE) + '\n[truncated]';
685
+ }
686
+ if (used + text.length > exports.NESTED_INSTRUCTIONS_TOTAL) {
687
+ parts.push(`(more instruction files omitted — the ${exports.NESTED_INSTRUCTIONS_TOTAL}-character budget was reached)`);
688
+ break;
689
+ }
690
+ used += text.length;
691
+ const label = f.kind === 'rule' ? 'Rule' : 'Subdirectory instructions';
692
+ parts.push(`[${label} — ${f.relPath}]\n${text}`);
693
+ }
694
+ return parts.length ? parts.join('\n\n---\n\n') : null;
695
+ }
696
+ //# sourceMappingURL=nestedInstructions.js.map