pi-subagents 0.14.1 → 0.16.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,37 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.16.0] - 2026-04-16
6
+
7
+ ### Added
8
+ - Top-level parallel `tasks` mode now supports a per-call `concurrency` override, matching the existing chain parallel-step concurrency control. This ships part of issue `#91`. Thanks @Gabrielgvl.
9
+
10
+ ### Changed
11
+ - Top-level parallel defaults and limits can now be configured through `~/.pi/agent/extensions/subagent/config.json` under `parallel.maxTasks` and `parallel.concurrency`, while keeping the existing defaults of 8 tasks and concurrency 4 when unset. This completes issue `#91`. Thanks @Gabrielgvl.
12
+
13
+ ### Fixed
14
+ - `context: "fork"` sync runs now create child sessions from a throwaway session-manager instance opened on the persisted parent session file, instead of mutating the live parent session manager. This keeps the parent session writing to its own file so the matching `toolResult(subagent)` no longer lands in a descendant session by accident. This fixes issue `#87`. Thanks @asmisha.
15
+ - Project agent and chain discovery now reads both `.agents/` and `.pi/agents/`, while preferring `.pi/agents/` when both locations define the same parsed name and keeping manager writes on the `.pi/agents/` path. This fixes issue `#88`. Thanks @desek.
16
+ - Ctrl+O expanded subagent results now actually show expanded content. Previously the `expanded` flag was received but ignored, so task text and tool-call args were identically truncated in both views. Now expanded mode shows the full task and longer (but still bounded) tool-call previews. Additionally, tool calls are no longer lost after foreground compaction: compact display summaries are preserved and shown in expanded view even after `messages` are stripped. This addresses issue `#90`. Thanks @asagajda.
17
+
18
+ ## [0.15.0] - 2026-04-16
19
+
20
+ ### Added
21
+ - Added `systemPromptMode` so subagents can replace Pi's base prompt with `--system-prompt` instead of always appending with `--append-system-prompt`, shipping the core of issue `#85` from @isvlasov.
22
+ - Added `inheritProjectContext` and `inheritSkills` so child runs can keep or strip inherited project instruction files (`AGENTS.md`, `CLAUDE.md`, etc.) and Pi's discovered skills block.
23
+
24
+ ### Changed
25
+ - Builtin subagents now default to `systemPromptMode: replace`, with builtin `delegate` staying on `append`.
26
+ - Builtin agents now inherit project-level instruction files by default unless the user overrides them.
27
+ - Builtin agent prompts were rewritten for the new prompt-assembly model, and builtin `reviewer` / `context-builder` tool lists now match their documented behaviors. This rounds out the prompt-assembly work merged in PR `#92`, which closed issue `#85`. Thanks @isvlasov.
28
+
29
+ ### Fixed
30
+ - Cross-platform tests now avoid machine-specific Pi install paths, align homedir-sensitive settings discovery on Windows CI, and use deterministic async config-write failure fixtures.
31
+ - Request-level `cwd` handling is now consistent across management and execution paths. `subagent` requests that target a worktree or nested checkout now resolve project agents, project settings, and builtin agent overrides from the requested `cwd` instead of accidentally inheriting the parent session's repo. This fixes issue `#83`. Thanks @hakin19 for the report.
32
+ - Relative child `cwd` values now resolve from the already-selected request/shared `cwd` across sync runs, async/background runs, chain steps, and top-level parallel tasks. This fixes cases where values like `packages/app` were interpreted from the wrong base directory, which could break skill lookup, output paths, and child process spawning.
33
+ - Worktree parallel-mode validation now compares task-level `cwd` overrides after relative-path resolution, so equivalent paths like `.` no longer trigger false conflict errors against the shared worktree base.
34
+ - Internal TypeScript source imports in the touched runtime paths now consistently use `.ts` local specifiers, matching the repo's direct TypeScript runtime loading conventions and reducing drift between adjacent modules.
35
+
5
36
  ## [0.14.1] - 2026-04-14
6
37
 
7
38
  ### Fixed
package/README.md CHANGED
@@ -42,6 +42,8 @@ Agents are markdown files with YAML frontmatter that define specialized subagent
42
42
  | User | `~/.pi/agent/agents/{name}.md` | Medium |
43
43
  | Project | `.pi/agents/{name}.md` (searches up directory tree) | Highest |
44
44
 
45
+ Project discovery also reads legacy `.agents/{name}.md` files. If both `.agents/` and `.pi/agents/` contain a project agent with the same parsed `name`, `.pi/agents/` wins and the agent manager still writes to `.pi/agents/`.
46
+
45
47
  Use `agentScope` parameter to control discovery: `"user"`, `"project"`, or `"both"` (default; project takes priority).
46
48
 
47
49
  **Builtin agents:** The extension ships with ready-to-use agents — `scout`, `planner`, `worker`, `reviewer`, `context-builder`, `researcher`, and `delegate`. They load at lowest priority so any user or project agent with the same name overrides them.
@@ -51,10 +53,44 @@ You can also override selected builtin fields without copying the whole agent. B
51
53
  - User scope: `~/.pi/agent/settings.json`
52
54
  - Project scope: `.pi/settings.json`
53
55
 
54
- Supported builtin override fields are `model`, `fallbackModels`, `thinking`, `skills`, `tools`, and `systemPrompt`. Project overrides beat user overrides. In `/agents`, press `e` on a builtin to create or edit its override. Overridden builtins show badges like `[builtin+user]` or `[builtin+project]`.
56
+ Supported builtin override fields are `model`, `fallbackModels`, `thinking`, `systemPromptMode`, `inheritProjectContext`, `inheritSkills`, `skills`, `tools`, and `systemPrompt`. Project overrides beat user overrides.
57
+
58
+ **Overriding builtin defaults:**
59
+
60
+ All builtin agents inherit project instruction files (`AGENTS.md`, `CLAUDE.md`, etc.) by default. To make a builtin agent run without those project instructions:
61
+
62
+ **Via settings file** — add to `~/.pi/agent/settings.json` (user scope) or `.pi/settings.json` (project scope):
63
+ ```json
64
+ {
65
+ "subagents": {
66
+ "agentOverrides": {
67
+ "reviewer": {
68
+ "inheritProjectContext": false
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ **Via `/agents` UI** — press `e` on any builtin agent (like `reviewer`), then toggle `inheritProjectContext` to `false` and save. This creates an override that you can edit or remove later without modifying the builtin definition itself.
76
+
77
+ Overridden builtins show badges like `[builtin+user]` or `[builtin+project]` to indicate they have customizations applied.
55
78
 
56
79
  > **Note:** The `researcher` agent uses `web_search`, `fetch_content`, and `get_search_content` tools which require the [pi-web-access](https://github.com/nicobailon/pi-web-access) extension. Install it with `pi install npm:pi-web-access`.
57
80
 
81
+ ### Prompt Assembly Philosophy
82
+
83
+ Subagents are designed to be **narrow and intentional**. Custom subagents default to seeing only what you explicitly give them — not Pi's entire base prompt, not your repo's `AGENTS.md`, and not the parent's discovered skills.
84
+
85
+ This prevents subtle bugs where a "simple" code review agent starts making decisions based on project architecture rules it shouldn't know about, or a focused research agent gets distracted by tool descriptions meant for the orchestrator.
86
+
87
+ Use inheritance flags to selectively add back context when you actually want it:
88
+ - **`inheritProjectContext: true`** — Keep Pi's inherited project-instructions block from project-level files like `AGENTS.md` and `CLAUDE.md`
89
+ - **`inheritSkills: true`** — Let the child use Pi's discovered skills catalog
90
+ - **`systemPromptMode: append`** — Add the agent's prompt to Pi's base instead of replacing it
91
+
92
+ Builtin agents ship a little differently: they all inherit project instruction files by default so they follow repo-specific rules out of the box. `delegate` is still the exception for `systemPromptMode` — it stays on `append` because its job is orchestration within the parent workflow, not isolated task execution.
93
+
58
94
  **Agent frontmatter:**
59
95
 
60
96
  ```yaml
@@ -66,6 +102,9 @@ extensions: # absent=all, empty=none, csv=allowlist
66
102
  model: claude-haiku-4-5
67
103
  fallbackModels: openai/gpt-5-mini, anthropic/claude-sonnet-4 # optional ordered fallbacks
68
104
  thinking: high # off, minimal, low, medium, high, xhigh
105
+ systemPromptMode: replace # replace by default, except builtin delegate
106
+ inheritProjectContext: false # custom agents default false; builtins opt into true
107
+ inheritSkills: false # strip Pi's discovered skills section
69
108
  skill: safe-bash, chrome-devtools # comma-separated skills to inject
70
109
  output: context.md # writes to {chain_dir}/context.md
71
110
  defaultReads: context.md # comma-separated files to read
@@ -81,10 +120,51 @@ The `thinking` field sets a default extended thinking level for the agent. At ru
81
120
 
82
121
  `fallbackModels` is an optional ordered list of backup models to try when the primary model fails with a provider/model-style error such as quota, auth, timeout, or provider/model unavailable. In markdown frontmatter, declare it as a comma-separated string. In management `config` objects, you can pass either a comma-separated string or a string array.
83
122
 
123
+ `systemPromptMode` — How the agent markdown body is passed to Pi:
124
+ - **`replace`** (default) — The agent's markdown body becomes the system prompt. Clean slate, no Pi base prompt baggage.
125
+ - **`append`** — The agent's prompt is appended to Pi's normal base prompt. Use this when you want Pi's full capabilities plus your extra instructions.
126
+
127
+ `inheritProjectContext` — Whether the child keeps Pi's inherited project-instructions block, built from project-level instruction files like `AGENTS.md` and `CLAUDE.md`:
128
+ - **`false`** (default) — Strip those inherited project-level instructions from the child's final system prompt. This does not remove repo access, cwd, tools, or the task itself; it only removes those inherited instructions.
129
+ - **`true`** — Keep those inherited project-level instructions. Use for specialists that should follow project-specific constraints and conventions.
130
+
131
+ `inheritSkills` — Whether the child keeps Pi's discovered skills section:
132
+ - **`false`** (default) — Skills catalog is stripped. Good for focused agents that shouldn't browse unrelated skills.
133
+ - **`true`** — Child sees the full skills list. Use for general-purpose assistants that might need varied tools.
134
+
135
+ The `skills` field still works independently — it injects specific skills directly into the agent prompt regardless of `inheritSkills`.
136
+
137
+ **Common Recipes**
138
+
139
+ | Goal | `systemPromptMode` | `inheritProjectContext` | `inheritSkills` |
140
+ |------|-------------------|------------------------|-----------------|
141
+ | Fully isolated specialist (custom-agent default) | `replace` | `false` | `false` |
142
+ | Specialist that should follow project instruction files | `replace` | `true` | `false` |
143
+ | Pi-plus-extensions | `append` | `true` | `true` |
144
+
145
+ - **Security auditor**: Fully isolated so it objectively checks for vulnerabilities without being biased by project conventions.
146
+ - **Architecture planner**: Repo-aware so it respects `AGENTS.md` constraints when making design decisions.
147
+ - **Generic helper**: Append mode with full inheritance so it behaves like a slightly-customized Pi.
148
+
84
149
  Fallback resolution follows the same conservative model lookup as normal execution. Explicit `provider/model` values are used as-is. Bare model IDs first prefer the current session provider when that provider actually exposes the model, then fall back to a unique registry match. If a bare ID is still ambiguous, it stays bare.
85
150
 
86
151
  Fallback is only used for provider/model availability failures. Ordinary task failures such as bad `bash` commands, missing files, or other tool/runtime errors do not trigger a model hop.
87
152
 
153
+ **Tool selection semantics**
154
+
155
+ The `tools` field controls builtin-tool allowlisting and a couple of related extension behaviors:
156
+
157
+ - `tools` **omitted** → `pi-subagents` does not pass `--tools`, so the child gets Pi's normal default builtin tools.
158
+ - `tools` **present** → listed builtin tool names become an explicit allowlist passed via `--tools`.
159
+ - `mcp:...` entries are split out of `tools` and forwarded as direct MCP tool selections.
160
+ - Path-like entries in `tools` (extension paths or file paths ending in `.ts` / `.js`) are treated as tool-extension paths, not builtin tool names.
161
+
162
+ This means:
163
+
164
+ - `tools` omitted + `extensions` omitted → child gets Pi's normal builtin tools and normal extension set.
165
+ - `tools: mcp:chrome-devtools` → child still gets Pi's default builtin tools, plus direct MCP tools from `chrome-devtools`.
166
+ - `tools: read, bash, mcp:chrome-devtools` → child is restricted to `read` and `bash` for builtin tools, plus direct MCP tools from `chrome-devtools`.
167
+
88
168
  **Extension sandboxing**
89
169
 
90
170
  Use `extensions` in frontmatter to control which extensions a subagent can access:
@@ -177,7 +257,7 @@ Append `[key=value,...]` to any agent name to override its defaults:
177
257
  ```
178
258
  /chain scout[output=context.md] "scan code" -> planner[reads=context.md] "analyze auth"
179
259
  /run scout[model=anthropic/claude-sonnet-4] summarize this codebase
180
- /parallel scanner[output=scan.md] "find issues" -> reviewer[output=review.md] "check style"
260
+ /parallel scanner[model=anthropic/claude-sonnet-4] "find issues" -> reviewer[skills=code-review+security] "check style"
181
261
  ```
182
262
 
183
263
  | Key | Example | Description |
@@ -229,7 +309,7 @@ Press **Ctrl+Shift+A** or type `/agents` to open the Agents Manager overlay —
229
309
  |--------|-------------|
230
310
  | List | Browse all agents and chains with search/filter, scope badges, chain badges |
231
311
  | Detail | View resolved prompt, frontmatter fields, recent run history, and active builtin override path |
232
- | Edit | Edit fields with specialized pickers (model, thinking, skills, prompt editor) |
312
+ | Edit | Edit fields with specialized pickers and toggles (model, thinking, prompt mode, inherited context, inherited skills, prompt editor) |
233
313
  | Chain Detail | View chain steps with flow visualization and dependency map |
234
314
  | Parallel Builder | Build parallel execution slots, add same agent multiple times, per-slot task overrides |
235
315
  | Task Input | Enter task and launch with optional skip-clarify toggle |
@@ -281,6 +361,8 @@ Chains are `.chain.md` files stored alongside agent files. They define reusable
281
361
  | User | `~/.pi/agent/agents/{name}.chain.md` |
282
362
  | Project | `.pi/agents/{name}.chain.md` |
283
363
 
364
+ Project chain discovery also reads legacy `.agents/{name}.chain.md` files. If both locations define the same parsed chain `name`, `.pi/agents/` wins and manager writes stay in `.pi/agents/`.
365
+
284
366
  **Format:**
285
367
 
286
368
  ```markdown
@@ -467,6 +549,9 @@ These are the parameters the **LLM agent** passes when it calls the `subagent` t
467
549
  // Parallel
468
550
  { tasks: [{ agent: "scout", task: "a" }, { agent: "scout", task: "b" }] }
469
551
 
552
+ // Parallel with top-level concurrency override
553
+ { tasks: [{ agent: "scout", task: "a" }, { agent: "reviewer", task: "b" }], concurrency: 2 }
554
+
470
555
  // Parallel with count shorthand (run the same task 3 times)
471
556
  { tasks: [{ agent: "scout", task: "audit auth", count: 3 }] }
472
557
 
@@ -573,6 +658,9 @@ Agent definitions are not loaded into LLM context by default. Management actions
573
658
  description: "Scans codebases for patterns and issues",
574
659
  scope: "user",
575
660
  systemPrompt: "You are a code scout...",
661
+ systemPromptMode: "replace",
662
+ inheritProjectContext: false,
663
+ inheritSkills: false,
576
664
  model: "anthropic/claude-sonnet-4",
577
665
  fallbackModels: ["openai/gpt-5-mini", "anthropic/claude-haiku-4-5"],
578
666
  tools: "read, bash, mcp:github/search_repositories",
@@ -613,7 +701,7 @@ Agent definitions are not loaded into LLM context by default. Management actions
613
701
  Notes:
614
702
  - `create` uses `config.scope` (`"user"` or `"project"`), not `agentScope`.
615
703
  - `update`/`delete` use `agentScope` only for scope disambiguation when the same name exists in both scopes.
616
- - Agent config mapping: `reads -> defaultReads`, `progress -> defaultProgress`, `extensions` controls extension sandboxing, `maxSubagentDepth` maps directly to agent frontmatter, `fallbackModels` maps directly to agent frontmatter, and `tools` supports `mcp:` entries that map to direct MCP tools.
704
+ - Agent config mapping: `reads -> defaultReads`, `progress -> defaultProgress`, `extensions` controls extension sandboxing, `maxSubagentDepth`, `fallbackModels`, `systemPromptMode`, `inheritProjectContext`, and `inheritSkills` map directly to agent frontmatter, and `tools` supports `mcp:` entries that map to direct MCP tools.
617
705
  - To clear any optional field, set it to `false` or `""` (e.g., `{ model: false }` or `{ skills: "" }`). Both work for all string-typed fields.
618
706
 
619
707
  ## Parameters
@@ -624,12 +712,13 @@ Notes:
624
712
  | `task` | string | - | Task string (single mode) |
625
713
  | `action` | string | - | Management action: `list`, `get`, `create`, `update`, `delete` |
626
714
  | `chainName` | string | - | Chain name for management get/update/delete |
627
- | `config` | object | - | Agent or chain config for management create/update. Agent configs also accept `fallbackModels` (comma-separated string or string array). |
715
+ | `config` | object | - | Agent or chain config for management create/update. Agent configs also accept `fallbackModels` (comma-separated string or string array), `systemPromptMode` (`append` or `replace`), `inheritProjectContext` (boolean), and `inheritSkills` (boolean). |
628
716
  | `output` | `string \| false` | agent default | Override output file for single agent (absolute path as-is, relative path resolved against cwd) |
629
717
  | `skill` | `string \| string[] \| false` | agent default | Override skills (comma-separated string, array, or false to disable) |
630
718
  | `model` | string | agent default | Override model for single agent |
631
719
  | `fallbackModels` | `string \| string[]` | agent default | Management/config-only field for ordered backup models. Markdown frontmatter uses a comma-separated string. |
632
720
  | `tasks` | `{agent, task, cwd?, count?, skill?}[]` | - | Parallel tasks. Foreground runs directly; background requests are converted to an equivalent chain. `count` repeats one task entry N times with the same settings. |
721
+ | `concurrency` | number | `config.parallel.concurrency` or `4` | Top-level `tasks` mode only: max concurrent tasks. |
633
722
  | `worktree` | boolean | false | Create isolated git worktrees for each parallel task. Requires clean git state. Per-worktree diffs included in output. |
634
723
  | `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
635
724
  | `context` | `"fresh" \| "fork"` | `fresh` | Execution context mode. `fork` uses a real branched session from the parent's current leaf for each child run |
@@ -764,6 +853,25 @@ This aggregated output becomes `{previous}` for the next step.
764
853
 
765
854
  `pi-subagents` reads optional JSON config from `~/.pi/agent/extensions/subagent/config.json`.
766
855
 
856
+ ### `parallel`
857
+
858
+ `parallel` controls top-level `tasks` mode defaults and limits.
859
+
860
+ ```json
861
+ {
862
+ "parallel": {
863
+ "maxTasks": 12,
864
+ "concurrency": 6
865
+ }
866
+ }
867
+ ```
868
+
869
+ Fields:
870
+ - `maxTasks` defaults to `8` when unset or invalid
871
+ - `concurrency` defaults to `4` when unset or invalid
872
+
873
+ Per-call `concurrency` on the `subagent` tool takes precedence over `config.parallel.concurrency` for top-level `tasks` runs.
874
+
767
875
  ### `defaultSessionDir`
768
876
 
769
877
  `defaultSessionDir` sets the fallback directory used for session logs. Eg:
@@ -8,6 +8,9 @@ import {
8
8
  type AgentSource,
9
9
  type ChainConfig,
10
10
  type ChainStepConfig,
11
+ defaultInheritProjectContext,
12
+ defaultInheritSkills,
13
+ defaultSystemPromptMode,
11
14
  discoverAgentsAll,
12
15
  } from "./agents.ts";
13
16
  import { serializeAgent } from "./agent-serializer.ts";
@@ -244,6 +247,18 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
244
247
  else if (typeof cfg.thinking === "string") target.thinking = cfg.thinking.trim() || undefined;
245
248
  else return "config.thinking must be a string or false when provided.";
246
249
  }
250
+ if (hasKey(cfg, "systemPromptMode")) {
251
+ if (cfg.systemPromptMode === "append" || cfg.systemPromptMode === "replace") target.systemPromptMode = cfg.systemPromptMode;
252
+ else return "config.systemPromptMode must be 'append' or 'replace' when provided.";
253
+ }
254
+ if (hasKey(cfg, "inheritProjectContext")) {
255
+ if (typeof cfg.inheritProjectContext !== "boolean") return "config.inheritProjectContext must be a boolean when provided.";
256
+ target.inheritProjectContext = cfg.inheritProjectContext;
257
+ }
258
+ if (hasKey(cfg, "inheritSkills")) {
259
+ if (typeof cfg.inheritSkills !== "boolean") return "config.inheritSkills must be a boolean when provided.";
260
+ target.inheritSkills = cfg.inheritSkills;
261
+ }
247
262
  if (hasKey(cfg, "output")) {
248
263
  if (cfg.output === false || cfg.output === "") target.output = undefined;
249
264
  else if (typeof cfg.output === "string") target.output = cfg.output;
@@ -320,6 +335,9 @@ export function formatAgentDetail(agent: AgentConfig): string {
320
335
  if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
321
336
  if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
322
337
  if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
338
+ lines.push(`System prompt mode: ${agent.systemPromptMode}`);
339
+ lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
340
+ lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
323
341
  if (agent.extensions !== undefined) lines.push(`Extensions: ${agent.extensions.length ? agent.extensions.join(", ") : "(none)"}`);
324
342
  if (agent.thinking) lines.push(`Thinking: ${agent.thinking}`);
325
343
  if (agent.output) lines.push(`Output: ${agent.output}`);
@@ -418,7 +436,16 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
418
436
  warnings.push(...chainStepWarnings(ctx, chain.steps));
419
437
  return result([`Created chain '${name}' at ${targetPath}.`, ...warnings].join("\n"));
420
438
  }
421
- const agent: AgentConfig = { name, description: cfg.description.trim(), source: scope, filePath: targetPath, systemPrompt: "" };
439
+ const agent: AgentConfig = {
440
+ name,
441
+ description: cfg.description.trim(),
442
+ source: scope,
443
+ filePath: targetPath,
444
+ systemPrompt: "",
445
+ systemPromptMode: defaultSystemPromptMode(name),
446
+ inheritProjectContext: defaultInheritProjectContext(name),
447
+ inheritSkills: defaultInheritSkills(),
448
+ };
422
449
  const applyError = applyAgentConfig(agent, cfg);
423
450
  if (applyError) return result(applyError, true);
424
451
  const mw = modelWarning(ctx, agent.model);
@@ -446,7 +473,6 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
446
473
  const target = targetOrError;
447
474
  const updated: AgentConfig = { ...target };
448
475
  const oldName = target.name;
449
- // Validate all fields before any filesystem mutation
450
476
  if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
451
477
  if (hasKey(cfg, "description") && (typeof cfg.description !== "string" || !cfg.description.trim())) return result("config.description must be a non-empty string when provided.", true);
452
478
  let newName: string | undefined;
@@ -456,7 +482,6 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
456
482
  }
457
483
  const applyError = applyAgentConfig(updated, cfg);
458
484
  if (applyError) return result(applyError, true);
459
- // Apply name/description (validated above)
460
485
  if (newName !== undefined) updated.name = newName;
461
486
  if (hasKey(cfg, "description")) updated.description = (cfg.description as string).trim();
462
487
  if (hasKey(cfg, "model")) {
@@ -471,7 +496,6 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
471
496
  const sw = skillsWarning(ctx.cwd, updated.skills);
472
497
  if (sw) warnings.push(sw);
473
498
  }
474
- // Filesystem mutations last
475
499
  if (updated.name !== oldName) {
476
500
  const renamed = renamePath("agent", target.filePath, updated.name, target.source, ctx.cwd);
477
501
  if (renamed.error) return result(renamed.error, true);
@@ -493,7 +517,6 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
493
517
  const target = targetOrError;
494
518
  const updated: ChainConfig = { ...target, steps: [...target.steps] };
495
519
  const oldName = target.name;
496
- // Validate all fields before any filesystem mutation
497
520
  if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
498
521
  if (hasKey(cfg, "description") && (typeof cfg.description !== "string" || !cfg.description.trim())) return result("config.description must be a non-empty string when provided.", true);
499
522
  let newName: string | undefined;
@@ -507,7 +530,6 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
507
530
  if (parsed.error) return result(parsed.error, true);
508
531
  parsedSteps = parsed.steps!;
509
532
  }
510
- // Apply validated changes to in-memory object
511
533
  if (newName !== undefined) updated.name = newName;
512
534
  if (hasKey(cfg, "description")) updated.description = (cfg.description as string).trim();
513
535
  if (parsedSteps) {
@@ -516,7 +538,6 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
516
538
  if (missing.length) warnings.push(`Warning: chain steps reference unknown agents: ${missing.join(", ")}.`);
517
539
  warnings.push(...chainStepWarnings(ctx, updated.steps));
518
540
  }
519
- // Filesystem mutations last
520
541
  if (updated.name !== oldName) {
521
542
  const renamed = renamePath("chain", target.filePath, updated.name, target.source, ctx.cwd);
522
543
  if (renamed.error) return result(renamed.error, true);
@@ -27,7 +27,7 @@ function renderFieldLine(
27
27
  width: number,
28
28
  theme: Theme,
29
29
  ): string {
30
- const labelWidth = 10;
30
+ const labelWidth = 12;
31
31
  const labelText = theme.fg("dim", pad(label, labelWidth));
32
32
  const available = Math.max(0, width - labelWidth);
33
33
  return `${labelText}${truncateToWidth(value, available)}`;
@@ -61,6 +61,9 @@ function buildDetailLines(
61
61
  const maxSubagentDepth = agent.maxSubagentDepth !== undefined ? String(agent.maxSubagentDepth) : "(default)";
62
62
 
63
63
  lines.push(renderFieldLine("Model:", agent.model ?? "default", contentWidth, theme));
64
+ lines.push(renderFieldLine("Prompt mode:", agent.systemPromptMode, contentWidth, theme));
65
+ lines.push(renderFieldLine("Project ctx:", agent.inheritProjectContext ? "on" : "off", contentWidth, theme));
66
+ lines.push(renderFieldLine("Skills ctx:", agent.inheritSkills ? "on" : "off", contentWidth, theme));
64
67
  if (agent.override) {
65
68
  const overrideLabel = `${agent.override.scope} · ${formatPath(agent.override.path)}`;
66
69
  lines.push(renderFieldLine("Override:", overrideLabel, contentWidth, theme));
@@ -1,6 +1,6 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
- import type { AgentConfig, BuiltinAgentOverrideBase } from "./agents.ts";
3
+ import { defaultSystemPromptMode, type AgentConfig, type BuiltinAgentOverrideBase } from "./agents.ts";
4
4
  import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
5
5
  import type { TextEditorState } from "./text-editor.ts";
6
6
  import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "./render-helpers.ts";
@@ -26,7 +26,7 @@ export interface CreateEditStateOptions {
26
26
  }
27
27
 
28
28
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
29
- const FIELD_ORDER = ["name", "description", "model", "fallbackModels", "thinking", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
29
+ const FIELD_ORDER = ["name", "description", "model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "tools", "extensions", "skills", "output", "reads", "progress", "interactive", "prompt"] as const;
30
30
  type ThinkingLevel = typeof THINKING_LEVELS[number];
31
31
  const PROMPT_VIEWPORT_HEIGHT = 16;
32
32
  const MODEL_SELECTOR_HEIGHT = 10;
@@ -45,6 +45,9 @@ function fieldValueMatchesBase(field: EditField, state: EditState): boolean {
45
45
  case "model": return state.draft.model === base.model;
46
46
  case "fallbackModels": return arraysEqual(state.draft.fallbackModels, base.fallbackModels);
47
47
  case "thinking": return state.draft.thinking === base.thinking;
48
+ case "systemPromptMode": return state.draft.systemPromptMode === base.systemPromptMode;
49
+ case "inheritProjectContext": return state.draft.inheritProjectContext === base.inheritProjectContext;
50
+ case "inheritSkills": return state.draft.inheritSkills === base.inheritSkills;
48
51
  case "tools": return arraysEqual(toolList(state.draft), toolList(base));
49
52
  case "skills": return arraysEqual(state.draft.skills, base.skills);
50
53
  case "prompt": return state.draft.systemPrompt === base.systemPrompt;
@@ -59,6 +62,9 @@ function resetFieldToBase(field: EditField, state: EditState): void {
59
62
  case "model": state.draft.model = base.model; break;
60
63
  case "fallbackModels": state.draft.fallbackModels = base.fallbackModels ? [...base.fallbackModels] : undefined; break;
61
64
  case "thinking": state.draft.thinking = base.thinking; break;
65
+ case "systemPromptMode": state.draft.systemPromptMode = base.systemPromptMode; break;
66
+ case "inheritProjectContext": state.draft.inheritProjectContext = base.inheritProjectContext; break;
67
+ case "inheritSkills": state.draft.inheritSkills = base.inheritSkills; break;
62
68
  case "tools": state.draft.tools = base.tools ? [...base.tools] : undefined; state.draft.mcpDirectTools = base.mcpDirectTools ? [...base.mcpDirectTools] : undefined; break;
63
69
  case "skills": state.draft.skills = base.skills ? [...base.skills] : undefined; break;
64
70
  case "prompt": state.draft.systemPrompt = base.systemPrompt; state.promptEditor = createEditorState(base.systemPrompt); break;
@@ -83,6 +89,9 @@ function renderFieldValue(field: EditField, state: EditState): string {
83
89
  case "model": return draft.model ?? "default";
84
90
  case "fallbackModels": return draft.fallbackModels && draft.fallbackModels.length > 0 ? draft.fallbackModels.join(", ") : "";
85
91
  case "thinking": return draft.thinking ?? "off";
92
+ case "systemPromptMode": return draft.systemPromptMode ?? defaultSystemPromptMode(draft.name);
93
+ case "inheritProjectContext": return draft.inheritProjectContext ? "on" : "off";
94
+ case "inheritSkills": return draft.inheritSkills ? "on" : "off";
86
95
  case "tools": return formatTools(draft);
87
96
  case "extensions": return draft.extensions !== undefined ? (draft.extensions.length > 0 ? draft.extensions.join(", ") : "") : "(all)";
88
97
  case "skills": return draft.skills && draft.skills.length > 0 ? draft.skills.join(", ") : "";
@@ -101,12 +110,28 @@ function applyFieldValue(field: EditField, state: EditState, value: string): voi
101
110
  case "description": draft.description = value.trim(); break;
102
111
  case "model": draft.model = value.trim() || undefined; break;
103
112
  case "fallbackModels": draft.fallbackModels = parseCommaList(value); break;
113
+ case "systemPromptMode": {
114
+ const trimmed = value.trim();
115
+ if (trimmed === "") {
116
+ draft.systemPromptMode = defaultSystemPromptMode(draft.name);
117
+ break;
118
+ }
119
+ if (trimmed === "append" || trimmed === "replace") {
120
+ draft.systemPromptMode = trimmed;
121
+ }
122
+ break;
123
+ }
104
124
  case "tools": { const parsed = parseTools(value); draft.tools = parsed.tools; draft.mcpDirectTools = parsed.mcp; break; }
105
125
  case "extensions": { const trimmed = value.trim(); draft.extensions = trimmed === "(all)" ? undefined : parseCommaList(trimmed) ?? []; break; }
106
126
  case "skills": draft.skills = parseCommaList(value); break;
107
127
  case "output": { const trimmed = value.trim(); draft.output = trimmed.length > 0 ? trimmed : undefined; break; }
108
128
  case "reads": draft.defaultReads = parseCommaList(value); break;
109
- case "progress": case "interactive": case "prompt": break;
129
+ case "inheritProjectContext":
130
+ case "inheritSkills":
131
+ case "progress":
132
+ case "interactive":
133
+ case "prompt":
134
+ break;
110
135
  }
111
136
  }
112
137
 
@@ -231,13 +256,19 @@ export function handleEditInput(screen: EditScreen, state: EditState, data: stri
231
256
  if (data === "m") { openModelPicker(state, models); return { nextScreen: "edit-field" }; }
232
257
  if (data === "t") { openThinkingPicker(state); return { nextScreen: "edit-field" }; }
233
258
  if (data === "s") { openSkillPicker(state, skills); return { nextScreen: "edit-field" }; }
234
- if (data === " " && (field === "progress" || field === "interactive")) { if (field === "progress") state.draft.defaultProgress = !state.draft.defaultProgress; if (field === "interactive") state.draft.interactive = !state.draft.interactive; return; }
259
+ if (data === " " && (field === "inheritProjectContext" || field === "inheritSkills" || field === "progress" || field === "interactive")) {
260
+ if (field === "inheritProjectContext") state.draft.inheritProjectContext = !state.draft.inheritProjectContext;
261
+ if (field === "inheritSkills") state.draft.inheritSkills = !state.draft.inheritSkills;
262
+ if (field === "progress") state.draft.defaultProgress = !state.draft.defaultProgress;
263
+ if (field === "interactive") state.draft.interactive = !state.draft.interactive;
264
+ return;
265
+ }
235
266
  if (matchesKey(data, "return")) {
236
267
  if (field === "model") { openModelPicker(state, models); return { nextScreen: "edit-field" }; }
237
268
  if (field === "thinking") { openThinkingPicker(state); return { nextScreen: "edit-field" }; }
238
269
  if (field === "skills") { openSkillPicker(state, skills); return { nextScreen: "edit-field" }; }
239
270
  if (field === "prompt") { state.promptEditor = createEditorState(state.draft.systemPrompt ?? ""); return { nextScreen: "edit-prompt" }; }
240
- if (field === "progress" || field === "interactive") return;
271
+ if (field === "inheritProjectContext" || field === "inheritSkills" || field === "progress" || field === "interactive") return;
241
272
  state.fieldMode = "text"; state.fieldEditor = createEditorState(renderFieldValue(field, state)); return { nextScreen: "edit-field" };
242
273
  }
243
274
  return;
@@ -304,8 +335,17 @@ export function renderEdit(screen: EditScreen, state: EditState, width: number,
304
335
  for (let i = 0; i < state.fields.length; i++) {
305
336
  const field = state.fields[i]!; if (field === "prompt") break;
306
337
  const isFocused = i === state.fieldIndex; const prefix = isFocused ? theme.fg("accent", "▸ ") : " ";
307
- const rawLabel = pad(`${field[0]!.toUpperCase()}${field.slice(1)}:`, labelWidth);
338
+ const fieldLabel = field === "systemPromptMode"
339
+ ? "Prompt Mode"
340
+ : field === "inheritProjectContext"
341
+ ? "Project Ctx"
342
+ : field === "inheritSkills"
343
+ ? "Skills Ctx"
344
+ : `${field[0]!.toUpperCase()}${field.slice(1)}`;
345
+ const rawLabel = pad(`${fieldLabel}:`, labelWidth);
308
346
  const labelText = state.overrideBase && !fieldValueMatchesBase(field, state) ? theme.fg("accent", rawLabel) : rawLabel; let valueText = renderFieldValue(field, state);
347
+ if (field === "inheritProjectContext") { const toggle = state.draft.inheritProjectContext ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.inheritProjectContext ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
348
+ if (field === "inheritSkills") { const toggle = state.draft.inheritSkills ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.inheritSkills ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
309
349
  if (field === "progress") { const toggle = state.draft.defaultProgress ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.defaultProgress ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
310
350
  if (field === "interactive") { const toggle = state.draft.interactive ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.interactive ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
311
351
  let displayValue = truncateToWidth(valueText, valueWidth);
package/agent-manager.ts CHANGED
@@ -5,6 +5,9 @@ import type { Component, TUI } from "@mariozechner/pi-tui";
5
5
  import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
6
6
  import {
7
7
  buildBuiltinOverrideConfig,
8
+ defaultInheritProjectContext,
9
+ defaultInheritSkills,
10
+ defaultSystemPromptMode,
8
11
  discoverAgentsAll,
9
12
  removeBuiltinAgentOverride,
10
13
  saveBuiltinAgentOverride,
@@ -40,7 +43,7 @@ interface NameInputState { mode: "new-agent" | "clone-agent" | "clone-chain" | "
40
43
  interface StatusMessage { text: string; type: "error" | "info"; }
41
44
  interface OverrideScopeState { selectedScope: "user" | "project"; allowProject: boolean; }
42
45
 
43
- const BUILTIN_OVERRIDE_FIELDS: EditField[] = ["model", "fallbackModels", "thinking", "tools", "skills", "prompt"];
46
+ const BUILTIN_OVERRIDE_FIELDS: EditField[] = ["model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "tools", "skills", "prompt"];
44
47
 
45
48
  function cloneConfig(config: AgentConfig): AgentConfig {
46
49
  return {
@@ -130,6 +133,9 @@ export class AgentManagerComponent implements Component {
130
133
  model: entry.config.model,
131
134
  fallbackModels: entry.config.fallbackModels ? [...entry.config.fallbackModels] : undefined,
132
135
  thinking: entry.config.thinking,
136
+ systemPromptMode: entry.config.systemPromptMode,
137
+ inheritProjectContext: entry.config.inheritProjectContext,
138
+ inheritSkills: entry.config.inheritSkills,
133
139
  systemPrompt: entry.config.systemPrompt,
134
140
  skills: entry.config.skills ? [...entry.config.skills] : undefined,
135
141
  tools: entry.config.tools ? [...entry.config.tools] : undefined,
@@ -233,6 +239,16 @@ export class AgentManagerComponent implements Component {
233
239
  filePath = path.join(dir, `${edit.draft.name}.md`);
234
240
  if (fs.existsSync(filePath)) { edit.error = "An agent with that name already exists."; return false; }
235
241
  fs.mkdirSync(dir, { recursive: true });
242
+ } else if (edit.draft.name !== entry.config.name) {
243
+ const nextPath = path.join(path.dirname(filePath), `${edit.draft.name}.md`);
244
+ if (nextPath !== filePath && fs.existsSync(nextPath)) {
245
+ edit.error = "An agent with that name already exists.";
246
+ return false;
247
+ }
248
+ if (nextPath !== filePath) {
249
+ fs.renameSync(filePath, nextPath);
250
+ filePath = nextPath;
251
+ }
236
252
  }
237
253
  try { const toSave: AgentConfig = { ...edit.draft, filePath }; fs.writeFileSync(filePath, serializeAgent(toSave), "utf-8"); entry.config = cloneConfig(toSave); entry.isNew = false; edit.error = undefined; return true; }
238
254
  catch (err) { edit.error = err instanceof Error ? err.message : "Failed to save agent."; return false; }
@@ -349,7 +365,17 @@ export class AgentManagerComponent implements Component {
349
365
  baseConfig = cloneConfig(sourceEntry.config);
350
366
  } else {
351
367
  const templateConfig = state.template?.config ?? {};
352
- baseConfig = { name, description: "Describe this agent", systemPrompt: "", source: state.scope, filePath: "", ...templateConfig };
368
+ baseConfig = {
369
+ name,
370
+ description: "Describe this agent",
371
+ systemPrompt: "",
372
+ systemPromptMode: defaultSystemPromptMode(name),
373
+ inheritProjectContext: defaultInheritProjectContext(name),
374
+ inheritSkills: defaultInheritSkills(),
375
+ source: state.scope,
376
+ filePath: "",
377
+ ...templateConfig,
378
+ };
353
379
  }
354
380
  const dir = state.scope === "project" ? this.agentData.projectDir : this.agentData.userDir;
355
381
  if (!dir) { state.error = "Project agents directory not found."; this.tui.requestRender(); return; }
@@ -8,6 +8,9 @@ export const KNOWN_FIELDS = new Set([
8
8
  "model",
9
9
  "fallbackModels",
10
10
  "thinking",
11
+ "systemPromptMode",
12
+ "inheritProjectContext",
13
+ "inheritSkills",
11
14
  "skill",
12
15
  "skills",
13
16
  "extensions",
@@ -40,6 +43,9 @@ export function serializeAgent(config: AgentConfig): string {
40
43
  const fallbackModelsValue = joinComma(config.fallbackModels);
41
44
  if (fallbackModelsValue) lines.push(`fallbackModels: ${fallbackModelsValue}`);
42
45
  if (config.thinking && config.thinking !== "off") lines.push(`thinking: ${config.thinking}`);
46
+ lines.push(`systemPromptMode: ${config.systemPromptMode}`);
47
+ lines.push(`inheritProjectContext: ${config.inheritProjectContext ? "true" : "false"}`);
48
+ lines.push(`inheritSkills: ${config.inheritSkills ? "true" : "false"}`);
43
49
 
44
50
  const skillsValue = joinComma(config.skills);
45
51
  if (skillsValue) lines.push(`skills: ${skillsValue}`);