pi-subagents 0.29.0 → 0.31.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 +43 -0
- package/README.md +125 -19
- package/agents/context-builder.md +3 -3
- package/agents/planner.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/scout.md +1 -1
- package/package.json +7 -7
- package/skills/pi-subagents/SKILL.md +30 -0
- package/src/agents/agent-management.ts +189 -8
- package/src/agents/agent-serializer.ts +35 -12
- package/src/agents/agents.ts +243 -24
- package/src/agents/frontmatter.ts +66 -2
- package/src/agents/proactive-skills.ts +191 -0
- package/src/agents/skills.ts +117 -20
- package/src/extension/doctor.ts +20 -0
- package/src/extension/fanout-child.ts +2 -1
- package/src/extension/index.ts +50 -5
- package/src/extension/schemas.ts +40 -79
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/runs/background/async-execution.ts +180 -67
- package/src/runs/background/async-job-tracker.ts +56 -11
- package/src/runs/background/async-resume.ts +53 -5
- package/src/runs/background/async-status.ts +4 -1
- package/src/runs/background/chain-append.ts +282 -0
- package/src/runs/background/chain-root-attachment.ts +161 -0
- package/src/runs/background/result-watcher.ts +11 -2
- package/src/runs/background/run-status.ts +1 -0
- package/src/runs/background/stale-run-reconciler.ts +9 -4
- package/src/runs/background/subagent-runner.ts +158 -11
- package/src/runs/foreground/chain-execution.ts +26 -2
- package/src/runs/foreground/execution.ts +114 -8
- package/src/runs/foreground/subagent-executor.ts +611 -87
- package/src/runs/shared/acceptance.ts +285 -34
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +5 -3
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +13 -1
- package/src/runs/shared/pi-args.ts +12 -3
- package/src/runs/shared/single-output.ts +15 -1
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +17 -2
- package/src/shared/utils.ts +19 -1
- package/src/slash/prompt-template-bridge.ts +26 -3
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +34 -4
- package/src/tui/render.ts +265 -13
|
@@ -39,6 +39,10 @@ const VALID_EVIDENCE = new Set<AcceptanceEvidenceKind>([
|
|
|
39
39
|
"review-findings",
|
|
40
40
|
"manual-notes",
|
|
41
41
|
]);
|
|
42
|
+
const ACCEPTANCE_CONFIG_KEYS = new Set(["level", "criteria", "evidence", "verify", "review", "stopRules", "reason"]);
|
|
43
|
+
const ACCEPTANCE_GATE_KEYS = new Set(["id", "must", "evidence", "severity"]);
|
|
44
|
+
const ACCEPTANCE_VERIFY_KEYS = new Set(["id", "command", "timeoutMs", "cwd", "env", "allowFailure"]);
|
|
45
|
+
const ACCEPTANCE_REVIEW_KEYS = new Set(["agent", "focus", "required"]);
|
|
42
46
|
|
|
43
47
|
function normalizeLevel(level: AcceptanceLevel | undefined): Exclude<AcceptanceLevel, "auto"> | "auto" {
|
|
44
48
|
return level ?? "auto";
|
|
@@ -144,13 +148,44 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
144
148
|
return errors;
|
|
145
149
|
}
|
|
146
150
|
const value = input as Record<string, unknown>;
|
|
151
|
+
for (const key of Object.keys(value)) {
|
|
152
|
+
if (!ACCEPTANCE_CONFIG_KEYS.has(key)) errors.push(`${pathLabel}.${key} is not supported.`);
|
|
153
|
+
}
|
|
147
154
|
if (value.level !== undefined && (typeof value.level !== "string" || !VALID_LEVELS.has(value.level as AcceptanceLevel))) {
|
|
148
155
|
errors.push(`${pathLabel}.level must be one of auto, none, attested, checked, verified, reviewed.`);
|
|
149
156
|
}
|
|
150
157
|
if (value.level === "none" && (typeof value.reason !== "string" || !value.reason.trim())) {
|
|
151
158
|
errors.push(`${pathLabel}.reason is required when level is none.`);
|
|
152
159
|
}
|
|
160
|
+
if (value.reason !== undefined && typeof value.reason !== "string") errors.push(`${pathLabel}.reason must be a string.`);
|
|
153
161
|
if (value.criteria !== undefined && !Array.isArray(value.criteria)) errors.push(`${pathLabel}.criteria must be an array.`);
|
|
162
|
+
if (Array.isArray(value.criteria)) {
|
|
163
|
+
for (const [index, criterion] of value.criteria.entries()) {
|
|
164
|
+
if (typeof criterion === "string") continue;
|
|
165
|
+
const criterionPath = `${pathLabel}.criteria[${index}]`;
|
|
166
|
+
if (!criterion || typeof criterion !== "object" || Array.isArray(criterion)) {
|
|
167
|
+
errors.push(`${criterionPath} must be a string or an object.`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const gate = criterion as Record<string, unknown>;
|
|
171
|
+
for (const key of Object.keys(gate)) {
|
|
172
|
+
if (!ACCEPTANCE_GATE_KEYS.has(key)) errors.push(`${criterionPath}.${key} is not supported.`);
|
|
173
|
+
}
|
|
174
|
+
if (typeof gate.id !== "string" || !gate.id.trim()) errors.push(`${criterionPath}.id is required.`);
|
|
175
|
+
if (typeof gate.must !== "string" || !gate.must.trim()) errors.push(`${criterionPath}.must is required.`);
|
|
176
|
+
if (gate.evidence !== undefined && !Array.isArray(gate.evidence)) errors.push(`${criterionPath}.evidence must be an array.`);
|
|
177
|
+
if (Array.isArray(gate.evidence)) {
|
|
178
|
+
for (const [evidenceIndex, item] of gate.evidence.entries()) {
|
|
179
|
+
if (typeof item !== "string" || !VALID_EVIDENCE.has(item as AcceptanceEvidenceKind)) {
|
|
180
|
+
errors.push(`${criterionPath}.evidence[${evidenceIndex}] is not a supported evidence kind.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (gate.severity !== undefined && gate.severity !== "required" && gate.severity !== "recommended") {
|
|
185
|
+
errors.push(`${criterionPath}.severity must be required or recommended.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
154
189
|
if (Array.isArray(value.evidence)) {
|
|
155
190
|
for (const [index, item] of value.evidence.entries()) {
|
|
156
191
|
if (typeof item !== "string" || !VALID_EVIDENCE.has(item as AcceptanceEvidenceKind)) {
|
|
@@ -168,11 +203,46 @@ export function validateAcceptanceInput(input: unknown, pathLabel = "acceptance"
|
|
|
168
203
|
continue;
|
|
169
204
|
}
|
|
170
205
|
const cmd = command as Record<string, unknown>;
|
|
206
|
+
for (const key of Object.keys(cmd)) {
|
|
207
|
+
if (!ACCEPTANCE_VERIFY_KEYS.has(key)) errors.push(`${pathLabel}.verify[${index}].${key} is not supported.`);
|
|
208
|
+
}
|
|
171
209
|
if (typeof cmd.id !== "string" || !cmd.id.trim()) errors.push(`${pathLabel}.verify[${index}].id is required.`);
|
|
172
210
|
if (typeof cmd.command !== "string" || !cmd.command.trim()) errors.push(`${pathLabel}.verify[${index}].command is required.`);
|
|
173
|
-
if (cmd.timeoutMs !== undefined && (typeof cmd.timeoutMs !== "number" || cmd.timeoutMs
|
|
174
|
-
errors.push(`${pathLabel}.verify[${index}].timeoutMs must be
|
|
211
|
+
if (cmd.timeoutMs !== undefined && (typeof cmd.timeoutMs !== "number" || !Number.isInteger(cmd.timeoutMs) || cmd.timeoutMs < 1)) {
|
|
212
|
+
errors.push(`${pathLabel}.verify[${index}].timeoutMs must be an integer >= 1.`);
|
|
175
213
|
}
|
|
214
|
+
if (cmd.cwd !== undefined && typeof cmd.cwd !== "string") errors.push(`${pathLabel}.verify[${index}].cwd must be a string.`);
|
|
215
|
+
if (cmd.env !== undefined) {
|
|
216
|
+
if (!cmd.env || typeof cmd.env !== "object" || Array.isArray(cmd.env)) {
|
|
217
|
+
errors.push(`${pathLabel}.verify[${index}].env must be an object.`);
|
|
218
|
+
} else {
|
|
219
|
+
for (const [envKey, envValue] of Object.entries(cmd.env as Record<string, unknown>)) {
|
|
220
|
+
if (typeof envValue !== "string") errors.push(`${pathLabel}.verify[${index}].env.${envKey} must be a string.`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (cmd.allowFailure !== undefined && typeof cmd.allowFailure !== "boolean") {
|
|
225
|
+
errors.push(`${pathLabel}.verify[${index}].allowFailure must be a boolean.`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (value.review !== undefined && value.review !== false) {
|
|
230
|
+
if (!value.review || typeof value.review !== "object" || Array.isArray(value.review)) {
|
|
231
|
+
errors.push(`${pathLabel}.review must be false or an object.`);
|
|
232
|
+
} else {
|
|
233
|
+
const review = value.review as Record<string, unknown>;
|
|
234
|
+
for (const key of Object.keys(review)) {
|
|
235
|
+
if (!ACCEPTANCE_REVIEW_KEYS.has(key)) errors.push(`${pathLabel}.review.${key} is not supported.`);
|
|
236
|
+
}
|
|
237
|
+
if (review.agent !== undefined && typeof review.agent !== "string") errors.push(`${pathLabel}.review.agent must be a string.`);
|
|
238
|
+
if (review.focus !== undefined && typeof review.focus !== "string") errors.push(`${pathLabel}.review.focus must be a string.`);
|
|
239
|
+
if (review.required !== undefined && typeof review.required !== "boolean") errors.push(`${pathLabel}.review.required must be a boolean.`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (value.stopRules !== undefined && !Array.isArray(value.stopRules)) errors.push(`${pathLabel}.stopRules must be an array.`);
|
|
243
|
+
if (Array.isArray(value.stopRules)) {
|
|
244
|
+
for (const [index, item] of value.stopRules.entries()) {
|
|
245
|
+
if (typeof item !== "string") errors.push(`${pathLabel}.stopRules[${index}] must be a string.`);
|
|
176
246
|
}
|
|
177
247
|
}
|
|
178
248
|
return errors;
|
|
@@ -258,16 +328,19 @@ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig): st
|
|
|
258
328
|
lines.push(
|
|
259
329
|
"",
|
|
260
330
|
"Finish with a fenced JSON block tagged `acceptance-report` in this shape:",
|
|
331
|
+
"Use empty arrays when no items apply; array fields contain strings unless object entries are shown.",
|
|
261
332
|
"```acceptance-report",
|
|
262
333
|
JSON.stringify({
|
|
263
334
|
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "specific proof" }],
|
|
264
|
-
changedFiles: [],
|
|
265
|
-
testsAddedOrUpdated: [],
|
|
335
|
+
changedFiles: ["src/file.ts"],
|
|
336
|
+
testsAddedOrUpdated: ["test/file.test.ts"],
|
|
266
337
|
commandsRun: [{ command: "command", result: "passed", summary: "short result" }],
|
|
267
|
-
validationOutput: [],
|
|
268
|
-
residualRisks: [],
|
|
338
|
+
validationOutput: ["validation output or concise summary"],
|
|
339
|
+
residualRisks: ["none"],
|
|
269
340
|
noStagedFiles: true,
|
|
270
|
-
|
|
341
|
+
diffSummary: "short description of the diff",
|
|
342
|
+
reviewFindings: ["blocker: file.ts:12 - issue found, or no blockers"],
|
|
343
|
+
manualNotes: "anything else the parent should know",
|
|
271
344
|
}, null, 2),
|
|
272
345
|
"```",
|
|
273
346
|
);
|
|
@@ -299,43 +372,142 @@ function extractBalancedJson(text: string, start: number): string | undefined {
|
|
|
299
372
|
return undefined;
|
|
300
373
|
}
|
|
301
374
|
|
|
302
|
-
|
|
303
|
-
|
|
375
|
+
function unwrapAcceptanceReport(value: unknown): unknown {
|
|
376
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
377
|
+
const record = value as { acceptance?: unknown; "acceptance-report"?: unknown };
|
|
378
|
+
if ("acceptance" in record) return record.acceptance;
|
|
379
|
+
if ("acceptance-report" in record) return record["acceptance-report"];
|
|
380
|
+
return value;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function isCommandsRunArray(value: unknown): value is NonNullable<AcceptanceReport["commandsRun"]> {
|
|
384
|
+
return Array.isArray(value) && value.every((item) => {
|
|
385
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
386
|
+
const command = item as { command?: unknown; result?: unknown; summary?: unknown };
|
|
387
|
+
return typeof command.command === "string"
|
|
388
|
+
&& (command.result === "passed" || command.result === "failed" || command.result === "not-run")
|
|
389
|
+
&& typeof command.summary === "string";
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function hasGenericAcceptanceReportSignal(value: unknown): boolean {
|
|
394
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
395
|
+
const record = value as Record<string, unknown>;
|
|
396
|
+
return "criteriaSatisfied" in record && (
|
|
397
|
+
isStringArray(record.changedFiles)
|
|
398
|
+
|| isStringArray(record.testsAddedOrUpdated)
|
|
399
|
+
|| isCommandsRunArray(record.commandsRun)
|
|
400
|
+
|| isStringArray(record.validationOutput)
|
|
401
|
+
|| isStringArray(record.residualRisks)
|
|
402
|
+
|| typeof record.noStagedFiles === "boolean"
|
|
403
|
+
|| typeof record.diffSummary === "string"
|
|
404
|
+
|| isStringArray(record.reviewFindings)
|
|
405
|
+
|| typeof record.manualNotes === "string"
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function parseReportJson(body: string): unknown {
|
|
410
|
+
const trimmed = body.trim();
|
|
411
|
+
try {
|
|
412
|
+
return JSON.parse(trimmed) as unknown;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
const jsonStart = trimmed.indexOf("{");
|
|
415
|
+
if (jsonStart > 0) {
|
|
416
|
+
const json = extractBalancedJson(trimmed, jsonStart);
|
|
417
|
+
if (json) return JSON.parse(json) as unknown;
|
|
418
|
+
}
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function fencedBlocks(output: string, tag: string): string[] {
|
|
424
|
+
return [...output.matchAll(new RegExp(`\`\`\`${tag}\\s*\\n([\\s\\S]*?)\`\`\``, "gi"))]
|
|
304
425
|
.map((match) => match[1]?.trim())
|
|
305
426
|
.filter((value): value is string => Boolean(value));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function validationPathLabelForWrapper(value: unknown): string {
|
|
430
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
|
431
|
+
const record = value as Record<string, unknown>;
|
|
432
|
+
if ("acceptance" in record) return "acceptance";
|
|
433
|
+
if ("acceptance-report" in record) return "acceptance-report";
|
|
434
|
+
return "";
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseAcceptanceReportBody(body: string): { report?: AcceptanceReport; errors: string[] } {
|
|
438
|
+
const parsed = parseReportJson(body);
|
|
439
|
+
const report = unwrapAcceptanceReport(parsed);
|
|
440
|
+
return validateAcceptanceReport(report, validationPathLabelForWrapper(parsed));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function parseGenericJsonAcceptanceReportBody(body: string): AcceptanceReport | undefined {
|
|
444
|
+
const parsed = parseReportJson(body);
|
|
445
|
+
const report = unwrapAcceptanceReport(parsed);
|
|
446
|
+
const validation = validateAcceptanceReport(report);
|
|
447
|
+
if (!validation.report) return undefined;
|
|
448
|
+
return hasGenericAcceptanceReportSignal(validation.report) ? validation.report : undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function parseAcceptanceReport(output: string): { report?: AcceptanceReport; error?: string } {
|
|
452
|
+
const fenced = fencedBlocks(output, "acceptance-report");
|
|
306
453
|
const parseErrors: string[] = [];
|
|
307
454
|
for (const body of fenced) {
|
|
308
455
|
try {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
: parsed;
|
|
313
|
-
if (isAcceptanceReport(report)) return { report };
|
|
314
|
-
parseErrors.push("acceptance-report block does not contain a valid acceptance report");
|
|
456
|
+
const validation = parseAcceptanceReportBody(body);
|
|
457
|
+
if (validation.report) return { report: validation.report };
|
|
458
|
+
parseErrors.push(`Invalid acceptance-report: ${validation.errors.join("; ")}`);
|
|
315
459
|
} catch (error) {
|
|
316
460
|
parseErrors.push(error instanceof Error ? error.message : String(error));
|
|
317
461
|
}
|
|
318
462
|
}
|
|
319
463
|
if (parseErrors.length > 0) return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}` };
|
|
464
|
+
for (const body of fencedBlocks(output, "(?:json|jsonc|json5)")) {
|
|
465
|
+
try {
|
|
466
|
+
const report = parseGenericJsonAcceptanceReportBody(body);
|
|
467
|
+
if (report) return { report };
|
|
468
|
+
} catch {
|
|
469
|
+
// Ignore unrelated or malformed generic JSON fences; only explicit
|
|
470
|
+
// acceptance-report fences should turn parse failures into blockers.
|
|
471
|
+
}
|
|
472
|
+
}
|
|
320
473
|
const markerIndex = output.search(/ACCEPTANCE_REPORT\s*:/i);
|
|
321
474
|
if (markerIndex !== -1) {
|
|
322
475
|
const jsonStart = output.indexOf("{", markerIndex);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
476
|
+
if (jsonStart !== -1) {
|
|
477
|
+
const json = extractBalancedJson(output, jsonStart);
|
|
478
|
+
if (json) {
|
|
479
|
+
try {
|
|
480
|
+
const parsed = JSON.parse(json) as unknown;
|
|
481
|
+
const report = unwrapAcceptanceReport(parsed);
|
|
482
|
+
const validation = validateAcceptanceReport(report, validationPathLabelForWrapper(parsed));
|
|
483
|
+
if (validation.report) return { report: validation.report };
|
|
484
|
+
return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}` };
|
|
485
|
+
} catch (error) {
|
|
486
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
487
|
+
}
|
|
331
488
|
}
|
|
332
489
|
}
|
|
333
490
|
}
|
|
334
|
-
}
|
|
335
491
|
return { error: "Structured acceptance report not found." };
|
|
336
492
|
}
|
|
337
493
|
|
|
338
494
|
export function stripAcceptanceReport(output: string): string {
|
|
495
|
+
const trailingFencePattern = /\n?```(acceptance-report|json|jsonc|json5)\s*\n([\s\S]*?)```\s*/gi;
|
|
496
|
+
let trailingFence: { index: number; tag: string; body: string } | undefined;
|
|
497
|
+
for (const match of output.matchAll(trailingFencePattern)) {
|
|
498
|
+
const end = (match.index ?? 0) + match[0].length;
|
|
499
|
+
if (output.slice(end).trim().length === 0 && match[1] && match[2]) {
|
|
500
|
+
trailingFence = { index: match.index ?? 0, tag: match[1].toLowerCase(), body: match[2] };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (trailingFence) {
|
|
504
|
+
if (trailingFence.tag === "acceptance-report") return output.slice(0, trailingFence.index).trimEnd();
|
|
505
|
+
try {
|
|
506
|
+
if (parseGenericJsonAcceptanceReportBody(trailingFence.body)) return output.slice(0, trailingFence.index).trimEnd();
|
|
507
|
+
} catch {
|
|
508
|
+
// Leave unrelated or malformed generic JSON fences visible.
|
|
509
|
+
}
|
|
510
|
+
}
|
|
339
511
|
return output
|
|
340
512
|
.replace(/\n?```acceptance-report\s*\n[\s\S]*?```\s*$/i, "")
|
|
341
513
|
.replace(/\n?ACCEPTANCE_REPORT\s*:\s*\{[\s\S]*\}\s*$/i, "")
|
|
@@ -346,26 +518,105 @@ function isStringArray(value: unknown): value is string[] {
|
|
|
346
518
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
347
519
|
}
|
|
348
520
|
|
|
349
|
-
function
|
|
350
|
-
|
|
521
|
+
function pathFor(base: string, segment: string): string {
|
|
522
|
+
return base ? `${base}.${segment}` : segment;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function describeValidationValue(value: unknown): string {
|
|
526
|
+
if (value === undefined) return "missing";
|
|
527
|
+
if (value === null) return "null";
|
|
528
|
+
if (Array.isArray(value)) return "array";
|
|
529
|
+
if (typeof value === "object") return "object";
|
|
530
|
+
if (typeof value === "string") {
|
|
531
|
+
const short = value.length > 80 ? `${value.slice(0, 77)}...` : value;
|
|
532
|
+
return JSON.stringify(short);
|
|
533
|
+
}
|
|
534
|
+
return `${typeof value} ${String(value)}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function pushTypeError(errors: string[], pathLabel: string, expected: string, value: unknown): void {
|
|
538
|
+
errors.push(`${pathLabel}: expected ${expected}; got ${describeValidationValue(value)}`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function validateStringArrayField(errors: string[], value: unknown, pathLabel: string): void {
|
|
542
|
+
if (!Array.isArray(value)) {
|
|
543
|
+
pushTypeError(errors, pathLabel, "string[]", value);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
for (const [index, item] of value.entries()) {
|
|
547
|
+
if (typeof item !== "string") pushTypeError(errors, `${pathLabel}[${index}]`, "string", item);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function validateAcceptanceReport(value: unknown, pathLabel = ""): { report?: AcceptanceReport; errors: string[] } {
|
|
552
|
+
const errors: string[] = [];
|
|
553
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
554
|
+
pushTypeError(errors, pathLabel || "acceptance-report", "object", value);
|
|
555
|
+
return { errors };
|
|
556
|
+
}
|
|
351
557
|
const report = value as AcceptanceReport;
|
|
352
558
|
if (report.criteriaSatisfied !== undefined) {
|
|
353
|
-
if (!Array.isArray(report.criteriaSatisfied))
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
559
|
+
if (!Array.isArray(report.criteriaSatisfied)) {
|
|
560
|
+
pushTypeError(errors, pathFor(pathLabel, "criteriaSatisfied"), "array", report.criteriaSatisfied);
|
|
561
|
+
} else {
|
|
562
|
+
for (const [index, item] of report.criteriaSatisfied.entries()) {
|
|
563
|
+
const itemPath = `${pathFor(pathLabel, "criteriaSatisfied")}[${index}]`;
|
|
564
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
565
|
+
pushTypeError(errors, itemPath, "object", item);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const criterion = item as { id?: unknown; status?: unknown; evidence?: unknown };
|
|
569
|
+
if (criterion.id !== undefined && typeof criterion.id !== "string") pushTypeError(errors, `${itemPath}.id`, "string", criterion.id);
|
|
570
|
+
if (criterion.status !== "satisfied" && criterion.status !== "not-satisfied" && criterion.status !== "not-applicable") {
|
|
571
|
+
pushTypeError(errors, `${itemPath}.status`, "one of \"satisfied\", \"not-satisfied\", \"not-applicable\"", criterion.status);
|
|
572
|
+
}
|
|
573
|
+
if (typeof criterion.evidence !== "string" || !criterion.evidence.trim()) pushTypeError(errors, `${itemPath}.evidence`, "non-empty string", criterion.evidence);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (report.changedFiles !== undefined) validateStringArrayField(errors, report.changedFiles, pathFor(pathLabel, "changedFiles"));
|
|
578
|
+
if (report.testsAddedOrUpdated !== undefined) validateStringArrayField(errors, report.testsAddedOrUpdated, pathFor(pathLabel, "testsAddedOrUpdated"));
|
|
579
|
+
if (report.commandsRun !== undefined) {
|
|
580
|
+
if (!Array.isArray(report.commandsRun)) {
|
|
581
|
+
pushTypeError(errors, pathFor(pathLabel, "commandsRun"), "array", report.commandsRun);
|
|
582
|
+
} else {
|
|
583
|
+
for (const [index, item] of report.commandsRun.entries()) {
|
|
584
|
+
const itemPath = `${pathFor(pathLabel, "commandsRun")}[${index}]`;
|
|
585
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
586
|
+
pushTypeError(errors, itemPath, "object", item);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const command = item as { command?: unknown; result?: unknown; summary?: unknown };
|
|
590
|
+
if (typeof command.command !== "string" || !command.command.trim()) pushTypeError(errors, `${itemPath}.command`, "non-empty string", command.command);
|
|
591
|
+
if (command.result !== "passed" && command.result !== "failed" && command.result !== "not-run") {
|
|
592
|
+
pushTypeError(errors, `${itemPath}.result`, "one of \"passed\", \"failed\", \"not-run\"", command.result);
|
|
593
|
+
}
|
|
594
|
+
if (typeof command.summary !== "string") pushTypeError(errors, `${itemPath}.summary`, "string", command.summary);
|
|
595
|
+
}
|
|
360
596
|
}
|
|
361
597
|
}
|
|
362
|
-
|
|
598
|
+
if (report.validationOutput !== undefined) validateStringArrayField(errors, report.validationOutput, pathFor(pathLabel, "validationOutput"));
|
|
599
|
+
if (report.residualRisks !== undefined) validateStringArrayField(errors, report.residualRisks, pathFor(pathLabel, "residualRisks"));
|
|
600
|
+
if (report.noStagedFiles !== undefined && typeof report.noStagedFiles !== "boolean") pushTypeError(errors, pathFor(pathLabel, "noStagedFiles"), "boolean", report.noStagedFiles);
|
|
601
|
+
if (report.diffSummary !== undefined && typeof report.diffSummary !== "string") pushTypeError(errors, pathFor(pathLabel, "diffSummary"), "string", report.diffSummary);
|
|
602
|
+
if (report.reviewFindings !== undefined) validateStringArrayField(errors, report.reviewFindings, pathFor(pathLabel, "reviewFindings"));
|
|
603
|
+
if (report.manualNotes !== undefined && typeof report.manualNotes !== "string") pushTypeError(errors, pathFor(pathLabel, "manualNotes"), "string", report.manualNotes);
|
|
604
|
+
if (report.notes !== undefined && typeof report.notes !== "string") pushTypeError(errors, pathFor(pathLabel, "notes"), "string", report.notes);
|
|
605
|
+
if (errors.length > 0) return { errors };
|
|
606
|
+
const hasReportField = report.criteriaSatisfied !== undefined
|
|
363
607
|
|| report.changedFiles !== undefined
|
|
364
608
|
|| report.testsAddedOrUpdated !== undefined
|
|
365
609
|
|| report.commandsRun !== undefined
|
|
610
|
+
|| report.validationOutput !== undefined
|
|
366
611
|
|| report.residualRisks !== undefined
|
|
612
|
+
|| report.noStagedFiles !== undefined
|
|
613
|
+
|| report.diffSummary !== undefined
|
|
367
614
|
|| report.manualNotes !== undefined
|
|
615
|
+
|| report.notes !== undefined
|
|
368
616
|
|| report.reviewFindings !== undefined;
|
|
617
|
+
return hasReportField
|
|
618
|
+
? { report, errors }
|
|
619
|
+
: { errors: [`${pathLabel || "acceptance-report"}: expected at least one acceptance report field`] };
|
|
369
620
|
}
|
|
370
621
|
|
|
371
622
|
function checkCriteriaSatisfied(criteria: ResolvedAcceptanceGate[], report: AcceptanceReport): AcceptanceRuntimeCheck[] {
|
|
@@ -8,6 +8,11 @@ const SAFE_OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
8
8
|
|
|
9
9
|
export class ChainOutputValidationError extends Error {}
|
|
10
10
|
|
|
11
|
+
export interface ChainOutputValidationContext {
|
|
12
|
+
priorOutputNames?: Iterable<string>;
|
|
13
|
+
startStepIndex?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
function outputNamesForStep(step: ChainStep): string[] {
|
|
12
17
|
if (isParallelStep(step)) return step.parallel.map((task) => task.as).filter((name): name is string => Boolean(name));
|
|
13
18
|
if (isDynamicParallelStep(step)) return [step.collect.as];
|
|
@@ -22,27 +27,37 @@ function taskTemplatesForStep(step: ChainStep): string[] {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutConfig: DynamicFanoutConfig = {}): void {
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
validateChainOutputBindingsWithContext(steps, dynamicFanoutConfig);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateChainOutputBindingsWithContext(
|
|
34
|
+
steps: ChainStep[],
|
|
35
|
+
dynamicFanoutConfig: DynamicFanoutConfig = {},
|
|
36
|
+
context: ChainOutputValidationContext = {},
|
|
37
|
+
): void {
|
|
38
|
+
const priorOutputNames = [...(context.priorOutputNames ?? [])];
|
|
39
|
+
const available = new Set<string>(priorOutputNames);
|
|
40
|
+
const seen = new Set<string>(priorOutputNames);
|
|
27
41
|
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
42
|
+
const displayStepIndex = (context.startStepIndex ?? 0) + stepIndex + 1;
|
|
28
43
|
const step = steps[stepIndex]!;
|
|
29
44
|
if (hasDynamicFanoutFields(step)) {
|
|
30
45
|
if (!isDynamicParallelStep(step)) {
|
|
31
|
-
throw new ChainOutputValidationError(`Dynamic chain step ${
|
|
46
|
+
throw new ChainOutputValidationError(`Dynamic chain step ${displayStepIndex} requires expand, a single parallel template object, and collect; dynamic expand/collect cannot be mixed with static parallel arrays.`);
|
|
32
47
|
}
|
|
33
48
|
try {
|
|
34
|
-
validateDynamicStepShape(step,
|
|
49
|
+
validateDynamicStepShape(step, displayStepIndex - 1, dynamicFanoutConfig);
|
|
35
50
|
} catch (error) {
|
|
36
51
|
if (error instanceof DynamicFanoutError) throw new ChainOutputValidationError(error.message);
|
|
37
52
|
throw error;
|
|
38
53
|
}
|
|
39
54
|
if (!available.has(step.expand.from.output)) {
|
|
40
|
-
throw new ChainOutputValidationError(`Dynamic chain step ${
|
|
55
|
+
throw new ChainOutputValidationError(`Dynamic chain step ${displayStepIndex} references unknown output '${step.expand.from.output}'. Named outputs are only available after producing step/group completes.`);
|
|
41
56
|
}
|
|
42
57
|
}
|
|
43
58
|
for (const name of outputNamesForStep(step)) {
|
|
44
59
|
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) {
|
|
45
|
-
throw new ChainOutputValidationError(`Invalid chain output name '${name}' at step ${
|
|
60
|
+
throw new ChainOutputValidationError(`Invalid chain output name '${name}' at step ${displayStepIndex}. Use /^[A-Za-z_][A-Za-z0-9_]*$/.`);
|
|
46
61
|
}
|
|
47
62
|
if (seen.has(name)) {
|
|
48
63
|
throw new ChainOutputValidationError(`Duplicate chain output name '${name}'. Each as name must be unique.`);
|
|
@@ -54,10 +69,10 @@ export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutCon
|
|
|
54
69
|
const rawReference = match[0];
|
|
55
70
|
const name = match[1]!;
|
|
56
71
|
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) {
|
|
57
|
-
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}' at step ${
|
|
72
|
+
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}' at step ${displayStepIndex}. Use {outputs.name} with /^[A-Za-z_][A-Za-z0-9_]*$/ names.`);
|
|
58
73
|
}
|
|
59
74
|
if (!available.has(name)) {
|
|
60
|
-
throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}' at step ${
|
|
75
|
+
throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}' at step ${displayStepIndex}. Named outputs are only available after producing step/group completes.`);
|
|
61
76
|
}
|
|
62
77
|
}
|
|
63
78
|
}
|
|
@@ -84,7 +84,7 @@ function stripFrameworkInstructions(task: string): string {
|
|
|
84
84
|
return task
|
|
85
85
|
.split("\n")
|
|
86
86
|
.filter((line) => !/^\s*\[(?:Write to|Read from):/i.test(line))
|
|
87
|
-
.filter((line) => !/^\s*(?:Create and maintain progress at:|Update progress at
|
|
87
|
+
.filter((line) => !/^\s*(?:Create and maintain progress at:|Update progress at:|\*\*Output:\*\*|Write your findings to(?: exactly this path)?:|This path is authoritative for this run\.|Ignore any other output filename or output path mentioned elsewhere)/i.test(line))
|
|
88
88
|
.join("\n");
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -26,6 +26,7 @@ export interface DynamicCollectedResult {
|
|
|
26
26
|
text: string;
|
|
27
27
|
structured?: unknown;
|
|
28
28
|
error?: string;
|
|
29
|
+
timedOut?: boolean;
|
|
29
30
|
outputPath?: string;
|
|
30
31
|
artifactPaths?: ArtifactPaths;
|
|
31
32
|
}
|
|
@@ -48,8 +49,8 @@ const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outpu
|
|
|
48
49
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
49
50
|
...DYNAMIC_PARALLEL_KEYS,
|
|
50
51
|
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "maxSubagentDepth",
|
|
51
|
-
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "mcpDirectTools", "completionGuard", "systemPrompt",
|
|
52
|
-
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance",
|
|
52
|
+
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "completionGuard", "systemPrompt",
|
|
53
|
+
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance", "parentSessionId",
|
|
53
54
|
]);
|
|
54
55
|
const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
|
|
55
56
|
|
|
@@ -262,7 +263,7 @@ export function materializeDynamicParallelStep(step: DynamicParallelStep, output
|
|
|
262
263
|
export function collectDynamicResults(
|
|
263
264
|
step: DynamicParallelStep,
|
|
264
265
|
items: DynamicMaterializedItem[],
|
|
265
|
-
results: Array<Pick<SingleResult, "agent" | "exitCode" | "error" | "structuredOutput" | "artifactPaths" | "savedOutputPath"> & { output?: string; finalOutput?: string }>,
|
|
266
|
+
results: Array<Pick<SingleResult, "agent" | "exitCode" | "error" | "timedOut" | "structuredOutput" | "artifactPaths" | "savedOutputPath"> & { output?: string; finalOutput?: string }>,
|
|
266
267
|
): DynamicCollectedResult[] {
|
|
267
268
|
return items.map((entry, index) => {
|
|
268
269
|
const result = results[index];
|
|
@@ -278,6 +279,7 @@ export function collectDynamicResults(
|
|
|
278
279
|
text,
|
|
279
280
|
...(result?.structuredOutput !== undefined ? { structured: result.structuredOutput } : {}),
|
|
280
281
|
...(result?.error ? { error: result.error } : {}),
|
|
282
|
+
...(result?.timedOut ? { timedOut: true } : {}),
|
|
281
283
|
...(result?.savedOutputPath ? { outputPath: result.savedOutputPath } : {}),
|
|
282
284
|
...(result?.artifactPaths ? { artifactPaths: result.artifactPaths } : {}),
|
|
283
285
|
};
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { getAgentDir } from "../../shared/utils.ts";
|
|
5
|
+
import { getAgentDir, getProjectConfigDir } from "../../shared/utils.ts";
|
|
6
6
|
|
|
7
7
|
const CACHE_VERSION = 1;
|
|
8
8
|
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -112,7 +112,7 @@ function loadMcpConfig(cwd: string): McpConfig {
|
|
|
112
112
|
function getConfigPaths(cwd: string): string[] {
|
|
113
113
|
const piGlobalPath = path.join(getAgentDir(), "mcp.json");
|
|
114
114
|
const projectPath = path.resolve(cwd, ".mcp.json");
|
|
115
|
-
const projectPiPath = path.resolve(cwd, "
|
|
115
|
+
const projectPiPath = path.resolve(getProjectConfigDir(cwd), "mcp.json");
|
|
116
116
|
const sources: string[] = [];
|
|
117
117
|
if (GENERIC_GLOBAL_CONFIG_PATH !== piGlobalPath) sources.push(GENERIC_GLOBAL_CONFIG_PATH);
|
|
118
118
|
sources.push(piGlobalPath);
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export interface RunnerSubagentStep {
|
|
2
|
+
/** Session id of the direct parent session for permission-system ask forwarding. */
|
|
3
|
+
parentSessionId?: string;
|
|
2
4
|
agent: string;
|
|
3
5
|
task: string;
|
|
6
|
+
importAsyncRoot?: {
|
|
7
|
+
runId: string;
|
|
8
|
+
asyncDir: string;
|
|
9
|
+
resultPath: string;
|
|
10
|
+
index: number;
|
|
11
|
+
};
|
|
4
12
|
phase?: string;
|
|
5
13
|
label?: string;
|
|
6
14
|
outputName?: string;
|
|
@@ -11,6 +19,7 @@ export interface RunnerSubagentStep {
|
|
|
11
19
|
modelCandidates?: string[];
|
|
12
20
|
tools?: string[];
|
|
13
21
|
extensions?: string[];
|
|
22
|
+
subagentOnlyExtensions?: string[];
|
|
14
23
|
mcpDirectTools?: string[];
|
|
15
24
|
completionGuard?: boolean;
|
|
16
25
|
systemPrompt?: string | null;
|
|
@@ -101,6 +110,7 @@ export interface ParallelTaskResult {
|
|
|
101
110
|
output: string;
|
|
102
111
|
exitCode: number | null;
|
|
103
112
|
error?: string;
|
|
113
|
+
timedOut?: boolean;
|
|
104
114
|
model?: string;
|
|
105
115
|
attemptedModels?: string[];
|
|
106
116
|
outputTargetPath?: string;
|
|
@@ -117,7 +127,9 @@ export function aggregateParallelOutputs(
|
|
|
117
127
|
const header = headerFormat(r.taskIndex ?? i, r.agent);
|
|
118
128
|
const hasOutput = Boolean(r.output?.trim());
|
|
119
129
|
const status =
|
|
120
|
-
r.
|
|
130
|
+
r.timedOut
|
|
131
|
+
? `TIMED OUT${r.error ? `: ${r.error}` : ""}`
|
|
132
|
+
: r.exitCode === -1
|
|
121
133
|
? "SKIPPED"
|
|
122
134
|
: r.exitCode !== 0 && r.exitCode !== null
|
|
123
135
|
? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
|
|
@@ -25,8 +25,10 @@ export const SUBAGENT_PARENT_CHILD_INDEX_ENV = "PI_SUBAGENT_PARENT_CHILD_INDEX";
|
|
|
25
25
|
export const SUBAGENT_PARENT_DEPTH_ENV = "PI_SUBAGENT_PARENT_DEPTH";
|
|
26
26
|
export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH";
|
|
27
27
|
export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV = "PI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
|
|
28
|
+
export const SUBAGENT_PARENT_SESSION_ENV = "PI_SUBAGENT_PARENT_SESSION";
|
|
28
29
|
|
|
29
30
|
interface BuildPiArgsInput {
|
|
31
|
+
parentSessionId?: string;
|
|
30
32
|
baseArgs: string[];
|
|
31
33
|
task: string;
|
|
32
34
|
sessionEnabled: boolean;
|
|
@@ -37,8 +39,10 @@ interface BuildPiArgsInput {
|
|
|
37
39
|
systemPromptMode?: "append" | "replace";
|
|
38
40
|
inheritProjectContext: boolean;
|
|
39
41
|
inheritSkills: boolean;
|
|
42
|
+
requireReadTool?: boolean;
|
|
40
43
|
tools?: string[];
|
|
41
44
|
extensions?: string[];
|
|
45
|
+
subagentOnlyExtensions?: string[];
|
|
42
46
|
systemPrompt?: string | null;
|
|
43
47
|
mcpDirectTools?: string[];
|
|
44
48
|
cwd?: string;
|
|
@@ -97,7 +101,10 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
97
101
|
args.push("--model", modelArg);
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
const
|
|
104
|
+
const declaredBuiltinToolsBase = input.tools?.filter((tool) => !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) ?? [];
|
|
105
|
+
const declaredBuiltinTools = input.requireReadTool && input.tools?.length && !declaredBuiltinToolsBase.includes("read")
|
|
106
|
+
? ["read", ...declaredBuiltinToolsBase]
|
|
107
|
+
: declaredBuiltinToolsBase;
|
|
101
108
|
const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
|
|
102
109
|
const toolExtensionPaths: string[] = [];
|
|
103
110
|
if (input.tools?.length) {
|
|
@@ -120,11 +127,11 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
120
127
|
: [PROMPT_RUNTIME_EXTENSION_PATH];
|
|
121
128
|
if (input.extensions !== undefined) {
|
|
122
129
|
args.push("--no-extensions");
|
|
123
|
-
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions])]) {
|
|
130
|
+
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions, ...(input.subagentOnlyExtensions ?? [])])]) {
|
|
124
131
|
args.push("--extension", extPath);
|
|
125
132
|
}
|
|
126
133
|
} else {
|
|
127
|
-
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths])]) {
|
|
134
|
+
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...(input.subagentOnlyExtensions ?? [])])]) {
|
|
128
135
|
args.push("--extension", extPath);
|
|
129
136
|
}
|
|
130
137
|
}
|
|
@@ -216,6 +223,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
216
223
|
env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
|
|
217
224
|
}
|
|
218
225
|
|
|
226
|
+
env[SUBAGENT_PARENT_SESSION_ENV] = input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
|
|
227
|
+
|
|
219
228
|
return { args, env, tempDir };
|
|
220
229
|
}
|
|
221
230
|
|