pi-subagents 0.25.0 → 0.27.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 +21 -0
- package/README.md +129 -17
- 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 +32 -17
- package/src/agents/agent-management.ts +57 -15
- package/src/agents/agent-serializer.ts +3 -2
- package/src/agents/agents.ts +47 -16
- package/src/agents/chain-serializer.ts +120 -0
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +1 -0
- package/src/extension/schemas.ts +138 -5
- package/src/runs/background/async-execution.ts +84 -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 +600 -31
- package/src/runs/foreground/chain-execution.ts +325 -118
- package/src/runs/foreground/execution.ts +222 -10
- package/src/runs/foreground/subagent-executor.ts +67 -0
- package/src/runs/shared/acceptance-contract.ts +291 -0
- package/src/runs/shared/acceptance-evaluation.ts +221 -0
- package/src/runs/shared/acceptance-finalization.ts +161 -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 +31 -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 +206 -0
- package/src/shared/formatters.ts +2 -2
- package/src/shared/settings.ts +53 -4
- package/src/shared/types.ts +250 -0
- package/src/slash/slash-commands.ts +41 -3
- package/src/tui/render.ts +162 -34
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";
|
|
@@ -108,14 +108,25 @@ interface SubagentSettings {
|
|
|
108
108
|
const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {} };
|
|
109
109
|
|
|
110
110
|
export interface ChainStepConfig {
|
|
111
|
-
agent
|
|
112
|
-
task
|
|
111
|
+
agent?: string;
|
|
112
|
+
task?: string;
|
|
113
|
+
phase?: string;
|
|
114
|
+
label?: string;
|
|
115
|
+
as?: string;
|
|
116
|
+
outputSchema?: string | Record<string, unknown>;
|
|
113
117
|
output?: string | false;
|
|
114
118
|
outputMode?: OutputMode;
|
|
115
119
|
reads?: string[] | false;
|
|
116
120
|
model?: string;
|
|
117
121
|
skills?: string[] | false;
|
|
118
122
|
progress?: boolean;
|
|
123
|
+
parallel?: unknown;
|
|
124
|
+
expand?: unknown;
|
|
125
|
+
collect?: unknown;
|
|
126
|
+
concurrency?: number;
|
|
127
|
+
failFast?: boolean;
|
|
128
|
+
worktree?: boolean;
|
|
129
|
+
acceptance?: AcceptanceInput;
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
export interface ChainConfig {
|
|
@@ -129,6 +140,12 @@ export interface ChainConfig {
|
|
|
129
140
|
extraFields?: Record<string, string>;
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
export interface ChainDiscoveryDiagnostic {
|
|
144
|
+
source: "user" | "project";
|
|
145
|
+
filePath: string;
|
|
146
|
+
error: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
interface AgentDiscoveryResult {
|
|
133
150
|
agents: AgentConfig[];
|
|
134
151
|
projectAgentsDir: string | null;
|
|
@@ -535,7 +552,7 @@ export function removeBuiltinAgentOverride(cwd: string, name: string, scope: "us
|
|
|
535
552
|
return filePath;
|
|
536
553
|
}
|
|
537
554
|
|
|
538
|
-
function
|
|
555
|
+
function listFilesRecursive(dir: string, predicate: (fileName: string) => boolean): string[] {
|
|
539
556
|
const files: string[] = [];
|
|
540
557
|
if (!fs.existsSync(dir)) return files;
|
|
541
558
|
|
|
@@ -549,7 +566,7 @@ function listMarkdownFilesRecursive(dir: string, predicate: (fileName: string) =
|
|
|
549
566
|
for (const entry of entries) {
|
|
550
567
|
const filePath = path.join(dir, entry.name);
|
|
551
568
|
if (entry.isDirectory()) {
|
|
552
|
-
files.push(...
|
|
569
|
+
files.push(...listFilesRecursive(filePath, predicate));
|
|
553
570
|
continue;
|
|
554
571
|
}
|
|
555
572
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
@@ -562,7 +579,7 @@ function listMarkdownFilesRecursive(dir: string, predicate: (fileName: string) =
|
|
|
562
579
|
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
563
580
|
const agents: AgentConfig[] = [];
|
|
564
581
|
|
|
565
|
-
for (const filePath of
|
|
582
|
+
for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
|
|
566
583
|
let content: string;
|
|
567
584
|
try {
|
|
568
585
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -689,10 +706,11 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
689
706
|
return agents;
|
|
690
707
|
}
|
|
691
708
|
|
|
692
|
-
function loadChainsFromDir(dir: string, source:
|
|
693
|
-
const chains
|
|
709
|
+
function loadChainsFromDir(dir: string, source: "user" | "project"): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
|
|
710
|
+
const chains = new Map<string, ChainConfig>();
|
|
711
|
+
const diagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
694
712
|
|
|
695
|
-
for (const filePath of
|
|
713
|
+
for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".chain.md") || fileName.endsWith(".chain.json"))) {
|
|
696
714
|
let content: string;
|
|
697
715
|
try {
|
|
698
716
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -701,13 +719,17 @@ function loadChainsFromDir(dir: string, source: AgentSource): ChainConfig[] {
|
|
|
701
719
|
}
|
|
702
720
|
|
|
703
721
|
try {
|
|
704
|
-
|
|
705
|
-
|
|
722
|
+
const chain = filePath.endsWith(".chain.json") ? parseJsonChain(content, source, filePath) : parseChain(content, source, filePath);
|
|
723
|
+
const existing = chains.get(chain.name);
|
|
724
|
+
if (existing && existing.filePath.endsWith(".chain.json") && filePath.endsWith(".chain.md")) continue;
|
|
725
|
+
chains.set(chain.name, chain);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
diagnostics.push({ source, filePath, error: error instanceof Error ? error.message : String(error) });
|
|
706
728
|
continue;
|
|
707
729
|
}
|
|
708
730
|
}
|
|
709
731
|
|
|
710
|
-
return chains;
|
|
732
|
+
return { chains: Array.from(chains.values()), diagnostics };
|
|
711
733
|
}
|
|
712
734
|
|
|
713
735
|
function isDirectory(p: string): boolean {
|
|
@@ -779,6 +801,7 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
779
801
|
user: AgentConfig[];
|
|
780
802
|
project: AgentConfig[];
|
|
781
803
|
chains: ChainConfig[];
|
|
804
|
+
chainDiagnostics: ChainDiscoveryDiagnostic[];
|
|
782
805
|
userDir: string;
|
|
783
806
|
projectDir: string | null;
|
|
784
807
|
userChainDir: string;
|
|
@@ -816,17 +839,25 @@ export function discoverAgentsAll(cwd: string): {
|
|
|
816
839
|
const project = Array.from(projectMap.values());
|
|
817
840
|
|
|
818
841
|
const chainMap = new Map<string, ChainConfig>();
|
|
842
|
+
const projectChainDiagnostics: ChainDiscoveryDiagnostic[] = [];
|
|
819
843
|
for (const dir of projectChainDirs) {
|
|
820
|
-
|
|
844
|
+
const loaded = loadChainsFromDir(dir, "project");
|
|
845
|
+
projectChainDiagnostics.push(...loaded.diagnostics);
|
|
846
|
+
for (const chain of loaded.chains) {
|
|
821
847
|
chainMap.set(chain.name, chain);
|
|
822
848
|
}
|
|
823
849
|
}
|
|
850
|
+
const userChains = loadChainsFromDir(userChainDir, "user");
|
|
824
851
|
const chains = [
|
|
825
|
-
...
|
|
852
|
+
...userChains.chains,
|
|
826
853
|
...Array.from(chainMap.values()),
|
|
827
854
|
];
|
|
855
|
+
const chainDiagnostics = [
|
|
856
|
+
...userChains.diagnostics,
|
|
857
|
+
...projectChainDiagnostics,
|
|
858
|
+
];
|
|
828
859
|
|
|
829
860
|
const userDir = process.env.PI_CODING_AGENT_DIR ? userDirOld : fs.existsSync(userDirNew) ? userDirNew : userDirOld;
|
|
830
861
|
|
|
831
|
-
return { builtin, user, project, chains, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
862
|
+
return { builtin, user, project, chains, chainDiagnostics, userDir, projectDir, userChainDir, projectChainDir, userSettingsPath, projectSettingsPath };
|
|
832
863
|
}
|
|
@@ -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,7 @@ 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.",
|
|
159
160
|
"Allowed management/control actions: list, get, status, interrupt, resume, doctor.",
|
|
160
161
|
"Agent config mutation actions create, update, and delete are blocked in this mode.",
|
|
161
162
|
].join("\n"),
|
package/src/extension/index.ts
CHANGED
|
@@ -395,6 +395,7 @@ EXECUTION (use exactly ONE mode):
|
|
|
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
397
|
• 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" })
|
|
398
|
+
• 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.
|
|
398
399
|
|
|
399
400
|
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
400
401
|
• {task} - The original task/request from the user
|
package/src/extension/schemas.ts
CHANGED
|
@@ -35,9 +35,82 @@ const ReadsOverride = Type.Unsafe({
|
|
|
35
35
|
description: "Files to read before running (array of filenames), or false to disable",
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
const JsonSchemaObject = Type.Unsafe({
|
|
39
|
+
type: "object",
|
|
40
|
+
additionalProperties: true,
|
|
41
|
+
description: "JSON Schema object for strict structured output. Non-object roots are rejected.",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const AcceptanceEvidenceKind = Type.String({
|
|
45
|
+
enum: [
|
|
46
|
+
"changed-files",
|
|
47
|
+
"tests-added",
|
|
48
|
+
"commands-run",
|
|
49
|
+
"validation-output",
|
|
50
|
+
"residual-risks",
|
|
51
|
+
"no-staged-files",
|
|
52
|
+
"diff-summary",
|
|
53
|
+
"review-findings",
|
|
54
|
+
"manual-notes",
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const AcceptanceGateSchema = Type.Object({
|
|
59
|
+
id: Type.String(),
|
|
60
|
+
must: Type.String(),
|
|
61
|
+
evidence: Type.Optional(Type.Array(AcceptanceEvidenceKind)),
|
|
62
|
+
severity: Type.Optional(Type.String({ enum: ["required", "recommended"] })),
|
|
63
|
+
}, { additionalProperties: false });
|
|
64
|
+
|
|
65
|
+
const AcceptanceVerifyCommandSchema = Type.Object({
|
|
66
|
+
id: Type.String(),
|
|
67
|
+
command: Type.String(),
|
|
68
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
69
|
+
cwd: Type.Optional(Type.String()),
|
|
70
|
+
env: Type.Optional(Type.Unsafe({ type: "object", additionalProperties: { type: "string" } })),
|
|
71
|
+
allowFailure: Type.Optional(Type.Boolean()),
|
|
72
|
+
}, { additionalProperties: false });
|
|
73
|
+
|
|
74
|
+
const AcceptanceReviewGateSchema = Type.Object({
|
|
75
|
+
agent: Type.Optional(Type.String()),
|
|
76
|
+
focus: Type.Optional(Type.String()),
|
|
77
|
+
required: Type.Optional(Type.Boolean()),
|
|
78
|
+
}, { additionalProperties: false });
|
|
79
|
+
|
|
80
|
+
const AcceptanceOverride = Type.Unsafe({
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
criteria: {
|
|
84
|
+
type: "array",
|
|
85
|
+
items: {
|
|
86
|
+
anyOf: [
|
|
87
|
+
{ type: "string" },
|
|
88
|
+
AcceptanceGateSchema,
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
evidence: { type: "array", items: AcceptanceEvidenceKind },
|
|
93
|
+
verify: { type: "array", items: AcceptanceVerifyCommandSchema },
|
|
94
|
+
review: AcceptanceReviewGateSchema,
|
|
95
|
+
stopRules: { type: "array", items: { type: "string" } },
|
|
96
|
+
maxFinalizationTurns: { type: "integer", minimum: 1, maximum: 10 },
|
|
97
|
+
},
|
|
98
|
+
additionalProperties: false,
|
|
99
|
+
allOf: [{
|
|
100
|
+
anyOf: [
|
|
101
|
+
{ required: ["criteria"] },
|
|
102
|
+
{ required: ["evidence"] },
|
|
103
|
+
{ required: ["verify"] },
|
|
104
|
+
{ required: ["review"] },
|
|
105
|
+
{ required: ["stopRules"] },
|
|
106
|
+
],
|
|
107
|
+
}],
|
|
108
|
+
description: "Optional acceptance contract. Use this for goal-style requests such as /goal, goal, active goal, or work until evidence says done: criteria define the target, evidence/verify define proof, stopRules define constraints, and maxFinalizationTurns defines the bounded loop. When present, the child must complete a same-session self-review/repair loop before acceptance is evaluated.",
|
|
109
|
+
});
|
|
110
|
+
|
|
38
111
|
const TaskItem = Type.Object({
|
|
39
|
-
agent: Type.String(),
|
|
40
|
-
task: Type.String(),
|
|
112
|
+
agent: Type.String(),
|
|
113
|
+
task: Type.String(),
|
|
41
114
|
cwd: Type.Optional(Type.String()),
|
|
42
115
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
43
116
|
output: Type.Optional(OutputOverride),
|
|
@@ -46,12 +119,17 @@ const TaskItem = Type.Object({
|
|
|
46
119
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
|
|
47
120
|
model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
|
|
48
121
|
skill: Type.Optional(SkillOverride),
|
|
122
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
49
123
|
});
|
|
50
124
|
|
|
51
125
|
// Parallel task item (within a parallel step)
|
|
52
126
|
const ParallelTaskSchema = Type.Object({
|
|
53
127
|
agent: Type.String(),
|
|
54
128
|
task: Type.Optional(Type.String({ description: "Task template with {task}, {previous}, {chain_dir} variables. Defaults to {previous}." })),
|
|
129
|
+
phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
|
|
130
|
+
label: Type.Optional(Type.String({ description: "Optional user-facing label for this parallel task." })),
|
|
131
|
+
as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
|
|
132
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
55
133
|
cwd: Type.Optional(Type.String()),
|
|
56
134
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
57
135
|
output: Type.Optional(OutputOverride),
|
|
@@ -60,14 +138,51 @@ const ParallelTaskSchema = Type.Object({
|
|
|
60
138
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
61
139
|
skill: Type.Optional(SkillOverride),
|
|
62
140
|
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
141
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
63
142
|
});
|
|
64
143
|
|
|
144
|
+
const DynamicExpandSchema = Type.Object({
|
|
145
|
+
from: Type.Object({
|
|
146
|
+
output: Type.String({ description: "Prior named structured output to expand from." }),
|
|
147
|
+
path: Type.String({ description: "JSON Pointer into the structured output, e.g. /items." }),
|
|
148
|
+
}, { additionalProperties: false }),
|
|
149
|
+
item: Type.Optional(Type.String({ description: "Template variable name for each item. Defaults to item." })),
|
|
150
|
+
key: Type.Optional(Type.String({ description: "JSON Pointer relative to each item for stable child ids." })),
|
|
151
|
+
maxItems: Type.Optional(Type.Integer({ minimum: 0, description: "Required fanout bound unless configured globally." })),
|
|
152
|
+
onEmpty: Type.Optional(Type.String({ enum: ["skip", "fail"], description: "Empty input behavior. Defaults to skip." })),
|
|
153
|
+
}, { additionalProperties: false });
|
|
154
|
+
|
|
155
|
+
const DynamicParallelTemplateSchema = Type.Object({
|
|
156
|
+
agent: Type.String(),
|
|
157
|
+
task: Type.Optional(Type.String({ description: "Task template with {item}, {item.path}, {task}, {previous}, {chain_dir}, and {outputs.name} variables." })),
|
|
158
|
+
phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
|
|
159
|
+
label: Type.Optional(Type.String({ description: "Optional user-facing label; item templates are supported." })),
|
|
160
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
161
|
+
cwd: Type.Optional(Type.String()),
|
|
162
|
+
output: Type.Optional(OutputOverride),
|
|
163
|
+
outputMode: Type.Optional(OutputModeOverride),
|
|
164
|
+
reads: Type.Optional(ReadsOverride),
|
|
165
|
+
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
166
|
+
skill: Type.Optional(SkillOverride),
|
|
167
|
+
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
168
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
169
|
+
}, { additionalProperties: false });
|
|
170
|
+
|
|
171
|
+
const DynamicCollectSchema = Type.Object({
|
|
172
|
+
as: Type.String({ description: "Safe output name for the ordered collected result array." }),
|
|
173
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
174
|
+
}, { additionalProperties: false });
|
|
175
|
+
|
|
65
176
|
// Flattened so chain steps do not need an object-shape anyOf/oneOf union.
|
|
66
177
|
const ChainItem = Type.Object({
|
|
67
178
|
agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
|
|
68
179
|
task: Type.Optional(Type.String({
|
|
69
|
-
description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder. Required for first step, defaults to '{previous}' for subsequent steps."
|
|
180
|
+
description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder, {outputs.name}=prior named output. Required for first step, defaults to '{previous}' for subsequent steps."
|
|
70
181
|
})),
|
|
182
|
+
phase: Type.Optional(Type.String({ description: "Optional phase/group label for status and graph rendering." })),
|
|
183
|
+
label: Type.Optional(Type.String({ description: "Optional user-facing label for this chain step." })),
|
|
184
|
+
as: Type.Optional(Type.String({ description: "Optional safe identifier used as {outputs.name} in later chain steps." })),
|
|
185
|
+
outputSchema: Type.Optional(JsonSchemaObject),
|
|
71
186
|
cwd: Type.Optional(Type.String()),
|
|
72
187
|
output: Type.Optional(OutputOverride),
|
|
73
188
|
outputMode: Type.Optional(OutputModeOverride),
|
|
@@ -75,13 +190,30 @@ const ChainItem = Type.Object({
|
|
|
75
190
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
76
191
|
skill: Type.Optional(SkillOverride),
|
|
77
192
|
model: Type.Optional(Type.String({ description: "Override model for this step" })),
|
|
78
|
-
|
|
193
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
194
|
+
parallel: Type.Optional(Type.Unsafe({
|
|
195
|
+
anyOf: [
|
|
196
|
+
Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
|
|
197
|
+
DynamicParallelTemplateSchema,
|
|
198
|
+
],
|
|
199
|
+
description: "Static parallel tasks array, or a single dynamic fanout child template when expand/collect are present.",
|
|
200
|
+
})),
|
|
201
|
+
expand: Type.Optional(DynamicExpandSchema),
|
|
202
|
+
collect: Type.Optional(DynamicCollectSchema),
|
|
79
203
|
concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
|
|
80
204
|
failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
|
|
81
205
|
worktree: Type.Optional(Type.Boolean({
|
|
82
206
|
description: "Create isolated git worktrees for each parallel task."
|
|
83
207
|
})),
|
|
84
|
-
}, {
|
|
208
|
+
}, {
|
|
209
|
+
description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
|
|
210
|
+
additionalProperties: false,
|
|
211
|
+
allOf: [
|
|
212
|
+
{ if: { required: ["expand"] }, then: { required: ["parallel", "collect"], properties: { parallel: { type: "object" } } } },
|
|
213
|
+
{ if: { required: ["collect"] }, then: { required: ["expand", "parallel"], properties: { parallel: { type: "object" } } } },
|
|
214
|
+
{ not: { required: ["expand"], properties: { parallel: { type: "array", items: {} } } } },
|
|
215
|
+
],
|
|
216
|
+
});
|
|
85
217
|
|
|
86
218
|
const ControlOverrides = Type.Object({
|
|
87
219
|
enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
|
|
@@ -165,4 +297,5 @@ export const SubagentParams = Type.Object({
|
|
|
165
297
|
outputMode: Type.Optional(OutputModeOverride),
|
|
166
298
|
skill: Type.Optional(SkillOverride),
|
|
167
299
|
model: Type.Optional(Type.String({ description: "Override model for single agent (e.g. 'anthropic/claude-sonnet-4')" })),
|
|
300
|
+
acceptance: Type.Optional(AcceptanceOverride),
|
|
168
301
|
});
|