pi-subagents 0.66.0 → 0.68.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 +138 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +43 -15
- package/docs/configuration.md +67 -23
- package/docs/extension-api.md +38 -19
- package/docs/missions.md +10 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +22 -12
- package/docs/standalone-background.md +59 -0
- package/docs/tool-reference.md +33 -20
- package/docs/watchdog.md +39 -10
- package/docs/workflows.md +37 -13
- package/index.ts +5 -2
- package/inspector-runner.mjs +2 -2
- package/package.json +4 -2
- package/prompts/parallel-review.md +1 -1
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +32 -0
- package/skills/pi-subagents/SKILL.md +32 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +11 -9
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +190 -71
- package/src/agents/builtin-names.ts +1 -0
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +94 -59
- package/src/api/required-child-extensions.ts +6 -0
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +66 -4
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +62 -39
- package/src/extension/public-execution.ts +7 -5
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +81 -81
- package/src/extension/tool-description.ts +30 -82
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +44 -31
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +348 -176
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +17 -12
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +23 -10
- package/src/runs/background/binary-bootstrap.ts +38 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +105 -7
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +81 -33
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +12 -33
- package/src/runs/background/runner-child-launch.ts +6 -1
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +51 -18
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +441 -304
- package/src/runs/background/subagent-wait.ts +176 -28
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +134 -248
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +374 -178
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +38 -11
- package/src/runs/shared/async-status-projection.ts +127 -33
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +28 -5
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +8 -1
- package/src/runs/shared/child-session.ts +127 -52
- package/src/runs/shared/child-tool-plan.ts +142 -11
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +45 -15
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +68 -8
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +74 -30
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -7
- package/src/slash/subagents-admin.ts +24 -13
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +168 -37
- package/src/watchdog/child-status.ts +28 -28
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +21 -1
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +10 -2
- package/src/watchdog/register-main.ts +39 -35
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +123 -74
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +100 -27
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +3 -0
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +5 -3
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +279 -10
- package/src/workflows/workflow-checklist.ts +2 -2
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/runner-server-preload.mjs +0 -13
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
package/src/tui/render.ts
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
WIDGET_KEY,
|
|
27
27
|
} from "../shared/types.ts";
|
|
28
28
|
import { previewDisplayText, sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
29
|
-
import {
|
|
29
|
+
import { formatShortcutLabel } from "../shared/shortcuts.ts";
|
|
30
30
|
import { formatContextUsage, formatTokens, formatUsage, formatDuration, formatModelThinking, formatToolCall, formatTokenUsage, shortenPath } from "../shared/formatters.ts";
|
|
31
31
|
import { getDisplayItems, getSingleResultOutput, PROMPT_REDACTED } from "../shared/utils.ts";
|
|
32
32
|
import { flatToLogicalStepIndex } from "../runs/background/parallel-groups.ts";
|
|
@@ -45,6 +45,10 @@ import { formatWorkflowChecklistBottleneck, formatWorkflowChecklistPhase, format
|
|
|
45
45
|
type Theme = ExtensionContext["ui"]["theme"];
|
|
46
46
|
|
|
47
47
|
interface WorkflowWidgetProjection {
|
|
48
|
+
now: number;
|
|
49
|
+
inlineFleetCovered?: boolean;
|
|
50
|
+
children?: AsyncJobState[];
|
|
51
|
+
materializedKeys?: Set<string>;
|
|
48
52
|
stages: WorkflowGraphNode[];
|
|
49
53
|
steps: AsyncJobStep[];
|
|
50
54
|
stageProgress?: { total: number; current?: number };
|
|
@@ -91,11 +95,11 @@ function liveDetailKeyText(): string {
|
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
export function liveDetailHintText(): string {
|
|
94
|
-
return `Press ${liveDetailKeyText()} for live detail
|
|
98
|
+
return `Press ${liveDetailKeyText()} for live detail`;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
function workflowDetailHintText(): string {
|
|
98
|
-
return `Press ${liveDetailKeyText()} for details
|
|
102
|
+
return `Press ${liveDetailKeyText()} for details`;
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
function foregroundSingleHintText(shortcut?: string): string {
|
|
@@ -443,11 +447,14 @@ function workflowStageProgress(job: AsyncJobState, stages = job.mode === "workfl
|
|
|
443
447
|
return { total: stages.length };
|
|
444
448
|
}
|
|
445
449
|
|
|
446
|
-
function buildWorkflowWidgetProjection(job: AsyncJobState): WorkflowWidgetProjection {
|
|
450
|
+
function buildWorkflowWidgetProjection(job: AsyncJobState, now = Date.now()): WorkflowWidgetProjection {
|
|
447
451
|
const stages = job.mode === "workflow" ? workflowGraphStageNodes(job.workflowGraph) : [];
|
|
448
452
|
const stageProgress = workflowStageProgress(job, stages);
|
|
449
|
-
const
|
|
450
|
-
const
|
|
453
|
+
const clock = job.status === "running" ? now : job.updatedAt;
|
|
454
|
+
const loadedSteps = workflowWidgetSteps(job, stages);
|
|
455
|
+
const steps = job.mode === "workflow" ? loadedSteps.map((step) => step.status === "running" && step.startedAt !== undefined && clock !== undefined
|
|
456
|
+
? { ...step, durationMs: Math.max(0, clock - step.startedAt) } : step) : loadedSteps;
|
|
457
|
+
const projection: WorkflowWidgetProjection = { stages, steps, now };
|
|
451
458
|
if (stageProgress) projection.stageProgress = stageProgress;
|
|
452
459
|
if (stages.length) projection.plannedKeys = new Set(stages.map((node) => node.id));
|
|
453
460
|
if (job.mode === "workflow") {
|
|
@@ -457,19 +464,27 @@ function buildWorkflowWidgetProjection(job: AsyncJobState): WorkflowWidgetProjec
|
|
|
457
464
|
hostSteps: job.hostSteps,
|
|
458
465
|
preflight: job.preflight,
|
|
459
466
|
trace: job.workflow?.trace,
|
|
460
|
-
now:
|
|
467
|
+
now: clock,
|
|
461
468
|
});
|
|
462
469
|
if (checklist.total > 0) projection.checklist = checklist;
|
|
463
470
|
}
|
|
464
471
|
return projection;
|
|
465
472
|
}
|
|
466
473
|
|
|
467
|
-
function workflowWidgetProjectionLookup(): WorkflowWidgetProjectionLookup {
|
|
474
|
+
function workflowWidgetProjectionLookup(now = Date.now(), childrenByParent = new Map<string, AsyncJobState[]>()): WorkflowWidgetProjectionLookup {
|
|
468
475
|
const projections = new WeakMap<AsyncJobState, WorkflowWidgetProjection>();
|
|
469
476
|
return (job: AsyncJobState) => {
|
|
470
477
|
const cached = projections.get(job);
|
|
471
478
|
if (cached) return cached;
|
|
472
|
-
const projection = buildWorkflowWidgetProjection(job);
|
|
479
|
+
const projection = buildWorkflowWidgetProjection(job, now);
|
|
480
|
+
const children = childrenByParent.get(job.asyncId);
|
|
481
|
+
if (children?.length) {
|
|
482
|
+
projection.children = children;
|
|
483
|
+
projection.materializedKeys = new Set(children.flatMap((child) => [child.asyncId, ...(child.workflowKey ? [child.workflowKey] : [])]));
|
|
484
|
+
for (const step of projection.steps) {
|
|
485
|
+
if (step.runId && projection.materializedKeys.has(step.runId) && step.workflowKey) projection.materializedKeys.add(step.workflowKey);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
473
488
|
projections.set(job, projection);
|
|
474
489
|
return projection;
|
|
475
490
|
};
|
|
@@ -1051,9 +1066,11 @@ function hostStepRenderKey(row: AsyncStatusWorkflowRow): unknown[] {
|
|
|
1051
1066
|
}
|
|
1052
1067
|
|
|
1053
1068
|
export function widgetRenderKey(job: AsyncJobState, expanded = false): string {
|
|
1054
|
-
const projection = buildWorkflowWidgetProjection(job);
|
|
1069
|
+
const projection = buildWorkflowWidgetProjection(job, job.updatedAt ?? 0);
|
|
1055
1070
|
return JSON.stringify({
|
|
1056
1071
|
asyncDir: job.asyncDir,
|
|
1072
|
+
parentWorkflowRunId: job.parentWorkflowRunId,
|
|
1073
|
+
workflowKey: job.workflowKey,
|
|
1057
1074
|
status: job.status,
|
|
1058
1075
|
description: job.mode === "workflow" ? job.description : undefined,
|
|
1059
1076
|
activityState: job.activityState,
|
|
@@ -1501,9 +1518,10 @@ function compactWorkflowStats(job: AsyncJobState, rows: readonly CompactWorkflow
|
|
|
1501
1518
|
const rowToolUses = rows.map((row) => row.toolUses).filter((value): value is number => value !== undefined);
|
|
1502
1519
|
const toolUses = job.toolCount ?? (rowToolUses.length ? rowToolUses.reduce((sum, value) => sum + value, 0) : undefined);
|
|
1503
1520
|
const rowDuration = rows.map((row) => row.durationMs).filter((value): value is number => value !== undefined);
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1506
|
-
|
|
1521
|
+
const end = job.status === "running" ? projection.now : job.updatedAt;
|
|
1522
|
+
const durationMs = job.status !== "queued" && job.startedAt !== undefined && end !== undefined
|
|
1523
|
+
? Math.max(0, end - job.startedAt)
|
|
1524
|
+
: job.status !== "queued" && rowDuration.length ? Math.max(...rowDuration) : undefined;
|
|
1507
1525
|
return statJoin(theme, [
|
|
1508
1526
|
`id: ${compactWorkflowShortId(job)}`,
|
|
1509
1527
|
...progress,
|
|
@@ -1540,8 +1558,11 @@ function compactWorkflowHeaderLine(job: AsyncJobState, theme: Theme, width?: num
|
|
|
1540
1558
|
function compactWorkflowWidgetBodyLines(job: AsyncJobState, theme: Theme, frame: number | undefined, projection: WorkflowWidgetProjection): string[] {
|
|
1541
1559
|
const rows = compactWorkflowLaneRows(job, projection.checklist);
|
|
1542
1560
|
const lines = [` ${compactWorkflowStats(job, rows, theme, projection)}`];
|
|
1561
|
+
if (projection.inlineFleetCovered) return [...lines, ` ${theme.fg("dim", "Workflow children shown in Fleet roster")}`];
|
|
1543
1562
|
if (rows.length) {
|
|
1544
|
-
for (const row of rows)
|
|
1563
|
+
for (const row of rows) {
|
|
1564
|
+
if (!projection.materializedKeys?.has(row.key)) lines.push(compactWorkflowLaneLine(row, theme, " ", frame));
|
|
1565
|
+
}
|
|
1545
1566
|
} else {
|
|
1546
1567
|
lines.push(` ${theme.fg("dim", "◦ waiting for workflow lanes")}`);
|
|
1547
1568
|
}
|
|
@@ -2085,7 +2106,8 @@ function widgetStats(job: AsyncJobState, theme: Theme, projection = buildWorkflo
|
|
|
2085
2106
|
if (projection.checklist && !checklistPrimary) parts.push(formatWorkflowChecklistSummary(projection.checklist));
|
|
2086
2107
|
if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
|
|
2087
2108
|
if (job.totalTokens?.total) parts.push(formatTokenUsage(job.totalTokens, "token"));
|
|
2088
|
-
|
|
2109
|
+
const end = job.status === "running" && (job.mode === "workflow" || job.parentWorkflowRunId) ? projection.now : job.updatedAt;
|
|
2110
|
+
if (job.status !== "queued" && job.startedAt !== undefined && end !== undefined) parts.push(formatDuration(Math.max(0, end - job.startedAt)));
|
|
2089
2111
|
return statJoin(theme, parts);
|
|
2090
2112
|
}
|
|
2091
2113
|
|
|
@@ -2364,7 +2386,8 @@ function hostStepWidgetLines(job: AsyncJobState, theme: Theme, indent: string):
|
|
|
2364
2386
|
}
|
|
2365
2387
|
|
|
2366
2388
|
function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
|
|
2367
|
-
const
|
|
2389
|
+
const steps = projection.steps.filter((step) => !projection.materializedKeys?.has(step.workflowKey ?? "") && !projection.materializedKeys?.has(step.runId ?? ""));
|
|
2390
|
+
const checklist = widgetChecklistWithoutMaterializedChildren(projection);
|
|
2368
2391
|
if (!expanded && job.mode === "workflow") {
|
|
2369
2392
|
return compactWorkflowWidgetBodyLines(job, theme, frame, projection);
|
|
2370
2393
|
}
|
|
@@ -2372,7 +2395,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2372
2395
|
const lane = projectAsyncLane(job, laneStepForJob(job, steps));
|
|
2373
2396
|
return [
|
|
2374
2397
|
...(expanded ? workflowPreflightLines(job) : []),
|
|
2375
|
-
...workflowChecklistWidgetLines(
|
|
2398
|
+
...workflowChecklistWidgetLines(checklist, theme, " ", expanded, frame),
|
|
2376
2399
|
...(lane ? formatLaneProjectionLines(lane, theme, " ") : []),
|
|
2377
2400
|
...hostStepWidgetLines(job, theme, " "),
|
|
2378
2401
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
@@ -2382,7 +2405,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2382
2405
|
if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, frame);
|
|
2383
2406
|
const lines: string[] = [
|
|
2384
2407
|
...(expanded ? workflowPreflightLines(job) : []),
|
|
2385
|
-
...workflowChecklistWidgetLines(
|
|
2408
|
+
...workflowChecklistWidgetLines(checklist, theme, " ", expanded, frame, { includeItemErrors: !expanded }),
|
|
2386
2409
|
];
|
|
2387
2410
|
const group = activeParallelWidgetGroup(job);
|
|
2388
2411
|
if (group) {
|
|
@@ -2415,7 +2438,7 @@ function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded
|
|
|
2415
2438
|
}
|
|
2416
2439
|
|
|
2417
2440
|
function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame?: number, projection = buildWorkflowWidgetProjection(job)): string[] {
|
|
2418
|
-
if (!expanded && job.mode === "workflow") return [compactWorkflowHeaderLine(job, theme, width), ...compactWorkflowWidgetBodyLines(job, theme, frame, projection)].map((line) => truncLine(line, width));
|
|
2441
|
+
if ((!expanded || projection.inlineFleetCovered) && job.mode === "workflow") return [compactWorkflowHeaderLine(job, theme, width), ...compactWorkflowWidgetBodyLines(job, theme, frame, projection)].map((line) => truncLine(line, width));
|
|
2419
2442
|
const stats = widgetStats(job, theme, projection, !expanded);
|
|
2420
2443
|
const count = job.mode === "workflow"
|
|
2421
2444
|
? projection.checklist?.total ?? projection.stageProgress?.total ?? job.stepsTotal ?? job.agents?.length ?? job.steps?.length
|
|
@@ -2522,6 +2545,10 @@ function buildSingleLineWidgetLines(jobs: AsyncJobState[], theme: Theme, width:
|
|
|
2522
2545
|
if (counts.failed.length > 0) parts.push(`${counts.failed.length} failed`);
|
|
2523
2546
|
if (counts.stopped.length > 0) parts.push(`${counts.stopped.length} stopped`);
|
|
2524
2547
|
if (counts.paused.length > 0) parts.push(`${counts.paused.length} paused`);
|
|
2548
|
+
for (const status of ["partial", "rejected"] as const) {
|
|
2549
|
+
const count = jobs.filter((job) => job.status === status).length;
|
|
2550
|
+
if (count > 0) parts.push(`${count} ${status}`);
|
|
2551
|
+
}
|
|
2525
2552
|
if (!hasActive && counts.complete.length > 0) parts.push(`${counts.complete.length}/${jobs.length} done`);
|
|
2526
2553
|
return [truncLine(`${theme.fg(hasActive ? "accent" : "dim", glyph)} ${theme.fg(hasActive ? "accent" : "dim", "subagents")} (${parts.join(", ") || `${jobs.length} total`})`, width)];
|
|
2527
2554
|
}
|
|
@@ -2723,6 +2750,68 @@ function fitAdaptiveWidgetLines(jobs: AsyncJobState[], buildLines: () => string[
|
|
|
2723
2750
|
}
|
|
2724
2751
|
|
|
2725
2752
|
const asyncWidgetUpdates = new WeakMap<ExtensionContext["ui"], (jobs: AsyncJobState[]) => void>();
|
|
2753
|
+
const inlineWorkflowCoverage = new WeakMap<ExtensionContext["ui"], ReadonlyMap<string, string>>();
|
|
2754
|
+
const asyncWidgetInvalidations = new WeakMap<ExtensionContext["ui"], () => void>();
|
|
2755
|
+
|
|
2756
|
+
/** Include child identity and freshness in the roster's existing snapshot. */
|
|
2757
|
+
export function inlineWorkflowRenderKey(job: AsyncJobState, children: AsyncJobState[]): string {
|
|
2758
|
+
if (!children.length) return widgetRenderKey(job);
|
|
2759
|
+
return JSON.stringify([widgetRenderKey(job), children.map((child) => [
|
|
2760
|
+
child.asyncId, widgetRenderKey(child, true), child.context,
|
|
2761
|
+
child.steps?.map((step) => [Boolean(step.runner), step.tokens?.window]), Boolean(child.workflowGraph),
|
|
2762
|
+
])]);
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
/** Presentation-only coverage from the mounted inline Fleet roster, never configuration. */
|
|
2766
|
+
export function setInlineWorkflowCoverage(ui: ExtensionContext["ui"], coverage: ReadonlyMap<string, string>): void {
|
|
2767
|
+
const previous = inlineWorkflowCoverage.get(ui);
|
|
2768
|
+
if ((previous?.size ?? 0) === coverage.size && [...coverage].every(([id, key]) => previous?.get(id) === key)) return;
|
|
2769
|
+
if (coverage.size) inlineWorkflowCoverage.set(ui, coverage);
|
|
2770
|
+
else inlineWorkflowCoverage.delete(ui);
|
|
2771
|
+
asyncWidgetInvalidations.get(ui)?.();
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
/** Attach only to loaded workflow parents; orphan jobs remain top-level. */
|
|
2775
|
+
function widgetJobTree(jobs: AsyncJobState[], now: number): { roots: AsyncJobState[]; projectionFor: WorkflowWidgetProjectionLookup } {
|
|
2776
|
+
const parents = new Map(jobs.filter((job) => job.mode === "workflow").map((job) => [job.asyncId, job]));
|
|
2777
|
+
const childrenByParent = new Map<string, AsyncJobState[]>();
|
|
2778
|
+
const roots: AsyncJobState[] = [];
|
|
2779
|
+
for (const job of jobs) {
|
|
2780
|
+
const parent = job.parentWorkflowRunId ? parents.get(job.parentWorkflowRunId) : undefined;
|
|
2781
|
+
// Root-only adaptive summaries cannot advertise live work under a non-running parent.
|
|
2782
|
+
const keepLiveRoot = isProgressiveActiveJob(job) && parent?.status !== "running";
|
|
2783
|
+
if (parent && parent.asyncId !== job.asyncId && !keepLiveRoot) {
|
|
2784
|
+
const children = childrenByParent.get(parent.asyncId) ?? [];
|
|
2785
|
+
children.push(job);
|
|
2786
|
+
childrenByParent.set(parent.asyncId, children);
|
|
2787
|
+
} else roots.push(job);
|
|
2788
|
+
}
|
|
2789
|
+
return { roots, projectionFor: workflowWidgetProjectionLookup(now, childrenByParent) };
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
function widgetChecklistWithoutMaterializedChildren(projection: WorkflowWidgetProjection): WorkflowChecklistProjection | undefined {
|
|
2793
|
+
const { checklist, materializedKeys } = projection;
|
|
2794
|
+
if (!checklist || !materializedKeys) return checklist;
|
|
2795
|
+
return { ...checklist, phases: checklist.phases.map((phase) => ({ ...phase, items: phase.items.filter((item) => !materializedKeys.has(item.key)) })) };
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
function materializedWidgetChildLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, frame: number | undefined, projectionFor: WorkflowWidgetProjectionLookup): string[] {
|
|
2799
|
+
const { children, inlineFleetCovered } = projectionFor(job);
|
|
2800
|
+
if (inlineFleetCovered || !children?.length) return [];
|
|
2801
|
+
const lines: string[] = [];
|
|
2802
|
+
const shown = orderedWidgetJobs(children).slice(0, MAX_WIDGET_JOBS);
|
|
2803
|
+
for (const [index, child] of shown.entries()) {
|
|
2804
|
+
const projection = projectionFor(child);
|
|
2805
|
+
const last = index === children.length - 1;
|
|
2806
|
+
const identity = child.workflowKey ?? child.asyncId;
|
|
2807
|
+
const stats = widgetStats(child, theme, projection, !expanded);
|
|
2808
|
+
lines.push(` ${last ? "└─" : "├─"} ${theme.bold(identity)} ${widgetStatusGlyph(child, theme, frame)} ${themeBold(theme, widgetJobName(child))}${contextModeBadge(theme, child.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`);
|
|
2809
|
+
for (const detail of foregroundStyleWidgetDetails(child, theme, expanded, Math.max(0, width - 6), frame, projection)) lines.push(` ${last ? " " : "│ "}${detail}`);
|
|
2810
|
+
for (const detail of materializedWidgetChildLines(child, theme, Math.max(0, width - 4), expanded, frame, projectionFor)) lines.push(` ${detail}`);
|
|
2811
|
+
}
|
|
2812
|
+
if (shown.length < children.length) lines.push(` +${children.length - shown.length} more workflow children`);
|
|
2813
|
+
return lines;
|
|
2814
|
+
}
|
|
2726
2815
|
|
|
2727
2816
|
function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"]): (tui: { requestRender(): void }, theme: Theme) => Component {
|
|
2728
2817
|
return (tui, theme) => {
|
|
@@ -2731,6 +2820,14 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
|
|
|
2731
2820
|
let cachedFrame: number | undefined;
|
|
2732
2821
|
let cachedExpanded: boolean | undefined;
|
|
2733
2822
|
let cachedLines: string[] | undefined;
|
|
2823
|
+
let cachedCoverage = "[]";
|
|
2824
|
+
let collapsed = false;
|
|
2825
|
+
const invalidate = (): void => {
|
|
2826
|
+
cachedLines = undefined;
|
|
2827
|
+
resetWidgetLayoutSession();
|
|
2828
|
+
tui.requestRender();
|
|
2829
|
+
};
|
|
2830
|
+
asyncWidgetInvalidations.set(ui, invalidate);
|
|
2734
2831
|
const update = (nextJobs: AsyncJobState[]): void => {
|
|
2735
2832
|
jobs = nextJobs;
|
|
2736
2833
|
cachedLines = undefined;
|
|
@@ -2738,25 +2835,60 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
|
|
|
2738
2835
|
};
|
|
2739
2836
|
asyncWidgetUpdates.set(ui, update);
|
|
2740
2837
|
const component = Object.assign(container, {
|
|
2838
|
+
// Only the mouse fields used here, without requiring newer Pi type exports.
|
|
2839
|
+
handleMouse(event: { type: string; button: string; y: number; shift: boolean; alt: boolean; ctrl: boolean }) {
|
|
2840
|
+
if (event.type !== "click" || event.button !== "left" || event.y !== 0) return undefined;
|
|
2841
|
+
if (event.shift || event.alt || event.ctrl) return undefined;
|
|
2842
|
+
collapsed = !collapsed;
|
|
2843
|
+
invalidate();
|
|
2844
|
+
return { handled: true };
|
|
2845
|
+
},
|
|
2741
2846
|
dispose(): void {
|
|
2742
2847
|
if (asyncWidgetUpdates.get(ui) === update) asyncWidgetUpdates.delete(ui);
|
|
2848
|
+
if (asyncWidgetInvalidations.get(ui) === invalidate) asyncWidgetInvalidations.delete(ui);
|
|
2743
2849
|
},
|
|
2744
2850
|
});
|
|
2745
2851
|
container.render = (renderWidth: number): string[] => {
|
|
2746
|
-
const
|
|
2852
|
+
const now = Date.now();
|
|
2853
|
+
const frame = Math.floor(now / WIDGET_ANIMATION_INTERVAL_MS);
|
|
2747
2854
|
const expanded = ui.getToolsExpanded?.() ?? false;
|
|
2748
|
-
|
|
2855
|
+
const coverage = inlineWorkflowCoverage.get(ui);
|
|
2856
|
+
const covered = new Set<string>();
|
|
2857
|
+
if (coverage?.size) {
|
|
2858
|
+
const childrenByParent = new Map<string, AsyncJobState[]>();
|
|
2859
|
+
for (const child of jobs) {
|
|
2860
|
+
if (!child.parentWorkflowRunId) continue;
|
|
2861
|
+
const children = childrenByParent.get(child.parentWorkflowRunId) ?? [];
|
|
2862
|
+
children.push(child);
|
|
2863
|
+
childrenByParent.set(child.parentWorkflowRunId, children);
|
|
2864
|
+
}
|
|
2865
|
+
for (const job of jobs) {
|
|
2866
|
+
const snapshot = coverage.get(job.asyncId);
|
|
2867
|
+
if (snapshot === undefined) continue;
|
|
2868
|
+
const children = childrenByParent.get(job.asyncId) ?? [];
|
|
2869
|
+
if (!children.some((child) => childrenByParent.has(child.asyncId))
|
|
2870
|
+
&& snapshot === inlineWorkflowRenderKey(job, children)) covered.add(job.asyncId);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
const coverageKey = JSON.stringify([...covered]);
|
|
2874
|
+
if (cachedLines && cachedRenderWidth === renderWidth && cachedFrame === frame && cachedExpanded === expanded && cachedCoverage === coverageKey) return cachedLines;
|
|
2875
|
+
if (cachedCoverage !== coverageKey) resetWidgetLayoutSession();
|
|
2876
|
+
cachedCoverage = coverageKey;
|
|
2749
2877
|
const width = Math.max(0, renderWidth - 2);
|
|
2750
|
-
const projectionFor =
|
|
2878
|
+
const { roots, projectionFor } = widgetJobTree(jobs, now);
|
|
2879
|
+
for (const job of roots) projectionFor(job).inlineFleetCovered = covered.has(job.asyncId);
|
|
2751
2880
|
const buildLines = (): string[] => expanded
|
|
2752
|
-
? buildWidgetLinesWithProjection(
|
|
2753
|
-
:
|
|
2754
|
-
? compactSingleWidgetLines(
|
|
2755
|
-
: buildWidgetLinesWithProjection(
|
|
2881
|
+
? buildWidgetLinesWithProjection(roots, theme, width, true, frame, projectionFor)
|
|
2882
|
+
: roots.length === 1 && !projectionFor(roots[0]!).children?.length
|
|
2883
|
+
? compactSingleWidgetLines(roots[0]!, theme, width, frame, projectionFor(roots[0]!))
|
|
2884
|
+
: buildWidgetLinesWithProjection(roots, theme, width, false, frame, projectionFor);
|
|
2756
2885
|
cachedRenderWidth = renderWidth;
|
|
2757
2886
|
cachedFrame = frame;
|
|
2758
2887
|
cachedExpanded = expanded;
|
|
2759
|
-
cachedLines =
|
|
2888
|
+
cachedLines = (collapsed
|
|
2889
|
+
? buildSingleLineWidgetLines(jobs, theme, width, frame)
|
|
2890
|
+
: fitAdaptiveWidgetLines(roots, buildLines, theme, width, expanded, frame, projectionFor)
|
|
2891
|
+
).map((line) => paddedWidgetLine(line, renderWidth));
|
|
2760
2892
|
return cachedLines;
|
|
2761
2893
|
};
|
|
2762
2894
|
return component;
|
|
@@ -2765,7 +2897,10 @@ function buildWidgetComponent(jobs: AsyncJobState[], ui: ExtensionContext["ui"])
|
|
|
2765
2897
|
|
|
2766
2898
|
function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number, projectionFor: WorkflowWidgetProjectionLookup = workflowWidgetProjectionLookup()): string[] {
|
|
2767
2899
|
if (jobs.length === 0) return [];
|
|
2768
|
-
if (jobs.length === 1) return
|
|
2900
|
+
if (jobs.length === 1) return [
|
|
2901
|
+
...buildSingleWidgetLines(jobs[0]!, theme, width, expanded, frame, projectionFor(jobs[0]!)),
|
|
2902
|
+
...materializedWidgetChildLines(jobs[0]!, theme, width, expanded, frame, projectionFor).map((line) => truncLine(line, width)),
|
|
2903
|
+
];
|
|
2769
2904
|
const running = jobs.filter((job) => job.status === "running");
|
|
2770
2905
|
const queued = jobs.filter((job) => job.status === "queued");
|
|
2771
2906
|
const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
|
|
@@ -2782,7 +2917,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
|
|
|
2782
2917
|
let slots = MAX_WIDGET_JOBS;
|
|
2783
2918
|
const appendJob = (job: AsyncJobState): void => {
|
|
2784
2919
|
const projection = projectionFor(job);
|
|
2785
|
-
const compactWorkflow = !expanded && job.mode === "workflow";
|
|
2920
|
+
const compactWorkflow = (!expanded || projection.inlineFleetCovered) && job.mode === "workflow";
|
|
2786
2921
|
const stats = compactWorkflow ? "" : widgetStats(job, theme, projection, !expanded);
|
|
2787
2922
|
const details = compactWorkflow
|
|
2788
2923
|
? [
|
|
@@ -2791,7 +2926,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
|
|
|
2791
2926
|
: [
|
|
2792
2927
|
` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
|
|
2793
2928
|
...widgetLaneDetailLines(job, theme, projection),
|
|
2794
|
-
...workflowChecklistWidgetLines(projection
|
|
2929
|
+
...workflowChecklistWidgetLines(widgetChecklistWithoutMaterializedChildren(projection), theme, " ", expanded, frame, { includeItemErrors: !expanded || !job.steps?.length }),
|
|
2795
2930
|
...widgetParallelAgentDetails(job, theme, expanded, width, frame),
|
|
2796
2931
|
];
|
|
2797
2932
|
items.push([
|
|
@@ -2799,6 +2934,7 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
|
|
|
2799
2934
|
? compactWorkflowHeaderLine(job, theme, width)
|
|
2800
2935
|
: `${widgetStatusGlyph(job, theme, frame)} ${themeBold(theme, widgetJobName(job))}${contextModeBadge(theme, job.context)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
|
|
2801
2936
|
...details,
|
|
2937
|
+
...materializedWidgetChildLines(job, theme, width, expanded, frame, projectionFor),
|
|
2802
2938
|
]);
|
|
2803
2939
|
};
|
|
2804
2940
|
|
|
@@ -2845,7 +2981,8 @@ function buildWidgetLinesWithProjection(jobs: AsyncJobState[], theme: Theme, wid
|
|
|
2845
2981
|
}
|
|
2846
2982
|
|
|
2847
2983
|
export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, frame?: number): string[] {
|
|
2848
|
-
|
|
2984
|
+
const { roots, projectionFor } = widgetJobTree(jobs, Date.now());
|
|
2985
|
+
return buildWidgetLinesWithProjection(roots, theme, width, expanded, frame, projectionFor);
|
|
2849
2986
|
}
|
|
2850
2987
|
|
|
2851
2988
|
/**
|
|
@@ -3347,9 +3484,6 @@ export function renderSubagentResult(
|
|
|
3347
3484
|
if (r.skillsWarning) {
|
|
3348
3485
|
c.addChild(new Text(fit(theme.fg("warning", `Warning: ${r.skillsWarning}`)), 0, 0));
|
|
3349
3486
|
}
|
|
3350
|
-
if (r.attemptedModels && r.attemptedModels.length > 1) {
|
|
3351
|
-
c.addChild(new Text(fit(theme.fg("dim", `Fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
|
|
3352
|
-
}
|
|
3353
3487
|
c.addChild(new Text(fit(theme.fg("dim", formatUsage(r.usage, r.model))), 0, 0));
|
|
3354
3488
|
if (r.sessionFile) {
|
|
3355
3489
|
c.addChild(new Text(fit(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`)), 0, 0));
|
|
@@ -3526,9 +3660,6 @@ export function renderSubagentResult(
|
|
|
3526
3660
|
if (r.skillsWarning) {
|
|
3527
3661
|
c.addChild(new Text(fit(theme.fg("warning", ` Warning: ${r.skillsWarning}`)), 0, 0));
|
|
3528
3662
|
}
|
|
3529
|
-
if (r.attemptedModels && r.attemptedModels.length > 1) {
|
|
3530
|
-
c.addChild(new Text(fit(theme.fg("dim", ` fallbacks: ${r.attemptedModels.join(" → ")}`)), 0, 0));
|
|
3531
|
-
}
|
|
3532
3663
|
|
|
3533
3664
|
if (rRunning && rProg) {
|
|
3534
3665
|
if (rProg.skills?.length) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChildWatchdogProgress, ChildWatchdogWarningSummary } from "../shared/types.ts";
|
|
2
|
-
import {
|
|
2
|
+
import { WATCHDOG_WARNING_CATEGORIES, WATCHDOG_WARNING_IMPORTANCES, type ResolvedWatchdogConfig, type WatchdogCadenceConfig, type WatchdogLspConfig } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
export const CHILD_WATCHDOG_WARNING_LIMIT = 20;
|
|
5
5
|
|
|
@@ -33,6 +33,7 @@ export interface ChildWatchdogStatusEvent {
|
|
|
33
33
|
phase: ChildWatchdogPhase;
|
|
34
34
|
ts: number;
|
|
35
35
|
reason?: string;
|
|
36
|
+
warning?: ChildWatchdogWarningSummary;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export type ChildWatchdogStateSnapshot = ChildWatchdogProgress;
|
|
@@ -127,6 +128,9 @@ function childConfigLsp(value: unknown): WatchdogLspConfig {
|
|
|
127
128
|
export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdogConfig | undefined {
|
|
128
129
|
if (!raw) return undefined;
|
|
129
130
|
const parsed = childConfigObject(JSON.parse(raw), "root");
|
|
131
|
+
if (Object.hasOwn(parsed, "fallbackModels")) {
|
|
132
|
+
throw new Error("Invalid child watchdog config: fallbackModels was removed; configure one model instead.");
|
|
133
|
+
}
|
|
130
134
|
if (parsed.enabled === false) return undefined;
|
|
131
135
|
if ("enabled" in parsed && parsed.enabled !== true) throw new Error("Invalid child watchdog config: enabled must be true or false.");
|
|
132
136
|
const thinking = parsed.thinking;
|
|
@@ -155,6 +159,15 @@ export function decodeChildWatchdogConfig(raw: string | undefined): ChildWatchdo
|
|
|
155
159
|
export function isChildWatchdogStatusEvent(value: unknown): value is ChildWatchdogStatusEvent {
|
|
156
160
|
if (!value || typeof value !== "object") return false;
|
|
157
161
|
const event = value as Partial<ChildWatchdogStatusEvent>;
|
|
162
|
+
const warningValue = event.warning;
|
|
163
|
+
const warning = warningValue as Partial<ChildWatchdogWarningSummary>;
|
|
164
|
+
const validWarning = warningValue === undefined || (warningValue !== null && typeof warningValue === "object" && !Array.isArray(warningValue)
|
|
165
|
+
&& (warning.severity === "concern" || warning.severity === "blocker")
|
|
166
|
+
&& typeof warning.importance === "string" && (WATCHDOG_WARNING_IMPORTANCES as readonly string[]).includes(warning.importance)
|
|
167
|
+
&& typeof warning.category === "string" && (WATCHDOG_WARNING_CATEGORIES as readonly string[]).includes(warning.category)
|
|
168
|
+
&& typeof warning.summary === "string" && typeof warning.evidence === "string" && typeof warning.recommendedAction === "string"
|
|
169
|
+
&& typeof warning.addressed === "boolean" && typeof warning.stalemate === "boolean"
|
|
170
|
+
&& (warning.displayedAt === undefined || typeof warning.displayedAt === "string"));
|
|
158
171
|
return event.type === CHILD_WATCHDOG_STATUS_EVENT
|
|
159
172
|
&& typeof event.seq === "number"
|
|
160
173
|
&& Number.isInteger(event.seq)
|
|
@@ -162,7 +175,8 @@ export function isChildWatchdogStatusEvent(value: unknown): value is ChildWatchd
|
|
|
162
175
|
&& typeof event.ts === "number"
|
|
163
176
|
&& Number.isFinite(event.ts)
|
|
164
177
|
&& typeof event.phase === "string"
|
|
165
|
-
&& (CHILD_WATCHDOG_PHASES as readonly string[]).includes(event.phase)
|
|
178
|
+
&& (CHILD_WATCHDOG_PHASES as readonly string[]).includes(event.phase)
|
|
179
|
+
&& validWarning;
|
|
166
180
|
}
|
|
167
181
|
|
|
168
182
|
export function childWatchdogIsActive(snapshot: ChildWatchdogStateSnapshot | undefined): boolean {
|
|
@@ -182,40 +196,20 @@ export function acceptChildWatchdogEvent(input: {
|
|
|
182
196
|
const eventIndex = input.event.childIndex ?? input.event.stepIndex;
|
|
183
197
|
if (input.childIndex !== undefined && eventIndex !== input.childIndex) return undefined;
|
|
184
198
|
if (input.current && input.event.seq <= input.current.seq) return undefined;
|
|
199
|
+
const warnings = input.event.warning
|
|
200
|
+
? [...(input.current?.warnings ?? []), input.event.warning].slice(-CHILD_WATCHDOG_WARNING_LIMIT)
|
|
201
|
+
: input.current?.warnings;
|
|
185
202
|
return {
|
|
186
203
|
phase: input.event.phase,
|
|
187
204
|
seq: input.event.seq,
|
|
188
205
|
lastUpdate: input.event.ts,
|
|
189
206
|
...(input.event.reason ? { reason: input.event.reason } : {}),
|
|
190
|
-
...(
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function childWatchdogWarningFromMessage(message: unknown): ChildWatchdogWarningSummary | undefined {
|
|
195
|
-
const candidate = message as { role?: unknown; customType?: unknown; details?: Record<string, unknown> } | undefined;
|
|
196
|
-
if (candidate?.role !== "custom" || candidate.customType !== SUBAGENT_WATCHDOG_WARNING_TYPE) return undefined;
|
|
197
|
-
const details = candidate.details ?? {};
|
|
198
|
-
if (details.severity !== "concern" && details.severity !== "blocker") return undefined;
|
|
199
|
-
if (typeof details.summary !== "string" || typeof details.evidence !== "string" || typeof details.recommendedAction !== "string") return undefined;
|
|
200
|
-
return {
|
|
201
|
-
severity: details.severity,
|
|
202
|
-
category: details.category as WatchdogCategory,
|
|
203
|
-
summary: details.summary,
|
|
204
|
-
evidence: details.evidence,
|
|
205
|
-
recommendedAction: details.recommendedAction,
|
|
206
|
-
...(typeof details.displayedAt === "string" ? { displayedAt: details.displayedAt } : {}),
|
|
207
|
-
addressed: false,
|
|
208
|
-
stalemate: details.state === "stalemate",
|
|
207
|
+
...(warnings?.length ? { warnings } : {}),
|
|
209
208
|
};
|
|
210
209
|
}
|
|
211
210
|
|
|
212
|
-
/**
|
|
213
|
-
export function applyChildWatchdogMessage(current: ChildWatchdogStateSnapshot | undefined, message: unknown
|
|
214
|
-
const warning = childWatchdogWarningFromMessage(message);
|
|
215
|
-
if (warning) {
|
|
216
|
-
const warnings = [...(current?.warnings ?? []), warning].slice(-CHILD_WATCHDOG_WARNING_LIMIT);
|
|
217
|
-
return current ? { ...current, warnings } : { phase: "idle", seq: 0, lastUpdate: now, warnings };
|
|
218
|
-
}
|
|
211
|
+
/** An assistant turn marks earlier warnings addressed. Undefined when unchanged. */
|
|
212
|
+
export function applyChildWatchdogMessage(current: ChildWatchdogStateSnapshot | undefined, message: unknown): ChildWatchdogStateSnapshot | undefined {
|
|
219
213
|
if ((message as { role?: unknown } | undefined)?.role !== "assistant" || !current?.warnings?.some((entry) => !entry.addressed)) return undefined;
|
|
220
214
|
return { ...current, warnings: current.warnings.map((entry) => entry.addressed ? entry : { ...entry, addressed: true }) };
|
|
221
215
|
}
|
|
@@ -223,3 +217,9 @@ export function applyChildWatchdogMessage(current: ChildWatchdogStateSnapshot |
|
|
|
223
217
|
export function unresolvedChildWatchdogBlockers(progress: Pick<ChildWatchdogProgress, "warnings"> | undefined): ChildWatchdogWarningSummary[] {
|
|
224
218
|
return (progress?.warnings ?? []).filter((warning) => warning.severity === "blocker" && (!warning.addressed || warning.stalemate));
|
|
225
219
|
}
|
|
220
|
+
|
|
221
|
+
export function childWatchdogProgressForModel(progress: ChildWatchdogProgress | undefined): ChildWatchdogProgress | undefined {
|
|
222
|
+
if (!progress) return undefined;
|
|
223
|
+
const warnings = (progress.warnings ?? []).filter((warning) => warning.importance === "high");
|
|
224
|
+
return { ...progress, warnings: warnings.length ? warnings : undefined };
|
|
225
|
+
}
|
|
@@ -216,7 +216,7 @@ export function watchdogWarningFromLspDiagnostics(result: WatchdogLspResult): Wa
|
|
|
216
216
|
return {
|
|
217
217
|
severity,
|
|
218
218
|
category: "correctness",
|
|
219
|
-
|
|
219
|
+
importance: "high",
|
|
220
220
|
source: "lsp",
|
|
221
221
|
summary: `LSP found ${count} ${kind}${count === 1 ? "" : "s"} in changed ${count === 1 ? "file" : "files"}.`,
|
|
222
222
|
evidence: evidence || formatDiagnostic(primary),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { normalizeModelSegment, resolveModelCandidate } from "../runs/shared/model-
|
|
2
|
+
import { normalizeModelSegment, resolveModelCandidate } from "../runs/shared/model-resolution.ts";
|
|
3
|
+
import type { WatchdogEndpointConfig } from "./types.ts";
|
|
3
4
|
import {
|
|
4
5
|
getSupportedThinkingLevels,
|
|
5
6
|
splitKnownThinkingSuffix,
|
|
@@ -43,6 +44,7 @@ export interface ResolvedWatchdogModelInput {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export interface WatchdogModelRecommendation {
|
|
47
|
+
source: "configured" | "suggested";
|
|
46
48
|
model: string;
|
|
47
49
|
thinking: ThinkingLevel;
|
|
48
50
|
label: string;
|
|
@@ -147,6 +149,7 @@ function resolveStrongCandidate(ctx: ExtensionContext, family: StrongWatchdogFam
|
|
|
147
149
|
if (!getSupportedThinkingLevels(modelInfo).includes(STRONG_WATCHDOG_THINKING)) continue;
|
|
148
150
|
const current = ctx.model ? fullModelId(toModelInfo(ctx.model)) : "no current session model";
|
|
149
151
|
return {
|
|
152
|
+
source: "suggested",
|
|
150
153
|
model: resolved.model,
|
|
151
154
|
thinking: STRONG_WATCHDOG_THINKING,
|
|
152
155
|
label: preference.label,
|
|
@@ -165,3 +168,20 @@ export function recommendStrongWatchdogModel(ctx: ExtensionContext): WatchdogMod
|
|
|
165
168
|
const current = ctx.model ? fullModelId(toModelInfo(ctx.model)) : "the current session";
|
|
166
169
|
throw new Error(`No authenticated strong complementary watchdog model was found for ${current}. Configure access to Opus 4.8 or GPT 5.5, then run the recommendation again.`);
|
|
167
170
|
}
|
|
171
|
+
|
|
172
|
+
export function recommendWatchdogModel(ctx: ExtensionContext, configured: WatchdogEndpointConfig): WatchdogModelRecommendation {
|
|
173
|
+
if (!configured.model) return recommendStrongWatchdogModel(ctx);
|
|
174
|
+
const resolved = resolveWatchdogModelInput(ctx, configured.model);
|
|
175
|
+
const thinking = resolved.thinking ?? parseWatchdogThinkingInput(configured.thinking) ?? "off";
|
|
176
|
+
return {
|
|
177
|
+
...resolved,
|
|
178
|
+
source: "configured",
|
|
179
|
+
thinking: thinking === false ? "off" : thinking,
|
|
180
|
+
label: "configured watchdog",
|
|
181
|
+
reason: "Keep the configured watchdog model and thinking; no replacement recommendation is needed. This is not a strength or independence assessment.",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function formatWatchdogRecommendation(recommendation: WatchdogModelRecommendation): string {
|
|
186
|
+
return `${recommendation.source === "configured" ? "Keep configured watchdog" : "Recommended strong watchdog"}: ${recommendation.model}:${recommendation.thinking}`;
|
|
187
|
+
}
|
|
@@ -4,6 +4,7 @@ import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
|
4
4
|
import { Type, type Static } from "typebox";
|
|
5
5
|
import { appendPermissionAudit, permissionArgsPreview } from "../runs/shared/permissions.ts";
|
|
6
6
|
import { agentStreamOptions } from "../shared/agent-stream-options.ts";
|
|
7
|
+
import { opencodeSessionHeaders } from "../shared/opencode-session-headers.ts";
|
|
7
8
|
import { decodeChildWatchdogConfig } from "./child-status.ts";
|
|
8
9
|
import { childResolvedConfig } from "./register-child.ts";
|
|
9
10
|
import { resolveWatchdogReviewModel } from "./review.ts";
|
|
@@ -94,6 +95,7 @@ export function createWatchdogPermissionArbiter(options: WatchdogPermissionArbit
|
|
|
94
95
|
const config = childResolvedConfig(childConfig);
|
|
95
96
|
const selection = await resolveWatchdogReviewModel(request.ctx, config);
|
|
96
97
|
const auth = selection.auth;
|
|
98
|
+
const sessionId = request.ctx.sessionManager.getSessionId();
|
|
97
99
|
const registeredProvider = (request.ctx.modelRegistry as {
|
|
98
100
|
getRegisteredProviderConfig?: (provider: string) => { api?: string; streamSimple?: StreamFn } | undefined;
|
|
99
101
|
}).getRegisteredProviderConfig?.(selection.model.provider);
|
|
@@ -104,7 +106,7 @@ export function createWatchdogPermissionArbiter(options: WatchdogPermissionArbit
|
|
|
104
106
|
...streamOptions,
|
|
105
107
|
...(auth.apiKey ? { apiKey: auth.apiKey } : {}),
|
|
106
108
|
env: auth.env || streamOptions?.env ? { ...(auth.env ?? {}), ...(streamOptions?.env ?? {}) } : undefined,
|
|
107
|
-
headers: { ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
109
|
+
headers: { ...opencodeSessionHeaders(model, sessionId), ...(streamOptions?.headers ?? {}), ...(auth.headers ?? {}) },
|
|
108
110
|
});
|
|
109
111
|
agent = new Agent({
|
|
110
112
|
initialState: {
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
type ChildWatchdogPhase,
|
|
11
11
|
type ChildWatchdogStatusEvent,
|
|
12
12
|
} from "./child-status.ts";
|
|
13
|
-
import type {
|
|
13
|
+
import type { ChildWatchdogWarningSummary } from "../shared/types.ts";
|
|
14
|
+
import { SUBAGENT_WATCHDOG_WARNING_TYPE, type ResolvedWatchdogConfig, type WatchdogWarningDetails } from "./types.ts";
|
|
14
15
|
|
|
15
16
|
export function childResolvedConfig(config: ChildWatchdogConfig): ResolvedWatchdogConfig {
|
|
16
17
|
return {
|
|
@@ -57,7 +58,7 @@ export function registerChildWatchdog(
|
|
|
57
58
|
let currentContext: ExtensionContext | undefined;
|
|
58
59
|
let diffBaseline: WatchdogDiffBaseline | undefined;
|
|
59
60
|
let seq = 0;
|
|
60
|
-
const emitStatus = (phase: ChildWatchdogPhase, reason?: string): void => {
|
|
61
|
+
const emitStatus = (phase: ChildWatchdogPhase, reason?: string, warning?: ChildWatchdogWarningSummary): void => {
|
|
61
62
|
writeStatus({
|
|
62
63
|
type: CHILD_WATCHDOG_STATUS_EVENT,
|
|
63
64
|
...(childConfig.runId ? { runId: childConfig.runId } : {}),
|
|
@@ -67,6 +68,7 @@ export function registerChildWatchdog(
|
|
|
67
68
|
phase,
|
|
68
69
|
ts: Date.now(),
|
|
69
70
|
...(reason ? { reason } : {}),
|
|
71
|
+
...(warning ? { warning } : {}),
|
|
70
72
|
});
|
|
71
73
|
};
|
|
72
74
|
const resolved = childResolvedConfig(childConfig);
|
|
@@ -77,8 +79,14 @@ export function registerChildWatchdog(
|
|
|
77
79
|
reviewChangesOnly: true,
|
|
78
80
|
displayWarning: (details, options) => {
|
|
79
81
|
const childDetails = childWarningDetails(details, childConfig);
|
|
82
|
+
emitStatus("reviewing", undefined, { severity: childDetails.severity, importance: childDetails.importance, category: childDetails.category, summary: childDetails.summary, evidence: childDetails.evidence, recommendedAction: childDetails.recommendedAction, ...(childDetails.displayedAt ? { displayedAt: childDetails.displayedAt } : {}), addressed: false, stalemate: childDetails.state === "stalemate" });
|
|
80
83
|
pi.sendMessage(createWatchdogWarningMessage(childDetails, { display: true, details: childDetails }), options);
|
|
81
84
|
},
|
|
85
|
+
displayUserWarning: (details) => {
|
|
86
|
+
const childDetails = childWarningDetails(details, childConfig);
|
|
87
|
+
emitStatus("reviewing", undefined, { severity: childDetails.severity, importance: childDetails.importance, category: childDetails.category, summary: childDetails.summary, evidence: childDetails.evidence, recommendedAction: childDetails.recommendedAction, ...(childDetails.displayedAt ? { displayedAt: childDetails.displayedAt } : {}), addressed: false, stalemate: childDetails.state === "stalemate" });
|
|
88
|
+
pi.appendEntry(SUBAGENT_WATCHDOG_WARNING_TYPE, childDetails);
|
|
89
|
+
},
|
|
82
90
|
});
|
|
83
91
|
const rememberContext = (ctx: ExtensionContext) => {
|
|
84
92
|
currentContext = ctx;
|