pi-subagents 0.40.0 → 0.42.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 (121) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +54 -1879
  3. package/agents/oracle.md +1 -0
  4. package/inspector-runner.mjs +11 -0
  5. package/package.json +13 -5
  6. package/prompts/parallel-context-build.md +1 -1
  7. package/prompts/parallel-handoff-plan.md +1 -1
  8. package/prompts/review-loop.md +1 -1
  9. package/skills/pi-subagents/SKILL.md +9 -7
  10. package/skills/pi-subagents/references/constraints-and-recipes.md +24 -32
  11. package/skills/pi-subagents/references/execution-controls.md +99 -97
  12. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  13. package/skills/pi-subagents/references/prompting-and-roles.md +23 -30
  14. package/src/agents/agent-management.ts +155 -65
  15. package/src/agents/agent-serializer.ts +19 -0
  16. package/src/agents/agents.ts +154 -71
  17. package/src/agents/chain-serializer.ts +10 -7
  18. package/src/agents/frontmatter.ts +5 -3
  19. package/src/agents/identity.ts +1 -1
  20. package/src/agents/proactive-skills.ts +13 -10
  21. package/src/agents/skills.ts +23 -6
  22. package/src/api/control-channel.ts +4 -0
  23. package/src/api/delegation.ts +26 -194
  24. package/src/api/external-runs.ts +129 -0
  25. package/src/api/intercom-bridge.ts +3 -0
  26. package/src/api/pi-args.ts +5 -0
  27. package/src/api/preflight.ts +3 -3
  28. package/src/api/shared-types.ts +19 -0
  29. package/src/extension/config.ts +10 -0
  30. package/src/extension/control-notices.ts +5 -39
  31. package/src/extension/doctor.ts +10 -9
  32. package/src/extension/fanout-child.ts +7 -4
  33. package/src/extension/index.ts +232 -68
  34. package/src/extension/rpc.ts +18 -12
  35. package/src/extension/schemas.ts +48 -37
  36. package/src/extension/tool-description.ts +36 -86
  37. package/src/inspectors/herdr/actions.ts +229 -0
  38. package/src/inspectors/herdr/client.ts +130 -0
  39. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  40. package/src/inspectors/herdr/project-panes.ts +154 -0
  41. package/src/integrations/herdr-status.ts +330 -0
  42. package/src/intercom/intercom-bridge.ts +3 -2
  43. package/src/intercom/result-intercom.ts +5 -1
  44. package/src/missions/actions.ts +374 -0
  45. package/src/missions/lifecycle.ts +314 -0
  46. package/src/missions/store.ts +442 -0
  47. package/src/missions/types.ts +135 -0
  48. package/src/policy/authority.ts +46 -0
  49. package/src/profiles/profiles.ts +29 -3
  50. package/src/runs/background/async-execution.ts +110 -50
  51. package/src/runs/background/async-job-tracker.ts +27 -4
  52. package/src/runs/background/async-resume.ts +6 -6
  53. package/src/runs/background/async-status.ts +29 -1
  54. package/src/runs/background/auto-drain.ts +3 -3
  55. package/src/runs/background/chain-append.ts +3 -2
  56. package/src/runs/background/control-channel.ts +9 -7
  57. package/src/runs/background/fleet-view.ts +3 -4
  58. package/src/runs/background/notify.ts +2 -1
  59. package/src/runs/background/process-terminal.ts +5 -5
  60. package/src/runs/background/result-watcher.ts +13 -5
  61. package/src/runs/background/run-id-resolver.ts +3 -3
  62. package/src/runs/background/run-status.ts +35 -8
  63. package/src/runs/background/scheduled-runs.ts +602 -375
  64. package/src/runs/background/stale-run-reconciler.ts +3 -3
  65. package/src/runs/background/subagent-runner.ts +608 -445
  66. package/src/runs/background/subagent-wait.ts +50 -9
  67. package/src/runs/background/wait-subscriptions.ts +253 -0
  68. package/src/runs/background/wait-tool.ts +12 -4
  69. package/src/runs/foreground/async-steering-action.ts +3 -3
  70. package/src/runs/foreground/chain-clarify.ts +8 -4
  71. package/src/runs/foreground/chain-execution.ts +56 -30
  72. package/src/runs/foreground/execution.ts +34 -9
  73. package/src/runs/foreground/subagent-executor.ts +1036 -274
  74. package/src/runs/shared/acceptance.ts +28 -6
  75. package/src/runs/shared/child-protocol.ts +302 -22
  76. package/src/runs/shared/dynamic-fanout.ts +1 -1
  77. package/src/runs/shared/external-cli-runner.ts +130 -0
  78. package/src/runs/shared/long-running-guard.ts +42 -1
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -0
  80. package/src/runs/shared/nested-events.ts +59 -5
  81. package/src/runs/shared/nested-render.ts +9 -4
  82. package/src/runs/shared/parallel-handoff.ts +86 -2
  83. package/src/runs/shared/parallel-utils.ts +11 -2
  84. package/src/runs/shared/permissions.ts +95 -0
  85. package/src/runs/shared/pi-args.ts +468 -109
  86. package/src/runs/shared/pi-spawn.ts +11 -1
  87. package/src/runs/shared/run-history.ts +1 -1
  88. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  89. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  90. package/src/runs/shared/turn-budget.ts +6 -6
  91. package/src/runs/shared/worktree.ts +122 -12
  92. package/src/shared/accessible-dir.ts +29 -7
  93. package/src/shared/artifacts.ts +143 -1
  94. package/src/shared/fork-context.ts +3 -2
  95. package/src/shared/launch-contract.ts +1 -0
  96. package/src/shared/settings.ts +10 -0
  97. package/src/shared/types.ts +156 -14
  98. package/src/shared/utils.ts +8 -6
  99. package/src/slash/delegation-adapters.ts +32 -194
  100. package/src/slash/delegation-request.ts +43 -126
  101. package/src/slash/prompt-template-bridge.ts +158 -205
  102. package/src/slash/prompt-workflows.ts +21 -57
  103. package/src/slash/slash-bridge.ts +14 -0
  104. package/src/slash/slash-commands.ts +31 -632
  105. package/src/slash/subagents-admin.ts +18 -14
  106. package/src/tui/fleet-status.ts +156 -21
  107. package/src/tui/fleet-transcript.ts +110 -5
  108. package/src/tui/fleet.ts +57 -25
  109. package/src/tui/render.ts +368 -152
  110. package/src/types/pi-runtime-compat.d.ts +8 -0
  111. package/src/watchdog/lsp-diagnostics.ts +12 -7
  112. package/src/watchdog/model-selection.ts +2 -2
  113. package/src/watchdog/permission-arbiter.ts +145 -0
  114. package/src/watchdog/register-child.ts +1 -1
  115. package/src/watchdog/register-main.ts +1 -1
  116. package/src/watchdog/review.ts +4 -1
  117. package/src/watchdog/runtime.ts +3 -2
  118. package/src/workflows/chat-progress.ts +140 -0
  119. package/src/workflows/scripted-workflow.ts +415 -0
  120. package/agents/advisor.md +0 -73
  121. package/src/extension/chain-validation.ts +0 -181
@@ -135,10 +135,10 @@ fixes worth doing now. Parent agents can also apply the same recipes directly
135
135
  with `subagent(...)` when the user describes the workflow in natural language
136
136
  instead of invoking a slash command.
137
137
 
138
- Additional user prompt templates can delegate into `pi-subagents` through the native `/prompt-workflow` and `/chain-prompts` commands. This is useful when a slash command should always run through a particular agent or with forked context. Prompt frontmatter can set `subagent`, `model`, `skill`, `cwd`, `worktree`, `fresh`, `fork`, or `inheritContext` for the native adapter.
138
+ Additional user prompt templates can delegate into `pi-subagents` through the native `/prompt-workflow` command. This is useful when a slash command should always run through a particular agent or with forked context. Prompt frontmatter can set `subagent`, `model`, `skill`, `cwd`, `fresh`, `fork`, or `inheritContext` for the native adapter.
139
139
 
140
140
  ## Extension RPC
141
141
 
142
- Other Pi extensions can call `pi-subagents` through the in-process event bus. The stable v1 channels are `subagents:rpc:v1:ready`, `subagents:rpc:v1:request`, and per-request replies at `subagents:rpc:v1:reply:<requestId>`. Envelopes use `{ version: 1, requestId, method, params }`, and replies use `{ version: 1, requestId, success, data | error }`. `ping` advertises the exact process-local async completion event as `events.asyncComplete` for RPC-spawn consumers.
142
+ Other Pi extensions can call `pi-subagents` through the in-process event bus. The RPC channels are `subagents:rpc:v1:ready`, `subagents:rpc:v1:request`, and per-request replies at `subagents:rpc:v1:reply:<requestId>`. Envelopes use `{ version: 1, requestId, method, params }`, and replies use `{ version: 1, requestId, success, data | error }`. `ping` advertises the exact process-local async completion event as `events.asyncComplete` for RPC-spawn consumers.
143
143
 
144
144
  Methods: `ping`, `status`, `spawn`, `steer`, `interrupt`, `resume`, and `stop`. `ping` capability metadata advertises optional projections: `capabilities.fleetStatus: { version: 1 }` adds bounded current-session `data.fleet` records (opaque reconciliation `key`, resolved `agent`, optional `role`, `model`, `effort`, caller-facing `goal`, `startedAt`, split `{ input, output, total }` tokens, plus `totalActive`/`omitted` overflow counts) to successful `status` replies; `capabilities.launchResolvedExtensions` advertises parent-resolved opaque launch-extension identifiers in status details; `capabilities.runtimeAcknowledgedExtensions` advertises the best-effort child-runtime acknowledgement projection fed by cooperating extensions emitting `subagent:acknowledge-extension`. Foreground `details.results[]` rows carry a stable numeric `index`; correlate children by `(runId, index)` rather than row position. Consumers should read status/result artifacts and RPC projections instead of scraping terminal output and must ignore unknown fields. `spawn` is async-only and rejects management actions, `async: false`, or `clarify: true`; it reuses the normal executor, so discovery, validation, session attribution, configured spawn caps, child-safety depth, artifacts, and async status are shared with the `subagent` tool. `status`, acknowledged async `steer`, and `interrupt` map to the normal control actions. RPC steer disables pause-and-revive recovery and advertises `capabilities.nonRecoveringSteer`, preserving the caller's authority over the exact spawned child. `resume` requires a target plus non-empty message and delegates to the package-owned revival path; it may set a caller-owned `file-only` output path but cannot override the persisted child model, tools, budgets, session ownership, or exclusive session lease. `stop` targets running async runs through the existing timeout control channel. `pi.events` is process-local, so separate Pi processes and child subagents need lifecycle artifact files or `pi-intercom` instead.
@@ -24,9 +24,7 @@ Agents can use the `subagent(...)` tool directly for execution, management, stat
24
24
  Humans often use the slash-command layer instead:
25
25
 
26
26
  - `/run` — launch a single agent
27
- - `/chain` — launch a chain of steps
28
- - `/parallel` — launch top-level parallel tasks
29
- - `/run-chain` — launch a saved `.chain.md` or `.chain.json` workflow
27
+ - `workflowScript` — the sole public surface for sequence, parallelism, branching, retries, and aggregation
30
28
  - `/subagents` — interactive admin for inspecting agents and editing model, thinking, or system prompt
31
29
  - `/subagents-stop [run-id]` — stop a current-session top-level async run; opens a selector when no id is given
32
30
  - `/subagents-detach [run-id]` — detach an active foreground single-subagent run without terminating its child
@@ -36,7 +34,7 @@ Humans often use the slash-command layer instead:
36
34
  - `/subagents-doctor` — diagnose setup, discovery, async paths, and intercom bridge state
37
35
  - `/subagents-models [agent]` — show the live runtime-loaded builtin model mapping
38
36
  - `/subagents-profiles`, `/subagents-load-profile`, `/subagents-refresh-provider-models`, `/subagents-generate-profiles`, `/subagents-check-profile` — manage model profiles and provider catalogs
39
- - `/prompt-workflow` and `/chain-prompts` — run prompt templates through native subagent single/chain workflows
37
+ - `/prompt-workflow` — run a prompt template through native single-agent or workflowScript execution
40
38
 
41
39
  Prefer the tool when you are writing agent logic. Prefer the slash commands when
42
40
  you are guiding a human through an interactive flow.
@@ -52,7 +50,7 @@ Packaged prompt shortcuts are also available for repeatable workflows. Treat the
52
50
 
53
51
  ## Applying Prompt Techniques Without Slash Commands
54
52
 
55
- The prompt templates in `prompts/` encode workflows the parent agent can run on demand. If the user provides a URL, issue, PR, plan, local file, screenshot, or freeform target, treat that target as the primary scope: read or fetch it before launching children, then include it explicitly in every child task. Do not depend on the parent conversation history when the recipe calls for fresh context.
53
+ The prompt templates in `prompts/` encode workflows the parent agent can run on demand. If the user provides a URL, issue, PR, plan, local file, screenshot, or freeform target, treat that target as the primary scope: read or fetch it before launching children, then include it explicitly in every child task. For targets outside the parent cwd, include the exact repository, explicit `cwd`, authority boundary, and expected output path in each child task. Do not depend on the parent conversation history when the recipe calls for fresh context.
56
54
 
57
55
  ### Parallel review technique
58
56
 
@@ -94,21 +92,18 @@ Use this when the question needs both external evidence and local implications.
94
92
 
95
93
  ### Parallel context-build technique
96
94
 
97
- Use this before planning or implementation when a stronger handoff is needed. Run a chain with one parallel step of `context-builder` agents rather than top-level parallel tasks, so relative output files live under the temporary chain directory. Give every task a distinct output path such as `context-build/request-and-scope.md`, `context-build/codebase-and-patterns.md`, and `context-build/validation-and-risks.md`. Choose two or three builders: request/scope, codebase/patterns, and validation/risks. Each builder must read every relevant file needed to understand its slice, follow imports/callers/tests/docs/config, conduct tool-available web research when needed, and include a compact `meta-prompt` section. The parent synthesizes the outputs into important context, recommended next meta-prompt, open questions, assumptions, and artifact paths.
95
+ Use this before planning or implementation when a stronger handoff is needed. Use `workflowScript` with `runs.all` to launch distinct `context-builder` lanes, each with an explicit output path. Give every task a distinct output path such as `context-build/request-and-scope.md`, `context-build/codebase-and-patterns.md`, and `context-build/validation-and-risks.md`. Choose two or three builders: request/scope, codebase/patterns, and validation/risks. Each builder must read every relevant file needed to understand its slice, follow imports/callers/tests/docs/config, conduct tool-available web research when needed, and include a compact `meta-prompt` section. The parent synthesizes the outputs into important context, recommended next meta-prompt, open questions, assumptions, and artifact paths.
98
96
 
99
97
  Example shape:
100
98
 
101
99
  ```typescript
102
- subagent({
103
- chain: [{
104
- parallel: [
105
- { agent: "context-builder", task: "Build request/scope context for: ...", output: "context-build/request-and-scope.md" },
106
- { agent: "context-builder", task: "Build codebase/pattern context for: ...", output: "context-build/codebase-and-patterns.md" },
107
- { agent: "context-builder", task: "Build validation/risk context for: ...", output: "context-build/validation-and-risks.md" }
108
- ]
109
- }],
110
- context: "fresh"
111
- })
100
+ subagent({ workflowScript: `
101
+ const results = await runs.all([
102
+ { key: "lane-a", agent: "reviewer", task: "Inspect lane A" },
103
+ { key: "lane-b", agent: "reviewer", task: "Inspect lane B" }
104
+ ]);
105
+ return results.map(result => result.output);
106
+ ` })
112
107
  ```
113
108
 
114
109
  ### Parallel handoff-plan technique
@@ -118,17 +113,13 @@ Use this when the user needs a solution brief or implementation-ready handoff fr
118
113
  Example shape:
119
114
 
120
115
  ```typescript
121
- subagent({
122
- chain: [
123
- { parallel: [
124
- { agent: "researcher", task: "Research the external reference and transferable implementation ideas for: ...", output: "handoff/external-reference.md" },
125
- { agent: "context-builder", task: "Build local codebase context for: ...", output: "handoff/local-context.md" },
126
- { agent: "context-builder", task: "Compare evidence and propose implementation strategy for: ...", output: "handoff/implementation-strategy.md" }
127
- ] },
128
- { agent: "context-builder", task: "Read {previous} and synthesize the final handoff plan and implementation-ready meta-prompt.", output: "handoff/final-handoff-plan.md" }
129
- ],
130
- context: "fresh"
131
- })
116
+ subagent({ workflowScript: `
117
+ const results = await runs.all([
118
+ { key: "lane-a", agent: "reviewer", task: "Inspect lane A" },
119
+ { key: "lane-b", agent: "reviewer", task: "Inspect lane B" }
120
+ ]);
121
+ return results.map(result => result.output);
122
+ ` })
132
123
  ```
133
124
 
134
125
  ### Gather-context-and-clarify technique
@@ -149,7 +140,7 @@ Use this when a broad diff has known reviewer findings across several items and
149
140
 
150
141
  Prefer `async: true`, `context: "fresh"` for planners/validators, `outputMode: "file-only"` for large summaries, and per-stage output names that will not collide. Add `phase` and `label` to make async status readable, and use `as` plus `{outputs.name}` when a later step needs a specific earlier result instead of the whole `{previous}` blob. Use this pattern instead of launching several writer workers into a dirty worktree. Include non-blocking suggestions in the writer prompt only when they are small, safe, and do not expand product scope; otherwise record them as deferred.
151
142
 
152
- When the first step can return a structured target list, prefer dynamic fanout instead of hand-authoring a static parallel group. Use `outputSchema` and `as` on the producer, then an `expand` step with `from: { output, path }`, an explicit `maxItems`, one `parallel` child template, and `collect.as`. Item templates may use `{item}` or a named item such as `{target.path}`. Do not use dynamic fanout for prose outputs, nested fanout, dynamic agent selection, reducers, `when` conditions, or arbitrary expressions; `.chain.md` does not support this syntax, so use direct JSON or a saved `.chain.json`.
143
+ When one child returns a structured target list, use ordinary JavaScript to validate/filter it and map bounded entries into `runs.all`; do not use the removed chain fanout DSL.
153
144
 
154
145
  Example shape:
155
146
 
@@ -221,12 +212,14 @@ Builtin role agents inherit the current Pi default model unless you override the
221
212
 
222
213
  A strong subagent prompt usually includes:
223
214
  - **Goal**: the concrete outcome the child should produce.
215
+ - **Target**: repository, explicit `cwd`, branch/ref/head, and source seam when the target is not the parent cwd.
216
+ - **Authority boundary**: whether the child may read, edit, commit, push, comment, close, merge, publish, or release. Omit or forbid actions that are not approved.
224
217
  - **Context/evidence**: relevant plan paths, files, diffs, decisions, or user constraints already approved.
225
218
  - **Success criteria**: what must be true before the child can finish.
226
219
  - **Hard constraints**: true invariants only, such as no edits for review-only tasks, one writer thread, child must not run subagents unless it is an explicitly assigned `tools: subagent` fanout child, or escalation for unapproved decisions.
227
220
  - **Validation**: targeted checks to run, or the next-best check when validation is impossible.
228
- - **Output**: the expected summary shape, artifact path, or finding format.
229
- - **Stop rules**: when to ask via `intercom`, when to stop after enough evidence, and when not to keep searching.
221
+ - **Output**: the expected summary shape, artifact path, or finding format. Use repo-qualified durable output paths for cross-codebase waves.
222
+ - **Stop rules**: when to ask via `intercom` or `contact_supervisor`, when to stop after enough evidence, and when not to keep searching.
230
223
 
231
224
  Avoid carrying over old prompt habits that over-specify every step. Use `must`, `always`, and `never` for real invariants; for judgment calls, give decision rules. For example, tell a reviewer to inspect the staged diff directly and report only evidence-backed findings, rather than prescribing every file or command. Tell a researcher the retrieval budget: start with broad targeted searches, fetch only the strongest sources, search again only when a required fact is missing, then stop.
232
225
 
@@ -46,7 +46,7 @@ interface ManagementParams {
46
46
  action?: string;
47
47
  agent?: string;
48
48
  chainName?: string;
49
- agentScope?: string;
49
+ agentScope?: unknown;
50
50
  config?: unknown;
51
51
  }
52
52
 
@@ -215,34 +215,54 @@ function skillsWarning(cwd: string, agent: Pick<AgentConfig, "skills" | "skillPa
215
215
  }
216
216
 
217
217
  export function editableAgentConfig(agent: AgentConfig): AgentConfig {
218
+ const { extensions: _extensions, ...withoutExtensions } = agent;
218
219
  const base = agent.override?.base;
220
+ const {
221
+ override: _override,
222
+ model: _model,
223
+ fallbackModels: _fallbackModels,
224
+ thinking: _thinking,
225
+ systemPromptMode: _systemPromptMode,
226
+ inheritProjectContext: _inheritProjectContext,
227
+ inheritSkills: _inheritSkills,
228
+ defaultContext: _defaultContext,
229
+ acceptanceRole: _acceptanceRole,
230
+ disabled: _disabled,
231
+ systemPrompt: _systemPrompt,
232
+ skills: _skills,
233
+ skillPath: _skillPath,
234
+ tools: _tools,
235
+ mcpDirectTools: _mcpDirectTools,
236
+ subagentOnlyExtensions: _subagentOnlyExtensions,
237
+ completionGuard: _completionGuard,
238
+ ...editable
239
+ } = withoutExtensions;
219
240
  if (!base) {
220
241
  return {
221
- ...agent,
222
- extensions: agent.extensionsFromDefault ? undefined : agent.extensions ? [...agent.extensions] : undefined,
242
+ ...withoutExtensions,
243
+ ...(agent.extensionsFromDefault ? {} : agent.extensions !== undefined ? { extensions: [...agent.extensions] } : {}),
223
244
  };
224
245
  }
225
246
 
226
247
  return {
227
- ...agent,
228
- model: base.model,
229
- fallbackModels: base.fallbackModels ? [...base.fallbackModels] : undefined,
230
- thinking: base.thinking,
248
+ ...editable,
249
+ ...(base.model !== undefined ? { model: base.model } : {}),
250
+ ...(base.fallbackModels !== undefined ? { fallbackModels: [...base.fallbackModels] } : {}),
251
+ ...(base.thinking !== undefined ? { thinking: base.thinking } : {}),
231
252
  systemPromptMode: base.systemPromptMode,
232
253
  inheritProjectContext: base.inheritProjectContext,
233
254
  inheritSkills: base.inheritSkills,
234
- defaultContext: base.defaultContext,
235
- acceptanceRole: base.acceptanceRole,
236
- disabled: base.disabled,
255
+ ...(base.defaultContext !== undefined ? { defaultContext: base.defaultContext } : {}),
256
+ ...(base.acceptanceRole !== undefined ? { acceptanceRole: base.acceptanceRole } : {}),
257
+ ...(base.disabled !== undefined ? { disabled: base.disabled } : {}),
237
258
  systemPrompt: base.systemPrompt,
238
- skills: base.skills ? [...base.skills] : undefined,
239
- skillPath: base.skillPath ? [...base.skillPath] : undefined,
240
- tools: base.tools ? [...base.tools] : undefined,
241
- mcpDirectTools: base.mcpDirectTools ? [...base.mcpDirectTools] : undefined,
242
- extensions: base.extensions ? [...base.extensions] : undefined,
243
- subagentOnlyExtensions: base.subagentOnlyExtensions ? [...base.subagentOnlyExtensions] : undefined,
244
- completionGuard: base.completionGuard,
245
- override: undefined,
259
+ ...(base.skills !== undefined ? { skills: [...base.skills] } : {}),
260
+ ...(base.skillPath !== undefined ? { skillPath: [...base.skillPath] } : {}),
261
+ ...(base.tools !== undefined ? { tools: [...base.tools] } : {}),
262
+ ...(base.mcpDirectTools !== undefined ? { mcpDirectTools: [...base.mcpDirectTools] } : {}),
263
+ ...(base.extensions !== undefined ? { extensions: [...base.extensions] } : {}),
264
+ ...(base.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...base.subagentOnlyExtensions] } : {}),
265
+ ...(base.completionGuard !== undefined ? { completionGuard: base.completionGuard } : {}),
246
266
  };
247
267
  }
248
268
 
@@ -266,6 +286,7 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
266
286
  if (hasKey(cfg, "description")) changed("description");
267
287
  if (hasKey(cfg, "aliases")) changed("alias", "aliases");
268
288
  if (hasKey(cfg, "systemPrompt")) changed("systemPrompt");
289
+ if (hasKey(cfg, "runner")) changed("runner");
269
290
  if (hasKey(cfg, "model")) changed("model");
270
291
  if (hasKey(cfg, "fallbackModels")) changed("fallbackModels");
271
292
  if (hasKey(cfg, "tools")) changed("tools");
@@ -364,7 +385,7 @@ function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: strin
364
385
  if (hasKey(s, "toolBudget")) {
365
386
  const validation = validateToolBudgetConfig(s.toolBudget, `config.steps[${i}].toolBudget`);
366
387
  if (validation.error) return { error: validation.error };
367
- step.toolBudget = s.toolBudget as ChainStepConfig["toolBudget"];
388
+ if (s.toolBudget !== undefined) step.toolBudget = s.toolBudget as ToolBudgetConfig;
368
389
  }
369
390
  steps.push(step);
370
391
  }
@@ -380,18 +401,23 @@ function parseTools(raw: string): { tools?: string[]; mcpDirectTools?: string[]
380
401
  if (direct) mcpDirectTools.push(direct);
381
402
  } else tools.push(item);
382
403
  }
383
- return { tools: tools.length ? tools : undefined, mcpDirectTools: mcpDirectTools.length ? mcpDirectTools : undefined };
404
+ return {
405
+ ...(tools.length ? { tools } : {}),
406
+ ...(mcpDirectTools.length ? { mcpDirectTools } : {}),
407
+ };
384
408
  }
385
409
 
386
410
  function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): string | undefined {
387
411
  if (hasKey(cfg, "aliases")) {
388
- if (cfg.aliases === false || cfg.aliases === "") target.aliases = undefined;
412
+ if (cfg.aliases === false || cfg.aliases === "") delete target.aliases;
389
413
  else if (typeof cfg.aliases === "string") {
390
414
  const aliases = parseCsv(cfg.aliases).filter((alias) => alias !== target.name);
391
- target.aliases = aliases.length ? aliases : undefined;
415
+ if (aliases.length) target.aliases = aliases;
416
+ else delete target.aliases;
392
417
  } else if (Array.isArray(cfg.aliases) && cfg.aliases.every((entry) => typeof entry === "string")) {
393
418
  const aliases = [...new Set(cfg.aliases.map((entry) => entry.trim()).filter(Boolean).filter((alias) => alias !== target.name))];
394
- target.aliases = aliases.length ? aliases : undefined;
419
+ if (aliases.length) target.aliases = aliases;
420
+ else delete target.aliases;
395
421
  } else return "config.aliases must be a comma-separated string, string array, or false when provided.";
396
422
  }
397
423
  if (hasKey(cfg, "systemPrompt")) {
@@ -399,58 +425,92 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
399
425
  else if (typeof cfg.systemPrompt === "string") target.systemPrompt = cfg.systemPrompt;
400
426
  else return "config.systemPrompt must be a string or false when provided.";
401
427
  }
428
+ if (hasKey(cfg, "runner")) {
429
+ if (cfg.runner === false || cfg.runner === "") delete target.runner;
430
+ else if (cfg.runner && typeof cfg.runner === "object" && !Array.isArray(cfg.runner)) {
431
+ const runner = cfg.runner as Record<string, unknown>;
432
+ if (runner.type === "pi" && Object.keys(runner).every((key) => key === "type")) target.runner = { type: "pi" };
433
+ else if (runner.type === "external-cli" && typeof runner.command === "string" && runner.command.trim()
434
+ && (runner.args === undefined || (Array.isArray(runner.args) && runner.args.every((arg) => typeof arg === "string")))
435
+ && (runner.promptDelivery === undefined || runner.promptDelivery === "stdin")
436
+ && Object.keys(runner).every((key) => ["type", "command", "args", "promptDelivery"].includes(key))) {
437
+ const runnerArgs = Array.isArray(runner.args) ? runner.args.filter((arg): arg is string => typeof arg === "string") : undefined;
438
+ target.runner = { type: "external-cli", command: runner.command.trim(), ...(runnerArgs?.length ? { args: runnerArgs } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" } : {}) };
439
+ } else return "config.runner must be { type: 'pi' } or { type: 'external-cli', command: string, args?: string[], promptDelivery?: 'stdin' }.";
440
+ } else return "config.runner must be an object, false, or empty string when provided.";
441
+ }
402
442
  if (hasKey(cfg, "model")) {
403
- if (cfg.model === false || cfg.model === "") target.model = undefined;
404
- else if (typeof cfg.model === "string") target.model = cfg.model.trim() || undefined;
405
- else return "config.model must be a string or false when provided.";
443
+ if (cfg.model === false || cfg.model === "") delete target.model;
444
+ else if (typeof cfg.model === "string") {
445
+ const model = cfg.model.trim();
446
+ if (model) target.model = model;
447
+ else delete target.model;
448
+ } else return "config.model must be a string or false when provided.";
406
449
  }
407
450
  if (hasKey(cfg, "fallbackModels")) {
408
- if (cfg.fallbackModels === false || cfg.fallbackModels === "") target.fallbackModels = undefined;
451
+ if (cfg.fallbackModels === false || cfg.fallbackModels === "") delete target.fallbackModels;
409
452
  else if (typeof cfg.fallbackModels === "string") {
410
453
  const models = parseCsv(cfg.fallbackModels);
411
- target.fallbackModels = models.length ? models : undefined;
454
+ if (models.length) target.fallbackModels = models;
455
+ else delete target.fallbackModels;
412
456
  } else if (Array.isArray(cfg.fallbackModels)) {
413
457
  const models = cfg.fallbackModels
414
458
  .filter((value): value is string => typeof value === "string")
415
459
  .map((value) => value.trim())
416
460
  .filter(Boolean);
417
- target.fallbackModels = models.length ? [...new Set(models)] : undefined;
461
+ if (models.length) target.fallbackModels = [...new Set(models)];
462
+ else delete target.fallbackModels;
418
463
  } else return "config.fallbackModels must be a comma-separated string, string array, or false when provided.";
419
464
  }
420
465
  if (hasKey(cfg, "tools")) {
421
- if (cfg.tools === false || cfg.tools === "") { target.tools = undefined; target.mcpDirectTools = undefined; }
422
- else if (typeof cfg.tools === "string") { const parsed = parseTools(cfg.tools); target.tools = parsed.tools; target.mcpDirectTools = parsed.mcpDirectTools; }
423
- else return "config.tools must be a comma-separated string or false when provided.";
466
+ if (cfg.tools === false || cfg.tools === "") { delete target.tools; delete target.mcpDirectTools; }
467
+ else if (typeof cfg.tools === "string") {
468
+ const parsed = parseTools(cfg.tools);
469
+ if (parsed.tools) target.tools = parsed.tools;
470
+ else delete target.tools;
471
+ if (parsed.mcpDirectTools) target.mcpDirectTools = parsed.mcpDirectTools;
472
+ else delete target.mcpDirectTools;
473
+ } else return "config.tools must be a comma-separated string or false when provided.";
424
474
  }
425
475
  if (hasKey(cfg, "skills")) {
426
- if (cfg.skills === false || cfg.skills === "") target.skills = undefined;
427
- else if (typeof cfg.skills === "string") { const skills = parseCsv(cfg.skills); target.skills = skills.length ? skills : undefined; }
428
- else return "config.skills must be a comma-separated string or false when provided.";
476
+ if (cfg.skills === false || cfg.skills === "") delete target.skills;
477
+ else if (typeof cfg.skills === "string") {
478
+ const skills = parseCsv(cfg.skills);
479
+ if (skills.length) target.skills = skills;
480
+ else delete target.skills;
481
+ } else return "config.skills must be a comma-separated string or false when provided.";
429
482
  }
430
483
  if (hasKey(cfg, "skillPath")) {
431
- if (cfg.skillPath === false || cfg.skillPath === "") target.skillPath = undefined;
432
- else if (typeof cfg.skillPath === "string") { const skillPath = parseCsv(cfg.skillPath); target.skillPath = skillPath.length ? skillPath : undefined; }
433
- else if (Array.isArray(cfg.skillPath) && cfg.skillPath.every((entry) => typeof entry === "string")) {
484
+ if (cfg.skillPath === false || cfg.skillPath === "") delete target.skillPath;
485
+ else if (typeof cfg.skillPath === "string") {
486
+ const skillPath = parseCsv(cfg.skillPath);
487
+ if (skillPath.length) target.skillPath = skillPath;
488
+ else delete target.skillPath;
489
+ } else if (Array.isArray(cfg.skillPath) && cfg.skillPath.every((entry) => typeof entry === "string")) {
434
490
  const skillPath = [...new Set(cfg.skillPath.map((entry) => entry.trim()).filter(Boolean))];
435
- target.skillPath = skillPath.length ? skillPath : undefined;
491
+ if (skillPath.length) target.skillPath = skillPath;
492
+ else delete target.skillPath;
436
493
  } else return "config.skillPath must be a comma-separated string, string array, or false when provided.";
437
494
  }
438
495
  if (hasKey(cfg, "extensions")) {
439
- if (cfg.extensions === false) target.extensions = undefined;
496
+ if (cfg.extensions === false) delete target.extensions;
440
497
  else if (cfg.extensions === "") target.extensions = [];
441
498
  else if (typeof cfg.extensions === "string") target.extensions = parseCsv(cfg.extensions);
442
499
  else return "config.extensions must be a comma-separated string, empty string, or false when provided.";
443
500
  }
444
501
  if (hasKey(cfg, "subagentOnlyExtensions")) {
445
- if (cfg.subagentOnlyExtensions === false) target.subagentOnlyExtensions = undefined;
502
+ if (cfg.subagentOnlyExtensions === false) delete target.subagentOnlyExtensions;
446
503
  else if (cfg.subagentOnlyExtensions === "") target.subagentOnlyExtensions = [];
447
504
  else if (typeof cfg.subagentOnlyExtensions === "string") target.subagentOnlyExtensions = parseCsv(cfg.subagentOnlyExtensions);
448
505
  else return "config.subagentOnlyExtensions must be a comma-separated string, empty string, or false when provided.";
449
506
  }
450
507
  if (hasKey(cfg, "thinking")) {
451
- if (cfg.thinking === false || cfg.thinking === "") target.thinking = undefined;
452
- else if (typeof cfg.thinking === "string") target.thinking = cfg.thinking.trim() || undefined;
453
- else return "config.thinking must be a string or false when provided.";
508
+ if (cfg.thinking === false || cfg.thinking === "") delete target.thinking;
509
+ else if (typeof cfg.thinking === "string") {
510
+ const thinking = cfg.thinking.trim();
511
+ if (thinking) target.thinking = thinking;
512
+ else delete target.thinking;
513
+ } else return "config.thinking must be a string or false when provided.";
454
514
  }
455
515
  if (hasKey(cfg, "systemPromptMode")) {
456
516
  if (cfg.systemPromptMode === "append" || cfg.systemPromptMode === "replace") target.systemPromptMode = cfg.systemPromptMode;
@@ -465,30 +525,31 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
465
525
  target.inheritSkills = cfg.inheritSkills;
466
526
  }
467
527
  if (hasKey(cfg, "defaultContext")) {
468
- if (cfg.defaultContext === false || cfg.defaultContext === "") target.defaultContext = undefined;
528
+ if (cfg.defaultContext === false || cfg.defaultContext === "") delete target.defaultContext;
469
529
  else if (cfg.defaultContext === "fresh" || cfg.defaultContext === "fork") target.defaultContext = cfg.defaultContext;
470
530
  else return "config.defaultContext must be 'fresh', 'fork', or false when provided.";
471
531
  }
472
532
  if (hasKey(cfg, "async")) {
473
- if (cfg.async === "") target.defaultAsync = undefined;
533
+ if (cfg.async === "") delete target.defaultAsync;
474
534
  else if (typeof cfg.async === "boolean") target.defaultAsync = cfg.async;
475
535
  else return "config.async must be a boolean or empty string when provided.";
476
536
  }
477
537
  if (hasKey(cfg, "timeoutMs")) {
478
- if (cfg.timeoutMs === false || cfg.timeoutMs === "") target.defaultTimeoutMs = undefined;
538
+ if (cfg.timeoutMs === false || cfg.timeoutMs === "") delete target.defaultTimeoutMs;
479
539
  else if (typeof cfg.timeoutMs === "number" && Number.isInteger(cfg.timeoutMs) && cfg.timeoutMs > 0) target.defaultTimeoutMs = cfg.timeoutMs;
480
540
  else return "config.timeoutMs must be a positive integer or false when provided.";
481
541
  }
482
542
  if (hasKey(cfg, "turnBudget")) {
483
- if (cfg.turnBudget === false || cfg.turnBudget === "") target.defaultTurnBudget = undefined;
543
+ if (cfg.turnBudget === false || cfg.turnBudget === "") delete target.defaultTurnBudget;
484
544
  else {
485
545
  const resolved = resolveTurnBudgetConfig(cfg.turnBudget, "config.turnBudget");
486
546
  if (resolved.error) return resolved.error;
487
- target.defaultTurnBudget = resolved.turnBudget;
547
+ if (resolved.turnBudget !== undefined) target.defaultTurnBudget = resolved.turnBudget;
548
+ else delete target.defaultTurnBudget;
488
549
  }
489
550
  }
490
551
  if (hasKey(cfg, "acceptance")) {
491
- if (cfg.acceptance === "") target.defaultAcceptance = undefined;
552
+ if (cfg.acceptance === "") delete target.defaultAcceptance;
492
553
  else {
493
554
  const errors = validateAcceptanceInput(cfg.acceptance, "config.acceptance");
494
555
  if (errors.length > 0) return errors.join(" ");
@@ -496,20 +557,21 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
496
557
  }
497
558
  }
498
559
  if (hasKey(cfg, "acceptanceRole")) {
499
- if (cfg.acceptanceRole === false || cfg.acceptanceRole === "") target.acceptanceRole = undefined;
560
+ if (cfg.acceptanceRole === false || cfg.acceptanceRole === "") delete target.acceptanceRole;
500
561
  else if (cfg.acceptanceRole === "read-only" || cfg.acceptanceRole === "writer") target.acceptanceRole = cfg.acceptanceRole;
501
562
  else return "config.acceptanceRole must be 'read-only', 'writer', or false when provided.";
502
563
  }
503
564
  if (hasKey(cfg, "output")) {
504
- if (cfg.output === false || cfg.output === "") target.output = undefined;
565
+ if (cfg.output === false || cfg.output === "") delete target.output;
505
566
  else if (typeof cfg.output === "string") target.output = cfg.output;
506
567
  else return "config.output must be a string or false when provided.";
507
568
  }
508
569
  if (hasKey(cfg, "reads")) {
509
- if (cfg.reads === false || cfg.reads === "") target.defaultReads = undefined;
570
+ if (cfg.reads === false || cfg.reads === "") delete target.defaultReads;
510
571
  else if (typeof cfg.reads === "string") {
511
572
  const reads = parseCsv(cfg.reads);
512
- target.defaultReads = reads.length ? reads : undefined;
573
+ if (reads.length) target.defaultReads = reads;
574
+ else delete target.defaultReads;
513
575
  } else return "config.reads must be a comma-separated string or false when provided.";
514
576
  }
515
577
  if (hasKey(cfg, "progress")) {
@@ -517,7 +579,7 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
517
579
  target.defaultProgress = cfg.progress;
518
580
  }
519
581
  if (hasKey(cfg, "maxSubagentDepth")) {
520
- if (cfg.maxSubagentDepth === false || cfg.maxSubagentDepth === "") target.maxSubagentDepth = undefined;
582
+ if (cfg.maxSubagentDepth === false || cfg.maxSubagentDepth === "") delete target.maxSubagentDepth;
521
583
  else if (typeof cfg.maxSubagentDepth === "number" && Number.isInteger(cfg.maxSubagentDepth) && cfg.maxSubagentDepth >= 0) {
522
584
  target.maxSubagentDepth = cfg.maxSubagentDepth;
523
585
  } else return "config.maxSubagentDepth must be an integer >= 0 or false when provided.";
@@ -527,13 +589,28 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
527
589
  target.completionGuard = cfg.completionGuard;
528
590
  }
529
591
  if (hasKey(cfg, "toolBudget")) {
530
- if (cfg.toolBudget === false || cfg.toolBudget === "") target.toolBudget = undefined;
592
+ if (cfg.toolBudget === false || cfg.toolBudget === "") delete target.toolBudget;
531
593
  else {
532
594
  const validation = validateToolBudgetConfig(cfg.toolBudget, "config.toolBudget");
533
595
  if (validation.error) return validation.error;
534
596
  target.toolBudget = cfg.toolBudget as ToolBudgetConfig;
535
597
  }
536
598
  }
599
+ if (target.runner?.type === "external-cli") {
600
+ const unsupported = [
601
+ target.tools?.length || target.mcpDirectTools?.length ? "tools" : undefined,
602
+ target.model ? "model" : undefined,
603
+ target.fallbackModels?.length ? "fallbackModels" : undefined,
604
+ target.thinking ? "thinking" : undefined,
605
+ target.extensions?.length ? "extensions" : undefined,
606
+ target.subagentOnlyExtensions?.length ? "subagentOnlyExtensions" : undefined,
607
+ target.skills?.length || target.skillPath?.length ? "skills" : undefined,
608
+ target.maxSubagentDepth !== undefined ? "maxSubagentDepth" : undefined,
609
+ target.completionGuard !== undefined ? "completionGuard" : undefined,
610
+ target.toolBudget ? "toolBudget" : undefined,
611
+ ].filter((field): field is string => Boolean(field));
612
+ if (unsupported.length > 0) return `config.runner type 'external-cli' does not support Pi-only fields: ${unsupported.join(", ")}.`;
613
+ }
537
614
  return undefined;
538
615
  }
539
616
 
@@ -599,6 +676,7 @@ function formatAgentDetail(agent: AgentConfig): string {
599
676
  if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
600
677
  if (agent.skillPath?.length) lines.push(`Skill paths: ${agent.skillPath.join(", ")}`);
601
678
  lines.push(`System prompt mode: ${agent.systemPromptMode}`);
679
+ if (agent.runner) lines.push(`Runner: ${JSON.stringify(agent.runner)}`);
602
680
  lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
603
681
  lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
604
682
  if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
@@ -687,7 +765,7 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
687
765
  const proactiveSuggestions = buildProactiveSkillSubagentRecommendationLines({
688
766
  agents,
689
767
  chains,
690
- config: ctx.config?.proactiveSkillSubagents,
768
+ ...(ctx.config?.proactiveSkillSubagents !== undefined ? { config: ctx.config.proactiveSkillSubagents } : {}),
691
769
  discoverAvailableSkills: () => discoverAvailableSkills(ctx.cwd),
692
770
  });
693
771
  const lines = [
@@ -857,7 +935,15 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
857
935
  if (isChain) {
858
936
  const parsed = parseStepList(cfg.steps);
859
937
  if (parsed.error) return result(parsed.error, true);
860
- const chain: ChainConfig = { name: runtimeName, localName: name, packageName: parsedPackage.packageName, description: cfg.description.trim(), source: scope, filePath: targetPath, steps: parsed.steps! };
938
+ const chain: ChainConfig = {
939
+ name: runtimeName,
940
+ localName: name,
941
+ ...(parsedPackage.packageName !== undefined ? { packageName: parsedPackage.packageName } : {}),
942
+ description: cfg.description.trim(),
943
+ source: scope,
944
+ filePath: targetPath,
945
+ steps: parsed.steps!,
946
+ };
861
947
  fs.writeFileSync(targetPath, serializeChain(chain), "utf-8");
862
948
  const missing = unknownChainAgents(ctx.cwd, chain.steps);
863
949
  if (missing.length) warnings.push(`Warning: chain steps reference unknown agents: ${missing.join(", ")}.`);
@@ -867,7 +953,7 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
867
953
  const agent: AgentConfig = {
868
954
  name: runtimeName,
869
955
  localName: name,
870
- packageName: parsedPackage.packageName,
956
+ ...(parsedPackage.packageName !== undefined ? { packageName: parsedPackage.packageName } : {}),
871
957
  description: cfg.description.trim(),
872
958
  source: scope,
873
959
  filePath: targetPath,
@@ -898,9 +984,10 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
898
984
  const warnings: string[] = [];
899
985
  if (params.agent) {
900
986
  const scopeHint = asDisambiguationScope(params.agentScope);
901
- const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
987
+ const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
902
988
  if ("content" in targetOrError) return targetOrError;
903
989
  const target = targetOrError;
990
+ if (target.source !== "user" && target.source !== "project") return result(`Cannot update ${target.source} agent '${target.name}'. Eject it to user or project scope first.`, true);
904
991
  const updated = editableAgentConfig(target);
905
992
  const oldName = target.name;
906
993
  if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
@@ -920,7 +1007,8 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
920
1007
  if (applyError) return result(applyError, true);
921
1008
  const preserveFrontmatterFields = preservedAgentFrontmatterFields(target, cfg);
922
1009
  updated.localName = newLocalName;
923
- updated.packageName = newPackageName;
1010
+ if (newPackageName !== undefined) updated.packageName = newPackageName;
1011
+ else delete updated.packageName;
924
1012
  updated.name = buildRuntimeName(newLocalName, newPackageName);
925
1013
  if (hasKey(cfg, "description")) updated.description = (cfg.description as string).trim();
926
1014
  if (hasKey(cfg, "model")) {
@@ -951,9 +1039,10 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
951
1039
  return result([headline, ...warnings].join("\n"));
952
1040
  }
953
1041
  const scopeHint = asDisambiguationScope(params.agentScope);
954
- const targetOrError = resolveTarget("chain", params.chainName!, findChains(params.chainName!, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
1042
+ const targetOrError = resolveTarget("chain", params.chainName!, findChains(params.chainName!, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
955
1043
  if ("content" in targetOrError) return targetOrError;
956
1044
  const target = targetOrError;
1045
+ if (target.source !== "user" && target.source !== "project") return result(`Cannot update ${target.source} chain '${target.name}'. Eject it to user or project scope first.`, true);
957
1046
  const updated: ChainConfig = { ...target, steps: [...target.steps] };
958
1047
  const oldName = target.name;
959
1048
  if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
@@ -976,7 +1065,8 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
976
1065
  parsedSteps = parsed.steps!;
977
1066
  }
978
1067
  updated.localName = newLocalName;
979
- updated.packageName = newPackageName;
1068
+ if (newPackageName !== undefined) updated.packageName = newPackageName;
1069
+ else delete updated.packageName;
980
1070
  updated.name = buildRuntimeName(newLocalName, newPackageName);
981
1071
  if (hasKey(cfg, "description")) updated.description = (cfg.description as string).trim();
982
1072
  if (parsedSteps) {
@@ -1002,7 +1092,7 @@ function handleDelete(params: ManagementParams, ctx: ManagementContext): AgentTo
1002
1092
  if (params.agent && params.chainName) return result("Specify either 'agent' or 'chainName', not both.", true);
1003
1093
  const scopeHint = asDisambiguationScope(params.agentScope);
1004
1094
  if (params.agent) {
1005
- const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
1095
+ const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
1006
1096
  if ("content" in targetOrError) return targetOrError;
1007
1097
  const target = targetOrError;
1008
1098
  fs.unlinkSync(target.filePath);
@@ -1011,7 +1101,7 @@ function handleDelete(params: ManagementParams, ctx: ManagementContext): AgentTo
1011
1101
  if (refs.length) lines.push(`Warning: chains reference deleted agent '${target.name}': ${refs.join(", ")}.`);
1012
1102
  return result(lines.join("\n"));
1013
1103
  }
1014
- const targetOrError = resolveTarget("chain", params.chainName!, findChains(params.chainName!, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
1104
+ const targetOrError = resolveTarget("chain", params.chainName!, findChains(params.chainName!, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
1015
1105
  if ("content" in targetOrError) return targetOrError;
1016
1106
  const target = targetOrError;
1017
1107
  fs.unlinkSync(target.filePath);
@@ -1,3 +1,4 @@
1
+ import { stringify as stringifyYaml } from "yaml";
1
2
  import type { AgentConfig } from "./agents.ts";
2
3
  import { frontmatterNameForConfig } from "./identity.ts";
3
4
 
@@ -32,7 +33,10 @@ export const KNOWN_FIELDS = new Set([
32
33
  "maxSubagentDepth",
33
34
  "completionGuard",
34
35
  "toolBudget",
36
+ "permission",
37
+ "permissions",
35
38
  "memory",
39
+ "runner",
36
40
  ]);
37
41
 
38
42
  function joinComma(values: string[] | undefined): string | undefined {
@@ -72,6 +76,14 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
72
76
  if (!preservingExistingFrontmatter || preserve("inheritProjectContext")) lines.push(`inheritProjectContext: ${config.inheritProjectContext ? "true" : "false"}`);
73
77
  if (!preservingExistingFrontmatter || preserve("inheritSkills")) lines.push(`inheritSkills: ${config.inheritSkills ? "true" : "false"}`);
74
78
  if (config.defaultContext || preserve("defaultContext")) lines.push(`defaultContext: ${config.defaultContext ?? ""}`);
79
+ if (config.runner || preserve("runner")) {
80
+ if (config.runner) {
81
+ lines.push("runner:");
82
+ for (const line of stringifyYaml(config.runner).trimEnd().split("\n")) lines.push(` ${line}`);
83
+ } else {
84
+ lines.push("runner:");
85
+ }
86
+ }
75
87
  if (config.defaultAsync !== undefined || preserve("async")) lines.push(`async: ${config.defaultAsync === undefined ? "" : config.defaultAsync ? "true" : "false"}`);
76
88
  if (config.defaultTimeoutMs !== undefined || preserve("timeoutMs")) lines.push(`timeoutMs: ${config.defaultTimeoutMs ?? ""}`);
77
89
  if (config.defaultTurnBudget || preserve("turnBudget")) lines.push(`turnBudget: ${config.defaultTurnBudget ? JSON.stringify(config.defaultTurnBudget) : ""}`);
@@ -115,6 +127,13 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
115
127
  if (config.toolBudget || preserve("toolBudget")) {
116
128
  lines.push(`toolBudget: ${config.toolBudget ? JSON.stringify(config.toolBudget) : ""}`);
117
129
  }
130
+ if (config.permissions || preserve("permission", "permissions")) {
131
+ const key = preserve("permission") && !preserve("permissions") ? "permission" : "permissions";
132
+ lines.push(`${key}:`);
133
+ if (config.permissions) {
134
+ for (const line of stringifyYaml(config.permissions).trimEnd().split("\n")) lines.push(` ${line}`);
135
+ }
136
+ }
118
137
 
119
138
  if (config.memory) {
120
139
  lines.push("memory:");