pi-subagents 0.12.2 → 0.12.5

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,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.12.5] - 2026-04-09
6
+
7
+ ### Fixed
8
+ - Slash-command result cards now finalize through the extension's own snapshot timing instead of relying on core to treat hidden custom messages as in-place updates. The final slash snapshot and hidden persisted message are written before the last status-clear redraw, so live `/run`, `/chain`, and `/parallel` cards update to their final state more reliably.
9
+ - Added focused slash-command regression coverage for the success/error ordering around visible placeholder messages, hidden final messages, and the final status-clear redraw.
10
+
11
+ ## [0.12.4] - 2026-04-04
12
+
13
+ ### Added
14
+ - Added configurable subagent recursion depth controls with global `maxSubagentDepth` config and per-agent `maxSubagentDepth` frontmatter overrides. Child delegation now honors stricter inherited limits while still allowing per-agent tightening.
15
+ - Added optional worktree setup hooks via extension config (`worktreeSetupHook`, `worktreeSetupHookTimeoutMs`). Hooks run once per created worktree, receive JSON over stdin, return JSON on stdout, and can declare synthetic helper paths (e.g. `.venv`, copied local config files) to exclude from patch capture.
16
+
17
+ ### Fixed
18
+ - Added support for loading agents and skills from `.agents/` and `~/.agents/` directories.
19
+ - Switched internal source imports from `.js` to `.ts` so the extension can be loaded directly from TypeScript sources under the strip-types/transform-types runtime path.
20
+ - Declared pi runtime packages and `@sinclair/typebox` as peer dependencies so direct source-loading environments fail less often from missing package resolution.
21
+ - Single-output runs now preserve agent-written file contents instead of overwriting them with the final assistant receipt, and artifacts/truncation now follow the authoritative saved file content.
22
+ - Async/background runs now reuse the current Node executable and prefer the resolved current pi CLI path on all platforms, avoiding PATH drift from wrapped or version-pinned parent launches.
23
+
24
+ ### Changed
25
+ - Added release documentation for TypeScript direct-runtime loading support and related package requirements.
26
+
5
27
  ## [0.12.2] - 2026-04-04
6
28
 
7
29
  ### Changed
package/README.md CHANGED
@@ -69,6 +69,7 @@ output: context.md # writes to {chain_dir}/context.md
69
69
  defaultReads: context.md # comma-separated files to read
70
70
  defaultProgress: true # maintain progress.md
71
71
  interactive: true # (parsed but not enforced in v1)
72
+ maxSubagentDepth: 1 # tighten nested delegation for this agent's children
72
73
  ---
73
74
 
74
75
  Your system prompt goes here (the markdown body after frontmatter).
@@ -303,7 +304,7 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
303
304
  - **Parallel-in-Chain**: Fan-out/fan-in patterns with `{ parallel: [...] }` steps within chains
304
305
  - **Worktree Isolation**: `worktree: true` gives each parallel agent its own git worktree, preventing filesystem conflicts during concurrent execution
305
306
  - **Chain Clarification TUI**: Interactive preview/edit of chain templates and behaviors before execution
306
- - **Agent Frontmatter Extensions**: Agents declare default chain behavior (`output`, `defaultReads`, `defaultProgress`, `skill`)
307
+ - **Agent Frontmatter Extensions**: Agents declare default chain behavior (`output`, `defaultReads`, `defaultProgress`, `skill`) plus optional recursion limits via `maxSubagentDepth`
307
308
  - **Chain Artifacts**: Shared directory at `<tmpdir>/pi-chain-runs/{runId}/` for inter-step files
308
309
  - **Solo Agent Output**: Agents with `output` write to temp dir and return path to caller
309
310
  - **Live Progress Display**: Real-time visibility during sync execution showing current tool, recent output, tokens, and duration
@@ -589,7 +590,7 @@ Agent definitions are not loaded into LLM context by default. Management actions
589
590
  Notes:
590
591
  - `create` uses `config.scope` (`"user"` or `"project"`), not `agentScope`.
591
592
  - `update`/`delete` use `agentScope` only for scope disambiguation when the same name exists in both scopes.
592
- - Agent config mapping: `reads -> defaultReads`, `progress -> defaultProgress`, `extensions` controls extension sandboxing, and `tools` supports `mcp:` entries that map to direct MCP tools.
593
+ - Agent config mapping: `reads -> defaultReads`, `progress -> defaultProgress`, `extensions` controls extension sandboxing, `maxSubagentDepth` maps directly to agent frontmatter, and `tools` supports `mcp:` entries that map to direct MCP tools.
593
594
  - To clear any optional field, set it to `false` or `""` (e.g., `{ model: false }` or `{ skills: "" }`). Both work for all string-typed fields.
594
595
 
595
596
  ## Parameters
@@ -695,14 +696,17 @@ After the parallel step completes, per-agent diff stats are appended to the outp
695
696
  - Working tree must be clean (no uncommitted changes) — commit or stash first
696
697
  - `node_modules/` is symlinked into each worktree to avoid reinstalling
697
698
  - Worktree runs use the shared parallel/step `cwd`. Task-level `cwd` overrides must be omitted or match that shared `cwd`; if you need different working directories, disable `worktree` or split the run.
699
+ - If `worktreeSetupHook` is configured, it must return valid JSON and complete before timeout
698
700
 
699
701
  **What happens under the hood:**
700
702
 
701
703
  1. `git worktree add` creates a temporary worktree per agent in `<tmpdir>/pi-worktree-*`
702
- 2. Each agent runs in its worktree's cwd (preserving subdirectory context)
703
- 3. After execution, `git add -A && git diff --cached` captures all changes (committed, modified, and new files)
704
- 4. Diff stats appear in the aggregated output; full `.patch` files are written to the artifacts directory
705
- 5. Worktrees and temp branches are cleaned up in a `finally` block
704
+ 2. Optional `worktreeSetupHook` runs once per worktree (JSON in on stdin, JSON out on stdout)
705
+ 3. Each agent runs in its worktree's cwd (preserving subdirectory context)
706
+ 4. Before diff capture, declared synthetic helper paths (for example `.venv` or copied local config files) are removed
707
+ 5. After execution, `git add -A && git diff --cached` captures all real changes (committed, modified, and new files)
708
+ 6. Diff stats appear in the aggregated output; full `.patch` files are written to the artifacts directory
709
+ 7. Worktrees and temp branches are cleaned up in a `finally` block
706
710
 
707
711
  If you use [pi-prompt-template-model](https://github.com/nicobailon/pi-prompt-template-model), worktree isolation is also available via `worktree: true` in chain template frontmatter or the `--worktree` CLI flag on `chain-prompts`. `pi-prompt-template-model` compare-style prompts can route through the same worktree machinery too; see the `pi-prompt-template-model` README and `examples/` directory for the installable prompt templates.
708
712
 
@@ -749,6 +753,54 @@ Session root resolution follows this precedence:
749
753
 
750
754
  Sessions are always enabled — every subagent run gets a session directory for tracking.
751
755
 
756
+ ### `maxSubagentDepth`
757
+
758
+ `maxSubagentDepth` sets the default recursion limit for nested delegation when no inherited `PI_SUBAGENT_MAX_DEPTH` is already in effect. Eg:
759
+
760
+ ```json
761
+ {
762
+ "maxSubagentDepth": 1
763
+ }
764
+ ```
765
+
766
+ Per-agent `maxSubagentDepth` can tighten that limit further for child runs, but it does not relax an already inherited stricter limit.
767
+
768
+ ### `worktreeSetupHook`
769
+
770
+ `worktreeSetupHook` configures an optional setup hook for worktree-isolated parallel runs. The hook runs once per created worktree, after `git worktree add` succeeds and before the agent starts.
771
+
772
+ ```json
773
+ {
774
+ "worktreeSetupHook": "./scripts/setup-worktree.mjs"
775
+ }
776
+ ```
777
+
778
+ Path rules:
779
+ - Must be an absolute path or a repo-relative path
780
+ - Bare command names from `PATH` are rejected
781
+ - `~/...` is supported for home-directory hooks
782
+
783
+ Hook I/O contract (JSON only):
784
+ - stdin: one JSON object with `repoRoot`, `worktreePath`, `agentCwd`, `branch`, `index`, `runId`, and `baseCommit`
785
+ - stdout: one JSON object, e.g. `{ "syntheticPaths": [".venv", ".env.local"] }`
786
+
787
+ `syntheticPaths` must be relative to the worktree root. These paths are removed before diff capture so helper files/symlinks do not pollute generated patches.
788
+
789
+ Tracked-file edits are never excluded. If the hook tries to mark tracked paths as synthetic, setup fails.
790
+
791
+ ### `worktreeSetupHookTimeoutMs`
792
+
793
+ Optional timeout (milliseconds) for each worktree hook invocation.
794
+
795
+ ```json
796
+ {
797
+ "worktreeSetupHook": "./scripts/setup-worktree.mjs",
798
+ "worktreeSetupHookTimeoutMs": 45000
799
+ }
800
+ ```
801
+
802
+ Default: `30000` ms.
803
+
752
804
  ## Chain Directory
753
805
  Each chain run creates `<tmpdir>/pi-chain-runs/{runId}/` containing:
754
806
  - `context.md` - Scout/context-builder output
@@ -821,6 +873,14 @@ Subagents can themselves call the `subagent` tool, which risks unbounded recursi
821
873
 
822
874
  By default nesting is limited to **2 levels**: `main session → subagent → sub-subagent`. Any deeper `subagent` calls are blocked and return an error with guidance to the calling agent.
823
875
 
876
+ You can configure the limit in three places:
877
+
878
+ 1. `PI_SUBAGENT_MAX_DEPTH` in the environment before starting `pi`
879
+ 2. `config.maxSubagentDepth` in `~/.pi/agent/extensions/subagent/config.json`
880
+ 3. `maxSubagentDepth` in an agent's frontmatter to tighten the limit for that agent's child runs
881
+
882
+ Environment inherits downward and wins for the current process. Per-agent limits can tighten child delegation but do not relax an already inherited stricter limit.
883
+
824
884
  Override the limit with `PI_SUBAGENT_MAX_DEPTH` **set before starting `pi`**:
825
885
 
826
886
  ```bash
@@ -9,11 +9,11 @@ import {
9
9
  type ChainConfig,
10
10
  type ChainStepConfig,
11
11
  discoverAgentsAll,
12
- } from "./agents.js";
13
- import { serializeAgent } from "./agent-serializer.js";
14
- import { serializeChain } from "./chain-serializer.js";
15
- import { discoverAvailableSkills } from "./skills.js";
16
- import type { Details } from "./types.js";
12
+ } from "./agents.ts";
13
+ import { serializeAgent } from "./agent-serializer.ts";
14
+ import { serializeChain } from "./chain-serializer.ts";
15
+ import { discoverAvailableSkills } from "./skills.ts";
16
+ import type { Details } from "./types.ts";
17
17
 
18
18
  type ManagementAction = "list" | "get" | "create" | "update" | "delete";
19
19
  type ManagementScope = "user" | "project";
@@ -235,6 +235,12 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
235
235
  if (typeof cfg.progress !== "boolean") return "config.progress must be a boolean when provided.";
236
236
  target.defaultProgress = cfg.progress;
237
237
  }
238
+ if (hasKey(cfg, "maxSubagentDepth")) {
239
+ if (cfg.maxSubagentDepth === false || cfg.maxSubagentDepth === "") target.maxSubagentDepth = undefined;
240
+ else if (typeof cfg.maxSubagentDepth === "number" && Number.isInteger(cfg.maxSubagentDepth) && cfg.maxSubagentDepth >= 0) {
241
+ target.maxSubagentDepth = cfg.maxSubagentDepth;
242
+ } else return "config.maxSubagentDepth must be an integer >= 0 or false when provided.";
243
+ }
238
244
  return undefined;
239
245
  }
240
246
 
@@ -293,6 +299,7 @@ export function formatAgentDetail(agent: AgentConfig): string {
293
299
  if (agent.output) lines.push(`Output: ${agent.output}`);
294
300
  if (agent.defaultReads?.length) lines.push(`Reads: ${agent.defaultReads.join(", ")}`);
295
301
  if (agent.defaultProgress) lines.push("Progress: true");
302
+ if (agent.maxSubagentDepth !== undefined) lines.push(`Max subagent depth: ${agent.maxSubagentDepth}`);
296
303
  if (agent.systemPrompt.trim()) lines.push("", "System Prompt:", agent.systemPrompt);
297
304
  return lines.join("\n");
298
305
  }
@@ -1,7 +1,7 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
- import type { ChainConfig, ChainStepConfig } from "./agents.js";
4
- import { row, renderFooter, renderHeader, formatPath, formatScrollInfo } from "./render-helpers.js";
3
+ import type { ChainConfig, ChainStepConfig } from "./agents.ts";
4
+ import { row, renderFooter, renderHeader, formatPath, formatScrollInfo } from "./render-helpers.ts";
5
5
 
6
6
  export interface ChainDetailState {
7
7
  scrollOffset: number;
@@ -1,12 +1,12 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
- import type { AgentConfig } from "./agents.js";
4
- import { formatDuration } from "./formatters.js";
5
- import type { RunEntry } from "./run-history.js";
6
- import { buildSkillInjection, resolveSkills } from "./skills.js";
7
- import { ensureCursorVisible, getCursorDisplayPos, renderEditor, wrapText } from "./text-editor.js";
8
- import type { TextEditorState } from "./text-editor.js";
9
- import { pad, row, renderHeader, renderFooter, formatPath, formatScrollInfo } from "./render-helpers.js";
3
+ import type { AgentConfig } from "./agents.ts";
4
+ import { formatDuration } from "./formatters.ts";
5
+ import type { RunEntry } from "./run-history.ts";
6
+ import { buildSkillInjection, resolveSkills } from "./skills.ts";
7
+ import { ensureCursorVisible, getCursorDisplayPos, renderEditor, wrapText } from "./text-editor.ts";
8
+ import type { TextEditorState } from "./text-editor.ts";
9
+ import { pad, row, renderHeader, renderFooter, formatPath, formatScrollInfo } from "./render-helpers.ts";
10
10
 
11
11
  export interface DetailState {
12
12
  resolved: boolean;
@@ -58,6 +58,7 @@ function buildDetailLines(
58
58
  const output = agent.output ?? "(none)";
59
59
  const reads = agent.defaultReads && agent.defaultReads.length > 0 ? agent.defaultReads.join(", ") : "(none)";
60
60
  const progress = agent.defaultProgress ? "on" : "off";
61
+ const maxSubagentDepth = agent.maxSubagentDepth !== undefined ? String(agent.maxSubagentDepth) : "(default)";
61
62
 
62
63
  lines.push(renderFieldLine("Model:", agent.model ?? "default", contentWidth, theme));
63
64
  lines.push(renderFieldLine("Thinking:", agent.thinking ?? "off", contentWidth, theme));
@@ -71,6 +72,7 @@ function buildDetailLines(
71
72
  lines.push(renderFieldLine("Output:", output, contentWidth, theme));
72
73
  lines.push(renderFieldLine("Reads:", reads, contentWidth, theme));
73
74
  lines.push(renderFieldLine("Progress:", progress, contentWidth, theme));
75
+ lines.push(renderFieldLine("Max depth:", maxSubagentDepth, contentWidth, theme));
74
76
 
75
77
  if (agent.extraFields) {
76
78
  for (const [key, value] of Object.entries(agent.extraFields)) {
@@ -1,9 +1,9 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
- import type { AgentConfig } from "./agents.js";
4
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.js";
5
- import type { TextEditorState } from "./text-editor.js";
6
- import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "./render-helpers.js";
3
+ import type { AgentConfig } from "./agents.ts";
4
+ import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
5
+ import type { TextEditorState } from "./text-editor.ts";
6
+ import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "./render-helpers.ts";
7
7
 
8
8
  export interface ModelInfo { provider: string; id: string; fullId: string; }
9
9
  export interface SkillInfo { name: string; source: string; description?: string; }
@@ -1,7 +1,7 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import type { AgentSource } from "./agents.js";
2
+ import type { AgentSource } from "./agents.ts";
3
3
  import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
4
- import { pad, row, renderHeader, renderFooter, fuzzyFilter, formatScrollInfo } from "./render-helpers.js";
4
+ import { pad, row, renderHeader, renderFooter, fuzzyFilter, formatScrollInfo } from "./render-helpers.ts";
5
5
 
6
6
  export interface ListAgent {
7
7
  id: string;
@@ -1,8 +1,8 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
3
- import type { TextEditorState } from "./text-editor.js";
4
- import { createEditorState, handleEditorInput, renderEditor, wrapText, getCursorDisplayPos, ensureCursorVisible } from "./text-editor.js";
5
- import { pad, row, renderHeader, renderFooter, fuzzyFilter } from "./render-helpers.js";
3
+ import type { TextEditorState } from "./text-editor.ts";
4
+ import { createEditorState, handleEditorInput, renderEditor, wrapText, getCursorDisplayPos, ensureCursorVisible } from "./text-editor.ts";
5
+ import { pad, row, renderHeader, renderFooter, fuzzyFilter } from "./render-helpers.ts";
6
6
 
7
7
  export interface ParallelSlot {
8
8
  agentName: string;
package/agent-manager.ts CHANGED
@@ -3,19 +3,19 @@ import * as path from "node:path";
3
3
  import type { Theme } from "@mariozechner/pi-coding-agent";
4
4
  import type { Component, TUI } from "@mariozechner/pi-tui";
5
5
  import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
6
- import type { AgentConfig, ChainConfig } from "./agents.js";
7
- import { serializeAgent } from "./agent-serializer.js";
8
- import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "./agent-templates.js";
9
- import { parseChain, serializeChain } from "./chain-serializer.js";
10
- import { renderList, handleListInput, type ListAgent, type ListState, type ListAction } from "./agent-manager-list.js";
11
- import { createParallelState, handleParallelInput, renderParallel, formatParallelTitle, type ParallelState, type AgentOption } from "./agent-manager-parallel.js";
12
- import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction } from "./agent-manager-detail.js";
13
- import { renderChainDetail, handleChainDetailInput, type ChainDetailAction, type ChainDetailState } from "./agent-manager-chain-detail.js";
14
- import { createEditState, handleEditInput, renderEdit, type EditScreen, type EditState, type ModelInfo, type SkillInfo } from "./agent-manager-edit.js";
15
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.js";
16
- import type { TextEditorState } from "./text-editor.js";
17
- import { loadRunsForAgent } from "./run-history.js";
18
- import { pad, row, renderHeader, renderFooter } from "./render-helpers.js";
6
+ import type { AgentConfig, ChainConfig } from "./agents.ts";
7
+ import { serializeAgent } from "./agent-serializer.ts";
8
+ import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "./agent-templates.ts";
9
+ import { parseChain, serializeChain } from "./chain-serializer.ts";
10
+ import { renderList, handleListInput, type ListAgent, type ListState, type ListAction } from "./agent-manager-list.ts";
11
+ import { createParallelState, handleParallelInput, renderParallel, formatParallelTitle, type ParallelState, type AgentOption } from "./agent-manager-parallel.ts";
12
+ import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction } from "./agent-manager-detail.ts";
13
+ import { renderChainDetail, handleChainDetailInput, type ChainDetailAction, type ChainDetailState } from "./agent-manager-chain-detail.ts";
14
+ import { createEditState, handleEditInput, renderEdit, type EditScreen, type EditState, type ModelInfo, type SkillInfo } from "./agent-manager-edit.ts";
15
+ import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
16
+ import type { TextEditorState } from "./text-editor.ts";
17
+ import { loadRunsForAgent } from "./run-history.ts";
18
+ import { pad, row, renderHeader, renderFooter } from "./render-helpers.ts";
19
19
 
20
20
  export type ManagerResult =
21
21
  | { action: "launch"; agent: string; task: string; skipClarify?: boolean }
@@ -61,8 +61,22 @@ export class AgentManagerComponent implements Component {
61
61
  private templateCursor = 0;
62
62
  private statusMessage?: StatusMessage;
63
63
  private nextId = 1;
64
+ private tui: TUI;
65
+ private theme: Theme;
66
+ private agentData: AgentData;
67
+ private models: ModelInfo[];
68
+ private skills: SkillInfo[];
69
+ private done: (result: ManagerResult) => void;
64
70
 
65
- constructor(private tui: TUI, private theme: Theme, private agentData: AgentData, private models: ModelInfo[], private skills: SkillInfo[], private done: (result: ManagerResult) => void) { this.loadEntries(); }
71
+ constructor(tui: TUI, theme: Theme, agentData: AgentData, models: ModelInfo[], skills: SkillInfo[], done: (result: ManagerResult) => void) {
72
+ this.tui = tui;
73
+ this.theme = theme;
74
+ this.agentData = agentData;
75
+ this.models = models;
76
+ this.skills = skills;
77
+ this.done = done;
78
+ this.loadEntries();
79
+ }
66
80
 
67
81
  private loadEntries(): void {
68
82
  const overridden = new Set([...this.agentData.user, ...this.agentData.project].map((c) => c.name));
package/agent-scope.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgentScope } from "./agents.js";
1
+ import type { AgentScope } from "./agents.ts";
2
2
 
3
3
  export function resolveExecutionAgentScope(scope: unknown): AgentScope {
4
4
  if (scope === "user" || scope === "project" || scope === "both") return scope;
@@ -1,4 +1,4 @@
1
- import type { AgentScope, AgentConfig } from "./agents.js";
1
+ import type { AgentScope, AgentConfig } from "./agents.ts";
2
2
 
3
3
  export function mergeAgentsForScope(
4
4
  scope: AgentScope,
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs";
2
- import type { AgentConfig } from "./agents.js";
2
+ import type { AgentConfig } from "./agents.ts";
3
3
 
4
4
  export const KNOWN_FIELDS = new Set([
5
5
  "name",
@@ -14,6 +14,7 @@ export const KNOWN_FIELDS = new Set([
14
14
  "defaultReads",
15
15
  "defaultProgress",
16
16
  "interactive",
17
+ "maxSubagentDepth",
17
18
  ]);
18
19
 
19
20
  function joinComma(values: string[] | undefined): string | undefined {
@@ -52,6 +53,9 @@ export function serializeAgent(config: AgentConfig): string {
52
53
 
53
54
  if (config.defaultProgress) lines.push("defaultProgress: true");
54
55
  if (config.interactive) lines.push("interactive: true");
56
+ if (Number.isInteger(config.maxSubagentDepth) && config.maxSubagentDepth >= 0) {
57
+ lines.push(`maxSubagentDepth: ${config.maxSubagentDepth}`);
58
+ }
55
59
 
56
60
  if (config.extraFields) {
57
61
  for (const [key, value] of Object.entries(config.extraFields)) {
@@ -1,4 +1,4 @@
1
- import type { AgentConfig } from "./agents.js";
1
+ import type { AgentConfig } from "./agents.ts";
2
2
 
3
3
  export interface AgentTemplate {
4
4
  name: string;
package/agents.ts CHANGED
@@ -6,10 +6,10 @@ import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { KNOWN_FIELDS } from "./agent-serializer.js";
10
- import { parseChain } from "./chain-serializer.js";
11
- import { mergeAgentsForScope } from "./agent-selection.js";
12
- import { parseFrontmatter } from "./frontmatter.js";
9
+ import { KNOWN_FIELDS } from "./agent-serializer.ts";
10
+ import { parseChain } from "./chain-serializer.ts";
11
+ import { mergeAgentsForScope } from "./agent-selection.ts";
12
+ import { parseFrontmatter } from "./frontmatter.ts";
13
13
 
14
14
  export type AgentScope = "user" | "project" | "both";
15
15
 
@@ -32,6 +32,7 @@ export interface AgentConfig {
32
32
  defaultReads?: string[];
33
33
  defaultProgress?: boolean;
34
34
  interactive?: boolean;
35
+ maxSubagentDepth?: number;
35
36
  extraFields?: Record<string, string>;
36
37
  }
37
38
 
@@ -134,6 +135,8 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
134
135
  if (!KNOWN_FIELDS.has(key)) extraFields[key] = value;
135
136
  }
136
137
 
138
+ const parsedMaxSubagentDepth = Number(frontmatter.maxSubagentDepth);
139
+
137
140
  agents.push({
138
141
  name: frontmatter.name,
139
142
  description: frontmatter.description,
@@ -151,6 +154,10 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
151
154
  defaultReads: defaultReads && defaultReads.length > 0 ? defaultReads : undefined,
152
155
  defaultProgress: frontmatter.defaultProgress === "true",
153
156
  interactive: frontmatter.interactive === "true",
157
+ maxSubagentDepth:
158
+ Number.isInteger(parsedMaxSubagentDepth) && parsedMaxSubagentDepth >= 0
159
+ ? parsedMaxSubagentDepth
160
+ : undefined,
154
161
  extraFields: Object.keys(extraFields).length > 0 ? extraFields : undefined,
155
162
  });
156
163
  }
@@ -205,6 +212,9 @@ function isDirectory(p: string): boolean {
205
212
  function findNearestProjectAgentsDir(cwd: string): string | null {
206
213
  let currentDir = cwd;
207
214
  while (true) {
215
+ const candidateAlt = path.join(currentDir, ".agents");
216
+ if (isDirectory(candidateAlt)) return candidateAlt;
217
+
208
218
  const candidate = path.join(currentDir, ".pi", "agents");
209
219
  if (isDirectory(candidate)) return candidate;
210
220
 
@@ -217,11 +227,16 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
217
227
  const BUILTIN_AGENTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "agents");
218
228
 
219
229
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
220
- const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
230
+ const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
231
+ const userDirNew = path.join(os.homedir(), ".agents");
221
232
  const projectAgentsDir = findNearestProjectAgentsDir(cwd);
222
233
 
223
234
  const builtinAgents = loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
224
- const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
235
+
236
+ const userAgentsOld = scope === "project" ? [] : loadAgentsFromDir(userDirOld, "user");
237
+ const userAgentsNew = scope === "project" ? [] : loadAgentsFromDir(userDirNew, "user");
238
+ const userAgents = [...userAgentsOld, ...userAgentsNew];
239
+
225
240
  const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
226
241
  const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents);
227
242
 
@@ -236,16 +251,23 @@ export function discoverAgentsAll(cwd: string): {
236
251
  userDir: string;
237
252
  projectDir: string | null;
238
253
  } {
239
- const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
254
+ const userDirOld = path.join(os.homedir(), ".pi", "agent", "agents");
255
+ const userDirNew = path.join(os.homedir(), ".agents");
240
256
  const projectDir = findNearestProjectAgentsDir(cwd);
241
257
 
242
258
  const builtin = loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
243
- const user = loadAgentsFromDir(userDir, "user");
259
+ const user = [
260
+ ...loadAgentsFromDir(userDirOld, "user"),
261
+ ...loadAgentsFromDir(userDirNew, "user"),
262
+ ];
244
263
  const project = projectDir ? loadAgentsFromDir(projectDir, "project") : [];
245
264
  const chains = [
246
- ...loadChainsFromDir(userDir, "user"),
265
+ ...loadChainsFromDir(userDirOld, "user"),
266
+ ...loadChainsFromDir(userDirNew, "user"),
247
267
  ...(projectDir ? loadChainsFromDir(projectDir, "project") : []),
248
268
  ];
249
269
 
270
+ const userDir = fs.existsSync(userDirNew) ? userDirNew : userDirOld;
271
+
250
272
  return { builtin, user, project, chains, userDir, projectDir };
251
273
  }
package/artifacts.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import type { ArtifactPaths } from "./types.js";
4
+ import type { ArtifactPaths } from "./types.ts";
5
5
 
6
6
  const TEMP_ARTIFACTS_DIR = path.join(os.tmpdir(), "pi-subagent-artifacts");
7
7
  const CLEANUP_MARKER_FILE = ".last-cleanup";
@@ -9,20 +9,21 @@ import * as path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { createRequire } from "node:module";
11
11
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
- import type { AgentConfig } from "./agents.js";
13
- import { applyThinkingSuffix } from "./pi-args.js";
14
- import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.js";
15
- import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep, type StepOverrides } from "./settings.js";
16
- import type { RunnerStep } from "./parallel-utils.js";
17
- import { resolvePiPackageRoot } from "./pi-spawn.js";
18
- import { buildSkillInjection, normalizeSkillInput, resolveSkills } from "./skills.js";
12
+ import type { AgentConfig } from "./agents.ts";
13
+ import { applyThinkingSuffix } from "./pi-args.ts";
14
+ import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
15
+ import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep, type StepOverrides } from "./settings.ts";
16
+ import type { RunnerStep } from "./parallel-utils.ts";
17
+ import { resolvePiPackageRoot } from "./pi-spawn.ts";
18
+ import { buildSkillInjection, normalizeSkillInput, resolveSkills } from "./skills.ts";
19
19
  import {
20
20
  type ArtifactConfig,
21
21
  type Details,
22
22
  type MaxOutputConfig,
23
23
  ASYNC_DIR,
24
24
  RESULTS_DIR,
25
- } from "./types.js";
25
+ resolveChildMaxSubagentDepth,
26
+ } from "./types.ts";
26
27
 
27
28
  const require = createRequire(import.meta.url);
28
29
  const piPackageRoot = resolvePiPackageRoot();
@@ -65,6 +66,9 @@ export interface AsyncChainParams {
65
66
  sessionRoot?: string;
66
67
  chainSkills?: string[];
67
68
  sessionFilesByFlatIndex?: (string | undefined)[];
69
+ maxSubagentDepth: number;
70
+ worktreeSetupHook?: string;
71
+ worktreeSetupHookTimeoutMs?: number;
68
72
  }
69
73
 
70
74
  export interface AsyncSingleParams {
@@ -81,6 +85,9 @@ export interface AsyncSingleParams {
81
85
  sessionFile?: string;
82
86
  skills?: string[];
83
87
  output?: string | false;
88
+ maxSubagentDepth: number;
89
+ worktreeSetupHook?: string;
90
+ worktreeSetupHookTimeoutMs?: number;
84
91
  }
85
92
 
86
93
  export interface AsyncExecutionResult {
@@ -106,7 +113,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): number | undefin
106
113
  fs.writeFileSync(cfgPath, JSON.stringify(cfg));
107
114
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
108
115
 
109
- const proc = spawn("node", [jitiCliPath, runner, cfgPath], {
116
+ const proc = spawn(process.execPath, [jitiCliPath, runner, cfgPath], {
110
117
  cwd,
111
118
  detached: true,
112
119
  stdio: "ignore",
@@ -134,6 +141,9 @@ export function executeAsyncChain(
134
141
  shareEnabled,
135
142
  sessionRoot,
136
143
  sessionFilesByFlatIndex,
144
+ maxSubagentDepth,
145
+ worktreeSetupHook,
146
+ worktreeSetupHookTimeoutMs,
137
147
  } = params;
138
148
  const chainSkills = params.chainSkills ?? [];
139
149
 
@@ -197,6 +207,7 @@ export function executeAsyncChain(
197
207
  skills: resolvedSkills.map((r) => r.name),
198
208
  outputPath,
199
209
  sessionFile,
210
+ maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
200
211
  };
201
212
  };
202
213
 
@@ -244,6 +255,8 @@ export function executeAsyncChain(
244
255
  asyncDir,
245
256
  sessionId: ctx.currentSessionId,
246
257
  piPackageRoot,
258
+ worktreeSetupHook,
259
+ worktreeSetupHookTimeoutMs,
247
260
  },
248
261
  id,
249
262
  runnerCwd,
@@ -301,6 +314,9 @@ export function executeAsyncSingle(
301
314
  shareEnabled,
302
315
  sessionRoot,
303
316
  sessionFile,
317
+ maxSubagentDepth,
318
+ worktreeSetupHook,
319
+ worktreeSetupHookTimeoutMs,
304
320
  } = params;
305
321
  const skillNames = params.skills ?? agentConfig.skills ?? [];
306
322
  const { resolved: resolvedSkills } = resolveSkills(skillNames, ctx.cwd);
@@ -341,6 +357,7 @@ export function executeAsyncSingle(
341
357
  skills: resolvedSkills.map((r) => r.name),
342
358
  outputPath,
343
359
  sessionFile,
360
+ maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
344
361
  },
345
362
  ],
346
363
  resultPath: path.join(RESULTS_DIR, `${id}.json`),
@@ -354,6 +371,8 @@ export function executeAsyncSingle(
354
371
  asyncDir,
355
372
  sessionId: ctx.currentSessionId,
356
373
  piPackageRoot,
374
+ worktreeSetupHook,
375
+ worktreeSetupHookTimeoutMs,
357
376
  },
358
377
  id,
359
378
  runnerCwd,