pi-subagents 0.30.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 +26 -0
- package/README.md +116 -17
- 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 +5 -0
- package/src/agents/agent-management.ts +170 -6
- package/src/agents/agent-serializer.ts +31 -13
- package/src/agents/agents.ts +207 -23
- package/src/agents/frontmatter.ts +66 -2
- package/src/agents/skills.ts +117 -20
- package/src/extension/doctor.ts +20 -0
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +47 -4
- package/src/extension/schemas.ts +10 -76
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/runs/background/async-execution.ts +14 -4
- package/src/runs/background/async-job-tracker.ts +56 -11
- package/src/runs/background/result-watcher.ts +11 -2
- package/src/runs/background/stale-run-reconciler.ts +9 -4
- package/src/runs/background/subagent-runner.ts +79 -3
- package/src/runs/foreground/chain-execution.ts +26 -2
- package/src/runs/foreground/execution.ts +113 -8
- package/src/runs/foreground/subagent-executor.ts +325 -77
- package/src/runs/shared/acceptance.ts +285 -34
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +4 -2
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +6 -1
- package/src/runs/shared/pi-args.ts +9 -1
- package/src/runs/shared/single-output.ts +15 -1
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +8 -2
- package/src/shared/utils.ts +19 -1
- package/src/slash/prompt-template-bridge.ts +26 -3
- package/src/slash/slash-commands.ts +33 -3
- 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[] {
|
|
@@ -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
|
}
|
|
@@ -49,7 +50,7 @@ const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
|
49
50
|
...DYNAMIC_PARALLEL_KEYS,
|
|
50
51
|
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "maxSubagentDepth",
|
|
51
52
|
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "completionGuard", "systemPrompt",
|
|
52
|
-
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance",
|
|
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,4 +1,6 @@
|
|
|
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;
|
|
4
6
|
importAsyncRoot?: {
|
|
@@ -108,6 +110,7 @@ export interface ParallelTaskResult {
|
|
|
108
110
|
output: string;
|
|
109
111
|
exitCode: number | null;
|
|
110
112
|
error?: string;
|
|
113
|
+
timedOut?: boolean;
|
|
111
114
|
model?: string;
|
|
112
115
|
attemptedModels?: string[];
|
|
113
116
|
outputTargetPath?: string;
|
|
@@ -124,7 +127,9 @@ export function aggregateParallelOutputs(
|
|
|
124
127
|
const header = headerFormat(r.taskIndex ?? i, r.agent);
|
|
125
128
|
const hasOutput = Boolean(r.output?.trim());
|
|
126
129
|
const status =
|
|
127
|
-
r.
|
|
130
|
+
r.timedOut
|
|
131
|
+
? `TIMED OUT${r.error ? `: ${r.error}` : ""}`
|
|
132
|
+
: r.exitCode === -1
|
|
128
133
|
? "SKIPPED"
|
|
129
134
|
: r.exitCode !== 0 && r.exitCode !== null
|
|
130
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,6 +39,7 @@ 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[];
|
|
42
45
|
subagentOnlyExtensions?: string[];
|
|
@@ -98,7 +101,10 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
98
101
|
args.push("--model", modelArg);
|
|
99
102
|
}
|
|
100
103
|
|
|
101
|
-
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;
|
|
102
108
|
const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
|
|
103
109
|
const toolExtensionPaths: string[] = [];
|
|
104
110
|
if (input.tools?.length) {
|
|
@@ -217,6 +223,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
217
223
|
env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
|
|
218
224
|
}
|
|
219
225
|
|
|
226
|
+
env[SUBAGENT_PARENT_SESSION_ENV] = input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
|
|
227
|
+
|
|
220
228
|
return { args, env, tempDir };
|
|
221
229
|
}
|
|
222
230
|
|
|
@@ -31,9 +31,23 @@ export function resolveSingleOutputPath(
|
|
|
31
31
|
return path.resolve(baseCwd, output);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function formatOutputPathInstruction(outputPath: string): string {
|
|
35
|
+
return [
|
|
36
|
+
`Write your findings to exactly this path: ${outputPath}`,
|
|
37
|
+
"This path is authoritative for this run.",
|
|
38
|
+
"Ignore any other output filename or output path mentioned elsewhere, including output destinations in the base agent prompt, system prompt, or task instructions.",
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
export function injectSingleOutputInstruction(task: string, outputPath: string | undefined): string {
|
|
35
43
|
if (!outputPath) return task;
|
|
36
|
-
return `${task}\n\n---\n**Output
|
|
44
|
+
return `${task}\n\n---\n**Output:**\n${formatOutputPathInstruction(outputPath)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function injectOutputPathSystemPrompt(systemPrompt: string, outputPath: string | undefined): string {
|
|
48
|
+
if (!outputPath) return systemPrompt;
|
|
49
|
+
const instruction = `Runtime output path override:\n${formatOutputPathInstruction(outputPath)}`;
|
|
50
|
+
return systemPrompt ? `${systemPrompt}\n\n${instruction}` : instruction;
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
function countLines(text: string): number {
|
package/src/shared/settings.ts
CHANGED
|
@@ -305,6 +305,7 @@ function resolveChainPath(filePath: string, chainDir: string): string {
|
|
|
305
305
|
* These are appended to the task to tell the agent what to read/write.
|
|
306
306
|
*/
|
|
307
307
|
export function writeInitialProgressFile(progressDir: string): void {
|
|
308
|
+
fs.mkdirSync(progressDir, { recursive: true });
|
|
308
309
|
fs.writeFileSync(path.join(progressDir, "progress.md"), INITIAL_PROGRESS_CONTENT);
|
|
309
310
|
}
|
|
310
311
|
|
package/src/shared/types.ts
CHANGED
|
@@ -391,6 +391,7 @@ export interface SingleResult {
|
|
|
391
391
|
detached?: boolean;
|
|
392
392
|
detachedReason?: string;
|
|
393
393
|
interrupted?: boolean;
|
|
394
|
+
timedOut?: boolean;
|
|
394
395
|
messages?: Message[];
|
|
395
396
|
usage: Usage;
|
|
396
397
|
model?: string;
|
|
@@ -681,6 +682,7 @@ export interface ForegroundResumeRun {
|
|
|
681
682
|
export interface SubagentState {
|
|
682
683
|
baseCwd: string;
|
|
683
684
|
currentSessionId: string | null;
|
|
685
|
+
subagentInProgress?: boolean;
|
|
684
686
|
asyncJobs: Map<string, AsyncJobState>;
|
|
685
687
|
foregroundRuns?: Map<string, ForegroundResumeRun>;
|
|
686
688
|
foregroundControls: Map<string, {
|
|
@@ -754,9 +756,13 @@ export const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom
|
|
|
754
756
|
// ============================================================================
|
|
755
757
|
|
|
756
758
|
export interface RunSyncOptions {
|
|
759
|
+
/** Session id of the direct parent session for permission-system ask forwarding. */
|
|
760
|
+
parentSessionId?: string;
|
|
757
761
|
cwd?: string;
|
|
758
762
|
signal?: AbortSignal;
|
|
759
763
|
interruptSignal?: AbortSignal;
|
|
764
|
+
timeoutMs?: number;
|
|
765
|
+
deadlineAt?: number;
|
|
760
766
|
allowIntercomDetach?: boolean;
|
|
761
767
|
intercomEvents?: IntercomEventBus;
|
|
762
768
|
onUpdate?: (r: import("@earendil-works/pi-agent-core").AgentToolResult<Details>) => void;
|
|
@@ -782,7 +788,7 @@ export interface RunSyncOptions {
|
|
|
782
788
|
availableModels?: Array<{ provider: string; id: string; fullId: string }>;
|
|
783
789
|
/** Current parent-session provider to prefer for ambiguous bare model ids */
|
|
784
790
|
preferredModelProvider?: string;
|
|
785
|
-
/** Skills to
|
|
791
|
+
/** Skills to make available (overrides agent default if provided) */
|
|
786
792
|
skills?: string[];
|
|
787
793
|
structuredOutput?: {
|
|
788
794
|
schema: JsonSchemaObject;
|
|
@@ -925,7 +931,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
|
|
|
925
931
|
export const POLL_INTERVAL_MS = 250;
|
|
926
932
|
export const MAX_WIDGET_JOBS = 4;
|
|
927
933
|
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
928
|
-
export const SUBAGENT_ACTIONS = ["list", "get", "create", "update", "delete", "status", "interrupt", "resume", "append-step", "doctor"] as const;
|
|
934
|
+
export const SUBAGENT_ACTIONS = ["list", "get", "models", "create", "update", "delete", "status", "interrupt", "resume", "append-step", "doctor"] as const;
|
|
929
935
|
|
|
930
936
|
export const DEFAULT_FORK_PREAMBLE =
|
|
931
937
|
"You are a delegated subagent running from a fork of the parent session. " +
|
package/src/shared/utils.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import * as piCodingAgent from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import type { Message } from "@earendil-works/pi-ai";
|
|
9
10
|
import { formatToolCall } from "./formatters.ts";
|
|
10
11
|
import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, SingleResult, ToolCallSummary } from "./types.ts";
|
|
@@ -13,11 +14,28 @@ import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, Singl
|
|
|
13
14
|
// File System Utilities
|
|
14
15
|
// ============================================================================
|
|
15
16
|
|
|
17
|
+
const DEFAULT_CONFIG_DIR_NAME = ".pi";
|
|
18
|
+
|
|
19
|
+
export function resolveConfigDirName(codingAgentModule: unknown = piCodingAgent): string {
|
|
20
|
+
const value = codingAgentModule && typeof codingAgentModule === "object"
|
|
21
|
+
? (codingAgentModule as { CONFIG_DIR_NAME?: unknown }).CONFIG_DIR_NAME
|
|
22
|
+
: undefined;
|
|
23
|
+
return typeof value === "string" && value.trim() ? value : DEFAULT_CONFIG_DIR_NAME;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getConfigDirName(): string {
|
|
27
|
+
return resolveConfigDirName();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getProjectConfigDir(projectRoot: string): string {
|
|
31
|
+
return path.join(projectRoot, getConfigDirName());
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
export function getAgentDir(): string {
|
|
17
35
|
const configured = process.env.PI_CODING_AGENT_DIR;
|
|
18
36
|
if (configured === "~") return os.homedir();
|
|
19
37
|
if (configured?.startsWith("~/")) return path.join(os.homedir(), configured.slice(2));
|
|
20
|
-
return configured || path.join(os.homedir(),
|
|
38
|
+
return configured || path.join(os.homedir(), getConfigDirName(), "agent");
|
|
21
39
|
}
|
|
22
40
|
|
|
23
41
|
const statusCache = new Map<string, { mtime: number; status: AsyncStatus }>();
|