pi-subagents 0.50.0 → 0.52.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 (109) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/agents/oracle.md +3 -1
  3. package/agents/reviewer.md +1 -0
  4. package/agents/scout.md +2 -2
  5. package/agents/worker.md +2 -1
  6. package/async-retention-discovery-worker.mjs +180 -0
  7. package/docs/agents.md +40 -2
  8. package/docs/configuration.md +30 -12
  9. package/docs/extension-api.md +42 -1
  10. package/docs/models.md +2 -0
  11. package/docs/observability.md +41 -5
  12. package/docs/tool-reference.md +38 -39
  13. package/docs/workflows.md +171 -3
  14. package/package.json +4 -2
  15. package/skills/pi-subagents/SKILL.md +6 -4
  16. package/skills/pi-subagents/references/constraints-and-recipes.md +12 -6
  17. package/skills/pi-subagents/references/execution-controls.md +29 -15
  18. package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
  19. package/skills/pi-subagents/references/prompting-and-roles.md +4 -2
  20. package/src/agents/agent-management.ts +124 -351
  21. package/src/agents/agents.ts +157 -31
  22. package/src/agents/skills.ts +1 -1
  23. package/src/api/external-job-provider.ts +185 -0
  24. package/src/api/preflight.ts +36 -8
  25. package/src/api/shared-types.ts +2 -0
  26. package/src/extension/config.ts +3 -3
  27. package/src/extension/doctor.ts +3 -6
  28. package/src/extension/fanout-child.ts +2 -2
  29. package/src/extension/index.ts +166 -88
  30. package/src/extension/public-execution.ts +31 -2
  31. package/src/extension/schemas.ts +12 -35
  32. package/src/extension/tool-description.ts +35 -24
  33. package/src/inspectors/herdr/actions.ts +2 -2
  34. package/src/inspectors/herdr/inspector-runner.ts +2 -1
  35. package/src/inspectors/herdr/project-panes.ts +2 -2
  36. package/src/intercom/native-supervisor-channel.ts +32 -10
  37. package/src/missions/lifecycle.ts +6 -1
  38. package/src/missions/store.ts +4 -9
  39. package/src/missions/workflow-state.ts +2 -2
  40. package/src/profiles/profiles.ts +3 -1
  41. package/src/runs/background/active-run-index.ts +31 -8
  42. package/src/runs/background/async-execution.ts +68 -40
  43. package/src/runs/background/async-job-tracker.ts +20 -4
  44. package/src/runs/background/async-resume.ts +47 -15
  45. package/src/runs/background/async-retention.ts +886 -0
  46. package/src/runs/background/async-status.ts +39 -53
  47. package/src/runs/background/chain-append.ts +3 -33
  48. package/src/runs/background/completion-dedupe.ts +5 -1
  49. package/src/runs/background/completion-replay.ts +22 -12
  50. package/src/runs/background/control-channel.ts +14 -68
  51. package/src/runs/background/fleet-view.ts +68 -19
  52. package/src/runs/background/index-segment.ts +59 -0
  53. package/src/runs/background/inspect-rpc.ts +443 -0
  54. package/src/runs/background/notify.ts +31 -5
  55. package/src/runs/background/result-files.ts +158 -90
  56. package/src/runs/background/result-watcher.ts +116 -20
  57. package/src/runs/background/resume-guidance.ts +8 -5
  58. package/src/runs/background/retained-children.ts +13 -3
  59. package/src/runs/background/run-id-query.ts +7 -0
  60. package/src/runs/background/run-id-resolver.ts +11 -9
  61. package/src/runs/background/run-status.ts +27 -18
  62. package/src/runs/background/scheduled-runs.ts +24 -4
  63. package/src/runs/background/stale-run-reconciler.ts +6 -3
  64. package/src/runs/background/steering.ts +11 -1
  65. package/src/runs/background/subagent-runner.ts +253 -140
  66. package/src/runs/background/subagent-wait.ts +8 -8
  67. package/src/runs/background/terminal-run-index.ts +129 -0
  68. package/src/runs/background/wait-completions.ts +21 -4
  69. package/src/runs/background/wait-subscriptions.ts +81 -2
  70. package/src/runs/foreground/async-steering-action.ts +21 -14
  71. package/src/runs/foreground/execution.ts +3 -1
  72. package/src/runs/foreground/subagent-executor.ts +666 -1579
  73. package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
  74. package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  75. package/src/runs/shared/acceptance.ts +4 -4
  76. package/src/runs/shared/chain-outputs.ts +1 -3
  77. package/src/runs/shared/external-job-bridge.ts +444 -0
  78. package/src/runs/shared/external-job-runner.ts +286 -0
  79. package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  80. package/src/runs/shared/model-fallback.ts +98 -38
  81. package/src/runs/shared/orca-progress-tabs.ts +84 -22
  82. package/src/runs/shared/parallel-handoff.ts +46 -4
  83. package/src/runs/shared/parallel-utils.ts +4 -15
  84. package/src/runs/shared/permissions.ts +5 -1
  85. package/src/runs/shared/pi-args.ts +8 -1
  86. package/src/runs/shared/session-lease.ts +0 -6
  87. package/src/runs/shared/subagent-control.ts +26 -4
  88. package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
  89. package/src/runs/shared/workflow-graph.ts +1 -23
  90. package/src/runs/shared/worktree.ts +14 -2
  91. package/src/shared/atomic-json.ts +22 -2
  92. package/src/shared/capacity-resilient-json.ts +102 -0
  93. package/src/shared/completion-owner.ts +14 -0
  94. package/src/shared/file-system-retry.ts +49 -1
  95. package/src/shared/fork-context.ts +42 -0
  96. package/src/shared/prompt-resources.ts +0 -40
  97. package/src/shared/settings.ts +3 -27
  98. package/src/shared/types.ts +71 -26
  99. package/src/shared/utils.ts +8 -0
  100. package/src/shared/watch-strategy.ts +10 -0
  101. package/src/slash/slash-bridge.ts +2 -1
  102. package/src/slash/slash-commands.ts +29 -3
  103. package/src/tui/fleet.ts +63 -15
  104. package/src/watchdog/change-signature.ts +1 -1
  105. package/src/watchdog/lsp-diagnostics.ts +1 -0
  106. package/src/workflows/chat-progress.ts +2 -2
  107. package/src/workflows/scripted-workflow.ts +220 -67
  108. package/src/runs/foreground/chain-clarify.ts +0 -1354
  109. package/src/runs/foreground/chain-execution.ts +0 -1581
@@ -4,16 +4,16 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
4
4
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import {
6
6
  type AgentConfig,
7
+ type AgentDiscoveryDiagnostic,
7
8
  type AgentScope,
8
9
  type AgentSource,
9
- type ChainConfig,
10
- type ChainStepConfig,
11
10
  BUILTIN_AGENT_NAMES,
12
11
  defaultInheritProjectContext,
13
12
  defaultInheritSkills,
14
13
  defaultSystemPromptMode,
15
14
  discoverAgentsAll,
16
15
  buildRuntimeName,
16
+ findBlockingAgentDiagnostic,
17
17
  frontmatterNameForConfig,
18
18
  parsePackageName,
19
19
  mergeBuiltinAgentOverride,
@@ -23,12 +23,11 @@ import {
23
23
  } from "./agents.ts";
24
24
  import { serializeAgent } from "./agent-serializer.ts";
25
25
  import { mergeAgentsForScope } from "./agent-selection.ts";
26
- import { serializeChain, serializeJsonChain } from "./chain-serializer.ts";
27
26
  import { discoverAvailableSkills, resolveSkills } from "./skills.ts";
28
27
  import {
29
28
  buildProactiveSkillSubagentRecommendationLines,
30
29
  } from "./proactive-skills.ts";
31
- import { parseFrontmatter } from "./frontmatter.ts";
30
+ import { parseFrontmatter, parseFrontmatterList } from "./frontmatter.ts";
32
31
  import { toModelInfo } from "../shared/model-info.ts";
33
32
  import { resolveSubagentModelOverride, type ParentModel } from "../runs/shared/model-fallback.ts";
34
33
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
@@ -45,7 +44,6 @@ type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { mod
45
44
  interface ManagementParams {
46
45
  action?: string;
47
46
  agent?: string;
48
- chainName?: string;
49
47
  agentScope?: unknown;
50
48
  config?: unknown;
51
49
  }
@@ -76,6 +74,14 @@ function hasKey(obj: Record<string, unknown>, key: string): boolean {
76
74
  return Object.prototype.hasOwnProperty.call(obj, key);
77
75
  }
78
76
 
77
+ function isJsonSerializable(value: unknown): boolean {
78
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
79
+ if (typeof value === "number") return Number.isFinite(value);
80
+ if (Array.isArray(value)) return value.every(isJsonSerializable);
81
+ if (value && typeof value === "object") return Object.values(value).every(isJsonSerializable);
82
+ return false;
83
+ }
84
+
79
85
  function asDisambiguationScope(scope: unknown): ManagementScope | undefined {
80
86
  if (scope === "user" || scope === "project") return scope;
81
87
  return undefined;
@@ -105,14 +111,15 @@ function allAgents(d: { builtin: AgentConfig[]; package: AgentConfig[]; user: Ag
105
111
  return [...d.builtin, ...d.package, ...d.user, ...d.project];
106
112
  }
107
113
 
108
- function availableNames(cwd: string, kind: "agent" | "chain"): string[] {
109
- const d = discoverAgentsAll(cwd);
110
- const items = kind === "agent" ? allAgents(d) : d.chains;
111
- return [...new Set(items.map((x) => x.name))].sort((a, b) => a.localeCompare(b));
114
+ function availableAgentNamesFromDiscovery(d: { builtin: AgentConfig[]; package: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[] }): string[] {
115
+ return [...new Set(allAgents(d).map((agent) => agent.name))].sort((a, b) => a.localeCompare(b));
112
116
  }
113
117
 
114
- function findAgents(name: string, cwd: string, scope: AgentScope = "both"): AgentConfig[] {
115
- const d = discoverAgentsAll(cwd);
118
+ function availableAgentNames(cwd: string): string[] {
119
+ return availableAgentNamesFromDiscovery(discoverAgentsAll(cwd));
120
+ }
121
+
122
+ function findAgentsInDiscovery(name: string, d: { builtin: AgentConfig[]; package: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[] }, scope: AgentScope = "both"): AgentConfig[] {
116
123
  const raw = name.trim();
117
124
  const sanitized = sanitizeName(raw);
118
125
  const scoped = mergeAgentsForScope(scope, d.user, d.project, d.builtin, d.package);
@@ -125,12 +132,14 @@ function findAgents(name: string, cwd: string, scope: AgentScope = "both"): Agen
125
132
  .sort((a, b) => a.source.localeCompare(b.source));
126
133
  }
127
134
 
128
- function findChains(name: string, cwd: string, scope: AgentScope = "both"): ChainConfig[] {
129
- const raw = name.trim();
130
- const sanitized = sanitizeName(raw);
131
- return discoverAgentsAll(cwd).chains
132
- .filter((c) => (scope === "both" || c.source === "package" || c.source === scope) && (c.name === raw || c.name === sanitized))
133
- .sort((a, b) => a.source.localeCompare(b.source));
135
+ function findAgents(name: string, cwd: string, scope: AgentScope = "both"): AgentConfig[] {
136
+ return findAgentsInDiscovery(name, discoverAgentsAll(cwd), scope);
137
+ }
138
+
139
+ function diagnosticsForScope(diagnostics: AgentDiscoveryDiagnostic[] | undefined, scope: AgentScope): AgentDiscoveryDiagnostic[] | undefined {
140
+ if (scope === "both") return diagnostics;
141
+ const excludedSource = scope === "user" ? "project" : "user";
142
+ return diagnostics?.filter((diagnostic) => diagnostic.source !== excludedSource);
134
143
  }
135
144
 
136
145
  const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3 };
@@ -156,9 +165,6 @@ function nameExistsInScope(cwd: string, scope: ManagementScope, name: string, ex
156
165
  for (const a of scope === "user" ? d.user : d.project) {
157
166
  if (a.name === name && a.filePath !== excludePath) return true;
158
167
  }
159
- for (const c of d.chains) {
160
- if (c.source === scope && c.name === name && c.filePath !== excludePath) return true;
161
- }
162
168
  return false;
163
169
  }
164
170
 
@@ -166,30 +172,6 @@ function isMutableSource(source: AgentSource): source is ManagementScope {
166
172
  return source === "user" || source === "project";
167
173
  }
168
174
 
169
- function unknownChainAgents(cwd: string, steps: ChainStepConfig[]): string[] {
170
- const d = discoverAgentsAll(cwd);
171
- const agents = allAgents(d);
172
- return [...new Set(steps.map((s) => s.agent).filter((agentName): agentName is string => typeof agentName === "string" && !resolveAgentName(agentName, agents).agent))]
173
- .sort((a, b) => a.localeCompare(b));
174
- }
175
-
176
- function chainStepWarnings(ctx: ManagementContext, steps: ChainStepConfig[]): string[] {
177
- const warnings: string[] = [];
178
- const available = new Set(discoverAvailableSkills(ctx.cwd).map((s) => s.name));
179
- for (let i = 0; i < steps.length; i++) {
180
- const s = steps[i]!;
181
- if (s.model) {
182
- const found = ctx.modelRegistry.getAvailable().some((m) => `${m.provider}/${m.id}` === s.model || m.id === s.model);
183
- if (!found) warnings.push(`Warning: step ${i + 1} (${s.agent}): model '${s.model}' is not in the current model registry.`);
184
- }
185
- if (Array.isArray(s.skills) && s.skills.length > 0) {
186
- const missing = s.skills.filter((sk) => !available.has(sk));
187
- if (missing.length) warnings.push(`Warning: step ${i + 1} (${s.agent}): skills not found: ${missing.join(", ")}.`);
188
- }
189
- }
190
- return warnings;
191
- }
192
-
193
175
  function modelWarning(ctx: ManagementContext, model: string | undefined): string | undefined {
194
176
  if (!model) return undefined;
195
177
  const found = ctx.modelRegistry.getAvailable().some((m) => `${m.provider}/${m.id}` === model || m.id === model);
@@ -214,6 +196,18 @@ function skillsWarning(cwd: string, agent: Pick<AgentConfig, "skills" | "skillPa
214
196
  return missing.length ? `Warning: skills not found: ${missing.join(", ")}.` : undefined;
215
197
  }
216
198
 
199
+ function withDeclaredExtensionPaths(config: AgentConfig, filePath: string): AgentConfig {
200
+ const { frontmatter } = parseFrontmatter(fs.readFileSync(filePath, "utf-8"));
201
+ const { extensions: _extensions, subagentOnlyExtensions: _subagentOnlyExtensions, ...withoutResolvedExtensions } = config;
202
+ return {
203
+ ...withoutResolvedExtensions,
204
+ ...(frontmatter.extensions !== undefined ? { extensions: parseFrontmatterList(frontmatter.extensions) } : {}),
205
+ ...(frontmatter.subagentOnlyExtensions !== undefined
206
+ ? { subagentOnlyExtensions: parseFrontmatterList(frontmatter.subagentOnlyExtensions) }
207
+ : {}),
208
+ };
209
+ }
210
+
217
211
  export function editableAgentConfig(agent: AgentConfig): AgentConfig {
218
212
  const { extensions: _extensions, ...withoutExtensions } = agent;
219
213
  const base = agent.override?.base;
@@ -238,13 +232,13 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
238
232
  ...editable
239
233
  } = withoutExtensions;
240
234
  if (!base) {
241
- return {
235
+ return withDeclaredExtensionPaths({
242
236
  ...withoutExtensions,
243
237
  ...(agent.extensionsFromDefault ? {} : agent.extensions !== undefined ? { extensions: [...agent.extensions] } : {}),
244
- };
238
+ }, agent.filePath);
245
239
  }
246
240
 
247
- return {
241
+ return withDeclaredExtensionPaths({
248
242
  ...editable,
249
243
  ...(base.model !== undefined ? { model: base.model } : {}),
250
244
  ...(base.fallbackModels !== undefined ? { fallbackModels: [...base.fallbackModels] } : {}),
@@ -263,7 +257,7 @@ export function editableAgentConfig(agent: AgentConfig): AgentConfig {
263
257
  ...(base.extensions !== undefined ? { extensions: [...base.extensions] } : {}),
264
258
  ...(base.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...base.subagentOnlyExtensions] } : {}),
265
259
  ...(base.completionGuard !== undefined ? { completionGuard: base.completionGuard } : {}),
266
- };
260
+ }, agent.filePath);
267
261
  }
268
262
 
269
263
  function readAgentFrontmatterFields(filePath: string): Set<string> {
@@ -329,69 +323,6 @@ export function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<
329
323
  return fields;
330
324
  }
331
325
 
332
- function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: string } {
333
- if (!Array.isArray(raw)) return { error: "config.steps must be an array." };
334
- if (raw.length === 0) return { error: "config.steps must include at least one step." };
335
- const steps: ChainStepConfig[] = [];
336
- for (let i = 0; i < raw.length; i++) {
337
- const item = raw[i];
338
- if (!item || typeof item !== "object" || Array.isArray(item)) return { error: `config.steps[${i}] must be an object.` };
339
- const s = item as Record<string, unknown>;
340
- if (typeof s.agent !== "string" || !s.agent.trim()) return { error: `config.steps[${i}].agent must be a non-empty string.` };
341
- const step: ChainStepConfig = { agent: s.agent.trim(), task: typeof s.task === "string" ? s.task : "" };
342
- if (hasKey(s, "phase")) {
343
- if (typeof s.phase === "string") step.phase = s.phase;
344
- else return { error: `config.steps[${i}].phase must be a string.` };
345
- }
346
- if (hasKey(s, "label")) {
347
- if (typeof s.label === "string") step.label = s.label;
348
- else return { error: `config.steps[${i}].label must be a string.` };
349
- }
350
- if (hasKey(s, "as")) {
351
- if (typeof s.as === "string") step.as = s.as;
352
- else return { error: `config.steps[${i}].as must be a string.` };
353
- }
354
- if (hasKey(s, "outputSchema")) {
355
- if (typeof s.outputSchema === "string") step.outputSchema = s.outputSchema;
356
- else return { error: `config.steps[${i}].outputSchema must be a schema file path string for saved chains.` };
357
- }
358
- if (hasKey(s, "output")) {
359
- if (s.output === false) step.output = false;
360
- else if (typeof s.output === "string") step.output = s.output;
361
- else return { error: `config.steps[${i}].output must be a string or false.` };
362
- }
363
- if (hasKey(s, "outputMode")) {
364
- if (s.outputMode === "inline" || s.outputMode === "file-only") step.outputMode = s.outputMode;
365
- else return { error: `config.steps[${i}].outputMode must be 'inline' or 'file-only'.` };
366
- }
367
- if (hasKey(s, "reads")) {
368
- if (s.reads === false) step.reads = false;
369
- else if (Array.isArray(s.reads)) step.reads = s.reads.filter((v): v is string => typeof v === "string").map((v) => v.trim()).filter(Boolean);
370
- else return { error: `config.steps[${i}].reads must be an array or false.` };
371
- }
372
- if (hasKey(s, "model")) {
373
- if (typeof s.model === "string") step.model = s.model;
374
- else return { error: `config.steps[${i}].model must be a string.` };
375
- }
376
- if (hasKey(s, "skills")) {
377
- if (s.skills === false) step.skills = false;
378
- else if (Array.isArray(s.skills)) step.skills = s.skills.filter((v): v is string => typeof v === "string").map((v) => v.trim()).filter(Boolean);
379
- else return { error: `config.steps[${i}].skills must be an array or false.` };
380
- }
381
- if (hasKey(s, "progress")) {
382
- if (typeof s.progress === "boolean") step.progress = s.progress;
383
- else return { error: `config.steps[${i}].progress must be a boolean.` };
384
- }
385
- if (hasKey(s, "toolBudget")) {
386
- const validation = validateToolBudgetConfig(s.toolBudget, `config.steps[${i}].toolBudget`);
387
- if (validation.error) return { error: validation.error };
388
- if (s.toolBudget !== undefined) step.toolBudget = s.toolBudget as ToolBudgetConfig;
389
- }
390
- steps.push(step);
391
- }
392
- return { steps };
393
- }
394
-
395
326
  function parseTools(raw: string): { tools?: string[]; mcpDirectTools?: string[] } {
396
327
  const tools: string[] = [];
397
328
  const mcpDirectTools: string[] = [];
@@ -436,7 +367,11 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
436
367
  && Object.keys(runner).every((key) => ["type", "command", "args", "promptDelivery"].includes(key))) {
437
368
  const runnerArgs = Array.isArray(runner.args) ? runner.args.filter((arg): arg is string => typeof arg === "string") : undefined;
438
369
  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' }.";
370
+ } else if (runner.type === "external-job" && typeof runner.provider === "string" && runner.provider.trim() === runner.provider && runner.provider
371
+ && (runner.options === undefined || (runner.options && typeof runner.options === "object" && !Array.isArray(runner.options) && isJsonSerializable(runner.options)))
372
+ && Object.keys(runner).every((key) => ["type", "provider", "options"].includes(key))) {
373
+ target.runner = { type: "external-job", provider: runner.provider, ...(runner.options ? { options: runner.options as Record<string, unknown> } : {}) };
374
+ } else return "config.runner must be { type: 'pi' }, { type: 'external-cli', command: string, args?: string[], promptDelivery?: 'stdin' }, or { type: 'external-job', provider: string, options?: object }.";
440
375
  } else return "config.runner must be an object, false, or empty string when provided.";
441
376
  }
442
377
  if (hasKey(cfg, "model")) {
@@ -596,7 +531,7 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
596
531
  target.toolBudget = cfg.toolBudget as ToolBudgetConfig;
597
532
  }
598
533
  }
599
- if (target.runner?.type === "external-cli") {
534
+ if (target.runner?.type === "external-cli" || target.runner?.type === "external-job") {
600
535
  const unsupported = [
601
536
  target.tools?.length || target.mcpDirectTools?.length ? "tools" : undefined,
602
537
  target.model ? "model" : undefined,
@@ -609,55 +544,40 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
609
544
  target.completionGuard !== undefined ? "completionGuard" : undefined,
610
545
  target.toolBudget ? "toolBudget" : undefined,
611
546
  ].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(", ")}.`;
547
+ if (unsupported.length > 0) return `config.runner type '${target.runner.type}' does not support Pi-only fields: ${unsupported.join(", ")}.`;
613
548
  }
614
549
  return undefined;
615
550
  }
616
551
 
617
552
  function resolveTarget<T extends { name: string; source: AgentSource; filePath: string }>(
618
- kind: "agent" | "chain",
619
553
  name: string,
620
554
  matches: T[],
621
555
  cwd: string,
622
556
  scopeHint?: string,
623
557
  ): T | AgentToolResult<Details> {
624
- const distinctNames = [...new Set(matches.map((m) => m.name))];
625
- if (distinctNames.length > 1) {
626
- return result(`Ambiguous ${kind} alias or name '${name}': ${distinctNames.sort((a, b) => a.localeCompare(b)).join(", ")}`, true);
627
- }
628
- const mutable = matches.filter((m): m is T & { source: ManagementScope } => isMutableSource(m.source));
558
+ const distinctNames = [...new Set(matches.map((match) => match.name))];
559
+ if (distinctNames.length > 1) return result(`Ambiguous agent alias or name '${name}': ${distinctNames.sort((a, b) => a.localeCompare(b)).join(", ")}`, true);
560
+ const mutable = matches.filter((match): match is T & { source: ManagementScope } => isMutableSource(match.source));
629
561
  if (mutable.length === 0) {
630
- if (matches.length > 0) {
631
- return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' is read-only and cannot be modified. Create a same-named ${kind} in user or project scope to override it.`, true);
632
- }
633
- const available = availableNames(cwd, kind);
634
- return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' not found. Available: ${available.join(", ") || "none"}.`, true);
562
+ if (matches.length > 0) return result(`Agent '${name}' is read-only and cannot be modified. Create a same-named agent in user or project scope to override it.`, true);
563
+ return result(`Agent '${name}' not found. Available: ${availableAgentNames(cwd).join(", ") || "none"}.`, true);
635
564
  }
636
565
  if (mutable.length === 1) return mutable[0]!;
637
566
  const scope = asDisambiguationScope(scopeHint);
638
567
  if (!scope) {
639
- const paths = mutable.map((m) => `${m.source}: ${m.filePath}`).join("\n");
640
- return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' exists in both scopes. Specify agentScope: 'user' or 'project'.\n${paths}`, true);
568
+ const paths = mutable.map((match) => `${match.source}: ${match.filePath}`).join("\n");
569
+ return result(`Agent '${name}' exists in both scopes. Specify agentScope: 'user' or 'project'.\n${paths}`, true);
641
570
  }
642
- const scoped = mutable.filter((m) => m.source === scope);
643
- if (scoped.length === 0) return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' not found in scope '${scope}'.`, true);
644
- if (scoped.length > 1) return result(`Multiple ${kind}s named '${name}' found in scope '${scope}': ${scoped.map((m) => m.filePath).join(", ")}`, true);
571
+ const scoped = mutable.filter((match) => match.source === scope);
572
+ if (scoped.length === 0) return result(`Agent '${name}' not found in scope '${scope}'.`, true);
573
+ if (scoped.length > 1) return result(`Multiple agents named '${name}' found in scope '${scope}': ${scoped.map((match) => match.filePath).join(", ")}`, true);
645
574
  return scoped[0]!;
646
575
  }
647
576
 
648
- function renamePath(
649
- kind: "agent" | "chain",
650
- currentPath: string,
651
- newName: string,
652
- scope: ManagementScope,
653
- cwd: string,
654
- ): { filePath?: string; error?: string } {
577
+ function renamePath(currentPath: string, newName: string, scope: ManagementScope, cwd: string): { filePath?: string; error?: string } {
655
578
  if (nameExistsInScope(cwd, scope, newName, currentPath)) return { error: `Name '${newName}' already exists in ${scope} scope.` };
656
- const ext = kind === "agent" ? ".md" : currentPath.endsWith(".chain.json") ? ".chain.json" : ".chain.md";
657
- const filePath = path.join(path.dirname(currentPath), `${newName}${ext}`);
658
- if (fs.existsSync(filePath) && filePath !== currentPath) {
659
- return { error: `File already exists at ${filePath} but is not a valid ${kind} definition. Remove or rename it first.` };
660
- }
579
+ const filePath = path.join(path.dirname(currentPath), `${newName}.md`);
580
+ if (fs.existsSync(filePath) && filePath !== currentPath) return { error: `File already exists at ${filePath} but is not a valid agent definition. Remove or rename it first.` };
661
581
  fs.renameSync(currentPath, filePath);
662
582
  return { filePath };
663
583
  }
@@ -700,56 +620,6 @@ function formatAgentDetail(agent: AgentConfig): string {
700
620
  return lines.join("\n");
701
621
  }
702
622
 
703
- function formatChainStepDetail(step: ChainStepConfig, index: number): string[] {
704
- const lines: string[] = [];
705
- if (step.expand || step.collect) {
706
- const parallel = step.parallel && !Array.isArray(step.parallel) && typeof step.parallel === "object" ? step.parallel as { agent?: unknown; task?: unknown; label?: unknown; outputSchema?: unknown } : undefined;
707
- const expand = step.expand && typeof step.expand === "object" ? step.expand as { from?: { output?: unknown; path?: unknown }; item?: unknown; key?: unknown; maxItems?: unknown; onEmpty?: unknown } : undefined;
708
- const collect = step.collect && typeof step.collect === "object" ? step.collect as { as?: unknown; outputSchema?: unknown } : undefined;
709
- lines.push(`${index + 1}. Dynamic fanout${typeof collect?.as === "string" ? ` -> ${collect.as}` : ""}`);
710
- if (expand?.from) lines.push(` Expand: ${String(expand.from.output ?? "?")}${String(expand.from.path ?? "")}`);
711
- if (typeof expand?.item === "string") lines.push(` Item variable: ${expand.item}`);
712
- if (typeof expand?.key === "string") lines.push(` Key: ${expand.key}`);
713
- if (typeof expand?.maxItems === "number") lines.push(` Max items: ${expand.maxItems}`);
714
- if (typeof expand?.onEmpty === "string") lines.push(` On empty: ${expand.onEmpty}`);
715
- if (parallel?.agent) lines.push(` Agent: ${String(parallel.agent)}`);
716
- if (typeof parallel?.label === "string") lines.push(` Label: ${parallel.label}`);
717
- if (typeof parallel?.task === "string" && parallel.task.trim()) lines.push(` Task: ${parallel.task}`);
718
- if (parallel?.outputSchema) lines.push(" Structured output: true");
719
- if (parallel && "toolBudget" in parallel) lines.push(` Tool budget: ${JSON.stringify((parallel as { toolBudget?: unknown }).toolBudget)}`);
720
- if (collect?.outputSchema) lines.push(" Collect schema: true");
721
- if (step.concurrency !== undefined) lines.push(` Concurrency: ${step.concurrency}`);
722
- if (step.failFast !== undefined) lines.push(` Fail fast: ${step.failFast ? "true" : "false"}`);
723
- return lines;
724
- }
725
- lines.push(`${index + 1}. ${step.agent}`);
726
- if (step.task?.trim()) lines.push(` Task: ${step.task}`);
727
- if (step.output === false) lines.push(" Output: false");
728
- else if (step.output) lines.push(` Output: ${step.output}`);
729
- if (step.outputMode) lines.push(` Output mode: ${step.outputMode}`);
730
- if (step.toolBudget) lines.push(` Tool budget: ${JSON.stringify(step.toolBudget)}`);
731
- if (step.reads === false) lines.push(" Reads: false");
732
- else if (Array.isArray(step.reads) && step.reads.length > 0) lines.push(` Reads: ${step.reads.join(", ")}`);
733
- if (step.model) lines.push(` Model: ${step.model}`);
734
- if (step.skills === false) lines.push(" Skills: false");
735
- else if (Array.isArray(step.skills) && step.skills.length > 0) lines.push(` Skills: ${step.skills.join(", ")}`);
736
- if (step.progress !== undefined) lines.push(` Progress: ${step.progress ? "true" : "false"}`);
737
- return lines;
738
- }
739
-
740
- function formatChainDetail(chain: ChainConfig): string {
741
- const lines: string[] = [`Chain: ${chain.name} (${chain.source})`, `Path: ${chain.filePath}`, `Description: ${chain.description}`];
742
- if (chain.packageName) {
743
- lines.push(`Local name: ${frontmatterNameForConfig(chain)}`);
744
- lines.push(`Package: ${chain.packageName}`);
745
- }
746
- lines.push("", "Steps:");
747
- for (let i = 0; i < chain.steps.length; i++) {
748
- lines.push(...formatChainStepDetail(chain.steps[i]!, i));
749
- }
750
- return lines.join("\n");
751
- }
752
-
753
623
  export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
754
624
  const scope = normalizeListScope(params.agentScope) ?? "both";
755
625
  const d = discoverAgentsAll(ctx.cwd);
@@ -760,11 +630,8 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
760
630
  const agents = visibleAgents.filter((a) => isAgentAllowedByCapabilityCeiling(a.name, capabilityCeiling));
761
631
  const restrictedAgents = visibleAgents.filter((a) => !isAgentAllowedByCapabilityCeiling(a.name, capabilityCeiling));
762
632
  const restrictedSources = capabilityCeilingAgentRestrictionSources(capabilityCeiling);
763
- const chains = d.chains.filter((c) => scope === "both" || c.source === "package" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
764
- const diagnostics = d.chainDiagnostics.filter((entry) => scope === "both" || entry.source === scope);
765
633
  const proactiveSuggestions = buildProactiveSkillSubagentRecommendationLines({
766
634
  agents,
767
- chains,
768
635
  ...(ctx.config?.proactiveSkillSubagents !== undefined ? { config: ctx.config.proactiveSkillSubagents } : {}),
769
636
  discoverAvailableSkills: () => discoverAvailableSkills(ctx.cwd),
770
637
  });
@@ -778,11 +645,12 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
778
645
  `Restricted agents (not executable in this session${restrictedSources?.length ? `; capability ceiling: ${restrictedSources.join(", ")}` : ""}):`,
779
646
  ...restrictedAgents.map((a) => `- ${a.name} (${a.source}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`),
780
647
  ] : []),
781
- "",
782
- "Chains:",
783
- ...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"]),
648
+ ...(d.agentDiagnostics?.length ? [
649
+ "",
650
+ "Invalid agent definitions:",
651
+ ...d.agentDiagnostics.map((diagnostic) => `- ${diagnostic.name ?? diagnostic.filePath} (${diagnostic.source}): ${diagnostic.error}`),
652
+ ] : []),
784
653
  ...(proactiveSuggestions.length ? ["", ...proactiveSuggestions] : []),
785
- ...(diagnostics.length ? ["", "Chain diagnostics:", ...diagnostics.map((entry) => `- ${entry.filePath}: ${entry.error}`)] : []),
786
654
  ];
787
655
  return result(lines.join("\n"));
788
656
  }
@@ -869,40 +737,23 @@ function handleModels(params: ManagementParams, ctx: ManagementContext): AgentTo
869
737
  }
870
738
 
871
739
  function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
872
- if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for get.", true);
740
+ if (!params.agent) return result("Specify 'agent' for get.", true);
873
741
  const scope = normalizeListScope(params.agentScope);
874
742
  if (!scope) return result("agentScope must be 'user', 'project', or 'both' for get.", true);
875
- const hasBoth = Boolean(params.agent && params.chainName);
876
- const blocks: string[] = [];
877
- let anyFound = false;
878
- if (params.agent) {
879
- const matches = findAgents(params.agent, ctx.cwd, scope);
880
- const distinctNames = [...new Set(matches.map((agent) => agent.name))];
881
- if (distinctNames.length > 1) {
882
- const msg = `Ambiguous agent alias or name '${params.agent}': ${distinctNames.sort((a, b) => a.localeCompare(b)).join(", ")}`;
883
- if (!hasBoth) return result(msg, true);
884
- blocks.push(msg);
885
- } else if (!matches.length) {
886
- const msg = `Agent '${params.agent}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`;
887
- if (!hasBoth) return result(msg, true);
888
- blocks.push(msg);
889
- } else {
890
- anyFound = true;
891
- blocks.push(...matches.map(formatAgentDetail));
892
- }
893
- }
894
- if (params.chainName) {
895
- const matches = findChains(params.chainName, ctx.cwd, scope);
896
- if (!matches.length) {
897
- const msg = `Chain '${params.chainName}' not found. Available: ${availableNames(ctx.cwd, "chain").join(", ") || "none"}.`;
898
- if (!hasBoth) return result(msg, true);
899
- blocks.push(msg);
900
- } else {
901
- anyFound = true;
902
- blocks.push(...matches.map(formatChainDetail));
903
- }
904
- }
905
- return result(blocks.join("\n\n"), !anyFound);
743
+ const discovered = discoverAgentsAll(ctx.cwd);
744
+ const matches = findAgentsInDiscovery(params.agent, discovered, scope);
745
+ const diagnostics = diagnosticsForScope(discovered.agentDiagnostics, scope);
746
+ const rawName = params.agent.trim();
747
+ const normalizedName = sanitizeName(rawName);
748
+ const diagnostic = findBlockingAgentDiagnostic(rawName, matches, diagnostics)
749
+ ?? (normalizedName !== rawName ? findBlockingAgentDiagnostic(normalizedName, matches, diagnostics) : undefined);
750
+ if (diagnostic) return result(`Agent '${params.agent}' has invalid configuration: ${diagnostic.error}`, true);
751
+ const distinctNames = [...new Set(matches.map((agent) => agent.name))];
752
+ if (distinctNames.length > 1) return result(`Ambiguous agent alias or name '${params.agent}': ${distinctNames.sort((a, b) => a.localeCompare(b)).join(", ")}`, true);
753
+ if (!matches.length) {
754
+ return result(`Agent '${params.agent}' not found. Available: ${availableAgentNamesFromDiscovery(discovered).join(", ") || "none"}.`, true);
755
+ }
756
+ return result(matches.map(formatAgentDetail).join("\n\n"));
906
757
  }
907
758
 
908
759
  export function handleCreate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
@@ -920,36 +771,16 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
920
771
  const scopeRaw = cfg.scope ?? "user";
921
772
  if (scopeRaw !== "user" && scopeRaw !== "project") return result("config.scope must be 'user' or 'project'.", true);
922
773
  const scope = scopeRaw as ManagementScope;
923
- const isChain = hasKey(cfg, "steps");
774
+ if (hasKey(cfg, "steps")) return result("Durable chain definitions were removed; use workflowScript or /prompt-workflow for repeatable workflows.", true);
924
775
  const d = discoverAgentsAll(ctx.cwd);
925
776
  const projectConfigDir = getProjectConfigDir(ctx.cwd);
926
- const targetDir = isChain
927
- ? scope === "user" ? d.userChainDir : d.projectChainDir ?? path.join(projectConfigDir, "chains")
928
- : scope === "user" ? d.userDir : d.projectDir ?? path.join(projectConfigDir, "agents");
777
+ const targetDir = scope === "user" ? d.userDir : d.projectDir ?? path.join(projectConfigDir, "agents");
929
778
  fs.mkdirSync(targetDir, { recursive: true });
930
779
  if (nameExistsInScope(ctx.cwd, scope, runtimeName)) return result(`Name '${runtimeName}' already exists in ${scope} scope. Use update instead.`, true);
931
- const targetPath = path.join(targetDir, isChain ? `${runtimeName}.chain.md` : `${runtimeName}.md`);
932
- if (fs.existsSync(targetPath)) return result(`File already exists at ${targetPath} but is not a valid ${isChain ? "chain" : "agent"} definition. Remove or rename it first.`, true);
780
+ const targetPath = path.join(targetDir, `${runtimeName}.md`);
781
+ if (fs.existsSync(targetPath)) return result(`File already exists at ${targetPath} but is not a valid agent definition. Remove or rename it first.`, true);
933
782
  const warnings: string[] = [];
934
- if (!isChain && d.builtin.some((a) => a.name === runtimeName)) warnings.push(`Note: this shadows the builtin agent '${runtimeName}'.`);
935
- if (isChain) {
936
- const parsed = parseStepList(cfg.steps);
937
- if (parsed.error) return result(parsed.error, true);
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
- };
947
- fs.writeFileSync(targetPath, serializeChain(chain), "utf-8");
948
- const missing = unknownChainAgents(ctx.cwd, chain.steps);
949
- if (missing.length) warnings.push(`Warning: chain steps reference unknown agents: ${missing.join(", ")}.`);
950
- warnings.push(...chainStepWarnings(ctx, chain.steps));
951
- return result([`Created chain '${runtimeName}' at ${targetPath}.`, ...warnings].join("\n"));
952
- }
783
+ if (d.builtin.some((a) => a.name === runtimeName)) warnings.push(`Note: this shadows the builtin agent '${runtimeName}'.`);
953
784
  const agent: AgentConfig = {
954
785
  name: runtimeName,
955
786
  localName: name,
@@ -975,75 +806,25 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
975
806
  }
976
807
 
977
808
  export function handleUpdate(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
978
- if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for update.", true);
979
- if (params.agent && params.chainName) return result("Specify either 'agent' or 'chainName', not both.", true);
809
+ if (!params.agent) return result("Specify 'agent' for update.", true);
980
810
  const parsedConfig = configObject(params.config);
981
811
  if (parsedConfig.error) return result(parsedConfig.error, true);
982
812
  const cfg = parsedConfig.value;
983
813
  if (!cfg) return result("config required for update.", true);
814
+ if (hasKey(cfg, "steps")) return result("Durable chain definitions were removed; use workflowScript or /prompt-workflow for repeatable workflows.", true);
984
815
  const warnings: string[] = [];
985
- if (params.agent) {
986
- const scopeHint = asDisambiguationScope(params.agentScope);
987
- const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
988
- if ("content" in targetOrError) return targetOrError;
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);
991
- const updated = editableAgentConfig(target);
992
- const oldName = target.name;
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);
994
- if (hasKey(cfg, "description") && (typeof cfg.description !== "string" || !cfg.description.trim())) return result("config.description must be a non-empty string when provided.", true);
995
- let newLocalName = target.localName ?? frontmatterNameForConfig(target);
996
- if (hasKey(cfg, "name")) {
997
- newLocalName = sanitizeName(cfg.name as string);
998
- if (!newLocalName) return result("config.name is invalid after sanitization.", true);
999
- }
1000
- let newPackageName = target.packageName;
1001
- if (hasKey(cfg, "package")) {
1002
- const parsedPackage = parsePackageConfig(cfg.package);
1003
- if (parsedPackage.error) return result(parsedPackage.error, true);
1004
- newPackageName = parsedPackage.packageName;
1005
- }
1006
- const applyError = applyAgentConfig(updated, cfg);
1007
- if (applyError) return result(applyError, true);
1008
- const preserveFrontmatterFields = preservedAgentFrontmatterFields(target, cfg);
1009
- updated.localName = newLocalName;
1010
- if (newPackageName !== undefined) updated.packageName = newPackageName;
1011
- else delete updated.packageName;
1012
- updated.name = buildRuntimeName(newLocalName, newPackageName);
1013
- if (hasKey(cfg, "description")) updated.description = (cfg.description as string).trim();
1014
- if (hasKey(cfg, "model")) {
1015
- const mw = modelWarning(ctx, updated.model);
1016
- if (mw) warnings.push(mw);
1017
- }
1018
- if (hasKey(cfg, "fallbackModels")) {
1019
- const fmw = fallbackModelsWarning(ctx, updated.fallbackModels);
1020
- if (fmw) warnings.push(fmw);
1021
- }
1022
- if (hasKey(cfg, "skills") || hasKey(cfg, "skillPath")) {
1023
- const sw = skillsWarning(ctx.cwd, updated);
1024
- if (sw) warnings.push(sw);
1025
- }
1026
- if (updated.name !== oldName) {
1027
- const renamed = renamePath("agent", target.filePath, updated.name, target.source, ctx.cwd);
1028
- if (renamed.error) return result(renamed.error, true);
1029
- updated.filePath = renamed.filePath!;
1030
- }
1031
- fs.writeFileSync(updated.filePath, serializeAgent(updated, { preserveFrontmatterFields }), "utf-8");
1032
- if (updated.name !== oldName) {
1033
- const refs = discoverAgentsAll(ctx.cwd).chains.filter((c) => c.steps.some((s) => s.agent === oldName)).map((c) => `${c.name} (${c.source})`);
1034
- if (refs.length) warnings.push(`Warning: chains still reference '${oldName}': ${refs.join(", ")}.`);
1035
- }
1036
- const headline = updated.name === oldName
1037
- ? `Updated agent '${updated.name}' at ${updated.filePath}.`
1038
- : `Updated agent '${oldName}' to '${updated.name}' at ${updated.filePath}.`;
1039
- return result([headline, ...warnings].join("\n"));
1040
- }
1041
816
  const scopeHint = asDisambiguationScope(params.agentScope);
1042
- const targetOrError = resolveTarget("chain", params.chainName!, findChains(params.chainName!, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
817
+ const targetOrError = resolveTarget(params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
1043
818
  if ("content" in targetOrError) return targetOrError;
1044
819
  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);
1046
- const updated: ChainConfig = { ...target, steps: [...target.steps] };
820
+ 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);
821
+ let updated: AgentConfig;
822
+ try {
823
+ updated = editableAgentConfig(target);
824
+ } catch (error) {
825
+ const message = error instanceof Error ? error.message : String(error);
826
+ return result(`Could not reread agent definition ${target.filePath} before updating '${target.name}': ${message}`, true);
827
+ }
1047
828
  const oldName = target.name;
1048
829
  if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
1049
830
  if (hasKey(cfg, "description") && (typeof cfg.description !== "string" || !cfg.description.trim())) return result("config.description must be a non-empty string when provided.", true);
@@ -1058,54 +839,46 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
1058
839
  if (parsedPackage.error) return result(parsedPackage.error, true);
1059
840
  newPackageName = parsedPackage.packageName;
1060
841
  }
1061
- let parsedSteps: ChainStepConfig[] | undefined;
1062
- if (hasKey(cfg, "steps")) {
1063
- const parsed = parseStepList(cfg.steps);
1064
- if (parsed.error) return result(parsed.error, true);
1065
- parsedSteps = parsed.steps!;
1066
- }
842
+ const applyError = applyAgentConfig(updated, cfg);
843
+ if (applyError) return result(applyError, true);
844
+ const preserveFrontmatterFields = preservedAgentFrontmatterFields(target, cfg);
1067
845
  updated.localName = newLocalName;
1068
846
  if (newPackageName !== undefined) updated.packageName = newPackageName;
1069
847
  else delete updated.packageName;
1070
848
  updated.name = buildRuntimeName(newLocalName, newPackageName);
1071
849
  if (hasKey(cfg, "description")) updated.description = (cfg.description as string).trim();
1072
- if (parsedSteps) {
1073
- updated.steps = parsedSteps;
1074
- const missing = unknownChainAgents(ctx.cwd, updated.steps);
1075
- if (missing.length) warnings.push(`Warning: chain steps reference unknown agents: ${missing.join(", ")}.`);
1076
- warnings.push(...chainStepWarnings(ctx, updated.steps));
850
+ if (hasKey(cfg, "model")) {
851
+ const mw = modelWarning(ctx, updated.model);
852
+ if (mw) warnings.push(mw);
853
+ }
854
+ if (hasKey(cfg, "fallbackModels")) {
855
+ const fmw = fallbackModelsWarning(ctx, updated.fallbackModels);
856
+ if (fmw) warnings.push(fmw);
857
+ }
858
+ if (hasKey(cfg, "skills") || hasKey(cfg, "skillPath")) {
859
+ const sw = skillsWarning(ctx.cwd, updated);
860
+ if (sw) warnings.push(sw);
1077
861
  }
1078
862
  if (updated.name !== oldName) {
1079
- const renamed = renamePath("chain", target.filePath, updated.name, target.source, ctx.cwd);
863
+ const renamed = renamePath(target.filePath, updated.name, target.source, ctx.cwd);
1080
864
  if (renamed.error) return result(renamed.error, true);
1081
865
  updated.filePath = renamed.filePath!;
1082
866
  }
1083
- fs.writeFileSync(updated.filePath, updated.filePath.endsWith(".chain.json") ? serializeJsonChain(updated) : serializeChain(updated), "utf-8");
867
+ fs.writeFileSync(updated.filePath, serializeAgent(updated, { preserveFrontmatterFields }), "utf-8");
1084
868
  const headline = updated.name === oldName
1085
- ? `Updated chain '${updated.name}' at ${updated.filePath}.`
1086
- : `Updated chain '${oldName}' to '${updated.name}' at ${updated.filePath}.`;
869
+ ? `Updated agent '${updated.name}' at ${updated.filePath}.`
870
+ : `Updated agent '${oldName}' to '${updated.name}' at ${updated.filePath}.`;
1087
871
  return result([headline, ...warnings].join("\n"));
1088
872
  }
1089
873
 
1090
874
  function handleDelete(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
1091
- if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for delete.", true);
1092
- if (params.agent && params.chainName) return result("Specify either 'agent' or 'chainName', not both.", true);
875
+ if (!params.agent) return result("Specify 'agent' for delete.", true);
1093
876
  const scopeHint = asDisambiguationScope(params.agentScope);
1094
- if (params.agent) {
1095
- const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
1096
- if ("content" in targetOrError) return targetOrError;
1097
- const target = targetOrError;
1098
- fs.unlinkSync(target.filePath);
1099
- const refs = discoverAgentsAll(ctx.cwd).chains.filter((c) => c.steps.some((s) => s.agent === target.name)).map((c) => `${c.name} (${c.source})`);
1100
- const lines = [`Deleted agent '${target.name}' at ${target.filePath}.`];
1101
- if (refs.length) lines.push(`Warning: chains reference deleted agent '${target.name}': ${refs.join(", ")}.`);
1102
- return result(lines.join("\n"));
1103
- }
1104
- const targetOrError = resolveTarget("chain", params.chainName!, findChains(params.chainName!, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
877
+ const targetOrError = resolveTarget(params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, scopeHint);
1105
878
  if ("content" in targetOrError) return targetOrError;
1106
879
  const target = targetOrError;
1107
880
  fs.unlinkSync(target.filePath);
1108
- return result(`Deleted chain '${target.name}' at ${target.filePath}.`);
881
+ return result(`Deleted agent '${target.name}' at ${target.filePath}.`);
1109
882
  }
1110
883
 
1111
884
  function handleEject(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
@@ -1118,7 +891,7 @@ function handleEject(params: ManagementParams, ctx: ManagementContext): AgentToo
1118
891
  const d = discoverAgentsAll(ctx.cwd);
1119
892
  const source = [...d.package, ...d.builtin].find((a) => a.name === raw || a.name === sanitized);
1120
893
  if (!source) {
1121
- return result(`Agent '${raw}' not found or is not a bundled/package agent. eject copies a builtin or package agent to ${scope} scope so it can be customized. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`, true);
894
+ return result(`Agent '${raw}' not found or is not a bundled/package agent. eject copies a builtin or package agent to ${scope} scope so it can be customized. Available: ${availableAgentNames(ctx.cwd).join(", ") || "none"}.`, true);
1122
895
  }
1123
896
  const runtimeName = source.name;
1124
897
  const existingCustom = (scope === "user" ? d.user : d.project).find((a) => a.name === runtimeName);
@@ -1126,7 +899,7 @@ function handleEject(params: ManagementParams, ctx: ManagementContext): AgentToo
1126
899
  return result(`Agent '${runtimeName}' is already a custom ${scope} agent at ${existingCustom.filePath}. Edit it with { action: "update", agent: "${runtimeName}" } or delete it first.`, true);
1127
900
  }
1128
901
  if (nameExistsInScope(ctx.cwd, scope, runtimeName)) {
1129
- return result(`An agent or chain named '${runtimeName}' already exists in ${scope} scope. Remove or rename it first.`, true);
902
+ return result(`An agent named '${runtimeName}' already exists in ${scope} scope. Remove or rename it first.`, true);
1130
903
  }
1131
904
  const projectConfigDir = getProjectConfigDir(ctx.cwd);
1132
905
  const targetDir = scope === "user" ? d.userDir : d.projectDir ?? path.join(projectConfigDir, "agents");
@@ -1159,7 +932,7 @@ function handleDisable(params: ManagementParams, ctx: ManagementContext): AgentT
1159
932
  const effective = resolveEffectiveAgent(d, raw);
1160
933
  if (effective.error) return result(effective.error, true);
1161
934
  if (!effective.agent) {
1162
- return result(`Agent '${raw}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`, true);
935
+ return result(`Agent '${raw}' not found. Available: ${availableAgentNames(ctx.cwd).join(", ") || "none"}.`, true);
1163
936
  }
1164
937
  const runtimeName = effective.agent.name;
1165
938
  const settingsPath = mergeBuiltinAgentOverride(ctx.cwd, runtimeName, scope, { disabled: true });
@@ -1183,7 +956,7 @@ function handleEnable(params: ManagementParams, ctx: ManagementContext): AgentTo
1183
956
  const effective = resolveEffectiveAgent(d, raw);
1184
957
  if (effective.error) return result(effective.error, true);
1185
958
  if (!effective.agent) {
1186
- return result(`Agent '${raw}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`, true);
959
+ return result(`Agent '${raw}' not found. Available: ${availableAgentNames(ctx.cwd).join(", ") || "none"}.`, true);
1187
960
  }
1188
961
  const runtimeName = effective.agent.name;
1189
962
  const { path: settingsPath, removed } = removeBuiltinAgentOverrideFields(ctx.cwd, runtimeName, scope, ["disabled"]);
@@ -1215,7 +988,7 @@ function handleReset(params: ManagementParams, ctx: ManagementContext): AgentToo
1215
988
  if (custom) {
1216
989
  return result(`Agent '${raw}' has no bundled default to reset to. Use { action: "delete", agent: "${custom.name}" } to remove the custom ${custom.source} agent.`, true);
1217
990
  }
1218
- return result(`Agent '${raw}' not found. Available: ${availableNames(ctx.cwd, "agent").join(", ") || "none"}.`, true);
991
+ return result(`Agent '${raw}' not found. Available: ${availableAgentNames(ctx.cwd).join(", ") || "none"}.`, true);
1219
992
  }
1220
993
  const runtimeName = bundled.name;
1221
994
  const custom = (scope === "user" ? d.user : d.project).find((a) => a.name === raw || a.name === sanitized);