pi-subagents 0.19.0 → 0.19.2

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/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.2",
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",
@@ -0,0 +1,13 @@
1
+ ---
2
+ description: Use subagents to gather context, then ask clarifying questions
3
+ ---
4
+
5
+ Based on our discussion and my intent, launch focused context-gathering subagents before planning or implementing.
6
+
7
+ Use `scout` to inspect the relevant local files, existing patterns, constraints, tests, and likely integration points. Use `researcher` when external docs, recent sources, ecosystem context, or primary evidence would improve the answer.
8
+
9
+ Give each subagent a specific meta prompt. Ask them to return concise findings plus the remaining clarification questions that matter for implementation confidence.
10
+
11
+ After they return, synthesize what we know and use the `interview` tool to ask me the unresolved questions needed to reach a shared understanding.
12
+
13
+ $@
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: Send an explicitly approved task to oracle-executor with full inherited context.
3
+ subagent: oracle-executor
4
+ inheritContext: true
5
+ ---
6
+
7
+ Launch the oracle-executor subagent with a strict implementation meta prompt that points it at the plan and asks it to execute:
8
+
9
+ $@
@@ -0,0 +1,50 @@
1
+ ---
2
+ description: Parallel subagents research
3
+ ---
4
+
5
+ Launch parallel research subagents to build a grounded answer to the current question or decision.
6
+
7
+ Use fresh context, not forked context, unless I explicitly ask for forked context. Researchers and scouts should inspect sources directly instead of relying on the main conversation history.
8
+
9
+ Use a combination of `researcher` and `scout` subagents:
10
+ - Use `researcher` for web, docs, standards, ecosystem, recent changes, benchmarks, and primary-source evidence.
11
+ - Use `scout` for local codebase context, existing implementation patterns, repo constraints, and files that would be affected.
12
+
13
+ Give each subagent a distinct angle. Unless I specify angles, use these three:
14
+
15
+ 1. External evidence
16
+ Use `researcher` to find current, authoritative sources: official docs, specs, release notes, benchmarks, issue threads, or primary explanations.
17
+
18
+ 2. Local code context
19
+ Use `scout` to inspect the repository for relevant files, existing patterns, constraints, tests, and likely integration points.
20
+
21
+ 3. Practical tradeoffs
22
+ Use `researcher` or `scout`, whichever fits the question, to compare options, risks, edge cases, maintenance cost, and what would be easiest to validate.
23
+
24
+ Adapt the angles when the question calls for it:
25
+ - Library/API questions: include official docs and recent examples.
26
+ - Architecture decisions: include local module boundaries, dependency direction, and migration cost.
27
+ - Debugging questions: include likely failure modes, local call paths, and exact error evidence.
28
+ - UI/product questions: include user flow, accessibility, design precedent, and implementation constraints.
29
+ - Time-sensitive topics: include a recent-developments angle and prefer 2026/2025 sources.
30
+
31
+ Prefer two or three strong subagents over many vague ones. The parent agent should frame the question and assign angles; the child agents should research or scout, not invent broad plans.
32
+
33
+ Ask each subagent to return concise findings with evidence:
34
+ - file paths and line ranges for local findings
35
+ - source links for external findings
36
+ - confidence level and gaps
37
+ - recommended next step or decision implication
38
+
39
+ Do not ask subagents to edit files. This is a research pass only unless I explicitly ask for implementation.
40
+
41
+ After the subagents return, synthesize the answer into:
42
+ - what we know
43
+ - what the local codebase implies
44
+ - tradeoffs and risks
45
+ - gaps or assumptions
46
+ - the recommended next move
47
+
48
+ If findings disagree, call out the disagreement instead of smoothing it over.
49
+
50
+ $@
@@ -1,8 +1,38 @@
1
1
  ---
2
2
  description: Parallel subagents review
3
3
  ---
4
- Great. Now let's launch parallel reviewers to conduct an adversarial review.
5
4
 
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.
5
+ Launch parallel reviewers for an adversarial review of the current work.
6
+
7
+ Use fresh context, not forked context, unless I explicitly ask for forked context. Reviewers should inspect the repository, relevant instructions, and current diff directly from files and commands. Do not rely on the main conversation history.
8
+
9
+ Give each reviewer a distinct angle. Unless I specify angles, use these three:
10
+
11
+ 1. Correctness and regressions
12
+ Check whether the change satisfies the request, preserves existing behavior, handles edge cases, and avoids hidden runtime failures.
13
+
14
+ 2. Tests and validation
15
+ Check whether tests or validation were added at the right layer, whether assertions are meaningful, and whether the chosen verification commands are enough.
16
+
17
+ 3. Simplicity and maintainability
18
+ Check for unnecessary complexity, duplicate structure, single-use wrappers, brittle abstractions, confusing names, verbosity, and cleanup that is clearly worth doing.
19
+
20
+ Adapt the angles when the work calls for it:
21
+ - TypeScript-heavy changes: include type safety, source-of-truth types, casts, and error-boundary discipline.
22
+ - UI-heavy changes: include UX, accessibility, copy, and visual quality.
23
+ - Security-sensitive changes: include unsafe input/output handling, auth boundaries, privacy, and data exposure.
24
+ - Docs-heavy changes: include clarity, accuracy, completeness, reader flow, and non-robotic prose.
25
+ - Large multi-file changes: consider a fourth reviewer for structural friction, module boundaries, and testability.
26
+
27
+ Prefer three strong reviewers over many vague reviewers.
28
+
29
+ Give every reviewer a specific task prompt naming its angle. Ask reviewers to return concise, evidence-backed findings with file/line references and suggested fixes. The response should be review feedback, not a context summary. Reviewers must not edit files unless I explicitly ask for a writer pass.
30
+
31
+ While reviewers run, do your own narrow inspection if useful. After they return, synthesize the feedback into:
32
+ - fixes worth doing now
33
+ - optional improvements
34
+ - feedback to ignore or defer, with a short reason
35
+
36
+ Do not blindly apply every reviewer suggestion. Ask before applying fixes unless I already told you to address review feedback.
7
37
 
8
38
  $@
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'."