pi-subagents 0.34.0 → 0.35.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 (98) hide show
  1. package/CHANGELOG.md +78 -9
  2. package/README.md +213 -32
  3. package/index.ts +1 -0
  4. package/install.mjs +1 -1
  5. package/package.json +23 -8
  6. package/prompts/review-loop.md +3 -1
  7. package/skills/pi-subagents/SKILL.md +87 -25
  8. package/src/agents/agent-management.ts +82 -15
  9. package/src/agents/agent-serializer.ts +19 -0
  10. package/src/agents/agents.ts +91 -49
  11. package/src/agents/frontmatter.ts +67 -13
  12. package/src/agents/skills.ts +25 -12
  13. package/src/api/background-work.ts +197 -0
  14. package/src/api/delegation.ts +158 -0
  15. package/src/extension/chain-validation.ts +165 -0
  16. package/src/extension/doctor.ts +15 -0
  17. package/src/extension/fanout-child.ts +3 -1
  18. package/src/extension/index.ts +65 -124
  19. package/src/extension/rpc.ts +10 -2
  20. package/src/extension/schemas.ts +18 -14
  21. package/src/extension/steering-notices.ts +35 -0
  22. package/src/extension/tool-description.ts +20 -9
  23. package/src/intercom/intercom-bridge.ts +3 -2
  24. package/src/intercom/native-supervisor-channel.ts +9 -1
  25. package/src/intercom/result-intercom.ts +4 -0
  26. package/src/runs/background/async-execution.ts +293 -44
  27. package/src/runs/background/async-job-tracker.ts +56 -9
  28. package/src/runs/background/async-resume.ts +159 -52
  29. package/src/runs/background/async-status.ts +25 -18
  30. package/src/runs/background/auto-drain.ts +67 -0
  31. package/src/runs/background/chain-root-attachment.ts +16 -8
  32. package/src/runs/background/control-channel.ts +260 -13
  33. package/src/runs/background/fleet-view.ts +23 -2
  34. package/src/runs/background/notify.ts +79 -10
  35. package/src/runs/background/result-watcher.ts +12 -9
  36. package/src/runs/background/run-id-resolver.ts +14 -2
  37. package/src/runs/background/run-status.ts +23 -15
  38. package/src/runs/background/scheduled-runs.ts +3 -0
  39. package/src/runs/background/stale-run-reconciler.ts +32 -10
  40. package/src/runs/background/steering.ts +237 -0
  41. package/src/runs/background/subagent-runner.ts +898 -236
  42. package/src/runs/background/subagent-wait.ts +484 -0
  43. package/src/runs/background/top-level-async.ts +2 -1
  44. package/src/runs/background/wait-config.ts +36 -0
  45. package/src/runs/background/wait-tool.ts +26 -0
  46. package/src/runs/foreground/async-steering-action.ts +230 -0
  47. package/src/runs/foreground/chain-clarify.ts +22 -6
  48. package/src/runs/foreground/chain-execution.ts +50 -32
  49. package/src/runs/foreground/execution.ts +308 -94
  50. package/src/runs/foreground/subagent-executor.ts +592 -268
  51. package/src/runs/shared/acceptance.ts +355 -97
  52. package/src/runs/shared/child-protocol.ts +121 -0
  53. package/src/runs/shared/completion-guard.ts +8 -127
  54. package/src/runs/shared/dynamic-fanout.ts +6 -4
  55. package/src/runs/shared/model-fallback.ts +36 -0
  56. package/src/runs/shared/nested-events.ts +9 -4
  57. package/src/runs/shared/nested-render.ts +4 -1
  58. package/src/runs/shared/parallel-utils.ts +7 -0
  59. package/src/runs/shared/pi-args.ts +34 -7
  60. package/src/runs/shared/pi-spawn.ts +18 -12
  61. package/src/runs/shared/session-lease.ts +279 -0
  62. package/src/runs/shared/single-output.ts +61 -6
  63. package/src/runs/shared/spawn-budget.ts +128 -0
  64. package/src/runs/shared/subagent-control.ts +10 -6
  65. package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
  66. package/src/runs/shared/task-intent.ts +176 -0
  67. package/src/runs/shared/tool-availability.ts +65 -0
  68. package/src/runs/shared/turn-budget.ts +49 -4
  69. package/src/shared/atomic-json.ts +4 -1
  70. package/src/shared/fork-context.ts +28 -3
  71. package/src/shared/model-info.ts +7 -4
  72. package/src/shared/status-format.ts +7 -1
  73. package/src/shared/types.ts +203 -25
  74. package/src/shared/utils.ts +35 -7
  75. package/src/slash/delegation-adapters.ts +457 -0
  76. package/src/slash/delegation-request.ts +103 -0
  77. package/src/slash/prompt-template-bridge.ts +167 -344
  78. package/src/slash/slash-commands.ts +239 -6
  79. package/src/slash/subagents-admin.ts +428 -0
  80. package/src/slash/subagents-editor.ts +86 -0
  81. package/src/tui/fleet.ts +405 -0
  82. package/src/tui/render.ts +90 -16
  83. package/src/watchdog/change-signature.ts +127 -0
  84. package/src/watchdog/child-status.ts +205 -0
  85. package/src/watchdog/emission-guard.ts +123 -0
  86. package/src/watchdog/lsp-diagnostics.ts +532 -0
  87. package/src/watchdog/model-selection.ts +167 -0
  88. package/src/watchdog/register-child.ts +117 -0
  89. package/src/watchdog/register-main.ts +433 -0
  90. package/src/watchdog/render.ts +54 -0
  91. package/src/watchdog/review.ts +293 -0
  92. package/src/watchdog/runtime.ts +712 -0
  93. package/src/watchdog/settings.ts +528 -0
  94. package/src/watchdog/tool-actions.ts +155 -0
  95. package/src/watchdog/turn-delta.ts +161 -0
  96. package/src/watchdog/types.ts +188 -0
  97. package/src/watchdog/warning-format.ts +73 -0
  98. package/src/runs/background/wait.ts +0 -394
@@ -21,8 +21,9 @@ import {
21
21
  removeBuiltinAgentOverrideFields,
22
22
  } from "./agents.ts";
23
23
  import { serializeAgent } from "./agent-serializer.ts";
24
+ import { mergeAgentsForScope } from "./agent-selection.ts";
24
25
  import { serializeChain, serializeJsonChain } from "./chain-serializer.ts";
25
- import { discoverAvailableSkills } from "./skills.ts";
26
+ import { discoverAvailableSkills, resolveSkills } from "./skills.ts";
26
27
  import {
27
28
  buildProactiveSkillSubagentRecommendationLines,
28
29
  } from "./proactive-skills.ts";
@@ -30,7 +31,9 @@ import { parseFrontmatter } from "./frontmatter.ts";
30
31
  import { toModelInfo } from "../shared/model-info.ts";
31
32
  import { resolveSubagentModelOverride, type ParentModel } from "../runs/shared/model-fallback.ts";
32
33
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
33
- import type { Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
34
+ import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
35
+ import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
36
+ import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
34
37
  import { getProjectConfigDir } from "../shared/utils.ts";
35
38
 
36
39
  type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete" | "eject" | "disable" | "enable" | "reset";
@@ -119,7 +122,7 @@ function findChains(name: string, cwd: string, scope: AgentScope = "both"): Chai
119
122
  const raw = name.trim();
120
123
  const sanitized = sanitizeName(raw);
121
124
  return discoverAgentsAll(cwd).chains
122
- .filter((c) => (scope === "both" || c.source === scope) && (c.name === raw || c.name === sanitized))
125
+ .filter((c) => (scope === "both" || c.source === "package" || c.source === scope) && (c.name === raw || c.name === sanitized))
123
126
  .sort((a, b) => a.source.localeCompare(b.source));
124
127
  }
125
128
 
@@ -187,14 +190,18 @@ function fallbackModelsWarning(ctx: ManagementContext, fallbackModels: string[]
187
190
  return missing.length ? `Warning: fallback models not in the current model registry: ${missing.join(", ")}.` : undefined;
188
191
  }
189
192
 
190
- function skillsWarning(cwd: string, skills: string[] | undefined): string | undefined {
191
- if (!skills || skills.length === 0) return undefined;
192
- const available = new Set(discoverAvailableSkills(cwd).map((s) => s.name));
193
- const missing = skills.filter((s) => !available.has(s));
193
+ function skillsWarning(cwd: string, agent: Pick<AgentConfig, "skills" | "skillPath" | "filePath">): string | undefined {
194
+ if (!agent.skills?.length) return undefined;
195
+ const { missing } = resolveSkills(
196
+ agent.skills,
197
+ cwd,
198
+ agent.skillPath,
199
+ agent.filePath ? path.dirname(agent.filePath) : cwd,
200
+ );
194
201
  return missing.length ? `Warning: skills not found: ${missing.join(", ")}.` : undefined;
195
202
  }
196
203
 
197
- function editableAgentConfig(agent: AgentConfig): AgentConfig {
204
+ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
198
205
  const base = agent.override?.base;
199
206
  if (!base) return { ...agent };
200
207
 
@@ -207,9 +214,11 @@ function editableAgentConfig(agent: AgentConfig): AgentConfig {
207
214
  inheritProjectContext: base.inheritProjectContext,
208
215
  inheritSkills: base.inheritSkills,
209
216
  defaultContext: base.defaultContext,
217
+ acceptanceRole: base.acceptanceRole,
210
218
  disabled: base.disabled,
211
219
  systemPrompt: base.systemPrompt,
212
220
  skills: base.skills ? [...base.skills] : undefined,
221
+ skillPath: base.skillPath ? [...base.skillPath] : undefined,
213
222
  tools: base.tools ? [...base.tools] : undefined,
214
223
  mcpDirectTools: base.mcpDirectTools ? [...base.mcpDirectTools] : undefined,
215
224
  subagentOnlyExtensions: base.subagentOnlyExtensions ? [...base.subagentOnlyExtensions] : undefined,
@@ -227,7 +236,7 @@ function readAgentFrontmatterFields(filePath: string): Set<string> {
227
236
  }
228
237
  }
229
238
 
230
- function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<string, unknown>): Set<string> {
239
+ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<string, unknown>): Set<string> {
231
240
  const fields = readAgentFrontmatterFields(agent.filePath);
232
241
  const changed = (...names: string[]) => {
233
242
  for (const name of names) fields.delete(name);
@@ -241,6 +250,7 @@ function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<string,
241
250
  if (hasKey(cfg, "fallbackModels")) changed("fallbackModels");
242
251
  if (hasKey(cfg, "tools")) changed("tools");
243
252
  if (hasKey(cfg, "skills")) changed("skill", "skills");
253
+ if (hasKey(cfg, "skillPath")) changed("skillPath");
244
254
  if (hasKey(cfg, "extensions")) changed("extensions");
245
255
  if (hasKey(cfg, "subagentOnlyExtensions")) changed("subagentOnlyExtensions");
246
256
  if (hasKey(cfg, "thinking")) {
@@ -260,6 +270,11 @@ function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<string,
260
270
  fields.add("inheritSkills");
261
271
  }
262
272
  if (hasKey(cfg, "defaultContext")) changed("defaultContext");
273
+ if (hasKey(cfg, "async")) changed("async");
274
+ if (hasKey(cfg, "timeoutMs")) changed("timeoutMs");
275
+ if (hasKey(cfg, "turnBudget")) changed("turnBudget");
276
+ if (hasKey(cfg, "acceptance")) changed("acceptance");
277
+ if (hasKey(cfg, "acceptanceRole")) changed("acceptanceRole");
263
278
  if (hasKey(cfg, "output")) changed("output");
264
279
  if (hasKey(cfg, "reads")) changed("defaultReads");
265
280
  if (hasKey(cfg, "progress")) changed("defaultProgress");
@@ -382,6 +397,14 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
382
397
  else if (typeof cfg.skills === "string") { const skills = parseCsv(cfg.skills); target.skills = skills.length ? skills : undefined; }
383
398
  else return "config.skills must be a comma-separated string or false when provided.";
384
399
  }
400
+ if (hasKey(cfg, "skillPath")) {
401
+ if (cfg.skillPath === false || cfg.skillPath === "") target.skillPath = undefined;
402
+ else if (typeof cfg.skillPath === "string") { const skillPath = parseCsv(cfg.skillPath); target.skillPath = skillPath.length ? skillPath : undefined; }
403
+ else if (Array.isArray(cfg.skillPath) && cfg.skillPath.every((entry) => typeof entry === "string")) {
404
+ const skillPath = [...new Set(cfg.skillPath.map((entry) => entry.trim()).filter(Boolean))];
405
+ target.skillPath = skillPath.length ? skillPath : undefined;
406
+ } else return "config.skillPath must be a comma-separated string, string array, or false when provided.";
407
+ }
385
408
  if (hasKey(cfg, "extensions")) {
386
409
  if (cfg.extensions === false) target.extensions = undefined;
387
410
  else if (cfg.extensions === "") target.extensions = [];
@@ -416,6 +439,37 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
416
439
  else if (cfg.defaultContext === "fresh" || cfg.defaultContext === "fork") target.defaultContext = cfg.defaultContext;
417
440
  else return "config.defaultContext must be 'fresh', 'fork', or false when provided.";
418
441
  }
442
+ if (hasKey(cfg, "async")) {
443
+ if (cfg.async === "") target.defaultAsync = undefined;
444
+ else if (typeof cfg.async === "boolean") target.defaultAsync = cfg.async;
445
+ else return "config.async must be a boolean or empty string when provided.";
446
+ }
447
+ if (hasKey(cfg, "timeoutMs")) {
448
+ if (cfg.timeoutMs === false || cfg.timeoutMs === "") target.defaultTimeoutMs = undefined;
449
+ else if (typeof cfg.timeoutMs === "number" && Number.isInteger(cfg.timeoutMs) && cfg.timeoutMs > 0) target.defaultTimeoutMs = cfg.timeoutMs;
450
+ else return "config.timeoutMs must be a positive integer or false when provided.";
451
+ }
452
+ if (hasKey(cfg, "turnBudget")) {
453
+ if (cfg.turnBudget === false || cfg.turnBudget === "") target.defaultTurnBudget = undefined;
454
+ else {
455
+ const resolved = resolveTurnBudgetConfig(cfg.turnBudget, "config.turnBudget");
456
+ if (resolved.error) return resolved.error;
457
+ target.defaultTurnBudget = resolved.turnBudget;
458
+ }
459
+ }
460
+ if (hasKey(cfg, "acceptance")) {
461
+ if (cfg.acceptance === "") target.defaultAcceptance = undefined;
462
+ else {
463
+ const errors = validateAcceptanceInput(cfg.acceptance, "config.acceptance");
464
+ if (errors.length > 0) return errors.join(" ");
465
+ target.defaultAcceptance = cfg.acceptance as AcceptanceInput;
466
+ }
467
+ }
468
+ if (hasKey(cfg, "acceptanceRole")) {
469
+ if (cfg.acceptanceRole === false || cfg.acceptanceRole === "") target.acceptanceRole = undefined;
470
+ else if (cfg.acceptanceRole === "read-only" || cfg.acceptanceRole === "writer") target.acceptanceRole = cfg.acceptanceRole;
471
+ else return "config.acceptanceRole must be 'read-only', 'writer', or false when provided.";
472
+ }
419
473
  if (hasKey(cfg, "output")) {
420
474
  if (cfg.output === false || cfg.output === "") target.output = undefined;
421
475
  else if (typeof cfg.output === "string") target.output = cfg.output;
@@ -508,10 +562,16 @@ function formatAgentDetail(agent: AgentConfig): string {
508
562
  if (agent.fallbackModels?.length) lines.push(`Fallback models: ${agent.fallbackModels.join(", ")}`);
509
563
  if (tools.length) lines.push(`Tools: ${tools.join(", ")}`);
510
564
  if (agent.skills?.length) lines.push(`Skills: ${agent.skills.join(", ")}`);
565
+ if (agent.skillPath?.length) lines.push(`Skill paths: ${agent.skillPath.join(", ")}`);
511
566
  lines.push(`System prompt mode: ${agent.systemPromptMode}`);
512
567
  lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
513
568
  lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
514
569
  if (agent.defaultContext) lines.push(`Default context: ${agent.defaultContext}`);
570
+ if (agent.defaultAsync !== undefined) lines.push(`Async: ${agent.defaultAsync ? "true" : "false"}`);
571
+ if (agent.defaultTimeoutMs !== undefined) lines.push(`Timeout: ${agent.defaultTimeoutMs}ms`);
572
+ if (agent.defaultTurnBudget) lines.push(`Turn budget: ${JSON.stringify(agent.defaultTurnBudget)}`);
573
+ if (agent.defaultAcceptance !== undefined) lines.push(`Acceptance: ${typeof agent.defaultAcceptance === "object" ? JSON.stringify(agent.defaultAcceptance) : String(agent.defaultAcceptance)}`);
574
+ if (agent.acceptanceRole) lines.push(`Acceptance role: ${agent.acceptanceRole}`);
515
575
  if (agent.source === "builtin") lines.push(`Disabled: ${agent.disabled ? "true" : "false"}`);
516
576
  if (agent.extensions !== undefined) lines.push(`Extensions: ${agent.extensions.length ? agent.extensions.join(", ") : "(none)"}`);
517
577
  if (agent.subagentOnlyExtensions !== undefined) lines.push(`Subagent-only extensions: ${agent.subagentOnlyExtensions.length ? agent.subagentOnlyExtensions.join(", ") : "(none)"}`);
@@ -580,7 +640,8 @@ function formatChainDetail(chain: ChainConfig): string {
580
640
  export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
581
641
  const scope = normalizeListScope(params.agentScope) ?? "both";
582
642
  const d = discoverAgentsAll(ctx.cwd);
583
- const scopedAgents = allAgents(d).filter((a) => scope === "both" || a.source === "builtin" || a.source === "package" || a.source === scope).sort((a, b) => a.name.localeCompare(b.name));
643
+ const scopedAgents = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package)
644
+ .sort((a, b) => a.name.localeCompare(b.name));
584
645
  const agents = scopedAgents.filter((a) => !a.disabled);
585
646
  const chains = d.chains.filter((c) => scope === "both" || c.source === "package" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
586
647
  const diagnostics = d.chainDiagnostics.filter((entry) => scope === "both" || entry.source === scope);
@@ -687,11 +748,17 @@ function handleModels(params: ManagementParams, ctx: ManagementContext): AgentTo
687
748
 
688
749
  function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
689
750
  if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for get.", true);
751
+ const scope = normalizeListScope(params.agentScope);
752
+ if (!scope) return result("agentScope must be 'user', 'project', or 'both' for get.", true);
690
753
  const hasBoth = Boolean(params.agent && params.chainName);
691
754
  const blocks: string[] = [];
692
755
  let anyFound = false;
693
756
  if (params.agent) {
694
- const matches = findAgents(params.agent, ctx.cwd, "both");
757
+ const raw = params.agent.trim();
758
+ const sanitized = sanitizeName(raw);
759
+ const d = discoverAgentsAll(ctx.cwd);
760
+ const matches = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package)
761
+ .filter((agent) => agent.name === raw || agent.name === sanitized);
695
762
  if (!matches.length) {
696
763
  const msg = `Agent '${params.agent}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`;
697
764
  if (!hasBoth) return result(msg, true);
@@ -702,7 +769,7 @@ function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolR
702
769
  }
703
770
  }
704
771
  if (params.chainName) {
705
- const matches = findChains(params.chainName, ctx.cwd, "both");
772
+ const matches = findChains(params.chainName, ctx.cwd, scope);
706
773
  if (!matches.length) {
707
774
  const msg = `Chain '${params.chainName}' not found. Available: ${availableNames(ctx.cwd, "chain").join(", ") || "none"}.`;
708
775
  if (!hasBoth) return result(msg, true);
@@ -770,7 +837,7 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
770
837
  if (mw) warnings.push(mw);
771
838
  const fmw = fallbackModelsWarning(ctx, agent.fallbackModels);
772
839
  if (fmw) warnings.push(fmw);
773
- const sw = skillsWarning(ctx.cwd, agent.skills);
840
+ const sw = skillsWarning(ctx.cwd, agent);
774
841
  if (sw) warnings.push(sw);
775
842
  fs.writeFileSync(targetPath, serializeAgent(agent), "utf-8");
776
843
  return result([`Created agent '${runtimeName}' at ${targetPath}.`, ...warnings].join("\n"));
@@ -819,8 +886,8 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
819
886
  const fmw = fallbackModelsWarning(ctx, updated.fallbackModels);
820
887
  if (fmw) warnings.push(fmw);
821
888
  }
822
- if (hasKey(cfg, "skills")) {
823
- const sw = skillsWarning(ctx.cwd, updated.skills);
889
+ if (hasKey(cfg, "skills") || hasKey(cfg, "skillPath")) {
890
+ const sw = skillsWarning(ctx.cwd, updated);
824
891
  if (sw) warnings.push(sw);
825
892
  }
826
893
  if (updated.name !== oldName) {
@@ -13,8 +13,14 @@ export const KNOWN_FIELDS = new Set([
13
13
  "inheritProjectContext",
14
14
  "inheritSkills",
15
15
  "defaultContext",
16
+ "async",
17
+ "timeoutMs",
18
+ "turnBudget",
19
+ "acceptance",
20
+ "acceptanceRole",
16
21
  "skill",
17
22
  "skills",
23
+ "skillPath",
18
24
  "extensions",
19
25
  "subagentOnlyExtensions",
20
26
  "output",
@@ -62,9 +68,22 @@ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptio
62
68
  if (!preservingExistingFrontmatter || preserve("inheritProjectContext")) lines.push(`inheritProjectContext: ${config.inheritProjectContext ? "true" : "false"}`);
63
69
  if (!preservingExistingFrontmatter || preserve("inheritSkills")) lines.push(`inheritSkills: ${config.inheritSkills ? "true" : "false"}`);
64
70
  if (config.defaultContext || preserve("defaultContext")) lines.push(`defaultContext: ${config.defaultContext ?? ""}`);
71
+ if (config.defaultAsync !== undefined || preserve("async")) lines.push(`async: ${config.defaultAsync === undefined ? "" : config.defaultAsync ? "true" : "false"}`);
72
+ if (config.defaultTimeoutMs !== undefined || preserve("timeoutMs")) lines.push(`timeoutMs: ${config.defaultTimeoutMs ?? ""}`);
73
+ if (config.defaultTurnBudget || preserve("turnBudget")) lines.push(`turnBudget: ${config.defaultTurnBudget ? JSON.stringify(config.defaultTurnBudget) : ""}`);
74
+ if (config.defaultAcceptance !== undefined || preserve("acceptance")) {
75
+ lines.push(`acceptance: ${config.defaultAcceptance === undefined
76
+ ? ""
77
+ : typeof config.defaultAcceptance === "object"
78
+ ? JSON.stringify(config.defaultAcceptance)
79
+ : String(config.defaultAcceptance)}`);
80
+ }
81
+ if (config.acceptanceRole || preserve("acceptanceRole")) lines.push(`acceptanceRole: ${config.acceptanceRole ?? ""}`);
65
82
 
66
83
  const skillsValue = joinComma(config.skills);
67
84
  if (skillsValue || preserve("skill", "skills")) lines.push(`skills: ${skillsValue ?? ""}`);
85
+ const skillPathValue = joinComma(config.skillPath);
86
+ if (skillPathValue || preserve("skillPath")) lines.push(`skillPath: ${skillPathValue ?? ""}`);
68
87
 
69
88
  if (config.extensions !== undefined) {
70
89
  const extensionsValue = joinComma(config.extensions);
@@ -4,19 +4,22 @@
4
4
 
5
5
  import { execSync } from "node:child_process";
6
6
  import * as fs from "node:fs";
7
+ import { parse as parseYaml } from "yaml";
7
8
  import * as os from "node:os";
8
9
  import * as path from "node:path";
9
10
  import { fileURLToPath } from "node:url";
10
- import type { AcceptanceInput, OutputMode, ToolBudgetConfig } from "../shared/types.ts";
11
+ import type { AcceptanceInput, AcceptanceRole, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
11
12
  import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
12
13
  import { KNOWN_FIELDS } from "./agent-serializer.ts";
13
14
  import { parseChain, parseJsonChain } from "./chain-serializer.ts";
14
15
  import { mergeAgentsForScope } from "./agent-selection.ts";
15
- import { parseFrontmatter } from "./frontmatter.ts";
16
+ import { parseFrontmatter, parseFrontmatterList } from "./frontmatter.ts";
16
17
  import { buildRuntimeName, parsePackageName } from "./identity.ts";
17
18
  import { parseModelScopeConfig, type ModelScopeConfig } from "../runs/shared/model-scope.ts";
18
19
  export { buildRuntimeName, frontmatterNameForConfig, parsePackageName } from "./identity.ts";
19
20
  import { parseMemoryFrontmatter } from "./agent-memory.ts";
21
+ import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
22
+ import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
20
23
 
21
24
  export type AgentScope = "user" | "project" | "both";
22
25
 
@@ -62,9 +65,11 @@ export interface BuiltinAgentOverrideBase {
62
65
  inheritProjectContext: boolean;
63
66
  inheritSkills: boolean;
64
67
  defaultContext?: AgentDefaultContext;
68
+ acceptanceRole?: AcceptanceRole;
65
69
  disabled?: boolean;
66
70
  systemPrompt: string;
67
71
  skills?: string[];
72
+ skillPath?: string[];
68
73
  tools?: string[];
69
74
  mcpDirectTools?: string[];
70
75
  subagentOnlyExtensions?: string[];
@@ -80,6 +85,7 @@ interface BuiltinAgentOverrideConfig {
80
85
  inheritProjectContext?: boolean;
81
86
  inheritSkills?: boolean;
82
87
  defaultContext?: AgentDefaultContext | false;
88
+ acceptanceRole?: AcceptanceRole | false;
83
89
  disabled?: boolean;
84
90
  systemPrompt?: string;
85
91
  skills?: string[] | false;
@@ -116,10 +122,16 @@ export interface AgentConfig {
116
122
  inheritProjectContext: boolean;
117
123
  inheritSkills: boolean;
118
124
  defaultContext?: AgentDefaultContext;
125
+ defaultAsync?: boolean;
126
+ defaultTimeoutMs?: number;
127
+ defaultTurnBudget?: TurnBudgetConfig;
128
+ defaultAcceptance?: AcceptanceInput;
129
+ acceptanceRole?: AcceptanceRole;
119
130
  systemPrompt: string;
120
131
  source: AgentSource;
121
132
  filePath: string;
122
133
  skills?: string[];
134
+ skillPath?: string[];
123
135
  extensions?: string[];
124
136
  subagentOnlyExtensions?: string[];
125
137
  output?: string;
@@ -301,6 +313,8 @@ function resolveSettingsPackageRoot(source: string, baseDir: string): string | u
301
313
  }
302
314
 
303
315
  function getGlobalNpmRoot(): string | null {
316
+ const offline = process.env.PI_OFFLINE?.toLowerCase();
317
+ if (offline === "1" || offline === "true" || offline === "yes") return null;
304
318
  if (cachedGlobalNpmRoot !== null) return cachedGlobalNpmRoot;
305
319
  try {
306
320
  cachedGlobalNpmRoot = fs.realpathSync(execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim());
@@ -494,9 +508,11 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
494
508
  inheritProjectContext: agent.inheritProjectContext,
495
509
  inheritSkills: agent.inheritSkills,
496
510
  defaultContext: agent.defaultContext,
511
+ acceptanceRole: agent.acceptanceRole,
497
512
  disabled: agent.disabled,
498
513
  systemPrompt: agent.systemPrompt,
499
514
  skills: agent.skills ? [...agent.skills] : undefined,
515
+ skillPath: agent.skillPath ? [...agent.skillPath] : undefined,
500
516
  tools: agent.tools ? [...agent.tools] : undefined,
501
517
  mcpDirectTools: agent.mcpDirectTools ? [...agent.mcpDirectTools] : undefined,
502
518
  subagentOnlyExtensions: agent.subagentOnlyExtensions ? [...agent.subagentOnlyExtensions] : undefined,
@@ -516,6 +532,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
516
532
  ...(override.inheritProjectContext !== undefined ? { inheritProjectContext: override.inheritProjectContext } : {}),
517
533
  ...(override.inheritSkills !== undefined ? { inheritSkills: override.inheritSkills } : {}),
518
534
  ...(override.defaultContext !== undefined ? { defaultContext: override.defaultContext } : {}),
535
+ ...(override.acceptanceRole !== undefined ? { acceptanceRole: override.acceptanceRole } : {}),
519
536
  ...(override.disabled !== undefined ? { disabled: override.disabled } : {}),
520
537
  ...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
521
538
  ...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
@@ -651,6 +668,14 @@ function parseBuiltinOverrideEntry(
651
668
  }
652
669
  }
653
670
 
671
+ if ("acceptanceRole" in input) {
672
+ if (input.acceptanceRole === "read-only" || input.acceptanceRole === "writer" || input.acceptanceRole === false) {
673
+ override.acceptanceRole = input.acceptanceRole;
674
+ } else {
675
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'acceptanceRole'; expected 'read-only', 'writer', or false.`);
676
+ }
677
+ }
678
+
654
679
  if ("disabled" in input) {
655
680
  if (typeof input.disabled === "boolean") {
656
681
  override.disabled = input.disabled;
@@ -786,6 +811,7 @@ function applyBuiltinOverride(
786
811
  if (override.inheritProjectContext !== undefined) next.inheritProjectContext = override.inheritProjectContext;
787
812
  if (override.inheritSkills !== undefined) next.inheritSkills = override.inheritSkills;
788
813
  if (override.defaultContext !== undefined) next.defaultContext = override.defaultContext === false ? undefined : override.defaultContext;
814
+ if (override.acceptanceRole !== undefined) next.acceptanceRole = override.acceptanceRole === false ? undefined : override.acceptanceRole;
789
815
  if (override.disabled !== undefined) next.disabled = override.disabled;
790
816
  if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
791
817
  if (override.skills !== undefined) next.skills = override.skills === false ? undefined : [...override.skills];
@@ -867,7 +893,7 @@ function applyBuiltinOverrides(
867
893
  });
868
894
  }
869
895
 
870
- function customAgentHasFrontmatterField(agent: AgentConfig, ...fields: string[]): boolean {
896
+ export function agentHasFrontmatterField(agent: AgentConfig, ...fields: string[]): boolean {
871
897
  const frontmatterFields = agentFrontmatterFields.get(agent);
872
898
  return frontmatterFields ? fields.some((field) => frontmatterFields.has(field)) : false;
873
899
  }
@@ -890,7 +916,7 @@ function applyCustomAgentOverride(
890
916
  frontmatterFields: string[],
891
917
  value: AgentConfig[K],
892
918
  ): void => {
893
- if (customAgentHasFrontmatterField(agent, ...frontmatterFields)) return;
919
+ if (agentHasFrontmatterField(agent, ...frontmatterFields)) return;
894
920
  mutable()[field] = value;
895
921
  anyFilled = true;
896
922
  };
@@ -920,6 +946,9 @@ function applyCustomAgentOverride(
920
946
  if (override.defaultContext !== undefined) {
921
947
  fill("defaultContext", ["defaultContext"], override.defaultContext === false ? undefined : override.defaultContext);
922
948
  }
949
+ if (override.acceptanceRole !== undefined) {
950
+ fill("acceptanceRole", ["acceptanceRole"], override.acceptanceRole === false ? undefined : override.acceptanceRole);
951
+ }
923
952
  if (override.disabled !== undefined && agent.disabled === undefined) {
924
953
  mutable().disabled = override.disabled;
925
954
  anyFilled = true;
@@ -927,7 +956,7 @@ function applyCustomAgentOverride(
927
956
  if (override.skills !== undefined) {
928
957
  fill("skills", ["skill", "skills"], override.skills === false ? undefined : [...override.skills]);
929
958
  }
930
- if (override.tools !== undefined && !customAgentHasFrontmatterField(agent, "tools")) {
959
+ if (override.tools !== undefined && !agentHasFrontmatterField(agent, "tools")) {
931
960
  const { tools, mcpDirectTools } = splitToolList(override.tools === false ? [] : override.tools);
932
961
  const target = mutable();
933
962
  target.tools = tools;
@@ -950,6 +979,8 @@ function applyCustomAgentOverride(
950
979
 
951
980
  if (!anyFilled || !next) return agent;
952
981
  next.override = { ...meta, base: cloneOverrideBase(agent) };
982
+ const frontmatterFields = agentFrontmatterFields.get(agent);
983
+ if (frontmatterFields) agentFrontmatterFields.set(next, frontmatterFields);
953
984
  return next;
954
985
  }
955
986
 
@@ -977,7 +1008,7 @@ function applyCustomAgentOverrides(
977
1008
 
978
1009
  export function buildBuiltinOverrideConfig(
979
1010
  base: BuiltinAgentOverrideBase,
980
- draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget">,
1011
+ draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget">,
981
1012
  ): BuiltinAgentOverrideConfig | undefined {
982
1013
  const override: BuiltinAgentOverrideConfig = {};
983
1014
 
@@ -988,6 +1019,7 @@ export function buildBuiltinOverrideConfig(
988
1019
  if (draft.inheritProjectContext !== base.inheritProjectContext) override.inheritProjectContext = draft.inheritProjectContext;
989
1020
  if (draft.inheritSkills !== base.inheritSkills) override.inheritSkills = draft.inheritSkills;
990
1021
  if (draft.defaultContext !== base.defaultContext) override.defaultContext = draft.defaultContext ?? false;
1022
+ if (draft.acceptanceRole !== base.acceptanceRole) override.acceptanceRole = draft.acceptanceRole ?? false;
991
1023
  if (draft.disabled !== base.disabled) override.disabled = draft.disabled ?? false;
992
1024
  if (draft.systemPrompt !== base.systemPrompt) override.systemPrompt = draft.systemPrompt;
993
1025
  if (!arraysEqual(draft.skills, base.skills)) override.skills = draft.skills ? [...draft.skills] : false;
@@ -1160,6 +1192,19 @@ function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {
1160
1192
  return parts.some((part, index) => part === ".agents" && parts[index + 1] === "skills");
1161
1193
  }
1162
1194
 
1195
+ function parseAgentAcceptanceFrontmatter(raw: string | undefined, agentName: string): AcceptanceInput | undefined {
1196
+ if (raw === undefined || !raw.trim()) return undefined;
1197
+ let parsed: unknown;
1198
+ try {
1199
+ parsed = parseYaml(raw);
1200
+ } catch (error) {
1201
+ throw new Error(`Agent '${agentName}' has invalid acceptance frontmatter: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1202
+ }
1203
+ const errors = validateAcceptanceInput(parsed, `Agent '${agentName}' acceptance frontmatter`);
1204
+ if (errors.length > 0) throw new Error(errors.join(" "));
1205
+ return parsed as AcceptanceInput;
1206
+ }
1207
+
1163
1208
  function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1164
1209
  const agents: AgentConfig[] = [];
1165
1210
 
@@ -1187,37 +1232,13 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1187
1232
  const packageName = parsedPackage.packageName;
1188
1233
  const runtimeName = buildRuntimeName(localName, packageName);
1189
1234
 
1190
- const rawTools = frontmatter.tools
1191
- ?.split(",")
1192
- .map((t) => t.trim())
1193
- .filter(Boolean);
1194
-
1195
- const mcpDirectTools: string[] = [];
1196
- const tools: string[] = [];
1197
- if (rawTools) {
1198
- for (const tool of rawTools) {
1199
- if (tool.startsWith("mcp:")) {
1200
- mcpDirectTools.push(tool.slice(4));
1201
- } else {
1202
- tools.push(tool);
1203
- }
1204
- }
1205
- }
1206
-
1207
- const defaultReads = frontmatter.defaultReads
1208
- ?.split(",")
1209
- .map((f) => f.trim())
1210
- .filter(Boolean);
1211
-
1235
+ const rawTools = parseFrontmatterList(frontmatter.tools);
1236
+ const { tools = [], mcpDirectTools = [] } = splitToolList(rawTools);
1237
+ const defaultReads = parseFrontmatterList(frontmatter.defaultReads);
1212
1238
  const skillStr = frontmatter.skill || frontmatter.skills;
1213
- const skills = skillStr
1214
- ?.split(",")
1215
- .map((s) => s.trim())
1216
- .filter(Boolean);
1217
- const fallbackModels = frontmatter.fallbackModels
1218
- ?.split(",")
1219
- .map((model) => model.trim())
1220
- .filter(Boolean);
1239
+ const skills = parseFrontmatterList(skillStr);
1240
+ const skillPath = parseFrontmatterList(frontmatter.skillPath);
1241
+ const fallbackModels = parseFrontmatterList(frontmatter.fallbackModels);
1221
1242
  const systemPromptMode = frontmatter.systemPromptMode === "replace"
1222
1243
  ? "replace"
1223
1244
  : frontmatter.systemPromptMode === "append"
@@ -1238,21 +1259,36 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1238
1259
  : frontmatter.defaultContext === "fresh"
1239
1260
  ? "fresh" as const
1240
1261
  : undefined;
1241
-
1242
- let extensions: string[] | undefined;
1243
- if (frontmatter.extensions !== undefined) {
1244
- extensions = frontmatter.extensions
1245
- .split(",")
1246
- .map((e) => e.trim())
1247
- .filter(Boolean);
1262
+ let defaultAsync: boolean | undefined;
1263
+ if (frontmatter.async !== undefined) {
1264
+ if (frontmatter.async === "true") defaultAsync = true;
1265
+ else if (frontmatter.async === "false") defaultAsync = false;
1266
+ else throw new Error(`Agent '${localName}' has invalid async frontmatter; expected true or false.`);
1267
+ }
1268
+ let defaultTimeoutMs: number | undefined;
1269
+ if (frontmatter.timeoutMs !== undefined) {
1270
+ const parsed = Number(frontmatter.timeoutMs);
1271
+ if (!Number.isInteger(parsed) || parsed <= 0) {
1272
+ throw new Error(`Agent '${localName}' has invalid timeoutMs frontmatter; expected a positive integer.`);
1273
+ }
1274
+ defaultTimeoutMs = parsed;
1248
1275
  }
1249
- let subagentOnlyExtensions: string[] | undefined;
1250
- if (frontmatter.subagentOnlyExtensions !== undefined) {
1251
- subagentOnlyExtensions = frontmatter.subagentOnlyExtensions
1252
- .split(",")
1253
- .map((e) => e.trim())
1254
- .filter(Boolean);
1276
+ let defaultTurnBudget: TurnBudgetConfig | undefined;
1277
+ if (frontmatter.turnBudget !== undefined && frontmatter.turnBudget.trim()) {
1278
+ const parsed = JSON.parse(frontmatter.turnBudget) as unknown;
1279
+ const resolved = resolveTurnBudgetConfig(parsed, `Agent '${localName}' turnBudget frontmatter`);
1280
+ if (resolved.error) throw new Error(resolved.error);
1281
+ defaultTurnBudget = resolved.turnBudget;
1255
1282
  }
1283
+ const defaultAcceptance = parseAgentAcceptanceFrontmatter(frontmatter.acceptance, localName);
1284
+ let acceptanceRole: AcceptanceRole | undefined;
1285
+ if (frontmatter.acceptanceRole !== undefined && frontmatter.acceptanceRole.trim()) {
1286
+ if (frontmatter.acceptanceRole === "read-only" || frontmatter.acceptanceRole === "writer") acceptanceRole = frontmatter.acceptanceRole;
1287
+ else throw new Error(`Agent '${localName}' has invalid acceptanceRole frontmatter; expected 'read-only' or 'writer'.`);
1288
+ }
1289
+
1290
+ const extensions = parseFrontmatterList(frontmatter.extensions);
1291
+ const subagentOnlyExtensions = parseFrontmatterList(frontmatter.subagentOnlyExtensions);
1256
1292
 
1257
1293
  const extraFields: Record<string, string> = {};
1258
1294
  for (const [key, value] of Object.entries(frontmatter)) {
@@ -1288,10 +1324,16 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
1288
1324
  inheritProjectContext,
1289
1325
  inheritSkills,
1290
1326
  defaultContext,
1327
+ defaultAsync,
1328
+ defaultTimeoutMs,
1329
+ defaultTurnBudget,
1330
+ defaultAcceptance,
1331
+ acceptanceRole,
1291
1332
  systemPrompt: body,
1292
1333
  source,
1293
1334
  filePath,
1294
1335
  skills: skills && skills.length > 0 ? skills : undefined,
1336
+ skillPath: skillPath && skillPath.length > 0 ? skillPath : undefined,
1295
1337
  extensions,
1296
1338
  subagentOnlyExtensions,
1297
1339
  output: frontmatter.output,