pi-subagents 0.48.0 → 0.49.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +47 -19
  2. package/docs/agents.md +1 -0
  3. package/docs/configuration.md +28 -5
  4. package/docs/missions.md +4 -2
  5. package/docs/observability.md +26 -2
  6. package/package.json +1 -1
  7. package/src/agents/agents.ts +27 -12
  8. package/src/api/preflight.ts +1 -1
  9. package/src/extension/config.ts +20 -0
  10. package/src/extension/index.ts +18 -8
  11. package/src/extension/public-execution.ts +30 -4
  12. package/src/extension/schemas.ts +8 -7
  13. package/src/extension/tool-description.ts +10 -8
  14. package/src/inspectors/herdr/actions.ts +11 -2
  15. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  16. package/src/intercom/intercom-bridge.ts +3 -1
  17. package/src/missions/store.ts +8 -3
  18. package/src/runs/background/active-async-capacity.ts +82 -25
  19. package/src/runs/background/async-execution.ts +24 -36
  20. package/src/runs/background/async-job-tracker.ts +4 -0
  21. package/src/runs/background/async-resume.ts +11 -1
  22. package/src/runs/background/fleet-view.ts +18 -5
  23. package/src/runs/background/resume-guidance.ts +27 -7
  24. package/src/runs/background/retained-children.ts +14 -6
  25. package/src/runs/background/run-status.ts +96 -4
  26. package/src/runs/background/subagent-runner.ts +4 -1
  27. package/src/runs/foreground/prompt-audit.ts +4 -3
  28. package/src/runs/foreground/subagent-executor.ts +219 -20
  29. package/src/runs/shared/completion-guard.ts +17 -1
  30. package/src/runs/shared/llm-intent-arbiter.ts +39 -23
  31. package/src/shared/agent-stream-options.ts +5 -0
  32. package/src/shared/artifacts.ts +2 -6
  33. package/src/shared/display-text.ts +50 -0
  34. package/src/shared/node-executable.ts +21 -0
  35. package/src/shared/types.ts +23 -4
  36. package/src/tui/fleet-transcript.ts +1 -48
  37. package/src/tui/fleet.ts +3 -1
  38. package/src/tui/render.ts +86 -39
  39. package/src/watchdog/permission-arbiter.ts +2 -1
  40. package/src/watchdog/review.ts +4 -3
  41. package/src/workflows/chat-progress.ts +8 -2
  42. package/src/workflows/scripted-workflow.ts +46 -7
package/CHANGELOG.md CHANGED
@@ -2,28 +2,56 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.49.0] - 2026-08-13
6
+
7
+ ### Added
8
+ - Run a single child with `{ agent, task? }` when a full workflow script is not needed (#1059).
9
+ - Adjust FleetView spacing and collapsed result height from the main window. Thanks to @pierre-mgmt for #1048.
10
+ - Inspect async run state with `debug.run`, without exposing prompts, secrets, or transcripts (#1037).
11
+ - Let builtin role overrides keep Pi's normal tools and extensions with `tools: "inherit"`. Thanks to @estanexanavsem for #1047 and @davidarny for #1049.
12
+ - Add simple terminal examples for FleetView, the async widget, and inline tool display. Thanks to @czottmann for #1050.
13
+
14
+ ### Changed
15
+ - Clean up active-run limits and artifact packaging code without changing behavior.
16
+
17
+ ### Fixed
18
+ - Trust live Herdr session roots only when the parent executor registered them for that async run.
19
+ - Let a workflow child disable the intercom bridge for one run with `intercomBridge: { mode: "off" }`, while normal async completion still works. Thanks to @jaudiger for #1072.
20
+ - Recover sibling children after a detached workflow fails (#1066).
21
+ - Show child session transcripts in standalone Herdr inspectors when the transcript is in a trusted session folder (#1069).
22
+ - Keep watchdog reviews, permission checks, Prompt Audit rewriting, and completion intent checks on the authenticated provider stream across the Pi 0.81 and 0.84 APIs. Thanks to @nuzayets for #1067.
23
+ - Mark children stopped by a parent workflow as stopped, not failed, and keep the stop reason (#1060).
24
+ - Keep subagent artifacts and automatic mission records out of project worktrees by default, so read-only workflows leave the tree clean (#1062).
25
+ - Make parents wait at dependency barriers after async launches, so child results are available before dependent work continues. Thanks to @exuanbo for #1045.
26
+ - Keep wait callers alive for intercom replies instead of reviving a detached wrapper. Thanks to @yayamaz for #1053.
27
+ - Keep workflow summary reports separate from child reports, and reject report path collisions before launch (#1038).
28
+ - Accept no-edit implementation challenge passes when the writer says the current solution is already best (#1054).
29
+ - Make the mutation guard safer for LLM intent checks, long tasks, and provider authentication. Thanks to @MarcusNeufeldt for #1044.
30
+ - Launch Herdr inspector panes with Node when Pi runs as a standalone executable. Thanks to @kevinpita for #1051.
31
+ - Sanitize async, nested, and result transcript output before showing it in terminal views. Thanks to @riesbri for #1046.
32
+
5
33
  ## [0.48.0] - 2026-08-13
6
34
 
7
35
  ### Added
8
- - Add a durable per-run child fan-out budget with a default cap of 64 across static, dynamic, workflow, and nested child admissions. Thanks to @asjer for #1031.
9
- - Add an opt-in per-session cap for concurrently active top-level async runs, with atomic admission, resume transfer, status/Fleet/RPC/doctor visibility, and release gated by the verified process-terminal behavior from #1030. Thanks to @asjer for #1029.
10
- - Add a live Prompt Audit drawer to Fleet for current-session foreground children. Prompt text is visible in the drawer, kept outside serializable Fleet state, and redacted from foreground input, transcript, metadata, result, progress, and run-history artifacts (#1021).
11
- - Add a global `timeoutMs` config option that sets the default run deadline for single, parallel, and chain launches (foreground, plus plain single-agent async) when neither the call nor the selected agent provides a timeout. It reaches parallel (`tasks: [...]`) and chain launches, which never adopt an agent's frontmatter `timeoutMs` (that default applies to single-agent launches only), so a long fan-out no longer falls back to the built-in 30-minute default and gets killed mid-run. Explicit call `timeoutMs`/`maxRuntimeMs` and agent frontmatter defaults still win; composite async runs stay unbounded at the top level by design. Thanks to @shaharmor for #1018.
12
- - Add a `PI_SUBAGENT_TASK_DELIVERY` environment setting (`auto` | `file`, default `auto`) controlling how the task text reaches child Pi processes. `file` writes the task to a temp `task.md` referenced as `@<path>` instead of embedding it in argv, for hosts where endpoint protection (EDR) pre-execution command-line scanning denies children whose argv embeds a long natural-language task. Thanks to @yanqianglu for #1028.
13
- - Escalate startup retries to file task delivery after an unexplained zero-activity `SIGKILL` child exit, so EDR-denied launches self-heal on retry in both foreground and background runs. Thanks to @yanqianglu for #1028.
14
-
15
- ### Fixed
16
- - Open Fleet Prompt Audit with the authored task visible by default and show a short live task summary in the normal Fleet detail pane (#1021).
17
- - Use full task-text hashes for LLM intent arbiter memoization so same-prefix review and implementation tasks cannot share a cached verdict.
18
- - Terminate async Pi writers as owned POSIX process groups on stop and timeout, and keep terminal process proof unknown until process-tree exit is verified. Thanks to @asjer for #1030.
19
- - Explain when a requested mission is scoped to another worktree by naming the current project root and mission directory (#1024).
20
- - Preserve the configured output reference when explicit acceptance rejects an otherwise completed foreground child, so useful reports remain available (#1023).
21
- - Reject configured worktree base directories inside the agent extensions directory, including symlink aliases (#1014).
22
- - Align unnamed intercom fallback orchestrator targets with pi-intercom's 18-character registered presence names so subagents without an explicit session name can reach their orchestrator. Thanks to @mystery4f for #1017.
23
- - Stop reading hyphenated adjectives like "must-fix items" or "should-fix tests" as implementation intent, which made the completion mutation guard hard-fail read-only review runs with a false "completed without making edits" error. Severity compounds (must|should|needs + dash + verb) are stripped before verb matching across every mutation pattern (incl. update/add/apply/make/do siblings), the acceptance-level write-capability check, and the patch-scope pattern, while CLI flags ("eslint --fix", "prettier --write") and clause-level dashes ("branch—fix it") keep their write intent. Thanks to @MarcusNeufeldt for #1020.
24
- - Add an optional LLM intent arbiter: when the completion guard is about to hard-fail a run that made no edits, a model decides — from the task text alone, never the child's own report — whether the task actually instructed file changes; only a confident read-only verdict rescues the run, before any failure state is published. Covers single, parallel, and chain foreground runs; enabled by default; set `PI_SUBAGENTS_LLM_INTENT_ARBITER=0` to disable. Thanks to @MarcusNeufeldt for #1020.
25
- - Tolerate empty-string entries in acceptance-report string-array fields instead of rejecting the whole report. Thanks to @hjiang for #1015.
26
- - Let single external-cli workflow children ignore inherited Pi models so model-less external runners start instead of failing preflight. Thanks to @twosunnus for #1016.
36
+ - Limit each run to 64 child launches by default, so accidental fan-out loops stop before they create too many children. Thanks to @asjer for #1031.
37
+ - Add an optional limit for how many top-level async runs one session can have active at the same time. Fleet, status, RPC, and doctor now show the limit and current usage. Thanks to @asjer for #1029.
38
+ - Add a live Prompt Audit drawer to Fleet for foreground children owned by the current session. It shows the prompt on screen without saving it to status files, history, transcripts, metadata, results, progress, or run artifacts (#1021).
39
+ - Add a global `timeoutMs` setting for default run deadlines on foreground launches and plain single-agent async runs. It applies when a launch or agent does not set its own timeout, and it prevents long foreground fan-outs from falling back to the built-in 30-minute limit. Composite async runs stay unbounded at the top level. Thanks to @shaharmor for #1018.
40
+ - Add `PI_SUBAGENT_TASK_DELIVERY=auto|file` for hosts that block child processes when the task text appears in the command line. File mode writes the task to a temporary `task.md` and passes that path instead. Thanks to @yanqianglu for #1028.
41
+ - Retry with file-based task delivery after a child exits with no activity, which helps recover from endpoint protection tools that block long command lines. Thanks to @yanqianglu for #1028.
42
+
43
+ ### Fixed
44
+ - Open Fleet Prompt Audit with the original task visible by default, and show a short task summary in the normal Fleet detail pane (#1021).
45
+ - Use the full task text when caching LLM intent decisions, so similar tasks with the same prefix cannot share the wrong answer.
46
+ - Stop async Pi writer processes as full process groups, and only mark process cleanup as proven after the process tree has actually exited. Thanks to @asjer for #1030.
47
+ - Explain when a mission belongs to another worktree, including both the current project root and the mission directory (#1024).
48
+ - Keep the configured output reference when explicit acceptance rejects a foreground child, so useful reports remain available (#1023).
49
+ - Reject worktree base directories inside the agent extensions directory, including symlinked paths (#1014).
50
+ - Make unnamed intercom fallback targets match pi-intercom's registered name length, so subagents without a custom session name can still reach their parent. Thanks to @mystery4f for #1017.
51
+ - Stop treating phrases like "must-fix items" or "should-fix tests" as instructions to edit files during read-only review tasks. Thanks to @MarcusNeufeldt for #1020.
52
+ - Add an optional LLM check before the mutation guard fails a foreground single, parallel, or chain child that made no edits. If the task was actually read-only, the run now completes instead of failing. Thanks to @MarcusNeufeldt for #1020.
53
+ - Accept empty strings inside acceptance-report string arrays instead of rejecting the full report. Thanks to @hjiang for #1015.
54
+ - Let single external-CLI workflow children start without inheriting a Pi model, so model-less external runners do not fail preflight. Thanks to @twosunnus for #1016.
27
55
 
28
56
  ## [0.47.1] - 2026-08-12
29
57
 
package/docs/agents.md CHANGED
@@ -77,6 +77,7 @@ Supported override fields: `description`, `model`, `fallbackModels`, `thinking`,
77
77
 
78
78
  - `description` replaces the discovered description for builtin and custom agents, which lets list output show deployment-specific routing or model metadata.
79
79
  - Use `defaultContext: false` or `acceptanceRole: false` to clear an inherited override.
80
+ - Use `tools: "inherit"` on a builtin when that one role should omit its bundled tool allowlist and receive Pi's normal builtins and ambient extensions. This keeps strict tools as the default for other builtins.
80
81
  - Project overrides beat user overrides.
81
82
  - Matching user and project agents also receive override fields that their frontmatter leaves unset, so a shared project config agent can keep the persona while local settings choose the model.
82
83
 
@@ -44,6 +44,29 @@ Defaults to `false`. The default registered model-facing tool schema and descrip
44
44
 
45
45
  Controls the `subagent` tool result shown inline in chat. The default, `"rich"`, shows live child activity and expands to detailed output. `"summary"` keeps the inline result at one stable row for running, completed, failed, stopped, and paused runs; it does not animate, show elapsed time, preview child output, or change when Pi's expand key is pressed. FleetView remains available for live progress and detailed inspection.
46
46
 
47
+ ## `mainWindowRenderer`
48
+
49
+ ```json
50
+ {
51
+ "mainWindowRenderer": {
52
+ "horizontalSpacing": 0,
53
+ "compactResultMaxLines": 4
54
+ }
55
+ }
56
+ ```
57
+
58
+ Controls only the main chat `subagent` call/result renderer. It does not change child execution, orchestration, FleetView, artifacts, transcripts, or model-facing content.
59
+
60
+ `horizontalSpacing` is an integer from `0` to `4`. The default preserves current spacing. Set it to `0` to remove the extra spaces before compact result details and between parts of the call row.
61
+
62
+ `compactResultMaxLines` is a positive integer. It caps only collapsed rich-result rows and adds an expand hint when rows are hidden. Expanded output remains uncapped.
63
+
64
+ With `"summary"`, a tool result looks like this:
65
+
66
+ ```text
67
+ ✓ reviewer · completed
68
+ ```
69
+
47
70
  ## `asyncByDefault`
48
71
 
49
72
  ```json
@@ -297,7 +320,6 @@ stdin is a JSON object with `repoRoot`, `worktreePath`, `agentCwd`, `branch`, `i
297
320
  {
298
321
  "missions": {
299
322
  "enabled": true,
300
- "directory": ".pi/subagents/missions",
301
323
  "globalIndex": true,
302
324
  "retainTerminal": 200
303
325
  }
@@ -306,7 +328,8 @@ stdin is a JSON object with `repoRoot`, `worktreePath`, `agentCwd`, `branch`, `i
306
328
 
307
329
  Automatic missions are enabled by default for ordinary launches with a task. Use per-launch `mission: false` for intentionally ephemeral work, or set `enabled: false` to disable automatic creation globally; explicit mission actions and `missionId`/`mission` launch fields still work.
308
330
 
309
- - `directory` may be absolute, `~/...`, or project-relative.
331
+ - Mission records default to a project-keyed directory under pi's agent directory (`~/.pi/agent/missions/projects/<project-hash>/`). This keeps the project worktree clean.
332
+ - `directory` may be absolute, `~/...`, or project-relative. Set it to `.pi/subagents/missions` to opt in to project-scoped records.
310
333
  - `retainTerminal` is a positive count (default `200`); pruning removes only the oldest completed, failed, or cancelled records and their pointers, never planned, active, waiting, needs-decision, or corrupt records.
311
334
  - The user-global index contains pointers only; missing-record pointers self-heal when globally listed. Set `globalIndex: false` to disable writes or `globalIndexDir` to redirect it.
312
335
 
@@ -335,11 +358,11 @@ Each fixed action resolves to `"auto"`, `"confirm"`, or `"forbid"`. This is inte
335
358
 
336
359
  Controls where subagent artifact files (inputs, outputs, transcripts, metadata) are stored:
337
360
 
338
- - `"project"` (default): writes to `<cwd>/.pi/subagents/artifacts/`.
339
- - `"session"`: stores artifacts under pi's session directory (`~/.pi/agent/sessions/<session>/subagent-artifacts/`), keeping the working directory clean.
361
+ - `"project"`: writes to `<cwd>/.pi/subagents/artifacts/`.
362
+ - `"session"` (default): stores artifacts under pi's session directory (`~/.pi/agent/sessions/<session>/subagent-artifacts/`), keeping the working directory clean. It falls back to the OS temp directory when no session file exists.
340
363
  - `"temp"`: uses the OS temp directory.
341
364
 
342
- This preference also controls the default chain scratch directory. `"project"` uses `<cwd>/.pi/subagents/chain-runs/`, while `"session"` and `"temp"` use the user-scoped temp chain directory.
365
+ This preference also controls the default chain scratch directory. `"project"` uses `<cwd>/.pi/subagents/chain-runs/`, while the default `"session"` and `"temp"` use the user-scoped temp chain directory.
343
366
 
344
367
  The `"session"` option uses the same directory that `cleanupAllArtifactDirs` already scans for age-based cleanup, so artifacts are still cleaned up automatically. Temporary chain directories are cleaned up separately after 24 hours.
345
368
 
package/docs/missions.md CHANGED
@@ -11,7 +11,9 @@ Missions are durable wrappers around runs. The noun map:
11
11
  - **Run** — one actual subagent execution.
12
12
  - **Receipt** — proof or a link for an external outcome, such as a PR, CI check, deployment, or release.
13
13
 
14
- Ordinary workflow launches create one enclosing mission by default, with detailed JSON records under `<cwd>/.pi/subagents/missions/` linking objectives, run ids, lifecycle status, decisions, artifact paths, and delivery receipts. Workflow children do not create separate missions. Each workflow child attempt is stored in the enclosing mission with its stable workflow key, run id when known, agent, task metadata, timestamps, session and artifact paths, and latest status heartbeat.
14
+ Ordinary workflow launches create one enclosing mission by default, with detailed JSON records under `~/.pi/agent/missions/projects/<project-hash>/` linking objectives, run ids, lifecycle status, decisions, artifact paths, and delivery receipts. Workflow children do not create separate missions. Each workflow child attempt is stored in the enclosing mission with its stable workflow key, run id when known, agent, task metadata, timestamps, session and artifact paths, and latest status heartbeat.
15
+
16
+ Records created under the old default `<project>/.pi/subagents/missions` stay on disk. Continue them by setting `missions.directory` to that path for the project or by copying the record into the new agent-dir project store. There is no automatic migration.
15
17
 
16
18
  Behavior:
17
19
 
@@ -19,7 +21,7 @@ Behavior:
19
21
  - Human receipts end with `Mission: <id> (<status>)`, while JSON/structured output text stays unchanged and `details.missionId` is authoritative.
20
22
  - Pass `mission: false` for an intentionally ephemeral workflow. It creates no mission for the workflow or its children and has no `state` global.
21
23
  - Set `missions.enabled: false` to disable automatic mission creation; explicit mission fields and actions still work.
22
- - A workflow with a mission can use `await state.get(key)` and `await state.set(key, value)` for durable JSON state. Missing keys return `undefined`. Keys use the same format as `runs.run` keys. Each set takes the state-file lock, reads the latest file, merges the key, and atomically writes `<cwd>/.pi/subagents/missions/<mission-id>/state.json`. The complete file cannot exceed 256 KiB. Each workflow caches the file on its first `get`. A `mission:false` workflow has no `state` global.
24
+ - A workflow with a mission can use `await state.get(key)` and `await state.set(key, value)` for durable JSON state. Missing keys return `undefined`. Keys use the same format as `runs.run` keys. Each set takes the state-file lock, reads the latest file, merges the key, and atomically writes `<mission-directory>/<mission-id>/state.json`. The complete file cannot exceed 256 KiB. Each workflow caches the file on its first `get`. A `mission:false` workflow has no `state` global.
23
25
 
24
26
  An explicit `mission` object must have exactly one non-empty `title` or `summary`. `objective` and `labels` are optional. When supplied, `goal` must be `true` and requires `budget: { tokens: <positive integer> }`.
25
27
 
@@ -23,13 +23,37 @@ subagent({ action: "status", id: "..." }) // one run
23
23
 
24
24
  Or ask naturally: "Show me the current async runs."
25
25
 
26
+ The under-editor async widget gives a short view while work runs. Its expand key follows your Pi keybinding:
27
+
28
+ ```text
29
+ async subagent worker · background
30
+ ● worker
31
+ ● Step 1/1: worker · running
32
+ ⎿ read: src/auth.ts | 2.0s
33
+ Press configured-expand-key for live detail
34
+ ```
35
+
26
36
  To inspect one background child in text, use `subagent({ action: "status", id: "...", view: "transcript" })`; add `index` for a specific child in a parallel or chain run.
27
37
 
28
38
  ## FleetView
29
39
 
30
40
  In the TUI, a persistent FleetView below the editor keeps active work visible as a compact summary. Set `fleetViewPlacement` to `"aboveEditor"` to move it above the editor.
31
41
 
32
- When the focused editor is empty, press `↓` or `←` to expand the summary into `main` plus active children with task, elapsed time, and token totals. Then use `↑`/`↓` or `j`/`k` to select a child and `Enter` to inspect it. Printable navigation keys are never intercepted before activation.
42
+ ```text
43
+ 2 active agents · ↓ 4.2k tokens · ↓/← to inspect
44
+ ```
45
+
46
+ After you expand it:
47
+
48
+ ```text
49
+ ↑↓/jk select · enter inspect · esc back
50
+
51
+ > main
52
+ scout · running 1m 12s · ↓ 2.8k tokens
53
+ reviewer · running 38s · ↓ 1.4k tokens
54
+ ```
55
+
56
+ When the focused editor is empty, press `↓` or `←` to expand the summary into `main` plus active children with agent name, state, elapsed time, and token totals. Then use `↑`/`↓` or `j`/`k` to select a child and `Enter` to inspect it. Printable navigation keys are never intercepted before activation.
33
57
 
34
58
  FleetView replaces the legacy above-editor async widget by default. Successful background completions stay quiet so inactive Pi tabs are not marked unread, while failed or paused completions still notify the originating session. Parallel runs show every active child independently. Chains with parallel groups keep their grouped shape in progress and results, so failed or paused agents stay visible next to completed ones. When a child is explicitly allowed to fan out with `tools: subagent`, its nested runs appear under that parent child in the main status tree instead of being hidden inside the child process.
35
59
 
@@ -126,7 +150,7 @@ Foreground and async runners share bounded child-protocol handling:
126
150
 
127
151
  ## Chain and debug artifacts
128
152
 
129
- Each chain run creates a scratch directory under its resolved chain root. With the default `artifactDir: "project"`, that root is `<cwd>/.pi/subagents/chain-runs/`. With `artifactDir: "session"` or `"temp"`, it is user-scoped temp storage:
153
+ Each chain run creates a scratch directory under its resolved chain root. With the default `artifactDir: "session"` or with `"temp"`, it is user-scoped temp storage. With `artifactDir: "project"`, the root is `<cwd>/.pi/subagents/chain-runs/`:
130
154
 
131
155
  ```text
132
156
  <tmpdir>/pi-subagents-<scope>/chain-runs/{runId}/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "Pi extension for single-agent delegation and scripted multi-agent workflows",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -92,7 +92,7 @@ interface BuiltinAgentOverrideConfig {
92
92
  disabled?: boolean;
93
93
  systemPrompt?: string;
94
94
  skills?: string[] | false;
95
- tools?: string[] | false;
95
+ tools?: string[] | false | "inherit";
96
96
  extensions?: string[] | false;
97
97
  subagentOnlyExtensions?: string[] | false;
98
98
  completionGuard?: boolean;
@@ -602,7 +602,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
602
602
  ...(override.disabled !== undefined ? { disabled: override.disabled } : {}),
603
603
  ...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
604
604
  ...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
605
- ...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
605
+ ...(override.tools !== undefined ? { tools: Array.isArray(override.tools) ? [...override.tools] : override.tools } : {}),
606
606
  ...(override.extensions !== undefined ? { extensions: override.extensions === false ? false : [...override.extensions] } : {}),
607
607
  ...(override.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: override.subagentOnlyExtensions === false ? false : [...override.subagentOnlyExtensions] } : {}),
608
608
  ...(override.completionGuard !== undefined ? { completionGuard: override.completionGuard } : {}),
@@ -738,6 +738,17 @@ function parseOverrideStringArrayOrFalse(
738
738
  return items;
739
739
  }
740
740
 
741
+ function parseToolsOverride(
742
+ value: unknown,
743
+ meta: { filePath: string; name: string },
744
+ ): BuiltinAgentOverrideConfig["tools"] | undefined {
745
+ if (typeof value === "string" && value.trim() === "inherit") return "inherit";
746
+ if (value === undefined || value === false || Array.isArray(value)) {
747
+ return parseOverrideStringArrayOrFalse(value, { ...meta, field: "tools" });
748
+ }
749
+ throw new Error(`Builtin override '${meta.name}' in '${meta.filePath}' has invalid 'tools'; expected an array of strings, "inherit", or false.`);
750
+ }
751
+
741
752
  function parseBuiltinOverrideEntry(
742
753
  name: string,
743
754
  value: unknown,
@@ -845,7 +856,7 @@ function parseBuiltinOverrideEntry(
845
856
  const skills = parseOverrideStringArrayOrFalse(input.skills, { filePath, name, field: "skills" });
846
857
  if (skills !== undefined) override.skills = skills;
847
858
 
848
- const tools = parseOverrideStringArrayOrFalse(input.tools, { filePath, name, field: "tools" });
859
+ const tools = parseToolsOverride(input.tools, { filePath, name });
849
860
  if (tools !== undefined) override.tools = tools;
850
861
 
851
862
  const extensions = parseOverrideStringArrayOrFalse(input.extensions, { filePath, name, field: "extensions" });
@@ -1004,6 +1015,17 @@ function applySubagentDefaults(
1004
1015
  );
1005
1016
  }
1006
1017
 
1018
+ function applyToolsOverride(target: AgentConfig, toolsOverride: string[] | false | "inherit"): void {
1019
+ if (toolsOverride === "inherit") {
1020
+ delete target.tools;
1021
+ delete target.mcpDirectTools;
1022
+ return;
1023
+ }
1024
+ const { tools, mcpDirectTools } = splitToolList(toolsOverride === false ? [] : toolsOverride);
1025
+ if (tools === undefined) delete target.tools; else target.tools = tools;
1026
+ if (mcpDirectTools === undefined) delete target.mcpDirectTools; else target.mcpDirectTools = mcpDirectTools;
1027
+ }
1028
+
1007
1029
  function applyBuiltinOverride(
1008
1030
  agent: AgentConfig,
1009
1031
  override: BuiltinAgentOverrideConfig,
@@ -1029,11 +1051,7 @@ function applyBuiltinOverride(
1029
1051
  if (override.disabled !== undefined) next.disabled = override.disabled;
1030
1052
  if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
1031
1053
  if (override.skills !== undefined) { if (override.skills === false) delete next.skills; else next.skills = [...override.skills]; }
1032
- if (override.tools !== undefined) {
1033
- const { tools, mcpDirectTools } = splitToolList(override.tools === false ? [] : override.tools);
1034
- if (tools === undefined) delete next.tools; else next.tools = tools;
1035
- if (mcpDirectTools === undefined) delete next.mcpDirectTools; else next.mcpDirectTools = mcpDirectTools;
1036
- }
1054
+ if (override.tools !== undefined) applyToolsOverride(next, override.tools);
1037
1055
  if (override.extensions !== undefined) { if (override.extensions === false) delete next.extensions; else next.extensions = [...override.extensions]; }
1038
1056
  if (override.subagentOnlyExtensions !== undefined) { if (override.subagentOnlyExtensions === false) delete next.subagentOnlyExtensions; else next.subagentOnlyExtensions = [...override.subagentOnlyExtensions]; }
1039
1057
  if (override.completionGuard !== undefined) next.completionGuard = override.completionGuard;
@@ -1175,10 +1193,7 @@ function applyCustomAgentOverride(
1175
1193
  fill("skills", ["skill", "skills"], override.skills === false ? undefined : [...override.skills]);
1176
1194
  }
1177
1195
  if (override.tools !== undefined && !agentHasFrontmatterField(agent, "tools")) {
1178
- const { tools, mcpDirectTools } = splitToolList(override.tools === false ? [] : override.tools);
1179
- const target = mutable();
1180
- if (tools === undefined) delete target.tools; else target.tools = tools;
1181
- if (mcpDirectTools === undefined) delete target.mcpDirectTools; else target.mcpDirectTools = mcpDirectTools;
1196
+ applyToolsOverride(mutable(), override.tools);
1182
1197
  anyFilled = true;
1183
1198
  }
1184
1199
  if (override.extensions !== undefined) {
@@ -285,7 +285,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
285
285
  return { ok: false, code: "denied_required_tool", message, diagnostics };
286
286
  }
287
287
  const artifactsEnabled = input.artifacts !== false;
288
- const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir ?? "project") : undefined;
288
+ const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir) : undefined;
289
289
  const artifactPaths = artifactsDir ? getArtifactPaths(artifactsDir, runId, agent.name, 0) : undefined;
290
290
  const outputPath = resolveSingleOutputPath(behavior.output, effectiveCwd, effectiveCwd, artifactsDir ? path.join(artifactsDir, "outputs", runId) : undefined);
291
291
  const sessionRoot = input.sessionDir ? path.resolve(input.sessionDir) : input.sessionRoot ? path.join(path.resolve(input.sessionRoot), runId) : undefined;
@@ -46,6 +46,25 @@ function validateArtifactConfig(value: unknown): void {
46
46
  }
47
47
  }
48
48
 
49
+ function validateMainWindowRendererConfig(value: unknown): void {
50
+ if (value === undefined) return;
51
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.mainWindowRenderer must be a JSON object");
52
+ const rendererConfig = value as Record<string, unknown>;
53
+ if (rendererConfig.horizontalSpacing !== undefined
54
+ && (typeof rendererConfig.horizontalSpacing !== "number"
55
+ || !Number.isInteger(rendererConfig.horizontalSpacing)
56
+ || rendererConfig.horizontalSpacing < 0
57
+ || rendererConfig.horizontalSpacing > 4)) {
58
+ throw new Error("config.mainWindowRenderer.horizontalSpacing must be an integer from 0 to 4");
59
+ }
60
+ if (rendererConfig.compactResultMaxLines !== undefined
61
+ && (typeof rendererConfig.compactResultMaxLines !== "number"
62
+ || !Number.isInteger(rendererConfig.compactResultMaxLines)
63
+ || rendererConfig.compactResultMaxLines < 1)) {
64
+ throw new Error("config.mainWindowRenderer.compactResultMaxLines must be a positive integer");
65
+ }
66
+ }
67
+
49
68
  function validateConfig(config: Record<string, unknown>): void {
50
69
  if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
51
70
  throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
@@ -65,6 +84,7 @@ function validateConfig(config: Record<string, unknown>): void {
65
84
  validateScheduledRunsConfig(config.scheduledRuns);
66
85
  validateFleetKeybindingsConfig(config.fleetKeybindings);
67
86
  validateArtifactConfig(config.artifactConfig);
87
+ validateMainWindowRendererConfig(config.mainWindowRenderer);
68
88
  }
69
89
 
70
90
  export function getConfigPath(): string {
@@ -59,6 +59,7 @@ import { resolveMissionStoreLocation } from "../missions/store.ts";
59
59
  import { listRetainedChildren } from "../runs/background/retained-children.ts";
60
60
  import {
61
61
  type Details,
62
+ type MainWindowRendererConfig,
62
63
  type SubagentState,
63
64
  DIRS,
64
65
  DEFAULT_ARTIFACT_CONFIG,
@@ -253,12 +254,13 @@ function rebuildSlashResultContainer(
253
254
  result: AgentToolResult<Details>,
254
255
  options: { expanded: boolean },
255
256
  theme: ExtensionContext["ui"]["theme"],
257
+ rendererConfig?: MainWindowRendererConfig,
256
258
  ): void {
257
259
  container.clear();
258
260
  container.addChild(new Spacer(1));
259
261
  const boxTheme = isSlashResultRunning(result) ? "toolPendingBg" : isSlashResultError(result) ? "toolErrorBg" : "toolSuccessBg";
260
262
  const box = new Box(1, 1, (text: string) => theme.bg(boxTheme, text));
261
- box.addChild(renderSubagentResult(result, options, theme));
263
+ box.addChild(renderSubagentResult(result, options, theme, undefined, rendererConfig));
262
264
  container.addChild(box);
263
265
  }
264
266
 
@@ -266,6 +268,7 @@ function createSlashResultComponent(
266
268
  details: SlashMessageDetails,
267
269
  options: { expanded: boolean },
268
270
  theme: ExtensionContext["ui"]["theme"],
271
+ rendererConfig?: MainWindowRendererConfig,
269
272
  ): Container {
270
273
  const container = new Container();
271
274
  let lastVersion = -1;
@@ -273,7 +276,7 @@ function createSlashResultComponent(
273
276
  const snapshot = getSlashRenderableSnapshot(details);
274
277
  if (snapshot.version !== lastVersion || isSlashResultRunning(snapshot.result)) {
275
278
  lastVersion = snapshot.version;
276
- rebuildSlashResultContainer(container, snapshot.result, options, theme);
279
+ rebuildSlashResultContainer(container, snapshot.result, options, theme, rendererConfig);
277
280
  }
278
281
  return Container.prototype.render.call(container, width);
279
282
  };
@@ -378,6 +381,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
378
381
  ...(config.authorityPolicy ? { authorityPolicy: config.authorityPolicy } : {}),
379
382
  ...(config.missions ? { missionStoreConfig: config.missions } : {}),
380
383
  parentSessionFile: null,
384
+ trustedSessionRoots: [],
381
385
  subagentInProgress: false,
382
386
  subagentSpawns: {
383
387
  sessionId: null,
@@ -481,7 +485,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
481
485
  pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
482
486
  const details = resolveSlashMessageDetails(message.details);
483
487
  if (!details) return undefined;
484
- return createSlashResultComponent(details, options, theme);
488
+ return createSlashResultComponent(details, options, theme, config.mainWindowRenderer);
485
489
  });
486
490
 
487
491
  pi.registerMessageRenderer<undefined>(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
@@ -581,22 +585,24 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
581
585
  },
582
586
 
583
587
  renderCall(args, theme) {
588
+ const gap = " ".repeat(config.mainWindowRenderer?.horizontalSpacing ?? 1);
589
+ const title = theme.fg("toolTitle", theme.bold("subagent"));
584
590
  if (args.action) {
585
591
  const target = args.agent || args.chainName || "";
586
592
  return new Text(
587
- `${theme.fg("toolTitle", theme.bold("subagent "))}${args.action}${target ? ` ${theme.fg("accent", target)}` : ""}`,
593
+ `${title}${gap}${args.action}${target ? `${gap}${theme.fg("accent", target)}` : ""}`,
588
594
  0, 0,
589
595
  );
590
596
  }
591
597
  if (args.workflowScript)
592
598
  return new Text(
593
- `${theme.fg("toolTitle", theme.bold("subagent "))}${formatWorkflowManifest(args.workflowScript, args.async, false)}`,
599
+ `${title}${gap}${formatWorkflowManifest(args.workflowScript, args.async, false)}`,
594
600
  0,
595
601
  0,
596
602
  );
597
- const asyncLabel = args.async === true ? theme.fg("warning", " [async]") : "";
603
+ const asyncLabel = args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : "";
598
604
  return new Text(
599
- `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
605
+ `${title}${gap}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
600
606
  0,
601
607
  0,
602
608
  );
@@ -607,7 +613,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
607
613
  const renderedResult = { ...result, isError: context.isError };
608
614
  return summaryInlineToolDisplay
609
615
  ? renderSubagentSummary(renderedResult, options, theme)
610
- : renderSubagentResult(renderedResult, options, theme);
616
+ : renderSubagentResult(renderedResult, options, theme, undefined, config.mainWindowRenderer);
611
617
  },
612
618
 
613
619
  };
@@ -754,6 +760,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
754
760
  goalTurnId = 0;
755
761
  state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
756
762
  state.parentSessionFile = ctx.sessionManager.getSessionFile();
763
+ state.trustedSessionRoots = [...new Set([
764
+ ...(config.defaultSessionDir ? [path.resolve(expandTilde(config.defaultSessionDir))] : []),
765
+ ...(state.parentSessionFile ? [getSubagentSessionRoot(state.parentSessionFile)] : []),
766
+ ])];
757
767
  state.subagentSpawns = {
758
768
  sessionId: state.currentSessionId,
759
769
  count: 0,
@@ -47,7 +47,7 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
47
47
  if (normalizedAction !== undefined) {
48
48
  const legacyAction = normalizedAction.toLowerCase();
49
49
  if (legacyAction === "single") {
50
- return { ok: false, error: "Direct execution was removed. Use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
50
+ return { ok: false, error: "action='single' is not supported. Omit action and pass { agent, task } for one child.", mode: "workflow" };
51
51
  }
52
52
  if (legacyAction === "parallel" || legacyAction === "tasks" || legacyAction === "chain") {
53
53
  return { ok: false, error: "Legacy top-level chain and parallel inputs were removed; use workflowScript.", mode: "workflow" };
@@ -64,13 +64,39 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
64
64
  if (params.workflowScript !== undefined) {
65
65
  return { ok: false, error: "workflowScript execution must omit action; only schedule.create accepts action with workflowScript.", mode: "management" };
66
66
  }
67
+ if (params.task !== undefined) {
68
+ return { ok: false, error: "Structured single-child task cannot be combined with a management/control action.", mode: "management" };
69
+ }
67
70
  return { ok: true, params: { ...params, action: normalizedAction } };
68
71
  }
69
- if (params.agent !== undefined || params.task !== undefined || params.step !== undefined) {
70
- return { ok: false, error: "Direct execution was removed. Use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
72
+ if (params.step !== undefined) {
73
+ return { ok: false, error: "step is only available with action='append-step'; it is not an execution mode.", mode: "workflow" };
74
+ }
75
+ if (params.workflowScript !== undefined && (params.agent !== undefined || params.task !== undefined)) {
76
+ return { ok: false, error: "Structured single-child execution cannot be combined with workflowScript.", mode: "workflow" };
77
+ }
78
+ if (params.agent !== undefined || params.task !== undefined) {
79
+ if (typeof params.agent !== "string" || !params.agent.trim()) {
80
+ return { ok: false, error: "Structured single-child execution requires agent to be a non-empty string.", mode: "workflow" };
81
+ }
82
+ if (params.task !== undefined && typeof params.task !== "string") {
83
+ return { ok: false, error: "Structured single-child task must be a string when provided.", mode: "workflow" };
84
+ }
85
+ const { agent: _agent, task: _task, ...workflowDefaults } = params;
86
+ const child = {
87
+ agent: params.agent.trim(),
88
+ ...(params.task !== undefined ? { task: params.task } : {}),
89
+ };
90
+ return {
91
+ ok: true,
92
+ params: {
93
+ ...workflowDefaults,
94
+ workflowScript: `console.info("Converted structured single-child request to workflow runs.run('main', ...)."); return runs.run("main", ${JSON.stringify(child)})`,
95
+ } as T,
96
+ };
71
97
  }
72
98
  if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) {
73
- return { ok: false, error: "Execution requires a non-empty workflowScript. Direct execution was removed; use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
99
+ return { ok: false, error: "Execution requires either { agent, task? } for one child or a non-empty workflowScript for orchestration.", mode: "workflow" };
74
100
  }
75
101
  return { ok: true, params };
76
102
  }
@@ -255,21 +255,22 @@ const ControlOverrides = Type.Object({
255
255
  });
256
256
 
257
257
  const SubagentParamProperties = {
258
- agent: Type.Optional(Type.String({ description: "Agent target for management actions such as get, update, delete, and models." })),
258
+ agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
259
+ task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action or workflowScript." })),
259
260
  resume: Type.Optional(Type.String({ description: "Retained child run id for a workflowScript runs.run/runs.all item. Mutually exclusive with agent; task supplies the follow-up." })),
260
261
  // Management action (when present, tool operates in management mode)
261
262
  action: Type.Optional(Type.String({ minLength: 1,
262
- description: "Optional management/control action. Omit this field for workflowScript execution; use it only for management/control actions."
263
+ description: "Optional management/control action. Omit this field for structured single-child or workflowScript execution; use it only for management/control actions."
263
264
  })),
264
265
  name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
265
266
  id: Type.Optional(Type.String({
266
- description: "Run id or prefix for status, interrupt, stop, dismiss, resume, steer, append-step, approve-checkpoint, reject-checkpoint, mission.attach-run, or the decision id for mission.resolve-decision."
267
+ description: "Run id/prefix for status/debug.run, interrupt, steer, append-step, approve-checkpoint, reject-checkpoint, or mission."
267
268
  })),
268
269
  runId: Type.Optional(Type.String({
269
- description: "Target run ID for interrupt, stop, dismiss, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run. Prefer id for new calls."
270
+ description: "Target run ID for debug.run, interrupt, steer, append-step, or mission.attach-run. Prefer id."
270
271
  })),
271
272
  dir: Type.Optional(Type.String({
272
- description: "Async run directory for action='status', action='stop', action='resume', or action='steer'."
273
+ description: "Async run directory for status/debug.run, stop, resume, or steer."
273
274
  })),
274
275
  handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
275
276
  index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
@@ -359,10 +360,10 @@ const { step: _legacyChainStep, ...subagentParamPropertiesWithoutStep } = Subage
359
360
  const trimmedSubagentParamProperties = {
360
361
  ...subagentParamPropertiesWithoutStep,
361
362
  id: Type.Optional(Type.String({
362
- description: "Run id or prefix for status, interrupt, stop, resume, steer, mission.attach-run, or the decision id for mission.resolve-decision."
363
+ description: "Run id/prefix for status/debug.run, interrupt, steer, or mission.attach-run."
363
364
  })),
364
365
  runId: Type.Optional(Type.String({
365
- description: "Target run ID for interrupt, stop, resume, steer, or mission.attach-run. Prefer id for new calls."
366
+ description: "Target run ID for debug.run, interrupt, steer, or mission.attach-run. Prefer id."
366
367
  })),
367
368
  };
368
369
  const SubagentParamsSchema = Type.Object(SubagentParamProperties);