pi-subagents 0.29.0 → 0.31.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 (48) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +125 -19
  3. package/agents/context-builder.md +3 -3
  4. package/agents/planner.md +1 -1
  5. package/agents/researcher.md +1 -1
  6. package/agents/scout.md +1 -1
  7. package/package.json +7 -7
  8. package/skills/pi-subagents/SKILL.md +30 -0
  9. package/src/agents/agent-management.ts +189 -8
  10. package/src/agents/agent-serializer.ts +35 -12
  11. package/src/agents/agents.ts +243 -24
  12. package/src/agents/frontmatter.ts +66 -2
  13. package/src/agents/proactive-skills.ts +191 -0
  14. package/src/agents/skills.ts +117 -20
  15. package/src/extension/doctor.ts +20 -0
  16. package/src/extension/fanout-child.ts +2 -1
  17. package/src/extension/index.ts +50 -5
  18. package/src/extension/schemas.ts +40 -79
  19. package/src/intercom/intercom-bridge.ts +2 -3
  20. package/src/runs/background/async-execution.ts +180 -67
  21. package/src/runs/background/async-job-tracker.ts +56 -11
  22. package/src/runs/background/async-resume.ts +53 -5
  23. package/src/runs/background/async-status.ts +4 -1
  24. package/src/runs/background/chain-append.ts +282 -0
  25. package/src/runs/background/chain-root-attachment.ts +161 -0
  26. package/src/runs/background/result-watcher.ts +11 -2
  27. package/src/runs/background/run-status.ts +1 -0
  28. package/src/runs/background/stale-run-reconciler.ts +9 -4
  29. package/src/runs/background/subagent-runner.ts +158 -11
  30. package/src/runs/foreground/chain-execution.ts +26 -2
  31. package/src/runs/foreground/execution.ts +114 -8
  32. package/src/runs/foreground/subagent-executor.ts +611 -87
  33. package/src/runs/shared/acceptance.ts +285 -34
  34. package/src/runs/shared/chain-outputs.ts +23 -8
  35. package/src/runs/shared/completion-guard.ts +1 -1
  36. package/src/runs/shared/dynamic-fanout.ts +5 -3
  37. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
  38. package/src/runs/shared/parallel-utils.ts +13 -1
  39. package/src/runs/shared/pi-args.ts +12 -3
  40. package/src/runs/shared/single-output.ts +15 -1
  41. package/src/runs/shared/subagent-control.ts +8 -11
  42. package/src/shared/settings.ts +1 -0
  43. package/src/shared/types.ts +17 -2
  44. package/src/shared/utils.ts +19 -1
  45. package/src/slash/prompt-template-bridge.ts +26 -3
  46. package/src/slash/slash-bridge.ts +3 -1
  47. package/src/slash/slash-commands.ts +34 -4
  48. package/src/tui/render.ts +265 -13
@@ -8,6 +8,7 @@ import {
8
8
  type AgentSource,
9
9
  type ChainConfig,
10
10
  type ChainStepConfig,
11
+ BUILTIN_AGENT_NAMES,
11
12
  defaultInheritProjectContext,
12
13
  defaultInheritSkills,
13
14
  defaultSystemPromptMode,
@@ -19,11 +20,18 @@ import {
19
20
  import { serializeAgent } from "./agent-serializer.ts";
20
21
  import { serializeChain, serializeJsonChain } from "./chain-serializer.ts";
21
22
  import { discoverAvailableSkills } from "./skills.ts";
22
- import type { Details } from "../shared/types.ts";
23
-
24
- type ManagementAction = "list" | "get" | "create" | "update" | "delete";
23
+ import {
24
+ buildProactiveSkillSubagentRecommendationLines,
25
+ } from "./proactive-skills.ts";
26
+ import { parseFrontmatter } from "./frontmatter.ts";
27
+ import { toModelInfo } from "../shared/model-info.ts";
28
+ import { resolveSubagentModelOverride, type ParentModel } from "../runs/shared/model-fallback.ts";
29
+ import type { Details, ExtensionConfig } from "../shared/types.ts";
30
+ import { getProjectConfigDir } from "../shared/utils.ts";
31
+
32
+ type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete";
25
33
  type ManagementScope = "user" | "project";
26
- type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry">;
34
+ type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig };
27
35
 
28
36
  interface ManagementParams {
29
37
  action?: string;
@@ -163,6 +171,84 @@ function skillsWarning(cwd: string, skills: string[] | undefined): string | unde
163
171
  return missing.length ? `Warning: skills not found: ${missing.join(", ")}.` : undefined;
164
172
  }
165
173
 
174
+ function editableAgentConfig(agent: AgentConfig): AgentConfig {
175
+ const base = agent.override?.base;
176
+ if (!base) return { ...agent };
177
+
178
+ return {
179
+ ...agent,
180
+ model: base.model,
181
+ fallbackModels: base.fallbackModels ? [...base.fallbackModels] : undefined,
182
+ thinking: base.thinking,
183
+ systemPromptMode: base.systemPromptMode,
184
+ inheritProjectContext: base.inheritProjectContext,
185
+ inheritSkills: base.inheritSkills,
186
+ defaultContext: base.defaultContext,
187
+ disabled: base.disabled,
188
+ systemPrompt: base.systemPrompt,
189
+ skills: base.skills ? [...base.skills] : undefined,
190
+ tools: base.tools ? [...base.tools] : undefined,
191
+ mcpDirectTools: base.mcpDirectTools ? [...base.mcpDirectTools] : undefined,
192
+ subagentOnlyExtensions: base.subagentOnlyExtensions ? [...base.subagentOnlyExtensions] : undefined,
193
+ completionGuard: base.completionGuard,
194
+ override: undefined,
195
+ };
196
+ }
197
+
198
+ function readAgentFrontmatterFields(filePath: string): Set<string> {
199
+ try {
200
+ const { frontmatter } = parseFrontmatter(fs.readFileSync(filePath, "utf-8"));
201
+ return new Set(Object.keys(frontmatter));
202
+ } catch {
203
+ return new Set();
204
+ }
205
+ }
206
+
207
+ function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<string, unknown>): Set<string> {
208
+ const fields = readAgentFrontmatterFields(agent.filePath);
209
+ const changed = (...names: string[]) => {
210
+ for (const name of names) fields.delete(name);
211
+ };
212
+
213
+ if (hasKey(cfg, "name")) changed("name");
214
+ if (hasKey(cfg, "package")) changed("package");
215
+ if (hasKey(cfg, "description")) changed("description");
216
+ if (hasKey(cfg, "systemPrompt")) changed("systemPrompt");
217
+ if (hasKey(cfg, "model")) changed("model");
218
+ if (hasKey(cfg, "fallbackModels")) changed("fallbackModels");
219
+ if (hasKey(cfg, "tools")) changed("tools");
220
+ if (hasKey(cfg, "skills")) changed("skill", "skills");
221
+ if (hasKey(cfg, "extensions")) changed("extensions");
222
+ if (hasKey(cfg, "subagentOnlyExtensions")) changed("subagentOnlyExtensions");
223
+ if (hasKey(cfg, "thinking")) {
224
+ changed("thinking");
225
+ if (cfg.thinking === "off") fields.add("thinking");
226
+ }
227
+ if (hasKey(cfg, "systemPromptMode")) {
228
+ changed("systemPromptMode");
229
+ fields.add("systemPromptMode");
230
+ }
231
+ if (hasKey(cfg, "inheritProjectContext")) {
232
+ changed("inheritProjectContext");
233
+ fields.add("inheritProjectContext");
234
+ }
235
+ if (hasKey(cfg, "inheritSkills")) {
236
+ changed("inheritSkills");
237
+ fields.add("inheritSkills");
238
+ }
239
+ if (hasKey(cfg, "defaultContext")) changed("defaultContext");
240
+ if (hasKey(cfg, "output")) changed("output");
241
+ if (hasKey(cfg, "reads")) changed("defaultReads");
242
+ if (hasKey(cfg, "progress")) changed("defaultProgress");
243
+ if (hasKey(cfg, "maxSubagentDepth")) changed("maxSubagentDepth");
244
+ if (hasKey(cfg, "completionGuard")) {
245
+ changed("completionGuard");
246
+ if (cfg.completionGuard === true) fields.add("completionGuard");
247
+ }
248
+
249
+ return fields;
250
+ }
251
+
166
252
  function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: string } {
167
253
  if (!Array.isArray(raw)) return { error: "config.steps must be an array." };
168
254
  if (raw.length === 0) return { error: "config.steps must include at least one step." };
@@ -273,6 +359,12 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
273
359
  else if (typeof cfg.extensions === "string") target.extensions = parseCsv(cfg.extensions);
274
360
  else return "config.extensions must be a comma-separated string, empty string, or false when provided.";
275
361
  }
362
+ if (hasKey(cfg, "subagentOnlyExtensions")) {
363
+ if (cfg.subagentOnlyExtensions === false) target.subagentOnlyExtensions = undefined;
364
+ else if (cfg.subagentOnlyExtensions === "") target.subagentOnlyExtensions = [];
365
+ else if (typeof cfg.subagentOnlyExtensions === "string") target.subagentOnlyExtensions = parseCsv(cfg.subagentOnlyExtensions);
366
+ else return "config.subagentOnlyExtensions must be a comma-separated string, empty string, or false when provided.";
367
+ }
276
368
  if (hasKey(cfg, "thinking")) {
277
369
  if (cfg.thinking === false || cfg.thinking === "") target.thinking = undefined;
278
370
  else if (typeof cfg.thinking === "string") target.thinking = cfg.thinking.trim() || undefined;
@@ -385,6 +477,7 @@ function formatAgentDetail(agent: AgentConfig): string {
385
477
  if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
386
478
  if (agent.source === "builtin") lines.push(`Disabled: ${agent.disabled ? "true" : "false"}`);
387
479
  if (agent.extensions !== undefined) lines.push(`Extensions: ${agent.extensions.length ? agent.extensions.join(", ") : "(none)"}`);
480
+ if (agent.subagentOnlyExtensions !== undefined) lines.push(`Subagent-only extensions: ${agent.subagentOnlyExtensions.length ? agent.subagentOnlyExtensions.join(", ") : "(none)"}`);
388
481
  if (agent.thinking) lines.push(`Thinking: ${agent.thinking}`);
389
482
  if (agent.output) lines.push(`Output: ${agent.output}`);
390
483
  if (agent.defaultReads?.length) lines.push(`Reads: ${agent.defaultReads.join(", ")}`);
@@ -450,6 +543,12 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
450
543
  const agents = scopedAgents.filter((a) => !a.disabled);
451
544
  const chains = d.chains.filter((c) => scope === "both" || c.source === "package" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
452
545
  const diagnostics = d.chainDiagnostics.filter((entry) => scope === "both" || entry.source === scope);
546
+ const proactiveSuggestions = buildProactiveSkillSubagentRecommendationLines({
547
+ agents,
548
+ chains,
549
+ config: ctx.config?.proactiveSkillSubagents,
550
+ discoverAvailableSkills: () => discoverAvailableSkills(ctx.cwd),
551
+ });
453
552
  const lines = [
454
553
  "Executable agents:",
455
554
  ...(agents.length
@@ -458,11 +557,90 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
458
557
  "",
459
558
  "Chains:",
460
559
  ...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"]),
560
+ ...(proactiveSuggestions.length ? ["", ...proactiveSuggestions] : []),
461
561
  ...(diagnostics.length ? ["", "Chain diagnostics:", ...diagnostics.map((entry) => `- ${entry.filePath}: ${entry.error}`)] : []),
462
562
  ];
463
563
  return result(lines.join("\n"));
464
564
  }
465
565
 
566
+ function formatModelSource(agent: AgentConfig, currentModel: ParentModel | undefined): string {
567
+ if (agent.override && agent.model !== agent.override.base.model) {
568
+ return `${agent.override.scope} override`;
569
+ }
570
+ if (agent.model) return "builtin agent config";
571
+ if (currentModel) return "inherits current session model";
572
+ return "inherit requested, but no current session model is available";
573
+ }
574
+
575
+ function handleModels(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
576
+ const requestedAgent = params.agent?.trim();
577
+ if (requestedAgent && !(BUILTIN_AGENT_NAMES as readonly string[]).includes(requestedAgent)) {
578
+ return result(`Builtin agent '${requestedAgent}' not found. Available: ${BUILTIN_AGENT_NAMES.join(", ")}.`, true);
579
+ }
580
+
581
+ const discovered = discoverAgentsAll(ctx.cwd);
582
+ const builtinByName = new Map(discovered.builtin.map((agent) => [agent.name, agent]));
583
+ const availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);
584
+ const currentModel = ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
585
+ const preferredProvider = ctx.model?.provider;
586
+ const names = requestedAgent ? [requestedAgent] : [...BUILTIN_AGENT_NAMES];
587
+
588
+ if (requestedAgent) {
589
+ const agent = builtinByName.get(requestedAgent);
590
+ if (!agent) return result(`Builtin agent '${requestedAgent}' not found.`, true);
591
+ const resolvedModel = resolveSubagentModelOverride(agent.model, currentModel, availableModels, preferredProvider);
592
+ const lines = [
593
+ "Builtin subagent model",
594
+ "",
595
+ `Agent: ${requestedAgent}`,
596
+ "Effective model:",
597
+ ` ${resolvedModel ?? "(unresolved)"}`,
598
+ `Source: ${formatModelSource(agent, currentModel)}`,
599
+ ];
600
+ if (agent.override) {
601
+ lines.push("Override file:");
602
+ lines.push(` ${agent.override.path}`);
603
+ }
604
+ if (agent.model && resolvedModel && agent.model !== resolvedModel) {
605
+ lines.push("Requested model setting:");
606
+ lines.push(` ${agent.model}`);
607
+ }
608
+ if (agent.disabled) lines.push("Disabled: true");
609
+ lines.push("Current session model:");
610
+ lines.push(` ${currentModel ? `${currentModel.provider}/${currentModel.id}` : "(unavailable)"}`);
611
+ return result(lines.join("\n"));
612
+ }
613
+
614
+ const lines = [
615
+ "Builtin subagent models",
616
+ "",
617
+ "Current session model:",
618
+ ` ${currentModel ? `${currentModel.provider}/${currentModel.id}` : "(unavailable)"}`,
619
+ "",
620
+ ];
621
+
622
+ for (const name of names) {
623
+ const agent = builtinByName.get(name);
624
+ if (!agent) {
625
+ lines.push(name);
626
+ lines.push(" model:");
627
+ lines.push(" (builtin definition not found)");
628
+ lines.push(" source: missing");
629
+ lines.push("");
630
+ continue;
631
+ }
632
+ const resolvedModel = resolveSubagentModelOverride(agent.model, currentModel, availableModels, preferredProvider);
633
+ const source = `${formatModelSource(agent, currentModel)}${agent.disabled ? "; disabled" : ""}`;
634
+ lines.push(name);
635
+ lines.push(" model:");
636
+ lines.push(` ${resolvedModel ?? "(unresolved)"}`);
637
+ lines.push(` source: ${source}`);
638
+ lines.push("");
639
+ }
640
+
641
+ return result(lines.join("\n"));
642
+ }
643
+
466
644
  function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
467
645
  if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for get.", true);
468
646
  const hasBoth = Boolean(params.agent && params.chainName);
@@ -510,9 +688,10 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
510
688
  const scope = scopeRaw as ManagementScope;
511
689
  const isChain = hasKey(cfg, "steps");
512
690
  const d = discoverAgentsAll(ctx.cwd);
691
+ const projectConfigDir = getProjectConfigDir(ctx.cwd);
513
692
  const targetDir = isChain
514
- ? scope === "user" ? d.userChainDir : d.projectChainDir ?? path.join(ctx.cwd, ".pi", "chains")
515
- : scope === "user" ? d.userDir : d.projectDir ?? path.join(ctx.cwd, ".pi", "agents");
693
+ ? scope === "user" ? d.userChainDir : d.projectChainDir ?? path.join(projectConfigDir, "chains")
694
+ : scope === "user" ? d.userDir : d.projectDir ?? path.join(projectConfigDir, "agents");
516
695
  fs.mkdirSync(targetDir, { recursive: true });
517
696
  if (nameExistsInScope(ctx.cwd, scope, runtimeName)) return result(`Name '${runtimeName}' already exists in ${scope} scope. Use update instead.`, true);
518
697
  const targetPath = path.join(targetDir, isChain ? `${runtimeName}.chain.md` : `${runtimeName}.md`);
@@ -566,7 +745,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
566
745
  const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
567
746
  if ("content" in targetOrError) return targetOrError;
568
747
  const target = targetOrError;
569
- const updated: AgentConfig = { ...target };
748
+ const updated = editableAgentConfig(target);
570
749
  const oldName = target.name;
571
750
  if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
572
751
  if (hasKey(cfg, "description") && (typeof cfg.description !== "string" || !cfg.description.trim())) return result("config.description must be a non-empty string when provided.", true);
@@ -583,6 +762,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
583
762
  }
584
763
  const applyError = applyAgentConfig(updated, cfg);
585
764
  if (applyError) return result(applyError, true);
765
+ const preserveFrontmatterFields = preservedAgentFrontmatterFields(target, cfg);
586
766
  updated.localName = newLocalName;
587
767
  updated.packageName = newPackageName;
588
768
  updated.name = buildRuntimeName(newLocalName, newPackageName);
@@ -604,7 +784,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
604
784
  if (renamed.error) return result(renamed.error, true);
605
785
  updated.filePath = renamed.filePath!;
606
786
  }
607
- fs.writeFileSync(updated.filePath, serializeAgent(updated), "utf-8");
787
+ fs.writeFileSync(updated.filePath, serializeAgent(updated, { preserveFrontmatterFields }), "utf-8");
608
788
  if (updated.name !== oldName) {
609
789
  const refs = discoverAgentsAll(ctx.cwd).chains.filter((c) => c.steps.some((s) => s.agent === oldName)).map((c) => `${c.name} (${c.source})`);
610
790
  if (refs.length) warnings.push(`Warning: chains still reference '${oldName}': ${refs.join(", ")}.`);
@@ -686,6 +866,7 @@ export function handleManagementAction(action: string, params: ManagementParams,
686
866
  switch (action as ManagementAction) {
687
867
  case "list": return handleList(params, ctx);
688
868
  case "get": return handleGet(params, ctx);
869
+ case "models": return handleModels(params, ctx);
689
870
  case "create": return handleCreate(params, ctx);
690
871
  case "update": return handleUpdate(params, ctx);
691
872
  case "delete": return handleDelete(params, ctx);
@@ -16,6 +16,7 @@ export const KNOWN_FIELDS = new Set([
16
16
  "skill",
17
17
  "skills",
18
18
  "extensions",
19
+ "subagentOnlyExtensions",
19
20
  "output",
20
21
  "defaultReads",
21
22
  "defaultProgress",
@@ -29,8 +30,14 @@ function joinComma(values: string[] | undefined): string | undefined {
29
30
  return values.join(", ");
30
31
  }
31
32
 
32
- export function serializeAgent(config: AgentConfig): string {
33
+ interface SerializeAgentOptions {
34
+ preserveFrontmatterFields?: ReadonlySet<string>;
35
+ }
36
+
37
+ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptions = {}): string {
33
38
  const lines: string[] = [];
39
+ const preserve = (...fields: string[]) => fields.some((field) => options.preserveFrontmatterFields?.has(field));
40
+ const preservingExistingFrontmatter = options.preserveFrontmatterFields !== undefined;
34
41
  lines.push("---");
35
42
  lines.push(`name: ${frontmatterNameForConfig(config)}`);
36
43
  if (config.packageName) lines.push(`package: ${config.packageName}`);
@@ -41,24 +48,30 @@ export function serializeAgent(config: AgentConfig): string {
41
48
  ...(config.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`),
42
49
  ];
43
50
  const toolsValue = joinComma(tools);
44
- if (toolsValue) lines.push(`tools: ${toolsValue}`);
51
+ if (toolsValue || preserve("tools")) lines.push(`tools: ${toolsValue ?? ""}`);
45
52
 
46
- if (config.model) lines.push(`model: ${config.model}`);
53
+ if (config.model || preserve("model")) lines.push(`model: ${config.model ?? ""}`);
47
54
  const fallbackModelsValue = joinComma(config.fallbackModels);
48
- if (fallbackModelsValue) lines.push(`fallbackModels: ${fallbackModelsValue}`);
49
- if (config.thinking && config.thinking !== "off") lines.push(`thinking: ${config.thinking}`);
50
- lines.push(`systemPromptMode: ${config.systemPromptMode}`);
51
- lines.push(`inheritProjectContext: ${config.inheritProjectContext ? "true" : "false"}`);
52
- lines.push(`inheritSkills: ${config.inheritSkills ? "true" : "false"}`);
53
- if (config.defaultContext) lines.push(`defaultContext: ${config.defaultContext}`);
55
+ if (fallbackModelsValue || preserve("fallbackModels")) lines.push(`fallbackModels: ${fallbackModelsValue ?? ""}`);
56
+ if ((config.thinking && (config.thinking !== "off" || preserve("thinking"))) || (!config.thinking && preserve("thinking"))) {
57
+ lines.push(`thinking: ${config.thinking ?? ""}`);
58
+ }
59
+ if (!preservingExistingFrontmatter || preserve("systemPromptMode")) lines.push(`systemPromptMode: ${config.systemPromptMode}`);
60
+ if (!preservingExistingFrontmatter || preserve("inheritProjectContext")) lines.push(`inheritProjectContext: ${config.inheritProjectContext ? "true" : "false"}`);
61
+ if (!preservingExistingFrontmatter || preserve("inheritSkills")) lines.push(`inheritSkills: ${config.inheritSkills ? "true" : "false"}`);
62
+ if (config.defaultContext || preserve("defaultContext")) lines.push(`defaultContext: ${config.defaultContext ?? ""}`);
54
63
 
55
64
  const skillsValue = joinComma(config.skills);
56
- if (skillsValue) lines.push(`skills: ${skillsValue}`);
65
+ if (skillsValue || preserve("skill", "skills")) lines.push(`skills: ${skillsValue ?? ""}`);
57
66
 
58
67
  if (config.extensions !== undefined) {
59
68
  const extensionsValue = joinComma(config.extensions);
60
69
  lines.push(`extensions: ${extensionsValue ?? ""}`);
61
70
  }
71
+ if (config.subagentOnlyExtensions !== undefined || preserve("subagentOnlyExtensions")) {
72
+ const subagentOnlyExtensionsValue = joinComma(config.subagentOnlyExtensions);
73
+ lines.push(`subagentOnlyExtensions: ${subagentOnlyExtensionsValue ?? ""}`);
74
+ }
62
75
 
63
76
  if (config.output) lines.push(`output: ${config.output}`);
64
77
 
@@ -71,12 +84,22 @@ export function serializeAgent(config: AgentConfig): string {
71
84
  if (typeof maxSubagentDepth === "number" && Number.isInteger(maxSubagentDepth) && maxSubagentDepth >= 0) {
72
85
  lines.push(`maxSubagentDepth: ${maxSubagentDepth}`);
73
86
  }
74
- if (config.completionGuard === false) lines.push("completionGuard: false");
87
+ if (config.completionGuard === false || preserve("completionGuard")) {
88
+ lines.push(`completionGuard: ${config.completionGuard === undefined ? "" : config.completionGuard ? "true" : "false"}`);
89
+ }
75
90
 
76
91
  if (config.extraFields) {
77
92
  for (const [key, value] of Object.entries(config.extraFields)) {
78
93
  if (KNOWN_FIELDS.has(key)) continue;
79
- lines.push(`${key}: ${value}`);
94
+ if (typeof value === "string" && value.includes("\n")) {
95
+ // Multi-line block value (e.g. permission: nested YAML)
96
+ lines.push(`${key}:`);
97
+ for (const blockLine of value.split("\n")) {
98
+ lines.push(` ${blockLine}`);
99
+ }
100
+ } else {
101
+ lines.push(`${key}: ${value}`);
102
+ }
80
103
  }
81
104
  }
82
105