pi-subagents 0.18.1 → 0.19.1
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 +17 -0
- package/README.md +2 -2
- package/agent-manager-chain-detail.ts +50 -6
- package/agent-manager-detail.ts +15 -2
- package/agent-manager.ts +76 -23
- package/async-execution.ts +45 -18
- package/chain-execution.ts +12 -2
- package/doctor.ts +198 -0
- package/execution.ts +2 -4
- package/index.ts +5 -2
- package/intercom-bridge.ts +61 -9
- package/package.json +5 -1
- package/prompts/parallel-review.md +8 -0
- package/schemas.ts +5 -2
- package/settings.ts +5 -0
- package/slash-commands.ts +95 -42
- package/slash-live-state.ts +3 -3
- package/subagent-executor.ts +137 -19
- package/subagent-runner.ts +8 -0
- package/subagents-status.ts +229 -9
- package/worktree.ts +19 -10
package/doctor.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { discoverAgentsAll, type AgentSource } from "./agents.ts";
|
|
4
|
+
import { isAsyncAvailable } from "./async-execution.ts";
|
|
5
|
+
import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic } from "./intercom-bridge.ts";
|
|
6
|
+
import { discoverAvailableSkills, type SkillSource } from "./skills.ts";
|
|
7
|
+
import {
|
|
8
|
+
ASYNC_DIR,
|
|
9
|
+
CHAIN_RUNS_DIR,
|
|
10
|
+
RESULTS_DIR,
|
|
11
|
+
TEMP_ROOT_DIR,
|
|
12
|
+
type ExtensionConfig,
|
|
13
|
+
type SubagentState,
|
|
14
|
+
} from "./types.ts";
|
|
15
|
+
|
|
16
|
+
interface DoctorPaths {
|
|
17
|
+
tempRootDir: string;
|
|
18
|
+
asyncDir: string;
|
|
19
|
+
resultsDir: string;
|
|
20
|
+
chainRunsDir: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DoctorDeps {
|
|
24
|
+
isAsyncAvailable: () => boolean;
|
|
25
|
+
discoverAgentsAll: typeof discoverAgentsAll;
|
|
26
|
+
discoverAvailableSkills: typeof discoverAvailableSkills;
|
|
27
|
+
diagnoseIntercomBridge: typeof diagnoseIntercomBridge;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DoctorReportInput {
|
|
31
|
+
cwd: string;
|
|
32
|
+
config: ExtensionConfig;
|
|
33
|
+
state: SubagentState;
|
|
34
|
+
context?: "fresh" | "fork";
|
|
35
|
+
requestedSessionDir?: string;
|
|
36
|
+
currentSessionFile?: string | null;
|
|
37
|
+
currentSessionId?: string | null;
|
|
38
|
+
orchestratorTarget?: string;
|
|
39
|
+
sessionError?: string;
|
|
40
|
+
expandTilde?: (value: string) => string;
|
|
41
|
+
paths?: DoctorPaths;
|
|
42
|
+
deps?: Partial<DoctorDeps>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_PATHS: DoctorPaths = {
|
|
46
|
+
tempRootDir: TEMP_ROOT_DIR,
|
|
47
|
+
asyncDir: ASYNC_DIR,
|
|
48
|
+
resultsDir: RESULTS_DIR,
|
|
49
|
+
chainRunsDir: CHAIN_RUNS_DIR,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const DEFAULT_DEPS: DoctorDeps = {
|
|
53
|
+
isAsyncAvailable,
|
|
54
|
+
discoverAgentsAll,
|
|
55
|
+
discoverAvailableSkills,
|
|
56
|
+
diagnoseIntercomBridge,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function errorText(error: unknown): string {
|
|
60
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function lineFromCheck(label: string, check: () => string): string {
|
|
64
|
+
try {
|
|
65
|
+
return check();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return `- ${label}: failed — ${errorText(error)}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatExistingDirectory(label: string, dirPath: string): string {
|
|
72
|
+
try {
|
|
73
|
+
if (!fs.existsSync(dirPath)) return `- ${label}: missing (${dirPath})`;
|
|
74
|
+
const stats = fs.statSync(dirPath);
|
|
75
|
+
if (!stats.isDirectory()) throw new Error(`not a directory: ${dirPath}`);
|
|
76
|
+
fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
77
|
+
return `- ${label}: ok (${dirPath})`;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return `- ${label}: failed (${dirPath}) — ${errorText(error)}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatSourceCounts(counts: Record<AgentSource, number>): string {
|
|
84
|
+
return `builtin ${counts.builtin}, user ${counts.user}, project ${counts.project}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatSkillSourceCounts(skills: Array<{ source: SkillSource }>): string {
|
|
88
|
+
const counts = new Map<SkillSource, number>();
|
|
89
|
+
for (const skill of skills) counts.set(skill.source, (counts.get(skill.source) ?? 0) + 1);
|
|
90
|
+
const ordered: SkillSource[] = [
|
|
91
|
+
"project",
|
|
92
|
+
"project-settings",
|
|
93
|
+
"project-package",
|
|
94
|
+
"user",
|
|
95
|
+
"user-settings",
|
|
96
|
+
"user-package",
|
|
97
|
+
"extension",
|
|
98
|
+
"builtin",
|
|
99
|
+
"unknown",
|
|
100
|
+
];
|
|
101
|
+
const parts = ordered
|
|
102
|
+
.map((source) => `${source} ${counts.get(source) ?? 0}`)
|
|
103
|
+
.filter((part) => !part.endsWith(" 0"));
|
|
104
|
+
return parts.length > 0 ? parts.join(", ") : "none";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatConfiguredSessionDir(input: DoctorReportInput): string {
|
|
108
|
+
if (input.requestedSessionDir) {
|
|
109
|
+
return path.resolve(input.expandTilde?.(input.requestedSessionDir) ?? input.requestedSessionDir);
|
|
110
|
+
}
|
|
111
|
+
if (input.config.defaultSessionDir) {
|
|
112
|
+
return path.resolve(input.expandTilde?.(input.config.defaultSessionDir) ?? input.config.defaultSessionDir);
|
|
113
|
+
}
|
|
114
|
+
return "not configured";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatSessionLines(input: DoctorReportInput): string[] {
|
|
118
|
+
const sessionFile = input.currentSessionFile ?? null;
|
|
119
|
+
const lines = [
|
|
120
|
+
lineFromCheck("configured session dir", () => `- configured session dir: ${formatConfiguredSessionDir(input)}`),
|
|
121
|
+
`- current session file: ${sessionFile ?? "not available"}`,
|
|
122
|
+
`- current session dir: ${sessionFile ? path.dirname(sessionFile) : "not available"}`,
|
|
123
|
+
`- current session id: ${input.currentSessionId ?? input.state.currentSessionId ?? "not available"}`,
|
|
124
|
+
];
|
|
125
|
+
if (input.sessionError) lines.push(`- session manager: failed — ${input.sessionError}`);
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatDiscovery(input: DoctorReportInput, deps: DoctorDeps): string[] {
|
|
130
|
+
return [
|
|
131
|
+
lineFromCheck("agents/chains", () => {
|
|
132
|
+
const discovered = deps.discoverAgentsAll(input.cwd);
|
|
133
|
+
const agentCounts = {
|
|
134
|
+
builtin: discovered.builtin.length,
|
|
135
|
+
user: discovered.user.length,
|
|
136
|
+
project: discovered.project.length,
|
|
137
|
+
};
|
|
138
|
+
const chainCounts = discovered.chains.reduce<Record<AgentSource, number>>((counts, chain) => {
|
|
139
|
+
counts[chain.source] += 1;
|
|
140
|
+
return counts;
|
|
141
|
+
}, { builtin: 0, user: 0, project: 0 });
|
|
142
|
+
return [
|
|
143
|
+
`- agents: total ${agentCounts.builtin + agentCounts.user + agentCounts.project} (${formatSourceCounts(agentCounts)})`,
|
|
144
|
+
`- chains: total ${discovered.chains.length} (${formatSourceCounts(chainCounts)})`,
|
|
145
|
+
].join("\n");
|
|
146
|
+
}),
|
|
147
|
+
lineFromCheck("skills", () => {
|
|
148
|
+
const skills = deps.discoverAvailableSkills(input.cwd);
|
|
149
|
+
return `- skills: total ${skills.length} (${formatSkillSourceCounts(skills)})`;
|
|
150
|
+
}),
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatIntercomDiagnostic(diagnostic: IntercomBridgeDiagnostic, context: "fresh" | "fork" | undefined): string[] {
|
|
155
|
+
const lines = [
|
|
156
|
+
`- bridge: ${diagnostic.active ? "active" : "inactive"}${diagnostic.reason ? ` (${diagnostic.reason})` : ""}`,
|
|
157
|
+
`- mode: ${diagnostic.mode}; context: ${context ?? "unspecified"}`,
|
|
158
|
+
`- orchestrator target: ${diagnostic.orchestratorTarget ?? "not available"}`,
|
|
159
|
+
`- pi-intercom: ${diagnostic.piIntercomAvailable ? "available" : "unavailable"} at ${diagnostic.extensionDir}`,
|
|
160
|
+
];
|
|
161
|
+
if (diagnostic.configPath && diagnostic.intercomConfigEnabled !== undefined) {
|
|
162
|
+
lines.push(`- intercom config: ${diagnostic.intercomConfigEnabled === false ? "disabled" : "enabled or absent"} (${diagnostic.configPath})`);
|
|
163
|
+
}
|
|
164
|
+
if (diagnostic.intercomConfigError) {
|
|
165
|
+
lines.push(`- intercom config warning: ${diagnostic.intercomConfigError}; runtime assumes enabled`);
|
|
166
|
+
}
|
|
167
|
+
return lines;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function buildDoctorReport(input: DoctorReportInput): string {
|
|
171
|
+
const paths = input.paths ?? DEFAULT_PATHS;
|
|
172
|
+
const deps = { ...DEFAULT_DEPS, ...input.deps };
|
|
173
|
+
const lines = [
|
|
174
|
+
"Subagents doctor report",
|
|
175
|
+
"",
|
|
176
|
+
"Runtime",
|
|
177
|
+
`- cwd: ${input.cwd}`,
|
|
178
|
+
lineFromCheck("async support", () => `- async support: ${deps.isAsyncAvailable() ? "available" : "unavailable"}`),
|
|
179
|
+
...formatSessionLines(input),
|
|
180
|
+
"",
|
|
181
|
+
"Filesystem",
|
|
182
|
+
formatExistingDirectory("temp root", paths.tempRootDir),
|
|
183
|
+
formatExistingDirectory("async runs", paths.asyncDir),
|
|
184
|
+
formatExistingDirectory("results", paths.resultsDir),
|
|
185
|
+
formatExistingDirectory("chain runs", paths.chainRunsDir),
|
|
186
|
+
"",
|
|
187
|
+
"Discovery",
|
|
188
|
+
...formatDiscovery(input, deps),
|
|
189
|
+
"",
|
|
190
|
+
"Intercom bridge",
|
|
191
|
+
...lineFromCheck("intercom bridge", () => formatIntercomDiagnostic(deps.diagnoseIntercomBridge({
|
|
192
|
+
config: input.config.intercomBridge,
|
|
193
|
+
context: input.context,
|
|
194
|
+
orchestratorTarget: input.orchestratorTarget,
|
|
195
|
+
}), input.context).join("\n")).split("\n"),
|
|
196
|
+
];
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
package/execution.ts
CHANGED
|
@@ -243,13 +243,11 @@ async function runSingleAttempt(
|
|
|
243
243
|
};
|
|
244
244
|
|
|
245
245
|
const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
|
|
246
|
-
if (!options.allowIntercomDetach || detached || processClosed) return;
|
|
246
|
+
if (!options.allowIntercomDetach || detached || processClosed || !intercomStarted) return;
|
|
247
247
|
if (!payload || typeof payload !== "object") return;
|
|
248
248
|
const requestId = (payload as { requestId?: unknown }).requestId;
|
|
249
249
|
if (typeof requestId !== "string" || requestId.length === 0) return;
|
|
250
|
-
|
|
251
|
-
options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted });
|
|
252
|
-
if (!accepted) return;
|
|
250
|
+
options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted: true });
|
|
253
251
|
detachForIntercom();
|
|
254
252
|
});
|
|
255
253
|
|
package/index.ts
CHANGED
|
@@ -401,7 +401,7 @@ EXECUTION (use exactly ONE mode):
|
|
|
401
401
|
• Before executing, use { action: "list" } to inspect configured agents/chains. Only execute agents listed as executable/non-disabled.
|
|
402
402
|
• SINGLE: { agent, task? } - one task; omit task for self-contained agents
|
|
403
403
|
• CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
|
|
404
|
-
• PARALLEL: { tasks: [{agent,task,count?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
404
|
+
• PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
405
405
|
• Optional context: { context: "fresh" | "fork" } (default: "fresh")
|
|
406
406
|
|
|
407
407
|
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
@@ -421,7 +421,10 @@ MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
|
421
421
|
|
|
422
422
|
CONTROL:
|
|
423
423
|
• { action: "status", id: "..." } - inspect an async/background run by id or prefix
|
|
424
|
-
• { action: "interrupt", id?: "..." } - soft-interrupt the current child turn and leave the run paused
|
|
424
|
+
• { action: "interrupt", id?: "..." } - soft-interrupt the current child turn and leave the run paused
|
|
425
|
+
|
|
426
|
+
DIAGNOSTICS:
|
|
427
|
+
• { action: "doctor" } - read-only report for runtime paths, discovery, sessions, and intercom`,
|
|
425
428
|
parameters: SubagentParams,
|
|
426
429
|
|
|
427
430
|
execute(id, params, signal, onUpdate, ctx) {
|
package/intercom-bridge.ts
CHANGED
|
@@ -8,14 +8,14 @@ const DEFAULT_INTERCOM_EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "
|
|
|
8
8
|
const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
|
|
9
9
|
const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
|
|
10
10
|
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
11
|
-
const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
11
|
+
export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
12
12
|
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the orchestrator in normal assistant text.
|
|
13
13
|
|
|
14
14
|
Use intercom only for coordination with the orchestrator session "{orchestratorTarget}".
|
|
15
15
|
- Need a decision or blocked: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
|
|
16
|
-
-
|
|
16
|
+
- Blocked or explicitly asked to send progress: intercom({ action: "send", to: "{orchestratorTarget}", message: "UPDATE: <summary>" })
|
|
17
17
|
|
|
18
|
-
If no
|
|
18
|
+
Do not send routine completion handoffs through intercom. If no coordination is needed, return a focused task result.`;
|
|
19
19
|
|
|
20
20
|
export interface IntercomBridgeState {
|
|
21
21
|
active: boolean;
|
|
@@ -25,6 +25,19 @@ export interface IntercomBridgeState {
|
|
|
25
25
|
instruction: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface IntercomBridgeDiagnostic {
|
|
29
|
+
active: boolean;
|
|
30
|
+
mode: IntercomBridgeMode;
|
|
31
|
+
wantsIntercom: boolean;
|
|
32
|
+
piIntercomAvailable: boolean;
|
|
33
|
+
extensionDir: string;
|
|
34
|
+
configPath?: string;
|
|
35
|
+
orchestratorTarget?: string;
|
|
36
|
+
reason?: string;
|
|
37
|
+
intercomConfigEnabled?: boolean;
|
|
38
|
+
intercomConfigError?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
interface ResolveIntercomBridgeInput {
|
|
29
42
|
config: ExtensionConfig["intercomBridge"];
|
|
30
43
|
context: "fresh" | "fork" | undefined;
|
|
@@ -68,14 +81,13 @@ function resolveIntercomBridgeConfig(value: ExtensionConfig["intercomBridge"]):
|
|
|
68
81
|
};
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
function
|
|
72
|
-
if (!fs.existsSync(configPath)) return true;
|
|
84
|
+
function intercomConfigStatus(configPath: string): { enabled: boolean; error?: unknown } {
|
|
85
|
+
if (!fs.existsSync(configPath)) return { enabled: true };
|
|
73
86
|
try {
|
|
74
87
|
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8")) as { enabled?: unknown };
|
|
75
|
-
return parsed.enabled !== false;
|
|
88
|
+
return { enabled: parsed.enabled !== false };
|
|
76
89
|
} catch (error) {
|
|
77
|
-
|
|
78
|
-
return true;
|
|
90
|
+
return { enabled: true, error };
|
|
79
91
|
}
|
|
80
92
|
}
|
|
81
93
|
|
|
@@ -119,6 +131,44 @@ function buildIntercomBridgeInstruction(orchestratorTarget: string, template: st
|
|
|
119
131
|
${instruction}`;
|
|
120
132
|
}
|
|
121
133
|
|
|
134
|
+
export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeDiagnostic {
|
|
135
|
+
const config = resolveIntercomBridgeConfig(input.config);
|
|
136
|
+
const mode = config.mode;
|
|
137
|
+
const extensionDir = path.resolve(input.extensionDir ?? DEFAULT_INTERCOM_EXTENSION_DIR);
|
|
138
|
+
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
139
|
+
const configPath = path.resolve(input.configPath ?? DEFAULT_INTERCOM_CONFIG_PATH);
|
|
140
|
+
const wantsIntercom = mode !== "off" && !(mode === "fork-only" && input.context !== "fork");
|
|
141
|
+
const piIntercomAvailable = fs.existsSync(extensionDir);
|
|
142
|
+
let configStatus: ReturnType<typeof intercomConfigStatus> | undefined;
|
|
143
|
+
let reason: string | undefined;
|
|
144
|
+
if (mode === "off") reason = "bridge mode is off";
|
|
145
|
+
else if (mode === "fork-only" && input.context !== "fork") reason = "bridge mode is fork-only and context is not fork";
|
|
146
|
+
else if (!orchestratorTarget) reason = "orchestrator target is not available";
|
|
147
|
+
else if (!piIntercomAvailable) reason = "pi-intercom extension was not found";
|
|
148
|
+
else {
|
|
149
|
+
configStatus = intercomConfigStatus(configPath);
|
|
150
|
+
if (!configStatus.enabled) reason = "intercom config is disabled";
|
|
151
|
+
}
|
|
152
|
+
let intercomConfigError: string | undefined;
|
|
153
|
+
if (configStatus?.error) {
|
|
154
|
+
const error = configStatus.error;
|
|
155
|
+
intercomConfigError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
active: reason === undefined,
|
|
160
|
+
mode,
|
|
161
|
+
wantsIntercom,
|
|
162
|
+
piIntercomAvailable,
|
|
163
|
+
extensionDir,
|
|
164
|
+
configPath,
|
|
165
|
+
...(orchestratorTarget ? { orchestratorTarget } : {}),
|
|
166
|
+
...(reason ? { reason } : {}),
|
|
167
|
+
...(configStatus ? { intercomConfigEnabled: configStatus.enabled } : {}),
|
|
168
|
+
...(intercomConfigError ? { intercomConfigError } : {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
122
172
|
export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
|
|
123
173
|
const config = resolveIntercomBridgeConfig(input.config);
|
|
124
174
|
const mode = config.mode;
|
|
@@ -144,7 +194,9 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
|
|
|
144
194
|
}
|
|
145
195
|
|
|
146
196
|
const configPath = path.resolve(input.configPath ?? DEFAULT_INTERCOM_CONFIG_PATH);
|
|
147
|
-
|
|
197
|
+
const intercomStatus = intercomConfigStatus(configPath);
|
|
198
|
+
if (intercomStatus.error) console.warn(`Failed to parse intercom config at '${configPath}'. Assuming enabled.`, intercomStatus.error);
|
|
199
|
+
if (!intercomStatus.enabled) {
|
|
148
200
|
return { active: false, mode, extensionDir, instruction: defaultInstruction };
|
|
149
201
|
}
|
|
150
202
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
|
|
5
5
|
"author": "Nico Bailon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"*.mjs",
|
|
32
32
|
"agents/",
|
|
33
33
|
"skills/**/*",
|
|
34
|
+
"prompts/**/*",
|
|
34
35
|
"README.md",
|
|
35
36
|
"CHANGELOG.md"
|
|
36
37
|
],
|
|
@@ -46,6 +47,9 @@
|
|
|
46
47
|
],
|
|
47
48
|
"skills": [
|
|
48
49
|
"./skills"
|
|
50
|
+
],
|
|
51
|
+
"prompts": [
|
|
52
|
+
"./prompts"
|
|
49
53
|
]
|
|
50
54
|
},
|
|
51
55
|
"peerDependencies": {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Parallel subagents review
|
|
3
|
+
---
|
|
4
|
+
Great. Now let's launch parallel reviewers to conduct an adversarial review.
|
|
5
|
+
|
|
6
|
+
Important: launch reviewers with fresh context, not forked context. Reviewers should inspect the repository and current diff directly from files and commands, without inheriting the main agent chat. Use forked context only if I explicitly ask for it.
|
|
7
|
+
|
|
8
|
+
$@
|
package/schemas.ts
CHANGED
|
@@ -26,6 +26,9 @@ export const TaskItem = Type.Object({
|
|
|
26
26
|
task: Type.String(),
|
|
27
27
|
cwd: Type.Optional(Type.String()),
|
|
28
28
|
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
29
|
+
output: Type.Optional(OutputOverride),
|
|
30
|
+
reads: Type.Optional(ReadsOverride),
|
|
31
|
+
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
|
|
29
32
|
model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
|
|
30
33
|
skill: Type.Optional(SkillOverride),
|
|
31
34
|
});
|
|
@@ -103,7 +106,7 @@ export const SubagentParams = Type.Object({
|
|
|
103
106
|
task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
|
|
104
107
|
// Management action (when present, tool operates in management mode)
|
|
105
108
|
action: Type.Optional(Type.String({
|
|
106
|
-
description: "Action:
|
|
109
|
+
description: "Action: 'list', 'get', 'create', 'update', 'delete', 'status', 'interrupt', or 'doctor'. Omit for execution mode."
|
|
107
110
|
})),
|
|
108
111
|
id: Type.Optional(Type.String({
|
|
109
112
|
description: "Run id or prefix for action='status' or action='interrupt'."
|
|
@@ -124,7 +127,7 @@ export const SubagentParams = Type.Object({
|
|
|
124
127
|
additionalProperties: true,
|
|
125
128
|
description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
|
|
126
129
|
})),
|
|
127
|
-
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?}, ...]" })),
|
|
130
|
+
tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, reads?, progress?}, ...]" })),
|
|
128
131
|
concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
|
|
129
132
|
worktree: Type.Optional(Type.Boolean({
|
|
130
133
|
description: "Create isolated git worktrees for each parallel task. " +
|
package/settings.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { AgentConfig } from "./agents.ts";
|
|
|
8
8
|
import { normalizeSkillInput } from "./skills.ts";
|
|
9
9
|
import { CHAIN_RUNS_DIR } from "./types.ts";
|
|
10
10
|
const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
11
|
+
const INITIAL_PROGRESS_CONTENT = "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n";
|
|
11
12
|
|
|
12
13
|
// =============================================================================
|
|
13
14
|
// Behavior Resolution Types
|
|
@@ -224,6 +225,10 @@ function resolveChainPath(filePath: string, chainDir: string): string {
|
|
|
224
225
|
* Build chain instructions from resolved behavior.
|
|
225
226
|
* These are appended to the task to tell the agent what to read/write.
|
|
226
227
|
*/
|
|
228
|
+
export function writeInitialProgressFile(progressDir: string): void {
|
|
229
|
+
fs.writeFileSync(path.join(progressDir, "progress.md"), INITIAL_PROGRESS_CONTENT);
|
|
230
|
+
}
|
|
231
|
+
|
|
227
232
|
export function buildChainInstructions(
|
|
228
233
|
behavior: ResolvedStepBehavior,
|
|
229
234
|
chainDir: string,
|
package/slash-commands.ts
CHANGED
|
@@ -3,11 +3,12 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
5
5
|
import { Key, matchesKey } from "@mariozechner/pi-tui";
|
|
6
|
-
import { discoverAgents, discoverAgentsAll } from "./agents.ts";
|
|
6
|
+
import { discoverAgents, discoverAgentsAll, type ChainConfig } from "./agents.ts";
|
|
7
7
|
import { AgentManagerComponent, type ManagerResult } from "./agent-manager.ts";
|
|
8
8
|
import { SubagentsStatusComponent } from "./subagents-status.ts";
|
|
9
9
|
import { discoverAvailableSkills } from "./skills.ts";
|
|
10
10
|
import type { SubagentParamsLike } from "./subagent-executor.ts";
|
|
11
|
+
import { isParallelStep, type ChainStep } from "./settings.ts";
|
|
11
12
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
12
13
|
import {
|
|
13
14
|
applySlashUpdate,
|
|
@@ -107,6 +108,36 @@ const makeAgentCompletions = (state: SubagentState, multiAgent: boolean) => (pre
|
|
|
107
108
|
return agents.filter((a) => a.name.startsWith(lastWord)).map((a) => ({ value: `${beforeLastWord}${a.name}`, label: a.name }));
|
|
108
109
|
};
|
|
109
110
|
|
|
111
|
+
const discoverSavedChains = (cwd: string): ChainConfig[] => {
|
|
112
|
+
const chainsByName = new Map<string, ChainConfig>();
|
|
113
|
+
for (const chain of discoverAgentsAll(cwd).chains) {
|
|
114
|
+
chainsByName.set(chain.name, chain);
|
|
115
|
+
}
|
|
116
|
+
return Array.from(chainsByName.values());
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const makeChainCompletions = (state: SubagentState) => (prefix: string) => {
|
|
120
|
+
if (prefix.includes(" ")) return null;
|
|
121
|
+
return discoverSavedChains(state.baseCwd)
|
|
122
|
+
.filter((chain) => chain.name.startsWith(prefix))
|
|
123
|
+
.map((chain) => ({ value: chain.name, label: chain.name }));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const mapSavedChainSteps = (chain: ChainConfig, worktree = false): ChainStep[] => {
|
|
127
|
+
return (chain.steps as Array<ChainStep & { skills?: string[] | false }>).map((step) => {
|
|
128
|
+
if (isParallelStep(step)) return worktree ? { ...step, worktree: true } : { ...step };
|
|
129
|
+
return {
|
|
130
|
+
agent: step.agent,
|
|
131
|
+
task: step.task || undefined,
|
|
132
|
+
output: step.output,
|
|
133
|
+
reads: step.reads,
|
|
134
|
+
progress: step.progress,
|
|
135
|
+
skill: step.skill ?? step.skills,
|
|
136
|
+
model: step.model,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
110
141
|
async function requestSlashRun(
|
|
111
142
|
pi: ExtensionAPI,
|
|
112
143
|
ctx: ExtensionContext,
|
|
@@ -308,48 +339,31 @@ async function openAgentManager(
|
|
|
308
339
|
);
|
|
309
340
|
if (!result) return;
|
|
310
341
|
|
|
342
|
+
const launchOptions: SubagentParamsLike = {
|
|
343
|
+
clarify: !result.skipClarify && !result.background,
|
|
344
|
+
agentScope: "both",
|
|
345
|
+
...(result.fork ? { context: "fork" as const } : {}),
|
|
346
|
+
...(result.background ? { async: true } : {}),
|
|
347
|
+
};
|
|
348
|
+
|
|
311
349
|
if (result.action === "chain") {
|
|
312
350
|
const chain = result.agents.map((name, i) => ({
|
|
313
351
|
agent: name,
|
|
314
352
|
...(i === 0 ? { task: result.task } : {}),
|
|
315
353
|
}));
|
|
316
|
-
await runSlashSubagent(pi, ctx, {
|
|
317
|
-
chain,
|
|
318
|
-
task: result.task,
|
|
319
|
-
clarify: true,
|
|
320
|
-
agentScope: "both",
|
|
321
|
-
});
|
|
354
|
+
await runSlashSubagent(pi, ctx, { chain, task: result.task, ...launchOptions });
|
|
322
355
|
return;
|
|
323
356
|
}
|
|
324
357
|
|
|
325
358
|
if (result.action === "launch") {
|
|
326
|
-
await runSlashSubagent(pi, ctx, {
|
|
327
|
-
agent: result.agent,
|
|
328
|
-
task: result.task,
|
|
329
|
-
clarify: !result.skipClarify,
|
|
330
|
-
agentScope: "both",
|
|
331
|
-
});
|
|
359
|
+
await runSlashSubagent(pi, ctx, { agent: result.agent, task: result.task, ...launchOptions });
|
|
332
360
|
} else if (result.action === "launch-chain") {
|
|
333
|
-
|
|
334
|
-
agent: step.agent,
|
|
335
|
-
task: step.task || undefined,
|
|
336
|
-
output: step.output,
|
|
337
|
-
reads: step.reads,
|
|
338
|
-
progress: step.progress,
|
|
339
|
-
skill: step.skills,
|
|
340
|
-
model: step.model,
|
|
341
|
-
}));
|
|
342
|
-
await runSlashSubagent(pi, ctx, {
|
|
343
|
-
chain: chainParam,
|
|
344
|
-
task: result.task,
|
|
345
|
-
clarify: !result.skipClarify,
|
|
346
|
-
agentScope: "both",
|
|
347
|
-
});
|
|
361
|
+
await runSlashSubagent(pi, ctx, { chain: mapSavedChainSteps(result.chain, result.worktree), task: result.task, ...launchOptions });
|
|
348
362
|
} else if (result.action === "parallel") {
|
|
349
363
|
await runSlashSubagent(pi, ctx, {
|
|
350
364
|
tasks: result.tasks,
|
|
351
|
-
|
|
352
|
-
|
|
365
|
+
...launchOptions,
|
|
366
|
+
...(result.worktree ? { worktree: true } : {}),
|
|
353
367
|
});
|
|
354
368
|
}
|
|
355
369
|
}
|
|
@@ -493,21 +507,53 @@ export function registerSlashCommands(
|
|
|
493
507
|
},
|
|
494
508
|
});
|
|
495
509
|
|
|
510
|
+
pi.registerCommand("run-chain", {
|
|
511
|
+
description: "Run a saved chain: /run-chain chainName -- task [--bg] [--fork]",
|
|
512
|
+
getArgumentCompletions: makeChainCompletions(state),
|
|
513
|
+
handler: async (args, ctx) => {
|
|
514
|
+
const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
|
|
515
|
+
const delimiterIndex = cleanedArgs.indexOf(" -- ");
|
|
516
|
+
const usage = "Usage: /run-chain <chainName> -- <task> [--bg] [--fork]";
|
|
517
|
+
if (delimiterIndex === -1) {
|
|
518
|
+
ctx.ui.notify(usage, "error");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const chainName = cleanedArgs.slice(0, delimiterIndex).trim();
|
|
522
|
+
const task = cleanedArgs.slice(delimiterIndex + 4).trim();
|
|
523
|
+
if (!chainName || !task) {
|
|
524
|
+
ctx.ui.notify(usage, "error");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const chain = discoverSavedChains(state.baseCwd).find((candidate) => candidate.name === chainName);
|
|
528
|
+
if (!chain) {
|
|
529
|
+
ctx.ui.notify(`Unknown chain: ${chainName}`, "error");
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const params: SubagentParamsLike = { chain: mapSavedChainSteps(chain), task, clarify: false, agentScope: "both" };
|
|
533
|
+
if (bg) params.async = true;
|
|
534
|
+
if (fork) params.context = "fork";
|
|
535
|
+
await runSlashSubagent(pi, ctx, params);
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
496
539
|
pi.registerCommand("parallel", {
|
|
497
540
|
description: "Run agents in parallel: /parallel scout \"task1\" -> reviewer \"task2\" [--bg] [--fork]",
|
|
498
541
|
getArgumentCompletions: makeAgentCompletions(state, true),
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
})
|
|
509
|
-
|
|
510
|
-
|
|
542
|
+
handler: async (args, ctx) => {
|
|
543
|
+
const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
|
|
544
|
+
const parsed = parseAgentArgs(state, cleanedArgs, "parallel", ctx);
|
|
545
|
+
if (!parsed) return;
|
|
546
|
+
const tasks = parsed.steps.map(({ name, config, task: stepTask }) => ({
|
|
547
|
+
agent: name,
|
|
548
|
+
task: stepTask ?? parsed.task,
|
|
549
|
+
...(config.output !== undefined ? { output: config.output } : {}),
|
|
550
|
+
...(config.reads !== undefined ? { reads: config.reads } : {}),
|
|
551
|
+
...(config.model ? { model: config.model } : {}),
|
|
552
|
+
...(config.skill !== undefined ? { skill: config.skill } : {}),
|
|
553
|
+
...(config.progress !== undefined ? { progress: config.progress } : {}),
|
|
554
|
+
}));
|
|
555
|
+
const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
|
|
556
|
+
if (bg) params.async = true;
|
|
511
557
|
if (fork) params.context = "fork";
|
|
512
558
|
await runSlashSubagent(pi, ctx, params);
|
|
513
559
|
},
|
|
@@ -523,6 +569,13 @@ export function registerSlashCommands(
|
|
|
523
569
|
},
|
|
524
570
|
});
|
|
525
571
|
|
|
572
|
+
pi.registerCommand("subagents-doctor", {
|
|
573
|
+
description: "Show subagent diagnostics",
|
|
574
|
+
handler: async (_args, ctx) => {
|
|
575
|
+
await runSlashSubagent(pi, ctx, { action: "doctor" });
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
|
|
526
579
|
pi.registerShortcut("ctrl+shift+a", {
|
|
527
580
|
handler: async (ctx) => {
|
|
528
581
|
await openAgentManager(pi, ctx);
|
package/slash-live-state.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import type { Message } from "@mariozechner/pi-ai";
|
|
3
|
-
import type { SubagentParamsLike } from "./subagent-executor.
|
|
4
|
-
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.
|
|
5
|
-
import { type Details, type SingleResult, type Usage, SLASH_RESULT_TYPE } from "./types.
|
|
3
|
+
import type { SubagentParamsLike } from "./subagent-executor.ts";
|
|
4
|
+
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
5
|
+
import { type Details, type SingleResult, type Usage, SLASH_RESULT_TYPE } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
export interface SlashMessageDetails {
|
|
8
8
|
requestId: string;
|