pi-subagents 0.35.0 → 0.36.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 +59 -0
- package/README.md +132 -24
- package/agents/advisor.md +73 -0
- package/package.json +8 -12
- package/skills/pi-subagents/SKILL.md +22 -9
- package/src/agents/agents.ts +22 -5
- package/src/api/delegation.ts +125 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +50 -38
- package/src/extension/rpc.ts +27 -2
- package/src/extension/schemas.ts +22 -2
- package/src/extension/tool-description.ts +2 -2
- package/src/intercom/intercom-bridge.ts +1 -1
- package/src/intercom/native-supervisor-channel.ts +45 -6
- package/src/intercom/result-intercom.ts +7 -0
- package/src/runs/background/async-execution.ts +34 -4
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +27 -5
- package/src/runs/background/async-status.ts +76 -3
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/completion-batcher.ts +6 -4
- package/src/runs/background/completion-dedupe.ts +2 -11
- package/src/runs/background/fleet-view.ts +9 -4
- package/src/runs/background/notify.ts +132 -120
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +3 -1
- package/src/runs/background/subagent-runner.ts +225 -43
- package/src/runs/background/subagent-wait.ts +130 -4
- package/src/runs/background/wait-tool.ts +2 -2
- package/src/runs/foreground/chain-execution.ts +176 -111
- package/src/runs/foreground/execution.ts +90 -36
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +394 -163
- package/src/runs/shared/acceptance.ts +55 -13
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/child-protocol.ts +1 -1
- package/src/runs/shared/completion-guard.ts +36 -5
- package/src/runs/shared/context-mode.ts +44 -0
- package/src/runs/shared/dynamic-fanout.ts +4 -4
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/nested-events.ts +27 -2
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +6 -0
- package/src/runs/shared/pi-args.ts +23 -14
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/structured-output.ts +112 -7
- package/src/runs/shared/subagent-control.ts +4 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +17 -18
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +3 -1
- package/src/runs/shared/tool-budget.ts +11 -5
- package/src/runs/shared/turn-budget.ts +2 -1
- package/src/runs/shared/worktree.ts +63 -14
- package/src/shared/accessible-dir.ts +25 -0
- package/src/shared/artifacts.ts +37 -7
- package/src/shared/atomic-json.ts +14 -42
- package/src/shared/child-transcript.ts +52 -0
- package/src/shared/file-system-retry.ts +47 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +211 -22
- package/src/slash/delegation-adapters.ts +152 -5
- package/src/slash/delegation-json.ts +108 -0
- package/src/slash/delegation-request.ts +182 -36
- package/src/slash/prompt-template-bridge.ts +222 -37
- package/src/slash/selector.ts +147 -0
- package/src/slash/slash-commands.ts +14 -5
- package/src/slash/slash-live-state.ts +2 -2
- package/src/slash/subagents-admin.ts +42 -42
- package/src/tui/fleet-status.ts +362 -0
- package/src/tui/fleet-transcript.ts +472 -0
- package/src/tui/fleet.ts +318 -59
- package/src/tui/render.ts +25 -15
- package/src/watchdog/change-signature.ts +105 -12
- package/src/watchdog/review.ts +7 -2
- package/src/watchdog/runtime.ts +5 -3
- package/src/slash/subagents-editor.ts +0 -86
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
@@ -5,6 +6,7 @@ import { getAgentDir } from "../../shared/utils.ts";
|
|
|
5
6
|
export interface RunEntry {
|
|
6
7
|
agent: string;
|
|
7
8
|
task: string;
|
|
9
|
+
taskHash?: string;
|
|
8
10
|
ts: number;
|
|
9
11
|
status: "ok" | "error";
|
|
10
12
|
duration: number;
|
|
@@ -13,24 +15,103 @@ export interface RunEntry {
|
|
|
13
15
|
|
|
14
16
|
const ROTATE_READ_THRESHOLD = 1200;
|
|
15
17
|
const ROTATE_KEEP = 1000;
|
|
18
|
+
const PRIVATE_DIR_MODE = 0o700;
|
|
19
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
20
|
+
const REDACTED_TASK = "[redacted]";
|
|
16
21
|
|
|
17
22
|
function getHistoryPath(): string {
|
|
18
23
|
return path.join(getAgentDir(), "run-history.jsonl");
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
function hashTask(task: string): string {
|
|
27
|
+
return createHash("sha256").update(task).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hardenHistoryStorage(historyPath: string): void {
|
|
31
|
+
const historyDir = path.dirname(historyPath);
|
|
32
|
+
fs.mkdirSync(historyDir, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
33
|
+
try { fs.chmodSync(historyDir, PRIVATE_DIR_MODE); } catch {}
|
|
34
|
+
try { if (fs.existsSync(historyPath)) fs.chmodSync(historyPath, PRIVATE_FILE_MODE); } catch {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sanitizeHistoryLine(line: string): string | undefined {
|
|
38
|
+
let value: unknown;
|
|
39
|
+
try {
|
|
40
|
+
value = JSON.parse(line);
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (!value || typeof value !== "object") return undefined;
|
|
45
|
+
|
|
46
|
+
const record = value as Record<string, unknown>;
|
|
47
|
+
const task = typeof record.task === "string" ? record.task : "";
|
|
48
|
+
const taskHash = typeof record.taskHash === "string" && record.taskHash
|
|
49
|
+
? record.taskHash
|
|
50
|
+
: task && task !== REDACTED_TASK
|
|
51
|
+
? hashTask(task)
|
|
52
|
+
: undefined;
|
|
53
|
+
|
|
54
|
+
return JSON.stringify({
|
|
55
|
+
...record,
|
|
56
|
+
task: REDACTED_TASK,
|
|
57
|
+
...(taskHash ? { taskHash } : {}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sanitizeHistoryLines(raw: string): { lines: string[]; changed: boolean } {
|
|
62
|
+
const lines: string[] = [];
|
|
63
|
+
let changed = false;
|
|
64
|
+
for (const line of raw.split("\n")) {
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
if (!trimmed) continue;
|
|
67
|
+
const sanitized = sanitizeHistoryLine(trimmed);
|
|
68
|
+
if (!sanitized) {
|
|
69
|
+
changed = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (sanitized !== trimmed) changed = true;
|
|
73
|
+
lines.push(sanitized);
|
|
74
|
+
}
|
|
75
|
+
return { lines, changed };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writePrivateHistory(historyPath: string, lines: string[]): void {
|
|
79
|
+
fs.writeFileSync(historyPath, lines.length ? `${lines.join("\n")}\n` : "", { encoding: "utf-8", mode: PRIVATE_FILE_MODE });
|
|
80
|
+
try { fs.chmodSync(historyPath, PRIVATE_FILE_MODE); } catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function sanitizeHistoryFile(historyPath: string): void {
|
|
84
|
+
if (!fs.existsSync(historyPath)) return;
|
|
85
|
+
const raw = fs.readFileSync(historyPath, "utf-8");
|
|
86
|
+
const { lines, changed } = sanitizeHistoryLines(raw);
|
|
87
|
+
if (changed) writePrivateHistory(historyPath, lines);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function appendPrivateHistoryLine(historyPath: string, line: string): void {
|
|
91
|
+
const fd = fs.openSync(historyPath, fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY, PRIVATE_FILE_MODE);
|
|
92
|
+
try {
|
|
93
|
+
fs.writeSync(fd, `${line}\n`);
|
|
94
|
+
} finally {
|
|
95
|
+
fs.closeSync(fd);
|
|
96
|
+
}
|
|
97
|
+
try { fs.chmodSync(historyPath, PRIVATE_FILE_MODE); } catch {}
|
|
98
|
+
}
|
|
99
|
+
|
|
21
100
|
export function recordRun(agent: string, task: string, exitCode: number, durationMs: number): void {
|
|
22
101
|
try {
|
|
23
102
|
const entry: RunEntry = {
|
|
24
103
|
agent,
|
|
25
|
-
task:
|
|
104
|
+
task: REDACTED_TASK,
|
|
105
|
+
taskHash: hashTask(task),
|
|
26
106
|
ts: Math.floor(Date.now() / 1000),
|
|
27
107
|
status: exitCode === 0 ? "ok" : "error",
|
|
28
108
|
duration: durationMs,
|
|
29
109
|
...(exitCode !== 0 ? { exit: exitCode } : {}),
|
|
30
110
|
};
|
|
31
111
|
const historyPath = getHistoryPath();
|
|
32
|
-
|
|
33
|
-
|
|
112
|
+
hardenHistoryStorage(historyPath);
|
|
113
|
+
try { sanitizeHistoryFile(historyPath); } catch {}
|
|
114
|
+
appendPrivateHistoryLine(historyPath, JSON.stringify(entry));
|
|
34
115
|
} catch {
|
|
35
116
|
// Best-effort — never crash the execution flow for history recording
|
|
36
117
|
}
|
|
@@ -38,6 +119,7 @@ export function recordRun(agent: string, task: string, exitCode: number, duratio
|
|
|
38
119
|
|
|
39
120
|
export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
40
121
|
const historyPath = getHistoryPath();
|
|
122
|
+
try { hardenHistoryStorage(historyPath); } catch {}
|
|
41
123
|
if (!fs.existsSync(historyPath)) return [];
|
|
42
124
|
let raw: string;
|
|
43
125
|
try {
|
|
@@ -46,11 +128,14 @@ export function loadRunsForAgent(agent: string): RunEntry[] {
|
|
|
46
128
|
return [];
|
|
47
129
|
}
|
|
48
130
|
|
|
49
|
-
let lines
|
|
131
|
+
let { lines, changed } = sanitizeHistoryLines(raw);
|
|
50
132
|
|
|
51
133
|
if (lines.length > ROTATE_READ_THRESHOLD) {
|
|
52
134
|
lines = lines.slice(-ROTATE_KEEP);
|
|
53
|
-
|
|
135
|
+
changed = true;
|
|
136
|
+
}
|
|
137
|
+
if (changed) {
|
|
138
|
+
try { writePrivateHistory(historyPath, lines); } catch {}
|
|
54
139
|
}
|
|
55
140
|
|
|
56
141
|
return lines
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import * as os from "node:os";
|
|
3
4
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV } from "../../shared/utils.ts";
|
|
5
7
|
import type { JsonSchemaObject } from "../../shared/types.ts";
|
|
6
8
|
|
|
7
9
|
export const STRUCTURED_OUTPUT_SCHEMA_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA";
|
|
8
10
|
export const STRUCTURED_OUTPUT_CAPTURE_ENV = "PI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE";
|
|
11
|
+
export const MISSING_STRUCTURED_OUTPUT_CALL_ERROR = "Missing structured_output call; this step has outputSchema and must finish by calling structured_output.";
|
|
9
12
|
|
|
10
13
|
export interface StructuredOutputRuntime {
|
|
11
14
|
schema: JsonSchemaObject;
|
|
@@ -13,11 +16,108 @@ export interface StructuredOutputRuntime {
|
|
|
13
16
|
outputPath: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
const SCHEMA_MAP_KEYWORDS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"] as const;
|
|
20
|
+
const SCHEMA_SINGLE_KEYWORDS = ["additionalItems", "additionalProperties", "contains", "not", "propertyNames", "if", "then", "else", "unevaluatedItems", "unevaluatedProperties", "contentSchema"] as const;
|
|
21
|
+
const SCHEMA_ARRAY_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems"] as const;
|
|
22
|
+
|
|
23
|
+
function rewriteLocalJsonPointerRefs(schema: unknown, pointerPrefix: string, inheritsWrapperResource = true): unknown {
|
|
24
|
+
if (typeof schema === "boolean" || !schema || typeof schema !== "object" || Array.isArray(schema)) return schema;
|
|
25
|
+
const source = schema as Record<string, unknown>;
|
|
26
|
+
const rewritten: Record<string, unknown> = { ...source };
|
|
27
|
+
const sharesWrapperResource = inheritsWrapperResource && typeof source.$id !== "string";
|
|
28
|
+
if (sharesWrapperResource) {
|
|
29
|
+
for (const keyword of ["$ref", "$dynamicRef", "$recursiveRef"] as const) {
|
|
30
|
+
const ref = source[keyword];
|
|
31
|
+
if (ref === "#") rewritten[keyword] = pointerPrefix;
|
|
32
|
+
else if (typeof ref === "string" && ref.startsWith("#/")) rewritten[keyword] = `${pointerPrefix}${ref.slice(1)}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const keyword of SCHEMA_MAP_KEYWORDS) {
|
|
36
|
+
const entries = source[keyword];
|
|
37
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue;
|
|
38
|
+
rewritten[keyword] = Object.fromEntries(Object.entries(entries).map(([name, nested]) => [
|
|
39
|
+
name,
|
|
40
|
+
rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource),
|
|
41
|
+
]));
|
|
42
|
+
}
|
|
43
|
+
const items = source.items;
|
|
44
|
+
if (Array.isArray(items)) rewritten.items = items.map((nested) => rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource));
|
|
45
|
+
else if (items !== undefined) rewritten.items = rewriteLocalJsonPointerRefs(items, pointerPrefix, sharesWrapperResource);
|
|
46
|
+
for (const keyword of SCHEMA_SINGLE_KEYWORDS) {
|
|
47
|
+
if (source[keyword] !== undefined) rewritten[keyword] = rewriteLocalJsonPointerRefs(source[keyword], pointerPrefix, sharesWrapperResource);
|
|
48
|
+
}
|
|
49
|
+
for (const keyword of SCHEMA_ARRAY_KEYWORDS) {
|
|
50
|
+
if (Array.isArray(source[keyword])) rewritten[keyword] = source[keyword].map((nested) => rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource));
|
|
51
|
+
}
|
|
52
|
+
const dependencies = source.dependencies;
|
|
53
|
+
if (dependencies && typeof dependencies === "object" && !Array.isArray(dependencies)) {
|
|
54
|
+
rewritten.dependencies = Object.fromEntries(Object.entries(dependencies).map(([name, nested]) => [
|
|
55
|
+
name,
|
|
56
|
+
Array.isArray(nested) ? nested : rewriteLocalJsonPointerRefs(nested, pointerPrefix, sharesWrapperResource),
|
|
57
|
+
]));
|
|
58
|
+
}
|
|
59
|
+
return rewritten;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createStructuredOutputToolParameters(schema: JsonSchemaObject): JsonSchemaObject {
|
|
63
|
+
return {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: { value: rewriteLocalJsonPointerRefs(schema, "#/properties/value") },
|
|
66
|
+
required: ["value"],
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
16
71
|
interface CompiledJsonSchema {
|
|
17
72
|
Check(value: unknown): boolean;
|
|
18
73
|
Errors(value: unknown): Iterable<{ instancePath?: string; message?: string }>;
|
|
19
74
|
}
|
|
20
75
|
|
|
76
|
+
type CompileJsonSchema = (schema: unknown) => CompiledJsonSchema;
|
|
77
|
+
|
|
78
|
+
let cachedCompile: Promise<CompileJsonSchema> | undefined;
|
|
79
|
+
|
|
80
|
+
export async function resolveCompileFromPackageRoot(packageRoot: string): Promise<CompileJsonSchema | undefined> {
|
|
81
|
+
const requireFromRoot = createRequire(path.join(packageRoot, "package.json"));
|
|
82
|
+
const resolved = requireFromRoot.resolve("typebox/compile");
|
|
83
|
+
const mod = (await import(pathToFileURL(resolved).href)) as { Compile?: unknown };
|
|
84
|
+
return typeof mod.Compile === "function" ? (mod.Compile as CompileJsonSchema) : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function importCompile(): Promise<CompileJsonSchema> {
|
|
88
|
+
const failures: string[] = [];
|
|
89
|
+
try {
|
|
90
|
+
const mod = (await import("typebox/compile")) as { Compile?: unknown };
|
|
91
|
+
if (typeof mod.Compile === "function") return mod.Compile as CompileJsonSchema;
|
|
92
|
+
failures.push("typebox/compile did not export a Compile function");
|
|
93
|
+
} catch (error) {
|
|
94
|
+
failures.push(`direct import failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
95
|
+
}
|
|
96
|
+
const packageRoot = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV];
|
|
97
|
+
if (packageRoot) {
|
|
98
|
+
try {
|
|
99
|
+
const compile = await resolveCompileFromPackageRoot(packageRoot);
|
|
100
|
+
if (compile) return compile;
|
|
101
|
+
failures.push("Pi package root typebox/compile did not export a Compile function");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
failures.push(`Pi package root import failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
failures.push(`${PI_CODING_AGENT_PACKAGE_ROOT_ENV} is not set`);
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`Cannot load typebox/compile for structured output validation (${failures.join("; ")})`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function loadCompile(): Promise<CompileJsonSchema> {
|
|
112
|
+
if (!cachedCompile) {
|
|
113
|
+
cachedCompile = importCompile().catch((error) => {
|
|
114
|
+
cachedCompile = undefined;
|
|
115
|
+
throw error;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return cachedCompile;
|
|
119
|
+
}
|
|
120
|
+
|
|
21
121
|
export function assertJsonSchemaObject(schema: unknown, label = "outputSchema"): asserts schema is JsonSchemaObject {
|
|
22
122
|
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
23
123
|
throw new Error(`${label} must be a JSON Schema object.`);
|
|
@@ -35,10 +135,11 @@ export function createStructuredOutputRuntime(schema: JsonSchemaObject, baseDir?
|
|
|
35
135
|
return { schema, schemaPath, outputPath };
|
|
36
136
|
}
|
|
37
137
|
|
|
38
|
-
export function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): { status: "valid" } | { status: "invalid"; message: string } {
|
|
138
|
+
export async function validateStructuredOutputValue(schema: JsonSchemaObject, value: unknown): Promise<{ status: "valid" } | { status: "invalid"; message: string }> {
|
|
139
|
+
const compile = await loadCompile();
|
|
39
140
|
let validator: CompiledJsonSchema;
|
|
40
141
|
try {
|
|
41
|
-
validator = (
|
|
142
|
+
validator = compile(schema);
|
|
42
143
|
} catch (error) {
|
|
43
144
|
return { status: "invalid", message: `invalid outputSchema: ${error instanceof Error ? error.message : String(error)}` };
|
|
44
145
|
}
|
|
@@ -52,9 +153,9 @@ export function validateStructuredOutputValue(schema: JsonSchemaObject, value: u
|
|
|
52
153
|
return { status: "invalid", message: errors.join("; ") || "schema validation failed" };
|
|
53
154
|
}
|
|
54
155
|
|
|
55
|
-
export function readStructuredOutput(runtime: StructuredOutputRuntime): { value?: unknown; error?: string } {
|
|
156
|
+
export async function readStructuredOutput(runtime: StructuredOutputRuntime): Promise<{ value?: unknown; error?: string }> {
|
|
56
157
|
if (!fs.existsSync(runtime.outputPath)) {
|
|
57
|
-
return { error:
|
|
158
|
+
return { error: MISSING_STRUCTURED_OUTPUT_CALL_ERROR };
|
|
58
159
|
}
|
|
59
160
|
let value: unknown;
|
|
60
161
|
try {
|
|
@@ -62,8 +163,12 @@ export function readStructuredOutput(runtime: StructuredOutputRuntime): { value?
|
|
|
62
163
|
} catch (error) {
|
|
63
164
|
return { error: `Failed to read structured output: ${error instanceof Error ? error.message : String(error)}` };
|
|
64
165
|
}
|
|
65
|
-
|
|
66
|
-
|
|
166
|
+
try {
|
|
167
|
+
const validation = await validateStructuredOutputValue(runtime.schema, value);
|
|
168
|
+
if (validation.status === "invalid") return { error: `Structured output validation failed: ${validation.message}` };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return { error: `Failed to validate structured output: ${error instanceof Error ? error.message : String(error)}` };
|
|
171
|
+
}
|
|
67
172
|
return { value };
|
|
68
173
|
}
|
|
69
174
|
|
|
@@ -193,11 +193,15 @@ export function formatControlNoticeMessage(event: ControlEvent, childIntercomTar
|
|
|
193
193
|
].filter((line): line is string => Boolean(line)).join("\n");
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
const supervisorHint = event.reason === "supervisor_request"
|
|
197
|
+
? "Supervisor request: reply to the pending request. If subagent_supervisor pending is empty, check intercom pending because an external intercom tool may own the request."
|
|
198
|
+
: undefined;
|
|
196
199
|
return [
|
|
197
200
|
`Subagent needs attention: ${event.agent}`,
|
|
198
201
|
`Run: ${runTarget}${event.index !== undefined ? ` step ${event.index + 1}` : ""}`,
|
|
199
202
|
`Signal: ${event.message}`,
|
|
200
203
|
event.recentFailureSummary ? `Recent failures: ${event.recentFailureSummary}` : undefined,
|
|
204
|
+
supervisorHint,
|
|
201
205
|
"Hint: Inspect status first unless the run is clearly blocked. Use steer for a top-level live async child, routed resume for a live nested child, or resume to revive a paused/completed/failed child.",
|
|
202
206
|
`Top-level live async nudge: ${steerCommand}`,
|
|
203
207
|
`Routed live nested nudge: ${nestedResumeCommand}`,
|
|
@@ -4,15 +4,14 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { registerNativeSupervisorClient } from "../../intercom/native-supervisor-channel.ts";
|
|
5
5
|
import { consumeSteerRequestsFromDir, steerAckPathFromDir, writeSteerAckAt, writeSteerCapabilityAt, writeSteerRequestToDir, type SteerRequest } from "../background/control-channel.ts";
|
|
6
6
|
import { SUBAGENT_CHILD_AGENT_ENV, SUBAGENT_CHILD_INDEX_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_STEER_ACK_DIR_ENV, SUBAGENT_STEER_CAPABILITY_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
7
|
-
import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
7
|
+
import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
8
8
|
import {
|
|
9
9
|
CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
|
|
10
|
-
formatChildToolDiagnostic,
|
|
11
10
|
REQUIRED_CHILD_TOOLS_ENV,
|
|
12
11
|
writeChildToolDiagnostic,
|
|
13
12
|
type ChildToolDiagnostic,
|
|
14
13
|
} from "./tool-availability.ts";
|
|
15
|
-
import { TOOL_BUDGET_ENV, decodeToolBudgetEnv, shouldBlockToolForBudget, toolBudgetBlockedMessage, toolBudgetSoftNudge } from "./tool-budget.ts";
|
|
14
|
+
import { TOOL_BUDGET_ENV, TOOL_BUDGET_ZERO_AUTH_ENV, decodeToolBudgetEnv, shouldBlockToolForBudget, toolBudgetBlockedMessage, toolBudgetSoftNudge } from "./tool-budget.ts";
|
|
16
15
|
import type { JsonSchemaObject, ResolvedToolBudget, SubagentState } from "../../shared/types.ts";
|
|
17
16
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
18
17
|
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
@@ -69,14 +68,20 @@ function readBooleanEnv(name: string): boolean | undefined {
|
|
|
69
68
|
return value !== "0";
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
function
|
|
73
|
-
const filePath = process.env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV]?.trim();
|
|
71
|
+
function readRequiredChildTools(): string[] | undefined {
|
|
74
72
|
const encoded = process.env[REQUIRED_CHILD_TOOLS_ENV]?.trim();
|
|
75
|
-
if (!
|
|
73
|
+
if (!encoded) return undefined;
|
|
76
74
|
const required = JSON.parse(encoded) as unknown;
|
|
77
75
|
if (!Array.isArray(required) || required.some((name) => typeof name !== "string" || !name)) {
|
|
78
76
|
throw new Error(`Invalid ${REQUIRED_CHILD_TOOLS_ENV} payload.`);
|
|
79
77
|
}
|
|
78
|
+
return required;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function refreshChildToolDiagnostic(pi: ExtensionAPI): ChildToolDiagnostic | undefined {
|
|
82
|
+
const filePath = process.env[CHILD_TOOL_DIAGNOSTIC_PATH_ENV]?.trim();
|
|
83
|
+
const required = readRequiredChildTools();
|
|
84
|
+
if (!filePath || !required) return undefined;
|
|
80
85
|
const available = pi.getAllTools().map((tool) => tool.name);
|
|
81
86
|
return writeChildToolDiagnostic(filePath, required, available, process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim());
|
|
82
87
|
}
|
|
@@ -329,7 +334,7 @@ export function registerSteeringInbox(
|
|
|
329
334
|
|
|
330
335
|
export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
331
336
|
registerSteeringInbox(pi);
|
|
332
|
-
registerToolBudget(pi, decodeToolBudgetEnv(process.env[TOOL_BUDGET_ENV]));
|
|
337
|
+
registerToolBudget(pi, decodeToolBudgetEnv(process.env[TOOL_BUDGET_ENV], { allowZero: process.env[TOOL_BUDGET_ZERO_AUTH_ENV] === "1" }));
|
|
333
338
|
registerChildWatchdog(pi);
|
|
334
339
|
const waitToolEnabled = resolveWaitToolConfig().enabled;
|
|
335
340
|
const waitState = {
|
|
@@ -365,6 +370,9 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
365
370
|
const sessionManager = (ctx as { sessionManager?: Parameters<typeof resolveCurrentSessionId>[0] } | undefined)?.sessionManager;
|
|
366
371
|
waitState.currentSessionId = sessionManager ? resolveCurrentSessionId(sessionManager) : null;
|
|
367
372
|
registerNativeSupervisorClientOnce();
|
|
373
|
+
if (readRequiredChildTools()?.includes("intercom")) registerNativeSupervisorFallbackOnce();
|
|
374
|
+
});
|
|
375
|
+
onRuntimeEvent("agent_start", () => {
|
|
368
376
|
refreshChildToolDiagnostic(pi);
|
|
369
377
|
});
|
|
370
378
|
onRuntimeEvent("agent_end", async (_event: unknown, ctx: unknown) => {
|
|
@@ -375,12 +383,7 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
375
383
|
const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
|
|
376
384
|
if (structuredOutputPath && structuredSchemaPath) {
|
|
377
385
|
const schema = JSON.parse(fs.readFileSync(structuredSchemaPath, "utf-8")) as JsonSchemaObject;
|
|
378
|
-
const parameters =
|
|
379
|
-
type: "object",
|
|
380
|
-
properties: { value: schema },
|
|
381
|
-
required: ["value"],
|
|
382
|
-
additionalProperties: false,
|
|
383
|
-
};
|
|
386
|
+
const parameters = createStructuredOutputToolParameters(schema);
|
|
384
387
|
const registerTool = pi.registerTool as unknown as (tool: {
|
|
385
388
|
name: string;
|
|
386
389
|
label: string;
|
|
@@ -394,7 +397,7 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
394
397
|
description: "Submit the required final structured output for this subagent step. This terminates the step.",
|
|
395
398
|
parameters: parameters as never,
|
|
396
399
|
async execute(_id: string, params: { value: unknown }) {
|
|
397
|
-
const validation = validateStructuredOutputValue(schema, params.value);
|
|
400
|
+
const validation = await validateStructuredOutputValue(schema, params.value);
|
|
398
401
|
if (validation.status === "invalid") {
|
|
399
402
|
throw new Error(`Structured output validation failed: ${validation.message}`);
|
|
400
403
|
}
|
|
@@ -417,7 +420,6 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
417
420
|
|
|
418
421
|
onRuntimeEvent("before_agent_start", async (event: { systemPrompt: string }) => {
|
|
419
422
|
registerNativeSupervisorFallbackOnce();
|
|
420
|
-
const toolDiagnostic = refreshChildToolDiagnostic(pi);
|
|
421
423
|
const intercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
|
|
422
424
|
if (intercomSessionName && typeof pi.setSessionName === "function") {
|
|
423
425
|
pi.setSessionName(intercomSessionName);
|
|
@@ -434,9 +436,6 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
434
436
|
fanoutChild: fanoutChild === true,
|
|
435
437
|
});
|
|
436
438
|
}
|
|
437
|
-
if (toolDiagnostic) {
|
|
438
|
-
rewritten = `${formatChildToolDiagnostic(toolDiagnostic)}\nDo not claim tool-dependent work succeeded; report this configuration error to the parent.\n\n${rewritten}`;
|
|
439
|
-
}
|
|
440
439
|
if (rewritten === event.systemPrompt) return;
|
|
441
440
|
return { systemPrompt: rewritten };
|
|
442
441
|
});
|
|
@@ -25,10 +25,11 @@ const REVIEW_ONLY_PATTERNS = [
|
|
|
25
25
|
];
|
|
26
26
|
|
|
27
27
|
const REVIEWER_REQUIRED_EDIT_PATTERNS = [
|
|
28
|
-
/\bmust\s+(?:edit|modify|change|fix|patch|apply)\b/i,
|
|
29
|
-
/\brequired\s+to\s+(?:edit|modify|change|fix|patch|apply)\b/i,
|
|
28
|
+
/\bmust\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
|
|
29
|
+
/\brequired\s+to\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
|
|
30
|
+
/(?:^|[.!?\n]\s*)implement\s+(?:the\s+)?(?:approved|requested|specified|file|code|source|fix(?:es)?|changes?)\b/i,
|
|
30
31
|
/\bregardless\s+of\s+findings\b/i,
|
|
31
|
-
/\balways\s+(?:edit|modify|change|fix|patch|apply)\b/i,
|
|
32
|
+
/\balways\s+(?:edit|modify|change|fix|patch|apply|implement)\b/i,
|
|
32
33
|
/\bapply\s+(?:the\s+)?fix(?:es)?\s+directly\b/i,
|
|
33
34
|
/\bmake\s+(?:the\s+)?code\s+changes\b/i,
|
|
34
35
|
];
|
|
@@ -134,8 +135,12 @@ function taskHasReadOnlyDeliverable(taskText: string): boolean {
|
|
|
134
135
|
return READ_ONLY_DELIVERABLE_PATTERNS.some((pattern) => pattern.test(taskText));
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
function isReviewerStyleAgent(agent: string): boolean {
|
|
139
|
+
return /\b(?:advisor|reviewer|oracle)\b/i.test(agent);
|
|
140
|
+
}
|
|
141
|
+
|
|
137
142
|
function hasImplementationIntent(agent: string, taskText: string): boolean {
|
|
138
|
-
if (
|
|
143
|
+
if (isReviewerStyleAgent(agent)) return REVIEWER_REQUIRED_EDIT_PATTERNS.some((pattern) => pattern.test(taskText));
|
|
139
144
|
if (agent === "worker") return WORKER_IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(taskText));
|
|
140
145
|
return GENERAL_IMPLEMENTATION_PATTERNS.some((pattern) => pattern.test(taskText));
|
|
141
146
|
}
|
|
@@ -151,7 +156,7 @@ export function classifyTaskMutationIntent(agent: string, task: string): TaskMut
|
|
|
151
156
|
|
|
152
157
|
if (RESEARCH_AGENT_PATTERNS.some((pattern) => pattern.test(agent))) return { kind: "read-only" };
|
|
153
158
|
if (hasImplementationIntent(agent, taskText)) return { kind: "implementation" };
|
|
154
|
-
if (
|
|
159
|
+
if (isReviewerStyleAgent(agent)) return { kind: "read-only" };
|
|
155
160
|
return taskHasReadOnlyDeliverable(taskTextWithoutScopedConstraints) ? { kind: "read-only" } : { kind: "unknown" };
|
|
156
161
|
}
|
|
157
162
|
|
|
@@ -11,13 +11,15 @@ export interface ChildToolDiagnostic {
|
|
|
11
11
|
missing: string[];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
const PI_CORE_CHILD_TOOLS = new Set(["bash", "edit", "find", "grep", "ls", "read", "write"]);
|
|
15
|
+
|
|
14
16
|
export function writeChildToolDiagnostic(
|
|
15
17
|
filePath: string,
|
|
16
18
|
required: string[],
|
|
17
19
|
available: string[],
|
|
18
20
|
agent?: string,
|
|
19
21
|
): ChildToolDiagnostic | undefined {
|
|
20
|
-
const availableNames = new Set(available);
|
|
22
|
+
const availableNames = new Set([...available, ...PI_CORE_CHILD_TOOLS]);
|
|
21
23
|
const missing = required.filter((name) => !availableNames.has(name));
|
|
22
24
|
if (missing.length === 0) {
|
|
23
25
|
fs.rmSync(filePath, { force: true });
|
|
@@ -2,6 +2,7 @@ import type { ResolvedToolBudget, ToolBudgetConfig, ToolBudgetState } from "../.
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_TOOL_BUDGET_BLOCK = ["read", "grep", "find", "ls"] as const;
|
|
4
4
|
export const TOOL_BUDGET_ENV = "PI_SUBAGENT_TOOL_BUDGET";
|
|
5
|
+
export const TOOL_BUDGET_ZERO_AUTH_ENV = "PI_SUBAGENT_TOOL_BUDGET_ZERO_AUTH";
|
|
5
6
|
|
|
6
7
|
export function normalizeToolBudgetBlock(block: ToolBudgetConfig["block"] | undefined): "*" | string[] {
|
|
7
8
|
if (block === "*") return "*";
|
|
@@ -9,12 +10,17 @@ export function normalizeToolBudgetBlock(block: ToolBudgetConfig["block"] | unde
|
|
|
9
10
|
return [...new Set(block.map((tool) => tool.trim()).filter(Boolean))];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function validateToolBudgetConfig(
|
|
13
|
+
export function validateToolBudgetConfig(
|
|
14
|
+
raw: unknown,
|
|
15
|
+
label = "toolBudget",
|
|
16
|
+
options: { minimumHard?: 0 | 1 } = {},
|
|
17
|
+
): { budget?: ResolvedToolBudget; error?: string } {
|
|
13
18
|
if (raw === undefined) return {};
|
|
14
19
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { error: `${label} must be an object with hard and optional soft/block.` };
|
|
15
20
|
const value = raw as ToolBudgetConfig;
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const minimumHard = options.minimumHard ?? 1;
|
|
22
|
+
if (typeof value.hard !== "number" || !Number.isInteger(value.hard) || value.hard < minimumHard) {
|
|
23
|
+
return { error: `${label}.hard must be an integer >= ${minimumHard}.` };
|
|
18
24
|
}
|
|
19
25
|
if (value.soft !== undefined && (typeof value.soft !== "number" || !Number.isInteger(value.soft) || value.soft < 1)) {
|
|
20
26
|
return { error: `${label}.soft must be an integer >= 1 when provided.` };
|
|
@@ -65,10 +71,10 @@ export function encodeToolBudgetEnv(budget: ResolvedToolBudget | undefined): str
|
|
|
65
71
|
return budget ? JSON.stringify(budget) : undefined;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
export function decodeToolBudgetEnv(value: string | undefined): ResolvedToolBudget | undefined {
|
|
74
|
+
export function decodeToolBudgetEnv(value: string | undefined, options: { allowZero?: boolean } = {}): ResolvedToolBudget | undefined {
|
|
69
75
|
if (!value?.trim()) return undefined;
|
|
70
76
|
const parsed = JSON.parse(value) as unknown;
|
|
71
|
-
const normalized = validateToolBudgetConfig(parsed, TOOL_BUDGET_ENV);
|
|
77
|
+
const normalized = validateToolBudgetConfig(parsed, TOOL_BUDGET_ENV, options.allowZero ? { minimumHard: 0 } : undefined);
|
|
72
78
|
if (normalized.error) throw new Error(normalized.error);
|
|
73
79
|
return normalized.budget;
|
|
74
80
|
}
|
|
@@ -88,10 +88,11 @@ export function turnBudgetDecision(
|
|
|
88
88
|
turnCount: number,
|
|
89
89
|
terminalAssistantStop: boolean,
|
|
90
90
|
toolWorkActiveOrStarting: boolean,
|
|
91
|
+
enforceHardLimit = false,
|
|
91
92
|
): "continue" | "defer" | "abort" {
|
|
92
93
|
const hardLimit = budget.maxTurns + budget.graceTurns;
|
|
93
94
|
if (turnCount < hardLimit) return "continue";
|
|
94
|
-
if (toolWorkActiveOrStarting) return "defer";
|
|
95
|
+
if (toolWorkActiveOrStarting && !enforceHardLimit) return "defer";
|
|
95
96
|
if (turnCount === hardLimit && terminalAssistantStop) return "continue";
|
|
96
97
|
return "abort";
|
|
97
98
|
}
|
|
@@ -18,7 +18,7 @@ interface WorktreeInfo {
|
|
|
18
18
|
syntheticPaths: string[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
interface WorktreeDiff {
|
|
21
|
+
export interface WorktreeDiff {
|
|
22
22
|
index: number;
|
|
23
23
|
agent: string;
|
|
24
24
|
branch: string;
|
|
@@ -27,6 +27,23 @@ interface WorktreeDiff {
|
|
|
27
27
|
insertions: number;
|
|
28
28
|
deletions: number;
|
|
29
29
|
patchPath: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WorktreeCleanupTask {
|
|
34
|
+
index: number;
|
|
35
|
+
path: string;
|
|
36
|
+
branch: string;
|
|
37
|
+
worktreeRemoved: boolean;
|
|
38
|
+
branchRemoved: boolean;
|
|
39
|
+
errors?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WorktreeCleanupReport {
|
|
43
|
+
state: "complete" | "partial";
|
|
44
|
+
tasks: WorktreeCleanupTask[];
|
|
45
|
+
pruned: boolean;
|
|
46
|
+
errors?: string[];
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
interface WorktreeTaskCwdConflict {
|
|
@@ -426,7 +443,7 @@ function removeSyntheticPathsBeforeDiff(worktree: WorktreeInfo): void {
|
|
|
426
443
|
}
|
|
427
444
|
}
|
|
428
445
|
|
|
429
|
-
function emptyDiff(index: number, agent: string, branch: string, patchPath: string): WorktreeDiff {
|
|
446
|
+
function emptyDiff(index: number, agent: string, branch: string, patchPath: string, error?: string): WorktreeDiff {
|
|
430
447
|
return {
|
|
431
448
|
index,
|
|
432
449
|
agent,
|
|
@@ -436,6 +453,7 @@ function emptyDiff(index: number, agent: string, branch: string, patchPath: stri
|
|
|
436
453
|
insertions: 0,
|
|
437
454
|
deletions: 0,
|
|
438
455
|
patchPath,
|
|
456
|
+
...(error ? { error } : {}),
|
|
439
457
|
};
|
|
440
458
|
}
|
|
441
459
|
|
|
@@ -497,13 +515,30 @@ function writeEmptyPatch(patchPath: string): void {
|
|
|
497
515
|
}
|
|
498
516
|
}
|
|
499
517
|
|
|
500
|
-
function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo):
|
|
501
|
-
|
|
502
|
-
|
|
518
|
+
function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): WorktreeCleanupTask {
|
|
519
|
+
const errors: string[] = [];
|
|
520
|
+
let worktreeRemoved = false;
|
|
521
|
+
let branchRemoved = false;
|
|
522
|
+
try {
|
|
523
|
+
runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]);
|
|
524
|
+
worktreeRemoved = true;
|
|
525
|
+
} catch (error) {
|
|
526
|
+
errors.push(`worktree removal failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
503
527
|
}
|
|
504
|
-
try {
|
|
505
|
-
|
|
528
|
+
try {
|
|
529
|
+
runGitChecked(repoCwd, ["branch", "-D", worktree.branch]);
|
|
530
|
+
branchRemoved = true;
|
|
531
|
+
} catch (error) {
|
|
532
|
+
errors.push(`branch removal failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
506
533
|
}
|
|
534
|
+
return {
|
|
535
|
+
index: worktree.index,
|
|
536
|
+
path: worktree.path,
|
|
537
|
+
branch: worktree.branch,
|
|
538
|
+
worktreeRemoved,
|
|
539
|
+
branchRemoved,
|
|
540
|
+
...(errors.length ? { errors } : {}),
|
|
541
|
+
};
|
|
507
542
|
}
|
|
508
543
|
|
|
509
544
|
function hasWorktreeChanges(diff: WorktreeDiff): boolean {
|
|
@@ -560,23 +595,37 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
|
|
|
560
595
|
const patchPath = path.join(diffsDir, `task-${index}-${safePatchAgentName(agent)}.patch`);
|
|
561
596
|
try {
|
|
562
597
|
diffs.push(captureWorktreeDiff(setup, worktree, agent, patchPath));
|
|
563
|
-
} catch {
|
|
564
|
-
// Preserve execution flow
|
|
598
|
+
} catch (error) {
|
|
599
|
+
// Preserve execution flow while retaining the failed capture as handoff evidence.
|
|
565
600
|
writeEmptyPatch(patchPath);
|
|
566
|
-
diffs.push(emptyDiff(index, agent, worktree.branch, patchPath));
|
|
601
|
+
diffs.push(emptyDiff(index, agent, worktree.branch, patchPath, error instanceof Error ? error.message : String(error)));
|
|
567
602
|
}
|
|
568
603
|
}
|
|
569
604
|
|
|
570
605
|
return diffs;
|
|
571
606
|
}
|
|
572
607
|
|
|
573
|
-
export function cleanupWorktrees(setup: WorktreeSetup):
|
|
608
|
+
export function cleanupWorktrees(setup: WorktreeSetup): WorktreeCleanupReport {
|
|
609
|
+
const tasks: WorktreeCleanupTask[] = [];
|
|
574
610
|
for (let index = setup.worktrees.length - 1; index >= 0; index--) {
|
|
575
|
-
cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!);
|
|
611
|
+
tasks.push(cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!));
|
|
576
612
|
}
|
|
577
|
-
|
|
578
|
-
|
|
613
|
+
tasks.sort((left, right) => left.index - right.index);
|
|
614
|
+
const errors: string[] = [];
|
|
615
|
+
let pruned = false;
|
|
616
|
+
try {
|
|
617
|
+
runGitChecked(setup.cwd, ["worktree", "prune"]);
|
|
618
|
+
pruned = true;
|
|
619
|
+
} catch (error) {
|
|
620
|
+
errors.push(`worktree prune failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
579
621
|
}
|
|
622
|
+
const state = tasks.every((task) => task.worktreeRemoved && task.branchRemoved) && pruned ? "complete" : "partial";
|
|
623
|
+
return {
|
|
624
|
+
state,
|
|
625
|
+
tasks,
|
|
626
|
+
pruned,
|
|
627
|
+
...(errors.length ? { errors } : {}),
|
|
628
|
+
};
|
|
580
629
|
}
|
|
581
630
|
|
|
582
631
|
export function formatWorktreeDiffSummary(diffs: WorktreeDiff[]): string {
|