pi-subagents 0.62.0 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -1
- package/docs/agents.md +5 -4
- package/docs/configuration.md +28 -2
- package/docs/models.md +5 -5
- package/docs/observability.md +4 -1
- package/docs/tool-reference.md +2 -2
- package/docs/watchdog.md +92 -114
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +4 -3
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
- package/src/agents/agent-management.ts +22 -4
- package/src/agents/agents.ts +107 -125
- package/src/api/shared-types.ts +3 -0
- package/src/extension/config.ts +20 -0
- package/src/inspectors/herdr/inspector-runner.ts +19 -13
- package/src/runs/background/active-async-capacity.ts +0 -1
- package/src/runs/background/async-execution.ts +56 -7
- package/src/runs/background/async-resume.ts +3 -0
- package/src/runs/background/async-status.ts +18 -2
- package/src/runs/background/notify.ts +54 -3
- package/src/runs/background/run-status.ts +22 -2
- package/src/runs/background/subagent-runner.ts +53 -3
- package/src/runs/background/wait-completions.ts +13 -0
- package/src/runs/foreground/execution.ts +7 -1
- package/src/runs/foreground/subagent-executor.ts +56 -4
- package/src/runs/shared/acceptance.ts +25 -9
- package/src/runs/shared/async-status-projection.ts +11 -43
- package/src/runs/shared/lane-metadata.ts +24 -3
- package/src/runs/shared/parallel-handoff.ts +4 -0
- package/src/runs/shared/pi-args.ts +9 -6
- package/src/runs/shared/subagent-control.ts +4 -2
- package/src/runs/shared/task-intent.ts +5 -2
- package/src/runs/shared/worktree.ts +467 -63
- package/src/shared/types.ts +38 -2
- package/src/shared/utils.ts +18 -7
- package/src/slash/subagents-admin.ts +24 -12
- package/src/tui/fleet-status.ts +61 -2
- package/src/tui/fleet.ts +12 -7
- package/src/tui/render.ts +227 -14
- package/src/watchdog/child-status.ts +54 -33
- package/src/watchdog/diff-tool.ts +77 -0
- package/src/watchdog/emission-guard.ts +5 -3
- package/src/watchdog/guidance.ts +20 -0
- package/src/watchdog/register-child.ts +16 -13
- package/src/watchdog/register-main.ts +10 -9
- package/src/watchdog/render.ts +4 -5
- package/src/watchdog/review.ts +15 -4
- package/src/watchdog/rules.ts +70 -0
- package/src/watchdog/runtime.ts +75 -92
- package/src/watchdog/scope.ts +0 -11
- package/src/watchdog/settings.ts +48 -104
- package/src/watchdog/types.ts +18 -32
- package/src/watchdog/warning-format.ts +0 -1
- package/src/workflows/chat-progress.ts +3 -2
- package/src/workflows/workflow-checklist.ts +439 -0
- package/src/workflows/workflow-preflight.ts +28 -1
package/src/shared/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { ThinkingLevel } from "./model-info.ts";
|
|
|
15
15
|
import type { GlobalMissionIndexRecord, MissionRecord, MissionStoreConfig } from "../missions/types.ts";
|
|
16
16
|
import type { ExtensionBindings } from "../runs/shared/extension-bindings.ts";
|
|
17
17
|
import type { WorkflowChildPermitContext } from "./workflow-child-permit.ts";
|
|
18
|
+
import type { WatchdogWarningDetails } from "../watchdog/types.ts";
|
|
18
19
|
|
|
19
20
|
// ============================================================================
|
|
20
21
|
// Basic Types
|
|
@@ -382,6 +383,17 @@ export type SubagentResultStatus = "completed" | "failed" | "paused" | "stopped"
|
|
|
382
383
|
export type SubagentOutputState = "present" | "absent" | "unknown";
|
|
383
384
|
export type SubagentRunMode = "single" | "parallel" | "chain" | "workflow";
|
|
384
385
|
export type SubagentResultMode = SubagentRunMode;
|
|
386
|
+
export type WorktreeProvider = "auto" | "native" | "worktrunk";
|
|
387
|
+
export type ManagedWorktreeProvider = Exclude<WorktreeProvider, "auto">;
|
|
388
|
+
|
|
389
|
+
export interface WorktreeNaming {
|
|
390
|
+
requestedBranch: string;
|
|
391
|
+
branchPrefix: string;
|
|
392
|
+
label: string;
|
|
393
|
+
sanitizedPathComponent: string;
|
|
394
|
+
collision?: "branch" | "path" | "both";
|
|
395
|
+
collisionSuffix?: string;
|
|
396
|
+
}
|
|
385
397
|
|
|
386
398
|
export interface ParallelHandoffPatch {
|
|
387
399
|
path: string;
|
|
@@ -425,6 +437,10 @@ export interface ParallelHandoffCleanupTask {
|
|
|
425
437
|
index: number;
|
|
426
438
|
path: string;
|
|
427
439
|
branch: string;
|
|
440
|
+
/** Provider that allocated this worktree; omitted in old manifests. */
|
|
441
|
+
provider?: ManagedWorktreeProvider;
|
|
442
|
+
/** Branch/path naming evidence retained with the cleanup authority. */
|
|
443
|
+
naming?: WorktreeNaming;
|
|
428
444
|
worktreeRemoved: boolean;
|
|
429
445
|
branchRemoved: boolean;
|
|
430
446
|
preserved?: boolean;
|
|
@@ -879,13 +895,19 @@ export interface SubagentResultIntercomPayload {
|
|
|
879
895
|
// Progress Tracking
|
|
880
896
|
// ============================================================================
|
|
881
897
|
|
|
898
|
+
export interface ChildWatchdogWarningSummary extends Pick<WatchdogWarningDetails, "severity" | "category" | "summary" | "evidence" | "recommendedAction" | "displayedAt"> {
|
|
899
|
+
/** True when a later assistant turn in the child followed the warning. */
|
|
900
|
+
addressed: boolean;
|
|
901
|
+
stalemate: boolean;
|
|
902
|
+
}
|
|
903
|
+
|
|
882
904
|
export interface ChildWatchdogProgress {
|
|
883
|
-
phase: "idle" | "reviewing" | "
|
|
905
|
+
phase: "idle" | "reviewing" | "stale" | "failed";
|
|
884
906
|
seq: number;
|
|
885
907
|
lastUpdate: number;
|
|
886
|
-
followUpPending: boolean;
|
|
887
908
|
reason?: string;
|
|
888
909
|
timedOut?: boolean;
|
|
910
|
+
warnings?: ChildWatchdogWarningSummary[];
|
|
889
911
|
}
|
|
890
912
|
|
|
891
913
|
export interface AgentProgress {
|
|
@@ -1202,6 +1224,8 @@ export interface SingleResult {
|
|
|
1202
1224
|
* result row's array position.
|
|
1203
1225
|
*/
|
|
1204
1226
|
index: number;
|
|
1227
|
+
/** Workflow child key that owns this result when returned from workflow details. */
|
|
1228
|
+
workflowKey?: string;
|
|
1205
1229
|
agent: string;
|
|
1206
1230
|
task: string;
|
|
1207
1231
|
/** Human-readable display name for the child's own session (agent + task
|
|
@@ -1307,6 +1331,8 @@ export interface WaitCompletionChild {
|
|
|
1307
1331
|
sessionFile?: string;
|
|
1308
1332
|
success?: boolean;
|
|
1309
1333
|
outputState?: SubagentOutputState;
|
|
1334
|
+
structuredOutput?: unknown;
|
|
1335
|
+
structuredOutputPath?: string;
|
|
1310
1336
|
error?: string;
|
|
1311
1337
|
model?: string;
|
|
1312
1338
|
contextOverflow?: boolean;
|
|
@@ -1841,6 +1867,10 @@ export interface AsyncStatus {
|
|
|
1841
1867
|
worktreePath?: string;
|
|
1842
1868
|
/** Display-only branch copied at launch; handoff remains authoritative. */
|
|
1843
1869
|
branch?: string;
|
|
1870
|
+
/** Display-only provisioning provider copied at launch; handoff remains authoritative. */
|
|
1871
|
+
provider?: ManagedWorktreeProvider;
|
|
1872
|
+
/** Display-only naming evidence copied at launch; handoff remains authoritative. */
|
|
1873
|
+
naming?: WorktreeNaming;
|
|
1844
1874
|
/** Child run identity for workflow capacity reconciliation. */
|
|
1845
1875
|
runId?: string;
|
|
1846
1876
|
/** True only when this workflow child owns a detached async runner. */
|
|
@@ -2554,6 +2584,12 @@ export interface ExtensionConfig {
|
|
|
2554
2584
|
worktreeSetupHook?: string;
|
|
2555
2585
|
worktreeSetupHookTimeoutMs?: number;
|
|
2556
2586
|
worktreeBaseDir?: string;
|
|
2587
|
+
/** Enable managed worktrees when a launch does not provide an explicit value. */
|
|
2588
|
+
worktree?: boolean;
|
|
2589
|
+
/** Worktree allocator selection. Defaults to auto. */
|
|
2590
|
+
worktreeProvider?: WorktreeProvider;
|
|
2591
|
+
/** Namespace used by managed worktree branches. Defaults to pi-subagents/. */
|
|
2592
|
+
worktreeBranchPrefix?: string;
|
|
2557
2593
|
/** Where to store subagent artifact files. Defaults to "session" (the pi session directory, or OS temp when unavailable). Set to "project" for cwd/.pi/subagents. */
|
|
2558
2594
|
artifactDir?: ArtifactDirPreference;
|
|
2559
2595
|
/** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
|
package/src/shared/utils.ts
CHANGED
|
@@ -237,6 +237,12 @@ export function findLatestSessionFile(sessionDir: string): string | null {
|
|
|
237
237
|
return latest ? latest.path : null;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
const PI_TURN_TIMING_FOOTER = /(?:\r?\n)*\x1b\[38;2;136;136;136m✻ Turn took [^()\r\n]+ \(Total time [^·\r\n]+ · \d+ turns?\)\x1b\[0m[ \t]*$/u;
|
|
241
|
+
|
|
242
|
+
function stripPiTurnTimingFooter(text: string): string {
|
|
243
|
+
return text.replace(PI_TURN_TIMING_FOOTER, "");
|
|
244
|
+
}
|
|
245
|
+
|
|
240
246
|
export function getFinalOutput(messages: Message[]): string {
|
|
241
247
|
const validTextParts: string[] = [];
|
|
242
248
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -246,21 +252,26 @@ export function getFinalOutput(messages: Message[]): string {
|
|
|
246
252
|
|| ("stopReason" in msg && msg.stopReason === "error");
|
|
247
253
|
if (hasAssistantError) continue;
|
|
248
254
|
const messageText = msg.content
|
|
249
|
-
.
|
|
250
|
-
|
|
255
|
+
.flatMap((part) => {
|
|
256
|
+
if (part.type !== "text") return [];
|
|
257
|
+
const text = stripPiTurnTimingFooter(part.text);
|
|
258
|
+
return text.trim().length > 0 ? [text] : [];
|
|
259
|
+
})
|
|
251
260
|
.join("\n");
|
|
252
261
|
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
253
262
|
const part = msg.content[j];
|
|
254
|
-
if (!part || part.type !== "text"
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
263
|
+
if (!part || part.type !== "text") continue;
|
|
264
|
+
const text = stripPiTurnTimingFooter(part.text);
|
|
265
|
+
if (text.trim().length === 0) continue;
|
|
266
|
+
validTextParts.push(text);
|
|
267
|
+
if (/```acceptance[-_]report\s*\n[\s\S]*?```/i.test(text)) return messageText;
|
|
268
|
+
for (const match of text.matchAll(/```(?:json|jsonc|json5)\s*\n([\s\S]*?)```/gi)) {
|
|
258
269
|
const body = match[1] ?? "";
|
|
259
270
|
if (/"(?:criteriaSatisfied|criteria_satisfied)"/.test(body) && /"(?:changedFiles|changed_files|testsAddedOrUpdated|tests_added_or_updated|commandsRun|commands_run|validationOutput|validation_output|residualRisks|residual_risks|noStagedFiles|no_staged_files|diffSummary|diff_summary|reviewFindings|review_findings|manualNotes|manual_notes)"/.test(body)) {
|
|
260
271
|
return messageText;
|
|
261
272
|
}
|
|
262
273
|
}
|
|
263
|
-
if (/ACCEPTANCE_REPORT\s*:/i.test(
|
|
274
|
+
if (/ACCEPTANCE_REPORT\s*:/i.test(text)) return messageText;
|
|
264
275
|
}
|
|
265
276
|
}
|
|
266
277
|
return validTextParts[0] ?? "";
|
|
@@ -2,7 +2,6 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import {
|
|
5
|
-
agentHasFrontmatterField,
|
|
6
5
|
discoverAgentsAll,
|
|
7
6
|
EXTRA_AGENT_DIRS_ENV,
|
|
8
7
|
frontmatterNameForConfig,
|
|
@@ -116,18 +115,15 @@ type AgentSelection =
|
|
|
116
115
|
|
|
117
116
|
function savesThroughSettings(agent: AgentConfig, field: EditableOverrideField): boolean {
|
|
118
117
|
if (agent.source === "builtin") return true;
|
|
119
|
-
if (agent.source === "package")
|
|
120
|
-
if (field === "systemPrompt") return false;
|
|
121
|
-
return !agentHasFrontmatterField(agent, field);
|
|
122
|
-
}
|
|
118
|
+
if (agent.source === "package") return true;
|
|
123
119
|
if (!agent.override) return false;
|
|
120
|
+
if (agent.override.fields?.includes(field) === true) return true;
|
|
124
121
|
// A lower-scope override can flow into a higher-scope custom agent with the
|
|
125
122
|
// same name. Persist that agent's edits in its own frontmatter instead of
|
|
126
123
|
// rewriting the shared lower-scope override used by another agent.
|
|
127
124
|
if (agent.source !== agent.override.scope) return false;
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// not redirect edits to an unrelated frontmatter-owned field.
|
|
125
|
+
// Compare the effective value with the pre-override base so an override on one
|
|
126
|
+
// field does not redirect edits to an unrelated frontmatter-owned field.
|
|
131
127
|
return agent[field] !== agent.override.base[field];
|
|
132
128
|
}
|
|
133
129
|
|
|
@@ -256,10 +252,12 @@ async function chooseThinking(ctx: ExtensionContext, agent: AgentConfig): Promis
|
|
|
256
252
|
}
|
|
257
253
|
|
|
258
254
|
async function chooseOverrideScope(ctx: ExtensionContext, agent: AgentConfig): Promise<"user" | "project" | undefined> {
|
|
259
|
-
if (agent.override?.scope) return agent.override.scope;
|
|
260
255
|
const d = discoverAgentsAll(ctx.cwd);
|
|
256
|
+
if (agent.override?.scope) {
|
|
257
|
+
return agent.source === "project" && agent.override.scope === "user" && d.projectSettingsPath ? "project" : agent.override.scope;
|
|
258
|
+
}
|
|
261
259
|
if (!d.projectSettingsPath || !ctx.hasUI) return "user";
|
|
262
|
-
const choice = await ctx.ui.select(`Save
|
|
260
|
+
const choice = await ctx.ui.select(`Save subagent override for ${agent.name}`, ["user", "project"]);
|
|
263
261
|
return choice === "user" || choice === "project" ? choice : undefined;
|
|
264
262
|
}
|
|
265
263
|
|
|
@@ -271,6 +269,18 @@ function persistSettingsField(
|
|
|
271
269
|
value: string | undefined,
|
|
272
270
|
): { filePath: string; overridden: boolean } {
|
|
273
271
|
const base = agent.override?.base ?? buildBuiltinBase(agent);
|
|
272
|
+
const shadowsLowerScope = scope === "project" && agent.override?.fieldScopes?.[field]?.includes("user") === true;
|
|
273
|
+
if (shadowsLowerScope && (value === undefined || value === base[field])) {
|
|
274
|
+
const filePath = field === "model"
|
|
275
|
+
? mergeBuiltinAgentOverride(ctx.cwd, agent.name, scope, { model: value ?? base.model ?? false })
|
|
276
|
+
: field === "thinking"
|
|
277
|
+
? mergeBuiltinAgentOverride(ctx.cwd, agent.name, scope, { thinking: value ?? (base.thinking === false ? "off" : base.thinking) ?? false })
|
|
278
|
+
: mergeBuiltinAgentOverride(ctx.cwd, agent.name, scope, { systemPrompt: value ?? base.systemPrompt });
|
|
279
|
+
return {
|
|
280
|
+
filePath,
|
|
281
|
+
overridden: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
274
284
|
if (value === undefined || value === base[field]) {
|
|
275
285
|
return {
|
|
276
286
|
filePath: removeBuiltinAgentOverrideFields(ctx.cwd, agent.name, scope, [field]).path,
|
|
@@ -358,13 +368,15 @@ async function saveAgentSystemPrompt(ctx: ExtensionContext, agent: AgentConfig,
|
|
|
358
368
|
}
|
|
359
369
|
|
|
360
370
|
async function editSystemPrompt(ctx: ExtensionContext, agent: AgentConfig): Promise<string | null> {
|
|
361
|
-
|
|
371
|
+
const saveThroughSettings = savesThroughSettings(agent, "systemPrompt");
|
|
372
|
+
if (!saveThroughSettings) {
|
|
362
373
|
const readOnlyMessage = readOnlyAgentMessage(agent, "systemPrompt");
|
|
363
374
|
if (readOnlyMessage) return readOnlyMessage;
|
|
364
375
|
}
|
|
365
376
|
const edited = await ctx.ui.editor(`Edit '${agent.name}' system prompt`, agent.systemPrompt ?? "");
|
|
366
377
|
if (edited === undefined) return null;
|
|
367
|
-
|
|
378
|
+
const projectOwnsLowerScopeOverride = agent.source === "project" && agent.override?.scope === "user" && discoverAgentsAll(ctx.cwd).projectSettingsPath;
|
|
379
|
+
if (edited.replace(/\s+$/, "") === (agent.systemPrompt ?? "").replace(/\s+$/, "") && !(saveThroughSettings && projectOwnsLowerScopeOverride)) {
|
|
368
380
|
return `System prompt for '${agent.name}' left unchanged.`;
|
|
369
381
|
}
|
|
370
382
|
return saveAgentSystemPrompt(ctx, agent, edited);
|
package/src/tui/fleet-status.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { contextModeLabel } from "../runs/shared/context-mode.ts";
|
|
|
8
8
|
import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
|
|
9
9
|
import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
|
|
10
10
|
import { isStaleExtensionContextError } from "../shared/extension-context.ts";
|
|
11
|
+
import { formatWorkflowChecklistBottleneck, formatWorkflowChecklistPhase, formatWorkflowChecklistSummary, projectWorkflowChecklist, type WorkflowChecklistPhase, type WorkflowChecklistProjection } from "../workflows/workflow-checklist.ts";
|
|
11
12
|
|
|
12
13
|
export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
|
|
13
14
|
|
|
@@ -35,6 +36,7 @@ type FleetStatusEntry = {
|
|
|
35
36
|
projectPane?: HerdrProjectPaneSnapshot;
|
|
36
37
|
nestedChildren?: NestedRunSummary[];
|
|
37
38
|
workflowRows?: AsyncStatusWorkflowRow[];
|
|
39
|
+
workflowChecklist?: WorkflowChecklistProjection;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
type FleetNestedRow = {
|
|
@@ -50,6 +52,7 @@ type FleetNestedRow = {
|
|
|
50
52
|
type FleetTreeRow =
|
|
51
53
|
| { kind: "owner"; entry: FleetStatusEntry }
|
|
52
54
|
| { kind: "child"; entry: FleetStatusEntry; last: boolean }
|
|
55
|
+
| { kind: "workflow-phase"; ownerKey: string; phase: WorkflowChecklistPhase; last: boolean }
|
|
53
56
|
| { kind: "workflow"; ownerKey: string; row: AsyncStatusWorkflowRow; last: boolean }
|
|
54
57
|
| { kind: "nested"; ownerKey: string; row: FleetNestedRow; last: boolean };
|
|
55
58
|
|
|
@@ -117,6 +120,18 @@ function visibleWorkflowRows(rows: AsyncStatusWorkflowRow[] | undefined, visible
|
|
|
117
120
|
return [{ name: `… +${rows.length - visible.length} hidden workflow steps`, state: "complete", overflow: rows.length - visible.length }, ...visible];
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
function visibleWorkflowPhases(checklist: WorkflowChecklistProjection | undefined, visibleLimit: number): WorkflowChecklistPhase[] {
|
|
124
|
+
const phases = checklist?.phases ?? [];
|
|
125
|
+
if (phases.length <= visibleLimit) return phases;
|
|
126
|
+
const selected = new Set<number>();
|
|
127
|
+
for (const [index, phase] of phases.entries()) {
|
|
128
|
+
if (phase.state !== "complete") selected.add(index);
|
|
129
|
+
if (selected.size >= visibleLimit) break;
|
|
130
|
+
}
|
|
131
|
+
for (let index = phases.length - 1; index >= 0 && selected.size < visibleLimit; index--) selected.add(index);
|
|
132
|
+
return [...selected].sort((left, right) => left - right).map((index) => phases[index]!);
|
|
133
|
+
}
|
|
134
|
+
|
|
120
135
|
function isWorkflowRowTerminal(row: AsyncStatusWorkflowRow): boolean {
|
|
121
136
|
if (row.kind) return row.state === "done" || row.state === "cancelled" || row.state === "error";
|
|
122
137
|
return row.state === "complete" || row.state === "completed";
|
|
@@ -224,10 +239,11 @@ function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
|
|
|
224
239
|
if (entry.parentKey && entryKeys.has(entry.parentKey)) continue;
|
|
225
240
|
rows.push({ kind: "owner", entry });
|
|
226
241
|
const attached = childrenByParent.get(entry.key) ?? [];
|
|
242
|
+
const workflowPhases = visibleWorkflowPhases(entry.workflowChecklist, attached.length > 0 ? 2 : 4);
|
|
227
243
|
const workflowRows = visibleWorkflowRows(entry.workflowRows, attached.length > 0 ? 2 : 4);
|
|
228
244
|
for (const [index, child] of attached.entries()) {
|
|
229
245
|
const nested = nestedFleetRows(child.nestedChildren, 3);
|
|
230
|
-
const laterRows = index < attached.length - 1 || workflowRows.length > 0 || Boolean(entry.nestedChildren?.length);
|
|
246
|
+
const laterRows = index < attached.length - 1 || workflowPhases.length > 0 || workflowRows.length > 0 || Boolean(entry.nestedChildren?.length);
|
|
231
247
|
rows.push({ kind: "child", entry: child, last: !laterRows && nested.length === 0 });
|
|
232
248
|
for (const [nestedIndex, row] of nested.entries()) rows.push({
|
|
233
249
|
kind: "nested",
|
|
@@ -236,6 +252,12 @@ function fleetTreeRows(entries: FleetStatusEntry[]): FleetTreeRow[] {
|
|
|
236
252
|
last: nestedIndex === nested.length - 1 && !laterRows,
|
|
237
253
|
});
|
|
238
254
|
}
|
|
255
|
+
for (const [index, phase] of workflowPhases.entries()) rows.push({
|
|
256
|
+
kind: "workflow-phase",
|
|
257
|
+
ownerKey: entry.key,
|
|
258
|
+
phase,
|
|
259
|
+
last: index === workflowPhases.length - 1 && workflowRows.length === 0 && !entry.nestedChildren?.length,
|
|
260
|
+
});
|
|
239
261
|
for (const [index, row] of workflowRows.entries()) rows.push({ kind: "workflow", ownerKey: entry.key, row, last: index === workflowRows.length - 1 && !entry.nestedChildren?.length });
|
|
240
262
|
const nested = nestedFleetRows(entry.nestedChildren, attached.length > 0 ? 3 : 4);
|
|
241
263
|
for (const [index, row] of nested.entries()) rows.push({ kind: "nested", ownerKey: entry.key, row, last: index === nested.length - 1 });
|
|
@@ -364,6 +386,14 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
364
386
|
const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
|
|
365
387
|
const workflowSteps = workflowStepsWithoutMaterializedChildren(job.steps, materializedChildrenByWorkflow.get(`async:${job.asyncId}`));
|
|
366
388
|
const workflowRows = projectAsyncWorkflowRows(workflowSteps, job.workflowGraph ?? job.hostSteps, job.preflight);
|
|
389
|
+
const workflowChecklist = projectWorkflowChecklist({
|
|
390
|
+
graph: job.workflowGraph,
|
|
391
|
+
steps: job.steps,
|
|
392
|
+
hostSteps: job.hostSteps,
|
|
393
|
+
preflight: job.preflight,
|
|
394
|
+
trace: job.workflow?.trace,
|
|
395
|
+
now: job.updatedAt ?? Date.now(),
|
|
396
|
+
});
|
|
367
397
|
entries.push({
|
|
368
398
|
key: `async:${job.asyncId}`,
|
|
369
399
|
...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
|
|
@@ -375,6 +405,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
375
405
|
...(job.totalTokens?.window !== undefined ? { window: job.totalTokens.window } : {}),
|
|
376
406
|
state: job.status,
|
|
377
407
|
...(workflowRows.length ? { workflowRows } : {}),
|
|
408
|
+
...(workflowChecklist.total ? { workflowChecklist } : {}),
|
|
378
409
|
...(job.nestedChildren?.length ? { nestedChildren: job.nestedChildren } : {}),
|
|
379
410
|
});
|
|
380
411
|
continue;
|
|
@@ -668,6 +699,8 @@ export class SubagentFleetStatus {
|
|
|
668
699
|
lines.push(this.renderEntry(rosterIndex, selectedIndex, row.entry, width, theme, row.kind === "child" ? (row.last ? "└─" : "├─") : undefined));
|
|
669
700
|
} else if (row.kind === "workflow") {
|
|
670
701
|
lines.push(this.renderWorkflowRow(row.row, row.last, width, theme));
|
|
702
|
+
} else if (row.kind === "workflow-phase") {
|
|
703
|
+
lines.push(this.renderWorkflowPhaseRow(row.phase, row.last, width, theme));
|
|
671
704
|
} else {
|
|
672
705
|
lines.push(this.renderNestedRow(row.row, row.last, width, theme));
|
|
673
706
|
}
|
|
@@ -691,7 +724,10 @@ export class SubagentFleetStatus {
|
|
|
691
724
|
private renderEntry(rosterIndex: number, selectedIndex: number, entry: FleetStatusEntry, width: number, theme: Theme, branch?: string): string {
|
|
692
725
|
const agent = entry.modelThinking ? `${entry.agent} (${entry.modelThinking})` : entry.agent;
|
|
693
726
|
const prefix = branch ? ` ${branch}` : " ";
|
|
694
|
-
const
|
|
727
|
+
const checklist = entry.workflowWrapper && entry.workflowChecklist
|
|
728
|
+
? ` · checklist ${formatWorkflowChecklistSummary(entry.workflowChecklist)}${entry.workflowChecklist.bottleneck ? ` · bottleneck ${formatWorkflowChecklistBottleneck(entry.workflowChecklist.bottleneck)}` : ""}`
|
|
729
|
+
: "";
|
|
730
|
+
const left = `${prefix} ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}${checklist}`;
|
|
695
731
|
const elapsed = Date.now() - entry.startedAt;
|
|
696
732
|
const rightText = entry.projectPane
|
|
697
733
|
? `${entry.projectPane.summary ?? "—"} · ${formatFleetElapsed(Date.now() - entry.projectPane.refreshedAt)} ago`
|
|
@@ -730,6 +766,20 @@ export class SubagentFleetStatus {
|
|
|
730
766
|
return theme.fg("warning", state);
|
|
731
767
|
}
|
|
732
768
|
|
|
769
|
+
private renderWorkflowPhaseRow(phase: WorkflowChecklistPhase, last: boolean, width: number, theme: Theme): string {
|
|
770
|
+
const marker = last ? "└─" : "├─";
|
|
771
|
+
const glyph = phase.state === "complete"
|
|
772
|
+
? theme.fg("success", "✓")
|
|
773
|
+
: phase.state === "running"
|
|
774
|
+
? theme.fg("accent", "●")
|
|
775
|
+
: phase.state === "blocked" || phase.state === "failed"
|
|
776
|
+
? theme.fg("error", phase.state === "blocked" ? "!" : "✗")
|
|
777
|
+
: phase.state === "queued"
|
|
778
|
+
? theme.fg("muted", "◦")
|
|
779
|
+
: theme.fg("warning", "■");
|
|
780
|
+
return truncateToWidth(` ${marker} ${glyph} ${theme.fg("muted", formatWorkflowChecklistPhase(phase))}`, width);
|
|
781
|
+
}
|
|
782
|
+
|
|
733
783
|
private renderWorkflowRow(row: AsyncStatusWorkflowRow, last: boolean, width: number, theme: Theme): string {
|
|
734
784
|
const marker = last ? "└─" : "├─";
|
|
735
785
|
const indent = " ";
|
|
@@ -809,6 +859,15 @@ export class SubagentFleetStatus {
|
|
|
809
859
|
entry.external,
|
|
810
860
|
Math.round((now - entry.startedAt) / 1000),
|
|
811
861
|
entry.tokens,
|
|
862
|
+
entry.workflowChecklist ? [
|
|
863
|
+
entry.workflowChecklist.total,
|
|
864
|
+
entry.workflowChecklist.done,
|
|
865
|
+
entry.workflowChecklist.running,
|
|
866
|
+
entry.workflowChecklist.queued,
|
|
867
|
+
entry.workflowChecklist.blocked,
|
|
868
|
+
entry.workflowChecklist.failed,
|
|
869
|
+
entry.workflowChecklist.phases.map((phase) => [phase.key, phase.state, phase.done, phase.total, phase.running, phase.queued, phase.blocked, phase.failed, phase.items.map((item) => [item.key, item.state, item.currentTool, item.currentPath, item.durationMs, item.toolCount, item.error])]),
|
|
870
|
+
] : undefined,
|
|
812
871
|
visibleWorkflowRows(entry.workflowRows, entry.parentKey ? 2 : 4).map((row) => [
|
|
813
872
|
row.kind,
|
|
814
873
|
row.name,
|
package/src/tui/fleet.ts
CHANGED
|
@@ -42,7 +42,7 @@ export const DEFAULT_FLEET_KEYBINDINGS: Record<FleetKeybindingAction, string[]>
|
|
|
42
42
|
pageDown: ["pageDown"],
|
|
43
43
|
refresh: ["r", "R"],
|
|
44
44
|
steer: ["s"],
|
|
45
|
-
inspect: ["H"],
|
|
45
|
+
inspect: ["return", "H"],
|
|
46
46
|
stop: ["D"],
|
|
47
47
|
toggleTools: ["x", "X", "ctrl+o"],
|
|
48
48
|
};
|
|
@@ -952,6 +952,12 @@ export class SubagentFleetComponent implements Component {
|
|
|
952
952
|
return { runId: parent.asyncId, asyncDir: parent.asyncDir };
|
|
953
953
|
}
|
|
954
954
|
|
|
955
|
+
private inspectSelectedHerdr(): void {
|
|
956
|
+
const target = this.selectedHerdrInspectAction();
|
|
957
|
+
if ("reason" in target || !this.options.actions?.inspect) this.setActionNotice({ text: "reason" in target ? target.reason : "Herdr inspector controls are unavailable in this context.", isError: true });
|
|
958
|
+
else this.runAction(() => this.options.actions!.inspect!(target));
|
|
959
|
+
}
|
|
960
|
+
|
|
955
961
|
private actionLines(): string[] {
|
|
956
962
|
const lines: string[] = [];
|
|
957
963
|
if (this.actionBusy) lines.push(this.theme.fg("accent", "Action pending..."));
|
|
@@ -1179,12 +1185,6 @@ export class SubagentFleetComponent implements Component {
|
|
|
1179
1185
|
}
|
|
1180
1186
|
return;
|
|
1181
1187
|
}
|
|
1182
|
-
if (matchesFleetAction(data, this.keybindings, "inspect")) {
|
|
1183
|
-
const target = this.selectedHerdrInspectAction();
|
|
1184
|
-
if ("reason" in target || !this.options.actions?.inspect) this.setActionNotice({ text: "reason" in target ? target.reason : "Herdr inspector controls are unavailable in this context.", isError: true });
|
|
1185
|
-
else this.runAction(() => this.options.actions!.inspect!(target));
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
1188
|
if (matchesFleetAction(data, this.keybindings, "stop")) {
|
|
1189
1189
|
const target = this.selectedAsyncAction();
|
|
1190
1190
|
if ("reason" in target || !this.options.actions) this.setActionNotice({ text: "reason" in target ? target.reason : "Fleet controls are unavailable in this context.", isError: true });
|
|
@@ -1201,6 +1201,11 @@ export class SubagentFleetComponent implements Component {
|
|
|
1201
1201
|
this.expandedTools = !this.expandedTools;
|
|
1202
1202
|
this.transcriptCache = undefined;
|
|
1203
1203
|
this.tui.requestRender();
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
if (matchesFleetAction(data, this.keybindings, "inspect")) {
|
|
1207
|
+
this.inspectSelectedHerdr();
|
|
1208
|
+
return;
|
|
1204
1209
|
}
|
|
1205
1210
|
}
|
|
1206
1211
|
|