@semalt-ai/code 1.8.5 → 1.19.0

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 (146) hide show
  1. package/.claude/settings.local.json +6 -1
  2. package/.github/workflows/ci.yml +69 -0
  3. package/CLAUDE.md +1584 -26
  4. package/README.md +147 -3
  5. package/examples/embed.js +74 -0
  6. package/index.js +251 -10
  7. package/lib/agent.js +711 -104
  8. package/lib/api.js +213 -49
  9. package/lib/args.js +74 -2
  10. package/lib/audit.js +23 -1
  11. package/lib/background.js +584 -0
  12. package/lib/checkpoints.js +757 -0
  13. package/lib/commands/auth.js +94 -0
  14. package/lib/commands/chat-session.js +306 -0
  15. package/lib/commands/chat-slash.js +399 -0
  16. package/lib/commands/chat-turn.js +446 -0
  17. package/lib/commands/chat.js +403 -0
  18. package/lib/commands/custom.js +157 -0
  19. package/lib/commands/history-utils.js +66 -0
  20. package/lib/commands/index.js +268 -0
  21. package/lib/commands/mcp.js +113 -0
  22. package/lib/commands/oneshot.js +193 -0
  23. package/lib/commands/registry.js +269 -0
  24. package/lib/commands/tasks.js +89 -0
  25. package/lib/compact.js +87 -0
  26. package/lib/config.js +333 -11
  27. package/lib/constants.js +372 -3
  28. package/lib/deny.js +199 -0
  29. package/lib/doctor.js +160 -0
  30. package/lib/headless.js +167 -0
  31. package/lib/hooks.js +286 -0
  32. package/lib/images.js +264 -0
  33. package/lib/internals.js +49 -0
  34. package/lib/mcp/boundary.js +131 -0
  35. package/lib/mcp/client.js +270 -0
  36. package/lib/mcp/oauth.js +134 -0
  37. package/lib/memory.js +209 -0
  38. package/lib/metrics.js +37 -2
  39. package/lib/payload.js +54 -0
  40. package/lib/permission-rules.js +401 -0
  41. package/lib/permissions.js +100 -10
  42. package/lib/pricing.js +67 -0
  43. package/lib/proc.js +62 -0
  44. package/lib/prompts.js +84 -5
  45. package/lib/sandbox.js +568 -0
  46. package/lib/sdk.js +328 -0
  47. package/lib/secrets.js +211 -0
  48. package/lib/skills.js +223 -0
  49. package/lib/subagents.js +516 -0
  50. package/lib/tool_registry.js +2558 -0
  51. package/lib/tool_specs.js +222 -2
  52. package/lib/tools.js +272 -1020
  53. package/lib/ui/format.js +22 -1
  54. package/lib/ui/input-field.js +16 -7
  55. package/lib/ui/status-bar.js +79 -11
  56. package/lib/ui/theme.js +1 -0
  57. package/lib/ui/web-activity.js +218 -0
  58. package/lib/verify.js +229 -0
  59. package/lib/web-extract.js +213 -0
  60. package/lib/web-summarize.js +68 -0
  61. package/package.json +19 -4
  62. package/scripts/lint.js +57 -0
  63. package/test/agent-loop.test.js +389 -0
  64. package/test/background.test.js +414 -0
  65. package/test/chat.test.js +114 -0
  66. package/test/checkpoints-agent.test.js +181 -0
  67. package/test/checkpoints.test.js +650 -0
  68. package/test/command-registry.test.js +160 -0
  69. package/test/compact.test.js +116 -0
  70. package/test/completion-lazy.test.js +52 -0
  71. package/test/config-merge.test.js +324 -0
  72. package/test/config-quarantine.test.js +128 -0
  73. package/test/config-write-guard-allow-anywhere.test.js +56 -0
  74. package/test/config-write-guard-skip.test.js +46 -0
  75. package/test/config-write-guard.test.js +153 -0
  76. package/test/context-split.test.js +215 -0
  77. package/test/cost-doctor.test.js +142 -0
  78. package/test/custom-commands-chat.test.js +106 -0
  79. package/test/custom-commands.test.js +230 -0
  80. package/test/deny-windows.test.js +120 -0
  81. package/test/deny.test.js +83 -0
  82. package/test/download-allow-anywhere.test.js +66 -0
  83. package/test/download-confine.test.js +153 -0
  84. package/test/executors.test.js +362 -0
  85. package/test/extract-tool-calls.test.js +315 -0
  86. package/test/fetch-url-validation.test.js +219 -0
  87. package/test/fixtures/tool-calls.js +57 -0
  88. package/test/fixtures/web-page.js +91 -0
  89. package/test/git-tools.test.js +384 -0
  90. package/test/grep-glob-serialize.test.js +242 -0
  91. package/test/grep-glob.test.js +268 -0
  92. package/test/harness/README.md +57 -0
  93. package/test/harness/chat-harness.js +142 -0
  94. package/test/harness/memwarn-headless-child.js +65 -0
  95. package/test/harness/mock-llm.js +120 -0
  96. package/test/harness/mock-mcp-server.js +142 -0
  97. package/test/harness/sse-server.js +69 -0
  98. package/test/headless.test.js +203 -0
  99. package/test/history-utils.test.js +88 -0
  100. package/test/hooks-agent.test.js +238 -0
  101. package/test/hooks-verify-sandbox.test.js +232 -0
  102. package/test/hooks.test.js +216 -0
  103. package/test/http-get-user-agent.test.js +142 -0
  104. package/test/images-api.test.js +208 -0
  105. package/test/images.test.js +238 -0
  106. package/test/max-iterations.test.js +216 -0
  107. package/test/mcp-boundary.test.js +57 -0
  108. package/test/mcp-client.test.js +267 -0
  109. package/test/mcp-oauth.test.js +86 -0
  110. package/test/memory-truncation-warning.test.js +222 -0
  111. package/test/memory.test.js +198 -0
  112. package/test/native-dispatch.test.js +356 -0
  113. package/test/output-chokepoint.test.js +188 -0
  114. package/test/path-guards.test.js +134 -0
  115. package/test/payload.test.js +99 -0
  116. package/test/permission-rules-agent.test.js +210 -0
  117. package/test/permission-rules.test.js +297 -0
  118. package/test/permissions.test.js +163 -0
  119. package/test/plan-mode.test.js +167 -0
  120. package/test/read-paginate.test.js +275 -0
  121. package/test/readonly-tools.test.js +177 -0
  122. package/test/result-cap.test.js +233 -0
  123. package/test/sandbox-agent.test.js +147 -0
  124. package/test/sandbox-integration.test.js +216 -0
  125. package/test/sandbox.test.js +408 -0
  126. package/test/sdk.test.js +234 -0
  127. package/test/shell-output-cap.test.js +181 -0
  128. package/test/skills-chat.test.js +110 -0
  129. package/test/skills.test.js +295 -0
  130. package/test/smoke.test.js +68 -0
  131. package/test/status-bar-pause.test.js +164 -0
  132. package/test/stream-parser.test.js +147 -0
  133. package/test/subagents-agent.test.js +178 -0
  134. package/test/subagents.test.js +222 -0
  135. package/test/tool-registry.test.js +85 -0
  136. package/test/trim-budget.test.js +101 -0
  137. package/test/verify-agent.test.js +317 -0
  138. package/test/verify.test.js +141 -0
  139. package/test/web-activity-ordering.test.js +194 -0
  140. package/test/web-activity.test.js +207 -0
  141. package/test/web-data-extraction-guidance.test.js +71 -0
  142. package/test/web-extract.test.js +185 -0
  143. package/test/web-fetch-agent.test.js +291 -0
  144. package/test/web-fetch-mode.test.js +193 -0
  145. package/test/web-search.test.js +380 -0
  146. package/lib/commands.js +0 -1438
@@ -0,0 +1,516 @@
1
+ 'use strict';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Subagents — isolated child agent loops (Task 3.6)
5
+ // ---------------------------------------------------------------------------
6
+ //
7
+ // A subagent is a SECOND agent loop run with its OWN isolated message history.
8
+ // It exists to keep the parent's context clean: noisy work (research, reading
9
+ // large files, review) happens in the child, and ONLY the child's final result
10
+ // returns to the parent. The parent never absorbs the child's intermediate
11
+ // turns.
12
+ //
13
+ // This builds directly on the runAgentLoop factory (lib/agent.js): a child
14
+ // runner is just another createAgentRunner instance, wired with WRAPPED
15
+ // executors that enforce the child's allowed-tool set, and sharing the parent's
16
+ // permission manager (no privilege escalation).
17
+ //
18
+ // Security model (load-bearing, per Phase 0):
19
+ // * No privilege escalation — the child uses the SAME permissionManager as
20
+ // the parent, so it can never auto-approve anything the parent wouldn't.
21
+ // * Tool constraint — a child's tools are restricted to its allowed set (from
22
+ // a `.semalt/agents/<name>.md` definition or an inline `tools` list). The
23
+ // wrapped agentExecShell/agentExecFile HARD-REFUSE anything outside the set,
24
+ // which is the enforcement that holds for BOTH the XML and native paths.
25
+ // * No recursion — a child can never invoke `spawn_agent` itself.
26
+ // * Untrusted result — a subagent's returned text may include external data it
27
+ // read, so the parent fences it in the UNTRUSTED_EXTERNAL_CONTENT delimiter
28
+ // (lib/agent.js), exactly like http_get / MCP / hook output.
29
+ //
30
+ // Custom agent definitions live at `.semalt/agents/<name>.md` (project) and
31
+ // `~/.semalt-ai/agents/<name>.md` (global) with optional frontmatter
32
+ // (name / model / tools / description) and a Markdown body = the child's system
33
+ // prompt. Discovery mirrors skills (Task 3.5) and custom commands (Task 3.1).
34
+
35
+ const fs = require('fs');
36
+ const os = require('os');
37
+ const path = require('path');
38
+
39
+ const { TOOL_REGISTRY } = require('./tool_registry');
40
+
41
+ const DEFAULT_MAX_CONCURRENCY = 3;
42
+ const DEFAULT_MAX_ITERATIONS = 12;
43
+ const SPAWN_AGENT_TOOL = 'spawn_agent';
44
+
45
+ // ── Agent-definition discovery ─────────────────────────────────────────────
46
+
47
+ // Split a frontmatter list value: `read_file, grep shell` or `[a, b]` → [a,b].
48
+ function _splitList(val) {
49
+ if (Array.isArray(val)) return val.map((v) => String(v).trim()).filter(Boolean);
50
+ if (typeof val !== 'string') return [];
51
+ const s = val.trim().replace(/^\[|\]$/g, '');
52
+ return s.split(/[\s,]+/).map((x) => x.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
53
+ }
54
+
55
+ // Turn a name into a slash/command-safe slug (lowercase, hyphenated). Mirrors
56
+ // the skills slugifier so `Code Reviewer` → `code-reviewer`.
57
+ function _slugify(s) {
58
+ return String(s || '')
59
+ .trim()
60
+ .toLowerCase()
61
+ .replace(/[\s_]+/g, '-')
62
+ .replace(/[^a-z0-9-]/g, '')
63
+ .replace(/-+/g, '-')
64
+ .replace(/^-|-$/g, '');
65
+ }
66
+
67
+ // Parse optional `---`-delimited frontmatter at the top of an agent definition.
68
+ // Recognized keys: name, model, tools (a.k.a. allowed-tools), description. The
69
+ // body is the child's system prompt. Pure. With no frontmatter the whole text
70
+ // is the body.
71
+ function parseAgentFrontmatter(text) {
72
+ const meta = { name: '', model: '', description: '', tools: [] };
73
+ if (typeof text !== 'string') return { meta, body: '' };
74
+ const src = text.replace(/^/, '').replace(/\r\n/g, '\n');
75
+ const m = /^---\n([\s\S]*?)\n---[ \t]*\n?/.exec(src);
76
+ if (!m) return { meta, body: src };
77
+ const body = src.slice(m[0].length);
78
+ for (const rawLine of m[1].split('\n')) {
79
+ const line = rawLine.trim();
80
+ if (!line || line.startsWith('#')) continue;
81
+ const idx = line.indexOf(':');
82
+ if (idx < 0) continue;
83
+ const key = line.slice(0, idx).trim().toLowerCase();
84
+ const val = line.slice(idx + 1).trim().replace(/^['"]|['"]$/g, '');
85
+ if (key === 'name') meta.name = val;
86
+ else if (key === 'model') meta.model = val;
87
+ else if (key === 'description') meta.description = val;
88
+ else if (key === 'tools' || key === 'allowed-tools' || key === 'allowed_tools') meta.tools = _splitList(val);
89
+ }
90
+ return { meta, body };
91
+ }
92
+
93
+ // Walk up from startDir for the nearest `.semalt/agents` directory, bounded by
94
+ // the repo root (the directory holding `.git` is the last one checked). Mirrors
95
+ // findProjectSkillsDir (Task 3.5). Returns the directory path or null.
96
+ function findProjectAgentsDir(startDir) {
97
+ let dir = path.resolve(startDir);
98
+ while (true) {
99
+ const candidate = path.join(dir, '.semalt', 'agents');
100
+ try { if (fs.statSync(candidate).isDirectory()) return candidate; } catch {}
101
+ let atRepoRoot = false;
102
+ try { atRepoRoot = fs.existsSync(path.join(dir, '.git')); } catch {}
103
+ if (atRepoRoot) break;
104
+ const parent = path.dirname(dir);
105
+ if (parent === dir) break;
106
+ dir = parent;
107
+ }
108
+ return null;
109
+ }
110
+
111
+ // Read every `<name>.md` agent definition under `dir` into a def list, sorted by
112
+ // filename for deterministic order. Unreadable files are skipped.
113
+ function loadAgentDefsFromDir(dir, source) {
114
+ const out = [];
115
+ let entries;
116
+ try { entries = fs.readdirSync(dir); } catch { return out; }
117
+ for (const entry of entries.slice().sort()) {
118
+ if (!/\.md$/i.test(entry)) continue;
119
+ const file = path.join(dir, entry);
120
+ let raw;
121
+ try {
122
+ if (!fs.statSync(file).isFile()) continue;
123
+ raw = fs.readFileSync(file, 'utf8');
124
+ } catch { continue; }
125
+ const { meta, body } = parseAgentFrontmatter(raw);
126
+ const base = entry.replace(/\.md$/i, '');
127
+ const slug = _slugify(meta.name || base);
128
+ if (!slug) continue;
129
+ out.push({
130
+ name: meta.name || base,
131
+ slug,
132
+ model: meta.model || '',
133
+ tools: meta.tools || [],
134
+ description: meta.description || '',
135
+ systemPrompt: (body || '').trim(),
136
+ source,
137
+ file,
138
+ });
139
+ }
140
+ return out;
141
+ }
142
+
143
+ // Discover agent definitions for a (home, cwd). Global defs load first, then the
144
+ // nearest project defs; on a slug collision project wins. home/cwd injectable.
145
+ function discoverAgentDefs(opts = {}) {
146
+ const home = opts.home || os.homedir();
147
+ const cwd = opts.cwd || process.cwd();
148
+ const global = loadAgentDefsFromDir(path.join(home, '.semalt-ai', 'agents'), 'global');
149
+ const projectDir = findProjectAgentsDir(cwd);
150
+ const project = projectDir ? loadAgentDefsFromDir(projectDir, 'project') : [];
151
+ const bySlug = new Map();
152
+ for (const d of project) if (!bySlug.has(d.slug)) bySlug.set(d.slug, d);
153
+ for (const d of global) if (!bySlug.has(d.slug)) bySlug.set(d.slug, d);
154
+ return Array.from(bySlug.values());
155
+ }
156
+
157
+ // ── Allowed-tool resolution ────────────────────────────────────────────────
158
+
159
+ // identifier (tag / specName / canonical action) → canonical action, built once
160
+ // from the static registry so a def listing `read_file` or `grep` resolves to
161
+ // the action tuple[0] the loop dispatches on.
162
+ let _idMap = null;
163
+ function _identifierMap() {
164
+ if (_idMap) return _idMap;
165
+ _idMap = new Map();
166
+ for (const e of TOOL_REGISTRY) {
167
+ const ids = new Set([e.tool, ...(e.specNames || []), ...(e.tags || [])]);
168
+ for (const id of ids) _idMap.set(String(id).toLowerCase(), e.tool);
169
+ }
170
+ return _idMap;
171
+ }
172
+
173
+ // Resolve a list of tool identifiers to a Set of canonical actions the child may
174
+ // run. `null`/empty → null, meaning "inherit all of the parent's tools" (still
175
+ // bounded by the shared permission manager). `*`/`all` also means inherit-all.
176
+ // `spawn_agent` is always dropped (no recursion). Unknown names are kept verbatim
177
+ // so dynamic/MCP tool names still pass through.
178
+ function resolveAllowedActions(list) {
179
+ if (!list || !list.length) return null;
180
+ const map = _identifierMap();
181
+ const set = new Set();
182
+ for (const name of list) {
183
+ const key = String(name || '').toLowerCase().trim();
184
+ if (!key) continue;
185
+ if (key === '*' || key === 'all') return null;
186
+ if (key === SPAWN_AGENT_TOOL) continue;
187
+ set.add(map.get(key) || key);
188
+ }
189
+ return set;
190
+ }
191
+
192
+ // ── Result formatting ──────────────────────────────────────────────────────
193
+
194
+ function _lastAssistantText(messages) {
195
+ if (!Array.isArray(messages)) return '';
196
+ for (let i = messages.length - 1; i >= 0; i--) {
197
+ const m = messages[i];
198
+ if (m && m.role === 'assistant' && typeof m.content === 'string' && m.content.trim()) {
199
+ return m.content.trim();
200
+ }
201
+ }
202
+ return '';
203
+ }
204
+
205
+ function _formatResults(results, parallel) {
206
+ if (!results.length) return '(no subagents were run)';
207
+ if (results.length === 1 && !parallel) {
208
+ const r = results[0];
209
+ return r.error ? `Subagent failed: ${r.error}` : (r.output || '(subagent produced no output)');
210
+ }
211
+ return results.map((r, i) => {
212
+ const head = `--- Subagent ${i + 1}${r.label ? ` (${r.label})` : ''} ---`;
213
+ const bodyText = r.error ? `failed: ${r.error}` : (r.output || '(no output)');
214
+ return `${head}\n${bodyText}`;
215
+ }).join('\n\n');
216
+ }
217
+
218
+ // ── Manager ────────────────────────────────────────────────────────────────
219
+
220
+ // Build the subagent manager. `deps` carries the same collaborators the agent
221
+ // runner needs (chatStream / extractToolCalls / agentExecShell / agentExecFile /
222
+ // describePermission / permissionManager / ui / getConfig) so it can construct a
223
+ // constrained CHILD runner per spawn. `agentDefs` are the discovered custom
224
+ // definitions. `createRunner` and `runChild` are injectable seams for tests.
225
+ function createSubagentManager(deps = {}) {
226
+ const {
227
+ chatStream,
228
+ extractToolCalls,
229
+ agentExecShell,
230
+ agentExecFile,
231
+ describePermission,
232
+ permissionManager,
233
+ ui,
234
+ getConfig,
235
+ agentDefs = [],
236
+ maxConcurrency = DEFAULT_MAX_CONCURRENCY,
237
+ maxIterations = DEFAULT_MAX_ITERATIONS,
238
+ createRunner = null,
239
+ runChild = null,
240
+ } = deps;
241
+
242
+ const _concurrency = Math.max(1, parseInt(maxConcurrency, 10) || DEFAULT_MAX_CONCURRENCY);
243
+
244
+ const _defsBySlug = new Map();
245
+ for (const d of agentDefs) if (d && d.slug && !_defsBySlug.has(d.slug)) _defsBySlug.set(d.slug, d);
246
+
247
+ function _resolveRunner() {
248
+ if (typeof createRunner === 'function') return createRunner;
249
+ // Lazy require to avoid a load-time cycle (agent.js does not require this).
250
+ return require('./agent').createAgentRunner;
251
+ }
252
+
253
+ // Normalize a raw spawn spec (string prompt or an object) into a concrete
254
+ // child-run plan: prompt, model, system prompt, allowed-action set, label.
255
+ function resolveSpec(raw) {
256
+ let r = raw;
257
+ if (typeof raw === 'string') r = { prompt: raw };
258
+ r = (r && typeof r === 'object') ? r : {};
259
+
260
+ const prompt = String(r.prompt != null ? r.prompt : (r.task != null ? r.task : '')).trim();
261
+ const agentName = r.agent || r.agent_type || r.name || '';
262
+ const def = agentName ? (_defsBySlug.get(_slugify(agentName)) || null) : null;
263
+
264
+ const cfg = (getConfig ? getConfig() : {}) || {};
265
+ const model = r.model || (def && def.model) || cfg.default_model || 'default';
266
+
267
+ const sysOverride = r.system_prompt || r.systemPrompt || (def && def.systemPrompt) || null;
268
+
269
+ let toolList = null;
270
+ if (Array.isArray(r.tools)) toolList = r.tools;
271
+ else if (typeof r.tools === 'string') toolList = _splitList(r.tools);
272
+ else if (def && def.tools && def.tools.length) toolList = def.tools;
273
+ const allowedActions = resolveAllowedActions(toolList);
274
+
275
+ const label = r.label || (def && def.name) || agentName || 'subagent';
276
+ const iters = Number.isInteger(r.max_iterations) && r.max_iterations > 0 ? r.max_iterations : maxIterations;
277
+
278
+ return {
279
+ prompt,
280
+ agentName: def ? def.name : (agentName || ''),
281
+ model,
282
+ systemPrompt: sysOverride ? String(sysOverride) : null,
283
+ allowedActions,
284
+ label,
285
+ maxIterations: iters,
286
+ def,
287
+ };
288
+ }
289
+
290
+ // Build a CHILD agent runner whose executors enforce `allowedActions` and can
291
+ // never spawn further subagents, sharing the parent's permission manager.
292
+ function _buildChildRunner(allowedActions) {
293
+ // Tool constraint is enforced at the EXECUTOR (not by filtering parse
294
+ // output): a hard refusal here holds for BOTH the XML and native paths, and
295
+ // gives the child feedback so it can adapt to an allowed tool instead of
296
+ // silently ending.
297
+ const isAllowed = (action) => {
298
+ if (action === SPAWN_AGENT_TOOL) return false; // no recursion, ever
299
+ if (!allowedActions) return true; // inherit-all (still permission-bounded)
300
+ return allowedActions.has(action);
301
+ };
302
+
303
+ const childExecShell = (command, options) => {
304
+ if (!isAllowed('shell')) {
305
+ return Promise.resolve({ exit_code: -1, stdout: '', stderr: 'shell is not permitted for this subagent', blocked: true });
306
+ }
307
+ return agentExecShell(command, options);
308
+ };
309
+
310
+ const childExecFile = (action, ...rest) => {
311
+ if (!isAllowed(action)) {
312
+ return Promise.resolve({ error: `tool "${action}" is not permitted for this subagent` });
313
+ }
314
+ return agentExecFile(action, ...rest);
315
+ };
316
+
317
+ // Lifecycle hooks are about the USER turn, not internal sub-loops — give the
318
+ // child a no-op runner so Stop/UserPromptSubmit don't double-fire.
319
+ const noopHooks = { run: async () => ({ feedback: [], blocked: false, blockReason: '' }) };
320
+
321
+ const create = _resolveRunner();
322
+ return create({
323
+ chatStream,
324
+ extractToolCalls,
325
+ agentExecShell: childExecShell,
326
+ agentExecFile: childExecFile,
327
+ describePermission,
328
+ permissionManager,
329
+ ui,
330
+ getConfig,
331
+ hooks: noopHooks,
332
+ });
333
+ }
334
+
335
+ // Run one subagent through a fresh, isolated message history and return only
336
+ // its final assistant text.
337
+ async function _defaultRunChild(spec, opts = {}) {
338
+ const runner = _buildChildRunner(spec.allowedActions);
339
+ const childMessages = [{ role: 'user', content: spec.prompt || '(no task provided)' }];
340
+ const signal = opts.signal || null;
341
+ const result = await runner.runAgentLoop(childMessages, spec.model, spec.maxIterations, null, {
342
+ // A bare onError keeps any warning/abort path from calling array methods
343
+ // (messages.sysWarn) that only exist on the chat's message wrapper.
344
+ callbacks: { onError: () => {} },
345
+ systemPrompt: spec.systemPrompt, // null → child uses the default system prompt
346
+ getAbortFlag: signal ? () => signal.aborted : null,
347
+ // Self-verification (Task 4.2) is a TOP-LEVEL gate on the user's task. A
348
+ // child declaring its sub-task done must not trigger the configured verify
349
+ // command — that belongs to the parent's final "done", not each subagent.
350
+ noVerify: true,
351
+ });
352
+ return _lastAssistantText(result && result.messages ? result.messages : childMessages);
353
+ }
354
+
355
+ async function runOne(raw, opts = {}) {
356
+ const spec = resolveSpec(raw);
357
+ const exec = typeof runChild === 'function' ? runChild : _defaultRunChild;
358
+ let output = '';
359
+ let error = null;
360
+ try {
361
+ output = await exec(spec, opts);
362
+ } catch (err) {
363
+ error = err && err.message ? err.message : String(err);
364
+ }
365
+ return { label: spec.label, agent: spec.agentName || '', model: spec.model, output: output || '', error };
366
+ }
367
+
368
+ // Run many subagents with bounded concurrency (a fixed-size worker pool).
369
+ async function runMany(rawSpecs, opts = {}) {
370
+ const specs = Array.isArray(rawSpecs) ? rawSpecs : [rawSpecs];
371
+ if (!specs.length) return [];
372
+ const limit = Math.max(1, Math.min(_concurrency, specs.length));
373
+ const results = new Array(specs.length);
374
+ let next = 0;
375
+ async function worker() {
376
+ while (true) {
377
+ const i = next++;
378
+ if (i >= specs.length) return;
379
+ results[i] = await runOne(specs[i], opts);
380
+ }
381
+ }
382
+ const workers = [];
383
+ for (let i = 0; i < limit; i++) workers.push(worker());
384
+ await Promise.all(workers);
385
+ return results;
386
+ }
387
+
388
+ // Tool entry point. Accepts a single spec or a `{ tasks: [...] }` / array form
389
+ // for bounded-parallel execution. Returns the subagent envelope the agent loop
390
+ // fences as untrusted.
391
+ async function spawn(params, opts = {}) {
392
+ let specs;
393
+ let parallel = false;
394
+ if (params && Array.isArray(params.tasks) && params.tasks.length) {
395
+ specs = params.tasks;
396
+ parallel = true;
397
+ } else if (Array.isArray(params)) {
398
+ specs = params;
399
+ parallel = true;
400
+ } else {
401
+ specs = [params];
402
+ }
403
+ const results = await runMany(specs, opts);
404
+ return { subagent: true, content: _formatResults(results, parallel), count: results.length, results };
405
+ }
406
+
407
+ return {
408
+ spawn,
409
+ runOne,
410
+ runMany,
411
+ resolveSpec,
412
+ listAgents: () => agentDefs.slice(),
413
+ permissionManager, // exposed so callers/tests can confirm no new manager was built
414
+ maxConcurrency: _concurrency,
415
+ };
416
+ }
417
+
418
+ // ── spawn_agent dynamic tool entry ─────────────────────────────────────────
419
+
420
+ function _parseSpawnXml(text, name) {
421
+ const out = [];
422
+ const re = new RegExp(`<${name}\\b([^>]*?)(?:\\/>|>([\\s\\S]*?)<\\/${name}>)`, 'g');
423
+ for (const m of text.matchAll(re)) {
424
+ const attrStr = m[1] || '';
425
+ const body = m[2] != null ? m[2] : '';
426
+ const attr = (k) => {
427
+ const mm = attrStr.match(new RegExp(`${k}="([^"]*)"`)) || attrStr.match(new RegExp(`${k}='([^']*)'`));
428
+ return mm ? mm[1] : null;
429
+ };
430
+ const trimmed = body.trim();
431
+ let params = {};
432
+ if (trimmed.startsWith('{')) {
433
+ try { params = JSON.parse(trimmed); } catch { params = { prompt: trimmed }; }
434
+ } else if (trimmed) {
435
+ params = { prompt: trimmed };
436
+ }
437
+ const agent = attr('agent') || attr('name');
438
+ const model = attr('model');
439
+ if (agent && !params.agent) params.agent = agent;
440
+ if (model && !params.model) params.model = model;
441
+ out.push([name, params]);
442
+ }
443
+ return out;
444
+ }
445
+
446
+ // Build the dynamic tool-registry entry for `spawn_agent`. Same shape as a
447
+ // static / MCP entry so it dispatches through the SAME agent loop. Registered at
448
+ // startup via lib/tool_registry.registerDynamicTool — kept out of the static
449
+ // parity check (lib/constants.js) like all dynamic tools.
450
+ function buildSpawnAgentEntry(manager) {
451
+ const name = SPAWN_AGENT_TOOL;
452
+ return {
453
+ tool: name,
454
+ subagent: true,
455
+ spec: {
456
+ description:
457
+ 'Launch a subagent with its OWN isolated context to handle a focused, ' +
458
+ 'noisy task (research, reading large files, review) and return ONLY its ' +
459
+ 'final result, keeping your context clean. Optionally pass `agent` to use ' +
460
+ 'a named definition from .semalt/agents, `model` to override the model, or ' +
461
+ '`tasks` (an array of task objects) to run several independent subagents ' +
462
+ 'in parallel with bounded concurrency.',
463
+ parameters: {
464
+ type: 'object',
465
+ properties: {
466
+ prompt: { type: 'string', description: 'The task/instructions for the subagent.' },
467
+ agent: { type: 'string', description: 'Optional named agent definition (.semalt/agents/<name>.md).' },
468
+ model: { type: 'string', description: 'Optional model override for the subagent.' },
469
+ tasks: {
470
+ type: 'array',
471
+ description: 'Optional independent subagent tasks to run in parallel. Each item is an object with the same fields as a single call.',
472
+ items: {
473
+ type: 'object',
474
+ properties: {
475
+ prompt: { type: 'string' },
476
+ agent: { type: 'string' },
477
+ model: { type: 'string' },
478
+ },
479
+ },
480
+ },
481
+ },
482
+ },
483
+ },
484
+ fromParams: (p) => [name, (p && typeof p === 'object') ? p : {}],
485
+ parseXml: (text) => _parseSpawnXml(text, name),
486
+ // Spawning a subagent runs a whole tool-use loop, so it REQUIRES approval by
487
+ // default — its tag is in no --allow-* tier, so a tier flag can never
488
+ // auto-launch one. The child's own tool calls are then gated by the SAME
489
+ // permission manager (no privilege escalation).
490
+ permission: (_ctx, args) => {
491
+ const p = (args && args[0]) || {};
492
+ const label = p.agent
493
+ ? `agent "${p.agent}"`
494
+ : (Array.isArray(p.tasks) ? `${p.tasks.length} parallel subagent(s)` : 'subagent');
495
+ return { actionType: 'agent', description: `Spawn ${label}`, tag: name };
496
+ },
497
+ execute: async (_ctx, args, options) => {
498
+ const params = (args && args[0]) || {};
499
+ const signal = (options && options.signal) || null;
500
+ return manager.spawn(params, { signal });
501
+ },
502
+ };
503
+ }
504
+
505
+ module.exports = {
506
+ DEFAULT_MAX_CONCURRENCY,
507
+ DEFAULT_MAX_ITERATIONS,
508
+ SPAWN_AGENT_TOOL,
509
+ parseAgentFrontmatter,
510
+ findProjectAgentsDir,
511
+ loadAgentDefsFromDir,
512
+ discoverAgentDefs,
513
+ resolveAllowedActions,
514
+ createSubagentManager,
515
+ buildSpawnAgentEntry,
516
+ };