pi-subagents 0.20.0 → 0.21.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +21 -4
  3. package/agent-management.ts +19 -9
  4. package/agent-manager-chain-detail.ts +1 -1
  5. package/agent-manager-detail.ts +2 -0
  6. package/agent-manager-edit.ts +31 -6
  7. package/agent-manager-parallel.ts +2 -2
  8. package/agent-manager.ts +4 -2
  9. package/agent-serializer.ts +2 -0
  10. package/agents/context-builder.md +13 -6
  11. package/agents/delegate.md +2 -0
  12. package/agents/oracle.md +4 -1
  13. package/agents/planner.md +5 -1
  14. package/agents/researcher.md +4 -1
  15. package/agents/reviewer.md +78 -20
  16. package/agents/scout.md +5 -2
  17. package/agents/worker.md +7 -4
  18. package/agents.ts +29 -7
  19. package/async-execution.ts +64 -35
  20. package/async-job-tracker.ts +52 -15
  21. package/async-status.ts +107 -14
  22. package/chain-clarify.ts +1 -1
  23. package/chain-execution.ts +10 -2
  24. package/completion-dedupe.ts +1 -1
  25. package/completion-guard.ts +125 -0
  26. package/doctor.ts +1 -1
  27. package/execution.ts +158 -26
  28. package/fork-context.ts +3 -3
  29. package/index.ts +17 -6
  30. package/intercom-bridge.ts +2 -1
  31. package/jsonl-writer.ts +2 -2
  32. package/long-running-guard.ts +175 -0
  33. package/model-fallback.ts +1 -1
  34. package/package.json +1 -1
  35. package/pi-args.ts +4 -2
  36. package/pi-spawn.ts +1 -1
  37. package/prompt-template-bridge.ts +9 -9
  38. package/prompts/parallel-cleanup.md +39 -8
  39. package/render.ts +239 -49
  40. package/result-intercom.ts +5 -5
  41. package/result-watcher.ts +21 -12
  42. package/run-status.ts +1 -1
  43. package/schemas.ts +24 -13
  44. package/session-tokens.ts +2 -6
  45. package/settings.ts +2 -2
  46. package/single-output.ts +1 -1
  47. package/skills/pi-subagents/SKILL.md +93 -20
  48. package/skills.ts +8 -2
  49. package/slash-bridge.ts +3 -3
  50. package/subagent-control.ts +103 -21
  51. package/subagent-executor.ts +82 -25
  52. package/subagent-prompt-runtime.ts +80 -3
  53. package/subagent-runner.ts +339 -93
  54. package/subagents-status.ts +9 -7
  55. package/text-editor.ts +22 -6
  56. package/top-level-async.ts +1 -1
  57. package/types.ts +63 -8
  58. package/utils.ts +3 -3
  59. package/worktree.ts +5 -5
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: reviewer
3
- description: Code review specialist that validates implementation and fixes issues
4
- tools: read, grep, find, ls, bash, edit, write
3
+ description: Versatile review specialist for code diffs, plans, proposed solutions, codebase health, and PR/issue validation
4
+ tools: read, grep, find, ls, bash, edit, write, intercom
5
5
  model: openai-codex/gpt-5.5
6
6
  thinking: high
7
7
  systemPromptMode: replace
@@ -11,28 +11,86 @@ defaultReads: plan.md, progress.md
11
11
  defaultProgress: true
12
12
  ---
13
13
 
14
- You are a review-and-fix subagent.
14
+ You are a disciplined review subagent. Your job is to inspect, evaluate, and report findings with evidence. You do not guess; you verify from the code, tests, docs, or requirements.
15
15
 
16
- Review the implementation against the plan, inspect the actual code, and fix any real problems you find.
16
+ ## Review types you handle
17
17
 
18
- Working rules:
19
- - Read the plan and current progress first when they are provided.
20
- - Use `bash` only for read-only inspection commands like `git diff`, `git log`, `git show`, or test commands.
21
- - Do not invent issues. Only report or fix problems you can justify from the code, tests, or requirements.
18
+ ### 1. Code diffs (changed files)
19
+ Inspect the actual diff or changed files. Verify:
20
+ - Implementation matches intent and requirements.
21
+ - Code is correct, coherent, and handles edge cases.
22
+ - Tests cover the change and still pass.
23
+ - No unintended side effects or regressions.
24
+ - The change is minimal and readable.
25
+
26
+ ### 2. Plans
27
+ Validate a proposed plan for:
28
+ - Feasibility and completeness.
29
+ - Missing steps or hidden risks.
30
+ - Alignment with existing architecture and constraints.
31
+ - Whether the scope is appropriately bounded.
32
+
33
+ ### 3. Proposed solutions
34
+ Evaluate a suggested approach for:
35
+ - Correctness and tradeoffs.
36
+ - Fit with existing codebase patterns.
37
+ - Whether simpler alternatives exist.
38
+ - Edge cases the proposal may miss.
39
+
40
+ ### 4. Current overall state of the codebase
41
+ Assess codebase health by inspecting key files, tests, and structure. Look for:
42
+ - Architecture drift or tech debt.
43
+ - Inconsistent patterns or naming.
44
+ - Areas lacking tests or documentation.
45
+ - Obvious bugs or fragile code.
46
+ - Opportunities to simplify or consolidate.
47
+
48
+ ### 5. Specific PR or issue
49
+ Review a PR or issue by understanding the context, then verifying:
50
+ - The fix or feature addresses the root cause.
51
+ - Changes are minimal and focused.
52
+ - No regressions are introduced.
53
+ - Tests and docs are updated as needed.
54
+
55
+ ## Working rules
56
+ - Read the plan, progress, and relevant files first when available.
57
+ - Use `bash` only for read-only inspection (e.g., `git diff`, `git log`, `git show`, test runs).
58
+ - Do not invent issues. Only report problems you can justify from evidence.
22
59
  - Prefer small corrective edits over broad rewrites.
23
- - If everything looks good, say so plainly and leave the code unchanged.
24
- - If you are asked to maintain progress, record what you checked and what you fixed.
60
+ - If everything looks good, say so plainly.
61
+ - If you are asked to maintain progress, record what you checked and what you found.
25
62
 
26
- Review checklist:
27
- 1. Implementation matches the plan and task requirements.
28
- 2. Code is correct and coherent.
29
- 3. Important edge cases are handled.
30
- 4. Tests and validation still make sense.
31
- 5. The final code is readable and minimal.
63
+ ## Pi-intercom handoff
64
+ If the `intercom` tool is available and pi-intercom is active, send your completed review back to the orchestrator through pi-intercom before finishing.
32
65
 
33
- When updating `progress.md`, add a review section like this:
66
+ Use a blocking `ask`, not a fire-and-forget `send`, so you stay alive long enough for the orchestrator to reply with follow-up questions or approval:
34
67
 
68
+ ```ts
69
+ intercom({
70
+ action: "ask",
71
+ to: "<orchestrator-or-parent-session>",
72
+ message: "Review complete.\n\n<your review feedback>\n\nReply if you want me to inspect a follow-up or clarify anything."
73
+ })
74
+ ```
75
+
76
+ How to pick the target:
77
+ - Prefer an explicit target named in the task or inherited intercom bridge instructions.
78
+ - Otherwise use `intercom({ action: "list" })` and choose the obvious planner/orchestrator/parent session in the same repo.
79
+ - If no safe target is discoverable, do not guess. Return the review normally and note that pi-intercom was unavailable or no target was clear.
80
+
81
+ After the `ask` returns:
82
+ - If the orchestrator requests clarification or a follow-up review, answer or inspect further, then use `intercom ask` again if another reply is useful.
83
+ - If the orchestrator confirms or does not need more, finish with the same concise review summary.
84
+
85
+ ## Review output format
86
+ Structure your findings clearly:
87
+
88
+ ```
35
89
  ## Review
36
- - Correct: what is already good
37
- - Fixed: issue and resolution
38
- - Note: observations or follow-up items
90
+ - Correct: what is already good (with evidence)
91
+ - Fixed: issue, location, and resolution (if you applied a fix)
92
+ - Blocker: critical issue that must be resolved before proceeding
93
+ - Note: observation, risk, or follow-up item
94
+ ```
95
+
96
+ When reviewing code, cite file paths and line numbers. When reviewing plans, cite specific sections and assumptions.
package/agents/scout.md CHANGED
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: scout
3
3
  description: Fast codebase recon that returns compressed context for handoff
4
- tools: read, grep, find, ls, bash, write
4
+ tools: read, grep, find, ls, bash, write, intercom
5
5
  model: openai-codex/gpt-5.5
6
- thinking: medium
6
+ thinking: low
7
7
  systemPromptMode: replace
8
8
  inheritProjectContext: true
9
9
  inheritSkills: false
@@ -46,3 +46,6 @@ Explain how the pieces connect.
46
46
 
47
47
  ## Start Here
48
48
  Name the first file another agent should open and why.
49
+
50
+ ## Pi-intercom handoff
51
+ If `intercom` is available and runtime bridge instructions or the task name a safe orchestrator target, send your completed scout findings back with a blocking `intercom({ action: "ask", ... })` before finishing. Keep the message concise, include the output path or top findings, and ask whether the orchestrator wants more context. If no safe target is available, do not guess; return normally.
package/agents/worker.md CHANGED
@@ -1,11 +1,12 @@
1
1
  ---
2
2
  name: worker
3
3
  description: Implementation agent for normal tasks and approved oracle handoffs
4
- model: openai-codex/gpt-5.3-codex
4
+ model: openai-codex/gpt-5.5
5
5
  thinking: high
6
6
  systemPromptMode: replace
7
7
  inheritProjectContext: true
8
8
  inheritSkills: false
9
+ defaultContext: fork
9
10
  defaultReads: context.md, plan.md
10
11
  defaultProgress: true
11
12
  ---
@@ -18,7 +19,7 @@ Use the provided tools directly. First understand the inherited context, supplie
18
19
 
19
20
  If the task is framed as an approved direction, oracle handoff, or execution plan, treat that direction as the contract. Validate it against the actual code, but do not silently make new product, architecture, or scope decisions.
20
21
 
21
- If the implementation reveals a decision that was not approved and is required to continue safely, pause and escalate. If runtime bridge instructions are present, use them as the source of truth for which parent session to contact and how to coordinate. Use `intercom({ action: "ask", ... })` when a new decision is needed. Use `intercom({ action: "send", ... })` only for concise blocked/progress updates when that extra coordination is helpful or explicitly requested.
22
+ If the implementation reveals a decision that was not approved and is required to continue safely, pause and escalate through the live coordination channel. If runtime bridge instructions are present, use them as the source of truth for which parent session to contact and how to coordinate. Use `intercom({ action: "ask", ... })` when a new decision is needed, and stay alive to receive the reply before continuing. Use `intercom({ action: "send", ... })` only for concise non-blocking progress updates when that extra coordination is helpful or explicitly requested. Do not finish your final response with a question that requires the orchestrator to choose before you can continue.
22
23
 
23
24
  Default responsibilities:
24
25
  - validate the task or approved direction against the actual code
@@ -34,9 +35,11 @@ Working rules:
34
35
  - Do not leave placeholder code, TODOs, or silent scope changes.
35
36
  - Use `bash` for inspection, validation, and relevant tests.
36
37
  - If there is supplied context or a plan, read it first.
37
- - If implementation reveals a gap in the approved direction, pause and escalate instead of silently patching around it with an implicit decision.
38
- - If implementation reveals an unapproved product or architecture choice, pause and ask instead of deciding it yourself.
38
+ - If implementation reveals a gap in the approved direction, pause and escalate with `intercom({ action: "ask", ... })` instead of silently patching around it with an implicit decision.
39
+ - If implementation reveals an unapproved product or architecture choice, use `intercom({ action: "ask", ... })` and wait for the reply instead of deciding it yourself or returning a final choose-one answer.
40
+ - If your delegated task expects code or file edits and you have not made those edits, do not return a success summary. Make the edits, ask the orchestrator if blocked, or explicitly report that no edits were made.
39
41
  - If you send a blocked/progress update through intercom, keep it short and still return the full structured task result normally.
42
+ - If `intercom` is available and runtime bridge instructions or the task name a safe orchestrator target, send your completed implementation summary back with a blocking `intercom({ action: "ask", ... })` before finishing. Stay alive for the reply so you can clarify or handle a small follow-up if requested. If no safe target is available, do not guess; return normally.
40
43
 
41
44
  When running in a chain, expect instructions about:
42
45
  - which files to read first
package/agents.ts CHANGED
@@ -14,7 +14,8 @@ import { parseFrontmatter } from "./frontmatter.ts";
14
14
  export type AgentScope = "user" | "project" | "both";
15
15
 
16
16
  export type AgentSource = "builtin" | "user" | "project";
17
- export type SystemPromptMode = "append" | "replace";
17
+ type SystemPromptMode = "append" | "replace";
18
+ export type AgentDefaultContext = "fresh" | "fork";
18
19
 
19
20
  export function defaultSystemPromptMode(name: string): SystemPromptMode {
20
21
  return name === "delegate" ? "append" : "replace";
@@ -35,6 +36,7 @@ export interface BuiltinAgentOverrideBase {
35
36
  systemPromptMode: SystemPromptMode;
36
37
  inheritProjectContext: boolean;
37
38
  inheritSkills: boolean;
39
+ defaultContext?: AgentDefaultContext;
38
40
  disabled?: boolean;
39
41
  systemPrompt: string;
40
42
  skills?: string[];
@@ -42,20 +44,21 @@ export interface BuiltinAgentOverrideBase {
42
44
  mcpDirectTools?: string[];
43
45
  }
44
46
 
45
- export interface BuiltinAgentOverrideConfig {
47
+ interface BuiltinAgentOverrideConfig {
46
48
  model?: string | false;
47
49
  fallbackModels?: string[] | false;
48
50
  thinking?: string | false;
49
51
  systemPromptMode?: SystemPromptMode;
50
52
  inheritProjectContext?: boolean;
51
53
  inheritSkills?: boolean;
54
+ defaultContext?: AgentDefaultContext | false;
52
55
  disabled?: boolean;
53
56
  systemPrompt?: string;
54
57
  skills?: string[] | false;
55
58
  tools?: string[] | false;
56
59
  }
57
60
 
58
- export interface BuiltinAgentOverrideInfo {
61
+ interface BuiltinAgentOverrideInfo {
59
62
  scope: "user" | "project";
60
63
  path: string;
61
64
  base: BuiltinAgentOverrideBase;
@@ -72,6 +75,7 @@ export interface AgentConfig {
72
75
  systemPromptMode: SystemPromptMode;
73
76
  inheritProjectContext: boolean;
74
77
  inheritSkills: boolean;
78
+ defaultContext?: AgentDefaultContext;
75
79
  systemPrompt: string;
76
80
  source: AgentSource;
77
81
  filePath: string;
@@ -113,7 +117,7 @@ export interface ChainConfig {
113
117
  extraFields?: Record<string, string>;
114
118
  }
115
119
 
116
- export interface AgentDiscoveryResult {
120
+ interface AgentDiscoveryResult {
117
121
  agents: AgentConfig[];
118
122
  projectAgentsDir: string | null;
119
123
  }
@@ -160,6 +164,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
160
164
  systemPromptMode: agent.systemPromptMode,
161
165
  inheritProjectContext: agent.inheritProjectContext,
162
166
  inheritSkills: agent.inheritSkills,
167
+ defaultContext: agent.defaultContext,
163
168
  disabled: agent.disabled,
164
169
  systemPrompt: agent.systemPrompt,
165
170
  skills: agent.skills ? [...agent.skills] : undefined,
@@ -178,6 +183,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
178
183
  ...(override.systemPromptMode !== undefined ? { systemPromptMode: override.systemPromptMode } : {}),
179
184
  ...(override.inheritProjectContext !== undefined ? { inheritProjectContext: override.inheritProjectContext } : {}),
180
185
  ...(override.inheritSkills !== undefined ? { inheritSkills: override.inheritSkills } : {}),
186
+ ...(override.defaultContext !== undefined ? { defaultContext: override.defaultContext } : {}),
181
187
  ...(override.disabled !== undefined ? { disabled: override.disabled } : {}),
182
188
  ...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
183
189
  ...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
@@ -198,11 +204,11 @@ function findNearestProjectRoot(cwd: string): string | null {
198
204
  }
199
205
  }
200
206
 
201
- export function getUserAgentSettingsPath(): string {
207
+ function getUserAgentSettingsPath(): string {
202
208
  return path.join(os.homedir(), ".pi", "agent", "settings.json");
203
209
  }
204
210
 
205
- export function getProjectAgentSettingsPath(cwd: string): string | null {
211
+ function getProjectAgentSettingsPath(cwd: string): string | null {
206
212
  const projectRoot = findNearestProjectRoot(cwd);
207
213
  return projectRoot ? path.join(projectRoot, ".pi", "settings.json") : null;
208
214
  }
@@ -302,6 +308,14 @@ function parseBuiltinOverrideEntry(
302
308
  }
303
309
  }
304
310
 
311
+ if ("defaultContext" in input) {
312
+ if (input.defaultContext === "fresh" || input.defaultContext === "fork" || input.defaultContext === false) {
313
+ override.defaultContext = input.defaultContext;
314
+ } else {
315
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'defaultContext'; expected 'fresh', 'fork', or false.`);
316
+ }
317
+ }
318
+
305
319
  if ("disabled" in input) {
306
320
  if (typeof input.disabled === "boolean") {
307
321
  override.disabled = input.disabled;
@@ -373,6 +387,7 @@ function applyBuiltinOverride(
373
387
  if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
374
388
  if (override.inheritProjectContext !== undefined) next.inheritProjectContext = override.inheritProjectContext;
375
389
  if (override.inheritSkills !== undefined) next.inheritSkills = override.inheritSkills;
390
+ if (override.defaultContext !== undefined) next.defaultContext = override.defaultContext === false ? undefined : override.defaultContext;
376
391
  if (override.disabled !== undefined) next.disabled = override.disabled;
377
392
  if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
378
393
  if (override.skills !== undefined) next.skills = override.skills === false ? undefined : [...override.skills];
@@ -420,7 +435,7 @@ function applyBuiltinOverrides(
420
435
 
421
436
  export function buildBuiltinOverrideConfig(
422
437
  base: BuiltinAgentOverrideBase,
423
- draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools">,
438
+ draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools">,
424
439
  ): BuiltinAgentOverrideConfig | undefined {
425
440
  const override: BuiltinAgentOverrideConfig = {};
426
441
 
@@ -430,6 +445,7 @@ export function buildBuiltinOverrideConfig(
430
445
  if (draft.systemPromptMode !== base.systemPromptMode) override.systemPromptMode = draft.systemPromptMode;
431
446
  if (draft.inheritProjectContext !== base.inheritProjectContext) override.inheritProjectContext = draft.inheritProjectContext;
432
447
  if (draft.inheritSkills !== base.inheritSkills) override.inheritSkills = draft.inheritSkills;
448
+ if (draft.defaultContext !== base.defaultContext) override.defaultContext = draft.defaultContext ?? false;
433
449
  if (draft.disabled !== base.disabled) override.disabled = draft.disabled ?? false;
434
450
  if (draft.systemPrompt !== base.systemPrompt) override.systemPrompt = draft.systemPrompt;
435
451
  if (!arraysEqual(draft.skills, base.skills)) override.skills = draft.skills ? [...draft.skills] : false;
@@ -568,6 +584,11 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
568
584
  : frontmatter.inheritSkills === "false"
569
585
  ? false
570
586
  : defaultInheritSkills();
587
+ const defaultContext = frontmatter.defaultContext === "fork"
588
+ ? "fork" as const
589
+ : frontmatter.defaultContext === "fresh"
590
+ ? "fresh" as const
591
+ : undefined;
571
592
 
572
593
  let extensions: string[] | undefined;
573
594
  if (frontmatter.extensions !== undefined) {
@@ -595,6 +616,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
595
616
  systemPromptMode,
596
617
  inheritProjectContext,
597
618
  inheritSkills,
619
+ defaultContext,
598
620
  systemPrompt: body,
599
621
  source,
600
622
  filePath,
@@ -55,14 +55,14 @@ const jitiCliPath: string | undefined = (() => {
55
55
  return undefined;
56
56
  })();
57
57
 
58
- export interface AsyncExecutionContext {
58
+ interface AsyncExecutionContext {
59
59
  pi: ExtensionAPI;
60
60
  cwd: string;
61
61
  currentSessionId: string;
62
62
  currentModelProvider?: string;
63
63
  }
64
64
 
65
- export interface AsyncChainParams {
65
+ interface AsyncChainParams {
66
66
  chain: ChainStep[];
67
67
  resultMode?: "parallel" | "chain";
68
68
  agents: AgentConfig[];
@@ -84,7 +84,7 @@ export interface AsyncChainParams {
84
84
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
85
85
  }
86
86
 
87
- export interface AsyncSingleParams {
87
+ interface AsyncSingleParams {
88
88
  agent: string;
89
89
  task?: string;
90
90
  agentConfig: AgentConfig;
@@ -108,7 +108,7 @@ export interface AsyncSingleParams {
108
108
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
109
109
  }
110
110
 
111
- export interface AsyncExecutionResult {
111
+ interface AsyncExecutionResult {
112
112
  content: Array<{ type: "text"; text: string }>;
113
113
  details: Details;
114
114
  isError?: boolean;
@@ -167,6 +167,10 @@ function formatAsyncStartError(mode: "single" | "chain", message: string): Async
167
167
  };
168
168
  }
169
169
 
170
+ const UNAVAILABLE_SUBAGENT_SKILL_ERROR = "Skills not found: pi-subagents";
171
+
172
+ class UnavailableSubagentSkillError extends Error {}
173
+
170
174
  /**
171
175
  * Execute a chain asynchronously
172
176
  */
@@ -240,7 +244,8 @@ export function executeAsyncChain(
240
244
  const instructionCwd = behaviorCwd ?? stepCwd;
241
245
  const behavior = resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills);
242
246
  const skillNames = behavior.skills === false ? [] : behavior.skills;
243
- const { resolved: resolvedSkills } = resolveSkillsWithFallback(skillNames, stepCwd, ctx.cwd);
247
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, stepCwd, ctx.cwd);
248
+ if (missingSkills.includes("pi-subagents")) throw new UnavailableSubagentSkillError(UNAVAILABLE_SUBAGENT_SKILL_ERROR);
244
249
 
245
250
  let systemPrompt = a.systemPrompt?.trim() ?? "";
246
251
  if (resolvedSkills.length > 0) {
@@ -285,36 +290,42 @@ export function executeAsyncChain(
285
290
  return sessionFile;
286
291
  };
287
292
 
288
- const steps: RunnerStep[] = chain.map((s, stepIndex) => {
289
- if (isParallelStep(s)) {
290
- const parallelBehaviors = s.parallel.map((task) => {
291
- const agent = agents.find((candidate) => candidate.name === task.agent)!;
292
- return resolveStepBehavior(agent, buildStepOverrides(task), chainSkills);
293
- });
294
- const progressPrecreated = parallelBehaviors.some((behavior) => behavior.progress);
295
- if (progressPrecreated) {
296
- if (!s.worktree) writeInitialProgressFile(runnerCwd);
297
- progressInstructionCreated = true;
298
- }
299
- return {
300
- parallel: s.parallel.map((t, taskIndex) => {
301
- let behaviorCwd: string | undefined;
302
- if (s.worktree) {
303
- try {
304
- behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex);
305
- } catch {
306
- behaviorCwd = undefined;
293
+ let steps: RunnerStep[];
294
+ try {
295
+ steps = chain.map((s, stepIndex) => {
296
+ if (isParallelStep(s)) {
297
+ const parallelBehaviors = s.parallel.map((task) => {
298
+ const agent = agents.find((candidate) => candidate.name === task.agent)!;
299
+ return resolveStepBehavior(agent, buildStepOverrides(task), chainSkills);
300
+ });
301
+ const progressPrecreated = parallelBehaviors.some((behavior) => behavior.progress);
302
+ if (progressPrecreated) {
303
+ if (!s.worktree) writeInitialProgressFile(runnerCwd);
304
+ progressInstructionCreated = true;
305
+ }
306
+ return {
307
+ parallel: s.parallel.map((t, taskIndex) => {
308
+ let behaviorCwd: string | undefined;
309
+ if (s.worktree) {
310
+ try {
311
+ behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex);
312
+ } catch {
313
+ behaviorCwd = undefined;
314
+ }
307
315
  }
308
- }
309
- return buildSeqStep(t, nextSessionFile(), behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex]);
310
- }),
311
- concurrency: s.concurrency,
312
- failFast: s.failFast,
313
- worktree: s.worktree,
314
- };
315
- }
316
- return buildSeqStep(s as SequentialStep, nextSessionFile());
317
- });
316
+ return buildSeqStep(t, nextSessionFile(), behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex]);
317
+ }),
318
+ concurrency: s.concurrency,
319
+ failFast: s.failFast,
320
+ worktree: s.worktree,
321
+ };
322
+ }
323
+ return buildSeqStep(s as SequentialStep, nextSessionFile());
324
+ });
325
+ } catch (error) {
326
+ if (error instanceof UnavailableSubagentSkillError) return formatAsyncStartError("chain", error.message);
327
+ throw error;
328
+ }
318
329
  let childTargetIndex = 0;
319
330
  const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
320
331
  if ("parallel" in step) {
@@ -365,16 +376,33 @@ export function executeAsyncChain(
365
376
  const firstAgents = isParallelStep(firstStep)
366
377
  ? firstStep.parallel.map((t) => t.agent)
367
378
  : [(firstStep as SequentialStep).agent];
379
+ const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
380
+ const flatAgents: string[] = [];
381
+ let flatStepStart = 0;
382
+ for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
383
+ const step = chain[stepIndex]!;
384
+ if (isParallelStep(step)) {
385
+ parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
386
+ flatAgents.push(...step.parallel.map((task) => task.agent));
387
+ flatStepStart += step.parallel.length;
388
+ } else {
389
+ flatAgents.push((step as SequentialStep).agent);
390
+ flatStepStart++;
391
+ }
392
+ }
368
393
  ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
369
394
  id,
370
395
  pid: spawnResult.pid,
371
396
  agent: firstAgents[0],
397
+ agents: flatAgents,
372
398
  task: isParallelStep(firstStep)
373
399
  ? firstStep.parallel[0]?.task?.slice(0, 50)
374
400
  : (firstStep as SequentialStep).task?.slice(0, 50),
375
401
  chain: chain.map((s) =>
376
402
  isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : (s as SequentialStep).agent,
377
403
  ),
404
+ chainStepCount: chain.length,
405
+ parallelGroups,
378
406
  cwd: runnerCwd,
379
407
  asyncDir,
380
408
  });
@@ -421,7 +449,8 @@ export function executeAsyncSingle(
421
449
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
422
450
  const skillNames = params.skills ?? agentConfig.skills ?? [];
423
451
  const availableModels = params.availableModels;
424
- const { resolved: resolvedSkills } = resolveSkillsWithFallback(skillNames, runnerCwd, ctx.cwd);
452
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, runnerCwd, ctx.cwd);
453
+ if (missingSkills.includes("pi-subagents")) return formatAsyncStartError("single", UNAVAILABLE_SUBAGENT_SKILL_ERROR);
425
454
  let systemPrompt = agentConfig.systemPrompt?.trim() ?? "";
426
455
  if (resolvedSkills.length > 0) {
427
456
  const injection = buildSkillInjection(resolvedSkills);
@@ -5,6 +5,8 @@ import { renderWidget } from "./render.ts";
5
5
  import { formatControlNoticeMessage } from "./subagent-control.ts";
6
6
  import {
7
7
  type AsyncJobState,
8
+ type AsyncParallelGroupStatus,
9
+ type AsyncStartedEvent,
8
10
  type ControlEvent,
9
11
  type SubagentState,
10
12
  POLL_INTERVAL_MS,
@@ -13,6 +15,23 @@ import {
13
15
  } from "./types.ts";
14
16
  import { readStatus } from "./utils.ts";
15
17
 
18
+
19
+ function isValidParallelGroup(group: AsyncParallelGroupStatus, stepCount: number, chainStepCount: number): boolean {
20
+ return Number.isInteger(group.start)
21
+ && Number.isInteger(group.count)
22
+ && Number.isInteger(group.stepIndex)
23
+ && group.start >= 0
24
+ && group.count > 0
25
+ && group.stepIndex >= 0
26
+ && group.stepIndex < chainStepCount
27
+ && group.start + group.count <= stepCount;
28
+ }
29
+
30
+ function normalizeParallelGroups(groups: AsyncParallelGroupStatus[] | undefined, stepCount: number, chainStepCount: number): AsyncParallelGroupStatus[] {
31
+ if (!groups?.length) return [];
32
+ return groups.filter((group) => isValidParallelGroup(group, stepCount, chainStepCount));
33
+ }
34
+
16
35
  interface AsyncJobTrackerOptions {
17
36
  completionRetentionMs?: number;
18
37
  pollIntervalMs?: number;
@@ -66,8 +85,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
66
85
  let parsed: unknown;
67
86
  try {
68
87
  parsed = JSON.parse(line);
69
- } catch {
70
- // Ignore malformed completed records but keep the poller alive for later events.
88
+ } catch (error) {
89
+ console.error(`Ignoring malformed async control event in '${eventsPath}':`, error);
71
90
  continue;
72
91
  }
73
92
  if (!parsed || typeof parsed !== "object" || (parsed as { type?: unknown }).type !== "subagent.control") continue;
@@ -83,7 +102,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
83
102
  if (record.channels.includes("event")) {
84
103
  pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
85
104
  }
86
- if (record.channels.includes("intercom") && record.intercom?.to && record.intercom.message) {
105
+ if (record.event.type !== "active_long_running" && record.channels.includes("intercom") && record.intercom?.to && record.intercom.message) {
87
106
  pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
88
107
  ...payload,
89
108
  to: record.intercom.to,
@@ -119,15 +138,30 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
119
138
  job.status = status.state;
120
139
  job.activityState = status.activityState;
121
140
  job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
122
- job.currentTool = status.currentTool ?? job.currentTool;
123
- job.currentToolStartedAt = status.currentToolStartedAt ?? job.currentToolStartedAt;
141
+ job.currentTool = status.currentTool;
142
+ job.currentToolStartedAt = status.currentToolStartedAt;
143
+ job.currentPath = status.currentPath;
144
+ job.turnCount = status.turnCount ?? job.turnCount;
145
+ job.toolCount = status.toolCount ?? job.toolCount;
124
146
  job.mode = status.mode;
125
147
  job.currentStep = status.currentStep ?? job.currentStep;
126
- job.stepsTotal = status.steps?.length ?? job.stepsTotal;
127
148
  job.startedAt = status.startedAt ?? job.startedAt;
128
149
  job.updatedAt = status.lastUpdate ?? Date.now();
129
150
  if (status.steps?.length) {
130
- job.agents = status.steps.map((step) => step.agent);
151
+ const groups = normalizeParallelGroups(status.parallelGroups, status.steps.length, status.chainStepCount ?? status.steps.length);
152
+ job.hasParallelGroups = groups.length > 0 || job.hasParallelGroups;
153
+ const activeGroup = status.currentStep !== undefined
154
+ ? groups.find((group) => status.currentStep! >= group.start && status.currentStep! < group.start + group.count)
155
+ : undefined;
156
+ const visibleSteps = activeGroup
157
+ ? status.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count)
158
+ : status.steps;
159
+ job.activeParallelGroup = Boolean(activeGroup);
160
+ job.agents = visibleSteps.map((step) => step.agent);
161
+ job.stepsTotal = visibleSteps.length;
162
+ job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
163
+ job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
164
+ if (status.state === "complete") job.completedSteps = visibleSteps.length;
131
165
  }
132
166
  job.sessionDir = status.sessionDir ?? job.sessionDir;
133
167
  job.outputFile = status.outputFile ?? job.outputFile;
@@ -153,23 +187,26 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
153
187
  };
154
188
 
155
189
  const handleStarted = (data: unknown) => {
156
- const info = data as {
157
- id?: string;
158
- asyncDir?: string;
159
- agent?: string;
160
- chain?: string[];
161
- };
190
+ const info = data as AsyncStartedEvent;
162
191
  if (!info.id) return;
163
192
  const now = Date.now();
164
193
  const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
165
- const agents = info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
194
+ const rawAgents = info.agents?.length ? info.agents : info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
195
+ const validParallelGroups = normalizeParallelGroups(info.parallelGroups, Number.MAX_SAFE_INTEGER, info.chainStepCount ?? Number.MAX_SAFE_INTEGER);
196
+ const firstGroup = validParallelGroups.find((group) => group.start === 0);
197
+ const firstGroupCount = firstGroup?.count;
198
+ const agents = firstGroupCount && firstGroupCount > 0
199
+ ? rawAgents?.slice(0, firstGroupCount)
200
+ : rawAgents;
166
201
  state.asyncJobs.set(info.id, {
167
202
  asyncId: info.id,
168
203
  asyncDir,
169
204
  status: "queued",
170
205
  mode: info.chain ? "chain" : "single",
171
206
  agents,
172
- stepsTotal: agents?.length,
207
+ stepsTotal: firstGroupCount ?? agents?.length,
208
+ hasParallelGroups: validParallelGroups.length > 0,
209
+ activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
173
210
  startedAt: now,
174
211
  updatedAt: now,
175
212
  });