pi-subagents 0.62.0 → 0.64.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 +52 -1
- package/docs/agents.md +5 -4
- package/docs/configuration.md +28 -2
- package/docs/models.md +5 -5
- package/docs/observability.md +4 -1
- package/docs/tool-reference.md +2 -2
- package/docs/watchdog.md +92 -114
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +4 -3
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
- package/src/agents/agent-management.ts +22 -4
- package/src/agents/agents.ts +107 -125
- package/src/api/shared-types.ts +3 -0
- package/src/extension/config.ts +20 -0
- package/src/inspectors/herdr/inspector-runner.ts +19 -13
- package/src/runs/background/active-async-capacity.ts +0 -1
- package/src/runs/background/async-execution.ts +56 -7
- package/src/runs/background/async-resume.ts +3 -0
- package/src/runs/background/async-status.ts +18 -2
- package/src/runs/background/notify.ts +54 -3
- package/src/runs/background/run-status.ts +22 -2
- package/src/runs/background/subagent-runner.ts +53 -3
- package/src/runs/background/wait-completions.ts +13 -0
- package/src/runs/foreground/execution.ts +7 -1
- package/src/runs/foreground/subagent-executor.ts +56 -4
- package/src/runs/shared/acceptance.ts +25 -9
- package/src/runs/shared/async-status-projection.ts +11 -43
- package/src/runs/shared/lane-metadata.ts +24 -3
- package/src/runs/shared/parallel-handoff.ts +4 -0
- package/src/runs/shared/pi-args.ts +9 -6
- package/src/runs/shared/subagent-control.ts +4 -2
- package/src/runs/shared/task-intent.ts +5 -2
- package/src/runs/shared/worktree.ts +467 -63
- package/src/shared/types.ts +38 -2
- package/src/shared/utils.ts +18 -7
- package/src/slash/subagents-admin.ts +24 -12
- package/src/tui/fleet-status.ts +61 -2
- package/src/tui/fleet.ts +12 -7
- package/src/tui/render.ts +227 -14
- package/src/watchdog/child-status.ts +54 -33
- package/src/watchdog/diff-tool.ts +77 -0
- package/src/watchdog/emission-guard.ts +5 -3
- package/src/watchdog/guidance.ts +20 -0
- package/src/watchdog/register-child.ts +16 -13
- package/src/watchdog/register-main.ts +10 -9
- package/src/watchdog/render.ts +4 -5
- package/src/watchdog/review.ts +15 -4
- package/src/watchdog/rules.ts +70 -0
- package/src/watchdog/runtime.ts +75 -92
- package/src/watchdog/scope.ts +0 -11
- package/src/watchdog/settings.ts +48 -104
- package/src/watchdog/types.ts +18 -32
- package/src/watchdog/warning-format.ts +0 -1
- package/src/workflows/chat-progress.ts +3 -2
- package/src/workflows/workflow-checklist.ts +439 -0
- package/src/workflows/workflow-preflight.ts +28 -1
package/src/tui/render.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
8
8
|
import { getMarkdownTheme, keyText, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
10
|
+
import { unresolvedChildWatchdogBlockers } from "../watchdog/child-status.ts";
|
|
10
11
|
import {
|
|
11
12
|
type AgentProgress,
|
|
12
13
|
type AsyncJobState,
|
|
@@ -39,6 +40,7 @@ import { encodeAsyncStatusSnapshotWidget } from "../runs/background/async-status
|
|
|
39
40
|
import { projectAsyncWorkflowRows, type AsyncStatusWorkflowRow } from "../runs/shared/async-status-projection.ts";
|
|
40
41
|
import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
|
|
41
42
|
import { workflowGraphStageNodes } from "../runs/shared/workflow-graph.ts";
|
|
43
|
+
import { formatWorkflowChecklistBottleneck, formatWorkflowChecklistPhase, formatWorkflowChecklistSummary, projectWorkflowChecklist, type WorkflowChecklistItem, type WorkflowChecklistProjection, type WorkflowChecklistState, type WorkflowChecklistStep } from "../workflows/workflow-checklist.ts";
|
|
42
44
|
|
|
43
45
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
44
46
|
|
|
@@ -47,6 +49,7 @@ interface WorkflowWidgetProjection {
|
|
|
47
49
|
steps: AsyncJobStep[];
|
|
48
50
|
stageProgress?: { total: number; current?: number };
|
|
49
51
|
plannedKeys?: Set<string>;
|
|
52
|
+
checklist?: WorkflowChecklistProjection;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
type WorkflowWidgetProjectionLookup = (job: AsyncJobState) => WorkflowWidgetProjection;
|
|
@@ -358,6 +361,7 @@ function workflowStepPriority(step: AsyncJobStep, currentNodeId?: string): numbe
|
|
|
358
361
|
|| step.turnBudgetExceeded === true
|
|
359
362
|
|| step.activityState === "needs_attention"
|
|
360
363
|
|| step.watchdog?.phase === "stale"
|
|
364
|
+
|| unresolvedChildWatchdogBlockers(step.watchdog).length > 0
|
|
361
365
|
|| gate !== undefined
|
|
362
366
|
) return 1;
|
|
363
367
|
if (step.status === "pending") return 2;
|
|
@@ -442,6 +446,17 @@ function buildWorkflowWidgetProjection(job: AsyncJobState): WorkflowWidgetProjec
|
|
|
442
446
|
const projection: WorkflowWidgetProjection = { stages, steps };
|
|
443
447
|
if (stageProgress) projection.stageProgress = stageProgress;
|
|
444
448
|
if (stages.length) projection.plannedKeys = new Set(stages.map((node) => node.id));
|
|
449
|
+
if (job.mode === "workflow") {
|
|
450
|
+
const checklist = projectWorkflowChecklist({
|
|
451
|
+
graph: job.workflowGraph,
|
|
452
|
+
steps: job.steps,
|
|
453
|
+
hostSteps: job.hostSteps,
|
|
454
|
+
preflight: job.preflight,
|
|
455
|
+
trace: job.workflow?.trace,
|
|
456
|
+
now: job.updatedAt ?? Date.now(),
|
|
457
|
+
});
|
|
458
|
+
if (checklist.total > 0) projection.checklist = checklist;
|
|
459
|
+
}
|
|
445
460
|
return projection;
|
|
446
461
|
}
|
|
447
462
|
|
|
@@ -492,6 +507,7 @@ function laneGate(step: AsyncJobStep | undefined): string | undefined {
|
|
|
492
507
|
}
|
|
493
508
|
|
|
494
509
|
function laneNextAction(state: AsyncLaneProjection["state"], step: AsyncJobStep | undefined, output: string | undefined, gate: string | undefined): string | undefined {
|
|
510
|
+
if (unresolvedChildWatchdogBlockers(step?.watchdog).length > 0) return "resolve watchdog blockers";
|
|
495
511
|
if (step?.watchdog?.phase === "stale") return "inspect stale state";
|
|
496
512
|
if (step?.toolBudgetBlocked === true || step?.turnBudgetExceeded === true) return "inspect blocked state";
|
|
497
513
|
if (gate === "review blockers") return "resolve review blockers";
|
|
@@ -530,6 +546,7 @@ export function projectAsyncLane(job: AsyncJobState, ...args: [selectedStep?: As
|
|
|
530
546
|
selectedStep?.activityState === "active_long_running" ? "long-running" : undefined,
|
|
531
547
|
selectedStep?.activityState === "needs_attention" ? "attention" : undefined,
|
|
532
548
|
selectedStep?.watchdog?.phase === "stale" ? "stale" : undefined,
|
|
549
|
+
unresolvedChildWatchdogBlockers(selectedStep?.watchdog).length > 0 ? `wd:${unresolvedChildWatchdogBlockers(selectedStep?.watchdog).length}` : undefined,
|
|
533
550
|
selectedStep?.toolBudgetBlocked === true || selectedStep?.turnBudgetExceeded === true ? "blocked" : undefined,
|
|
534
551
|
].filter((chip): chip is string => Boolean(chip));
|
|
535
552
|
const state = isTerminalLaneState(job.status) ? job.status : selectedStep?.status ?? job.status;
|
|
@@ -988,6 +1005,7 @@ function widgetStepRenderKey(step: AsyncJobStep, index: number, expanded = false
|
|
|
988
1005
|
step.execution?.stopped,
|
|
989
1006
|
step.execution?.detached,
|
|
990
1007
|
step.watchdog?.phase,
|
|
1008
|
+
unresolvedChildWatchdogBlockers(step.watchdog).length,
|
|
991
1009
|
step.error,
|
|
992
1010
|
expanded ? expandedStepActivityRenderKey(step) : undefined,
|
|
993
1011
|
nestedRenderKey(step.children, expanded),
|
|
@@ -1059,6 +1077,15 @@ export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
|
|
|
1059
1077
|
} : undefined,
|
|
1060
1078
|
preflight: expanded ? job.preflight : job.preflight ? formatWorkflowPreflightPlanSummary(job.preflight) : undefined,
|
|
1061
1079
|
preflightWarnings: expanded ? job.workflow?.preflightWarnings : job.workflow?.preflightWarnings?.length || undefined,
|
|
1080
|
+
checklist: projection.checklist ? {
|
|
1081
|
+
total: projection.checklist.total,
|
|
1082
|
+
done: projection.checklist.done,
|
|
1083
|
+
running: projection.checklist.running,
|
|
1084
|
+
queued: projection.checklist.queued,
|
|
1085
|
+
blocked: projection.checklist.blocked,
|
|
1086
|
+
failed: projection.checklist.failed,
|
|
1087
|
+
phases: projection.checklist.phases.map((phase) => [phase.key, phase.state, phase.done, phase.total, phase.running, phase.queued, phase.blocked, phase.failed, phase.paused, phase.stopped, phase.items.map((item) => [item.key, item.state, item.currentTool, item.currentPath, item.durationMs, item.toolCount, item.error])]),
|
|
1088
|
+
} : undefined,
|
|
1062
1089
|
steps: job.steps?.map((step, index) => widgetStepRenderKey(step, index, expanded)),
|
|
1063
1090
|
nestedChildren: nestedRenderKey(job.nestedChildren, expanded),
|
|
1064
1091
|
lane: laneRenderKey(job, projection),
|
|
@@ -1234,6 +1261,85 @@ function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string
|
|
|
1234
1261
|
return theme.fg("dim", status);
|
|
1235
1262
|
}
|
|
1236
1263
|
|
|
1264
|
+
function workflowChecklistStateLabel(state: WorkflowChecklistState): string {
|
|
1265
|
+
if (state === "running") return "running";
|
|
1266
|
+
if (state === "complete") return "complete";
|
|
1267
|
+
return state;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function workflowChecklistGlyph(item: { state: WorkflowChecklistState; startedAt?: number; durationMs?: number; toolCount?: number; currentToolStartedAt?: number }, theme: Theme, frame?: number): string {
|
|
1271
|
+
if (item.state === "running") return theme.fg("accent", runningGlyph(animatedSeed(runningSeed(item.startedAt, item.durationMs, item.toolCount, item.currentToolStartedAt), frame)));
|
|
1272
|
+
if (item.state === "complete") return theme.fg("success", "✓");
|
|
1273
|
+
if (item.state === "blocked") return theme.fg("error", "!");
|
|
1274
|
+
if (item.state === "failed") return theme.fg("error", "✗");
|
|
1275
|
+
if (item.state === "paused" || item.state === "stopped") return theme.fg("warning", "■");
|
|
1276
|
+
return theme.fg("muted", "◦");
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function workflowChecklistItemPriority(state: WorkflowChecklistState): number {
|
|
1280
|
+
if (state === "blocked") return 0;
|
|
1281
|
+
if (state === "failed") return 1;
|
|
1282
|
+
if (state === "running") return 2;
|
|
1283
|
+
if (state === "paused") return 3;
|
|
1284
|
+
if (state === "stopped") return 4;
|
|
1285
|
+
if (state === "queued") return 5;
|
|
1286
|
+
return 6;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function workflowChecklistItemLine(item: WorkflowChecklistItem, theme: Theme, indent: string, frame?: number): string {
|
|
1290
|
+
const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter(Boolean).join(" · ");
|
|
1291
|
+
const context = contextModeBadge(theme, item.context);
|
|
1292
|
+
const state = item.state === "complete" ? "" : ` ${theme.fg("dim", `· ${workflowChecklistStateLabel(item.state)}`)}`;
|
|
1293
|
+
const details = [
|
|
1294
|
+
item.currentTool ? `${item.currentTool}${item.durationMs !== undefined ? ` ${formatDuration(item.durationMs)}` : ""}` : undefined,
|
|
1295
|
+
!item.currentTool && item.currentPath ? shortenPath(item.currentPath) : undefined,
|
|
1296
|
+
!item.currentTool && item.durationMs !== undefined ? formatDuration(item.durationMs) : undefined,
|
|
1297
|
+
item.toolCount !== undefined ? `${item.toolCount} tools` : undefined,
|
|
1298
|
+
item.outputName ? `out:${item.outputName}` : undefined,
|
|
1299
|
+
item.error ? `error:${oneLine(item.error)}` : undefined,
|
|
1300
|
+
].filter(Boolean).join(" · ");
|
|
1301
|
+
return `${indent}${workflowChecklistGlyph(item, theme, frame)} ${theme.bold(identity || item.key)}${context}${state}${details ? ` ${theme.fg("dim", `· ${details}`)}` : ""}`;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
const COLLAPSED_WORKFLOW_PHASE_LIMIT = 4;
|
|
1305
|
+
|
|
1306
|
+
function workflowChecklistWidgetLines(checklist: WorkflowChecklistProjection | undefined, theme: Theme, indent: string, expanded: boolean, frame?: number, includeSummary = true, limitPhases = !expanded, includeBottleneckOutput = expanded): string[] {
|
|
1307
|
+
if (!checklist?.total) return [];
|
|
1308
|
+
const lines = includeSummary ? [`${indent}${theme.fg("dim", `Checklist ${formatWorkflowChecklistSummary(checklist)}`)}`] : [];
|
|
1309
|
+
const phases = !limitPhases || checklist.phases.length <= COLLAPSED_WORKFLOW_PHASE_LIMIT
|
|
1310
|
+
? checklist.phases
|
|
1311
|
+
: checklist.phases
|
|
1312
|
+
.map((phase, index) => ({ phase, index }))
|
|
1313
|
+
.sort((left, right) => Number(left.phase.state !== "running") - Number(right.phase.state !== "running")
|
|
1314
|
+
|| Number(left.phase.state === "complete") - Number(right.phase.state === "complete")
|
|
1315
|
+
|| left.index - right.index)
|
|
1316
|
+
.slice(0, COLLAPSED_WORKFLOW_PHASE_LIMIT)
|
|
1317
|
+
.sort((left, right) => left.index - right.index)
|
|
1318
|
+
.map(({ phase }) => phase);
|
|
1319
|
+
for (const phase of phases) {
|
|
1320
|
+
const phaseItem = phase.items.find((item) => item.state === "running") ?? phase.items[0];
|
|
1321
|
+
const glyph = workflowChecklistGlyph({
|
|
1322
|
+
state: phase.state,
|
|
1323
|
+
startedAt: phaseItem?.startedAt,
|
|
1324
|
+
durationMs: phaseItem?.durationMs,
|
|
1325
|
+
toolCount: phaseItem?.toolCount,
|
|
1326
|
+
currentToolStartedAt: phaseItem?.currentToolStartedAt,
|
|
1327
|
+
}, theme, frame);
|
|
1328
|
+
lines.push(`${indent}${glyph} ${theme.bold(formatWorkflowChecklistPhase(phase))}`);
|
|
1329
|
+
if (expanded) {
|
|
1330
|
+
for (const item of [...phase.items].sort((left, right) => workflowChecklistItemPriority(left.state) - workflowChecklistItemPriority(right.state))) {
|
|
1331
|
+
lines.push(workflowChecklistItemLine(item, theme, `${indent} `, frame));
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
const bottleneck = formatWorkflowChecklistBottleneck(checklist.bottleneck, { includeOutput: includeBottleneckOutput });
|
|
1336
|
+
if (bottleneck) {
|
|
1337
|
+
const tone = checklist.bottleneck?.state === "blocked" || checklist.bottleneck?.state === "failed" ? "error" : checklist.bottleneck?.state === "running" ? "accent" : "warning";
|
|
1338
|
+
lines.push(`${indent}${theme.fg(tone, `bottleneck · ${bottleneck}`)}`);
|
|
1339
|
+
}
|
|
1340
|
+
return lines;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1237
1343
|
function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], snapshotNow?: number): string {
|
|
1238
1344
|
const facts: string[] = [];
|
|
1239
1345
|
if (step.currentTool && step.currentToolStartedAt !== undefined && snapshotNow !== undefined) facts.push(`${step.currentTool} ${formatDuration(Math.max(0, snapshotNow - step.currentToolStartedAt))}`);
|
|
@@ -1725,12 +1831,15 @@ function buildForegroundResultEntries(
|
|
|
1725
1831
|
return label.itemTitle === "Agent" ? withDuplicateForegroundLabels(entries, label.totalCount) : entries;
|
|
1726
1832
|
}
|
|
1727
1833
|
|
|
1728
|
-
function widgetStats(job: AsyncJobState, theme: Theme, projection = buildWorkflowWidgetProjection(job)): string {
|
|
1834
|
+
function widgetStats(job: AsyncJobState, theme: Theme, projection = buildWorkflowWidgetProjection(job), collapsed = false): string {
|
|
1729
1835
|
const parts: string[] = [];
|
|
1730
1836
|
const { stageProgress } = projection;
|
|
1731
1837
|
const stepsTotal = stageProgress?.total ?? job.stepsTotal ?? (job.agents?.length ?? 1);
|
|
1732
1838
|
const isSingleChild = isSingleChildAsyncJob(job);
|
|
1733
|
-
|
|
1839
|
+
const checklistPrimary = collapsed && job.mode === "workflow" && projection.checklist !== undefined;
|
|
1840
|
+
if (checklistPrimary) {
|
|
1841
|
+
parts.push(formatWorkflowChecklistSummary(projection.checklist!));
|
|
1842
|
+
} else if (stageProgress) {
|
|
1734
1843
|
const currentStage = stageProgress.current !== undefined ? projection.stages[stageProgress.current] : undefined;
|
|
1735
1844
|
const focus = currentStage
|
|
1736
1845
|
? [compactTaskText(undefined, currentStage.label) ?? boundedLaneValue(currentStage.id), currentStage.agent ? boundedLaneValue(currentStage.agent) : "", workflowNodeStatusLabel(currentStage.status)].filter(Boolean)
|
|
@@ -1762,6 +1871,7 @@ function widgetStats(job: AsyncJobState, theme: Theme, projection = buildWorkflo
|
|
|
1762
1871
|
} else if (stepsTotal > 1) {
|
|
1763
1872
|
parts.push(`steps ${stepsTotal}`);
|
|
1764
1873
|
}
|
|
1874
|
+
if (projection.checklist && !checklistPrimary) parts.push(formatWorkflowChecklistSummary(projection.checklist));
|
|
1765
1875
|
if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
|
|
1766
1876
|
if (job.totalTokens?.total) parts.push(formatTokenUsage(job.totalTokens, "token"));
|
|
1767
1877
|
if (job.startedAt !== undefined && job.updatedAt !== undefined) parts.push(formatDuration(Math.max(0, job.updatedAt - job.startedAt)));
|
|
@@ -2044,10 +2154,20 @@ function hostStepWidgetLines(job: AsyncJobState, theme: Theme, indent: string):
|
|
|
2044
2154
|
|
|
2045
2155
|
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
|
|
2046
2156
|
const { steps } = projection;
|
|
2157
|
+
const checklistPrimary = !expanded && job.mode === "workflow" && projection.checklist !== undefined;
|
|
2158
|
+
if (checklistPrimary) {
|
|
2159
|
+
const lines = [
|
|
2160
|
+
...workflowPreflightLines(job, false),
|
|
2161
|
+
...workflowChecklistWidgetLines(projection.checklist, theme, " ", false, frame, false),
|
|
2162
|
+
];
|
|
2163
|
+
if (job.status === "running" || projection.checklist!.running > 0) lines.push(` ${theme.fg("accent", liveDetailHintText())}`);
|
|
2164
|
+
return lines;
|
|
2165
|
+
}
|
|
2047
2166
|
if (!steps.length) {
|
|
2048
2167
|
const lane = projectAsyncLane(job, laneStepForJob(job, steps));
|
|
2049
2168
|
return [
|
|
2050
2169
|
...workflowPreflightLines(job, expanded),
|
|
2170
|
+
...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
|
|
2051
2171
|
...(lane ? formatLaneProjectionLines(lane, theme, " ") : []),
|
|
2052
2172
|
...hostStepWidgetLines(job, theme, " "),
|
|
2053
2173
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
@@ -2055,7 +2175,10 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2055
2175
|
];
|
|
2056
2176
|
}
|
|
2057
2177
|
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
|
|
2058
|
-
const lines: string[] =
|
|
2178
|
+
const lines: string[] = [
|
|
2179
|
+
...workflowPreflightLines(job, expanded),
|
|
2180
|
+
...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
|
|
2181
|
+
];
|
|
2059
2182
|
const group = activeParallelWidgetGroup(job);
|
|
2060
2183
|
if (group) {
|
|
2061
2184
|
lines.push(...parallelWidgetGroupDetails(job, theme, group, expanded, width, frame, Boolean(job.activeParallelGroup)));
|
|
@@ -2087,13 +2210,16 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2087
2210
|
}
|
|
2088
2211
|
|
|
2089
2212
|
function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
|
|
2090
|
-
const stats = widgetStats(job, theme, projection);
|
|
2091
|
-
const count = job.mode === "
|
|
2213
|
+
const stats = widgetStats(job, theme, projection, !expanded);
|
|
2214
|
+
const count = job.mode === "workflow"
|
|
2215
|
+
? projection.checklist?.total ?? projection.stageProgress?.total ?? job.stepsTotal ?? job.agents?.length ?? job.steps?.length
|
|
2216
|
+
: job.mode === "chain" ? job.chainStepCount : projection.stageProgress?.total ?? job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
|
|
2092
2217
|
const mode = widgetJobName(job);
|
|
2093
2218
|
const title = isSingleChildAsyncJob(job)
|
|
2094
2219
|
? "async subagent"
|
|
2095
2220
|
: `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
|
|
2096
|
-
const
|
|
2221
|
+
const checklistPrimary = !expanded && job.mode === "workflow" && projection.checklist !== undefined;
|
|
2222
|
+
const collapseDetails = !checklistPrimary && job.steps?.length === 1 && shouldCollapseSingleChildDetails(job, job.steps[0]!);
|
|
2097
2223
|
const summary = `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, mode)}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`;
|
|
2098
2224
|
return [
|
|
2099
2225
|
`${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
|
|
@@ -2260,10 +2386,11 @@ function progressiveHeaderLine(jobs: AsyncJobState[], theme: Theme, width: numbe
|
|
|
2260
2386
|
}
|
|
2261
2387
|
|
|
2262
2388
|
function progressiveJobLine(job: AsyncJobState, theme: Theme, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string {
|
|
2263
|
-
const
|
|
2264
|
-
const
|
|
2389
|
+
const checklistPrimary = job.mode === "workflow" && projection.checklist !== undefined;
|
|
2390
|
+
const stats = widgetStats(job, theme, projection, true);
|
|
2391
|
+
const activity = checklistPrimary ? "" : widgetActivity(job);
|
|
2265
2392
|
const status = job.status === "complete" ? "done" : job.status;
|
|
2266
|
-
const lane = projectAsyncLane(job, laneStepForJob(job, projection.steps));
|
|
2393
|
+
const lane = checklistPrimary ? undefined : projectAsyncLane(job, laneStepForJob(job, projection.steps));
|
|
2267
2394
|
const laneSummary = lane
|
|
2268
2395
|
? [lane.label ?? lane.role, lane.phase ? `phase:${lane.phase}` : undefined, lane.output ? `out:${lane.output}` : undefined, lane.workspace ? `workspace:${lane.workspace}` : `ref:${lane.ref}`].filter(Boolean).join(" · ")
|
|
2269
2396
|
: "";
|
|
@@ -2430,12 +2557,22 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
|
|
|
2430
2557
|
let slots = MAX_WIDGET_JOBS;
|
|
2431
2558
|
const appendJob = (job: AsyncJobState): void => {
|
|
2432
2559
|
const projection = projectionFor(job);
|
|
2433
|
-
const
|
|
2560
|
+
const checklistPrimary = !expanded && job.mode === "workflow" && projection.checklist !== undefined;
|
|
2561
|
+
const stats = widgetStats(job, theme, projection, !expanded);
|
|
2562
|
+
const details = checklistPrimary
|
|
2563
|
+
? [
|
|
2564
|
+
...workflowChecklistWidgetLines(projection.checklist, theme, " ", false, frame, false),
|
|
2565
|
+
...(job.status === "running" || projection.checklist!.running > 0 ? [` ${theme.fg("accent", liveDetailHintText())}`] : []),
|
|
2566
|
+
]
|
|
2567
|
+
: [
|
|
2568
|
+
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
2569
|
+
...widgetLaneDetailLines(job, theme, projection),
|
|
2570
|
+
...workflowChecklistWidgetLines(projection.checklist, theme, " ", expanded, frame),
|
|
2571
|
+
...widgetParallelAgentDetails(job, theme, expanded, width, frame),
|
|
2572
|
+
];
|
|
2434
2573
|
items.push([
|
|
2435
2574
|
`${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
2436
|
-
|
|
2437
|
-
...widgetLaneDetailLines(job, theme, projection),
|
|
2438
|
-
...widgetParallelAgentDetails(job, theme, expanded, width, frame),
|
|
2575
|
+
...details,
|
|
2439
2576
|
]);
|
|
2440
2577
|
};
|
|
2441
2578
|
|
|
@@ -2580,6 +2717,62 @@ function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue:
|
|
|
2580
2717
|
return "running";
|
|
2581
2718
|
}
|
|
2582
2719
|
|
|
2720
|
+
function foregroundWorkflowChecklist(details: Details): WorkflowChecklistProjection | undefined {
|
|
2721
|
+
if (details.mode !== "workflow") return undefined;
|
|
2722
|
+
const stageNodes = details.workflowGraph ? workflowGraphStageNodes(details.workflowGraph) : [];
|
|
2723
|
+
const steps: WorkflowChecklistStep[] = details.results.flatMap((result, resultIndex) => {
|
|
2724
|
+
const stableIndex = foregroundResultIndex(details, resultIndex);
|
|
2725
|
+
const workflowKey = (result as { workflowKey?: unknown }).workflowKey;
|
|
2726
|
+
if (details.workflowGraph && typeof workflowKey !== "string") return [];
|
|
2727
|
+
const node = typeof workflowKey === "string"
|
|
2728
|
+
? stageNodes.find((candidate) => candidate.id === workflowKey)
|
|
2729
|
+
: undefined;
|
|
2730
|
+
const progress = foregroundProgressForResult(details, resultIndex);
|
|
2731
|
+
const status = isResultRunning(result) || progress?.status === "running"
|
|
2732
|
+
? "running"
|
|
2733
|
+
: result.detached
|
|
2734
|
+
? "paused"
|
|
2735
|
+
: result.stopped
|
|
2736
|
+
? "stopped"
|
|
2737
|
+
: result.exitCode === 0
|
|
2738
|
+
? "complete"
|
|
2739
|
+
: "failed";
|
|
2740
|
+
return [{
|
|
2741
|
+
key: node?.id ?? (typeof workflowKey === "string" ? workflowKey : `result-${resultIndex + 1}`),
|
|
2742
|
+
label: node?.label ?? workflowLabelForResult(details, resultIndex) ?? result.task ?? result.agent,
|
|
2743
|
+
phase: node?.phase,
|
|
2744
|
+
agent: result.agent,
|
|
2745
|
+
status,
|
|
2746
|
+
context: result.context,
|
|
2747
|
+
activityState: progress?.activityState,
|
|
2748
|
+
startedAt: progress?.lastActivityAt !== undefined && progress.durationMs !== undefined ? progress.lastActivityAt - progress.durationMs : undefined,
|
|
2749
|
+
durationMs: progress?.durationMs,
|
|
2750
|
+
currentTool: progress?.currentTool,
|
|
2751
|
+
currentToolStartedAt: progress?.currentToolStartedAt,
|
|
2752
|
+
currentPath: progress?.currentPath,
|
|
2753
|
+
turnCount: progress?.turnCount,
|
|
2754
|
+
toolCount: progress?.toolCount ?? result.progressSummary?.toolCount,
|
|
2755
|
+
error: result.error,
|
|
2756
|
+
toolBudgetBlocked: result.toolBudgetBlocked,
|
|
2757
|
+
turnBudgetExceeded: result.turnBudgetExceeded,
|
|
2758
|
+
timedOut: result.timedOut,
|
|
2759
|
+
stopped: result.stopped,
|
|
2760
|
+
acceptance: result.acceptance ? { status: result.acceptance.status, reviewResult: result.acceptance.reviewResult ? { status: result.acceptance.reviewResult.status } : undefined } : undefined,
|
|
2761
|
+
review: result.review ? { status: result.review.status } : undefined,
|
|
2762
|
+
watchdog: result.watchdog,
|
|
2763
|
+
}];
|
|
2764
|
+
});
|
|
2765
|
+
const checklist = projectWorkflowChecklist({
|
|
2766
|
+
graph: details.workflowGraph,
|
|
2767
|
+
steps,
|
|
2768
|
+
hostSteps: details.workflow?.receipt?.hostSteps,
|
|
2769
|
+
preflight: details.preflight,
|
|
2770
|
+
trace: details.workflow?.trace,
|
|
2771
|
+
now: Date.now(),
|
|
2772
|
+
});
|
|
2773
|
+
return checklist.total > 0 ? checklist : undefined;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2583
2776
|
function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, layout: MainWindowRenderLayout, frame?: number, expanded = false): Component {
|
|
2584
2777
|
const workflow = d.workflow;
|
|
2585
2778
|
const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace, d.preflight) : d.preflight ? buildWorkflowChatProgressRows([], d.preflight) : [];
|
|
@@ -2595,6 +2788,10 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
|
|
|
2595
2788
|
c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Repo ${repoLabel}`), width), 0, 0));
|
|
2596
2789
|
if (d.preflight) c.addChild(new Text(truncLine(theme.fg("dim", formatWorkflowPreflightPlanSummary(d.preflight, { indent: rowIndent })), width), 0, 0));
|
|
2597
2790
|
if (phase) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Phase ${phase}`), width), 0, 0));
|
|
2791
|
+
const checklist = projectWorkflowChecklist({ hostSteps: workflow?.receipt?.hostSteps, preflight: d.preflight, trace: workflow?.trace, now: Date.now() });
|
|
2792
|
+
for (const line of workflowChecklistWidgetLines(checklist, theme, rowIndent, false, frame, true, !expanded, expanded)) {
|
|
2793
|
+
c.addChild(new Text(truncLine(line, width), 0, 0));
|
|
2794
|
+
}
|
|
2598
2795
|
if (rows.length === 0) {
|
|
2599
2796
|
c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}◦ waiting for workflow child launches`), width), 0, 0));
|
|
2600
2797
|
return c;
|
|
@@ -2653,7 +2850,9 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
|
|
|
2653
2850
|
}
|
|
2654
2851
|
const multiLabel = buildMultiProgressLabel(d, hasRunning);
|
|
2655
2852
|
const itemTitle = multiLabel.itemTitle;
|
|
2656
|
-
const
|
|
2853
|
+
const workflowChecklist = foregroundWorkflowChecklist(d);
|
|
2854
|
+
const checklistPrimary = d.mode === "workflow" && workflowChecklist !== undefined;
|
|
2855
|
+
const stats = statJoin(theme, [checklistPrimary ? formatWorkflowChecklistSummary(workflowChecklist) : multiLabel.headerLabel, formatProgressStats(theme, totalSummary), formatTotalCostStat(d.totalCost)]);
|
|
2657
2856
|
const aggregatePresentation = semanticResultPresentation({
|
|
2658
2857
|
running: hasRunning,
|
|
2659
2858
|
detached,
|
|
@@ -2671,6 +2870,16 @@ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLa
|
|
|
2671
2870
|
const rowIndent = mainWindowIndent(layout, 1);
|
|
2672
2871
|
const detailIndent = mainWindowIndent(layout, 2);
|
|
2673
2872
|
c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
|
|
2873
|
+
if (checklistPrimary) {
|
|
2874
|
+
for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, rowIndent, false, frame, false)) {
|
|
2875
|
+
c.addChild(new Text(truncLine(line, width), 0, 0));
|
|
2876
|
+
}
|
|
2877
|
+
if (hasRunning || workflowChecklist.running > 0) c.addChild(new Text(truncLine(theme.fg("accent", `${rowIndent}${liveDetailHintText()}`), width), 0, 0));
|
|
2878
|
+
return c;
|
|
2879
|
+
}
|
|
2880
|
+
for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, rowIndent, false, frame)) {
|
|
2881
|
+
c.addChild(new Text(truncLine(line, width), 0, 0));
|
|
2882
|
+
}
|
|
2674
2883
|
|
|
2675
2884
|
const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
|
|
2676
2885
|
const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
|
|
@@ -3012,6 +3221,10 @@ export function renderSubagentResult(
|
|
|
3012
3221
|
if (chainVis) {
|
|
3013
3222
|
c.addChild(new Text(fit(` ${chainVis}`), 0, 0));
|
|
3014
3223
|
}
|
|
3224
|
+
const workflowChecklist = foregroundWorkflowChecklist(d);
|
|
3225
|
+
for (const line of workflowChecklistWidgetLines(workflowChecklist, theme, " ", true, frame)) {
|
|
3226
|
+
c.addChild(new Text(fit(line), 0, 0));
|
|
3227
|
+
}
|
|
3015
3228
|
|
|
3016
3229
|
const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
|
|
3017
3230
|
const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ChildWatchdogProgress, ChildWatchdogWarningSummary } from "../shared/types.ts";
|
|
2
|
+
import { SUBAGENT_WATCHDOG_WARNING_TYPE, type ResolvedWatchdogConfig, type WatchdogCadenceConfig, type WatchdogCategory, type WatchdogLspConfig } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export const CHILD_WATCHDOG_WARNING_LIMIT = 20;
|
|
2
5
|
|
|
3
6
|
export const CHILD_WATCHDOG_CONFIG_ENV = "PI_SUBAGENT_WATCHDOG_CHILD_CONFIG";
|
|
4
7
|
export const CHILD_WATCHDOG_STATUS_EVENT = "subagent.watchdog.status";
|
|
5
8
|
|
|
6
|
-
export const CHILD_WATCHDOG_PHASES = ["idle", "reviewing", "
|
|
9
|
+
export const CHILD_WATCHDOG_PHASES = ["idle", "reviewing", "stale", "failed"] as const;
|
|
7
10
|
export type ChildWatchdogPhase = typeof CHILD_WATCHDOG_PHASES[number];
|
|
8
11
|
|
|
9
12
|
export interface ChildWatchdogConfig {
|
|
10
|
-
enabled: boolean;
|
|
11
13
|
runId?: string;
|
|
12
14
|
agent?: string;
|
|
13
15
|
childIndex?: number;
|
|
@@ -17,9 +19,9 @@ export interface ChildWatchdogConfig {
|
|
|
17
19
|
model?: string;
|
|
18
20
|
thinking?: string | false;
|
|
19
21
|
lsp: WatchdogLspConfig;
|
|
20
|
-
autoFollowBlockers: boolean;
|
|
21
|
-
autoFollowMaxAttempts: number | null;
|
|
22
22
|
stalemateRepeats: number;
|
|
23
|
+
/** Mid-run review cadence; everyNTools null means boundary reviews only. */
|
|
24
|
+
cadence: WatchdogCadenceConfig;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface ChildWatchdogStatusEvent {
|
|
@@ -31,18 +33,10 @@ export interface ChildWatchdogStatusEvent {
|
|
|
31
33
|
seq: number;
|
|
32
34
|
phase: ChildWatchdogPhase;
|
|
33
35
|
ts: number;
|
|
34
|
-
followUpPending: boolean;
|
|
35
36
|
reason?: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export
|
|
39
|
-
phase: ChildWatchdogPhase;
|
|
40
|
-
seq: number;
|
|
41
|
-
lastUpdate: number;
|
|
42
|
-
followUpPending: boolean;
|
|
43
|
-
reason?: string;
|
|
44
|
-
timedOut?: boolean;
|
|
45
|
-
}
|
|
39
|
+
export type ChildWatchdogStateSnapshot = ChildWatchdogProgress;
|
|
46
40
|
|
|
47
41
|
export function resolveChildWatchdogConfig(input: {
|
|
48
42
|
config: ResolvedWatchdogConfig;
|
|
@@ -55,8 +49,8 @@ export function resolveChildWatchdogConfig(input: {
|
|
|
55
49
|
if (!enabled) return undefined;
|
|
56
50
|
const model = override?.model ?? input.config.children.model;
|
|
57
51
|
const thinking = override?.thinking ?? input.config.children.thinking;
|
|
52
|
+
const cadence = override?.cadence ?? input.config.children.cadence ?? input.config.cadence;
|
|
58
53
|
return {
|
|
59
|
-
enabled: true,
|
|
60
54
|
...(input.runId ? { runId: input.runId } : {}),
|
|
61
55
|
...(input.agent ? { agent: input.agent } : {}),
|
|
62
56
|
...(input.childIndex !== undefined ? { childIndex: input.childIndex } : {}),
|
|
@@ -66,16 +60,11 @@ export function resolveChildWatchdogConfig(input: {
|
|
|
66
60
|
...(model ? { model } : {}),
|
|
67
61
|
...(thinking !== undefined ? { thinking } : {}),
|
|
68
62
|
lsp: { ...input.config.lsp },
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
stalemateRepeats: input.config.children.autoFollow.stalemateRepeats,
|
|
63
|
+
stalemateRepeats: input.config.stalemateRepeats,
|
|
64
|
+
cadence: { everyNTools: cadence.everyNTools ?? null },
|
|
72
65
|
};
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
export function encodeChildWatchdogConfig(config: ChildWatchdogConfig | undefined): string | undefined {
|
|
76
|
-
return config ? JSON.stringify(config) : undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
68
|
function childConfigObject(value: unknown, field: string): Record<string, unknown> {
|
|
80
69
|
if (value && typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
|
|
81
70
|
throw new Error(`Invalid child watchdog config: ${field} must be an object.`);
|
|
@@ -108,10 +97,12 @@ function childConfigNullableNonNegativeInteger(input: Record<string, unknown>, f
|
|
|
108
97
|
throw new Error(`Invalid child watchdog config: ${field} must be null or a non-negative integer.`);
|
|
109
98
|
}
|
|
110
99
|
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
100
|
+
function childConfigCadence(value: unknown): WatchdogCadenceConfig {
|
|
101
|
+
const input = childConfigObject(value, "cadence");
|
|
102
|
+
const everyNTools = input.everyNTools;
|
|
103
|
+
if (everyNTools === null) return { everyNTools: null };
|
|
104
|
+
if (typeof everyNTools === "number" && Number.isInteger(everyNTools) && everyNTools >= 5) return { everyNTools };
|
|
105
|
+
throw new Error("Invalid child watchdog config: cadence.everyNTools must be null or an integer >= 5.");
|
|
115
106
|
}
|
|
116
107
|
|
|
117
108
|
function childConfigLsp(value: unknown): WatchdogLspConfig {
|
|
@@ -138,7 +129,7 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
|
|
|
138
129
|
if (!raw) return undefined;
|
|
139
130
|
const parsed = childConfigObject(JSON.parse(raw), "root");
|
|
140
131
|
if (parsed.enabled === false) return undefined;
|
|
141
|
-
if (parsed.enabled !== true) throw new Error("Invalid child watchdog config: enabled must be true or false.");
|
|
132
|
+
if ("enabled" in parsed && parsed.enabled !== true) throw new Error("Invalid child watchdog config: enabled must be true or false.");
|
|
142
133
|
const thinking = parsed.thinking;
|
|
143
134
|
if (thinking !== undefined && typeof thinking !== "string" && thinking !== false) {
|
|
144
135
|
throw new Error("Invalid child watchdog config: thinking must be a string or false.");
|
|
@@ -148,7 +139,6 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
|
|
|
148
139
|
const childIndex = childConfigOptionalIndex(parsed, "childIndex");
|
|
149
140
|
const model = childConfigOptionalString(parsed, "model");
|
|
150
141
|
return {
|
|
151
|
-
enabled: true,
|
|
152
142
|
...(runId ? { runId } : {}),
|
|
153
143
|
...(agent ? { agent } : {}),
|
|
154
144
|
...(childIndex !== undefined ? { childIndex } : {}),
|
|
@@ -158,9 +148,8 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
|
|
|
158
148
|
...(model ? { model } : {}),
|
|
159
149
|
...(thinking !== undefined ? { thinking: thinking as string | false } : {}),
|
|
160
150
|
lsp: childConfigLsp(parsed.lsp),
|
|
161
|
-
autoFollowBlockers: childConfigBoolean(parsed, "autoFollowBlockers"),
|
|
162
|
-
autoFollowMaxAttempts: childConfigNullableNonNegativeInteger(parsed, "autoFollowMaxAttempts"),
|
|
163
151
|
stalemateRepeats: childConfigPositiveInteger(parsed, "stalemateRepeats"),
|
|
152
|
+
cadence: childConfigCadence(parsed.cadence),
|
|
164
153
|
};
|
|
165
154
|
}
|
|
166
155
|
|
|
@@ -173,14 +162,13 @@ export function isChildWatchdogStatusEvent(value: unknown): value is ChildWatchd
|
|
|
173
162
|
&& event.seq >= 0
|
|
174
163
|
&& typeof event.ts === "number"
|
|
175
164
|
&& Number.isFinite(event.ts)
|
|
176
|
-
&& typeof event.followUpPending === "boolean"
|
|
177
165
|
&& typeof event.phase === "string"
|
|
178
166
|
&& (CHILD_WATCHDOG_PHASES as readonly string[]).includes(event.phase);
|
|
179
167
|
}
|
|
180
168
|
|
|
181
169
|
export function childWatchdogIsActive(snapshot: ChildWatchdogStateSnapshot | undefined): boolean {
|
|
182
170
|
if (!snapshot) return false;
|
|
183
|
-
return snapshot.
|
|
171
|
+
return snapshot.phase === "reviewing";
|
|
184
172
|
}
|
|
185
173
|
|
|
186
174
|
export function acceptChildWatchdogEvent(input: {
|
|
@@ -199,7 +187,40 @@ export function acceptChildWatchdogEvent(input: {
|
|
|
199
187
|
phase: input.event.phase,
|
|
200
188
|
seq: input.event.seq,
|
|
201
189
|
lastUpdate: input.event.ts,
|
|
202
|
-
followUpPending: input.event.followUpPending,
|
|
203
190
|
...(input.event.reason ? { reason: input.event.reason } : {}),
|
|
191
|
+
...(input.current?.warnings?.length ? { warnings: input.current.warnings } : {}),
|
|
204
192
|
};
|
|
205
193
|
}
|
|
194
|
+
|
|
195
|
+
function childWatchdogWarningFromMessage(message: unknown): ChildWatchdogWarningSummary | undefined {
|
|
196
|
+
const candidate = message as { role?: unknown; customType?: unknown; details?: Record<string, unknown> } | undefined;
|
|
197
|
+
if (candidate?.role !== "custom" || candidate.customType !== SUBAGENT_WATCHDOG_WARNING_TYPE) return undefined;
|
|
198
|
+
const details = candidate.details ?? {};
|
|
199
|
+
if (details.severity !== "concern" && details.severity !== "blocker") return undefined;
|
|
200
|
+
if (typeof details.summary !== "string" || typeof details.evidence !== "string" || typeof details.recommendedAction !== "string") return undefined;
|
|
201
|
+
return {
|
|
202
|
+
severity: details.severity,
|
|
203
|
+
category: details.category as WatchdogCategory,
|
|
204
|
+
summary: details.summary,
|
|
205
|
+
evidence: details.evidence,
|
|
206
|
+
recommendedAction: details.recommendedAction,
|
|
207
|
+
...(typeof details.displayedAt === "string" ? { displayedAt: details.displayedAt } : {}),
|
|
208
|
+
addressed: false,
|
|
209
|
+
stalemate: details.state === "stalemate",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** A watchdog warning message is appended; an assistant turn marks earlier warnings addressed. Undefined when unchanged. */
|
|
214
|
+
export function applyChildWatchdogMessage(current: ChildWatchdogStateSnapshot | undefined, message: unknown, now = Date.now()): ChildWatchdogStateSnapshot | undefined {
|
|
215
|
+
const warning = childWatchdogWarningFromMessage(message);
|
|
216
|
+
if (warning) {
|
|
217
|
+
const warnings = [...(current?.warnings ?? []), warning].slice(-CHILD_WATCHDOG_WARNING_LIMIT);
|
|
218
|
+
return current ? { ...current, warnings } : { phase: "idle", seq: 0, lastUpdate: now, warnings };
|
|
219
|
+
}
|
|
220
|
+
if ((message as { role?: unknown } | undefined)?.role !== "assistant" || !current?.warnings?.some((entry) => !entry.addressed)) return undefined;
|
|
221
|
+
return { ...current, warnings: current.warnings.map((entry) => entry.addressed ? entry : { ...entry, addressed: true }) };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function unresolvedChildWatchdogBlockers(progress: Pick<ChildWatchdogProgress, "warnings"> | undefined): ChildWatchdogWarningSummary[] {
|
|
225
|
+
return (progress?.warnings ?? []).filter((warning) => warning.severity === "blocker" && (!warning.addressed || warning.stalemate));
|
|
226
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import { Type, type Static } from "typebox";
|
|
5
|
+
|
|
6
|
+
export const WATCHDOG_DIFF_TOOL_NAME = "watchdog_diff";
|
|
7
|
+
export const WATCHDOG_DIFF_MAX_CHARS = 24_000;
|
|
8
|
+
const MAX_UNTRACKED_FILES = 50;
|
|
9
|
+
|
|
10
|
+
export interface WatchdogDiffBaseline {
|
|
11
|
+
root: string;
|
|
12
|
+
ref: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const WatchdogDiffParams = Type.Object({
|
|
16
|
+
path: Type.Optional(Type.String({ description: "Restrict the diff to one file or directory, relative to the repo root." })),
|
|
17
|
+
stat: Type.Optional(Type.Boolean({ description: "Return per-file change counts instead of the full diff." })),
|
|
18
|
+
}, { additionalProperties: false });
|
|
19
|
+
|
|
20
|
+
type WatchdogDiffParams = Static<typeof WatchdogDiffParams>;
|
|
21
|
+
|
|
22
|
+
function runGit(root: string, args: string[]): { ok: boolean; stdout: string; stderr: string } {
|
|
23
|
+
const result = spawnSync("git", ["-C", root, ...args], { encoding: "utf-8", maxBuffer: 16 * 1024 * 1024, windowsHide: true });
|
|
24
|
+
return { ok: result.status === 0, stdout: result.stdout ?? "", stderr: (result.stderr ?? "").trim() };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** HEAD at session start, so later child commits still show in the diff. */
|
|
28
|
+
export function captureWatchdogDiffBaseline(cwd: string): WatchdogDiffBaseline | undefined {
|
|
29
|
+
const toplevel = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
30
|
+
if (!toplevel.ok) return undefined;
|
|
31
|
+
const head = runGit(cwd, ["rev-parse", "HEAD"]);
|
|
32
|
+
if (!head.ok) return undefined;
|
|
33
|
+
const root = toplevel.stdout.trim();
|
|
34
|
+
const ref = head.stdout.trim();
|
|
35
|
+
return root && ref ? { root, ref } : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validatePath(value: string | undefined): string | undefined {
|
|
39
|
+
const trimmed = value?.trim();
|
|
40
|
+
if (!trimmed) return undefined;
|
|
41
|
+
if (trimmed.startsWith("-")) throw new Error("watchdog_diff path must not start with '-'.");
|
|
42
|
+
if (path.isAbsolute(trimmed)) throw new Error("watchdog_diff path must be relative to the repo root.");
|
|
43
|
+
if (trimmed.split(/[\\/]/).includes("..")) throw new Error("watchdog_diff path must not contain '..'.");
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function bound(text: string): string {
|
|
48
|
+
if (text.length <= WATCHDOG_DIFF_MAX_CHARS) return text;
|
|
49
|
+
const marker = `\n\n[... ${text.length - WATCHDOG_DIFF_MAX_CHARS} characters omitted; call again with a narrower path ...]`;
|
|
50
|
+
return `${text.slice(0, WATCHDOG_DIFF_MAX_CHARS - marker.length)}${marker}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** In a shared cwd, changes already pending when the session started also appear. */
|
|
54
|
+
export function createWatchdogDiffTool(baseline: WatchdogDiffBaseline): AgentTool<typeof WatchdogDiffParams, { chars: number }> {
|
|
55
|
+
return {
|
|
56
|
+
name: WATCHDOG_DIFF_TOOL_NAME,
|
|
57
|
+
label: "Watchdog diff",
|
|
58
|
+
description: "Show the repository diff since the review baseline, plus untracked file paths. Optional path narrows it; stat:true returns per-file counts only.",
|
|
59
|
+
parameters: WatchdogDiffParams,
|
|
60
|
+
executionMode: "sequential",
|
|
61
|
+
async execute(_toolCallId, params: WatchdogDiffParams) {
|
|
62
|
+
const pathFilter = validatePath(params.path);
|
|
63
|
+
const diff = runGit(baseline.root, ["diff", "--no-color", "--no-ext-diff", ...(params.stat === true ? ["--stat"] : []), baseline.ref, "--", ...(pathFilter ? [pathFilter] : [])]);
|
|
64
|
+
if (!diff.ok) throw new Error(`git diff failed: ${diff.stderr || "unknown error"}`);
|
|
65
|
+
const untrackedResult = runGit(baseline.root, ["ls-files", "--others", "--exclude-standard", "-z", "--", ...(pathFilter ? [pathFilter] : [])]);
|
|
66
|
+
const untracked = untrackedResult.ok ? untrackedResult.stdout.split("\0").filter(Boolean) : [];
|
|
67
|
+
const sections = [diff.stdout.trimEnd()];
|
|
68
|
+
if (untracked.length) {
|
|
69
|
+
const shown = untracked.slice(0, MAX_UNTRACKED_FILES);
|
|
70
|
+
sections.push(["Untracked files (use read to inspect):", ...shown.map((file) => ` ${file}`)].join("\n"));
|
|
71
|
+
if (untracked.length > shown.length) sections.push(`... ${untracked.length - shown.length} more untracked files`);
|
|
72
|
+
}
|
|
73
|
+
const text = bound(sections.filter(Boolean).join("\n\n")) || `No changes since baseline ${baseline.ref.slice(0, 12)}.`;
|
|
74
|
+
return { content: [{ type: "text", text }], details: { chars: text.length } };
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|