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 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) {
@@ -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 intercomEnabled(configPath: string): boolean {
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
- console.warn(`Failed to parse intercom config at '${configPath}'. Assuming enabled.`, error);
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
- if (!intercomEnabled(configPath)) {
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.19.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",
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: management ('list','get','create','update','delete') or control ('status','interrupt'). Omit for execution mode."
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
- const chainParam = (result.chain.steps as unknown as ChainStep[]).map((step) => {
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
- handler: async (args, ctx) => {
496
- const { args: cleanedArgs, bg, fork } = extractExecutionFlags(args);
497
- const parsed = parseAgentArgs(state, cleanedArgs, "parallel", ctx);
498
- if (!parsed) return;
499
- const tasks = parsed.steps.map(({ name, config, task: stepTask }) => ({
500
- agent: name,
501
- task: stepTask ?? parsed.task,
502
- ...(config.output !== undefined ? { output: config.output } : {}),
503
- ...(config.reads !== undefined ? { reads: config.reads } : {}),
504
- ...(config.model ? { model: config.model } : {}),
505
- ...(config.skill !== undefined ? { skill: config.skill } : {}),
506
- ...(config.progress !== undefined ? { progress: config.progress } : {}),
507
- }));
508
- const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
509
- if (bg) params.async = true;
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);
@@ -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.js";
4
- import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.js";
5
- import { type Details, type SingleResult, type Usage, SLASH_RESULT_TYPE } from "./types.js";
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;
@@ -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(", ")}` }],
@@ -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.js";
7
- import { ASYNC_DIR } from "./types.js";
8
- import { formatDuration, formatTokens, shortenPath } from "./formatters.js";
9
- import { formatScrollInfo, renderFooter, renderHeader, row } from "./render-helpers.js";
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
- private tui: TUI,
184
- private theme: Theme,
185
- private done: () => void,
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();