pi-subagents 0.28.0 → 0.29.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.
- package/CHANGELOG.md +14 -0
- package/README.md +18 -61
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +4 -35
- package/src/agents/agent-management.ts +10 -20
- package/src/agents/agent-selection.ts +2 -0
- package/src/agents/agent-serializer.ts +0 -10
- package/src/agents/agents.ts +304 -47
- package/src/agents/chain-serializer.ts +4 -9
- package/src/extension/doctor.ts +4 -3
- package/src/extension/fanout-child.ts +0 -2
- package/src/extension/index.ts +3 -8
- package/src/extension/schemas.ts +32 -22
- package/src/intercom/intercom-bridge.ts +11 -1
- package/src/intercom/result-intercom.ts +0 -5
- package/src/runs/background/async-execution.ts +20 -11
- package/src/runs/background/run-status.ts +1 -7
- package/src/runs/background/subagent-runner.ts +81 -211
- package/src/runs/foreground/chain-execution.ts +62 -58
- package/src/runs/foreground/execution.ts +38 -343
- package/src/runs/foreground/subagent-executor.ts +28 -99
- package/src/runs/shared/acceptance.ts +605 -22
- package/src/runs/shared/completion-guard.ts +3 -26
- package/src/runs/shared/model-fallback.ts +38 -0
- package/src/runs/shared/parallel-utils.ts +6 -10
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
- package/src/runs/shared/workflow-graph.ts +2 -6
- package/src/shared/atomic-json.ts +68 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +10 -48
- package/src/shared/utils.ts +2 -8
- package/src/tui/render.ts +14 -29
- package/src/runs/shared/acceptance-contract.ts +0 -318
- package/src/runs/shared/acceptance-evaluation.ts +0 -221
- package/src/runs/shared/acceptance-finalization.ts +0 -173
- package/src/runs/shared/acceptance-reports.ts +0 -127
package/src/agents/agents.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Agent discovery and configuration
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
5
6
|
import * as fs from "node:fs";
|
|
6
7
|
import * as os from "node:os";
|
|
7
8
|
import * as path from "node:path";
|
|
@@ -17,7 +18,7 @@ export { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./
|
|
|
17
18
|
|
|
18
19
|
export type AgentScope = "user" | "project" | "both";
|
|
19
20
|
|
|
20
|
-
export type AgentSource = "builtin" | "user" | "project";
|
|
21
|
+
export type AgentSource = "builtin" | "package" | "user" | "project";
|
|
21
22
|
type SystemPromptMode = "append" | "replace";
|
|
22
23
|
export type AgentDefaultContext = "fresh" | "fork";
|
|
23
24
|
|
|
@@ -46,8 +47,6 @@ export interface BuiltinAgentOverrideBase {
|
|
|
46
47
|
skills?: string[];
|
|
47
48
|
tools?: string[];
|
|
48
49
|
mcpDirectTools?: string[];
|
|
49
|
-
maxExecutionTimeMs?: number;
|
|
50
|
-
maxTokens?: number;
|
|
51
50
|
completionGuard?: boolean;
|
|
52
51
|
}
|
|
53
52
|
|
|
@@ -63,8 +62,6 @@ interface BuiltinAgentOverrideConfig {
|
|
|
63
62
|
systemPrompt?: string;
|
|
64
63
|
skills?: string[] | false;
|
|
65
64
|
tools?: string[] | false;
|
|
66
|
-
maxExecutionTimeMs?: number | false;
|
|
67
|
-
maxTokens?: number | false;
|
|
68
65
|
completionGuard?: boolean;
|
|
69
66
|
}
|
|
70
67
|
|
|
@@ -98,8 +95,6 @@ export interface AgentConfig {
|
|
|
98
95
|
defaultProgress?: boolean;
|
|
99
96
|
interactive?: boolean;
|
|
100
97
|
maxSubagentDepth?: number;
|
|
101
|
-
maxExecutionTimeMs?: number;
|
|
102
|
-
maxTokens?: number;
|
|
103
98
|
completionGuard?: boolean;
|
|
104
99
|
disabled?: boolean;
|
|
105
100
|
extraFields?: Record<string, string>;
|
|
@@ -147,7 +142,7 @@ export interface ChainConfig {
|
|
|
147
142
|
}
|
|
148
143
|
|
|
149
144
|
export interface ChainDiscoveryDiagnostic {
|
|
150
|
-
source:
|
|
145
|
+
source: AgentSource;
|
|
151
146
|
filePath: string;
|
|
152
147
|
error: string;
|
|
153
148
|
}
|
|
@@ -161,6 +156,259 @@ function getUserChainDir(): string {
|
|
|
161
156
|
return path.join(getAgentDir(), "chains");
|
|
162
157
|
}
|
|
163
158
|
|
|
159
|
+
interface PackageSubagentPaths {
|
|
160
|
+
agents: string[];
|
|
161
|
+
chains: string[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let cachedGlobalNpmRoot: string | null = null;
|
|
165
|
+
|
|
166
|
+
function readJsonFileBestEffort(filePath: string): unknown {
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
169
|
+
} catch {
|
|
170
|
+
// Installed package scans are opportunistic; bad third-party manifests
|
|
171
|
+
// should not break local agent discovery.
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readOptionalJsonFile(filePath: string): unknown {
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
181
|
+
? (error as { code?: unknown }).code
|
|
182
|
+
: undefined;
|
|
183
|
+
if (code === "ENOENT") return null;
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isSafePackagePath(value: string): boolean {
|
|
189
|
+
return value.length > 0
|
|
190
|
+
&& !path.isAbsolute(value)
|
|
191
|
+
&& value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseNpmPackageName(source: string): string | undefined {
|
|
195
|
+
const spec = source.slice(4).trim();
|
|
196
|
+
if (!spec) return undefined;
|
|
197
|
+
const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/);
|
|
198
|
+
const packageName = match?.[1] ?? spec;
|
|
199
|
+
return isSafePackagePath(packageName) ? packageName : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stripGitRef(repoPath: string): string {
|
|
203
|
+
const atIndex = repoPath.indexOf("@");
|
|
204
|
+
const hashIndex = repoPath.indexOf("#");
|
|
205
|
+
const refIndex = [atIndex, hashIndex].filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
206
|
+
return refIndex === undefined ? repoPath : repoPath.slice(0, refIndex);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseGitPackagePath(source: string): { host: string; repoPath: string } | undefined {
|
|
210
|
+
const spec = source.slice(4).trim();
|
|
211
|
+
if (!spec) return undefined;
|
|
212
|
+
|
|
213
|
+
let host = "";
|
|
214
|
+
let repoPath = "";
|
|
215
|
+
const scpLike = spec.match(/^git@([^:]+):(.+)$/);
|
|
216
|
+
if (scpLike) {
|
|
217
|
+
host = scpLike[1] ?? "";
|
|
218
|
+
repoPath = scpLike[2] ?? "";
|
|
219
|
+
} else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(spec)) {
|
|
220
|
+
try {
|
|
221
|
+
const url = new URL(spec);
|
|
222
|
+
host = url.hostname;
|
|
223
|
+
repoPath = url.pathname.replace(/^\/+/, "");
|
|
224
|
+
} catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
const slashIndex = spec.indexOf("/");
|
|
229
|
+
if (slashIndex < 0) return undefined;
|
|
230
|
+
host = spec.slice(0, slashIndex);
|
|
231
|
+
repoPath = spec.slice(slashIndex + 1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const normalizedPath = stripGitRef(repoPath).replace(/\.git$/, "").replace(/^\/+/, "");
|
|
235
|
+
if (!host || !isSafePackagePath(host) || !isSafePackagePath(normalizedPath) || normalizedPath.split(/[\\/]/).length < 2) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
return { host, repoPath: normalizedPath };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolveSettingsPackageRoot(source: string, baseDir: string): string | undefined {
|
|
242
|
+
const trimmed = source.trim();
|
|
243
|
+
if (!trimmed) return undefined;
|
|
244
|
+
if (trimmed.startsWith("git:")) {
|
|
245
|
+
const parsed = parseGitPackagePath(trimmed);
|
|
246
|
+
return parsed ? path.join(baseDir, "git", parsed.host, parsed.repoPath) : undefined;
|
|
247
|
+
}
|
|
248
|
+
if (trimmed.startsWith("npm:")) {
|
|
249
|
+
const packageName = parseNpmPackageName(trimmed);
|
|
250
|
+
return packageName ? path.join(baseDir, "npm", "node_modules", packageName) : undefined;
|
|
251
|
+
}
|
|
252
|
+
const normalized = trimmed.startsWith("file:") ? trimmed.slice(5) : trimmed;
|
|
253
|
+
if (normalized === "~") return os.homedir();
|
|
254
|
+
if (normalized.startsWith("~/")) return path.join(os.homedir(), normalized.slice(2));
|
|
255
|
+
if (path.isAbsolute(normalized)) return normalized;
|
|
256
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("./") || normalized.startsWith("../")) {
|
|
257
|
+
return path.resolve(baseDir, normalized);
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getGlobalNpmRoot(): string | null {
|
|
263
|
+
if (cachedGlobalNpmRoot !== null) return cachedGlobalNpmRoot;
|
|
264
|
+
try {
|
|
265
|
+
cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim());
|
|
266
|
+
return cachedGlobalNpmRoot;
|
|
267
|
+
} catch {
|
|
268
|
+
cachedGlobalNpmRoot = "";
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function stringArray(value: unknown): string[] {
|
|
274
|
+
if (!Array.isArray(value)) return [];
|
|
275
|
+
return value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function extractSubagentPathsFromPackageRoot(packageRoot: string): PackageSubagentPaths {
|
|
279
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
280
|
+
const pkg = readJsonFileBestEffort(packageJsonPath);
|
|
281
|
+
if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return { agents: [], chains: [] };
|
|
282
|
+
|
|
283
|
+
const roots: Record<string, unknown>[] = [];
|
|
284
|
+
const piSubagents = (pkg as { "pi-subagents"?: unknown })["pi-subagents"];
|
|
285
|
+
if (piSubagents && typeof piSubagents === "object" && !Array.isArray(piSubagents)) {
|
|
286
|
+
roots.push(piSubagents as Record<string, unknown>);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const pi = (pkg as { pi?: unknown }).pi;
|
|
290
|
+
if (pi && typeof pi === "object" && !Array.isArray(pi)) {
|
|
291
|
+
const subagents = (pi as { subagents?: unknown }).subagents;
|
|
292
|
+
if (subagents && typeof subagents === "object" && !Array.isArray(subagents)) {
|
|
293
|
+
roots.push(subagents as Record<string, unknown>);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const agents: string[] = [];
|
|
298
|
+
const chains: string[] = [];
|
|
299
|
+
for (const root of roots) {
|
|
300
|
+
for (const entry of stringArray(root.agents)) agents.push(path.resolve(packageRoot, entry));
|
|
301
|
+
for (const entry of stringArray(root.chains)) chains.push(path.resolve(packageRoot, entry));
|
|
302
|
+
}
|
|
303
|
+
return { agents, chains };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function collectPackageRootsFromNodeModules(nodeModulesDir: string): string[] {
|
|
307
|
+
const roots: string[] = [];
|
|
308
|
+
if (!fs.existsSync(nodeModulesDir)) return roots;
|
|
309
|
+
|
|
310
|
+
let entries: fs.Dirent[];
|
|
311
|
+
try {
|
|
312
|
+
entries = fs.readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
313
|
+
} catch {
|
|
314
|
+
return roots;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for (const entry of entries) {
|
|
318
|
+
if (entry.name.startsWith(".")) continue;
|
|
319
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
320
|
+
|
|
321
|
+
if (entry.name.startsWith("@")) {
|
|
322
|
+
const scopeDir = path.join(nodeModulesDir, entry.name);
|
|
323
|
+
let scopeEntries: fs.Dirent[];
|
|
324
|
+
try {
|
|
325
|
+
scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
326
|
+
} catch {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
for (const scopeEntry of scopeEntries) {
|
|
330
|
+
if (scopeEntry.name.startsWith(".")) continue;
|
|
331
|
+
if (!scopeEntry.isDirectory() && !scopeEntry.isSymbolicLink()) continue;
|
|
332
|
+
roots.push(path.join(scopeDir, scopeEntry.name));
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
roots.push(path.join(nodeModulesDir, entry.name));
|
|
338
|
+
}
|
|
339
|
+
return roots;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function collectSettingsPackageRoots(settingsFile: string, baseDir: string): string[] {
|
|
343
|
+
const settings = readOptionalJsonFile(settingsFile);
|
|
344
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) return [];
|
|
345
|
+
const packages = (settings as { packages?: unknown }).packages;
|
|
346
|
+
if (!Array.isArray(packages)) return [];
|
|
347
|
+
|
|
348
|
+
const roots: string[] = [];
|
|
349
|
+
for (const entry of packages) {
|
|
350
|
+
const packageSource = typeof entry === "string"
|
|
351
|
+
? entry
|
|
352
|
+
: typeof entry === "object" && entry !== null && typeof (entry as { source?: unknown }).source === "string"
|
|
353
|
+
? (entry as { source: string }).source
|
|
354
|
+
: undefined;
|
|
355
|
+
if (!packageSource) continue;
|
|
356
|
+
const packageRoot = resolveSettingsPackageRoot(packageSource, baseDir);
|
|
357
|
+
if (packageRoot) roots.push(packageRoot);
|
|
358
|
+
}
|
|
359
|
+
return roots;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolean; includeProject: boolean } = { includeUser: true, includeProject: true }): PackageSubagentPaths {
|
|
363
|
+
const agentDir = getAgentDir();
|
|
364
|
+
const projectRoot = findNearestProjectRoot(cwd) ?? cwd;
|
|
365
|
+
const packageRoots = [
|
|
366
|
+
projectRoot,
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
if (options.includeProject) {
|
|
370
|
+
packageRoots.push(
|
|
371
|
+
...collectPackageRootsFromNodeModules(path.join(projectRoot, ".pi", "npm", "node_modules")),
|
|
372
|
+
...collectSettingsPackageRoots(path.join(projectRoot, ".pi", "settings.json"), path.join(projectRoot, ".pi")),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (options.includeUser) {
|
|
377
|
+
packageRoots.push(
|
|
378
|
+
...collectPackageRootsFromNodeModules(path.join(agentDir, "npm", "node_modules")),
|
|
379
|
+
...collectSettingsPackageRoots(path.join(agentDir, "settings.json"), agentDir),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (options.includeUser) {
|
|
384
|
+
const globalRoot = getGlobalNpmRoot();
|
|
385
|
+
if (globalRoot) packageRoots.push(...collectPackageRootsFromNodeModules(globalRoot));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const seenRoots = new Set<string>();
|
|
389
|
+
const seenAgents = new Set<string>();
|
|
390
|
+
const seenChains = new Set<string>();
|
|
391
|
+
const agents: string[] = [];
|
|
392
|
+
const chains: string[] = [];
|
|
393
|
+
for (const packageRoot of packageRoots) {
|
|
394
|
+
const resolvedRoot = path.resolve(packageRoot);
|
|
395
|
+
if (seenRoots.has(resolvedRoot)) continue;
|
|
396
|
+
seenRoots.add(resolvedRoot);
|
|
397
|
+
const paths = extractSubagentPathsFromPackageRoot(resolvedRoot);
|
|
398
|
+
for (const agentDir of paths.agents) {
|
|
399
|
+
if (seenAgents.has(agentDir)) continue;
|
|
400
|
+
seenAgents.add(agentDir);
|
|
401
|
+
agents.push(agentDir);
|
|
402
|
+
}
|
|
403
|
+
for (const chainDir of paths.chains) {
|
|
404
|
+
if (seenChains.has(chainDir)) continue;
|
|
405
|
+
seenChains.add(chainDir);
|
|
406
|
+
chains.push(chainDir);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return { agents, chains };
|
|
410
|
+
}
|
|
411
|
+
|
|
164
412
|
function splitToolList(rawTools: string[] | undefined): { tools?: string[]; mcpDirectTools?: string[] } {
|
|
165
413
|
const mcpDirectTools: string[] = [];
|
|
166
414
|
const tools: string[] = [];
|
|
@@ -209,8 +457,6 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
209
457
|
skills: agent.skills ? [...agent.skills] : undefined,
|
|
210
458
|
tools: agent.tools ? [...agent.tools] : undefined,
|
|
211
459
|
mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : undefined,
|
|
212
|
-
maxExecutionTimeMs: agent.maxExecutionTimeMs,
|
|
213
|
-
maxTokens: agent.maxTokens,
|
|
214
460
|
completionGuard: agent.completionGuard,
|
|
215
461
|
};
|
|
216
462
|
}
|
|
@@ -230,8 +476,6 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
|
|
|
230
476
|
...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
|
|
231
477
|
...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
|
|
232
478
|
...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
|
|
233
|
-
...(override.maxExecutionTimeMs !== undefined ? { maxExecutionTimeMs: override.maxExecutionTimeMs } : {}),
|
|
234
|
-
...(override.maxTokens !== undefined ? { maxTokens: override.maxTokens } : {}),
|
|
235
479
|
...(override.completionGuard !== undefined ? { completionGuard: override.completionGuard } : {}),
|
|
236
480
|
};
|
|
237
481
|
}
|
|
@@ -369,22 +613,6 @@ function parseBuiltinOverrideEntry(
|
|
|
369
613
|
}
|
|
370
614
|
}
|
|
371
615
|
|
|
372
|
-
if ("maxExecutionTimeMs" in input) {
|
|
373
|
-
if (input.maxExecutionTimeMs === false || (typeof input.maxExecutionTimeMs === "number" && Number.isInteger(input.maxExecutionTimeMs) && input.maxExecutionTimeMs >= 1)) {
|
|
374
|
-
override.maxExecutionTimeMs = input.maxExecutionTimeMs;
|
|
375
|
-
} else {
|
|
376
|
-
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'maxExecutionTimeMs'; expected an integer >= 1 or false.`);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if ("maxTokens" in input) {
|
|
381
|
-
if (input.maxTokens === false || (typeof input.maxTokens === "number" && Number.isInteger(input.maxTokens) && input.maxTokens >= 1)) {
|
|
382
|
-
override.maxTokens = input.maxTokens;
|
|
383
|
-
} else {
|
|
384
|
-
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'maxTokens'; expected an integer >= 1 or false.`);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
616
|
if ("completionGuard" in input) {
|
|
389
617
|
if (typeof input.completionGuard === "boolean") {
|
|
390
618
|
override.completionGuard = input.completionGuard;
|
|
@@ -465,8 +693,6 @@ function applyBuiltinOverride(
|
|
|
465
693
|
next.tools = tools;
|
|
466
694
|
next.mcpDirectTools = mcpDirectTools;
|
|
467
695
|
}
|
|
468
|
-
if (override.maxExecutionTimeMs !== undefined) next.maxExecutionTimeMs = override.maxExecutionTimeMs === false ? undefined : override.maxExecutionTimeMs;
|
|
469
|
-
if (override.maxTokens !== undefined) next.maxTokens = override.maxTokens === false ? undefined : override.maxTokens;
|
|
470
696
|
if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
|
|
471
697
|
|
|
472
698
|
return next;
|
|
@@ -507,7 +733,7 @@ function applyBuiltinOverrides(
|
|
|
507
733
|
|
|
508
734
|
export function buildBuiltinOverrideConfig(
|
|
509
735
|
base: BuiltinAgentOverrideBase,
|
|
510
|
-
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "
|
|
736
|
+
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "completionGuard">,
|
|
511
737
|
): BuiltinAgentOverrideConfig | undefined {
|
|
512
738
|
const override: BuiltinAgentOverrideConfig = {};
|
|
513
739
|
|
|
@@ -525,8 +751,6 @@ export function buildBuiltinOverrideConfig(
|
|
|
525
751
|
const baseTools = joinToolList(base);
|
|
526
752
|
const draftTools = joinToolList(draft);
|
|
527
753
|
if (!arraysEqual(draftTools, baseTools)) override.tools = draftTools ? [...draftTools] : false;
|
|
528
|
-
if (draft.maxExecutionTimeMs !== base.maxExecutionTimeMs) override.maxExecutionTimeMs = draft.maxExecutionTimeMs ?? false;
|
|
529
|
-
if (draft.maxTokens !== base.maxTokens) override.maxTokens = draft.maxTokens ?? false;
|
|
530
754
|
if ((draft.completionGuard !== false) !== (base.completionGuard !== false)) {
|
|
531
755
|
override.completionGuard = draft.completionGuard !== false;
|
|
532
756
|
}
|
|
@@ -695,8 +919,6 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
695
919
|
}
|
|
696
920
|
|
|
697
921
|
const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth);
|
|
698
|
-
const parsedMaxExecutionTimeMs = Number(frontmatter.maxExecutionTimeMs);
|
|
699
|
-
const parsedMaxTokens = Number(frontmatter.maxTokens);
|
|
700
922
|
const completionGuard = frontmatter.completionGuard === "false"
|
|
701
923
|
? false
|
|
702
924
|
: frontmatter.completionGuard === "true"
|
|
@@ -730,14 +952,6 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
730
952
|
Number.isInteger(parsedMaxSubagentDepth) && parsedMaxSubagentDepth >= 0
|
|
731
953
|
? parsedMaxSubagentDepth
|
|
732
954
|
: undefined,
|
|
733
|
-
maxExecutionTimeMs:
|
|
734
|
-
Number.isInteger(parsedMaxExecutionTimeMs) && parsedMaxExecutionTimeMs >= 1
|
|
735
|
-
? parsedMaxExecutionTimeMs
|
|
736
|
-
: undefined,
|
|
737
|
-
maxTokens:
|
|
738
|
-
Number.isInteger(parsedMaxTokens) && parsedMaxTokens >= 1
|
|
739
|
-
? parsedMaxTokens
|
|
740
|
-
: undefined,
|
|
741
955
|
completionGuard,
|
|
742
956
|
extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
|
|
743
957
|
});
|
|
@@ -746,7 +960,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
746
960
|
return agents;
|
|
747
961
|
}
|
|
748
962
|
|
|
749
|
-
function loadChainsFromDir(dir: string, source:
|
|
963
|
+
function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
|
|
750
964
|
const chains = new Map<string, ChainConfig>();
|
|
751
965
|
const diagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
752
966
|
|
|
@@ -808,6 +1022,22 @@ function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; pref
|
|
|
808
1022
|
}
|
|
809
1023
|
const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
|
|
810
1024
|
|
|
1025
|
+
export const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
|
|
1026
|
+
|
|
1027
|
+
// Additional read-only directories to scan for agent definitions, supplied by the
|
|
1028
|
+
// launcher via PI_SUBAGENT_EXTRA_AGENT_DIRS (PATH-style, split on os/path delimiter).
|
|
1029
|
+
// Lets a hermetic wrapper (e.g. a Nix-store install) expose bundled agents without
|
|
1030
|
+
// copying or symlinking them into the writable agent dir. Loaded as "user" source,
|
|
1031
|
+
// at lower precedence than agents the user placed in their own agent dir.
|
|
1032
|
+
function extraUserAgentDirs(): string[] {
|
|
1033
|
+
const raw = process.env[EXTRA_AGENT_DIRS_ENV];
|
|
1034
|
+
if (!raw) return [];
|
|
1035
|
+
return raw
|
|
1036
|
+
.split(path.delimiter)
|
|
1037
|
+
.map((dir) => dir.trim())
|
|
1038
|
+
.filter((dir) => dir.length > 0);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
811
1041
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
812
1042
|
const userDirOld = path.join(getAgentDir(), "agents");
|
|
813
1043
|
const userDirNew = path.join(os.homedir(), ".agents");
|
|
@@ -816,6 +1046,10 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
816
1046
|
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
817
1047
|
const userSettings = scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);
|
|
818
1048
|
const projectSettings = scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath);
|
|
1049
|
+
const packageSubagentPaths = collectPackageSubagentPaths(cwd, {
|
|
1050
|
+
includeUser: scope !== "project",
|
|
1051
|
+
includeProject: scope !== "user",
|
|
1052
|
+
});
|
|
819
1053
|
|
|
820
1054
|
const builtinAgents = applyBuiltinOverrides(
|
|
821
1055
|
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
@@ -825,12 +1059,14 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
825
1059
|
projectSettingsPath,
|
|
826
1060
|
);
|
|
827
1061
|
|
|
1062
|
+
const userAgentsExtra = scope === "project" ? [] : extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user"));
|
|
828
1063
|
const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
|
|
829
1064
|
const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
|
|
830
|
-
const userAgents = [...userAgentsOld, ...userAgentsNew];
|
|
1065
|
+
const userAgents = [...userAgentsExtra, ...userAgentsOld, ...userAgentsNew];
|
|
831
1066
|
|
|
832
1067
|
const projectAgents = scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project"));
|
|
833
|
-
const
|
|
1068
|
+
const packageAgents = packageSubagentPaths.agents.flatMap((dir) => loadAgentsFromDir(dir, "package"));
|
|
1069
|
+
const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
|
|
834
1070
|
.filter((agent) => agent.disabled !== true);
|
|
835
1071
|
|
|
836
1072
|
return { agents, projectAgentsDir };
|
|
@@ -838,6 +1074,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
|
|
|
838
1074
|
|
|
839
1075
|
export function discoverAgentsAll(cwd: string): {
|
|
840
1076
|
builtin: AgentConfig[];
|
|
1077
|
+
package: AgentConfig[];
|
|
841
1078
|
user: AgentConfig[];
|
|
842
1079
|
project: AgentConfig[];
|
|
843
1080
|
chains: ChainConfig[];
|
|
@@ -858,6 +1095,7 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
858
1095
|
const projectSettingsPath = getProjectAgentSettingsPath(cwd);
|
|
859
1096
|
const userSettings = readSubagentSettings(userSettingsPath);
|
|
860
1097
|
const projectSettings = readSubagentSettings(projectSettingsPath);
|
|
1098
|
+
const packageSubagentPaths = collectPackageSubagentPaths(cwd);
|
|
861
1099
|
|
|
862
1100
|
const builtin = applyBuiltinOverrides(
|
|
863
1101
|
loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
|
|
@@ -867,9 +1105,17 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
867
1105
|
projectSettingsPath,
|
|
868
1106
|
);
|
|
869
1107
|
const user = [
|
|
1108
|
+
...extraUserAgentDirs().flatMap((dir) => loadAgentsFromDir(dir, "user")),
|
|
870
1109
|
...loadAgentsFromDir(userDirOld, "user"),
|
|
871
1110
|
...loadAgentsFromDir(userDirNew, "user"),
|
|
872
1111
|
];
|
|
1112
|
+
const packageMap = new Map<string, AgentConfig>();
|
|
1113
|
+
for (const dir of packageSubagentPaths.agents) {
|
|
1114
|
+
for (const agent of loadAgentsFromDir(dir, "package")) {
|
|
1115
|
+
if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
const packageAgents = Array.from(packageMap.values());
|
|
873
1119
|
const projectMap = new Map<string, AgentConfig>();
|
|
874
1120
|
for (const dir of projectDirs) {
|
|
875
1121
|
for (const agent of loadAgentsFromDir(dir, "project")) {
|
|
@@ -879,6 +1125,15 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
879
1125
|
const project = Array.from(projectMap.values());
|
|
880
1126
|
|
|
881
1127
|
const chainMap = new Map<string, ChainConfig>();
|
|
1128
|
+
const packageChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
1129
|
+
const packageChainMap = new Map<string, ChainConfig>();
|
|
1130
|
+
for (const dir of packageSubagentPaths.chains) {
|
|
1131
|
+
const loaded = loadChainsFromDir(dir, "package");
|
|
1132
|
+
packageChainDiagnostics.push(...loaded.diagnostics);
|
|
1133
|
+
for (const chain of loaded.chains) {
|
|
1134
|
+
if (!packageChainMap.has(chain.name)) packageChainMap.set(chain.name, chain);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
882
1137
|
const projectChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
883
1138
|
for (const dir of projectChainDirs) {
|
|
884
1139
|
const loaded = loadChainsFromDir(dir, "project");
|
|
@@ -889,15 +1144,17 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
889
1144
|
}
|
|
890
1145
|
const userChains = loadChainsFromDir(userChainDir, "user");
|
|
891
1146
|
const chains = [
|
|
1147
|
+
...Array.from(packageChainMap.values()),
|
|
892
1148
|
...userChains.chains,
|
|
893
1149
|
...Array.from(chainMap.values()),
|
|
894
1150
|
];
|
|
895
1151
|
const chainDiagnostics = [
|
|
1152
|
+
...packageChainDiagnostics,
|
|
896
1153
|
...userChains.diagnostics,
|
|
897
1154
|
...projectChainDiagnostics,
|
|
898
1155
|
];
|
|
899
1156
|
|
|
900
1157
|
const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
|
|
901
1158
|
|
|
902
|
-
return { builtin, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
1159
|
+
return { builtin, package: packageAgents, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
903
1160
|
}
|
|
@@ -4,6 +4,7 @@ import { parseFrontmatter } from "./frontmatter.ts";
|
|
|
4
4
|
import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
|
|
5
5
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
6
6
|
import type { ChainStep } from "../shared/settings.ts";
|
|
7
|
+
import type { AgentSource } from "./agents.ts";
|
|
7
8
|
|
|
8
9
|
function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
9
10
|
const lines = sectionBody.split("\n");
|
|
@@ -83,7 +84,7 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
|
83
84
|
return step;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
export function parseChain(content: string, source:
|
|
87
|
+
export function parseChain(content: string, source: AgentSource, filePath: string): ChainConfig {
|
|
87
88
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
88
89
|
if (!frontmatter.name || !frontmatter.description) {
|
|
89
90
|
throw new Error("Chain frontmatter must include name and description");
|
|
@@ -124,7 +125,7 @@ export function parseChain(content: string, source: "user" | "project", filePath
|
|
|
124
125
|
};
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
export function parseJsonChain(content: string, source:
|
|
128
|
+
export function parseJsonChain(content: string, source: AgentSource, filePath: string): ChainConfig {
|
|
128
129
|
let parsed: unknown;
|
|
129
130
|
try {
|
|
130
131
|
parsed = JSON.parse(content);
|
|
@@ -151,17 +152,11 @@ export function parseJsonChain(content: string, source: "user" | "project", file
|
|
|
151
152
|
throw new Error(`JSON chain '${filePath}' step ${i + 1} must be an object.`);
|
|
152
153
|
}
|
|
153
154
|
const stepRecord = step as Record<string, unknown>;
|
|
154
|
-
const parallel = stepRecord.parallel;
|
|
155
|
-
if (Array.isArray(parallel) && Object.hasOwn(stepRecord, "acceptance")) {
|
|
156
|
-
throw new Error(`Invalid JSON chain '${filePath}': step ${i + 1} acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
|
|
157
|
-
}
|
|
158
|
-
if (parallel && typeof parallel === "object" && !Array.isArray(parallel) && Object.hasOwn(stepRecord, "acceptance")) {
|
|
159
|
-
throw new Error(`Invalid JSON chain '${filePath}': step ${i + 1} acceptance is not supported on dynamic fanout groups; set acceptance on the dynamic template.`);
|
|
160
|
-
}
|
|
161
155
|
const acceptanceErrors = validateAcceptanceInput(stepRecord.acceptance, `step ${i + 1} acceptance`);
|
|
162
156
|
if (acceptanceErrors.length > 0) {
|
|
163
157
|
throw new Error(`Invalid JSON chain '${filePath}': ${acceptanceErrors.join(" ")}`);
|
|
164
158
|
}
|
|
159
|
+
const parallel = stepRecord.parallel;
|
|
165
160
|
if (Array.isArray(parallel)) {
|
|
166
161
|
for (let taskIndex = 0; taskIndex < parallel.length; taskIndex++) {
|
|
167
162
|
const task = parallel[taskIndex];
|
package/src/extension/doctor.ts
CHANGED
|
@@ -81,7 +81,7 @@ function formatExistingDirectory(label: string, dirPath: string): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function formatSourceCounts(counts: Record<AgentSource, number>): string {
|
|
84
|
-
return `builtin ${counts.builtin}, user ${counts.user}, project ${counts.project}`;
|
|
84
|
+
return `builtin ${counts.builtin}, package ${counts.package}, user ${counts.user}, project ${counts.project}`;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
function formatSkillSourceCounts(skills: Array<{ source: SkillSource }>): string {
|
|
@@ -132,15 +132,16 @@ function formatDiscovery(input: DoctorReportInput, deps: DoctorDeps): string[] {
|
|
|
132
132
|
const discovered = deps.discoverAgentsAll(input.cwd);
|
|
133
133
|
const agentCounts = {
|
|
134
134
|
builtin: discovered.builtin.length,
|
|
135
|
+
package: discovered.package?.length ?? 0,
|
|
135
136
|
user: discovered.user.length,
|
|
136
137
|
project: discovered.project.length,
|
|
137
138
|
};
|
|
138
139
|
const chainCounts = discovered.chains.reduce<Record<AgentSource, number>>((counts, chain) => {
|
|
139
140
|
counts[chain.source] += 1;
|
|
140
141
|
return counts;
|
|
141
|
-
}, { builtin: 0, user: 0, project: 0 });
|
|
142
|
+
}, { builtin: 0, package: 0, user: 0, project: 0 });
|
|
142
143
|
return [
|
|
143
|
-
`- agents: total ${agentCounts.builtin + agentCounts.user + agentCounts.project} (${formatSourceCounts(agentCounts)})`,
|
|
144
|
+
`- agents: total ${agentCounts.builtin + agentCounts.package + agentCounts.user + agentCounts.project} (${formatSourceCounts(agentCounts)})`,
|
|
144
145
|
`- chains: total ${discovered.chains.length} (${formatSourceCounts(chainCounts)})`,
|
|
145
146
|
].join("\n");
|
|
146
147
|
}),
|
|
@@ -156,8 +156,6 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
156
156
|
label: "Subagent",
|
|
157
157
|
description: [
|
|
158
158
|
"Delegate to subagents from child-safe fanout mode.",
|
|
159
|
-
"For goal-style requests such as /goal, goal, active goal, or work until evidence says done, use explicit acceptance on the delegated run: criteria for the target, evidence/verify for proof, stopRules for constraints, and maxFinalizationTurns for the bounded loop.",
|
|
160
|
-
"For implementation handoffs from a plan, PRD, spec, issue, or broad fix, put implementation instructions and plan paths in task, and put the definition of done, evidence, verification commands, constraints, and loop cap in acceptance.",
|
|
161
159
|
"Allowed management/control actions: list, get, status, interrupt, resume, doctor.",
|
|
162
160
|
"Agent config mutation actions create, update, and delete are blocked in this mode.",
|
|
163
161
|
].join("\n"),
|
package/src/extension/index.ts
CHANGED
|
@@ -33,8 +33,7 @@ import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
|
|
|
33
33
|
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
|
|
34
34
|
import { inspectSubagentStatus } from "../runs/background/run-status.ts";
|
|
35
35
|
import registerSubagentNotify, { type SubagentNotifyDetails } from "../runs/background/notify.ts";
|
|
36
|
-
import { SUBAGENT_CHILD_ENV
|
|
37
|
-
import registerFanoutChildSubagentExtension from "./fanout-child.ts";
|
|
36
|
+
import { SUBAGENT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
38
37
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
39
38
|
import { loadConfig } from "./config.ts";
|
|
40
39
|
import {
|
|
@@ -209,7 +208,6 @@ class SubagentControlNoticeComponent implements Component {
|
|
|
209
208
|
|
|
210
209
|
export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
211
210
|
if (process.env[SUBAGENT_CHILD_ENV] === "1") {
|
|
212
|
-
if (process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1") registerFanoutChildSubagentExtension(pi);
|
|
213
211
|
return;
|
|
214
212
|
}
|
|
215
213
|
const globalStore = globalThis as Record<string, unknown>;
|
|
@@ -394,10 +392,7 @@ EXECUTION (use exactly ONE mode):
|
|
|
394
392
|
• SINGLE: { agent, task? } - one task; omit task for self-contained agents
|
|
395
393
|
• CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
|
|
396
394
|
• PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
397
|
-
• Foreground timeout: { timeoutMs } or { maxRuntimeMs } - wall-clock limit for foreground single, parallel, and chain runs. Timed-out children return timedOut:true with completed sibling/prior results preserved. Not for async/background runs.
|
|
398
395
|
• Optional context: { context: "fresh" | "fork" } (default: if any requested agent has defaultContext: "fork", the whole invocation uses fork; otherwise "fresh"; inspect agent defaults via { action: "list" })
|
|
399
|
-
• Goal-style requests: when the user says “/goal”, “goal”, “active goal”, “work until evidence says done”, or “verify against a goal”, model that as explicit acceptance. Use acceptance.criteria for the target, acceptance.evidence/verify for proof, acceptance.stopRules for constraints, and acceptance.maxFinalizationTurns for the bounded loop.
|
|
400
|
-
• Plan/spec implementation handoffs: when delegating a plan, PRD, spec, issue, or broad fix to an editing-capable child, prefer structured acceptance instead of burying validation requirements in task prose. Put the implementation instructions and plan paths in task; put the definition of done, evidence, verification commands, constraints, and loop cap in acceptance.
|
|
401
396
|
|
|
402
397
|
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
403
398
|
• {task} - The original task/request from the user
|
|
@@ -409,8 +404,8 @@ Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", t
|
|
|
409
404
|
MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
410
405
|
• { action: "list" } - discover executable agents/chains
|
|
411
406
|
• { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
|
|
412
|
-
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext,
|
|
413
|
-
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis",
|
|
407
|
+
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, ... } }
|
|
408
|
+
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
|
|
414
409
|
• { action: "delete", agent: "code-analysis.custom-agent" }
|
|
415
410
|
• Use chainName for chain operations; packaged chains also use dotted runtime names
|
|
416
411
|
|