@bacnh85/pi-subagent 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@
2
2
  name: general-purpose
3
3
  description: General-purpose sub-agent for any delegated task. Use when no specialized agent fits. Good for complex research, multi-step operations, and code modifications.
4
4
  tools: read, bash, edit, write, grep, find, ls
5
+ color: yellow
5
6
  ---
6
7
 
7
8
  You are a capable coding assistant running as a sub-agent. Complete the delegated task efficiently and return a concise summary of your findings or changes.
@@ -3,6 +3,8 @@ name: reviewer
3
3
  description: Code review specialist. Use for correctness, security, regression, and meaningful test-gap review.
4
4
  tools: read, grep, find, ls
5
5
  thinking: high
6
+ color: purple
7
+ sandbox: read-only
6
8
  ---
7
9
 
8
10
  You are an independent senior code reviewer. Inspect the requested Git scope with read-only tools.
package/agents/scout.md CHANGED
@@ -3,6 +3,8 @@ name: scout
3
3
  description: Fast codebase recon that returns compressed context for handoff. Use for finding files, understanding structure, locating symbols.
4
4
  tools: read, grep, find, ls
5
5
  thinking: low
6
+ color: cyan
7
+ sandbox: read-only
6
8
  ---
7
9
 
8
10
  You are a scout. Quickly investigate a codebase and return structured findings that another agent can use without re-reading everything.
package/agents/worker.md CHANGED
@@ -2,6 +2,7 @@
2
2
  name: worker
3
3
  description: General-purpose coding agent with full tool access. Use only when explicitly requested for isolated implementation.
4
4
  thinking: medium
5
+ color: green
5
6
  ---
6
7
 
7
8
  You are a skilled software engineer. Implement the requested task with care and precision.
@@ -13,12 +13,16 @@ import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/
13
13
 
14
14
  export type AgentScope = "user" | "project" | "both";
15
15
 
16
+ export type AgentColor = "red" | "blue" | "green" | "yellow" | "purple" | "orange" | "pink" | "cyan";
17
+
16
18
  export interface AgentConfig {
17
19
  name: string;
18
20
  description: string;
19
21
  tools?: string[];
20
22
  model?: string;
21
23
  thinking?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
24
+ sandbox?: "read-only" | "workspace-write";
25
+ color?: AgentColor;
22
26
  systemPrompt: string;
23
27
  source: "user" | "project" | "bundled";
24
28
  filePath: string;
@@ -160,6 +164,26 @@ function loadAgentsFromDir(
160
164
  }
161
165
  }
162
166
 
167
+ if (typeof frontmatter.sandbox === "string" && frontmatter.sandbox) {
168
+ const validSandboxes = ["read-only", "workspace-write"];
169
+ if (!validSandboxes.includes(frontmatter.sandbox)) {
170
+ diagnostics.push({
171
+ filePath,
172
+ issue: `Invalid sandbox mode "${frontmatter.sandbox}". Valid values: ${validSandboxes.join(", ")}. Using default.`,
173
+ severity: "warn",
174
+ });
175
+ }
176
+ }
177
+
178
+ const VALID_COLORS = ["red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan"] as const;
179
+ if (typeof frontmatter.color === "string" && frontmatter.color && !VALID_COLORS.includes(frontmatter.color as any)) {
180
+ diagnostics.push({
181
+ filePath,
182
+ issue: `Invalid color "${frontmatter.color}". Valid values: ${VALID_COLORS.join(", ")}. Ignoring.`,
183
+ severity: "warn",
184
+ });
185
+ }
186
+
163
187
  agents.push({
164
188
  name: frontmatter.name,
165
189
  description: frontmatter.description,
@@ -168,6 +192,12 @@ function loadAgentsFromDir(
168
192
  thinking: typeof frontmatter.thinking === "string" && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(frontmatter.thinking)
169
193
  ? frontmatter.thinking as AgentConfig["thinking"]
170
194
  : undefined,
195
+ sandbox: typeof frontmatter.sandbox === "string" && ["read-only", "workspace-write"].includes(frontmatter.sandbox)
196
+ ? frontmatter.sandbox as "read-only" | "workspace-write"
197
+ : undefined,
198
+ color: typeof frontmatter.color === "string" && VALID_COLORS.includes(frontmatter.color as any)
199
+ ? frontmatter.color as AgentColor
200
+ : undefined,
171
201
  systemPrompt: body,
172
202
  source,
173
203
  filePath,
@@ -153,21 +153,34 @@ export default function (pi: ExtensionAPI) {
153
153
  threadStore.clear();
154
154
  });
155
155
 
156
- // Proactively steer agents toward sub-agent delegation when users mention it
157
- pi.on("before_agent_start", async (event) => {
158
- const prompt = event.prompt.toLowerCase();
159
- if (/\b(delegate to|use a subagent|run in parallel|spawn an agent|scout|review this|chain|worker agent)\b/.test(prompt)) {
160
- return {
161
- systemPrompt:
162
- event.systemPrompt +
163
- "\n\nThe subagent tool is available for delegating tasks to specialized agents with isolated context. Use /subagent to list available agents. Bundled: scout (fast recon), reviewer (code review), worker (implementation), general-purpose (fallback). Modes: single, parallel (max 8), chain.",
164
- };
165
- }
166
- });
167
-
168
156
  // Resolve bundled agents directory relative to this extension file
169
157
  const bundledAgentsDir = path.resolve(__dirname, "../agents");
170
158
 
159
+ // Inject available agent catalog into system prompt for semantic auto-delegation
160
+ pi.on("before_agent_start", async (event) => {
161
+ const ctx = currentCtx;
162
+ const discovery = discoverAgents(event.cwd ?? ctx?.cwd ?? process.cwd(), "both", bundledAgentsDir);
163
+ const catalog = discovery.agents
164
+ .map((a) => {
165
+ const modelInfo = a.model ? ` (model: ${a.model})` : " (inherits parent)";
166
+ const thinkingInfo = a.thinking ? `, thinking: ${a.thinking}` : "";
167
+ const sandboxInfo = a.sandbox ? `, sandbox: ${a.sandbox}` : "";
168
+ return `- **${a.name}**: ${a.description}${modelInfo}${thinkingInfo}${sandboxInfo}`;
169
+ })
170
+ .join("\n");
171
+ return {
172
+ systemPrompt:
173
+ event.systemPrompt +
174
+ `\n\n## Available Subagents\n${catalog}\n\n` +
175
+ "The subagent tool can delegate tasks to these specialized agents with isolated context. " +
176
+ "Use for read-heavy exploration, parallel analysis, or work that would flood the main context.\n" +
177
+ "Prefer **scout** for fast read-only exploration. " +
178
+ "Prefer **reviewer** for code review (high thinking, read-only). " +
179
+ "Prefer **worker** for implementation (medium thinking, all tools). " +
180
+ "Modes: single, parallel (max 8 tasks, 4 concurrent), chain.",
181
+ };
182
+ });
183
+
171
184
  // Public one-request/one-response service used by pi-review.
172
185
  pi.events.on(SUBAGENT_REQUEST_EVENT, (raw) => {
173
186
  const request = raw as SubagentRunRequest;
@@ -179,7 +192,7 @@ export default function (pi: ExtensionAPI) {
179
192
  request.respond({ id: request.id, ok: false, error: `Unknown agent: ${request.agent}` });
180
193
  return;
181
194
  }
182
- const thread = threadStore.createThread({ agentName: agent.name, task: request.task, mode: "single" });
195
+ const thread = threadStore.createThread({ agentName: agent.name, task: request.task, mode: "single", color: agent.color });
183
196
  void runNamedAgent({
184
197
  agent: request.readOnly ? { ...agent, tools: ["read", "grep", "find", "ls"] } : agent,
185
198
  task: request.task,
@@ -541,6 +554,7 @@ export default function (pi: ExtensionAPI) {
541
554
  task: taskWithContext,
542
555
  mode: "chain-step",
543
556
  toolCallId: _toolCallId,
557
+ color: agents.find(a => a.name === step.agent)?.color,
544
558
  });
545
559
  const result = await runOne(
546
560
  step.agent, taskWithContext, step.cwd,
@@ -632,6 +646,7 @@ export default function (pi: ExtensionAPI) {
632
646
  task: t.task,
633
647
  mode: "parallel-task",
634
648
  toolCallId: _toolCallId,
649
+ color: agents.find(a => a.name === t.agent)?.color,
635
650
  }),
636
651
  );
637
652
 
@@ -746,6 +761,7 @@ export default function (pi: ExtensionAPI) {
746
761
  task: params.task,
747
762
  mode: "single",
748
763
  toolCallId: _toolCallId,
764
+ color: agents.find(a => a.name === params.agent)?.color,
749
765
  });
750
766
  const result = await runOne(
751
767
  params.agent, params.task, params.cwd,
@@ -798,6 +814,14 @@ export default function (pi: ExtensionAPI) {
798
814
  // TUI rendering
799
815
  // ------------------------------------------------------------------
800
816
 
817
+ /** Look up agent color by name for TUI rendering. */
818
+ const resolveAgentColor = (name: string): string => {
819
+ const ctx = currentCtx;
820
+ if (!ctx) return "accent";
821
+ const found = discoverAgents(ctx.cwd, "both", bundledAgentsDir).agents.find(a => a.name === name);
822
+ return found?.color ?? "accent";
823
+ };
824
+
801
825
  renderCall(args, theme, _context) {
802
826
  const scope: AgentScope = args.agentScope ?? "user";
803
827
  const fg = theme.fg.bind(theme);
@@ -816,7 +840,7 @@ export default function (pi: ExtensionAPI) {
816
840
  "\n " +
817
841
  fg("muted", `${i + 1}.`) +
818
842
  " " +
819
- fg("accent", step.agent) +
843
+ fg(resolveAgentColor(step.agent), step.agent) +
820
844
  fg("dim", ` ${preview}`);
821
845
  }
822
846
  if (args.chain.length > 3)
@@ -832,7 +856,7 @@ export default function (pi: ExtensionAPI) {
832
856
  fg("muted", ` [${scope}]`);
833
857
  for (const t of args.tasks.slice(0, 3)) {
834
858
  const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
835
- text += `\n ${fg("accent", t.agent)}${fg("dim", ` ${preview}`)}`;
859
+ text += `\n ${fg(resolveAgentColor(t.agent), t.agent)}${fg("dim", ` ${preview}`)}`;
836
860
  }
837
861
  if (args.tasks.length > 3)
838
862
  text += `\n ${fg("muted", `... +${args.tasks.length - 3} more`)}`;
@@ -848,7 +872,7 @@ export default function (pi: ExtensionAPI) {
848
872
  : "...";
849
873
  let text =
850
874
  fg("toolTitle", theme.bold("subagent ")) +
851
- fg("accent", agentName) +
875
+ fg(resolveAgentColor(agentName), agentName) +
852
876
  fg("muted", ` [${scope}]`);
853
877
  text += `\n ${fg("dim", preview)}`;
854
878
  return new Text(text, 0, 0);
@@ -895,7 +919,7 @@ export default function (pi: ExtensionAPI) {
895
919
  container.addChild(
896
920
  new Text(
897
921
  fg("muted", `─── Step ${r.exitCode !== -1 ? "" : "?"}: `) +
898
- fg("accent", r.agent) +
922
+ fg(resolveAgentColor(r.agent), r.agent) +
899
923
  ` ${stepIcon}`,
900
924
  0,
901
925
  0,
@@ -930,7 +954,8 @@ export default function (pi: ExtensionAPI) {
930
954
  fg("accent", `${successCount}/${details.results.length} steps`);
931
955
  for (const r of details.results) {
932
956
  const stepIcon = isFailedResult(r) ? fg("error", "✗") : fg("success", "✓");
933
- text += `\n ${stepIcon} ${fg("accent", r.agent)}`;
957
+ const color = resolveAgentColor(r.agent);
958
+ text += `\n ${stepIcon} ${fg(color, r.agent)}`;
934
959
  }
935
960
  const totalUsage = formatUsageStats(aggregateUsage(details.results));
936
961
  if (totalUsage) text += `\n${fg("dim", totalUsage)}`;
@@ -973,7 +998,7 @@ export default function (pi: ExtensionAPI) {
973
998
  : fg("success", "✓");
974
999
  container.addChild(
975
1000
  new Text(
976
- fg("muted", "─── ") + fg("accent", r.agent) + ` ${taskIcon}`,
1001
+ fg("muted", "─── ") + fg(resolveAgentColor(r.agent), r.agent) + ` ${taskIcon}`,
977
1002
  0,
978
1003
  0,
979
1004
  ),
@@ -1013,7 +1038,7 @@ export default function (pi: ExtensionAPI) {
1013
1038
  : isFailedResult(r)
1014
1039
  ? fg("error", "✗")
1015
1040
  : fg("success", "✓");
1016
- text += `\n ${taskIcon} ${fg("accent", r.agent)}`;
1041
+ text += `\n ${taskIcon} ${fg(resolveAgentColor(r.agent), r.agent)}`;
1017
1042
  }
1018
1043
  if (!isRunning) {
1019
1044
  const totalUsage = formatUsageStats(aggregateUsage(details.results));
@@ -123,7 +123,8 @@ export class ThreadViewer {
123
123
  else if (this.thread.mode === "chain-step") modeLabel = t.fg("muted", " [chain]");
124
124
 
125
125
  // Header
126
- let header = `${icon} ${t.fg("toolTitle", t.bold(this.thread.agentName))}${modeLabel}`;
126
+ const agentColor = this.thread.color ?? "accent";
127
+ let header = `${icon} ${t.fg(agentColor, t.bold(this.thread.agentName))}${modeLabel}`;
127
128
  if (status === "running") header += ` ${t.fg("warning", "(running...)")}`;
128
129
  else if (status === "aborted") header += ` ${t.fg("error", "[aborted]")}`;
129
130
  if (result && isErr && result.stopReason && result.stopReason !== "error" && result.stopReason !== "aborted") {
@@ -23,6 +23,7 @@ export interface SubagentThread {
23
23
  status: ThreadStatus;
24
24
  result?: SubAgentResult;
25
25
  toolCallId?: string;
26
+ color?: string;
26
27
  createdAt: number;
27
28
  updatedAt: number;
28
29
  }
@@ -53,6 +54,7 @@ export class ThreadStore {
53
54
  task: string;
54
55
  mode: ThreadMode;
55
56
  toolCallId?: string;
57
+ color?: string;
56
58
  }): SubagentThread {
57
59
  const id = cryptoGenId();
58
60
  const now = Date.now();
@@ -63,6 +65,7 @@ export class ThreadStore {
63
65
  mode: params.mode,
64
66
  status: "running",
65
67
  toolCallId: params.toolCallId,
68
+ color: params.color,
66
69
  createdAt: now,
67
70
  updatedAt: now,
68
71
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-subagent",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "In-process subagents for Pi with isolated SDK sessions, parallel and chained delegation, and inspectable threads.",
5
5
  "type": "module",
6
6
  "license": "MIT",