pi-subagents 0.14.0 → 0.15.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,32 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.15.0] - 2026-04-16
6
+
7
+ ### Added
8
+ - 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.
9
+ - 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.
10
+
11
+ ### Changed
12
+ - Builtin subagents now default to `systemPromptMode: replace`, with builtin `delegate` staying on `append`.
13
+ - Builtin agents now inherit project-level instruction files by default unless the user overrides them.
14
+ - 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.
15
+
16
+ ### Fixed
17
+ - 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.
18
+ - 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.
19
+ - 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.
20
+ - 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.
21
+ - 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.
22
+
23
+ ## [0.14.1] - 2026-04-14
24
+
25
+ ### Fixed
26
+ - Completed foreground subagent results now return compact payloads instead of inlining full raw message histories and per-result progress objects, preventing long tool-heavy sync runs from overwhelming the parent agent return path.
27
+ - Prompt-template delegation now rebuilds minimal assistant messages from compact foreground results when raw message arrays are intentionally omitted.
28
+ - UI/status wording now uses plain text labels instead of glyph-heavy markers across foreground rendering, parallel summaries, save-result receipts, installer output, agent manager views, clarify screens, and the corresponding README/CHANGELOG examples.
29
+ - Added a realistic foreground integration repro for issue `#80` and cleaned up the touched tests to remove the remaining blunt `as any` fixture casts.
30
+
5
31
  ## [0.14.0] - 2026-04-14
6
32
 
7
33
  ### Added
@@ -496,7 +522,7 @@
496
522
  - Pre-selects current thinking level if already set
497
523
  - **Model selector in chain TUI** - Press `[m]` to select a different model for any step
498
524
  - Fuzzy search through all available models
499
- - Shows current model with indicator
525
+ - Shows the current model with a `current` badge
500
526
  - Provider/model format (e.g., `anthropic/claude-haiku-4-5`)
501
527
  - Override indicator (✎) when model differs from agent default
502
528
  - **Model visibility in chain execution** - Shows which model each step is using
@@ -534,8 +560,8 @@
534
560
 
535
561
  ### Improved
536
562
  - **Per-step progress indicators** - When progress is enabled, each step shows its role:
537
- - Step 1: `● creates & updates progress.md`
538
- - Step 2+: `↔ reads & updates progress.md`
563
+ - Step 1: `writes progress.md`
564
+ - Step 2+: `reads progress.md`
539
565
  - Clear visualization of progress.md data flow through the chain
540
566
  - **Comprehensive tool descriptions** - Better documentation of chain variables:
541
567
  - Tool description now explains `{task}`, `{previous}`, `{chain_dir}` in detail
@@ -591,7 +617,7 @@
591
617
  ### Improved
592
618
  - **Tool description now explicitly shows the three modes** (SINGLE, CHAIN, PARALLEL) with syntax - helps agents pick the right mode when user says "scout → planner"
593
619
  - **Chain execution observability** - Now shows:
594
- - Chain visualization with status icons: `✓scout → planner` (✓=done, ●=running, ○=pending, ✗=failed) - sequential chains only
620
+ - Chain visualization with status labels: `done scout → running planner` (`done`, `running`, `pending`, `failed`) - sequential chains only
595
621
  - Accurate step counter: "step 1/2" instead of misleading "1/1"
596
622
  - Current tool and recent output for running step
597
623
 
package/README.md CHANGED
@@ -51,10 +51,44 @@ You can also override selected builtin fields without copying the whole agent. B
51
51
  - User scope: `~/.pi/agent/settings.json`
52
52
  - Project scope: `.pi/settings.json`
53
53
 
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]`.
54
+ Supported builtin override fields are `model`, `fallbackModels`, `thinking`, `systemPromptMode`, `inheritProjectContext`, `inheritSkills`, `skills`, `tools`, and `systemPrompt`. Project overrides beat user overrides.
55
+
56
+ **Overriding builtin defaults:**
57
+
58
+ All builtin agents inherit project instruction files (`AGENTS.md`, `CLAUDE.md`, etc.) by default. To make a builtin agent run without those project instructions:
59
+
60
+ **Via settings file** — add to `~/.pi/agent/settings.json` (user scope) or `.pi/settings.json` (project scope):
61
+ ```json
62
+ {
63
+ "subagents": {
64
+ "agentOverrides": {
65
+ "reviewer": {
66
+ "inheritProjectContext": false
67
+ }
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ **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.
74
+
75
+ Overridden builtins show badges like `[builtin+user]` or `[builtin+project]` to indicate they have customizations applied.
55
76
 
56
77
  > **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
78
 
79
+ ### Prompt Assembly Philosophy
80
+
81
+ 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.
82
+
83
+ 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.
84
+
85
+ Use inheritance flags to selectively add back context when you actually want it:
86
+ - **`inheritProjectContext: true`** — Keep Pi's inherited project-instructions block from project-level files like `AGENTS.md` and `CLAUDE.md`
87
+ - **`inheritSkills: true`** — Let the child use Pi's discovered skills catalog
88
+ - **`systemPromptMode: append`** — Add the agent's prompt to Pi's base instead of replacing it
89
+
90
+ 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.
91
+
58
92
  **Agent frontmatter:**
59
93
 
60
94
  ```yaml
@@ -66,6 +100,9 @@ extensions: # absent=all, empty=none, csv=allowlist
66
100
  model: claude-haiku-4-5
67
101
  fallbackModels: openai/gpt-5-mini, anthropic/claude-sonnet-4 # optional ordered fallbacks
68
102
  thinking: high # off, minimal, low, medium, high, xhigh
103
+ systemPromptMode: replace # replace by default, except builtin delegate
104
+ inheritProjectContext: false # custom agents default false; builtins opt into true
105
+ inheritSkills: false # strip Pi's discovered skills section
69
106
  skill: safe-bash, chrome-devtools # comma-separated skills to inject
70
107
  output: context.md # writes to {chain_dir}/context.md
71
108
  defaultReads: context.md # comma-separated files to read
@@ -81,10 +118,51 @@ The `thinking` field sets a default extended thinking level for the agent. At ru
81
118
 
82
119
  `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
120
 
121
+ `systemPromptMode` — How the agent markdown body is passed to Pi:
122
+ - **`replace`** (default) — The agent's markdown body becomes the system prompt. Clean slate, no Pi base prompt baggage.
123
+ - **`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.
124
+
125
+ `inheritProjectContext` — Whether the child keeps Pi's inherited project-instructions block, built from project-level instruction files like `AGENTS.md` and `CLAUDE.md`:
126
+ - **`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.
127
+ - **`true`** — Keep those inherited project-level instructions. Use for specialists that should follow project-specific constraints and conventions.
128
+
129
+ `inheritSkills` — Whether the child keeps Pi's discovered skills section:
130
+ - **`false`** (default) — Skills catalog is stripped. Good for focused agents that shouldn't browse unrelated skills.
131
+ - **`true`** — Child sees the full skills list. Use for general-purpose assistants that might need varied tools.
132
+
133
+ The `skills` field still works independently — it injects specific skills directly into the agent prompt regardless of `inheritSkills`.
134
+
135
+ **Common Recipes**
136
+
137
+ | Goal | `systemPromptMode` | `inheritProjectContext` | `inheritSkills` |
138
+ |------|-------------------|------------------------|-----------------|
139
+ | Fully isolated specialist (custom-agent default) | `replace` | `false` | `false` |
140
+ | Specialist that should follow project instruction files | `replace` | `true` | `false` |
141
+ | Pi-plus-extensions | `append` | `true` | `true` |
142
+
143
+ - **Security auditor**: Fully isolated so it objectively checks for vulnerabilities without being biased by project conventions.
144
+ - **Architecture planner**: Repo-aware so it respects `AGENTS.md` constraints when making design decisions.
145
+ - **Generic helper**: Append mode with full inheritance so it behaves like a slightly-customized Pi.
146
+
84
147
  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
148
 
86
149
  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
150
 
151
+ **Tool selection semantics**
152
+
153
+ The `tools` field controls builtin-tool allowlisting and a couple of related extension behaviors:
154
+
155
+ - `tools` **omitted** → `pi-subagents` does not pass `--tools`, so the child gets Pi's normal default builtin tools.
156
+ - `tools` **present** → listed builtin tool names become an explicit allowlist passed via `--tools`.
157
+ - `mcp:...` entries are split out of `tools` and forwarded as direct MCP tool selections.
158
+ - Path-like entries in `tools` (extension paths or file paths ending in `.ts` / `.js`) are treated as tool-extension paths, not builtin tool names.
159
+
160
+ This means:
161
+
162
+ - `tools` omitted + `extensions` omitted → child gets Pi's normal builtin tools and normal extension set.
163
+ - `tools: mcp:chrome-devtools` → child still gets Pi's default builtin tools, plus direct MCP tools from `chrome-devtools`.
164
+ - `tools: read, bash, mcp:chrome-devtools` → child is restricted to `read` and `bash` for builtin tools, plus direct MCP tools from `chrome-devtools`.
165
+
88
166
  **Extension sandboxing**
89
167
 
90
168
  Use `extensions` in frontmatter to control which extensions a subagent can access:
@@ -229,7 +307,7 @@ Press **Ctrl+Shift+A** or type `/agents` to open the Agents Manager overlay —
229
307
  |--------|-------------|
230
308
  | List | Browse all agents and chains with search/filter, scope badges, chain badges |
231
309
  | 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) |
310
+ | Edit | Edit fields with specialized pickers and toggles (model, thinking, prompt mode, inherited context, inherited skills, prompt editor) |
233
311
  | Chain Detail | View chain steps with flow visualization and dependency map |
234
312
  | Parallel Builder | Build parallel execution slots, add same agent multiple times, per-slot task overrides |
235
313
  | Task Input | Enter task and launch with optional skip-clarify toggle |
@@ -573,6 +651,9 @@ Agent definitions are not loaded into LLM context by default. Management actions
573
651
  description: "Scans codebases for patterns and issues",
574
652
  scope: "user",
575
653
  systemPrompt: "You are a code scout...",
654
+ systemPromptMode: "replace",
655
+ inheritProjectContext: false,
656
+ inheritSkills: false,
576
657
  model: "anthropic/claude-sonnet-4",
577
658
  fallbackModels: ["openai/gpt-5-mini", "anthropic/claude-haiku-4-5"],
578
659
  tools: "read, bash, mcp:github/search_repositories",
@@ -613,7 +694,7 @@ Agent definitions are not loaded into LLM context by default. Management actions
613
694
  Notes:
614
695
  - `create` uses `config.scope` (`"user"` or `"project"`), not `agentScope`.
615
696
  - `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.
697
+ - 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
698
  - To clear any optional field, set it to `false` or `""` (e.g., `{ model: false }` or `{ skills: "" }`). Both work for all string-typed fields.
618
699
 
619
700
  ## Parameters
@@ -624,7 +705,7 @@ Notes:
624
705
  | `task` | string | - | Task string (single mode) |
625
706
  | `action` | string | - | Management action: `list`, `get`, `create`, `update`, `delete` |
626
707
  | `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). |
708
+ | `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
709
  | `output` | `string \| false` | agent default | Override output file for single agent (absolute path as-is, relative path resolved against cwd) |
629
710
  | `skill` | `string \| string[] \| false` | agent default | Override skills (comma-separated string, array, or false to disable) |
630
711
  | `model` | string | agent default | Override model for single agent |
@@ -921,8 +1002,8 @@ Requirements:
921
1002
  During sync execution, the collapsed view shows real-time progress for single, chain, and parallel modes.
922
1003
 
923
1004
  **Chains:**
924
- - Header: `... chain 1/2 | 8 tools, 1.4k tok, 38s`
925
- - Chain visualization with status: `✓scout → planner` (✓=done, ●=running, ○=pending, ✗=failed)
1005
+ - Header: `running chain 1/2 | 8 tools, 1.4k tok, 38s`
1006
+ - Chain visualization with status: `done scout → running planner` (`done`, `running`, `pending`, `failed`)
926
1007
  - Current tool: `> read: packages/tui/src/...`
927
1008
  - Recent output lines (last 2-3 lines)
928
1009
 
@@ -933,7 +1014,7 @@ During sync execution, the collapsed view shows real-time progress for single, c
933
1014
 
934
1015
  Press **Ctrl+O** to expand the full streaming view with complete output per step.
935
1016
 
936
- > **Note:** Chain visualization (the `✓scout → planner` line) is only shown for sequential chains. Chains with parallel steps show per-step cards instead.
1017
+ > **Note:** Chain visualization (the `done scout → running planner` line) is only shown for sequential chains. Chains with parallel steps show per-step cards instead.
937
1018
 
938
1019
  ## Nested subagent recursion guard
939
1020
 
@@ -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));
@@ -106,7 +109,7 @@ function buildDetailLines(
106
109
 
107
110
  for (const run of recentRuns) {
108
111
  const when = pad(formatRelativeTime(run.ts), 8);
109
- const status = run.status === "ok" ? "✓" : "✗";
112
+ const status = run.status;
110
113
  const task = truncateToWidth(`"${run.task}"`, 34);
111
114
  const tail = run.status === "ok" ? formatDuration(run.duration) : `exit ${run.exit ?? 1}`;
112
115
  lines.push(truncateToWidth(` ${when} ${status} ${task} ${tail}`, contentWidth));
@@ -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);
@@ -200,9 +200,9 @@ export function renderList(
200
200
  const count = selectionCount(state.selected, agent.id);
201
201
  const isShadowed = agent.kind === "agent" && agent.source === "project" && userNames.has(agent.name);
202
202
 
203
- const cursorChar = isCursor ? theme.fg("accent", "") : " ";
204
- const selectBadge = count > 1 ? theme.fg("accent", `×${count}`.padStart(2)) : count === 1 ? theme.fg("accent", " ✓") : " ";
205
- const shadowMarker = isShadowed ? theme.fg("warning", "") : " ";
203
+ const cursorChar = isCursor ? theme.fg("accent", ">") : " ";
204
+ const selectBadge = count > 0 ? theme.fg("accent", String(count).padStart(2)) : " ";
205
+ const shadowMarker = isShadowed ? theme.fg("warning", "!") : " ";
206
206
  const prefix = `${cursorChar}${selectBadge}${shadowMarker} `;
207
207
 
208
208
  const modelRaw = agent.kind === "chain" ? `${agent.stepCount ?? 0} steps` : (agent.model ?? "default");
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}`);