pi-subagents 0.61.0 → 0.63.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 +11 -6
- package/docs/configuration.md +31 -5
- package/docs/extension-api.md +2 -2
- package/docs/models.md +5 -5
- package/docs/observability.md +5 -2
- package/docs/tool-reference.md +3 -3
- package/install.mjs +0 -1
- package/package.json +1 -1
- package/skills/pi-subagents/references/execution-controls.md +3 -4
- 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 +41 -4
- package/src/agents/agent-serializer.ts +3 -0
- package/src/agents/agents.ts +120 -124
- package/src/agents/runtime-agent-registry.ts +5 -1
- package/src/api/preflight.ts +4 -0
- package/src/api/shared-types.ts +3 -0
- package/src/extension/config.ts +20 -0
- package/src/extension/public-execution.ts +1 -0
- package/src/extension/schemas.ts +6 -2
- package/src/extension/tool-description.ts +1 -1
- package/src/inspectors/herdr/inspector-runner.ts +19 -13
- package/src/runs/background/active-async-capacity.ts +26 -8
- package/src/runs/background/async-execution.ts +100 -23
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-status.ts +18 -2
- package/src/runs/background/notify.ts +13 -1
- package/src/runs/background/process-terminal.ts +16 -0
- package/src/runs/background/run-status.ts +22 -2
- package/src/runs/background/scheduled-runs.ts +63 -6
- package/src/runs/background/steering.ts +4 -1
- package/src/runs/background/subagent-runner.ts +44 -9
- package/src/runs/background/wait-completions.ts +13 -0
- package/src/runs/background/wait-tool.ts +1 -7
- package/src/runs/foreground/execution.ts +14 -7
- package/src/runs/foreground/subagent-executor.ts +38 -5
- package/src/runs/shared/acceptance.ts +85 -18
- package/src/runs/shared/capability-ceiling.ts +1 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/lane-metadata.ts +24 -3
- package/src/runs/shared/parallel-handoff.ts +4 -0
- package/src/runs/shared/parallel-utils.ts +2 -6
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +32 -14
- package/src/runs/shared/pi-spawn.ts +69 -35
- package/src/runs/shared/structured-output.ts +33 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
- package/src/runs/shared/task-intent.ts +21 -7
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/worktree.ts +467 -63
- package/src/shared/atomic-json.ts +3 -1
- package/src/shared/fork-context.ts +0 -12
- package/src/shared/fork-session-cwd.ts +27 -0
- package/src/shared/launch-contract.ts +3 -0
- package/src/shared/types.ts +32 -1
- package/src/shared/utils.ts +18 -7
- package/src/slash/slash-commands.ts +1 -1
- package/src/slash/subagents-admin.ts +26 -12
- package/src/tui/fleet-status.ts +61 -2
- package/src/tui/fleet.ts +12 -7
- package/src/tui/render.ts +222 -14
- package/src/workflows/workflow-checklist.ts +441 -0
|
@@ -15,6 +15,7 @@ type AtomicJsonWriterOptions = {
|
|
|
15
15
|
mode?: number;
|
|
16
16
|
retryRenameErrors?: boolean;
|
|
17
17
|
retryDirectoryErrors?: boolean;
|
|
18
|
+
ignoreCleanupErrorAfterSuccess?: boolean;
|
|
18
19
|
retryDelaysMs?: readonly number[];
|
|
19
20
|
wait?: (delayMs: number) => void;
|
|
20
21
|
};
|
|
@@ -46,6 +47,7 @@ export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (
|
|
|
46
47
|
const mode = options.mode;
|
|
47
48
|
const retryRenameErrors = options.retryRenameErrors ?? process.platform === "win32";
|
|
48
49
|
const retryDirectoryErrors = options.retryDirectoryErrors ?? retryRenameErrors;
|
|
50
|
+
const ignoreCleanupErrorAfterSuccess = options.ignoreCleanupErrorAfterSuccess ?? false;
|
|
49
51
|
const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS;
|
|
50
52
|
const renameRetryDelaysMs = retryRenameErrors ? retryDelaysMs : [];
|
|
51
53
|
const directoryRetryDelaysMs = retryDirectoryErrors ? retryDelaysMs : [];
|
|
@@ -71,7 +73,7 @@ export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (
|
|
|
71
73
|
} catch (cleanupError) {
|
|
72
74
|
// Preserve the write/rename failure: cleanup is best effort and must
|
|
73
75
|
// not hide the error callers need to classify or report.
|
|
74
|
-
if (writeError === undefined) throw cleanupError;
|
|
76
|
+
if (writeError === undefined && !ignoreCleanupErrorAfterSuccess) throw cleanupError;
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
};
|
|
@@ -174,18 +174,6 @@ function readSessionEntries(sessionFile: string): BranchSessionEntry[] {
|
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
/** Keep Pi from restoring a forked session into the parent's cwd instead of the child launch cwd. */
|
|
178
|
-
export function alignForkedSessionCwd(sessionFile: string, cwd: string): void {
|
|
179
|
-
const entries = readSessionEntries(sessionFile);
|
|
180
|
-
const header = entries[0];
|
|
181
|
-
if (header?.type !== "session") throw new Error(`Forked session ${sessionFile} does not start with a session header.`);
|
|
182
|
-
const resolvedCwd = path.resolve(cwd);
|
|
183
|
-
const effectiveCwd = fs.realpathSync.native(resolvedCwd);
|
|
184
|
-
if (header.cwd === effectiveCwd) return;
|
|
185
|
-
header.cwd = effectiveCwd;
|
|
186
|
-
fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
|
|
187
|
-
}
|
|
188
|
-
|
|
189
177
|
export function createForkContextResolver(
|
|
190
178
|
sessionManager: ForkableSessionManager,
|
|
191
179
|
requestedContext: unknown,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
interface SessionEntry {
|
|
5
|
+
type?: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Keep Pi from restoring a forked session into the parent's cwd instead of the child launch cwd. */
|
|
11
|
+
export function alignForkedSessionCwd(sessionFile: string, cwd: string): void {
|
|
12
|
+
const lines = fs.readFileSync(sessionFile, "utf-8").split("\n").filter((line) => line.trim().length > 0);
|
|
13
|
+
const entries = lines.map((line, index) => {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(line) as SessionEntry;
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
18
|
+
throw new Error(`Unable to inspect forked session ${sessionFile}: invalid JSONL on line ${index + 1}: ${cause.message}`, { cause });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const header = entries[0];
|
|
22
|
+
if (header?.type !== "session") throw new Error(`Forked session ${sessionFile} does not start with a session header.`);
|
|
23
|
+
const effectiveCwd = fs.realpathSync.native(path.resolve(cwd));
|
|
24
|
+
if (header.cwd === effectiveCwd) return;
|
|
25
|
+
header.cwd = effectiveCwd;
|
|
26
|
+
fs.writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, "utf-8");
|
|
27
|
+
}
|
|
@@ -51,6 +51,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
|
|
|
51
51
|
fast: agent.fast,
|
|
52
52
|
thinking: agent.thinking,
|
|
53
53
|
tools: agent.tools,
|
|
54
|
+
excludeTools: agent.excludeTools,
|
|
54
55
|
allowNestedSubagents: agent.allowNestedSubagents,
|
|
55
56
|
mcpDirectTools: agent.mcpDirectTools,
|
|
56
57
|
extensions: agent.extensions,
|
|
@@ -93,6 +94,7 @@ export interface LaunchBindingInput {
|
|
|
93
94
|
inheritSkills: boolean;
|
|
94
95
|
skills?: string[];
|
|
95
96
|
tools?: string[];
|
|
97
|
+
excludeTools?: string[];
|
|
96
98
|
extensions?: string[];
|
|
97
99
|
subagentOnlyExtensions?: string[];
|
|
98
100
|
mcpDirectTools?: string[];
|
|
@@ -120,6 +122,7 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
|
|
|
120
122
|
inheritSkills: input.inheritSkills,
|
|
121
123
|
skills: input.skills,
|
|
122
124
|
tools: input.tools,
|
|
125
|
+
excludeTools: input.excludeTools,
|
|
123
126
|
extensions: input.extensions,
|
|
124
127
|
subagentOnlyExtensions: input.subagentOnlyExtensions,
|
|
125
128
|
mcpDirectTools: input.mcpDirectTools,
|
package/src/shared/types.ts
CHANGED
|
@@ -382,6 +382,17 @@ export type SubagentResultStatus = "completed" | "failed" | "paused" | "stopped"
|
|
|
382
382
|
export type SubagentOutputState = "present" | "absent" | "unknown";
|
|
383
383
|
export type SubagentRunMode = "single" | "parallel" | "chain" | "workflow";
|
|
384
384
|
export type SubagentResultMode = SubagentRunMode;
|
|
385
|
+
export type WorktreeProvider = "auto" | "native" | "worktrunk";
|
|
386
|
+
export type ManagedWorktreeProvider = Exclude<WorktreeProvider, "auto">;
|
|
387
|
+
|
|
388
|
+
export interface WorktreeNaming {
|
|
389
|
+
requestedBranch: string;
|
|
390
|
+
branchPrefix: string;
|
|
391
|
+
label: string;
|
|
392
|
+
sanitizedPathComponent: string;
|
|
393
|
+
collision?: "branch" | "path" | "both";
|
|
394
|
+
collisionSuffix?: string;
|
|
395
|
+
}
|
|
385
396
|
|
|
386
397
|
export interface ParallelHandoffPatch {
|
|
387
398
|
path: string;
|
|
@@ -425,6 +436,10 @@ export interface ParallelHandoffCleanupTask {
|
|
|
425
436
|
index: number;
|
|
426
437
|
path: string;
|
|
427
438
|
branch: string;
|
|
439
|
+
/** Provider that allocated this worktree; omitted in old manifests. */
|
|
440
|
+
provider?: ManagedWorktreeProvider;
|
|
441
|
+
/** Branch/path naming evidence retained with the cleanup authority. */
|
|
442
|
+
naming?: WorktreeNaming;
|
|
428
443
|
worktreeRemoved: boolean;
|
|
429
444
|
branchRemoved: boolean;
|
|
430
445
|
preserved?: boolean;
|
|
@@ -780,6 +795,7 @@ export interface SteeringRecoveryDescriptor {
|
|
|
780
795
|
thinking?: string;
|
|
781
796
|
thinkingCeiling?: ThinkingLevel;
|
|
782
797
|
tools?: string[];
|
|
798
|
+
excludeTools?: string[];
|
|
783
799
|
allowNestedSubagents?: boolean;
|
|
784
800
|
extensions?: string[];
|
|
785
801
|
subagentOnlyExtensions?: string[];
|
|
@@ -994,6 +1010,7 @@ export interface AcceptanceReviewGate {
|
|
|
994
1010
|
|
|
995
1011
|
export interface AcceptanceConfig {
|
|
996
1012
|
level?: AcceptanceLevel;
|
|
1013
|
+
report?: "on" | "off";
|
|
997
1014
|
criteria?: Array<string | AcceptanceGate>;
|
|
998
1015
|
evidence?: AcceptanceEvidenceKind[];
|
|
999
1016
|
verify?: AcceptanceVerifyCommand[];
|
|
@@ -1200,6 +1217,8 @@ export interface SingleResult {
|
|
|
1200
1217
|
* result row's array position.
|
|
1201
1218
|
*/
|
|
1202
1219
|
index: number;
|
|
1220
|
+
/** Workflow child key that owns this result when returned from workflow details. */
|
|
1221
|
+
workflowKey?: string;
|
|
1203
1222
|
agent: string;
|
|
1204
1223
|
task: string;
|
|
1205
1224
|
/** Human-readable display name for the child's own session (agent + task
|
|
@@ -1305,6 +1324,8 @@ export interface WaitCompletionChild {
|
|
|
1305
1324
|
sessionFile?: string;
|
|
1306
1325
|
success?: boolean;
|
|
1307
1326
|
outputState?: SubagentOutputState;
|
|
1327
|
+
structuredOutput?: unknown;
|
|
1328
|
+
structuredOutputPath?: string;
|
|
1308
1329
|
error?: string;
|
|
1309
1330
|
model?: string;
|
|
1310
1331
|
contextOverflow?: boolean;
|
|
@@ -1342,7 +1363,7 @@ export interface AgentCapabilityRow {
|
|
|
1342
1363
|
restrictionSources?: string[];
|
|
1343
1364
|
aliases?: string[];
|
|
1344
1365
|
runner: { type: "pi" } | { type: "external-cli"; adapter?: string; capabilities: ExternalCliCapabilities } | { type: "external-job"; provider: string; available?: boolean; capabilities: ExternalJobRunnerStatus["capabilities"] };
|
|
1345
|
-
tools: { ambient: boolean; names: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
|
|
1366
|
+
tools: { ambient: boolean; names: string[]; excludeTools?: string[]; mcpDirectTools: string[]; mutationTools?: string[] };
|
|
1346
1367
|
model?: { value?: string; fallbackModels?: string[]; thinking?: string | false };
|
|
1347
1368
|
execution?: { defaultAsync?: boolean; timeoutMs?: number };
|
|
1348
1369
|
output?: { path?: string; mode?: OutputMode };
|
|
@@ -1839,6 +1860,10 @@ export interface AsyncStatus {
|
|
|
1839
1860
|
worktreePath?: string;
|
|
1840
1861
|
/** Display-only branch copied at launch; handoff remains authoritative. */
|
|
1841
1862
|
branch?: string;
|
|
1863
|
+
/** Display-only provisioning provider copied at launch; handoff remains authoritative. */
|
|
1864
|
+
provider?: ManagedWorktreeProvider;
|
|
1865
|
+
/** Display-only naming evidence copied at launch; handoff remains authoritative. */
|
|
1866
|
+
naming?: WorktreeNaming;
|
|
1842
1867
|
/** Child run identity for workflow capacity reconciliation. */
|
|
1843
1868
|
runId?: string;
|
|
1844
1869
|
/** True only when this workflow child owns a detached async runner. */
|
|
@@ -2552,6 +2577,12 @@ export interface ExtensionConfig {
|
|
|
2552
2577
|
worktreeSetupHook?: string;
|
|
2553
2578
|
worktreeSetupHookTimeoutMs?: number;
|
|
2554
2579
|
worktreeBaseDir?: string;
|
|
2580
|
+
/** Enable managed worktrees when a launch does not provide an explicit value. */
|
|
2581
|
+
worktree?: boolean;
|
|
2582
|
+
/** Worktree allocator selection. Defaults to auto. */
|
|
2583
|
+
worktreeProvider?: WorktreeProvider;
|
|
2584
|
+
/** Namespace used by managed worktree branches. Defaults to pi-subagents/. */
|
|
2585
|
+
worktreeBranchPrefix?: string;
|
|
2555
2586
|
/** 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. */
|
|
2556
2587
|
artifactDir?: ArtifactDirPreference;
|
|
2557
2588
|
/** 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] ?? "";
|
|
@@ -417,7 +417,7 @@ function detailsFromSessionEntry(entry: unknown): Details | undefined {
|
|
|
417
417
|
}
|
|
418
418
|
if (record.type !== "message" || !record.message || typeof record.message !== "object") return undefined;
|
|
419
419
|
const message = record.message as { role?: unknown; toolName?: unknown; details?: unknown };
|
|
420
|
-
if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_wait"
|
|
420
|
+
if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_wait")) return undefined;
|
|
421
421
|
return isSubagentDetails(message.details) ? message.details : undefined;
|
|
422
422
|
}
|
|
423
423
|
|
|
@@ -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,
|
|
@@ -97,6 +96,7 @@ function buildBuiltinBase(agent: AgentConfig): BuiltinAgentOverrideBase {
|
|
|
97
96
|
systemPrompt: agent.systemPrompt,
|
|
98
97
|
...(agent.skills !== undefined ? { skills: [...agent.skills] } : {}),
|
|
99
98
|
...(agent.tools !== undefined ? { tools: [...agent.tools] } : {}),
|
|
99
|
+
...(agent.excludeTools !== undefined ? { excludeTools: [...agent.excludeTools] } : {}),
|
|
100
100
|
...(agent.mcpDirectTools !== undefined ? { mcpDirectTools: [...agent.mcpDirectTools] } : {}),
|
|
101
101
|
...(agent.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...agent.subagentOnlyExtensions] } : {}),
|
|
102
102
|
...(agent.mutationTools !== undefined ? { mutationTools: [...agent.mutationTools] } : {}),
|
|
@@ -115,18 +115,15 @@ type AgentSelection =
|
|
|
115
115
|
|
|
116
116
|
function savesThroughSettings(agent: AgentConfig, field: EditableOverrideField): boolean {
|
|
117
117
|
if (agent.source === "builtin") return true;
|
|
118
|
-
if (agent.source === "package")
|
|
119
|
-
if (field === "systemPrompt") return false;
|
|
120
|
-
return !agentHasFrontmatterField(agent, field);
|
|
121
|
-
}
|
|
118
|
+
if (agent.source === "package") return true;
|
|
122
119
|
if (!agent.override) return false;
|
|
120
|
+
if (agent.override.fields?.includes(field) === true) return true;
|
|
123
121
|
// A lower-scope override can flow into a higher-scope custom agent with the
|
|
124
122
|
// same name. Persist that agent's edits in its own frontmatter instead of
|
|
125
123
|
// rewriting the shared lower-scope override used by another agent.
|
|
126
124
|
if (agent.source !== agent.override.scope) return false;
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// 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.
|
|
130
127
|
return agent[field] !== agent.override.base[field];
|
|
131
128
|
}
|
|
132
129
|
|
|
@@ -188,6 +185,7 @@ function metadataFor(agent: AgentConfig): string {
|
|
|
188
185
|
if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
|
|
189
186
|
if (agent.thinking !== undefined) lines.push(`Thinking: ${agent.thinking === false ? "off" : agent.thinking}`);
|
|
190
187
|
if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
|
|
188
|
+
if (agent.excludeTools?.length) lines.push(`Excluded tools: ${agent.excludeTools.join(", ")}`);
|
|
191
189
|
if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
|
|
192
190
|
lines.push(`System prompt mode: ${agent.systemPromptMode}`);
|
|
193
191
|
lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
|
|
@@ -254,10 +252,12 @@ async function chooseThinking(ctx: ExtensionContext, agent: AgentConfig): Promis
|
|
|
254
252
|
}
|
|
255
253
|
|
|
256
254
|
async function chooseOverrideScope(ctx: ExtensionContext, agent: AgentConfig): Promise<"user" | "project" | undefined> {
|
|
257
|
-
if (agent.override?.scope) return agent.override.scope;
|
|
258
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
|
+
}
|
|
259
259
|
if (!d.projectSettingsPath || !ctx.hasUI) return "user";
|
|
260
|
-
const choice = await ctx.ui.select(`Save
|
|
260
|
+
const choice = await ctx.ui.select(`Save subagent override for ${agent.name}`, ["user", "project"]);
|
|
261
261
|
return choice === "user" || choice === "project" ? choice : undefined;
|
|
262
262
|
}
|
|
263
263
|
|
|
@@ -269,6 +269,18 @@ function persistSettingsField(
|
|
|
269
269
|
value: string | undefined,
|
|
270
270
|
): { filePath: string; overridden: boolean } {
|
|
271
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
|
+
}
|
|
272
284
|
if (value === undefined || value === base[field]) {
|
|
273
285
|
return {
|
|
274
286
|
filePath: removeBuiltinAgentOverrideFields(ctx.cwd, agent.name, scope, [field]).path,
|
|
@@ -356,13 +368,15 @@ async function saveAgentSystemPrompt(ctx: ExtensionContext, agent: AgentConfig,
|
|
|
356
368
|
}
|
|
357
369
|
|
|
358
370
|
async function editSystemPrompt(ctx: ExtensionContext, agent: AgentConfig): Promise<string | null> {
|
|
359
|
-
|
|
371
|
+
const saveThroughSettings = savesThroughSettings(agent, "systemPrompt");
|
|
372
|
+
if (!saveThroughSettings) {
|
|
360
373
|
const readOnlyMessage = readOnlyAgentMessage(agent, "systemPrompt");
|
|
361
374
|
if (readOnlyMessage) return readOnlyMessage;
|
|
362
375
|
}
|
|
363
376
|
const edited = await ctx.ui.editor(`Edit '${agent.name}' system prompt`, agent.systemPrompt ?? "");
|
|
364
377
|
if (edited === undefined) return null;
|
|
365
|
-
|
|
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)) {
|
|
366
380
|
return `System prompt for '${agent.name}' left unchanged.`;
|
|
367
381
|
}
|
|
368
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
|
|