pi-subagents 0.25.0 → 0.28.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 +34 -0
- package/README.md +175 -19
- package/package.json +1 -1
- package/prompts/parallel-context-build.md +3 -1
- package/prompts/parallel-handoff-plan.md +3 -1
- package/skills/pi-subagents/SKILL.md +60 -17
- package/src/agents/agent-management.ts +71 -15
- package/src/agents/agent-serializer.ts +13 -2
- package/src/agents/agents.ts +88 -17
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +2 -0
- package/src/extension/index.ts +5 -2
- package/src/extension/schemas.ts +132 -6
- package/src/intercom/result-intercom.ts +5 -0
- package/src/runs/background/async-execution.ts +88 -6
- package/src/runs/background/async-status.ts +11 -1
- package/src/runs/background/run-status.ts +10 -1
- package/src/runs/background/subagent-runner.ts +665 -39
- package/src/runs/foreground/chain-execution.ts +369 -118
- package/src/runs/foreground/execution.ts +392 -19
- package/src/runs/foreground/subagent-executor.ts +126 -3
- package/src/runs/shared/acceptance-contract.ts +318 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +173 -0
- package/src/runs/shared/acceptance-reports.ts +127 -0
- package/src/runs/shared/acceptance.ts +22 -0
- package/src/runs/shared/chain-outputs.ts +101 -0
- package/src/runs/shared/completion-guard.ts +26 -3
- package/src/runs/shared/dynamic-fanout.ts +293 -0
- package/src/runs/shared/parallel-utils.ts +33 -1
- package/src/runs/shared/pi-args.ts +11 -0
- package/src/runs/shared/structured-output.ts +77 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
- package/src/runs/shared/workflow-graph.ts +210 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +265 -1
- package/src/shared/utils.ts +7 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +178 -45
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
parsePackageName,
|
|
18
18
|
} from "./agents.ts";
|
|
19
19
|
import { serializeAgent } from "./agent-serializer.ts";
|
|
20
|
-
import { serializeChain } from "./chain-serializer.ts";
|
|
20
|
+
import { serializeChain, serializeJsonChain } from "./chain-serializer.ts";
|
|
21
21
|
import { discoverAvailableSkills } from "./skills.ts";
|
|
22
22
|
import type { Details } from "../shared/types.ts";
|
|
23
23
|
|
|
@@ -169,6 +169,22 @@ function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: strin
|
|
|
169
169
|
const s = item as Record<string, unknown>;
|
|
170
170
|
if (typeof s.agent !== "string" || !s.agent.trim()) return { error: `config.steps[${i}].agent must be a non-empty string.` };
|
|
171
171
|
const step: ChainStepConfig = { agent: s.agent.trim(), task: typeof s.task === "string" ? s.task : "" };
|
|
172
|
+
if (hasKey(s, "phase")) {
|
|
173
|
+
if (typeof s.phase === "string") step.phase = s.phase;
|
|
174
|
+
else return { error: `config.steps[${i}].phase must be a string.` };
|
|
175
|
+
}
|
|
176
|
+
if (hasKey(s, "label")) {
|
|
177
|
+
if (typeof s.label === "string") step.label = s.label;
|
|
178
|
+
else return { error: `config.steps[${i}].label must be a string.` };
|
|
179
|
+
}
|
|
180
|
+
if (hasKey(s, "as")) {
|
|
181
|
+
if (typeof s.as === "string") step.as = s.as;
|
|
182
|
+
else return { error: `config.steps[${i}].as must be a string.` };
|
|
183
|
+
}
|
|
184
|
+
if (hasKey(s, "outputSchema")) {
|
|
185
|
+
if (typeof s.outputSchema === "string") step.outputSchema = s.outputSchema;
|
|
186
|
+
else return { error: `config.steps[${i}].outputSchema must be a schema file path string for saved chains.` };
|
|
187
|
+
}
|
|
172
188
|
if (hasKey(s, "output")) {
|
|
173
189
|
if (s.output === false) step.output = false;
|
|
174
190
|
else if (typeof s.output === "string") step.output = s.output;
|
|
@@ -297,6 +313,18 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
|
|
|
297
313
|
target.maxSubagentDepth = cfg.maxSubagentDepth;
|
|
298
314
|
} else return "config.maxSubagentDepth must be an integer >= 0 or false when provided.";
|
|
299
315
|
}
|
|
316
|
+
if (hasKey(cfg, "maxExecutionTimeMs")) {
|
|
317
|
+
if (cfg.maxExecutionTimeMs === false || cfg.maxExecutionTimeMs === "") target.maxExecutionTimeMs = undefined;
|
|
318
|
+
else if (typeof cfg.maxExecutionTimeMs === "number" && Number.isInteger(cfg.maxExecutionTimeMs) && cfg.maxExecutionTimeMs >= 1) {
|
|
319
|
+
target.maxExecutionTimeMs = cfg.maxExecutionTimeMs;
|
|
320
|
+
} else return "config.maxExecutionTimeMs must be an integer >= 1 or false when provided.";
|
|
321
|
+
}
|
|
322
|
+
if (hasKey(cfg, "maxTokens")) {
|
|
323
|
+
if (cfg.maxTokens === false || cfg.maxTokens === "") target.maxTokens = undefined;
|
|
324
|
+
else if (typeof cfg.maxTokens === "number" && Number.isInteger(cfg.maxTokens) && cfg.maxTokens >= 1) {
|
|
325
|
+
target.maxTokens = cfg.maxTokens;
|
|
326
|
+
} else return "config.maxTokens must be an integer >= 1 or false when provided.";
|
|
327
|
+
}
|
|
300
328
|
if (hasKey(cfg, "completionGuard")) {
|
|
301
329
|
if (typeof cfg.completionGuard !== "boolean") return "config.completionGuard must be a boolean when provided.";
|
|
302
330
|
target.completionGuard = cfg.completionGuard;
|
|
@@ -339,7 +367,7 @@ function renamePath(
|
|
|
339
367
|
cwd: string,
|
|
340
368
|
): { filePath?: string; error?: string } {
|
|
341
369
|
if (nameExistsInScope(cwd, scope, newName, currentPath)) return { error: `Name '${newName}' already exists in ${scope} scope.` };
|
|
342
|
-
const ext = kind === "agent" ? ".md" : ".chain.md";
|
|
370
|
+
const ext = kind === "agent" ? ".md" : currentPath.endsWith(".chain.json") ? ".chain.json" : ".chain.md";
|
|
343
371
|
const filePath = path.join(path.dirname(currentPath), `${newName}${ext}`);
|
|
344
372
|
if (fs.existsSync(filePath) && filePath !== currentPath) {
|
|
345
373
|
return { error: `File already exists at ${filePath} but is not a valid ${kind} definition. Remove or rename it first.` };
|
|
@@ -370,11 +398,48 @@ function formatAgentDetail(agent: AgentConfig): string {
|
|
|
370
398
|
if (agent.defaultReads?.length) lines.push(`Reads: ${agent.defaultReads.join(", ")}`);
|
|
371
399
|
if (agent.defaultProgress) lines.push("Progress: true");
|
|
372
400
|
if (agent.maxSubagentDepth !== undefined) lines.push(`Max subagent depth: ${agent.maxSubagentDepth}`);
|
|
401
|
+
if (agent.maxExecutionTimeMs !== undefined) lines.push(`Max execution time: ${agent.maxExecutionTimeMs}ms`);
|
|
402
|
+
if (agent.maxTokens !== undefined) lines.push(`Max tokens: ${agent.maxTokens}`);
|
|
373
403
|
if (agent.completionGuard === false) lines.push("Completion guard: false");
|
|
374
404
|
if (agent.systemPrompt.trim()) lines.push("", "System Prompt:", agent.systemPrompt);
|
|
375
405
|
return lines.join("\n");
|
|
376
406
|
}
|
|
377
407
|
|
|
408
|
+
function formatChainStepDetail(step: ChainStepConfig, index: number): string[] {
|
|
409
|
+
const lines: string[] = [];
|
|
410
|
+
if (step.expand || step.collect) {
|
|
411
|
+
const parallel = step.parallel && !Array.isArray(step.parallel) && typeof step.parallel === "object" ? step.parallel as { agent?: unknown; task?: unknown; label?: unknown; outputSchema?: unknown } : undefined;
|
|
412
|
+
const expand = step.expand && typeof step.expand === "object" ? step.expand as { from?: { output?: unknown; path?: unknown }; item?: unknown; key?: unknown; maxItems?: unknown; onEmpty?: unknown } : undefined;
|
|
413
|
+
const collect = step.collect && typeof step.collect === "object" ? step.collect as { as?: unknown; outputSchema?: unknown } : undefined;
|
|
414
|
+
lines.push(`${index + 1}. Dynamic fanout${typeof collect?.as === "string" ? ` -> ${collect.as}` : ""}`);
|
|
415
|
+
if (expand?.from) lines.push(` Expand: ${String(expand.from.output ?? "?")}${String(expand.from.path ?? "")}`);
|
|
416
|
+
if (typeof expand?.item === "string") lines.push(` Item variable: ${expand.item}`);
|
|
417
|
+
if (typeof expand?.key === "string") lines.push(` Key: ${expand.key}`);
|
|
418
|
+
if (typeof expand?.maxItems === "number") lines.push(` Max items: ${expand.maxItems}`);
|
|
419
|
+
if (typeof expand?.onEmpty === "string") lines.push(` On empty: ${expand.onEmpty}`);
|
|
420
|
+
if (parallel?.agent) lines.push(` Agent: ${String(parallel.agent)}`);
|
|
421
|
+
if (typeof parallel?.label === "string") lines.push(` Label: ${parallel.label}`);
|
|
422
|
+
if (typeof parallel?.task === "string" && parallel.task.trim()) lines.push(` Task: ${parallel.task}`);
|
|
423
|
+
if (parallel?.outputSchema) lines.push(" Structured output: true");
|
|
424
|
+
if (collect?.outputSchema) lines.push(" Collect schema: true");
|
|
425
|
+
if (step.concurrency !== undefined) lines.push(` Concurrency: ${step.concurrency}`);
|
|
426
|
+
if (step.failFast !== undefined) lines.push(` Fail fast: ${step.failFast ? "true" : "false"}`);
|
|
427
|
+
return lines;
|
|
428
|
+
}
|
|
429
|
+
lines.push(`${index + 1}. ${step.agent}`);
|
|
430
|
+
if (step.task?.trim()) lines.push(` Task: ${step.task}`);
|
|
431
|
+
if (step.output === false) lines.push(" Output: false");
|
|
432
|
+
else if (step.output) lines.push(` Output: ${step.output}`);
|
|
433
|
+
if (step.outputMode) lines.push(` Output mode: ${step.outputMode}`);
|
|
434
|
+
if (step.reads === false) lines.push(" Reads: false");
|
|
435
|
+
else if (Array.isArray(step.reads) && step.reads.length > 0) lines.push(` Reads: ${step.reads.join(", ")}`);
|
|
436
|
+
if (step.model) lines.push(` Model: ${step.model}`);
|
|
437
|
+
if (step.skills === false) lines.push(" Skills: false");
|
|
438
|
+
else if (Array.isArray(step.skills) && step.skills.length > 0) lines.push(` Skills: ${step.skills.join(", ")}`);
|
|
439
|
+
if (step.progress !== undefined) lines.push(` Progress: ${step.progress ? "true" : "false"}`);
|
|
440
|
+
return lines;
|
|
441
|
+
}
|
|
442
|
+
|
|
378
443
|
function formatChainDetail(chain: ChainConfig): string {
|
|
379
444
|
const lines: string[] = [`Chain: ${chain.name} (${chain.source})`, `Path: ${chain.filePath}`, `Description: ${chain.description}`];
|
|
380
445
|
if (chain.packageName) {
|
|
@@ -383,18 +448,7 @@ function formatChainDetail(chain: ChainConfig): string {
|
|
|
383
448
|
}
|
|
384
449
|
lines.push("", "Steps:");
|
|
385
450
|
for (let i = 0; i < chain.steps.length; i++) {
|
|
386
|
-
|
|
387
|
-
lines.push(`${i + 1}. ${s.agent}`);
|
|
388
|
-
if (s.task.trim()) lines.push(` Task: ${s.task}`);
|
|
389
|
-
if (s.output === false) lines.push(" Output: false");
|
|
390
|
-
else if (s.output) lines.push(` Output: ${s.output}`);
|
|
391
|
-
if (s.outputMode) lines.push(` Output mode: ${s.outputMode}`);
|
|
392
|
-
if (s.reads === false) lines.push(" Reads: false");
|
|
393
|
-
else if (Array.isArray(s.reads) && s.reads.length > 0) lines.push(` Reads: ${s.reads.join(", ")}`);
|
|
394
|
-
if (s.model) lines.push(` Model: ${s.model}`);
|
|
395
|
-
if (s.skills === false) lines.push(" Skills: false");
|
|
396
|
-
else if (Array.isArray(s.skills) && s.skills.length > 0) lines.push(` Skills: ${s.skills.join(", ")}`);
|
|
397
|
-
if (s.progress !== undefined) lines.push(` Progress: ${s.progress ? "true" : "false"}`);
|
|
451
|
+
lines.push(...formatChainStepDetail(chain.steps[i]!, i));
|
|
398
452
|
}
|
|
399
453
|
return lines.join("\n");
|
|
400
454
|
}
|
|
@@ -405,6 +459,7 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
|
|
|
405
459
|
const scopedAgents = allAgents(d).filter((a) => scope === "both" || a.source === "builtin" || a.source === scope).sort((a, b) => a.name.localeCompare(b.name));
|
|
406
460
|
const agents = scopedAgents.filter((a) => !a.disabled);
|
|
407
461
|
const chains = d.chains.filter((c) => scope === "both" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
|
|
462
|
+
const diagnostics = d.chainDiagnostics.filter((entry) => scope === "both" || entry.source === scope);
|
|
408
463
|
const lines = [
|
|
409
464
|
"Executable agents:",
|
|
410
465
|
...(agents.length
|
|
@@ -413,6 +468,7 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
|
|
|
413
468
|
"",
|
|
414
469
|
"Chains:",
|
|
415
470
|
...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"]),
|
|
471
|
+
...(diagnostics.length ? ["", "Chain diagnostics:", ...diagnostics.map((entry) => `- ${entry.filePath}: ${entry.error}`)] : []),
|
|
416
472
|
];
|
|
417
473
|
return result(lines.join("\n"));
|
|
418
474
|
}
|
|
@@ -608,7 +664,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
|
|
|
608
664
|
if (renamed.error) return result(renamed.error, true);
|
|
609
665
|
updated.filePath = renamed.filePath!;
|
|
610
666
|
}
|
|
611
|
-
fs.writeFileSync(updated.filePath, serializeChain(updated), "utf-8");
|
|
667
|
+
fs.writeFileSync(updated.filePath, updated.filePath.endsWith(".chain.json") ? serializeJsonChain(updated) : serializeChain(updated), "utf-8");
|
|
612
668
|
const headline = updated.name === oldName
|
|
613
669
|
? `Updated chain '${updated.name}' at ${updated.filePath}.`
|
|
614
670
|
: `Updated chain '${oldName}' to '${updated.name}' at ${updated.filePath}.`;
|
|
@@ -21,6 +21,8 @@ export const KNOWN_FIELDS = new Set([
|
|
|
21
21
|
"defaultProgress",
|
|
22
22
|
"interactive",
|
|
23
23
|
"maxSubagentDepth",
|
|
24
|
+
"maxExecutionTimeMs",
|
|
25
|
+
"maxTokens",
|
|
24
26
|
"completionGuard",
|
|
25
27
|
]);
|
|
26
28
|
|
|
@@ -67,8 +69,17 @@ export function serializeAgent(config: AgentConfig): string {
|
|
|
67
69
|
|
|
68
70
|
if (config.defaultProgress) lines.push("defaultProgress: true");
|
|
69
71
|
if (config.interactive) lines.push("interactive: true");
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
const maxSubagentDepth = config.maxSubagentDepth;
|
|
73
|
+
if (typeof maxSubagentDepth === "number" && Number.isInteger(maxSubagentDepth) && maxSubagentDepth >= 0) {
|
|
74
|
+
lines.push(`maxSubagentDepth: ${maxSubagentDepth}`);
|
|
75
|
+
}
|
|
76
|
+
const maxExecutionTimeMs = config.maxExecutionTimeMs;
|
|
77
|
+
if (typeof maxExecutionTimeMs === "number" && Number.isInteger(maxExecutionTimeMs) && maxExecutionTimeMs >= 1) {
|
|
78
|
+
lines.push(`maxExecutionTimeMs: ${maxExecutionTimeMs}`);
|
|
79
|
+
}
|
|
80
|
+
const maxTokens = config.maxTokens;
|
|
81
|
+
if (typeof maxTokens === "number" && Number.isInteger(maxTokens) && maxTokens >= 1) {
|
|
82
|
+
lines.push(`maxTokens: ${maxTokens}`);
|
|
72
83
|
}
|
|
73
84
|
if (config.completionGuard === false) lines.push("completionGuard: false");
|
|
74
85
|
|
package/src/agents/agents.ts
CHANGED
|
@@ -6,10 +6,10 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import type { OutputMode } from "../shared/types.ts";
|
|
9
|
+
import type { AcceptanceInput, OutputMode } from "../shared/types.ts";
|
|
10
10
|
import { getAgentDir } from "../shared/utils.ts";
|
|
11
11
|
import { KNOWN_FIELDS } from "./agent-serializer.ts";
|
|
12
|
-
import { parseChain } from "./chain-serializer.ts";
|
|
12
|
+
import { parseChain, parseJsonChain } from "./chain-serializer.ts";
|
|
13
13
|
import { mergeAgentsForScope } from "./agent-selection.ts";
|
|
14
14
|
import { parseFrontmatter } from "./frontmatter.ts";
|
|
15
15
|
import { buildRuntimeName, parsePackageName } from "./identity.ts";
|
|
@@ -46,6 +46,8 @@ export interface BuiltinAgentOverrideBase {
|
|
|
46
46
|
skills?: string[];
|
|
47
47
|
tools?: string[];
|
|
48
48
|
mcpDirectTools?: string[];
|
|
49
|
+
maxExecutionTimeMs?: number;
|
|
50
|
+
maxTokens?: number;
|
|
49
51
|
completionGuard?: boolean;
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -61,6 +63,8 @@ interface BuiltinAgentOverrideConfig {
|
|
|
61
63
|
systemPrompt?: string;
|
|
62
64
|
skills?: string[] | false;
|
|
63
65
|
tools?: string[] | false;
|
|
66
|
+
maxExecutionTimeMs?: number | false;
|
|
67
|
+
maxTokens?: number | false;
|
|
64
68
|
completionGuard?: boolean;
|
|
65
69
|
}
|
|
66
70
|
|
|
@@ -94,6 +98,8 @@ export interface AgentConfig {
|
|
|
94
98
|
defaultProgress?: boolean;
|
|
95
99
|
interactive?: boolean;
|
|
96
100
|
maxSubagentDepth?: number;
|
|
101
|
+
maxExecutionTimeMs?: number;
|
|
102
|
+
maxTokens?: number;
|
|
97
103
|
completionGuard?: boolean;
|
|
98
104
|
disabled?: boolean;
|
|
99
105
|
extraFields?: Record<string, string>;
|
|
@@ -108,14 +114,25 @@ interface SubagentSettings {
|
|
|
108
114
|
const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {} };
|
|
109
115
|
|
|
110
116
|
export interface ChainStepConfig {
|
|
111
|
-
agent
|
|
112
|
-
task
|
|
117
|
+
agent?: string;
|
|
118
|
+
task?: string;
|
|
119
|
+
phase?: string;
|
|
120
|
+
label?: string;
|
|
121
|
+
as?: string;
|
|
122
|
+
outputSchema?: string | Record<string, unknown>;
|
|
113
123
|
output?: string | false;
|
|
114
124
|
outputMode?: OutputMode;
|
|
115
125
|
reads?: string[] | false;
|
|
116
126
|
model?: string;
|
|
117
127
|
skills?: string[] | false;
|
|
118
128
|
progress?: boolean;
|
|
129
|
+
parallel?: unknown;
|
|
130
|
+
expand?: unknown;
|
|
131
|
+
collect?: unknown;
|
|
132
|
+
concurrency?: number;
|
|
133
|
+
failFast?: boolean;
|
|
134
|
+
worktree?: boolean;
|
|
135
|
+
acceptance?: AcceptanceInput;
|
|
119
136
|
}
|
|
120
137
|
|
|
121
138
|
export interface ChainConfig {
|
|
@@ -129,6 +146,12 @@ export interface ChainConfig {
|
|
|
129
146
|
extraFields?: Record<string, string>;
|
|
130
147
|
}
|
|
131
148
|
|
|
149
|
+
export interface ChainDiscoveryDiagnostic {
|
|
150
|
+
source: "user" | "project";
|
|
151
|
+
filePath: string;
|
|
152
|
+
error: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
132
155
|
interface AgentDiscoveryResult {
|
|
133
156
|
agents: AgentConfig[];
|
|
134
157
|
projectAgentsDir: string | null;
|
|
@@ -186,6 +209,8 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
186
209
|
skills: agent.skills ? [...agent.skills] : undefined,
|
|
187
210
|
tools: agent.tools ? [...agent.tools] : undefined,
|
|
188
211
|
mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : undefined,
|
|
212
|
+
maxExecutionTimeMs: agent.maxExecutionTimeMs,
|
|
213
|
+
maxTokens: agent.maxTokens,
|
|
189
214
|
completionGuard: agent.completionGuard,
|
|
190
215
|
};
|
|
191
216
|
}
|
|
@@ -205,6 +230,8 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
|
|
|
205
230
|
...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
|
|
206
231
|
...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
|
|
207
232
|
...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
|
|
233
|
+
...(override.maxExecutionTimeMs !== undefined ? { maxExecutionTimeMs: override.maxExecutionTimeMs } : {}),
|
|
234
|
+
...(override.maxTokens !== undefined ? { maxTokens: override.maxTokens } : {}),
|
|
208
235
|
...(override.completionGuard !== undefined ? { completionGuard: override.completionGuard } : {}),
|
|
209
236
|
};
|
|
210
237
|
}
|
|
@@ -342,6 +369,22 @@ function parseBuiltinOverrideEntry(
|
|
|
342
369
|
}
|
|
343
370
|
}
|
|
344
371
|
|
|
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
|
+
|
|
345
388
|
if ("completionGuard" in input) {
|
|
346
389
|
if (typeof input.completionGuard === "boolean") {
|
|
347
390
|
override.completionGuard = input.completionGuard;
|
|
@@ -422,6 +465,8 @@ function applyBuiltinOverride(
|
|
|
422
465
|
next.tools = tools;
|
|
423
466
|
next.mcpDirectTools = mcpDirectTools;
|
|
424
467
|
}
|
|
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;
|
|
425
470
|
if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
|
|
426
471
|
|
|
427
472
|
return next;
|
|
@@ -462,7 +507,7 @@ function applyBuiltinOverrides(
|
|
|
462
507
|
|
|
463
508
|
export function buildBuiltinOverrideConfig(
|
|
464
509
|
base: BuiltinAgentOverrideBase,
|
|
465
|
-
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "completionGuard">,
|
|
510
|
+
draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "maxExecutionTimeMs" | "maxTokens" | "completionGuard">,
|
|
466
511
|
): BuiltinAgentOverrideConfig | undefined {
|
|
467
512
|
const override: BuiltinAgentOverrideConfig = {};
|
|
468
513
|
|
|
@@ -480,6 +525,8 @@ export function buildBuiltinOverrideConfig(
|
|
|
480
525
|
const baseTools = joinToolList(base);
|
|
481
526
|
const draftTools = joinToolList(draft);
|
|
482
527
|
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;
|
|
483
530
|
if ((draft.completionGuard !== false) !== (base.completionGuard !== false)) {
|
|
484
531
|
override.completionGuard = draft.completionGuard !== false;
|
|
485
532
|
}
|
|
@@ -535,7 +582,7 @@ export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "us
|
|
|
535
582
|
return filePath;
|
|
536
583
|
}
|
|
537
584
|
|
|
538
|
-
function
|
|
585
|
+
function listFilesRecursive(dir: string, predicate: (fileName: string) => boolean): string[] {
|
|
539
586
|
const files: string[] = [];
|
|
540
587
|
if (!fs.existsSync(dir)) return files;
|
|
541
588
|
|
|
@@ -549,7 +596,7 @@ function listMarkdownFilesRecursive(dir: string, predicate: (fileName: string) =
|
|
|
549
596
|
for (const entry of entries) {
|
|
550
597
|
const filePath = path.join(dir, entry.name);
|
|
551
598
|
if (entry.isDirectory()) {
|
|
552
|
-
files.push(...
|
|
599
|
+
files.push(...listFilesRecursive(filePath, predicate));
|
|
553
600
|
continue;
|
|
554
601
|
}
|
|
555
602
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
@@ -562,7 +609,7 @@ function listMarkdownFilesRecursive(dir: string, predicate: (fileName: string) =
|
|
|
562
609
|
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
563
610
|
const agents: AgentConfig[] = [];
|
|
564
611
|
|
|
565
|
-
for (const filePath of
|
|
612
|
+
for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
|
|
566
613
|
let content: string;
|
|
567
614
|
try {
|
|
568
615
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -648,6 +695,8 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
648
695
|
}
|
|
649
696
|
|
|
650
697
|
const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth);
|
|
698
|
+
const parsedMaxExecutionTimeMs = Number(frontmatter.maxExecutionTimeMs);
|
|
699
|
+
const parsedMaxTokens = Number(frontmatter.maxTokens);
|
|
651
700
|
const completionGuard = frontmatter.completionGuard === "false"
|
|
652
701
|
? false
|
|
653
702
|
: frontmatter.completionGuard === "true"
|
|
@@ -681,6 +730,14 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
681
730
|
Number.isInteger(parsedMaxSubagentDepth) && parsedMaxSubagentDepth >= 0
|
|
682
731
|
? parsedMaxSubagentDepth
|
|
683
732
|
: 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,
|
|
684
741
|
completionGuard,
|
|
685
742
|
extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
|
|
686
743
|
});
|
|
@@ -689,10 +746,11 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
689
746
|
return agents;
|
|
690
747
|
}
|
|
691
748
|
|
|
692
|
-
function loadChainsFromDir(dir: string, source:
|
|
693
|
-
const chains
|
|
749
|
+
function loadChainsFromDir(dir: string, source: "user" | "project"): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
|
|
750
|
+
const chains = new Map<string, ChainConfig>();
|
|
751
|
+
const diagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
694
752
|
|
|
695
|
-
for (const filePath of
|
|
753
|
+
for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".chain.md") || fileName.endsWith(".chain.json"))) {
|
|
696
754
|
let content: string;
|
|
697
755
|
try {
|
|
698
756
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -701,13 +759,17 @@ function loadChainsFromDir(dir: string, source: AgentSource): ChainConfig[] {
|
|
|
701
759
|
}
|
|
702
760
|
|
|
703
761
|
try {
|
|
704
|
-
|
|
705
|
-
|
|
762
|
+
const chain = filePath.endsWith(".chain.json") ? parseJsonChain(content, source, filePath) : parseChain(content, source, filePath);
|
|
763
|
+
const existing = chains.get(chain.name);
|
|
764
|
+
if (existing && existing.filePath.endsWith(".chain.json") && filePath.endsWith(".chain.md")) continue;
|
|
765
|
+
chains.set(chain.name, chain);
|
|
766
|
+
} catch (error) {
|
|
767
|
+
diagnostics.push({ source, filePath, error: error instanceof Error ? error.message : String(error) });
|
|
706
768
|
continue;
|
|
707
769
|
}
|
|
708
770
|
}
|
|
709
771
|
|
|
710
|
-
return chains;
|
|
772
|
+
return { chains: Array.from(chains.values()), diagnostics };
|
|
711
773
|
}
|
|
712
774
|
|
|
713
775
|
function isDirectory(p: string): boolean {
|
|
@@ -779,6 +841,7 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
779
841
|
user: AgentConfig[];
|
|
780
842
|
project: AgentConfig[];
|
|
781
843
|
chains: ChainConfig[];
|
|
844
|
+
chainDiagnostics: ChainDiscoveryDiagnostic[];
|
|
782
845
|
userDir: string;
|
|
783
846
|
projectDir: string | null;
|
|
784
847
|
userChainDir: string;
|
|
@@ -816,17 +879,25 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
816
879
|
const project = Array.from(projectMap.values());
|
|
817
880
|
|
|
818
881
|
const chainMap = new Map<string, ChainConfig>();
|
|
882
|
+
const projectChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
819
883
|
for (const dir of projectChainDirs) {
|
|
820
|
-
|
|
884
|
+
const loaded = loadChainsFromDir(dir, "project");
|
|
885
|
+
projectChainDiagnostics.push(...loaded.diagnostics);
|
|
886
|
+
for (const chain of loaded.chains) {
|
|
821
887
|
chainMap.set(chain.name, chain);
|
|
822
888
|
}
|
|
823
889
|
}
|
|
890
|
+
const userChains = loadChainsFromDir(userChainDir, "user");
|
|
824
891
|
const chains = [
|
|
825
|
-
...
|
|
892
|
+
...userChains.chains,
|
|
826
893
|
...Array.from(chainMap.values()),
|
|
827
894
|
];
|
|
895
|
+
const chainDiagnostics = [
|
|
896
|
+
...userChains.diagnostics,
|
|
897
|
+
...projectChainDiagnostics,
|
|
898
|
+
];
|
|
828
899
|
|
|
829
900
|
const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
|
|
830
901
|
|
|
831
|
-
return { builtin, user, project, chains, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
902
|
+
return { builtin, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
832
903
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ChainConfig, ChainStepConfig } from "./agents.ts";
|
|
2
2
|
import { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
|
|
3
3
|
import { parseFrontmatter } from "./frontmatter.ts";
|
|
4
|
+
import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
|
|
5
|
+
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
6
|
+
import type { ChainStep } from "../shared/settings.ts";
|
|
4
7
|
|
|
5
8
|
function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
6
9
|
const lines = sectionBody.split("\n");
|
|
@@ -20,6 +23,25 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
|
20
23
|
else if (rawValue) step.output = rawValue;
|
|
21
24
|
continue;
|
|
22
25
|
}
|
|
26
|
+
if (key === "phase") {
|
|
27
|
+
if (rawValue) step.phase = rawValue;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (key === "label") {
|
|
31
|
+
if (rawValue) step.label = rawValue;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (key === "as") {
|
|
35
|
+
if (rawValue) step.as = rawValue;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (key === "outputschema") {
|
|
39
|
+
if (rawValue.startsWith("{") || rawValue.startsWith("[")) {
|
|
40
|
+
throw new Error("Inline outputSchema values are not supported in .chain.md files; use a schema file path.");
|
|
41
|
+
}
|
|
42
|
+
if (rawValue) step.outputSchema = rawValue;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
23
45
|
if (key === "outputmode") {
|
|
24
46
|
if (rawValue === "inline" || rawValue === "file-only") step.outputMode = rawValue;
|
|
25
47
|
continue;
|
|
@@ -102,6 +124,100 @@ export function parseChain(content: string, source: "user" | "project", filePath
|
|
|
102
124
|
};
|
|
103
125
|
}
|
|
104
126
|
|
|
127
|
+
export function parseJsonChain(content: string, source: "user" | "project", filePath: string): ChainConfig {
|
|
128
|
+
let parsed: unknown;
|
|
129
|
+
try {
|
|
130
|
+
parsed = JSON.parse(content);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
throw new Error(`Invalid JSON chain '${filePath}': ${message}`);
|
|
134
|
+
}
|
|
135
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
136
|
+
throw new Error(`JSON chain '${filePath}' must contain an object root.`);
|
|
137
|
+
}
|
|
138
|
+
const input = parsed as Record<string, unknown>;
|
|
139
|
+
if (typeof input.name !== "string" || !input.name.trim()) {
|
|
140
|
+
throw new Error(`JSON chain '${filePath}' must include string name.`);
|
|
141
|
+
}
|
|
142
|
+
if (typeof input.description !== "string" || !input.description.trim()) {
|
|
143
|
+
throw new Error(`JSON chain '${filePath}' must include string description.`);
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(input.chain)) {
|
|
146
|
+
throw new Error(`JSON chain '${filePath}' must include array chain.`);
|
|
147
|
+
}
|
|
148
|
+
for (let i = 0; i < input.chain.length; i++) {
|
|
149
|
+
const step = input.chain[i];
|
|
150
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
151
|
+
throw new Error(`JSON chain '${filePath}' step ${i + 1} must be an object.`);
|
|
152
|
+
}
|
|
153
|
+
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
|
+
const acceptanceErrors = validateAcceptanceInput(stepRecord.acceptance, `step ${i + 1} acceptance`);
|
|
162
|
+
if (acceptanceErrors.length > 0) {
|
|
163
|
+
throw new Error(`Invalid JSON chain '${filePath}': ${acceptanceErrors.join(" ")}`);
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(parallel)) {
|
|
166
|
+
for (let taskIndex = 0; taskIndex < parallel.length; taskIndex++) {
|
|
167
|
+
const task = parallel[taskIndex];
|
|
168
|
+
if (!task || typeof task !== "object" || Array.isArray(task)) continue;
|
|
169
|
+
const taskErrors = validateAcceptanceInput((task as Record<string, unknown>).acceptance, `step ${i + 1} parallel task ${taskIndex + 1} acceptance`);
|
|
170
|
+
if (taskErrors.length > 0) {
|
|
171
|
+
throw new Error(`Invalid JSON chain '${filePath}': ${taskErrors.join(" ")}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else if (parallel && typeof parallel === "object") {
|
|
175
|
+
const templateErrors = validateAcceptanceInput((parallel as Record<string, unknown>).acceptance, `step ${i + 1} dynamic template acceptance`);
|
|
176
|
+
if (templateErrors.length > 0) {
|
|
177
|
+
throw new Error(`Invalid JSON chain '${filePath}': ${templateErrors.join(" ")}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
validateChainOutputBindings(input.chain as ChainStep[], { maxItems: Number.MAX_SAFE_INTEGER });
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof ChainOutputValidationError) throw new Error(`Invalid JSON chain '${filePath}': ${error.message}`);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
const parsedPackage = parsePackageName(typeof input.package === "string" ? input.package : undefined, `Chain '${input.name}' package`);
|
|
188
|
+
if (parsedPackage.error) throw new Error(parsedPackage.error);
|
|
189
|
+
const extraFields: Record<string, string> = {};
|
|
190
|
+
for (const [key, value] of Object.entries(input)) {
|
|
191
|
+
if (key === "name" || key === "package" || key === "description" || key === "chain") continue;
|
|
192
|
+
if (typeof value === "string") extraFields[key] = value;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
name: buildRuntimeName(input.name.trim(), parsedPackage.packageName),
|
|
196
|
+
localName: input.name.trim(),
|
|
197
|
+
packageName: parsedPackage.packageName,
|
|
198
|
+
description: input.description.trim(),
|
|
199
|
+
source,
|
|
200
|
+
filePath,
|
|
201
|
+
steps: input.chain as ChainStepConfig[],
|
|
202
|
+
extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function serializeJsonChain(config: ChainConfig): string {
|
|
207
|
+
const root: Record<string, unknown> = {
|
|
208
|
+
name: frontmatterNameForConfig(config),
|
|
209
|
+
description: config.description,
|
|
210
|
+
chain: config.steps,
|
|
211
|
+
};
|
|
212
|
+
if (config.packageName) root.package = config.packageName;
|
|
213
|
+
if (config.extraFields) {
|
|
214
|
+
for (const [key, value] of Object.entries(config.extraFields)) {
|
|
215
|
+
if (key !== "name" && key !== "description" && key !== "package" && key !== "chain") root[key] = value;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return `${JSON.stringify(root, null, 2)}\n`;
|
|
219
|
+
}
|
|
220
|
+
|
|
105
221
|
export function serializeChain(config: ChainConfig): string {
|
|
106
222
|
const lines: string[] = [];
|
|
107
223
|
lines.push("---");
|
|
@@ -121,6 +237,10 @@ export function serializeChain(config: ChainConfig): string {
|
|
|
121
237
|
lines.push(`## ${step.agent}`);
|
|
122
238
|
if (step.output === false) lines.push("output: false");
|
|
123
239
|
else if (step.output) lines.push(`output: ${step.output}`);
|
|
240
|
+
if (step.phase) lines.push(`phase: ${step.phase}`);
|
|
241
|
+
if (step.label) lines.push(`label: ${step.label}`);
|
|
242
|
+
if (step.as) lines.push(`as: ${step.as}`);
|
|
243
|
+
if (step.outputSchema) lines.push(`outputSchema: ${step.outputSchema}`);
|
|
124
244
|
if (step.outputMode) lines.push(`outputMode: ${step.outputMode}`);
|
|
125
245
|
if (step.reads === false) lines.push("reads: false");
|
|
126
246
|
else if (Array.isArray(step.reads) && step.reads.length > 0) lines.push(`reads: ${step.reads.join(", ")}`);
|
|
@@ -156,6 +156,8 @@ 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.",
|
|
159
161
|
"Allowed management/control actions: list, get, status, interrupt, resume, doctor.",
|
|
160
162
|
"Agent config mutation actions create, update, and delete are blocked in this mode.",
|
|
161
163
|
].join("\n"),
|
package/src/extension/index.ts
CHANGED
|
@@ -394,7 +394,10 @@ EXECUTION (use exactly ONE mode):
|
|
|
394
394
|
• SINGLE: { agent, task? } - one task; omit task for self-contained agents
|
|
395
395
|
• CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
|
|
396
396
|
• 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.
|
|
397
398
|
• 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.
|
|
398
401
|
|
|
399
402
|
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
400
403
|
• {task} - The original task/request from the user
|
|
@@ -406,8 +409,8 @@ Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", t
|
|
|
406
409
|
MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
407
410
|
• { action: "list" } - discover executable agents/chains
|
|
408
411
|
• { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
|
|
409
|
-
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, ... } }
|
|
410
|
-
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
|
|
412
|
+
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, maxExecutionTimeMs, maxTokens, ... } }
|
|
413
|
+
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", maxExecutionTimeMs, maxTokens, ... } } - merge
|
|
411
414
|
• { action: "delete", agent: "code-analysis.custom-agent" }
|
|
412
415
|
• Use chainName for chain operations; packaged chains also use dotted runtime names
|
|
413
416
|
|