pi-subagents 0.9.2 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.11.0] - 2026-02-23
6
+
7
+ ### Added
8
+ - **Background mode toggle in clarify TUI**: Press `b` to toggle background/async execution for any mode (single, parallel, chain). Shows `[b]g:ON` in footer when enabled. Previously async execution required programmatic `clarify: false, async: true` — now users can interactively choose background mode after previewing/editing parameters.
9
+ - **`--bg` flag for slash commands**: `/run scout "task" --bg`, `/chain scout "task" -> planner --bg`, `/parallel scout "a" -> scout "b" --bg` now run in background without needing the TUI.
10
+
11
+ ### Fixed
12
+ - Task edits in clarify TUI were lost when launching in background mode if no other behavior (model, output, reads) was modified. The async handoff now always applies the edited template.
13
+
14
+ ## [0.10.0] - 2026-02-23
15
+
16
+ ### Added
17
+ - **Async parallel chain support**: Chains with `{ parallel: [...] }` steps now work in async mode. Previously they were rejected with "Async mode doesn't support chains with parallel steps." The async runner now spawns concurrent pi processes for parallel step groups with configurable `concurrency` and `failFast` options. Inspired by PR #31 from @marcfargas.
18
+ - **Comprehensive test suite**: 85 integration tests and 12 E2E tests covering all execution modes (single, parallel, chain, async), error handling, template resolution, and tool validation. Uses `@marcfargas/pi-test-harness` for subprocess mocking and in-process session testing. Thanks @marcfargas for PR #32.
19
+ - GitHub Actions CI workflow running tests on both Ubuntu and Windows with Node.js 24.
20
+
21
+ ### Changed
22
+ - **BREAKING:** `share` parameter now defaults to `false`. Previously, sessions were silently uploaded to GitHub Gists without user consent. Users who want session sharing must now explicitly pass `share: true`. Added documentation explaining what the feature does and its privacy implications.
23
+
24
+ ### Fixed
25
+ - `mapConcurrent` with `limit=0` returned array of undefined values instead of processing items sequentially. Now clamps limit to at least 1.
26
+ - ANSI background color bleed in truncated text. The `truncLine` function now properly tracks and re-applies all active ANSI styles (bold, colors, etc.) before the ellipsis, preventing style leakage. Also uses `Intl.Segmenter` for correct Unicode/emoji handling. Thanks @monotykamary for identifying the issue.
27
+ - `detectSubagentError` no longer produces false positives when the agent recovers from tool errors. Previously, any error in the last tool result would override exitCode 0→1, even if the agent had already produced complete output. Now only errors AFTER the agent's final text response are flagged. Thanks @marcfargas for the fix and comprehensive test coverage.
28
+ - Parallel mode (`tasks: [...]`) now returns aggregated output from all tasks instead of just a success count. Previously only returned "3/3 succeeded" with actual task outputs lost.
29
+ - Session sharing fallback no longer fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`. The fallback now resolves the main entry point and walks up to find the package root instead of trying to resolve `package.json` directly.
30
+ - Skills from globally-installed npm packages (via `pi install npm:...`) are now discoverable by subagents. Previously only scanned local `.pi/npm/node_modules/` paths, missing the global npm root where pi actually installs packages.
31
+ - **Windows compatibility**: Fixed `ENAMETOOLONG` errors when tasks exceed command-line length limits by writing long tasks to temp files using pi's `@file` syntax. Thanks @marcfargas.
32
+ - **Windows compatibility**: Suppressed flashing console windows when spawning async runner processes (`windowsHide: true`).
33
+ - **Windows compatibility**: Fixed pi CLI resolution in async runner by passing `piPackageRoot` through to `getPiSpawnCommand`.
34
+ - **Cross-platform paths**: Replaced `startsWith("/")` checks with `path.isAbsolute()` for correct Windows absolute path detection. Replaced template string path concatenation with `path.join()` for consistent path separators.
35
+ - **Resilience**: Added error handling and auto-restart for the results directory watcher. Previously, if the directory was deleted or became inaccessible, the watcher would die silently.
36
+ - **Resilience**: Added `ensureAccessibleDir` helper that verifies directory accessibility after creation and attempts recovery if the directory has broken ACLs (can happen on Windows with Azure AD/Entra ID after wake-from-sleep).
37
+
5
38
  ## [0.9.2] - 2026-02-19
6
39
 
7
40
  ### Fixed
package/README.md CHANGED
@@ -162,6 +162,18 @@ Append `[key=value,...]` to any agent name to override its defaults:
162
162
 
163
163
  Set `output=false`, `reads=false`, or `skills=false` to explicitly disable.
164
164
 
165
+ ### Background Execution
166
+
167
+ Add `--bg` at the end of any slash command to run in the background:
168
+
169
+ ```
170
+ /run scout "full security audit of the codebase" --bg
171
+ /chain scout "analyze auth system" -> planner "design refactor plan" -> worker --bg
172
+ /parallel scout "scan frontend" -> scout "scan backend" -> scout "scan infra" --bg
173
+ ```
174
+
175
+ Background tasks run asynchronously and notify you when complete. Check status with `subagent_status`.
176
+
165
177
  ## Agents Manager
166
178
 
167
179
  Press **Ctrl+Shift+A** or type `/agents` to open the Agents Manager overlay — a TUI for browsing, viewing, editing, creating, and launching agents and chains.
@@ -270,10 +282,10 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
270
282
  | Mode | Async Support | Notes |
271
283
  |------|---------------|-------|
272
284
  | Single | Yes | `{ agent, task }` - agents with `output` write to temp dir |
273
- | Chain | Yes* | `{ chain: [{agent, task}...] }` with `{task}`, `{previous}`, `{chain_dir}` variables |
274
- | Parallel | Sync only | `{ tasks: [{agent, task}...] }` - auto-downgrades if async requested |
285
+ | Chain | Yes | `{ chain: [{agent, task}...] }` with `{task}`, `{previous}`, `{chain_dir}` variables |
286
+ | Parallel | Yes | `{ tasks: [{agent, task}...] }` - via TUI toggle or converted to chain for async |
275
287
 
276
- *Chain defaults to sync with TUI clarification. Use `clarify: false` to enable async (sequential-only chains; parallel-in-chain requires sync mode).
288
+ All modes support background/async execution. For programmatic async, use `clarify: false, async: true`. For interactive async, use `clarify: true` and press `b` in the TUI to toggle background mode before running. Chains with parallel steps (`{ parallel: [...] }`) run concurrently with configurable `concurrency` and `failFast` options.
277
289
 
278
290
  **Clarify TUI for single/parallel:**
279
291
 
@@ -290,13 +302,14 @@ Single and parallel modes also support the clarify TUI for previewing/editing pa
290
302
  **Clarification TUI keybindings:**
291
303
 
292
304
  *Navigation mode:*
293
- - `Enter` - Run
305
+ - `Enter` - Run (foreground) or launch in background if `b` is toggled on
294
306
  - `Esc` - Cancel
295
307
  - `↑↓` - Navigate between steps/tasks (parallel, chain)
296
308
  - `e` - Edit task/template (all modes)
297
309
  - `m` - Select model (all modes)
298
310
  - `t` - Select thinking level (all modes)
299
311
  - `s` - Select skills (all modes)
312
+ - `b` - Toggle background/async execution (all modes) — shows `[b]g:ON` when enabled
300
313
  - `w` - Edit writes/output file (single, chain only)
301
314
  - `r` - Edit reads list (chain only)
302
315
  - `p` - Toggle progress tracking (chain only)
@@ -423,6 +436,16 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
423
436
  { agent: "worker", task: "Refactor module C" }
424
437
  ], concurrency: 2, failFast: true } // limit concurrency, stop on first failure
425
438
  ]}
439
+
440
+ // Async chain with parallel step (runs in background)
441
+ { chain: [
442
+ { agent: "scout", task: "Gather context" },
443
+ { parallel: [
444
+ { agent: "worker", task: "Implement feature A based on {previous}" },
445
+ { agent: "worker", task: "Implement feature B based on {previous}" }
446
+ ]},
447
+ { agent: "reviewer", task: "Review all changes from {previous}" }
448
+ ], clarify: false, async: true }
426
449
  ```
427
450
 
428
451
  **subagent_status tool:**
@@ -514,7 +537,7 @@ Notes:
514
537
  | `maxOutput` | `{bytes?, lines?}` | 200KB, 5000 lines | Truncation limits for final output |
515
538
  | `artifacts` | boolean | true | Write debug artifacts |
516
539
  | `includeProgress` | boolean | false | Include full progress in result |
517
- | `share` | boolean | true | Create shareable session log |
540
+ | `share` | boolean | false | Upload session to GitHub Gist (see [Session Sharing](#session-sharing)) |
518
541
  | `sessionDir` | string | temp | Directory to store session logs |
519
542
 
520
543
  **ChainItem** can be either a sequential step or a parallel step:
@@ -608,6 +631,25 @@ Files per task:
608
631
 
609
632
  Session files (JSONL) are stored under a per-run session dir (temp by default). The session file path is shown in output. Set `sessionDir` to keep session logs outside `<tmpdir>`.
610
633
 
634
+ ## Session Sharing
635
+
636
+ When `share: true` is passed, the extension will:
637
+
638
+ 1. Export the full session (all tool calls, file contents, outputs) to an HTML file
639
+ 2. Upload it to a GitHub Gist using your `gh` CLI credentials
640
+ 3. Return a shareable URL (`https://shittycodingagent.ai/session/?<gistId>`)
641
+
642
+ **This is disabled by default.** Session data may contain sensitive information like source code, file paths, environment variables, or credentials that appear in tool outputs.
643
+
644
+ To enable sharing for a specific run:
645
+ ```typescript
646
+ { agent: "scout", task: "...", share: true }
647
+ ```
648
+
649
+ Requirements:
650
+ - GitHub CLI (`gh`) must be installed and authenticated (`gh auth login`)
651
+ - Gists are created as "secret" (unlisted but accessible to anyone with the URL)
652
+
611
653
  ## Live progress (sync mode)
612
654
 
613
655
  During sync execution, the collapsed view shows real-time progress for single, chain, and parallel modes.
@@ -686,10 +728,16 @@ Async events:
686
728
  ├── artifacts.ts # Artifact management
687
729
  ├── formatters.ts # Output formatting utilities
688
730
  ├── schemas.ts # TypeBox parameter schemas
689
- ├── utils.ts # Shared utility functions
731
+ ├── utils.ts # Shared utility functions (mapConcurrent, readStatus, etc.)
690
732
  ├── types.ts # Shared types and constants
691
733
  ├── subagent-runner.ts # Async runner (detached process)
734
+ ├── parallel-utils.ts # Parallel execution utilities for async runner
735
+ ├── pi-spawn.ts # Cross-platform pi CLI spawning
736
+ ├── single-output.ts # Solo agent output file handling
692
737
  ├── notify.ts # Async completion notifications
738
+ ├── completion-dedupe.ts # Completion deduplication for notifications
739
+ ├── file-coalescer.ts # Debounced file write coalescing
740
+ ├── jsonl-writer.ts # JSONL event stream writer
693
741
  ├── agent-manager.ts # Overlay orchestrator, screen routing, CRUD
694
742
  ├── agent-manager-list.ts # List screen (search, multi-select, progressive footer)
695
743
  ├── agent-manager-detail.ts # Detail screen (resolved prompt, runs, fields)
@@ -698,6 +746,8 @@ Async events:
698
746
  ├── agent-manager-chain-detail.ts # Chain detail screen (flow visualization)
699
747
  ├── agent-management.ts # Management action handlers (list, get, create, update, delete)
700
748
  ├── agent-serializer.ts # Serialize agents to markdown frontmatter
749
+ ├── agent-scope.ts # Agent scope resolution utilities
750
+ ├── agent-selection.ts # Agent selection state management
701
751
  ├── agent-templates.ts # Agent/chain creation templates
702
752
  ├── render-helpers.ts # Shared pad/row/header/footer helpers
703
753
  ├── run-history.ts # Per-agent run recording (JSONL)
@@ -12,7 +12,8 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
12
  import type { AgentConfig } from "./agents.js";
13
13
  import { applyThinkingSuffix } from "./execution.js";
14
14
  import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.js";
15
- import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep, type StepOverrides } from "./settings.js";
15
+ import { isParallelStep, resolveStepBehavior, type ChainStep, type ParallelStep, type SequentialStep, type StepOverrides } from "./settings.js";
16
+ import type { RunnerStep } from "./parallel-utils.js";
16
17
  import { resolvePiPackageRoot } from "./pi-spawn.js";
17
18
  import { buildSkillInjection, normalizeSkillInput, resolveSkills } from "./skills.js";
18
19
  import {
@@ -105,6 +106,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): number | undefin
105
106
  cwd,
106
107
  detached: true,
107
108
  stdio: "ignore",
109
+ windowsHide: true,
108
110
  });
109
111
  proc.unref();
110
112
  return proc.pid;
@@ -119,28 +121,20 @@ export function executeAsyncChain(
119
121
  ): AsyncExecutionResult {
120
122
  const { chain, agents, ctx, cwd, maxOutput, artifactsDir, artifactConfig, shareEnabled, sessionRoot } = params;
121
123
  const chainSkills = params.chainSkills ?? [];
122
-
123
- // Async mode doesn't support parallel steps (v1 limitation)
124
- const hasParallelInChain = chain.some(isParallelStep);
125
- if (hasParallelInChain) {
126
- return {
127
- content: [{ type: "text", text: "Async mode doesn't support chains with parallel steps. Use clarify: true (sync mode) for parallel-in-chain." }],
128
- isError: true,
129
- details: { mode: "chain" as const, results: [] },
130
- };
131
- }
132
-
133
- // At this point, all steps are sequential
134
- const seqSteps = chain as SequentialStep[];
135
124
 
136
125
  // Validate all agents exist before building steps
137
- for (const s of seqSteps) {
138
- if (!agents.find((x) => x.name === s.agent)) {
139
- return {
140
- content: [{ type: "text", text: `Unknown agent: ${s.agent}` }],
141
- isError: true,
142
- details: { mode: "chain" as const, results: [] },
143
- };
126
+ for (const s of chain) {
127
+ const stepAgents = isParallelStep(s)
128
+ ? s.parallel.map((t) => t.agent)
129
+ : [(s as SequentialStep).agent];
130
+ for (const agentName of stepAgents) {
131
+ if (!agents.find((x) => x.name === agentName)) {
132
+ return {
133
+ content: [{ type: "text", text: `Unknown agent: ${agentName}` }],
134
+ isError: true,
135
+ details: { mode: "chain" as const, results: [] },
136
+ };
137
+ }
144
138
  }
145
139
  }
146
140
 
@@ -149,7 +143,8 @@ export function executeAsyncChain(
149
143
  fs.mkdirSync(asyncDir, { recursive: true });
150
144
  } catch {}
151
145
 
152
- const steps = seqSteps.map((s) => {
146
+ /** Build a resolved runner step from a SequentialStep */
147
+ const buildSeqStep = (s: SequentialStep) => {
153
148
  const a = agents.find((x) => x.name === s.agent)!;
154
149
  const stepSkillInput = normalizeSkillInput(s.skill);
155
150
  const stepOverrides: StepOverrides = { skills: stepSkillInput };
@@ -162,9 +157,15 @@ export function executeAsyncChain(
162
157
  const injection = buildSkillInjection(resolvedSkills);
163
158
  systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
164
159
  }
160
+
161
+ // Resolve output path and inject instruction into task
162
+ // Use step's cwd if specified, otherwise fall back to chain-level cwd
163
+ const outputPath = resolveSingleOutputPath(s.output, ctx.cwd, s.cwd ?? cwd);
164
+ const task = injectSingleOutputInstruction(s.task ?? "{previous}", outputPath);
165
+
165
166
  return {
166
167
  agent: s.agent,
167
- task: s.task ?? "{previous}",
168
+ task,
168
169
  cwd: s.cwd,
169
170
  model: applyThinkingSuffix(s.model ?? a.model, a.thinking),
170
171
  tools: a.tools,
@@ -172,7 +173,28 @@ export function executeAsyncChain(
172
173
  mcpDirectTools: a.mcpDirectTools,
173
174
  systemPrompt,
174
175
  skills: resolvedSkills.map((r) => r.name),
176
+ outputPath,
175
177
  };
178
+ };
179
+
180
+ // Build runner steps — sequential steps become flat objects,
181
+ // parallel steps become { parallel: [...], concurrency?, failFast? }
182
+ const steps: RunnerStep[] = chain.map((s) => {
183
+ if (isParallelStep(s)) {
184
+ return {
185
+ parallel: s.parallel.map((t) => buildSeqStep({
186
+ agent: t.agent,
187
+ task: t.task,
188
+ cwd: t.cwd,
189
+ skill: t.skill,
190
+ model: t.model,
191
+ output: t.output,
192
+ })),
193
+ concurrency: s.concurrency,
194
+ failFast: s.failFast,
195
+ };
196
+ }
197
+ return buildSeqStep(s as SequentialStep);
176
198
  });
177
199
 
178
200
  const runnerCwd = cwd ?? ctx.cwd;
@@ -197,22 +219,34 @@ export function executeAsyncChain(
197
219
  );
198
220
 
199
221
  if (pid) {
200
- const firstAgent = chain[0] as SequentialStep;
222
+ const firstStep = chain[0];
223
+ const firstAgents = isParallelStep(firstStep)
224
+ ? firstStep.parallel.map((t) => t.agent)
225
+ : [(firstStep as SequentialStep).agent];
201
226
  ctx.pi.events.emit("subagent:started", {
202
227
  id,
203
228
  pid,
204
- agent: firstAgent.agent,
205
- task: firstAgent.task?.slice(0, 50),
206
- chain: chain.map((s) => (s as SequentialStep).agent),
229
+ agent: firstAgents[0],
230
+ task: isParallelStep(firstStep)
231
+ ? firstStep.parallel[0]?.task?.slice(0, 50)
232
+ : (firstStep as SequentialStep).task?.slice(0, 50),
233
+ chain: chain.map((s) =>
234
+ isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : (s as SequentialStep).agent,
235
+ ),
207
236
  cwd: runnerCwd,
208
237
  asyncDir,
209
238
  });
210
239
  }
211
240
 
241
+ // Build chain description with parallel groups shown as [agent1+agent2]
242
+ const chainDesc = chain
243
+ .map((s) =>
244
+ isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : (s as SequentialStep).agent,
245
+ )
246
+ .join(" -> ");
247
+
212
248
  return {
213
- content: [
214
- { type: "text", text: `Async chain: ${chain.map((s) => (s as SequentialStep).agent).join(" -> ")} [${id}]` },
215
- ],
249
+ content: [{ type: "text", text: `Async chain: ${chainDesc} [${id}]` }],
216
250
  details: { mode: "chain", results: [], asyncId: id, asyncDir },
217
251
  };
218
252
  }
package/chain-clarify.ts CHANGED
@@ -42,6 +42,8 @@ export interface ChainClarifyResult {
42
42
  templates: string[];
43
43
  /** User-modified behavior overrides per step (undefined = no changes) */
44
44
  behaviorOverrides: (BehaviorOverride | undefined)[];
45
+ /** User requested background/async execution */
46
+ runInBackground?: boolean;
45
47
  }
46
48
 
47
49
  type EditMode = "template" | "output" | "reads" | "model" | "thinking" | "skills";
@@ -88,6 +90,8 @@ export class ChainClarifyComponent implements Component {
88
90
  private saveMessageTimer: ReturnType<typeof setTimeout> | null = null;
89
91
  private saveChainNameState: TextEditorState = createEditorState();
90
92
  private savingChain = false;
93
+ /** Run in background (async) mode */
94
+ private runInBackground = false;
91
95
 
92
96
  constructor(
93
97
  private tui: TUI,
@@ -463,7 +467,7 @@ export class ChainClarifyComponent implements Component {
463
467
  for (let i = 0; i < this.agentConfigs.length; i++) {
464
468
  overrides.push(this.behaviorOverrides.get(i));
465
469
  }
466
- this.done({ confirmed: true, templates: this.templates, behaviorOverrides: overrides });
470
+ this.done({ confirmed: true, templates: this.templates, behaviorOverrides: overrides, runInBackground: this.runInBackground });
467
471
  return;
468
472
  }
469
473
 
@@ -539,6 +543,13 @@ export class ChainClarifyComponent implements Component {
539
543
  return;
540
544
  }
541
545
 
546
+ // 'b' to toggle background/async execution (all modes)
547
+ if (data === "b") {
548
+ this.runInBackground = !this.runInBackground;
549
+ this.tui.requestRender();
550
+ return;
551
+ }
552
+
542
553
  if (data === "S") {
543
554
  this.saveOverridesToAgent();
544
555
  return;
@@ -1179,13 +1190,14 @@ export class ChainClarifyComponent implements Component {
1179
1190
 
1180
1191
  /** Get footer text based on mode */
1181
1192
  private getFooterText(): string {
1193
+ const bgLabel = this.runInBackground ? '[b]g:ON' : '[b]g';
1182
1194
  switch (this.mode) {
1183
1195
  case 'single':
1184
- return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hink [w]rite [s]kill [S]ave ';
1196
+ return ` [Enter] Run • [Esc] Cancel • e m t w s ${bgLabel} S `;
1185
1197
  case 'parallel':
1186
- return ' [Enter] Run • [Esc] Cancel • [e]dit [m]odel [t]hink [s]kill [S]ave • ↑↓ Nav ';
1198
+ return ` [Enter] Run • [Esc] Cancel • e m t s ${bgLabel} S • ↑↓ Nav `;
1187
1199
  case 'chain':
1188
- return ' [Enter] Run • [Esc] Cancel • e m t w r p s S W • ↑↓ Nav ';
1200
+ return ` [Enter] Run • [Esc] Cancel • e m t w r p s ${bgLabel} S W • ↑↓ Nav `;
1189
1201
  }
1190
1202
  }
1191
1203
 
@@ -83,6 +83,11 @@ export interface ChainExecutionResult {
83
83
  content: Array<{ type: "text"; text: string }>;
84
84
  details: Details;
85
85
  isError?: boolean;
86
+ /** User requested async execution via TUI - caller should dispatch to executeAsyncChain */
87
+ requestedAsync?: {
88
+ chain: ChainStep[];
89
+ chainSkills: string[];
90
+ };
86
91
  }
87
92
 
88
93
  /**
@@ -210,6 +215,31 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
210
215
  details: { mode: "chain", results: [] },
211
216
  };
212
217
  }
218
+
219
+ // User requested background execution - return early so caller can dispatch to async
220
+ if (result.runInBackground) {
221
+ removeChainDir(chainDir); // Will be recreated by async runner
222
+ // Apply TUI edits (templates + behavior overrides) to chain steps
223
+ const updatedChain = chainSteps.map((step, i) => {
224
+ if (isParallelStep(step)) return step; // Parallel steps unchanged (TUI skipped for parallel chains)
225
+ const override = result.behaviorOverrides[i];
226
+ return {
227
+ ...step,
228
+ task: result.templates[i] as string, // Always use edited template
229
+ ...(override?.model ? { model: override.model } : {}),
230
+ ...(override?.output !== undefined ? { output: override.output } : {}),
231
+ ...(override?.reads !== undefined ? { reads: override.reads } : {}),
232
+ ...(override?.progress !== undefined ? { progress: override.progress } : {}),
233
+ ...(override?.skills !== undefined ? { skill: override.skills } : {}),
234
+ };
235
+ });
236
+ return {
237
+ content: [{ type: "text", text: "Launching in background..." }],
238
+ details: { mode: "chain", results: [] },
239
+ requestedAsync: { chain: updatedChain as ChainStep[], chainSkills },
240
+ };
241
+ }
242
+
213
243
  // Update templates from TUI result
214
244
  templates = result.templates;
215
245
  // Store behavior overrides from TUI (used below in sequential step execution)
@@ -493,7 +523,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
493
523
  // Validate expected output file was created
494
524
  if (behavior.output && r.exitCode === 0) {
495
525
  try {
496
- const expectedPath = behavior.output.startsWith("/")
526
+ const expectedPath = path.isAbsolute(behavior.output)
497
527
  ? behavior.output
498
528
  : path.join(chainDir, behavior.output);
499
529
  if (!fs.existsSync(expectedPath)) {
package/execution.ts CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  import { spawn } from "node:child_process";
6
6
  import * as fs from "node:fs";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
7
9
  import type { Message } from "@mariozechner/pi-ai";
8
10
  import type { AgentConfig } from "./agents.js";
9
11
  import {
@@ -123,7 +125,20 @@ export async function runSync(
123
125
  tmpDir = tmp.dir;
124
126
  args.push("--append-system-prompt", tmp.path);
125
127
  }
126
- args.push(`Task: ${task}`);
128
+
129
+ // When the task is too long for a CLI argument (Windows ENAMETOOLONG),
130
+ // write it to a temp file and use pi's @file syntax instead.
131
+ const TASK_ARG_LIMIT = 8000;
132
+ if (task.length > TASK_ARG_LIMIT) {
133
+ if (!tmpDir) {
134
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
135
+ }
136
+ const taskFilePath = path.join(tmpDir, "task.md");
137
+ fs.writeFileSync(taskFilePath, `Task: ${task}`, { mode: 0o600 });
138
+ args.push(`@${taskFilePath}`);
139
+ } else {
140
+ args.push(`Task: ${task}`);
141
+ }
127
142
 
128
143
  const result: SingleResult = {
129
144
  agent: agentName,