pi-subagents 0.40.0 → 0.41.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 (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. package/src/extension/chain-validation.ts +0 -181
@@ -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.
@@ -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
 
@@ -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:");