@nexrall/code-core 1.4.24 → 1.4.26

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.
@@ -0,0 +1,45 @@
1
+ import type { Message } from '../types';
2
+ export interface StoredAgent {
3
+ id: string;
4
+ /**
5
+ * The agent type this ran as, or null for an unnamed (general-purpose) run.
6
+ *
7
+ * Stored so resumption can be re-authorised against the CURRENT permission
8
+ * rules. Without it, an id would be a permanent bypass of any deny rule added
9
+ * after the agent first ran.
10
+ */
11
+ agentName: string | null;
12
+ /** Full conversation: the sub-agent's own user/assistant/tool messages. */
13
+ messages: Message[];
14
+ /** The description shown in the UI, for `/agents --resumable` style listings. */
15
+ description: string;
16
+ createdAt: number;
17
+ updatedAt: number;
18
+ /**
19
+ * Monotonic touch counter, used for ordering instead of `updatedAt`.
20
+ *
21
+ * Date.now() has millisecond resolution and these operations take
22
+ * microseconds, so two agents touched in the same tick compare EQUAL and a
23
+ * stable sort silently falls back to insertion order — i.e. "most recent"
24
+ * would have been wrong exactly when several things happen at once, which is
25
+ * the normal case for parallel sub-tasks. The timestamps are kept because
26
+ * they are meaningful to humans; the ordering does not depend on them.
27
+ */
28
+ seq: number;
29
+ /** Rough JSON size, cached so the byte cap doesn't re-serialise on every write. */
30
+ bytes: number;
31
+ }
32
+ /** Store a finished sub-agent's transcript. Returns its resumable id. */
33
+ export declare function rememberAgent(agentName: string | null, description: string, messages: Message[]): string;
34
+ /** Append a resumed run's new messages to an existing agent. */
35
+ export declare function updateAgent(id: string, messages: Message[]): void;
36
+ export declare function getAgent(id: string): StoredAgent | undefined;
37
+ /** Newest first — the order a "which agents can I resume?" list wants. */
38
+ export declare function listAgents(): StoredAgent[];
39
+ /** Test seam. Not exported through index.ts's public surface by intent. */
40
+ export declare function _resetAgentRegistry(): void;
41
+ export declare const _limits: {
42
+ MAX_AGENTS: number;
43
+ MAX_TOTAL_BYTES: number;
44
+ };
45
+ //# sourceMappingURL=agentRegistry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agentRegistry.d.ts","sourceRoot":"","sources":["../../src/agent/agentRegistry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AA8CxC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,2EAA2E;IAC3E,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;OASG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;CACf;AA4CD,yEAAyE;AACzE,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,OAAO,EAAE,GAClB,MAAM,CAeR;AAED,gEAAgE;AAChE,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI,CAYjE;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAE5D;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,IAAI,WAAW,EAAE,CAE1C;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C;AAED,eAAO,MAAM,OAAO;;;CAAkC,CAAC"}
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._limits = void 0;
4
+ exports.rememberAgent = rememberAgent;
5
+ exports.updateAgent = updateAgent;
6
+ exports.getAgent = getAgent;
7
+ exports.listAgents = listAgents;
8
+ exports._resetAgentRegistry = _resetAgentRegistry;
9
+ // ─── Resumable sub-agents ────────────────────────────────────────────────────
10
+ //
11
+ // A sub-agent normally starts with a blank slate: fresh context, no memory of a
12
+ // previous run. That is the whole point for one-shot research — but it makes
13
+ // follow-up work absurdly expensive. "Now also check the auth path" re-reads
14
+ // every file the last run already read, because the only way to add to a
15
+ // finished sub-task is to describe the whole job again from scratch.
16
+ //
17
+ // This registry keeps a completed sub-agent's transcript in memory so a later
18
+ // `task` call can continue it instead of restarting it. Claude Code does the
19
+ // same thing; the design decisions worth writing down are the ones about what
20
+ // NOT to do:
21
+ //
22
+ // • IN-MEMORY, process-lifetime only. Deliberately not written to disk. A
23
+ // transcript is unredacted tool output — file contents, command output,
24
+ // whatever a repo happens to contain. Persisting it would create a new
25
+ // durable copy of material the user never asked us to store, in a new place
26
+ // they would have to know to clean up. Losing resumability when the process
27
+ // exits is a much smaller cost than that.
28
+ //
29
+ // • BOUNDED, by entries and by bytes. A transcript is the largest object this
30
+ // process handles, and an agent loop that spawns sub-agents in a cycle would
31
+ // otherwise grow the heap until the CLI dies — a leak that shows up only in
32
+ // the longest sessions, i.e. the ones where losing work hurts most.
33
+ //
34
+ // • Resumption re-checks permission at the CALL SITE, not here. An id is a
35
+ // capability: if a rule denies `task(explorer)` after an explorer agent has
36
+ // already run, resuming by id must not hand that agent back. This module
37
+ // therefore stores the agent's NAME alongside its history, so the caller can
38
+ // re-evaluate the current rules against it. See runSubTask.
39
+ /** Max resumable agents kept alive. Oldest is evicted first. */
40
+ const MAX_AGENTS = 20;
41
+ /**
42
+ * Max total transcript bytes across all stored agents (~8 MB of JSON).
43
+ *
44
+ * Sized to comfortably hold a handful of long research runs while staying far
45
+ * below the point where it competes with the model context for memory. Eviction
46
+ * is by age, not by size, so one enormous transcript cannot pin out many small
47
+ * useful ones.
48
+ */
49
+ const MAX_TOTAL_BYTES = 8 * 1024 * 1024;
50
+ /**
51
+ * Insertion-ordered store. A Map is enough: Maps iterate in insertion order, so
52
+ * "oldest" is simply the first key, and re-inserting on update moves an entry to
53
+ * the back — giving LRU-by-touch for free without a second data structure.
54
+ */
55
+ const _agents = new Map();
56
+ let _counter = 0;
57
+ let _seq = 0;
58
+ function totalBytes() {
59
+ let n = 0;
60
+ for (const a of _agents.values())
61
+ n += a.bytes;
62
+ return n;
63
+ }
64
+ function evictUntilWithinLimits() {
65
+ while (_agents.size > MAX_AGENTS) {
66
+ const oldest = _agents.keys().next().value;
67
+ if (oldest === undefined)
68
+ break;
69
+ _agents.delete(oldest);
70
+ }
71
+ // Byte cap is checked second: an entry that is individually enormous should
72
+ // still be storable (it is the most expensive thing to recompute), but it
73
+ // must not drag the total past the cap alongside its neighbours.
74
+ while (totalBytes() > MAX_TOTAL_BYTES && _agents.size > 1) {
75
+ const oldest = _agents.keys().next().value;
76
+ if (oldest === undefined)
77
+ break;
78
+ _agents.delete(oldest);
79
+ }
80
+ }
81
+ function sizeOf(messages) {
82
+ try {
83
+ return JSON.stringify(messages).length;
84
+ }
85
+ catch {
86
+ // A transcript that cannot be serialised (cycles shouldn't happen, but a
87
+ // crash here would take down a completed sub-task's result) is treated as
88
+ // large so it gets evicted early rather than silently counted as free.
89
+ return MAX_TOTAL_BYTES;
90
+ }
91
+ }
92
+ /** Store a finished sub-agent's transcript. Returns its resumable id. */
93
+ function rememberAgent(agentName, description, messages) {
94
+ const id = `agent_${++_counter}`;
95
+ const now = Date.now();
96
+ _agents.set(id, {
97
+ id,
98
+ agentName,
99
+ description,
100
+ messages,
101
+ createdAt: now,
102
+ updatedAt: now,
103
+ seq: ++_seq,
104
+ bytes: sizeOf(messages),
105
+ });
106
+ evictUntilWithinLimits();
107
+ return id;
108
+ }
109
+ /** Append a resumed run's new messages to an existing agent. */
110
+ function updateAgent(id, messages) {
111
+ const existing = _agents.get(id);
112
+ if (!existing)
113
+ return;
114
+ existing.messages = messages;
115
+ existing.updatedAt = Date.now();
116
+ existing.seq = ++_seq;
117
+ existing.bytes = sizeOf(messages);
118
+ // Re-insert so this becomes the most recently used entry, protecting an
119
+ // actively-continued agent from being evicted by a burst of new one-shots.
120
+ _agents.delete(id);
121
+ _agents.set(id, existing);
122
+ evictUntilWithinLimits();
123
+ }
124
+ function getAgent(id) {
125
+ return _agents.get(id);
126
+ }
127
+ /** Newest first — the order a "which agents can I resume?" list wants. */
128
+ function listAgents() {
129
+ return [..._agents.values()].sort((a, b) => b.seq - a.seq);
130
+ }
131
+ /** Test seam. Not exported through index.ts's public surface by intent. */
132
+ function _resetAgentRegistry() {
133
+ _agents.clear();
134
+ _counter = 0;
135
+ _seq = 0;
136
+ }
137
+ exports._limits = { MAX_AGENTS, MAX_TOTAL_BYTES };
138
+ //# sourceMappingURL=agentRegistry.js.map
@@ -20,6 +20,20 @@ export interface AgentType {
20
20
  */
21
21
  testFilesOnly?: boolean;
22
22
  }
23
+ /**
24
+ * A problem found while loading an agent definition.
25
+ *
26
+ * These used to be silently swallowed. Every one of them changed what an agent
27
+ * could DO — a mistyped tool name removed a capability, a missing frontmatter
28
+ * block removed the allowlist entirely — and the user was told nothing.
29
+ */
30
+ export interface AgentWarning {
31
+ /** Absolute path of the definition file the problem was found in. */
32
+ file: string;
33
+ /** Agent name, when one could be determined. */
34
+ agent: string;
35
+ message: string;
36
+ }
23
37
  /**
24
38
  * Discover all agent types. Precedence: project > global > plugin > builtin.
25
39
  *
@@ -30,6 +44,23 @@ export interface AgentType {
30
44
  * readdir order is not guaranteed to be stable across machines or platforms.
31
45
  */
32
46
  export declare function loadAgentTypes(workDir: string): AgentType[];
47
+ /**
48
+ * As `loadAgentTypes`, but also reports what was wrong with the definitions.
49
+ *
50
+ * Split in two so the common caller stays a one-liner while the CLI's `/agents`
51
+ * and the agent loop can surface problems. Warnings never hide an agent: a
52
+ * flawed definition still loads (fail-closed on PERMISSIONS, not on existence),
53
+ * because making a user's agent vanish over a typo is its own kind of silent
54
+ * failure.
55
+ */
56
+ export declare function loadAgentTypesWithWarnings(workDir: string): {
57
+ types: AgentType[];
58
+ warnings: AgentWarning[];
59
+ };
60
+ /** The tool names a hand-written allowlist may use (exported for validation + tests). */
61
+ export declare function knownToolNames(): string[];
62
+ /** The built-in agents, for clients that want to show them alongside user-defined ones. */
63
+ export declare function builtinAgents(): AgentType[];
33
64
  /**
34
65
  * One line per agent for the system prompt's <available_subagents> block.
35
66
  *
@@ -1 +1 @@
1
- {"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpD;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AA4RD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAmB3D;AACD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,CAY1D;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAIjG"}
1
+ {"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"AAiCA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpD;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AA6QD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AA0JD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAqB5G;AAED,yFAAyF;AACzF,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,IAAI,SAAS,EAAE,CAE3C;AACD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,CAY1D;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAIjG"}
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadAgentTypes = loadAgentTypes;
37
+ exports.loadAgentTypesWithWarnings = loadAgentTypesWithWarnings;
38
+ exports.knownToolNames = knownToolNames;
39
+ exports.builtinAgents = builtinAgents;
37
40
  exports.summariseAgents = summariseAgents;
38
41
  exports.findAgentType = findAgentType;
39
42
  const fs = __importStar(require("fs"));
@@ -57,17 +60,38 @@ const index_1 = require("../plugins/index");
57
60
  const READ_ONLY_TOOLS = [
58
61
  // Universal
59
62
  'read_file', 'search_files', 'glob', 'list_directory', 'bash', 'bash_output',
63
+ // kill_shell belongs next to bash_output: an agent that can start a background
64
+ // process and poll it but never stop it leaks that process past its own
65
+ // lifetime. Stopping a shell you started is not a write to the repo.
66
+ 'kill_shell',
60
67
  'notebook_read', 'todo_write', 'todo_read',
68
+ // Skills are reusable prompt playbooks, and loop.ts advertises the skills
69
+ // catalogue to sub-agents at EVERY depth — so withholding the tool that loads
70
+ // one meant showing every sub-agent a menu it could not order from.
71
+ 'use_skill',
61
72
  // VS Code language server (ignored on the CLI)
62
73
  'get_symbols', 'get_workspace_symbols', 'find_references', 'go_to_definition',
63
74
  'get_hover', 'get_diagnostics',
64
75
  ];
65
- /** Read-only + the network, for agents that must consult external sources. */
66
- const RESEARCH_TOOLS = [...READ_ONLY_TOOLS, 'web_search', 'fetch_url'];
76
+ /**
77
+ * Read-only + the network, for agents that must consult external sources.
78
+ *
79
+ * `web_search` is deliberately ABSENT despite the name of this list. It is a
80
+ * SERVER-SIDE tool: Anthropic executes it and returns the result inside the same
81
+ * assistant message, so loop.ts filters those blocks out before the permission
82
+ * gate ever runs. Listing it here would be theatre — it grants nothing (agents
83
+ * without it can still search) and denies nothing. Naming the absence is the
84
+ * only way to stop it being "helpfully" re-added.
85
+ */
86
+ const RESEARCH_TOOLS = [...READ_ONLY_TOOLS, 'fetch_url'];
67
87
  /** Read-only + the write tools, for agents that produce code. */
68
88
  const WRITE_TOOLS = [
69
89
  ...READ_ONLY_TOOLS,
70
90
  'write_file', 'edit_file', 'multi_edit', 'create_directory', 'move_file', 'copy_file',
91
+ // notebook_edit is a write tool like any other, and allowsTestOnlyWrite already
92
+ // knows its shape (`source` is cell CONTENT, not a path). Omitting it just meant
93
+ // test-writer silently could not touch notebooks.
94
+ 'notebook_edit',
71
95
  ];
72
96
  const BUILTIN_AGENTS = [
73
97
  {
@@ -247,17 +271,43 @@ const BUILTIN_AGENTS = [
247
271
  ].join('\n'),
248
272
  },
249
273
  ];
274
+ /**
275
+ * Every tool name a client may offer, for validating a hand-written allowlist.
276
+ *
277
+ * Deliberately a SEPARATE list rather than an import from tools/executor.ts:
278
+ * that module's TOOL_MAP is the CLI's local dispatch table and legitimately
279
+ * lacks `get_diagnostics` (intercepted by VS Code before the executor) and
280
+ * `web_search` (run server-side by Anthropic). Validating against it would
281
+ * reject two perfectly valid names. A canary test asserts every name used by the
282
+ * builtin agents appears here, so the two cannot drift apart unnoticed.
283
+ */
284
+ const KNOWN_TOOL_NAMES = new Set([
285
+ 'read_file', 'write_file', 'edit_file', 'multi_edit', 'list_directory', 'create_directory',
286
+ 'move_file', 'copy_file', 'delete_file', 'search_files', 'glob',
287
+ 'bash', 'bash_output', 'kill_shell',
288
+ 'notebook_read', 'notebook_edit',
289
+ 'todo_write', 'todo_read', 'memory_write', 'memory_read', 'use_skill',
290
+ 'fetch_url', 'web_search', 'generate_image', 'stock_photo', 'open_in_browser',
291
+ 'task',
292
+ 'get_symbols', 'get_workspace_symbols', 'find_references', 'go_to_definition',
293
+ 'get_hover', 'get_diagnostics',
294
+ ]);
295
+ /** Frontmatter keys this parser understands, for typo detection. */
296
+ const KNOWN_META_KEYS = new Set([
297
+ 'name', 'description', 'tools', 'model', 'test_files_only', 'testfilesonly',
298
+ ]);
299
+ const VALID_MODELS = ['turbo', 'pro', 'ultra'];
250
300
  function parseFrontmatter(raw) {
251
301
  const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
252
302
  if (!m)
253
- return { meta: {}, body: raw.trim() };
303
+ return { meta: {}, body: raw.trim(), ok: false };
254
304
  const meta = {};
255
305
  for (const line of m[1].split(/\r?\n/)) {
256
306
  const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
257
307
  if (kv)
258
308
  meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
259
309
  }
260
- return { meta, body: (m[2] ?? '').trim() };
310
+ return { meta, body: (m[2] ?? '').trim(), ok: true };
261
311
  }
262
312
  function parseModel(v) {
263
313
  const s = (v ?? '').toLowerCase();
@@ -277,41 +327,118 @@ function parseToolList(v) {
277
327
  .filter(Boolean);
278
328
  return tools.length ? tools : undefined;
279
329
  }
280
- function loadDir(dir, source, into) {
330
+ function loadDir(dir, source, into, warnings) {
281
331
  let entries;
282
332
  try {
283
- entries = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
333
+ entries = fs.readdirSync(dir, { withFileTypes: true });
284
334
  }
285
335
  catch {
336
+ // Overwhelmingly "no .nexrall/agents here", which is the normal case and not
337
+ // worth a word. A genuine permission error is rare enough that the cost of
338
+ // staying quiet is lower than warning on every repo that has no agents.
286
339
  return;
287
340
  }
288
- for (const file of entries) {
341
+ for (const entry of entries) {
342
+ const full = path.join(dir, entry.name);
343
+ // Recurse into subfolders so definitions can be organised (agents/review/…),
344
+ // matching Claude Code. Identity still comes only from the name field / filename.
345
+ if (entry.isDirectory()) {
346
+ loadDir(full, source, into, warnings);
347
+ continue;
348
+ }
349
+ if (!entry.name.endsWith('.md'))
350
+ continue;
351
+ let raw;
289
352
  try {
290
- const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
291
- const { meta, body } = parseFrontmatter(raw);
292
- const name = (meta.name || path.basename(file, '.md')).trim();
293
- if (!name)
294
- continue;
295
- // Earlier tiers win (project > global > plugin).
296
- if (source !== 'project' && into.has(name))
297
- continue;
298
- into.set(name, {
299
- name,
300
- description: meta.description || `Custom ${name} agent`,
301
- tools: parseToolList(meta.tools),
302
- model: parseModel(meta.model),
303
- prompt: body,
304
- source,
305
- // Exposed to user/plugin definitions too `test_files_only: true` (or
306
- // `testFilesOnly`) lets anyone build a test-writing agent that genuinely
307
- // cannot touch production source, rather than only the builtin getting
308
- // that guarantee.
309
- ...(parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}),
353
+ raw = fs.readFileSync(full, 'utf-8');
354
+ }
355
+ catch (err) {
356
+ warnings.push({ file: full, agent: path.basename(entry.name, '.md'), message: `could not be read (${err.message}) — this agent was skipped` });
357
+ continue;
358
+ }
359
+ const { meta, body, ok } = parseFrontmatter(raw);
360
+ const name = (meta.name || path.basename(entry.name, '.md')).trim();
361
+ if (!name)
362
+ continue;
363
+ // Earlier tiers win (project > global > plugin).
364
+ if (source !== 'project' && into.has(name))
365
+ continue;
366
+ const tools = parseToolList(meta.tools);
367
+ let effectiveTools = tools;
368
+ // ── FAIL CLOSED on a definition we could not parse ────────────────────────
369
+ //
370
+ // A file with no `---` frontmatter block used to yield tools === undefined,
371
+ // which means "no allowlist" — i.e. FULL access, including write_file, bash
372
+ // and delete_file. So the worse the file, the more power it got: the exact
373
+ // inversion you do not want in a permission system. A malformed definition
374
+ // is now read-only, which is both the safe reading and, for anyone hand-
375
+ // writing an agent, almost always the intended one.
376
+ if (!ok) {
377
+ effectiveTools = READ_ONLY_TOOLS;
378
+ warnings.push({
379
+ file: full,
380
+ agent: name,
381
+ message: 'has no valid YAML frontmatter (a `---` block must be the first thing in the file), so no ' +
382
+ 'tool allowlist could be read. Treating it as READ-ONLY. Add frontmatter with a `tools:` line ' +
383
+ 'to grant more.',
310
384
  });
311
385
  }
312
- catch {
313
- /* skip unreadable / malformed definitions */
386
+ else {
387
+ if (!meta.description) {
388
+ warnings.push({
389
+ file: full,
390
+ agent: name,
391
+ message: 'has no `description:` — that text is the ONLY thing the model uses to decide when to delegate to this agent, so it will rarely be picked.',
392
+ });
393
+ }
394
+ if (meta.model !== undefined && parseModel(meta.model) === undefined) {
395
+ warnings.push({
396
+ file: full,
397
+ agent: name,
398
+ message: `has model: "${meta.model}", which is not valid — use one of ${VALID_MODELS.join(', ')}, or omit the line to inherit the current session's model.`,
399
+ });
400
+ }
401
+ // The highest-value warning of the lot. An allowlist only ever GRANTS, so a
402
+ // misspelled name is not an error anywhere — the tool is simply never
403
+ // permitted, and the model is told "Permission denied", which points it at
404
+ // the user rather than at the typo.
405
+ const unknown = (tools ?? []).filter((t) => !KNOWN_TOOL_NAMES.has(t) && !t.includes('__'));
406
+ if (unknown.length) {
407
+ warnings.push({
408
+ file: full,
409
+ agent: name,
410
+ message: `lists unknown tool name(s): ${unknown.join(', ')}. An allowlist only grants, so these silently do nothing and the agent cannot use them. Tool names are lower_snake_case (read_file, search_files, glob, bash).`,
411
+ });
412
+ }
413
+ const strayKeys = Object.keys(meta).filter((k) => !KNOWN_META_KEYS.has(k));
414
+ if (strayKeys.length) {
415
+ warnings.push({
416
+ file: full,
417
+ agent: name,
418
+ message: `has unrecognised frontmatter key(s): ${strayKeys.join(', ')} — these are ignored.`,
419
+ });
420
+ }
421
+ if (!body) {
422
+ warnings.push({
423
+ file: full,
424
+ agent: name,
425
+ message: 'has an empty body — the text below the frontmatter IS the agent\'s system prompt, so it currently has no instructions.',
426
+ });
427
+ }
314
428
  }
429
+ into.set(name, {
430
+ name,
431
+ description: meta.description || `Custom ${name} agent`,
432
+ tools: effectiveTools,
433
+ model: parseModel(meta.model),
434
+ prompt: body,
435
+ source,
436
+ // Exposed to user/plugin definitions too — `test_files_only: true` (or
437
+ // `testFilesOnly`) lets anyone build a test-writing agent that genuinely
438
+ // cannot touch production source, rather than only the builtin getting
439
+ // that guarantee.
440
+ ...(parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}),
441
+ });
315
442
  }
316
443
  }
317
444
  /**
@@ -324,11 +451,24 @@ function loadDir(dir, source, into) {
324
451
  * readdir order is not guaranteed to be stable across machines or platforms.
325
452
  */
326
453
  function loadAgentTypes(workDir) {
454
+ return loadAgentTypesWithWarnings(workDir).types;
455
+ }
456
+ /**
457
+ * As `loadAgentTypes`, but also reports what was wrong with the definitions.
458
+ *
459
+ * Split in two so the common caller stays a one-liner while the CLI's `/agents`
460
+ * and the agent loop can surface problems. Warnings never hide an agent: a
461
+ * flawed definition still loads (fail-closed on PERMISSIONS, not on existence),
462
+ * because making a user's agent vanish over a typo is its own kind of silent
463
+ * failure.
464
+ */
465
+ function loadAgentTypesWithWarnings(workDir) {
327
466
  const out = new Map();
328
- loadDir(path.join(workDir, '.nexrall', 'agents'), 'project', out);
329
- loadDir(path.join(os.homedir(), '.nexrall', 'agents'), 'global', out);
467
+ const warnings = [];
468
+ loadDir(path.join(workDir, '.nexrall', 'agents'), 'project', out, warnings);
469
+ loadDir(path.join(os.homedir(), '.nexrall', 'agents'), 'global', out, warnings);
330
470
  for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'agents'))
331
- loadDir(dir, 'plugin', out);
471
+ loadDir(dir, 'plugin', out, warnings);
332
472
  for (const agent of BUILTIN_AGENTS) {
333
473
  if (!out.has(agent.name))
334
474
  out.set(agent.name, agent);
@@ -336,7 +476,7 @@ function loadAgentTypes(workDir) {
336
476
  // Builtins first in their declared order (the common, cache-friendly case), then
337
477
  // everything user-supplied alphabetically.
338
478
  const builtinOrder = new Map(BUILTIN_AGENTS.map((a, i) => [a.name, i]));
339
- return [...out.values()].sort((a, b) => {
479
+ const types = [...out.values()].sort((a, b) => {
340
480
  const ai = builtinOrder.get(a.name);
341
481
  const bi = builtinOrder.get(b.name);
342
482
  if (ai !== undefined && bi !== undefined)
@@ -347,6 +487,15 @@ function loadAgentTypes(workDir) {
347
487
  return 1;
348
488
  return a.name.localeCompare(b.name);
349
489
  });
490
+ return { types, warnings };
491
+ }
492
+ /** The tool names a hand-written allowlist may use (exported for validation + tests). */
493
+ function knownToolNames() {
494
+ return [...KNOWN_TOOL_NAMES].sort();
495
+ }
496
+ /** The built-in agents, for clients that want to show them alongside user-defined ones. */
497
+ function builtinAgents() {
498
+ return BUILTIN_AGENTS;
350
499
  }
351
500
  /**
352
501
  * One line per agent for the system prompt's <available_subagents> block.
@@ -6,6 +6,20 @@ export declare function resolveMaxIterations(optionValue: number | undefined, se
6
6
  * dozen lines.
7
7
  */
8
8
  export declare function createLimiter(max: number): <T>(fn: () => Promise<T>) => Promise<T>;
9
+ /**
10
+ * Thrown by a sub-agent's permission gate when the AGENT DEFINITION forbids a
11
+ * tool — as opposed to the user declining it.
12
+ *
13
+ * The distinction matters to the model, which is why this is an exception rather
14
+ * than a `false`: both used to collapse into "Permission denied by user", so an
15
+ * agent blocked by its own allowlist (very often a mistyped tool name) was told
16
+ * the human had refused. The rational response to that is to ask again, which
17
+ * can never succeed. Carrying a reason lets the tool_result say what is actually
18
+ * true and what to do instead.
19
+ */
20
+ export declare class ToolNotAllowedError extends Error {
21
+ constructor(message: string);
22
+ }
9
23
  /**
10
24
  * Reduce a sub-agent's message history to the text its parent should receive.
11
25
  *
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}