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.
Files changed (57) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/docs/agents.md +5 -4
  3. package/docs/configuration.md +28 -2
  4. package/docs/models.md +5 -5
  5. package/docs/observability.md +4 -1
  6. package/docs/tool-reference.md +2 -2
  7. package/docs/watchdog.md +92 -114
  8. package/package.json +1 -1
  9. package/skills/pi-subagents/references/execution-controls.md +4 -3
  10. package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
  11. package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
  12. package/src/agents/agent-management.ts +22 -4
  13. package/src/agents/agents.ts +107 -125
  14. package/src/api/shared-types.ts +3 -0
  15. package/src/extension/config.ts +20 -0
  16. package/src/inspectors/herdr/inspector-runner.ts +19 -13
  17. package/src/runs/background/active-async-capacity.ts +0 -1
  18. package/src/runs/background/async-execution.ts +56 -7
  19. package/src/runs/background/async-resume.ts +3 -0
  20. package/src/runs/background/async-status.ts +18 -2
  21. package/src/runs/background/notify.ts +54 -3
  22. package/src/runs/background/run-status.ts +22 -2
  23. package/src/runs/background/subagent-runner.ts +53 -3
  24. package/src/runs/background/wait-completions.ts +13 -0
  25. package/src/runs/foreground/execution.ts +7 -1
  26. package/src/runs/foreground/subagent-executor.ts +56 -4
  27. package/src/runs/shared/acceptance.ts +25 -9
  28. package/src/runs/shared/async-status-projection.ts +11 -43
  29. package/src/runs/shared/lane-metadata.ts +24 -3
  30. package/src/runs/shared/parallel-handoff.ts +4 -0
  31. package/src/runs/shared/pi-args.ts +9 -6
  32. package/src/runs/shared/subagent-control.ts +4 -2
  33. package/src/runs/shared/task-intent.ts +5 -2
  34. package/src/runs/shared/worktree.ts +467 -63
  35. package/src/shared/types.ts +38 -2
  36. package/src/shared/utils.ts +18 -7
  37. package/src/slash/subagents-admin.ts +24 -12
  38. package/src/tui/fleet-status.ts +61 -2
  39. package/src/tui/fleet.ts +12 -7
  40. package/src/tui/render.ts +227 -14
  41. package/src/watchdog/child-status.ts +54 -33
  42. package/src/watchdog/diff-tool.ts +77 -0
  43. package/src/watchdog/emission-guard.ts +5 -3
  44. package/src/watchdog/guidance.ts +20 -0
  45. package/src/watchdog/register-child.ts +16 -13
  46. package/src/watchdog/register-main.ts +10 -9
  47. package/src/watchdog/render.ts +4 -5
  48. package/src/watchdog/review.ts +15 -4
  49. package/src/watchdog/rules.ts +70 -0
  50. package/src/watchdog/runtime.ts +75 -92
  51. package/src/watchdog/scope.ts +0 -11
  52. package/src/watchdog/settings.ts +48 -104
  53. package/src/watchdog/types.ts +18 -32
  54. package/src/watchdog/warning-format.ts +0 -1
  55. package/src/workflows/chat-progress.ts +3 -2
  56. package/src/workflows/workflow-checklist.ts +439 -0
  57. 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
- if (stageProgress) {
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[] = workflowPreflightLines(job, expanded);
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 === "chain" ? job.chainStepCount : projection.stageProgress?.total ?? job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
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 collapseDetails = job.steps?.length === 1 && shouldCollapseSingleChildDetails(job, job.steps[0]!);
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 stats = widgetStats(job, theme, projection);
2264
- const activity = widgetActivity(job);
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 stats = widgetStats(job, theme, projection);
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
- ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
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 stats = statJoin(theme, [multiLabel.headerLabel, formatProgressStats(theme, totalSummary), formatTotalCostStat(d.totalCost)]);
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 { ResolvedWatchdogConfig, WatchdogLspConfig } from "./types.ts";
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", "autofollow", "settling", "stale", "failed"] as const;
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 interface ChildWatchdogStateSnapshot {
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
- autoFollowBlockers: input.config.children.autoFollow.blockers,
70
- autoFollowMaxAttempts: input.config.children.autoFollow.maxAttempts,
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 childConfigBoolean(input: Record<string, unknown>, field: string): boolean {
112
- const value = input[field];
113
- if (typeof value === "boolean") return value;
114
- throw new Error(`Invalid child watchdog config: ${field} must be a boolean.`);
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.followUpPending || snapshot.phase === "reviewing" || snapshot.phase === "autofollow" || snapshot.phase === "settling";
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
+ }