pi-subagents 0.30.0 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +189 -18
- package/agents/context-builder.md +3 -3
- package/agents/planner.md +1 -1
- package/agents/researcher.md +1 -1
- package/agents/scout.md +1 -1
- package/package.json +7 -7
- package/skills/pi-subagents/SKILL.md +5 -0
- package/src/agents/agent-management.ts +170 -6
- package/src/agents/agent-serializer.ts +31 -13
- package/src/agents/agents.ts +207 -23
- package/src/agents/frontmatter.ts +66 -2
- package/src/agents/skills.ts +117 -20
- package/src/extension/doctor.ts +20 -0
- package/src/extension/fanout-child.ts +1 -0
- package/src/extension/index.ts +58 -4
- package/src/extension/schemas.ts +10 -76
- package/src/intercom/intercom-bridge.ts +27 -4
- package/src/profiles/profiles.ts +637 -0
- package/src/runs/background/async-execution.ts +14 -4
- package/src/runs/background/async-job-tracker.ts +56 -11
- package/src/runs/background/async-resume.ts +11 -13
- package/src/runs/background/control-channel.ts +177 -0
- package/src/runs/background/result-watcher.ts +11 -2
- package/src/runs/background/stale-run-reconciler.ts +9 -4
- package/src/runs/background/subagent-runner.ts +86 -3
- package/src/runs/foreground/chain-execution.ts +26 -2
- package/src/runs/foreground/execution.ts +113 -8
- package/src/runs/foreground/subagent-executor.ts +356 -86
- package/src/runs/shared/acceptance.ts +285 -34
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/dynamic-fanout.ts +4 -2
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +6 -1
- package/src/runs/shared/pi-args.ts +9 -1
- package/src/runs/shared/single-output.ts +15 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +1 -0
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +9 -2
- package/src/shared/utils.ts +19 -1
- package/src/slash/prompt-template-bridge.ts +26 -3
- package/src/slash/slash-commands.ts +642 -43
- 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,
|
|
@@ -22,11 +23,15 @@ import { discoverAvailableSkills } from "./skills.ts";
|
|
|
22
23
|
import {
|
|
23
24
|
buildProactiveSkillSubagentRecommendationLines,
|
|
24
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";
|
|
25
29
|
import type { Details, ExtensionConfig } from "../shared/types.ts";
|
|
30
|
+
import { getProjectConfigDir } from "../shared/utils.ts";
|
|
26
31
|
|
|
27
|
-
type ManagementAction = "list" | "get" | "create" | "update" | "delete";
|
|
32
|
+
type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete";
|
|
28
33
|
type ManagementScope = "user" | "project";
|
|
29
|
-
type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { config?: ExtensionConfig };
|
|
34
|
+
type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig };
|
|
30
35
|
|
|
31
36
|
interface ManagementParams {
|
|
32
37
|
action?: string;
|
|
@@ -166,6 +171,84 @@ function skillsWarning(cwd: string, skills: string[] | undefined): string | unde
|
|
|
166
171
|
return missing.length ? `Warning: skills not found: ${missing.join(", ")}.` : undefined;
|
|
167
172
|
}
|
|
168
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
|
+
|
|
169
252
|
function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: string } {
|
|
170
253
|
if (!Array.isArray(raw)) return { error: "config.steps must be an array." };
|
|
171
254
|
if (raw.length === 0) return { error: "config.steps must include at least one step." };
|
|
@@ -480,6 +563,84 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
|
|
|
480
563
|
return result(lines.join("\n"));
|
|
481
564
|
}
|
|
482
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
|
+
|
|
483
644
|
function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
484
645
|
if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for get.", true);
|
|
485
646
|
const hasBoth = Boolean(params.agent && params.chainName);
|
|
@@ -527,9 +688,10 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
|
|
|
527
688
|
const scope = scopeRaw as ManagementScope;
|
|
528
689
|
const isChain = hasKey(cfg, "steps");
|
|
529
690
|
const d = discoverAgentsAll(ctx.cwd);
|
|
691
|
+
const projectConfigDir = getProjectConfigDir(ctx.cwd);
|
|
530
692
|
const targetDir = isChain
|
|
531
|
-
? scope === "user" ? d.userChainDir : d.projectChainDir ?? path.join(
|
|
532
|
-
: scope === "user" ? d.userDir : d.projectDir ?? path.join(
|
|
693
|
+
? scope === "user" ? d.userChainDir : d.projectChainDir ?? path.join(projectConfigDir, "chains")
|
|
694
|
+
: scope === "user" ? d.userDir : d.projectDir ?? path.join(projectConfigDir, "agents");
|
|
533
695
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
534
696
|
if (nameExistsInScope(ctx.cwd, scope, runtimeName)) return result(`Name '${runtimeName}' already exists in ${scope} scope. Use update instead.`, true);
|
|
535
697
|
const targetPath = path.join(targetDir, isChain ? `${runtimeName}.chain.md` : `${runtimeName}.md`);
|
|
@@ -583,7 +745,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
|
|
|
583
745
|
const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
|
|
584
746
|
if ("content" in targetOrError) return targetOrError;
|
|
585
747
|
const target = targetOrError;
|
|
586
|
-
const updated
|
|
748
|
+
const updated = editableAgentConfig(target);
|
|
587
749
|
const oldName = target.name;
|
|
588
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);
|
|
589
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);
|
|
@@ -600,6 +762,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
|
|
|
600
762
|
}
|
|
601
763
|
const applyError = applyAgentConfig(updated, cfg);
|
|
602
764
|
if (applyError) return result(applyError, true);
|
|
765
|
+
const preserveFrontmatterFields = preservedAgentFrontmatterFields(target, cfg);
|
|
603
766
|
updated.localName = newLocalName;
|
|
604
767
|
updated.packageName = newPackageName;
|
|
605
768
|
updated.name = buildRuntimeName(newLocalName, newPackageName);
|
|
@@ -621,7 +784,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
|
|
|
621
784
|
if (renamed.error) return result(renamed.error, true);
|
|
622
785
|
updated.filePath = renamed.filePath!;
|
|
623
786
|
}
|
|
624
|
-
fs.writeFileSync(updated.filePath, serializeAgent(updated), "utf-8");
|
|
787
|
+
fs.writeFileSync(updated.filePath, serializeAgent(updated, { preserveFrontmatterFields }), "utf-8");
|
|
625
788
|
if (updated.name !== oldName) {
|
|
626
789
|
const refs = discoverAgentsAll(ctx.cwd).chains.filter((c) => c.steps.some((s) => s.agent === oldName)).map((c) => `${c.name} (${c.source})`);
|
|
627
790
|
if (refs.length) warnings.push(`Warning: chains still reference '${oldName}': ${refs.join(", ")}.`);
|
|
@@ -703,6 +866,7 @@ export function handleManagementAction(action: string, params: ManagementParams,
|
|
|
703
866
|
switch (action as ManagementAction) {
|
|
704
867
|
case "list": return handleList(params, ctx);
|
|
705
868
|
case "get": return handleGet(params, ctx);
|
|
869
|
+
case "models": return handleModels(params, ctx);
|
|
706
870
|
case "create": return handleCreate(params, ctx);
|
|
707
871
|
case "update": return handleUpdate(params, ctx);
|
|
708
872
|
case "delete": return handleDelete(params, ctx);
|
|
@@ -30,8 +30,14 @@ function joinComma(values: string[] | undefined): string | undefined {
|
|
|
30
30
|
return values.join(", ");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
interface SerializeAgentOptions {
|
|
34
|
+
preserveFrontmatterFields?: ReadonlySet<string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function serializeAgent(config: AgentConfig, options: SerializeAgentOptions = {}): string {
|
|
34
38
|
const lines: string[] = [];
|
|
39
|
+
const preserve = (...fields: string[]) => fields.some((field) => options.preserveFrontmatterFields?.has(field));
|
|
40
|
+
const preservingExistingFrontmatter = options.preserveFrontmatterFields !== undefined;
|
|
35
41
|
lines.push("---");
|
|
36
42
|
lines.push(`name: ${frontmatterNameForConfig(config)}`);
|
|
37
43
|
if (config.packageName) lines.push(`package: ${config.packageName}`);
|
|
@@ -42,25 +48,27 @@ export function serializeAgent(config: AgentConfig): string {
|
|
|
42
48
|
...(config.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`),
|
|
43
49
|
];
|
|
44
50
|
const toolsValue = joinComma(tools);
|
|
45
|
-
if (toolsValue) lines.push(`tools: ${toolsValue}`);
|
|
51
|
+
if (toolsValue || preserve("tools")) lines.push(`tools: ${toolsValue ?? ""}`);
|
|
46
52
|
|
|
47
|
-
if (config.model) lines.push(`model: ${config.model}`);
|
|
53
|
+
if (config.model || preserve("model")) lines.push(`model: ${config.model ?? ""}`);
|
|
48
54
|
const fallbackModelsValue = joinComma(config.fallbackModels);
|
|
49
|
-
if (fallbackModelsValue) lines.push(`fallbackModels: ${fallbackModelsValue}`);
|
|
50
|
-
if (config.thinking && config.thinking !== "off"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
lines.push(`
|
|
54
|
-
if (
|
|
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 ?? ""}`);
|
|
55
63
|
|
|
56
64
|
const skillsValue = joinComma(config.skills);
|
|
57
|
-
if (skillsValue) lines.push(`skills: ${skillsValue}`);
|
|
65
|
+
if (skillsValue || preserve("skill", "skills")) lines.push(`skills: ${skillsValue ?? ""}`);
|
|
58
66
|
|
|
59
67
|
if (config.extensions !== undefined) {
|
|
60
68
|
const extensionsValue = joinComma(config.extensions);
|
|
61
69
|
lines.push(`extensions: ${extensionsValue ?? ""}`);
|
|
62
70
|
}
|
|
63
|
-
if (config.subagentOnlyExtensions !== undefined) {
|
|
71
|
+
if (config.subagentOnlyExtensions !== undefined || preserve("subagentOnlyExtensions")) {
|
|
64
72
|
const subagentOnlyExtensionsValue = joinComma(config.subagentOnlyExtensions);
|
|
65
73
|
lines.push(`subagentOnlyExtensions: ${subagentOnlyExtensionsValue ?? ""}`);
|
|
66
74
|
}
|
|
@@ -76,12 +84,22 @@ export function serializeAgent(config: AgentConfig): string {
|
|
|
76
84
|
if (typeof maxSubagentDepth === "number" && Number.isInteger(maxSubagentDepth) && maxSubagentDepth >= 0) {
|
|
77
85
|
lines.push(`maxSubagentDepth: ${maxSubagentDepth}`);
|
|
78
86
|
}
|
|
79
|
-
if (config.completionGuard === false
|
|
87
|
+
if (config.completionGuard === false || preserve("completionGuard")) {
|
|
88
|
+
lines.push(`completionGuard: ${config.completionGuard === undefined ? "" : config.completionGuard ? "true" : "false"}`);
|
|
89
|
+
}
|
|
80
90
|
|
|
81
91
|
if (config.extraFields) {
|
|
82
92
|
for (const [key, value] of Object.entries(config.extraFields)) {
|
|
83
93
|
if (KNOWN_FIELDS.has(key)) continue;
|
|
84
|
-
|
|
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
|
+
}
|
|
85
103
|
}
|
|
86
104
|
}
|
|
87
105
|
|