@nexrall/code-core 1.4.71 → 1.4.72
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/agent/agentRegistry.d.ts.map +1 -1
- package/dist/agent/agentRegistry.js +28 -10
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +159 -48
- package/dist/agent/askOnce.d.ts.map +1 -1
- package/dist/agent/askOnce.js +14 -5
- package/dist/agent/backgroundAgents.d.ts.map +1 -1
- package/dist/agent/backgroundAgents.js +22 -8
- package/dist/agent/claimEvidence.d.ts.map +1 -1
- package/dist/agent/claimEvidence.js +2 -1
- package/dist/agent/compaction.d.ts.map +1 -1
- package/dist/agent/compaction.js +31 -10
- package/dist/agent/crossProcessLock.d.ts.map +1 -1
- package/dist/agent/crossProcessLock.js +14 -6
- package/dist/agent/editCompleteness.d.ts.map +1 -1
- package/dist/agent/fileLocks.d.ts.map +1 -1
- package/dist/agent/fileLocks.js +14 -5
- package/dist/agent/flaky.d.ts.map +1 -1
- package/dist/agent/hooks.d.ts.map +1 -1
- package/dist/agent/hooks.js +284 -75
- package/dist/agent/iterationPolicy.d.ts.map +1 -1
- package/dist/agent/iterationPolicy.js +26 -23
- package/dist/agent/lifecycleHost.d.ts.map +1 -1
- package/dist/agent/lifecycleHost.js +35 -11
- package/dist/agent/loop.d.ts +22 -4
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +223 -51
- package/dist/agent/memory.d.ts.map +1 -1
- package/dist/agent/memory.js +13 -7
- package/dist/agent/modelCatalogue.d.ts.map +1 -1
- package/dist/agent/modelCatalogue.js +4 -1
- package/dist/agent/nestedInstructions.d.ts +122 -0
- package/dist/agent/nestedInstructions.d.ts.map +1 -0
- package/dist/agent/nestedInstructions.js +696 -0
- package/dist/agent/peerRegistry.d.ts.map +1 -1
- package/dist/agent/peerRegistry.js +44 -6
- package/dist/agent/peerTransport.d.ts.map +1 -1
- package/dist/agent/peerTransport.js +18 -6
- package/dist/agent/planMode.d.ts.map +1 -1
- package/dist/agent/planMode.js +194 -41
- package/dist/agent/readDedupe.d.ts.map +1 -1
- package/dist/agent/readDedupe.js +3 -1
- package/dist/agent/securityLint.d.ts.map +1 -1
- package/dist/agent/sharedTasks.d.ts.map +1 -1
- package/dist/agent/sharedTasks.js +20 -5
- package/dist/agent/skills.d.ts +79 -0
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +403 -14
- package/dist/agent/subAgentBudget.d.ts.map +1 -1
- package/dist/agent/subTask.d.ts.map +1 -1
- package/dist/agent/subTask.js +87 -37
- package/dist/agent/subTaskSupport.d.ts.map +1 -1
- package/dist/agent/subTaskSupport.js +43 -14
- package/dist/agent/testIntegrity.d.ts.map +1 -1
- package/dist/agent/testIntegrity.js +4 -2
- package/dist/agent/toolDescriptions.d.ts.map +1 -1
- package/dist/agent/toolPrefetch.d.ts.map +1 -1
- package/dist/agent/toolPrefetch.js +15 -11
- package/dist/agent/trust.d.ts.map +1 -1
- package/dist/agent/trust.js +6 -2
- package/dist/agent/worktree.d.ts.map +1 -1
- package/dist/agent/worktree.js +70 -16
- package/dist/agent/worktreeEnforcement.d.ts.map +1 -1
- package/dist/agent/worktreeEnforcement.js +9 -4
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +63 -31
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -1
- package/dist/checkpoint/manager.d.ts.map +1 -1
- package/dist/checkpoint/manager.js +22 -7
- package/dist/commands/loader.d.ts.map +1 -1
- package/dist/commands/loader.js +6 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/mcp/client.d.ts +34 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +89 -24
- package/dist/mcp/httpClient.d.ts +24 -1
- package/dist/mcp/httpClient.d.ts.map +1 -1
- package/dist/mcp/httpClient.js +80 -14
- package/dist/mcp/instructions.d.ts +11 -0
- package/dist/mcp/instructions.d.ts.map +1 -0
- package/dist/mcp/instructions.js +42 -0
- package/dist/mcp/manager.d.ts +24 -0
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +117 -24
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +48 -14
- package/dist/mcp/sseClient.d.ts +9 -1
- package/dist/mcp/sseClient.d.ts.map +1 -1
- package/dist/mcp/sseClient.js +43 -10
- package/dist/mcp/stats.d.ts.map +1 -1
- package/dist/permissions/bashClassify.d.ts.map +1 -1
- package/dist/permissions/bashClassify.js +25 -3
- package/dist/permissions/destructive.d.ts.map +1 -1
- package/dist/permissions/destructive.js +7 -2
- package/dist/permissions/destructiveTokens.d.ts.map +1 -1
- package/dist/permissions/destructiveTokens.js +104 -24
- package/dist/permissions/modePolicy.d.ts.map +1 -1
- package/dist/permissions/modePolicy.js +40 -17
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +7 -5
- package/dist/plugins/data.d.ts.map +1 -1
- package/dist/plugins/data.js +6 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +18 -7
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +17 -10
- package/dist/plugins/sources.d.ts.map +1 -1
- package/dist/plugins/sources.js +9 -3
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +454 -168
- package/dist/tools/sandbox.d.ts.map +1 -1
- package/dist/tools/sandbox.js +5 -1
- package/dist/tools/symbols.d.ts.map +1 -1
- package/dist/tools/symbols.js +30 -8
- package/dist/tools/tsLangService.d.ts.map +1 -1
- package/dist/tools/tsLangService.js +12 -4
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/util/frontmatter.d.ts.map +1 -1
- package/dist/util/miniYaml.d.ts.map +1 -1
- package/dist/util/miniYaml.js +3 -1
- 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
|