pi-subagents 0.12.2 → 0.12.4

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/chain-clarify.ts CHANGED
@@ -11,12 +11,12 @@ import { matchesKey, visibleWidth, truncateToWidth } from "@mariozechner/pi-tui"
11
11
  import * as fs from "node:fs";
12
12
  import * as os from "node:os";
13
13
  import * as path from "node:path";
14
- import type { AgentConfig, ChainConfig, ChainStepConfig } from "./agents.js";
15
- import type { ResolvedStepBehavior } from "./settings.js";
16
- import type { TextEditorState } from "./text-editor.js";
17
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.js";
18
- import { updateFrontmatterField } from "./agent-serializer.js";
19
- import { serializeChain } from "./chain-serializer.js";
14
+ import type { AgentConfig, ChainConfig, ChainStepConfig } from "./agents.ts";
15
+ import type { ResolvedStepBehavior } from "./settings.ts";
16
+ import type { TextEditorState } from "./text-editor.ts";
17
+ import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
18
+ import { updateFrontmatterField } from "./agent-serializer.ts";
19
+ import { serializeChain } from "./chain-serializer.ts";
20
20
 
21
21
  /** Clarify TUI mode */
22
22
  export type ClarifyMode = 'single' | 'parallel' | 'chain';
@@ -92,20 +92,42 @@ export class ChainClarifyComponent implements Component {
92
92
  private savingChain = false;
93
93
  /** Run in background (async) mode */
94
94
  private runInBackground = false;
95
+ private tui: TUI;
96
+ private theme: Theme;
97
+ private agentConfigs: AgentConfig[];
98
+ private templates: string[];
99
+ private originalTask: string;
100
+ private chainDir: string | undefined;
101
+ private resolvedBehaviors: ResolvedStepBehavior[];
102
+ private availableModels: ModelInfo[];
103
+ private availableSkills: Array<{ name: string; source: string; description?: string }>;
104
+ private done: (result: ChainClarifyResult) => void;
105
+ private mode: ClarifyMode;
95
106
 
96
107
  constructor(
97
- private tui: TUI,
98
- private theme: Theme,
99
- private agentConfigs: AgentConfig[],
100
- private templates: string[],
101
- private originalTask: string,
102
- private chainDir: string | undefined, // undefined for single/parallel modes
103
- private resolvedBehaviors: ResolvedStepBehavior[],
104
- private availableModels: ModelInfo[],
105
- private availableSkills: Array<{ name: string; source: string; description?: string }>,
106
- private done: (result: ChainClarifyResult) => void,
107
- private mode: ClarifyMode = 'chain', // Mode: 'single', 'parallel', or 'chain'
108
+ tui: TUI,
109
+ theme: Theme,
110
+ agentConfigs: AgentConfig[],
111
+ templates: string[],
112
+ originalTask: string,
113
+ chainDir: string | undefined, // undefined for single/parallel modes
114
+ resolvedBehaviors: ResolvedStepBehavior[],
115
+ availableModels: ModelInfo[],
116
+ availableSkills: Array<{ name: string; source: string; description?: string }>,
117
+ done: (result: ChainClarifyResult) => void,
118
+ mode: ClarifyMode = 'chain', // Mode: 'single', 'parallel', or 'chain'
108
119
  ) {
120
+ this.tui = tui;
121
+ this.theme = theme;
122
+ this.agentConfigs = agentConfigs;
123
+ this.templates = templates;
124
+ this.originalTask = originalTask;
125
+ this.chainDir = chainDir;
126
+ this.resolvedBehaviors = resolvedBehaviors;
127
+ this.availableModels = availableModels;
128
+ this.availableSkills = availableSkills;
129
+ this.done = done;
130
+ this.mode = mode;
109
131
  // Initialize filtered models
110
132
  this.filteredModels = [...availableModels];
111
133
  this.filteredSkills = [...availableSkills];
@@ -6,8 +6,8 @@ import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
8
8
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
9
- import type { AgentConfig } from "./agents.js";
10
- import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride, type ModelInfo } from "./chain-clarify.js";
9
+ import type { AgentConfig } from "./agents.ts";
10
+ import { ChainClarifyComponent, type ChainClarifyResult, type BehaviorOverride, type ModelInfo } from "./chain-clarify.ts";
11
11
  import {
12
12
  resolveChainTemplates,
13
13
  createChainDir,
@@ -24,12 +24,12 @@ import {
24
24
  type ParallelTaskResult,
25
25
  type ResolvedStepBehavior,
26
26
  type ResolvedTemplates,
27
- } from "./settings.js";
28
- import { discoverAvailableSkills, normalizeSkillInput } from "./skills.js";
29
- import { runSync } from "./execution.js";
30
- import { buildChainSummary } from "./formatters.js";
31
- import { getFinalOutput, mapConcurrent } from "./utils.js";
32
- import { recordRun } from "./run-history.js";
27
+ } from "./settings.ts";
28
+ import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
29
+ import { runSync } from "./execution.ts";
30
+ import { buildChainSummary } from "./formatters.ts";
31
+ import { getSingleResultOutput, mapConcurrent } from "./utils.ts";
32
+ import { recordRun } from "./run-history.ts";
33
33
  import {
34
34
  cleanupWorktrees,
35
35
  createWorktrees,
@@ -46,7 +46,8 @@ import {
46
46
  type Details,
47
47
  type SingleResult,
48
48
  MAX_CONCURRENCY,
49
- } from "./types.js";
49
+ resolveChildMaxSubagentDepth,
50
+ } from "./types.ts";
50
51
 
51
52
  /** Resolve a model name to its full provider/model format */
52
53
  function resolveModelFullId(modelName: string | undefined, availableModels: ModelInfo[]): string | undefined {
@@ -102,6 +103,7 @@ interface ParallelChainRunInput {
102
103
  chainAgents: string[];
103
104
  totalSteps: number;
104
105
  worktreeSetup?: WorktreeSetup;
106
+ maxSubagentDepth: number;
105
107
  }
106
108
 
107
109
  function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
@@ -191,11 +193,16 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
191
193
  const effectiveModel =
192
194
  (task.model ? resolveModelFullId(task.model, input.availableModels) : null)
193
195
  ?? resolveModelFullId(taskAgentConfig?.model, input.availableModels);
196
+ const maxSubagentDepth = resolveChildMaxSubagentDepth(input.maxSubagentDepth, taskAgentConfig?.maxSubagentDepth);
194
197
 
195
198
  const taskCwd = input.worktreeSetup
196
199
  ? input.worktreeSetup.worktrees[taskIndex]!.agentCwd
197
200
  : (task.cwd ?? input.cwd);
198
201
 
202
+ const outputPath = typeof behavior.output === "string"
203
+ ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(input.chainDir, behavior.output))
204
+ : undefined;
205
+
199
206
  const result = await runSync(input.ctx.cwd, input.agents, task.agent, taskStr, {
200
207
  cwd: taskCwd,
201
208
  signal: input.signal,
@@ -206,6 +213,8 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
206
213
  share: input.shareEnabled,
207
214
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
208
215
  artifactConfig: input.artifactConfig,
216
+ outputPath,
217
+ maxSubagentDepth,
209
218
  modelOverride: effectiveModel,
210
219
  skills: behavior.skills === false ? [] : behavior.skills,
211
220
  onUpdate: input.onUpdate
@@ -256,6 +265,9 @@ export interface ChainExecutionParams {
256
265
  onUpdate?: (r: AgentToolResult<Details>) => void;
257
266
  chainSkills?: string[];
258
267
  chainDir?: string;
268
+ maxSubagentDepth: number;
269
+ worktreeSetupHook?: string;
270
+ worktreeSetupHookTimeoutMs?: number;
259
271
  }
260
272
 
261
273
  export interface ChainExecutionResult {
@@ -458,7 +470,12 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
458
470
  );
459
471
  }
460
472
  try {
461
- worktreeSetup = createWorktrees(parallelCwd, `${runId}-s${stepIndex}`, step.parallel.length);
473
+ worktreeSetup = createWorktrees(parallelCwd, `${runId}-s${stepIndex}`, step.parallel.length, {
474
+ agents: step.parallel.map((task) => task.agent),
475
+ setupHook: params.worktreeSetupHook
476
+ ? { hookPath: params.worktreeSetupHook, timeoutMs: params.worktreeSetupHookTimeoutMs }
477
+ : undefined,
478
+ });
462
479
  } catch (error) {
463
480
  const message = error instanceof Error ? error.message : String(error);
464
481
  return buildChainExecutionErrorResult(message, {
@@ -506,6 +523,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
506
523
  chainAgents,
507
524
  totalSteps,
508
525
  worktreeSetup,
526
+ maxSubagentDepth: params.maxSubagentDepth,
509
527
  });
510
528
  globalTaskIndex += step.parallel.length;
511
529
 
@@ -551,7 +569,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
551
569
  return {
552
570
  agent: result.agent,
553
571
  taskIndex: i,
554
- output: getFinalOutput(result.messages),
572
+ output: getSingleResultOutput(result),
555
573
  exitCode: result.exitCode,
556
574
  error: result.error,
557
575
  outputTargetPath,
@@ -629,6 +647,11 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
629
647
  ?? resolveModelFullId(agentConfig.model, availableModels);
630
648
 
631
649
  // Run step
650
+ const outputPath = typeof behavior.output === "string"
651
+ ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
652
+ : undefined;
653
+ const maxSubagentDepth = resolveChildMaxSubagentDepth(params.maxSubagentDepth, agentConfig.maxSubagentDepth);
654
+
632
655
  const r = await runSync(ctx.cwd, agents, seqStep.agent, stepTask, {
633
656
  cwd: seqStep.cwd ?? cwd,
634
657
  signal,
@@ -639,6 +662,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
639
662
  share: shareEnabled,
640
663
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
641
664
  artifactConfig,
665
+ outputPath,
666
+ maxSubagentDepth,
642
667
  modelOverride: effectiveModel,
643
668
  skills: behavior.skills === false ? [] : behavior.skills,
644
669
  onUpdate: onUpdate
@@ -709,7 +734,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
709
734
  };
710
735
  }
711
736
 
712
- prev = getFinalOutput(r.messages);
737
+ prev = getSingleResultOutput(r);
713
738
  }
714
739
  }
715
740
 
@@ -1,5 +1,5 @@
1
- import type { ChainConfig, ChainStepConfig } from "./agents.js";
2
- import { parseFrontmatter } from "./frontmatter.js";
1
+ import type { ChainConfig, ChainStepConfig } from "./agents.ts";
2
+ import { parseFrontmatter } from "./frontmatter.ts";
3
3
 
4
4
  function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
5
5
  const lines = sectionBody.split("\n");
package/execution.ts CHANGED
@@ -4,13 +4,13 @@
4
4
 
5
5
  import { spawn } from "node:child_process";
6
6
  import type { Message } from "@mariozechner/pi-ai";
7
- import type { AgentConfig } from "./agents.js";
7
+ import type { AgentConfig } from "./agents.ts";
8
8
  import {
9
9
  ensureArtifactsDir,
10
10
  getArtifactPaths,
11
11
  writeArtifact,
12
12
  writeMetadata,
13
- } from "./artifacts.js";
13
+ } from "./artifacts.ts";
14
14
  import {
15
15
  type AgentProgress,
16
16
  type ArtifactPaths,
@@ -19,18 +19,19 @@ import {
19
19
  DEFAULT_MAX_OUTPUT,
20
20
  truncateOutput,
21
21
  getSubagentDepthEnv,
22
- } from "./types.js";
22
+ } from "./types.ts";
23
23
  import {
24
24
  getFinalOutput,
25
25
  findLatestSessionFile,
26
26
  detectSubagentError,
27
27
  extractToolArgsPreview,
28
28
  extractTextFromContent,
29
- } from "./utils.js";
30
- import { buildSkillInjection, resolveSkills } from "./skills.js";
31
- import { getPiSpawnCommand } from "./pi-spawn.js";
32
- import { createJsonlWriter } from "./jsonl-writer.js";
33
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "./pi-args.js";
29
+ } from "./utils.ts";
30
+ import { buildSkillInjection, resolveSkills } from "./skills.ts";
31
+ import { getPiSpawnCommand } from "./pi-spawn.ts";
32
+ import { createJsonlWriter } from "./jsonl-writer.ts";
33
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "./pi-args.ts";
34
+ import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
34
35
 
35
36
  /**
36
37
  * Run a subagent synchronously (blocking until complete)
@@ -59,6 +60,7 @@ export async function runSync(
59
60
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
60
61
  const effectiveModel = modelOverride ?? agent.model;
61
62
  const modelArg = applyThinkingSuffix(effectiveModel, agent.thinking);
63
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
62
64
 
63
65
  const skillNames = options.skills ?? agent.skills ?? [];
64
66
  const { resolved: resolvedSkills, missing: missingSkills } = resolveSkills(skillNames, runtimeCwd);
@@ -125,7 +127,7 @@ export async function runSync(
125
127
  }
126
128
  }
127
129
 
128
- const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv() };
130
+ const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
129
131
 
130
132
  let closeJsonlWriter: (() => Promise<void>) | undefined;
131
133
  const exitCode = await new Promise<number>((resolve) => {
@@ -299,9 +301,17 @@ export async function runSync(
299
301
  durationMs: progress.durationMs,
300
302
  };
301
303
 
304
+ let fullOutput = getFinalOutput(result.messages);
305
+ if (options.outputPath && result.exitCode === 0) {
306
+ const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, outputSnapshot);
307
+ fullOutput = resolvedOutput.fullOutput;
308
+ result.savedOutputPath = resolvedOutput.savedPath;
309
+ result.outputSaveError = resolvedOutput.saveError;
310
+ }
311
+ result.finalOutput = fullOutput;
312
+
302
313
  if (artifactPathsResult && artifactConfig?.enabled !== false) {
303
314
  result.artifactPaths = artifactPathsResult;
304
- const fullOutput = getFinalOutput(result.messages);
305
315
 
306
316
  if (artifactConfig?.includeOutput !== false) {
307
317
  writeArtifact(artifactPathsResult.outputPath, fullOutput);
@@ -332,7 +342,6 @@ export async function runSync(
332
342
  }
333
343
  } else if (maxOutput) {
334
344
  const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
335
- const fullOutput = getFinalOutput(result.messages);
336
345
  const truncationResult = truncateOutput(fullOutput, config);
337
346
  if (truncationResult.truncated) {
338
347
  result.truncation = truncationResult;
package/formatters.ts CHANGED
@@ -4,9 +4,9 @@
4
4
 
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
- import type { Usage, SingleResult } from "./types.js";
8
- import type { ChainStep, SequentialStep } from "./settings.js";
9
- import { isParallelStep } from "./settings.js";
7
+ import type { Usage, SingleResult } from "./types.ts";
8
+ import type { ChainStep, SequentialStep } from "./settings.ts";
9
+ import { isParallelStep } from "./settings.ts";
10
10
 
11
11
  /**
12
12
  * Format token count with k suffix for large numbers
package/index.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * Toggle: async parameter (default: false, configurable via config.json)
10
10
  *
11
11
  * Config file: ~/.pi/agent/extensions/subagent/config.json
12
- * { "asyncByDefault": true }
12
+ * { "asyncByDefault": true, "maxSubagentDepth": 1, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
13
13
  */
14
14
 
15
15
  import * as fs from "node:fs";
@@ -18,20 +18,20 @@ import * as path from "node:path";
18
18
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
19
19
  import { type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@mariozechner/pi-coding-agent";
20
20
  import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
21
- import { discoverAgents } from "./agents.js";
22
- import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "./artifacts.js";
23
- import { cleanupOldChainDirs } from "./settings.js";
24
- import { renderWidget, renderSubagentResult } from "./render.js";
25
- import { SubagentParams, StatusParams } from "./schemas.js";
26
- import { findByPrefix, readStatus } from "./utils.js";
27
- import { createSubagentExecutor } from "./subagent-executor.js";
28
- import { createAsyncJobTracker } from "./async-job-tracker.js";
29
- import { createResultWatcher } from "./result-watcher.js";
30
- import { registerSlashCommands } from "./slash-commands.js";
31
- import { registerPromptTemplateDelegationBridge } from "./prompt-template-bridge.js";
32
- import { registerSlashSubagentBridge } from "./slash-bridge.js";
33
- import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "./slash-live-state.js";
34
- import { formatAsyncRunList, listAsyncRuns } from "./async-status.js";
21
+ import { discoverAgents } from "./agents.ts";
22
+ import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "./artifacts.ts";
23
+ import { cleanupOldChainDirs } from "./settings.ts";
24
+ import { renderWidget, renderSubagentResult } from "./render.ts";
25
+ import { SubagentParams, StatusParams } from "./schemas.ts";
26
+ import { findByPrefix, readStatus } from "./utils.ts";
27
+ import { createSubagentExecutor } from "./subagent-executor.ts";
28
+ import { createAsyncJobTracker } from "./async-job-tracker.ts";
29
+ import { createResultWatcher } from "./result-watcher.ts";
30
+ import { registerSlashCommands } from "./slash-commands.ts";
31
+ import { registerPromptTemplateDelegationBridge } from "./prompt-template-bridge.ts";
32
+ import { registerSlashSubagentBridge } from "./slash-bridge.ts";
33
+ import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "./slash-live-state.ts";
34
+ import { formatAsyncRunList, listAsyncRuns } from "./async-status.ts";
35
35
  import {
36
36
  type Details,
37
37
  type ExtensionConfig,
@@ -41,7 +41,7 @@ import {
41
41
  RESULTS_DIR,
42
42
  SLASH_RESULT_TYPE,
43
43
  WIDGET_KEY,
44
- } from "./types.js";
44
+ } from "./types.ts";
45
45
 
46
46
  /**
47
47
  * Derive subagent session base directory from parent session file.
package/notify.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
- import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.js";
6
+ import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
7
7
 
8
8
  interface ChainStepResult {
9
9
  agent: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
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",
@@ -46,6 +46,13 @@
46
46
  "./notify.ts"
47
47
  ]
48
48
  },
49
+ "peerDependencies": {
50
+ "@mariozechner/pi-agent-core": "*",
51
+ "@mariozechner/pi-ai": "*",
52
+ "@mariozechner/pi-coding-agent": "*",
53
+ "@mariozechner/pi-tui": "*",
54
+ "@sinclair/typebox": "*"
55
+ },
49
56
  "devDependencies": {
50
57
  "@marcfargas/pi-test-harness": "^0.5.0",
51
58
  "@mariozechner/pi-agent-core": "^0.65.0",
package/parallel-utils.ts CHANGED
@@ -17,6 +17,7 @@ export interface RunnerSubagentStep {
17
17
  skills?: string[];
18
18
  outputPath?: string;
19
19
  sessionFile?: string;
20
+ maxSubagentDepth?: number;
20
21
  }
21
22
 
22
23
  /** Parallel step group — multiple agents running concurrently */
package/pi-spawn.ts CHANGED
@@ -83,15 +83,12 @@ export function resolveWindowsPiCliScript(deps: PiSpawnDeps = {}): string | unde
83
83
  }
84
84
 
85
85
  export function getPiSpawnCommand(args: string[], deps: PiSpawnDeps = {}): PiSpawnCommand {
86
- const platform = deps.platform ?? process.platform;
87
- if (platform === "win32") {
88
- const piCliPath = resolveWindowsPiCliScript(deps);
89
- if (piCliPath) {
90
- return {
91
- command: deps.execPath ?? process.execPath,
92
- args: [piCliPath, ...args],
93
- };
94
- }
86
+ const piCliPath = resolveWindowsPiCliScript(deps);
87
+ if (piCliPath) {
88
+ return {
89
+ command: deps.execPath ?? process.execPath,
90
+ args: [piCliPath, ...args],
91
+ };
95
92
  }
96
93
 
97
94
  return { command: "pi", args };
package/render.ts CHANGED
@@ -10,9 +10,9 @@ import {
10
10
  type Details,
11
11
  MAX_WIDGET_JOBS,
12
12
  WIDGET_KEY,
13
- } from "./types.js";
14
- import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "./formatters.js";
15
- import { getFinalOutput, getDisplayItems, getOutputTail, getLastActivity } from "./utils.js";
13
+ } from "./types.ts";
14
+ import { formatTokens, formatUsage, formatDuration, formatToolCall, shortenPath } from "./formatters.ts";
15
+ import { getDisplayItems, getLastActivity, getOutputTail, getSingleResultOutput } from "./utils.ts";
16
16
 
17
17
  type Theme = ExtensionContext["ui"]["theme"];
18
18
 
@@ -199,7 +199,7 @@ export function renderSubagentResult(
199
199
  ? theme.fg("success", "ok")
200
200
  : theme.fg("error", "X");
201
201
  const contextBadge = d.context === "fork" ? theme.fg("warning", " [fork]") : "";
202
- const output = r.truncation?.text || getFinalOutput(r.messages);
202
+ const output = r.truncation?.text || getSingleResultOutput(r);
203
203
 
204
204
  const progressInfo = isRunning && r.progress
205
205
  ? ` | ${r.progress.toolCount} tools, ${formatTokens(r.progress.tokens)} tok, ${formatDuration(r.progress.durationMs)}`
@@ -283,7 +283,7 @@ export function renderSubagentResult(
283
283
  const hasEmptyWithoutTarget = d.results.some((r) =>
284
284
  r.exitCode === 0
285
285
  && r.progress?.status !== "running"
286
- && hasEmptyTextOutputWithoutOutputTarget(r.task, getFinalOutput(r.messages)),
286
+ && hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
287
287
  );
288
288
  const icon = hasRunning
289
289
  ? theme.fg("warning", "...")
@@ -337,7 +337,7 @@ export function renderSubagentResult(
337
337
  const isComplete = result && result.exitCode === 0 && result.progress?.status !== "running";
338
338
  const isEmptyWithoutTarget = Boolean(result)
339
339
  && Boolean(isComplete)
340
- && hasEmptyTextOutputWithoutOutputTarget(result.task, getFinalOutput(result.messages));
340
+ && hasEmptyTextOutputWithoutOutputTarget(result.task, getSingleResultOutput(result));
341
341
  const isCurrent = i === (d.currentStepIndex ?? d.results.length);
342
342
  const stepIcon = isFailed
343
343
  ? theme.fg("error", "✗")
@@ -395,7 +395,7 @@ export function renderSubagentResult(
395
395
  const rProg = r.progress || progressFromArray || r.progressSummary;
396
396
  const rRunning = rProg?.status === "running";
397
397
 
398
- const resultOutput = getFinalOutput(r.messages);
398
+ const resultOutput = getSingleResultOutput(r);
399
399
  const statusIcon = rRunning
400
400
  ? theme.fg("warning", "●")
401
401
  : r.exitCode !== 0
package/schemas.ts CHANGED
@@ -70,7 +70,7 @@ export const SubagentParams = Type.Object({
70
70
  })),
71
71
  // Agent/chain configuration for create/update (nested to avoid conflicts with execution fields)
72
72
  config: Type.Optional(Type.Any({
73
- description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent."
73
+ description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent."
74
74
  })),
75
75
  tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?}, ...]" })),
76
76
  worktree: Type.Optional(Type.Boolean({
package/settings.ts CHANGED
@@ -5,8 +5,8 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
- import type { AgentConfig } from "./agents.js";
9
- import { normalizeSkillInput } from "./skills.js";
8
+ import type { AgentConfig } from "./agents.ts";
9
+ import { normalizeSkillInput } from "./skills.ts";
10
10
 
11
11
  const CHAIN_RUNS_DIR = path.join(os.tmpdir(), "pi-chain-runs");
12
12
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
package/single-output.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
 
4
+ export interface SingleOutputSnapshot {
5
+ exists: boolean;
6
+ mtimeMs?: number;
7
+ size?: number;
8
+ }
9
+
4
10
  export function resolveSingleOutputPath(
5
11
  output: string | false | undefined,
6
12
  runtimeCwd: string,
@@ -19,6 +25,16 @@ export function injectSingleOutputInstruction(task: string, outputPath: string |
19
25
  return `${task}\n\n---\n**Output:** Write your findings to: ${outputPath}`;
20
26
  }
21
27
 
28
+ export function captureSingleOutputSnapshot(outputPath: string | undefined): SingleOutputSnapshot | undefined {
29
+ if (!outputPath) return undefined;
30
+ try {
31
+ const stat = fs.statSync(outputPath);
32
+ return { exists: true, mtimeMs: stat.mtimeMs, size: stat.size };
33
+ } catch {
34
+ return { exists: false };
35
+ }
36
+ }
37
+
22
38
  export function persistSingleOutput(
23
39
  outputPath: string | undefined,
24
40
  fullOutput: string,
@@ -33,23 +49,47 @@ export function persistSingleOutput(
33
49
  }
34
50
  }
35
51
 
52
+ export function resolveSingleOutput(
53
+ outputPath: string | undefined,
54
+ fallbackOutput: string,
55
+ beforeRun: SingleOutputSnapshot | undefined,
56
+ ): { fullOutput: string; savedPath?: string; saveError?: string } {
57
+ if (!outputPath) return { fullOutput: fallbackOutput };
58
+
59
+ try {
60
+ const stat = fs.statSync(outputPath);
61
+ const changedSinceStart = !beforeRun?.exists
62
+ || stat.mtimeMs !== beforeRun.mtimeMs
63
+ || stat.size !== beforeRun.size;
64
+ if (changedSinceStart) {
65
+ return {
66
+ fullOutput: fs.readFileSync(outputPath, "utf-8"),
67
+ savedPath: outputPath,
68
+ };
69
+ }
70
+ } catch {}
71
+
72
+ const save = persistSingleOutput(outputPath, fallbackOutput);
73
+ if (save.savedPath) return { fullOutput: fallbackOutput, savedPath: save.savedPath };
74
+ return { fullOutput: fallbackOutput, saveError: save.error };
75
+ }
76
+
36
77
  export function finalizeSingleOutput(params: {
37
78
  fullOutput: string;
38
79
  truncatedOutput?: string;
39
80
  outputPath?: string;
40
81
  exitCode: number;
82
+ savedPath?: string;
83
+ saveError?: string;
41
84
  }): { displayOutput: string; savedPath?: string; saveError?: string } {
42
85
  let displayOutput = params.truncatedOutput || params.fullOutput;
43
- if (params.outputPath && params.exitCode === 0) {
44
- const save = persistSingleOutput(params.outputPath, params.fullOutput);
45
- if (save.savedPath) {
46
- displayOutput += `\n\n📄 Output saved to: ${save.savedPath}`;
47
- return { displayOutput, savedPath: save.savedPath };
48
- }
49
- if (save.error) {
50
- displayOutput += `\n\n⚠️ Failed to save output to: ${params.outputPath}\n${save.error}`;
51
- return { displayOutput, saveError: save.error };
52
- }
86
+ if (params.exitCode === 0 && params.savedPath) {
87
+ displayOutput += `\n\n📄 Output saved to: ${params.savedPath}`;
88
+ return { displayOutput, savedPath: params.savedPath };
89
+ }
90
+ if (params.exitCode === 0 && params.saveError && params.outputPath) {
91
+ displayOutput += `\n\n⚠️ Failed to save output to: ${params.outputPath}\n${params.saveError}`;
92
+ return { displayOutput, saveError: params.saveError };
53
93
  }
54
94
  return { displayOutput };
55
95
  }
package/skills.ts CHANGED
@@ -187,7 +187,9 @@ function collectSettingsSkillPaths(cwd: string): string[] {
187
187
  function buildSkillPaths(cwd: string): string[] {
188
188
  const defaultSkillPaths = [
189
189
  path.join(cwd, CONFIG_DIR, "skills"),
190
+ path.join(cwd, ".agents", "skills"),
190
191
  path.join(AGENT_DIR, "skills"),
192
+ path.join(os.homedir(), ".agents", "skills"),
191
193
  ];
192
194
  const packagePaths = collectPackageSkillPaths(cwd);
193
195
  const settingsPaths = collectSettingsSkillPaths(cwd);
@@ -203,10 +205,11 @@ function inferSkillSource(sourceInfo: { source: string; scope: string }, filePat
203
205
  // Fallback: infer from file path when sourceInfo isn't specific enough
204
206
  // (e.g. scope === "temporary" for skills loaded via explicit skillPaths)
205
207
  const projectRoot = path.resolve(cwd, CONFIG_DIR);
206
- const isProjectScoped = isWithinPath(filePath, projectRoot);
208
+ const altProjectRoot = path.resolve(cwd, ".agents");
209
+ const isProjectScoped = isWithinPath(filePath, projectRoot) || isWithinPath(filePath, altProjectRoot);
207
210
  if (isProjectScoped) return "project";
208
211
 
209
- const isUserScoped = isWithinPath(filePath, AGENT_DIR);
212
+ const isUserScoped = isWithinPath(filePath, AGENT_DIR) || isWithinPath(filePath, path.join(os.homedir(), ".agents"));
210
213
  if (isUserScoped) return "user";
211
214
 
212
215
  const globalRoot = getGlobalNpmRoot();