pi-subagents 0.19.0 → 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 +6 -0
- package/doctor.ts +198 -0
- package/index.ts +4 -1
- package/intercom-bridge.ts +58 -6
- package/package.json +1 -1
- package/schemas.ts +1 -1
- package/slash-commands.ts +83 -29
- package/slash-live-state.ts +3 -3
- package/subagent-executor.ts +35 -1
- package/subagents-status.ts +13 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.19.1] - 2026-04-26
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added `subagent({ action: "doctor" })` and `/subagents-doctor` for read-only subagent environment diagnostics.
|
|
9
|
+
- Added `/run-chain` to launch saved `.chain.md` workflows directly from slash commands with completion, shared task input, and `--bg`/`--fork` support.
|
|
10
|
+
|
|
5
11
|
## [0.19.0] - 2026-04-26
|
|
6
12
|
|
|
7
13
|
### Added
|
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/index.ts
CHANGED
|
@@ -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
|
@@ -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
package/schemas.ts
CHANGED
|
@@ -106,7 +106,7 @@ export const SubagentParams = Type.Object({
|
|
|
106
106
|
task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
|
|
107
107
|
// Management action (when present, tool operates in management mode)
|
|
108
108
|
action: Type.Optional(Type.String({
|
|
109
|
-
description: "Action:
|
|
109
|
+
description: "Action: 'list', 'get', 'create', 'update', 'delete', 'status', 'interrupt', or 'doctor'. Omit for execution mode."
|
|
110
110
|
})),
|
|
111
111
|
id: Type.Optional(Type.String({
|
|
112
112
|
description: "Run id or prefix for action='status' or action='interrupt'."
|
package/slash-commands.ts
CHANGED
|
@@ -3,7 +3,7 @@ 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";
|
|
@@ -108,6 +108,36 @@ const makeAgentCompletions = (state: SubagentState, multiAgent: boolean) => (pre
|
|
|
108
108
|
return agents.filter((a) => a.name.startsWith(lastWord)).map((a) => ({ value: `${beforeLastWord}${a.name}`, label: a.name }));
|
|
109
109
|
};
|
|
110
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
|
+
|
|
111
141
|
async function requestSlashRun(
|
|
112
142
|
pi: ExtensionAPI,
|
|
113
143
|
ctx: ExtensionContext,
|
|
@@ -328,19 +358,7 @@ async function openAgentManager(
|
|
|
328
358
|
if (result.action === "launch") {
|
|
329
359
|
await runSlashSubagent(pi, ctx, { agent: result.agent, task: result.task, ...launchOptions });
|
|
330
360
|
} else if (result.action === "launch-chain") {
|
|
331
|
-
|
|
332
|
-
if (isParallelStep(step)) return result.worktree ? { ...step, worktree: true } : { ...step };
|
|
333
|
-
return {
|
|
334
|
-
agent: step.agent,
|
|
335
|
-
task: step.task || undefined,
|
|
336
|
-
output: step.output,
|
|
337
|
-
reads: step.reads,
|
|
338
|
-
progress: step.progress,
|
|
339
|
-
skill: step.skill ?? (step as typeof step & { skills?: string[] | false }).skills,
|
|
340
|
-
model: step.model,
|
|
341
|
-
};
|
|
342
|
-
});
|
|
343
|
-
await runSlashSubagent(pi, ctx, { chain: chainParam, task: result.task, ...launchOptions });
|
|
361
|
+
await runSlashSubagent(pi, ctx, { chain: mapSavedChainSteps(result.chain, result.worktree), task: result.task, ...launchOptions });
|
|
344
362
|
} else if (result.action === "parallel") {
|
|
345
363
|
await runSlashSubagent(pi, ctx, {
|
|
346
364
|
tasks: result.tasks,
|
|
@@ -489,24 +507,53 @@ export function registerSlashCommands(
|
|
|
489
507
|
},
|
|
490
508
|
});
|
|
491
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
|
+
|
|
492
539
|
pi.registerCommand("parallel", {
|
|
493
540
|
description: "Run agents in parallel: /parallel scout \"task1\" -> reviewer \"task2\" [--bg] [--fork]",
|
|
494
541
|
getArgumentCompletions: makeAgentCompletions(state, true),
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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;
|
|
510
557
|
if (fork) params.context = "fork";
|
|
511
558
|
await runSlashSubagent(pi, ctx, params);
|
|
512
559
|
},
|
|
@@ -522,6 +569,13 @@ export function registerSlashCommands(
|
|
|
522
569
|
},
|
|
523
570
|
});
|
|
524
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
|
+
|
|
525
579
|
pi.registerShortcut("ctrl+shift+a", {
|
|
526
580
|
handler: async (ctx) => {
|
|
527
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;
|
package/subagent-executor.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { ChainClarifyComponent, type ChainClarifyResult, type ModelInfo } from "
|
|
|
9
9
|
import { executeChain } from "./chain-execution.ts";
|
|
10
10
|
import { resolveExecutionAgentScope } from "./agent-scope.ts";
|
|
11
11
|
import { handleManagementAction } from "./agent-management.ts";
|
|
12
|
+
import { buildDoctorReport } from "./doctor.ts";
|
|
12
13
|
import { runSync } from "./execution.ts";
|
|
13
14
|
import { resolveModelCandidate } from "./model-fallback.ts";
|
|
14
15
|
import { aggregateParallelOutputs } from "./parallel-utils.ts";
|
|
@@ -1544,6 +1545,39 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1544
1545
|
const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd);
|
|
1545
1546
|
const paramsWithResolvedCwd = params.cwd === undefined ? params : { ...params, cwd: requestCwd };
|
|
1546
1547
|
if (params.action) {
|
|
1548
|
+
if (params.action === "doctor") {
|
|
1549
|
+
let currentSessionFile: string | null = null;
|
|
1550
|
+
let currentSessionId = deps.state.currentSessionId;
|
|
1551
|
+
let sessionError: string | undefined;
|
|
1552
|
+
try {
|
|
1553
|
+
currentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
|
|
1554
|
+
currentSessionId = ctx.sessionManager.getSessionId();
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
1557
|
+
}
|
|
1558
|
+
let orchestratorTarget: string | undefined;
|
|
1559
|
+
try {
|
|
1560
|
+
orchestratorTarget = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
|
|
1561
|
+
} catch {}
|
|
1562
|
+
return {
|
|
1563
|
+
content: [{
|
|
1564
|
+
type: "text",
|
|
1565
|
+
text: buildDoctorReport({
|
|
1566
|
+
cwd: requestCwd,
|
|
1567
|
+
config: deps.config,
|
|
1568
|
+
state: deps.state,
|
|
1569
|
+
context: paramsWithResolvedCwd.context,
|
|
1570
|
+
requestedSessionDir: paramsWithResolvedCwd.sessionDir,
|
|
1571
|
+
currentSessionFile,
|
|
1572
|
+
currentSessionId,
|
|
1573
|
+
orchestratorTarget,
|
|
1574
|
+
sessionError,
|
|
1575
|
+
expandTilde: deps.expandTilde,
|
|
1576
|
+
}),
|
|
1577
|
+
}],
|
|
1578
|
+
details: { mode: "management", results: [] },
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1547
1581
|
if (params.action === "status") {
|
|
1548
1582
|
const foreground = getForegroundControl(deps.state, paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId);
|
|
1549
1583
|
if (foreground) return foregroundStatusResult(foreground);
|
|
@@ -1576,7 +1610,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
|
|
|
1576
1610
|
details: { mode: "management", results: [] },
|
|
1577
1611
|
};
|
|
1578
1612
|
}
|
|
1579
|
-
const validActions = ["list", "get", "create", "update", "delete", "status", "interrupt"];
|
|
1613
|
+
const validActions = ["list", "get", "create", "update", "delete", "status", "interrupt", "doctor"];
|
|
1580
1614
|
if (!validActions.includes(params.action)) {
|
|
1581
1615
|
return {
|
|
1582
1616
|
content: [{ type: "text", text: `Unknown action: ${params.action}. Valid: ${validActions.join(", ")}` }],
|
package/subagents-status.ts
CHANGED
|
@@ -3,10 +3,10 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
4
4
|
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
5
5
|
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
6
|
-
import { type AsyncRunOverlayData, type AsyncRunSummary, listAsyncRunsForOverlay } from "./async-status.
|
|
7
|
-
import { ASYNC_DIR } from "./types.
|
|
8
|
-
import { formatDuration, formatTokens, shortenPath } from "./formatters.
|
|
9
|
-
import { formatScrollInfo, renderFooter, renderHeader, row } from "./render-helpers.
|
|
6
|
+
import { type AsyncRunOverlayData, type AsyncRunSummary, listAsyncRunsForOverlay } from "./async-status.ts";
|
|
7
|
+
import { ASYNC_DIR } from "./types.ts";
|
|
8
|
+
import { formatDuration, formatTokens, shortenPath } from "./formatters.ts";
|
|
9
|
+
import { formatScrollInfo, renderFooter, renderHeader, row } from "./render-helpers.ts";
|
|
10
10
|
|
|
11
11
|
const AUTO_REFRESH_MS = 2000;
|
|
12
12
|
const DETAIL_EVENT_LIMIT = 8;
|
|
@@ -178,13 +178,19 @@ export class SubagentsStatusComponent implements Component {
|
|
|
178
178
|
private recent: AsyncRunSummary[] = [];
|
|
179
179
|
private rows: StatusRow[] = [];
|
|
180
180
|
private errorMessage?: string;
|
|
181
|
+
private tui: TUI;
|
|
182
|
+
private theme: Theme;
|
|
183
|
+
private done: () => void;
|
|
181
184
|
|
|
182
185
|
constructor(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
+
tui: TUI,
|
|
187
|
+
theme: Theme,
|
|
188
|
+
done: () => void,
|
|
186
189
|
deps: StatusOverlayDeps = {},
|
|
187
190
|
) {
|
|
191
|
+
this.tui = tui;
|
|
192
|
+
this.theme = theme;
|
|
193
|
+
this.done = done;
|
|
188
194
|
this.listRunsForOverlay = deps.listRunsForOverlay ?? listAsyncRunsForOverlay;
|
|
189
195
|
const refreshMs = deps.refreshMs ?? AUTO_REFRESH_MS;
|
|
190
196
|
this.reload();
|