pi-subagents 0.59.0 → 0.61.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 +69 -0
- package/docs/agents.md +2 -2
- package/docs/configuration.md +9 -5
- package/docs/extension-api.md +14 -7
- package/docs/models.md +1 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +15 -3
- package/docs/workflows.md +15 -14
- package/install.mjs +2 -1
- package/package.json +1 -1
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +89 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +30 -234
- package/skills/pi-subagents/references/execution-controls.md +79 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +35 -28
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +256 -88
- package/src/agents/agents.ts +527 -221
- package/src/api/background-work.ts +7 -2
- package/src/api/external-runs.ts +67 -4
- package/src/api/preflight.ts +13 -8
- package/src/api/shared-types.ts +3 -0
- package/src/extension/index.ts +7 -4
- package/src/extension/public-execution.ts +48 -4
- package/src/extension/rpc.ts +62 -4
- package/src/extension/schemas.ts +12 -7
- package/src/extension/tool-description.ts +16 -16
- package/src/runs/background/async-execution.ts +53 -37
- package/src/runs/background/async-job-tracker.ts +65 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-status.ts +104 -11
- package/src/runs/background/auto-drain.ts +1 -1
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +1 -1
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/run-status.ts +15 -4
- package/src/runs/background/subagent-runner.ts +6 -3
- package/src/runs/background/subagent-wait.ts +30 -23
- package/src/runs/background/wait-completions.ts +4 -1
- package/src/runs/background/wait-tool.ts +24 -18
- package/src/runs/foreground/execution.ts +72 -4
- package/src/runs/foreground/subagent-executor.ts +276 -59
- package/src/runs/shared/acceptance.ts +43 -18
- package/src/runs/shared/async-status-projection.ts +138 -4
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/host-step-status.ts +1 -0
- package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/src/runs/shared/model-fallback.ts +61 -17
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +47 -1
- package/src/runs/shared/single-output.ts +45 -18
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/workflow-graph.ts +16 -0
- package/src/shared/types.ts +74 -4
- package/src/shared/workflow-child-permit.ts +91 -0
- package/src/slash/prompt-template-bridge.ts +37 -1
- package/src/slash/slash-commands.ts +18 -26
- package/src/tui/fleet-status.ts +11 -3
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +673 -145
- package/src/watchdog/change-signature.ts +40 -1
- package/src/workflows/host-command.ts +6 -1
- package/src/workflows/scripted-workflow.ts +206 -6
- package/src/workflows/workflow-child-summary.ts +1 -1
- package/src/workflows/workflow-receipt.ts +41 -4
- package/src/workflows/workflow-resources.ts +150 -0
|
@@ -678,7 +678,15 @@ function parseAcceptanceReportBody(body: string): { report?: AcceptanceReport; e
|
|
|
678
678
|
return validateAcceptanceReport(parseReportJson(body));
|
|
679
679
|
}
|
|
680
680
|
|
|
681
|
-
|
|
681
|
+
interface AcceptanceReportParseResult {
|
|
682
|
+
report?: AcceptanceReport;
|
|
683
|
+
error?: string;
|
|
684
|
+
/** True when an acceptance-report signal was found but its envelope was invalid. */
|
|
685
|
+
malformed?: boolean;
|
|
686
|
+
sourcePath?: string;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function parseUnterminatedAcceptanceReportFence(output: string): AcceptanceReportParseResult {
|
|
682
690
|
const opener = /```acceptance[-_]report\b[^\n]*\n/gi.exec(output);
|
|
683
691
|
if (!opener) return {};
|
|
684
692
|
const bodyStart = opener.index + opener[0].length;
|
|
@@ -687,13 +695,13 @@ function parseUnterminatedAcceptanceReportFence(output: string): { report?: Acce
|
|
|
687
695
|
const validation = validateAcceptanceReport(JSON.parse(output.slice(bodyStart).trim()) as unknown);
|
|
688
696
|
return validation.report
|
|
689
697
|
? { report: validation.report }
|
|
690
|
-
: { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}
|
|
698
|
+
: { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
|
|
691
699
|
} catch (error) {
|
|
692
|
-
return { error: `Failed to parse acceptance-report: ${error instanceof Error ? error.message : String(error)}
|
|
700
|
+
return { error: `Failed to parse acceptance-report: ${error instanceof Error ? error.message : String(error)}`, malformed: true };
|
|
693
701
|
}
|
|
694
702
|
}
|
|
695
703
|
|
|
696
|
-
function parseGenericJsonAcceptanceReportBody(body: string):
|
|
704
|
+
function parseGenericJsonAcceptanceReportBody(body: string): AcceptanceReportParseResult {
|
|
697
705
|
const parsed = parseReportJson(body);
|
|
698
706
|
const normalized = normalizeAcceptanceReportValue(parsed);
|
|
699
707
|
const hasCriteriaMarker = normalized.value !== null
|
|
@@ -704,12 +712,12 @@ function parseGenericJsonAcceptanceReportBody(body: string): { report?: Acceptan
|
|
|
704
712
|
const validation = validateAcceptanceReport(parsed);
|
|
705
713
|
return validation.report
|
|
706
714
|
? { report: validation.report }
|
|
707
|
-
: { error: `Invalid acceptance-report: ${validation.errors.join("; ")}
|
|
715
|
+
: { error: `Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
|
|
708
716
|
}
|
|
709
717
|
|
|
710
718
|
export const ACCEPTANCE_REPORT_NOT_FOUND = "Structured acceptance report not found.";
|
|
711
719
|
|
|
712
|
-
export function parseAcceptanceReport(output: string):
|
|
720
|
+
export function parseAcceptanceReport(output: string): AcceptanceReportParseResult {
|
|
713
721
|
const explicitFencePresent = /```acceptance[-_]report\b/i.test(output);
|
|
714
722
|
const fenced = fencedBlocks(output, "acceptance[-_]report");
|
|
715
723
|
const parseErrors: string[] = [];
|
|
@@ -722,17 +730,17 @@ export function parseAcceptanceReport(output: string): { report?: AcceptanceRepo
|
|
|
722
730
|
parseErrors.push(error instanceof Error ? error.message : String(error));
|
|
723
731
|
}
|
|
724
732
|
}
|
|
725
|
-
if (parseErrors.length > 0) return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}
|
|
733
|
+
if (parseErrors.length > 0) return { error: `Failed to parse acceptance-report: ${parseErrors.join("; ")}`, malformed: true };
|
|
726
734
|
if (explicitFencePresent) {
|
|
727
735
|
const recovered = parseUnterminatedAcceptanceReportFence(output);
|
|
728
736
|
if (recovered.report || recovered.error) return recovered;
|
|
729
|
-
return { error: "Failed to parse acceptance-report: Empty or unterminated acceptance-report fence." };
|
|
737
|
+
return { error: "Failed to parse acceptance-report: Empty or unterminated acceptance-report fence.", malformed: true };
|
|
730
738
|
}
|
|
731
739
|
for (const body of fencedBlocks(output, "(?:json|jsonc|json5)")) {
|
|
732
740
|
try {
|
|
733
741
|
const parsed = parseGenericJsonAcceptanceReportBody(body);
|
|
734
742
|
if (parsed.report) return { report: parsed.report };
|
|
735
|
-
if (parsed.error) return { error: `Failed to parse acceptance-report: ${parsed.error}
|
|
743
|
+
if (parsed.error) return { error: `Failed to parse acceptance-report: ${parsed.error}`, malformed: parsed.malformed };
|
|
736
744
|
} catch {
|
|
737
745
|
// Ignore unrelated malformed generic JSON. A recognizable report shape
|
|
738
746
|
// returns exact validation errors above instead of being mistaken for prose.
|
|
@@ -742,20 +750,20 @@ export function parseAcceptanceReport(output: string): { report?: AcceptanceRepo
|
|
|
742
750
|
if (markerIndex !== -1) {
|
|
743
751
|
const jsonStart = output.indexOf("{", markerIndex);
|
|
744
752
|
if (jsonStart === -1) {
|
|
745
|
-
return { error: "Failed to parse acceptance-report: Expected a JSON object after ACCEPTANCE_REPORT:." };
|
|
753
|
+
return { error: "Failed to parse acceptance-report: Expected a JSON object after ACCEPTANCE_REPORT:.", malformed: true };
|
|
746
754
|
}
|
|
747
755
|
const json = extractBalancedJson(output, jsonStart);
|
|
748
756
|
if (!json) {
|
|
749
|
-
return { error: "Failed to parse acceptance-report: Unterminated JSON object after ACCEPTANCE_REPORT:." };
|
|
757
|
+
return { error: "Failed to parse acceptance-report: Unterminated JSON object after ACCEPTANCE_REPORT:.", malformed: true };
|
|
750
758
|
}
|
|
751
759
|
try {
|
|
752
760
|
const parsed = JSON.parse(json) as unknown;
|
|
753
761
|
const validation = validateAcceptanceReport(parsed);
|
|
754
762
|
if (validation.report) return { report: validation.report };
|
|
755
|
-
return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}
|
|
763
|
+
return { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
|
|
756
764
|
} catch (error) {
|
|
757
765
|
const message = error instanceof Error ? error.message : String(error);
|
|
758
|
-
return { error: `Failed to parse acceptance-report: ${message}
|
|
766
|
+
return { error: `Failed to parse acceptance-report: ${message}`, malformed: true };
|
|
759
767
|
}
|
|
760
768
|
}
|
|
761
769
|
return { error: ACCEPTANCE_REPORT_NOT_FOUND };
|
|
@@ -764,14 +772,18 @@ export function parseAcceptanceReport(output: string): { report?: AcceptanceRepo
|
|
|
764
772
|
function parseAcceptanceReportSources(
|
|
765
773
|
output: string,
|
|
766
774
|
fileOutput: { content: string; path: string; authoritative?: boolean } | undefined,
|
|
767
|
-
):
|
|
775
|
+
): AcceptanceReportParseResult {
|
|
768
776
|
const fromText = () => parseAcceptanceReport(output);
|
|
769
777
|
const fromFile = () => {
|
|
770
778
|
if (!fileOutput) return { error: ACCEPTANCE_REPORT_NOT_FOUND };
|
|
771
779
|
const parsed = parseAcceptanceReport(fileOutput.content);
|
|
772
780
|
return parsed.report || parsed.error === ACCEPTANCE_REPORT_NOT_FOUND
|
|
773
781
|
? parsed
|
|
774
|
-
: {
|
|
782
|
+
: {
|
|
783
|
+
...parsed,
|
|
784
|
+
error: `${parsed.error} (in configured output ${fileOutput.path})`,
|
|
785
|
+
sourcePath: fileOutput.path,
|
|
786
|
+
};
|
|
775
787
|
};
|
|
776
788
|
const [primary, secondary] = fileOutput?.authoritative ? [fromFile, fromText] : [fromText, fromFile];
|
|
777
789
|
const first = primary();
|
|
@@ -1272,7 +1284,7 @@ export async function evaluateAcceptance(input: {
|
|
|
1272
1284
|
* be misattributed). Searched for the acceptance report; searched before
|
|
1273
1285
|
* the assistant output when `authoritative` (outputMode "file-only").
|
|
1274
1286
|
*/
|
|
1275
|
-
fileOutput?: { content: string; path: string; authoritative?: boolean };
|
|
1287
|
+
fileOutput?: { content: string; path: string; authoritative?: boolean; durable?: boolean };
|
|
1276
1288
|
report?: AcceptanceReport;
|
|
1277
1289
|
reportError?: string;
|
|
1278
1290
|
reviewResult?: AcceptanceReviewResult;
|
|
@@ -1296,16 +1308,25 @@ export async function evaluateAcceptance(input: {
|
|
|
1296
1308
|
};
|
|
1297
1309
|
if (acceptance.level === "none") return ledger;
|
|
1298
1310
|
|
|
1299
|
-
const parsed = input.reportError
|
|
1311
|
+
const parsed: AcceptanceReportParseResult = input.reportError
|
|
1300
1312
|
? { error: input.reportError }
|
|
1301
1313
|
: input.report
|
|
1302
1314
|
? (() => {
|
|
1303
1315
|
const validation = validateAcceptanceReport(input.report);
|
|
1304
1316
|
return validation.report
|
|
1305
1317
|
? { report: validation.report }
|
|
1306
|
-
: { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}
|
|
1318
|
+
: { error: `Failed to parse acceptance-report: Invalid acceptance-report: ${validation.errors.join("; ")}`, malformed: true };
|
|
1307
1319
|
})()
|
|
1308
1320
|
: parseAcceptanceReportSources(input.output, input.fileOutput);
|
|
1321
|
+
const durableFileOutput = input.fileOutput?.authoritative && input.fileOutput.durable ? input.fileOutput : undefined;
|
|
1322
|
+
if (parsed.malformed && parsed.sourcePath && durableFileOutput) {
|
|
1323
|
+
ledger.recovery = {
|
|
1324
|
+
status: "available-for-review",
|
|
1325
|
+
reason: "acceptance-metadata-rejected",
|
|
1326
|
+
reportPath: parsed.sourcePath,
|
|
1327
|
+
reportHash: hash(durableFileOutput.content),
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1309
1330
|
const needsReport = acceptanceRequiresChildReport(acceptance);
|
|
1310
1331
|
if (parsed.report) {
|
|
1311
1332
|
ledger.childReport = parsed.report;
|
|
@@ -1314,6 +1335,10 @@ export async function evaluateAcceptance(input: {
|
|
|
1314
1335
|
} else if (!input.reportOptional || needsReport || parsed.error !== ACCEPTANCE_REPORT_NOT_FOUND) {
|
|
1315
1336
|
ledger.childReportParseError = parsed.error;
|
|
1316
1337
|
ledger.runtimeChecks.push({ id: "attestation", status: "failed", message: parsed.error ?? "Structured acceptance report missing." });
|
|
1338
|
+
if (ledger.recovery) {
|
|
1339
|
+
ledger.status = "rejected";
|
|
1340
|
+
ledger.evidenceStatus = "rejected";
|
|
1341
|
+
}
|
|
1317
1342
|
if (!input.reportOptional) {
|
|
1318
1343
|
ledger.status = "rejected";
|
|
1319
1344
|
ledger.evidenceStatus = "rejected";
|
|
@@ -2,6 +2,7 @@ import { sanitizeDisplayText, truncateDisplayText } from "../../shared/display-t
|
|
|
2
2
|
import { formatModelThinking } from "../../shared/formatters.ts";
|
|
3
3
|
import type { AsyncJobState, AsyncJobStep, HostStepFreshnessV1, HostStepMonitorKind, HostStepNodeV1, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentRunMode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../../shared/types.ts";
|
|
4
4
|
import { HOST_STEP_MAX_COUNT, HOST_STEP_MAX_DETAIL_CHARS, HOST_STEP_MAX_LABEL_CHARS, HOST_STEP_MAX_PROVIDER_CHARS, HOST_STEP_MAX_REASON_CHARS, HOST_STEP_MAX_REF_CHARS, HOST_STEP_MAX_ROLE_CHARS, HOST_STEP_MAX_TARGET_CHARS, hostStepReportName, parseHostStepNode, validHostStepNodes } from "./host-step-status.ts";
|
|
5
|
+
import { workflowGraphStageNodes } from "./workflow-graph.ts";
|
|
5
6
|
|
|
6
7
|
export const ASYNC_STATUS_SNAPSHOT_KIND = "pi-subagents.async-status-snapshot";
|
|
7
8
|
export const ASYNC_STATUS_SNAPSHOT_VERSION = 1;
|
|
@@ -15,6 +16,21 @@ const DEFAULT_MAX_SERIALIZED_BYTES = 32 * 1024;
|
|
|
15
16
|
export type AsyncStatusSnapshotState = "queued" | "running" | "complete" | "failed" | "partial" | "paused" | "stopped" | "rejected";
|
|
16
17
|
export type AsyncStatusSnapshotKind = "subagent" | "workflow" | "step";
|
|
17
18
|
|
|
19
|
+
const ASYNC_STATUS_SNAPSHOT_STATES: Record<AsyncStatusSnapshotState, true> = {
|
|
20
|
+
queued: true,
|
|
21
|
+
running: true,
|
|
22
|
+
complete: true,
|
|
23
|
+
failed: true,
|
|
24
|
+
partial: true,
|
|
25
|
+
paused: true,
|
|
26
|
+
stopped: true,
|
|
27
|
+
rejected: true,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function isAsyncStatusSnapshotState(value: string): value is AsyncStatusSnapshotState {
|
|
31
|
+
return Object.hasOwn(ASYNC_STATUS_SNAPSHOT_STATES, value);
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
export interface AsyncStatusSnapshotActivityV1 {
|
|
19
35
|
state?: string;
|
|
20
36
|
currentTool?: string;
|
|
@@ -87,6 +103,7 @@ export interface AsyncStatusWorkflowRow {
|
|
|
87
103
|
state: AsyncJobStep["status"] | HostStepState | "planned";
|
|
88
104
|
/** Present only for typed host-owned monitor rows; legacy child rows omit it. */
|
|
89
105
|
kind?: HostStepMonitorKind;
|
|
106
|
+
context?: AsyncJobStep["context"];
|
|
90
107
|
modelThinking?: string;
|
|
91
108
|
activity?: string;
|
|
92
109
|
startedAt?: number;
|
|
@@ -155,7 +172,8 @@ function publicCount(value: unknown): number | undefined {
|
|
|
155
172
|
function normalizeState(value: string): AsyncStatusSnapshotState {
|
|
156
173
|
if (value === "completed") return "complete";
|
|
157
174
|
if (value === "pending") return "queued";
|
|
158
|
-
|
|
175
|
+
if (isAsyncStatusSnapshotState(value)) return value;
|
|
176
|
+
return "partial";
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
function terminalState(state: AsyncStatusSnapshotState): boolean {
|
|
@@ -230,6 +248,16 @@ function projectStep(step: AsyncJobStep | NestedStepSummary, index: number, dept
|
|
|
230
248
|
return node;
|
|
231
249
|
}
|
|
232
250
|
|
|
251
|
+
function projectWorkflowGraphNode(node: WorkflowGraphSnapshot["nodes"][number], index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
|
|
252
|
+
const step: AsyncJobStep = {
|
|
253
|
+
agent: node.agent ?? node.label,
|
|
254
|
+
status: workflowGraphStepStatus(node.status),
|
|
255
|
+
workflowKey: node.id,
|
|
256
|
+
label: node.label,
|
|
257
|
+
};
|
|
258
|
+
return projectStep(step, node.flatIndex ?? index, depth, ctx);
|
|
259
|
+
}
|
|
260
|
+
|
|
233
261
|
function projectNestedRun(child: NestedRunSummary, index: number, depth: number, ctx: ProjectionContext): AsyncStatusSnapshotNodeV1 {
|
|
234
262
|
const state = normalizeState(child.state);
|
|
235
263
|
const startedAt = publicTime(child.startedAt);
|
|
@@ -310,9 +338,14 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
|
|
|
310
338
|
};
|
|
311
339
|
if (ctx.caps.maxDepth > 0) {
|
|
312
340
|
const stepChildren = job.steps?.map((step, index) => projectStep(step, step.index ?? index, 1, ctx)) ?? [];
|
|
341
|
+
const loadedKeys = new Set(job.steps?.flatMap((step) => step.workflowKey ? [step.workflowKey] : []) ?? []);
|
|
342
|
+
const graphStages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
|
|
343
|
+
const graphChildren = graphStages
|
|
344
|
+
.filter((graphNode) => !loadedKeys.has(graphNode.id))
|
|
345
|
+
.map((graphNode, index) => projectWorkflowGraphNode(graphNode, index, 1, ctx));
|
|
313
346
|
const nestedChildren = job.nestedChildren?.map((child, index) => projectNestedRun(child, index, 1, ctx)) ?? [];
|
|
314
347
|
const hostStepChildren = validHostStepList(job.hostSteps).map((hostStep) => projectHostStep(hostStep, ctx));
|
|
315
|
-
const ordinaryChildren = [...stepChildren, ...nestedChildren];
|
|
348
|
+
const ordinaryChildren = [...stepChildren, ...graphChildren, ...nestedChildren];
|
|
316
349
|
const retainedHostSteps = hostStepChildren.slice(0, ctx.caps.maxChildrenPerNode);
|
|
317
350
|
const retainedOrdinaryChildren = ordinaryChildren.slice(0, ctx.caps.maxChildrenPerNode - retainedHostSteps.length);
|
|
318
351
|
const bounded: AsyncStatusSnapshotNodeV1[] = [];
|
|
@@ -320,7 +353,10 @@ function projectRun(job: AsyncJobState, ctx: ProjectionContext): AsyncStatusSnap
|
|
|
320
353
|
ctx.omitted.children += ordinaryChildren.length - retainedOrdinaryChildren.length + hostStepChildren.length - retainedHostSteps.length;
|
|
321
354
|
if (bounded.length) node.children = bounded;
|
|
322
355
|
} else {
|
|
323
|
-
|
|
356
|
+
const loadedKeys = new Set(job.steps?.flatMap((step) => step.workflowKey ? [step.workflowKey] : []) ?? []);
|
|
357
|
+
const graphStages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
|
|
358
|
+
const graphCount = graphStages.filter((graphNode) => !loadedKeys.has(graphNode.id)).length;
|
|
359
|
+
ctx.omitted.children += (job.steps?.length ?? 0) + graphCount + (job.nestedChildren?.length ?? 0) + validHostStepList(job.hostSteps).length;
|
|
324
360
|
}
|
|
325
361
|
return node;
|
|
326
362
|
}
|
|
@@ -392,6 +428,50 @@ function isWorkflowPreflight(value: readonly HostStepNodeV1[] | WorkflowGraphSna
|
|
|
392
428
|
return value !== undefined && !Array.isArray(value) && "lanes" in value;
|
|
393
429
|
}
|
|
394
430
|
|
|
431
|
+
function isWorkflowGraph(value: readonly HostStepNodeV1[] | WorkflowGraphSnapshot | WorkflowPreflightV1 | undefined): value is WorkflowGraphSnapshot {
|
|
432
|
+
return value !== undefined && !Array.isArray(value) && "nodes" in value;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function workflowGraphRowState(status: WorkflowGraphSnapshot["nodes"][number]["status"]): AsyncStatusWorkflowRow["state"] {
|
|
436
|
+
switch (status) {
|
|
437
|
+
case "pending":
|
|
438
|
+
return "planned";
|
|
439
|
+
case "completed":
|
|
440
|
+
return "complete";
|
|
441
|
+
case "detached":
|
|
442
|
+
return "paused";
|
|
443
|
+
default:
|
|
444
|
+
return status;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function workflowGraphStepStatus(status: WorkflowGraphSnapshot["nodes"][number]["status"]): AsyncJobStep["status"] {
|
|
449
|
+
switch (status) {
|
|
450
|
+
case "completed":
|
|
451
|
+
return "complete";
|
|
452
|
+
case "detached":
|
|
453
|
+
return "paused";
|
|
454
|
+
default:
|
|
455
|
+
return status;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function workflowGraphRowName(node: WorkflowGraphSnapshot["nodes"][number]): string {
|
|
460
|
+
const key = publicText(node.id, "stage", HOST_STEP_MAX_LABEL_CHARS);
|
|
461
|
+
const label = publicOptionalText(node.label, HOST_STEP_MAX_LABEL_CHARS);
|
|
462
|
+
const phase = publicOptionalText(node.phase, HOST_STEP_MAX_LABEL_CHARS);
|
|
463
|
+
const agent = publicOptionalText(node.agent, HOST_STEP_MAX_LABEL_CHARS);
|
|
464
|
+
return `${phase ? `${phase}: ` : ""}${key}${label && label !== node.id ? ` · ${label}` : ""}${agent ? ` (${agent})` : ""}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function projectWorkflowGraphRow(node: WorkflowGraphSnapshot["nodes"][number], preflight?: WorkflowPreflightLaneV1): AsyncStatusWorkflowRow {
|
|
468
|
+
return {
|
|
469
|
+
name: workflowGraphRowName(node),
|
|
470
|
+
state: workflowGraphRowState(node.status),
|
|
471
|
+
...(preflight ? { preflight } : {}),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
395
475
|
/** Project loaded workflow child facts plus stored preflight hints into compact rows. */
|
|
396
476
|
export function projectAsyncWorkflowRows(
|
|
397
477
|
steps: readonly AsyncJobStep[] | undefined,
|
|
@@ -399,9 +479,62 @@ export function projectAsyncWorkflowRows(
|
|
|
399
479
|
preflightOverride?: WorkflowPreflightV1,
|
|
400
480
|
): AsyncStatusWorkflowRow[] {
|
|
401
481
|
const preflight = preflightOverride ?? (isWorkflowPreflight(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined);
|
|
402
|
-
const
|
|
482
|
+
const graph = isWorkflowGraph(hostStepsOrPreflight) ? hostStepsOrPreflight : undefined;
|
|
483
|
+
const hostSteps = isWorkflowPreflight(hostStepsOrPreflight) || graph ? undefined : hostStepsOrPreflight;
|
|
403
484
|
const loaded = steps ?? [];
|
|
404
485
|
const declared = new Map<string, WorkflowPreflightLaneV1>();
|
|
486
|
+
if (graph) {
|
|
487
|
+
const loadedIndexesByKey = new Map<string, number[]>();
|
|
488
|
+
for (const [index, step] of loaded.entries()) {
|
|
489
|
+
if (!step.workflowKey) continue;
|
|
490
|
+
const indexes = loadedIndexesByKey.get(step.workflowKey) ?? [];
|
|
491
|
+
indexes.push(index);
|
|
492
|
+
loadedIndexesByKey.set(step.workflowKey, indexes);
|
|
493
|
+
}
|
|
494
|
+
const consumed = new Set<number>();
|
|
495
|
+
const childRows: AsyncStatusWorkflowRow[] = [];
|
|
496
|
+
for (const lane of preflight?.lanes ?? []) declared.set(lane.key, lane);
|
|
497
|
+
const graphStages = workflowGraphStageNodes(graph);
|
|
498
|
+
const graphKeys = new Set(graphStages.map((node) => node.id));
|
|
499
|
+
const graphPhaseByNodeId = new Map<string, string>();
|
|
500
|
+
for (const phase of graph.phases) {
|
|
501
|
+
for (const nodeId of phase.nodeIds) {
|
|
502
|
+
if (graphKeys.has(nodeId) && !graphPhaseByNodeId.has(nodeId)) graphPhaseByNodeId.set(nodeId, phase.title);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const graphLaneKeys = new Set(graphPhaseByNodeId.values());
|
|
506
|
+
const preflightForNode = (node: WorkflowGraphSnapshot["nodes"][number]): WorkflowPreflightLaneV1 | undefined => {
|
|
507
|
+
const phaseTitle = graphPhaseByNodeId.get(node.id);
|
|
508
|
+
return declared.get(node.id) ?? (node.phase ? declared.get(node.phase) : undefined) ?? (phaseTitle ? declared.get(phaseTitle) : undefined);
|
|
509
|
+
};
|
|
510
|
+
for (const node of graphStages) {
|
|
511
|
+
const indexes = (loadedIndexesByKey.get(node.id) ?? []).filter((index) => !consumed.has(index));
|
|
512
|
+
if (indexes.length > 0) {
|
|
513
|
+
for (const index of indexes) {
|
|
514
|
+
consumed.add(index);
|
|
515
|
+
childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, preflightForNode(node)));
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
childRows.push(projectWorkflowGraphRow(node, preflightForNode(node)));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
for (const lane of preflight?.lanes ?? []) {
|
|
522
|
+
if (graphKeys.has(lane.key) || graphLaneKeys.has(lane.key)) continue;
|
|
523
|
+
const indexes = (loadedIndexesByKey.get(lane.key) ?? []).filter((index) => !consumed.has(index));
|
|
524
|
+
if (indexes.length > 0) {
|
|
525
|
+
for (const index of indexes) {
|
|
526
|
+
consumed.add(index);
|
|
527
|
+
childRows.push(projectLoadedWorkflowRow(loaded[index]!, index, lane));
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
childRows.push({ name: lane.key, state: "planned", preflight: lane });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
for (const [index, step] of loaded.entries()) {
|
|
534
|
+
if (!consumed.has(index)) childRows.push(projectLoadedWorkflowRow(step, index, step.workflowKey ? declared.get(step.workflowKey) : undefined));
|
|
535
|
+
}
|
|
536
|
+
return [...childRows, ...validHostStepList(graph).map(hostStepRow)];
|
|
537
|
+
}
|
|
405
538
|
const loadedIndexByKey = new Map<string, number>();
|
|
406
539
|
for (const [index, step] of loaded.entries()) {
|
|
407
540
|
if (step.workflowKey !== undefined && !loadedIndexByKey.has(step.workflowKey)) loadedIndexByKey.set(step.workflowKey, index);
|
|
@@ -431,6 +564,7 @@ function projectLoadedWorkflowRow(step: AsyncJobStep, index: number, preflight?:
|
|
|
431
564
|
return {
|
|
432
565
|
name: workflowStepName(step, index),
|
|
433
566
|
state: step.status,
|
|
567
|
+
...(step.context ? { context: step.context } : {}),
|
|
434
568
|
...(modelThinking ? { modelThinking } : {}),
|
|
435
569
|
...(activity ? { activity } : {}),
|
|
436
570
|
...(step.startedAt !== undefined ? { startedAt: step.startedAt } : {}),
|
|
@@ -144,6 +144,7 @@ export function validHostStepNodes(graph: WorkflowGraphSnapshot | undefined): Ho
|
|
|
144
144
|
export function assertWorkflowGraphHostSteps(graph: WorkflowGraphSnapshot | undefined, source = "status", expectedRunId?: string): void {
|
|
145
145
|
if (!graph) return;
|
|
146
146
|
if (expectedRunId !== undefined && graph.runId !== expectedRunId) throw new Error(`Invalid host step '${source}': workflowGraph.runId does not match the status run id.`);
|
|
147
|
+
if (!Array.isArray(graph.nodes)) throw new Error(`Invalid host step '${source}.workflowGraph': nodes must be an array.`);
|
|
147
148
|
const hostStepCount = graph.nodes.filter((node) => node.kind === "host-step").length;
|
|
148
149
|
if (hostStepCount > HOST_STEP_MAX_COUNT) throw new Error(`Invalid host step '${source}': workflowGraph contains more than ${HOST_STEP_MAX_COUNT} host steps.`);
|
|
149
150
|
const hostSteps: HostStepNodeV1[] = [];
|
|
@@ -125,11 +125,8 @@ export function formatUnresolvedMcpDirectToolSelectors(selectors: readonly strin
|
|
|
125
125
|
|
|
126
126
|
function getServerPrefix(serverName: string, mode: McpToolPrefix): string {
|
|
127
127
|
if (mode === "none") return "";
|
|
128
|
-
if (mode === "short")
|
|
129
|
-
|
|
130
|
-
return short || "mcp";
|
|
131
|
-
}
|
|
132
|
-
return serverName.replace(/-/g, "_");
|
|
128
|
+
if (mode === "short") return serverName.replace(/-?mcp$/i, "") || "mcp";
|
|
129
|
+
return serverName;
|
|
133
130
|
}
|
|
134
131
|
|
|
135
132
|
function formatToolName(toolName: string, serverName: string, prefix: McpToolPrefix): string {
|
|
@@ -321,15 +321,23 @@ export function resolveSubagentModelOverride(
|
|
|
321
321
|
const explicit = trimmed && trimmed !== INHERIT_MODEL ? trimmed : undefined;
|
|
322
322
|
if (!parentModel) throwForUnresolvedEnforcedInheritScope(options?.scope, explicit === undefined || options?.source === "inherited");
|
|
323
323
|
let resolved: string | undefined;
|
|
324
|
+
let resolvedFromRegistry = explicit === undefined;
|
|
324
325
|
if (explicit === undefined) {
|
|
325
326
|
resolved = parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;
|
|
326
327
|
} else {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
328
|
+
const candidate = resolveSubagentModelCandidate(explicit, availableModels, preferredProvider);
|
|
329
|
+
if (options?.source === "explicit") {
|
|
330
|
+
resolved = candidate ?? resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
|
|
331
|
+
throwForExplicitModelExclusion(resolved);
|
|
332
|
+
resolvedFromRegistry = true;
|
|
333
|
+
} else if (candidate) {
|
|
334
|
+
resolved = candidate;
|
|
335
|
+
resolvedFromRegistry = true;
|
|
336
|
+
} else {
|
|
337
|
+
resolved = explicit;
|
|
338
|
+
}
|
|
331
339
|
}
|
|
332
|
-
if (resolved && options?.scope) {
|
|
340
|
+
if (resolved && options?.scope && resolvedFromRegistry) {
|
|
333
341
|
const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
|
|
334
342
|
enforceModelScopes(resolved, options.scope, source, options.onWarn);
|
|
335
343
|
}
|
|
@@ -362,12 +370,33 @@ export function resolveEffectiveSubagentModel(
|
|
|
362
370
|
);
|
|
363
371
|
}
|
|
364
372
|
|
|
373
|
+
export type ModelOrigin = ModelSource | "configured";
|
|
374
|
+
|
|
365
375
|
export interface BuildModelCandidatesOptions {
|
|
366
376
|
/** Fallback models warn by default and throw when strict scope enforcement is enabled. */
|
|
367
377
|
scope?: ModelScopeCheckRule | ModelScopeCheckRule[];
|
|
368
378
|
onWarn?: (violation: ModelScopeViolation) => void;
|
|
369
379
|
/** The primary model came from the running parent session, not configuration. */
|
|
370
380
|
primaryModelFromParent?: boolean;
|
|
381
|
+
/** How the primary model was selected. Explicit stays strict and does not rotate to fallbacks. */
|
|
382
|
+
origin?: ModelOrigin;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const ZERO_USABLE_MODEL_CANDIDATES_ERROR =
|
|
386
|
+
"No usable subagent models remain after registry, scope, and cached-exclusion filtering.";
|
|
387
|
+
|
|
388
|
+
export function resolveModelOrigin(input: {
|
|
389
|
+
explicitModel?: string | boolean;
|
|
390
|
+
agentModel?: string | boolean;
|
|
391
|
+
parentModel?: ParentModel;
|
|
392
|
+
fromParent?: boolean;
|
|
393
|
+
storedOrigin?: ModelOrigin;
|
|
394
|
+
}): ModelOrigin {
|
|
395
|
+
if (input.storedOrigin) return input.storedOrigin;
|
|
396
|
+
if (input.fromParent) return "inherited";
|
|
397
|
+
if (inheritsParentModel(input.explicitModel, input.agentModel, input.parentModel)) return "inherited";
|
|
398
|
+
const trimmed = typeof input.explicitModel === "string" ? input.explicitModel.trim() : "";
|
|
399
|
+
return trimmed && trimmed !== INHERIT_MODEL ? "explicit" : "configured";
|
|
371
400
|
}
|
|
372
401
|
|
|
373
402
|
export function inheritsParentModel(
|
|
@@ -388,36 +417,51 @@ export function buildModelCandidates(
|
|
|
388
417
|
options?: BuildModelCandidatesOptions,
|
|
389
418
|
): string[] {
|
|
390
419
|
if (!primaryModel) throwForUnresolvedEnforcedInheritScope(options?.scope, true);
|
|
420
|
+
const origin = options?.origin ?? (options?.primaryModelFromParent ? "inherited" : "configured");
|
|
421
|
+
const scopes = configuredScopes(options?.scope);
|
|
422
|
+
const warnCachedExclusion = (candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>) => {
|
|
423
|
+
const reason = redactSecretValues((exclusion.reason ?? "runtime-failure").replace(/[\u0000-\u001f\u007f]+/g, " ")).slice(0, 240);
|
|
424
|
+
console.warn(`[pi-subagents] Skipping model '${candidate}' due to a cached exclusion (reason: ${reason}; expires: ${new Date(exclusion.expiresAt).toISOString()}).`);
|
|
425
|
+
};
|
|
426
|
+
if (origin === "explicit" && primaryModel) {
|
|
427
|
+
const normalized = resolveRequiredSubagentModelCandidate(primaryModel.trim(), availableModels, preferredProvider);
|
|
428
|
+
throwForExplicitModelExclusion(normalized);
|
|
429
|
+
enforceModelScopes(normalized, scopes, "explicit", options?.onWarn);
|
|
430
|
+
primaryModel = normalized;
|
|
431
|
+
}
|
|
391
432
|
const seen = new Set<string>();
|
|
392
433
|
const candidates: string[] = [];
|
|
393
434
|
const rawCandidates = [primaryModel, ...(fallbackModels ?? [])];
|
|
435
|
+
let skippedPrimary: string | undefined;
|
|
394
436
|
for (let index = 0; index < rawCandidates.length; index++) {
|
|
395
437
|
const raw = rawCandidates[index];
|
|
396
438
|
if (!raw) continue;
|
|
397
439
|
const model = raw.trim();
|
|
398
|
-
const normalized = index === 0
|
|
399
|
-
?
|
|
400
|
-
? model
|
|
401
|
-
: resolveRequiredSubagentModelCandidate(model, availableModels, preferredProvider)
|
|
440
|
+
const normalized = index === 0 && (origin === "inherited" || origin === "explicit" || options?.primaryModelFromParent)
|
|
441
|
+
? model
|
|
402
442
|
: resolveSubagentModelCandidate(model, availableModels, preferredProvider);
|
|
403
443
|
if (!normalized) {
|
|
404
|
-
|
|
444
|
+
if (index === 0) skippedPrimary = model;
|
|
445
|
+
else console.warn(`[pi-subagents] Skipping fallback model '${model}' because it is unavailable in this environment.`);
|
|
405
446
|
continue;
|
|
406
447
|
}
|
|
407
448
|
if (seen.has(normalized)) continue;
|
|
408
|
-
const scopes = configuredScopes(options?.scope);
|
|
409
449
|
if (index > 0 || scopes.some((scope) => scope.enforce === true && scope.strict === true)) {
|
|
410
450
|
enforceModelScopes(normalized, scopes, "inherited", options?.onWarn);
|
|
411
451
|
}
|
|
412
452
|
seen.add(normalized);
|
|
413
453
|
candidates.push(normalized);
|
|
414
454
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
455
|
+
const resolved = filterFallbackCandidates(candidates, { onExcluded: warnCachedExclusion });
|
|
456
|
+
if (resolved.length === 0) {
|
|
457
|
+
if (skippedPrimary) resolveRequiredSubagentModelCandidate(skippedPrimary, availableModels, preferredProvider);
|
|
458
|
+
if (candidates.length > 0) throw new Error(ZERO_USABLE_MODEL_CANDIDATES_ERROR);
|
|
459
|
+
return resolved;
|
|
460
|
+
}
|
|
461
|
+
if (skippedPrimary) {
|
|
462
|
+
console.warn(`[pi-subagents] Skipping primary model '${skippedPrimary}' because it is unavailable in this environment.`);
|
|
463
|
+
}
|
|
464
|
+
return resolved;
|
|
421
465
|
}
|
|
422
466
|
|
|
423
467
|
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import type { ArtifactPaths, TimeoutRecoverySummary, TrackedMutationEvidence, TrackedMutationFingerprint, TrackedMutationSnapshot } from "../../shared/types.ts";
|
|
6
|
+
import type { ArtifactPaths, TimeoutRecoveryProjection, TimeoutRecoverySummary, TrackedMutationEvidence, TrackedMutationFingerprint, TrackedMutationSnapshot } from "../../shared/types.ts";
|
|
7
7
|
|
|
8
8
|
const MAX_TRACKED_PATHS = 500;
|
|
9
9
|
const MAX_HASH_BYTES = 1024 * 1024;
|
|
@@ -15,7 +15,7 @@ function gitArguments(args: string[]): string[] {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function gitOutput(cwd: string, args: string[], maxBuffer = MAX_HASH_BYTES): string {
|
|
18
|
-
return execFileSync("git", gitArguments(args), { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer });
|
|
18
|
+
return execFileSync("git", gitArguments(args), { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer, windowsHide: true });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function splitNul(output: string): string[] {
|
|
@@ -26,7 +26,7 @@ function hashLargeDiff(cwd: string, relativePath: string): string {
|
|
|
26
26
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tracked-diff-"));
|
|
27
27
|
const diffPath = path.join(tempDir, "diff.patch");
|
|
28
28
|
try {
|
|
29
|
-
execFileSync("git", gitArguments(["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath]), { cwd, stdio: "ignore" });
|
|
29
|
+
execFileSync("git", gitArguments(["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath]), { cwd, stdio: "ignore", windowsHide: true });
|
|
30
30
|
const hash = createHash("sha256");
|
|
31
31
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
32
32
|
const fd = fs.openSync(diffPath, "r");
|
|
@@ -110,9 +110,48 @@ function formatPathList(paths: string[]): string {
|
|
|
110
110
|
return paths.length > MAX_TIMEOUT_FILES ? `${shown}, ... (${paths.length - MAX_TIMEOUT_FILES} more)` : shown;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/** Keep status and completion details to bounded routing evidence, not raw output or effects. */
|
|
114
|
+
export function projectTimeoutRecovery(value: unknown): TimeoutRecoveryProjection | undefined {
|
|
115
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
116
|
+
const source = value as Record<string, unknown>;
|
|
117
|
+
const termination = source.termination === "timed-out" || source.termination === "stopped" ? source.termination : undefined;
|
|
118
|
+
if (!termination || !Array.isArray(source.changedFiles)) return undefined;
|
|
119
|
+
const allChangedFiles = source.changedFiles.filter((file): file is string => typeof file === "string");
|
|
120
|
+
const changedFiles = allChangedFiles.slice(0, MAX_TIMEOUT_FILES);
|
|
121
|
+
const reportStatus = source.reportStatus === "missing"
|
|
122
|
+
|| source.reportStatus === "written"
|
|
123
|
+
|| source.reportStatus === "not-requested"
|
|
124
|
+
|| source.reportStatus === "unknown"
|
|
125
|
+
? source.reportStatus
|
|
126
|
+
: undefined;
|
|
127
|
+
return {
|
|
128
|
+
termination,
|
|
129
|
+
changedFiles,
|
|
130
|
+
...(source.truncated === true || allChangedFiles.length > changedFiles.length ? { truncated: true } : {}),
|
|
131
|
+
...(source.recoveryNeeded === true ? { recoveryNeeded: true } : {}),
|
|
132
|
+
...(source.reason === "timed-out-with-dirty-worktree" ? { reason: source.reason } : {}),
|
|
133
|
+
...(reportStatus ? { reportStatus } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Render only the bounded recovery route needed by a parent/operator. */
|
|
138
|
+
export function formatTimeoutRecoveryLines(value: unknown, indent = ""): string[] {
|
|
139
|
+
const recovery = projectTimeoutRecovery(value);
|
|
140
|
+
if (!recovery?.recoveryNeeded) return [];
|
|
141
|
+
const changedFiles = recovery.changedFiles.length > 0
|
|
142
|
+
? `${recovery.changedFiles.join(", ")}${recovery.truncated ? ", …" : ""}`
|
|
143
|
+
: "none";
|
|
144
|
+
const changedFileCount = recovery.changedFiles.length > 0 ? `${recovery.changedFiles.length}${recovery.truncated ? "+" : ""}` : "0";
|
|
145
|
+
return [
|
|
146
|
+
`${indent}Recovery needed: review the diff and artifacts before resuming or launching dependent stages.`,
|
|
147
|
+
`${indent}Recovery evidence: requested report: ${recovery.reportStatus ?? "unknown"}; changed tracked files: ${changedFiles} (${changedFileCount}); classification: ${recovery.reason ?? recovery.termination}`,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
113
151
|
export function buildTimeoutRecoverySummary(input: {
|
|
114
152
|
termination: "timed-out" | "stopped";
|
|
115
153
|
evidence: TrackedMutationEvidence;
|
|
154
|
+
requiredOutputMissing?: boolean;
|
|
116
155
|
currentTool?: string;
|
|
117
156
|
currentToolArgs?: string;
|
|
118
157
|
currentPath?: string;
|
|
@@ -122,11 +161,19 @@ export function buildTimeoutRecoverySummary(input: {
|
|
|
122
161
|
}): TimeoutRecoverySummary {
|
|
123
162
|
const warning = "Inspect partial changes before retrying or resuming the child.";
|
|
124
163
|
const changedFiles = input.evidence.changedFiles.slice(0, MAX_TIMEOUT_FILES);
|
|
164
|
+
let reportStatus: "missing" | "written" | "not-requested" = "not-requested";
|
|
165
|
+
if (input.requiredOutputMissing === true) reportStatus = "missing";
|
|
166
|
+
else if (input.requiredOutputMissing === false) reportStatus = "written";
|
|
167
|
+
const recoveryNeeded = input.termination === "timed-out"
|
|
168
|
+
&& reportStatus === "missing"
|
|
169
|
+
&& input.evidence.changedFiles.length > 0;
|
|
125
170
|
const lines = [
|
|
126
171
|
"Recovery summary:",
|
|
127
172
|
`- termination: ${input.termination}`,
|
|
128
173
|
`- changed tracked files: ${input.evidence.unavailable ? `unavailable (${input.evidence.unavailable})` : formatPathList(input.evidence.changedFiles)}`,
|
|
129
174
|
];
|
|
175
|
+
if (input.requiredOutputMissing !== undefined) lines.push(`- requested report: ${reportStatus}`);
|
|
176
|
+
if (recoveryNeeded) lines.push("- Recovery needed: review the diff and artifacts before resuming or launching dependent stages.");
|
|
130
177
|
if (input.currentTool) lines.push(`- active tool: ${input.currentTool}${input.currentToolArgs ? ` — ${input.currentToolArgs}` : ""}`);
|
|
131
178
|
if (input.currentPath) lines.push(`- active path: ${input.currentPath}`);
|
|
132
179
|
if (input.sessionFile) lines.push(`- session file: ${input.sessionFile}`);
|
|
@@ -138,6 +185,8 @@ export function buildTimeoutRecoverySummary(input: {
|
|
|
138
185
|
termination: input.termination,
|
|
139
186
|
changedFiles,
|
|
140
187
|
truncated: input.evidence.changedFiles.length > changedFiles.length || input.evidence.truncated || undefined,
|
|
188
|
+
...(recoveryNeeded ? { recoveryNeeded: true, reason: "timed-out-with-dirty-worktree" as const } : {}),
|
|
189
|
+
reportStatus,
|
|
141
190
|
currentTool: input.currentTool,
|
|
142
191
|
currentToolArgs: input.currentToolArgs,
|
|
143
192
|
currentPath: input.currentPath,
|
|
@@ -7,7 +7,7 @@ export interface PermissionConfig { rules?: PermissionRules }
|
|
|
7
7
|
|
|
8
8
|
export const PERMISSION_POLICY_ENV = "PI_SUBAGENT_PERMISSION_POLICY";
|
|
9
9
|
export const PERMISSION_AUDIT_PATH_ENV = "PI_SUBAGENT_PERMISSION_AUDIT_PATH";
|
|
10
|
-
const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "subagent_wait", "structured_output"]);
|
|
10
|
+
const INTERNAL_TOOLS = new Set(["contact_supervisor", "intercom", "bg_wait", "subagent_wait", "structured_output"]);
|
|
11
11
|
const DECISIONS = new Set<PermissionDecision>(["allow", "ask", "deny"]);
|
|
12
12
|
const MAX_POLICY_BYTES = 16 * 1024;
|
|
13
13
|
const MAX_PREVIEW_BYTES = 2048;
|