pi-extensible-workflows 2.0.0 → 3.1.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 (44) hide show
  1. package/README.md +18 -11
  2. package/dist/src/agent-execution.d.ts +4 -94
  3. package/dist/src/agent-execution.js +34 -208
  4. package/dist/src/budget.d.ts +38 -0
  5. package/dist/src/budget.js +160 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +60 -8
  8. package/dist/src/doctor-cleanup.d.ts +41 -0
  9. package/dist/src/doctor-cleanup.js +597 -0
  10. package/dist/src/doctor.d.ts +7 -2
  11. package/dist/src/doctor.js +127 -22
  12. package/dist/src/execution.d.ts +17 -0
  13. package/dist/src/execution.js +630 -0
  14. package/dist/src/host.d.ts +101 -0
  15. package/dist/src/host.js +3084 -0
  16. package/dist/src/index.d.ts +13 -634
  17. package/dist/src/index.js +10 -4432
  18. package/dist/src/persistence.d.ts +9 -19
  19. package/dist/src/persistence.js +175 -61
  20. package/dist/src/registry.d.ts +43 -0
  21. package/dist/src/registry.js +279 -0
  22. package/dist/src/session-inspector.js +5 -3
  23. package/dist/src/types.d.ts +692 -0
  24. package/dist/src/types.js +27 -0
  25. package/dist/src/utils.d.ts +32 -0
  26. package/dist/src/utils.js +168 -0
  27. package/dist/src/validation.d.ts +28 -0
  28. package/dist/src/validation.js +832 -0
  29. package/package.json +3 -2
  30. package/skills/pi-extensible-workflows/SKILL.md +69 -34
  31. package/src/agent-execution.ts +37 -189
  32. package/src/budget.ts +75 -0
  33. package/src/cli.ts +47 -7
  34. package/src/doctor-cleanup.ts +337 -0
  35. package/src/doctor.ts +117 -24
  36. package/src/execution.ts +540 -0
  37. package/src/host.ts +2598 -0
  38. package/src/index.ts +14 -3856
  39. package/src/persistence.ts +122 -37
  40. package/src/registry.ts +240 -0
  41. package/src/session-inspector.ts +5 -3
  42. package/src/types.ts +158 -0
  43. package/src/utils.ts +142 -0
  44. package/src/validation.ts +688 -0
package/src/doctor.ts CHANGED
@@ -14,22 +14,31 @@ import {
14
14
  DEFAULT_SETTINGS,
15
15
  loadSettings,
16
16
  resolveAgentResourcePolicy,
17
+ resolveWorkflowSettings,
17
18
  resolveModelReference,
19
+ parseThinking,
18
20
  parseRoleMarkdown,
19
21
  registeredWorkflowFunctions,
22
+ registeredWorkflowRoleDirectoryRegistrations,
20
23
  workflowRoleDirectories,
21
24
  workflowProjectSettingsPath,
22
25
  workflowSettingsPath,
23
26
  type AgentResourceExclusions,
24
27
  type AgentResourcePolicy,
28
+ type WorkflowCatalogModelAlias,
29
+ type WorkflowExtensionMetadata,
25
30
  type WorkflowFunction,
31
+ type WorkflowRoleDirectoryRegistration,
26
32
  type WorkflowSettings,
33
+ type WorkflowSettingsSources,
27
34
  } from "./index.js";
28
35
  import type { AgentDefinition } from "./agent-execution.js";
36
+ import { loadingRegistry, type WorkflowRegistryApi } from "./registry.js";
37
+ import { disabledResources, unmatchedResourcePatterns } from "./utils.js";
29
38
 
30
39
  export type DoctorSeverity = "error" | "warning";
31
40
  export interface DoctorDiagnostic { severity: DoctorSeverity; code: string; message: string; source?: string; hint?: string }
32
- export interface DoctorRole { name: string; path: string; scope: "global" | "project"; active: boolean; overrides?: string; overriddenBy?: string }
41
+ export interface DoctorRole { name: string; path: string; scope: "extension" | "global" | "project"; active: boolean; overrides?: string; overriddenBy?: string; extension?: WorkflowExtensionMetadata }
33
42
  export interface DoctorFunction { name: string; description: string; valid: boolean }
34
43
  export interface DoctorTrust { required: boolean; trusted: boolean; source: string }
35
44
  export interface DoctorPiState {
@@ -47,11 +56,13 @@ export interface DoctorReport {
47
56
  agentDir: string;
48
57
  settingsPath: string;
49
58
  settings: Readonly<WorkflowSettings>;
59
+ settingsSources: WorkflowSettingsSources;
50
60
  trust: DoctorTrust;
51
61
  activeTools: readonly string[];
52
62
  roles: readonly DoctorRole[];
53
63
  functions: readonly DoctorFunction[];
54
64
  resourcePolicy: AgentResourcePolicy;
65
+ modelAliases: readonly WorkflowCatalogModelAlias[];
55
66
  diagnostics: readonly DoctorDiagnostic[];
56
67
  }
57
68
  export interface DoctorOptions {
@@ -60,6 +71,7 @@ export interface DoctorOptions {
60
71
  settingsPath?: string;
61
72
  discoverPi?: (cwd: string, agentDir: string) => Promise<DoctorPiState>;
62
73
  activeTools?: readonly string[];
74
+ registry?: WorkflowRegistryApi;
63
75
  }
64
76
 
65
77
  const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
@@ -68,7 +80,11 @@ function canonical(path: string): string {
68
80
  const absolute = resolve(path);
69
81
  try { return realpathSync(absolute); } catch { return absolute; }
70
82
  }
71
-
83
+ function isDynamicModelAlias(value: string, aliases: ReadonlySet<string>): boolean {
84
+ const match = /^([^/\s:]+)(?::([^\s]+))?$/.exec(value);
85
+ const name = match?.[1];
86
+ return Boolean(name && (match[2] === undefined || parseThinking(match[2]) !== undefined) && aliases.has(name));
87
+ }
72
88
  async function readCredentials(agentDir: string): Promise<InMemoryCredentialStore> {
73
89
  const credentials = new InMemoryCredentialStore();
74
90
  try {
@@ -151,7 +167,27 @@ function roleFilesFrom(dirs: readonly string[]): string[] {
151
167
  const paths = dirs.flatMap((dir) => roleFiles(dir));
152
168
  return [...new Map(paths.map((path) => [basename(path, ".md"), path])).values()].sort();
153
169
  }
154
-
170
+ type ExtensionRoleFile = { name: string; path: string; directory: string; extension: WorkflowExtensionMetadata };
171
+ type ExtensionRoleScan = { files: ExtensionRoleFile[]; empty: WorkflowRoleDirectoryRegistration[]; errors: Array<{ registration: WorkflowRoleDirectoryRegistration; error: unknown }> };
172
+ function extensionLabel(extension: WorkflowExtensionMetadata): string { return `Extension "${extension.headline}" (${extension.version})`; }
173
+ function scanExtensionRoleFiles(registrations: readonly WorkflowRoleDirectoryRegistration[]): ExtensionRoleScan {
174
+ const files: ExtensionRoleFile[] = [];
175
+ const empty: WorkflowRoleDirectoryRegistration[] = [];
176
+ const errors: Array<{ registration: WorkflowRoleDirectoryRegistration; error: unknown }> = [];
177
+ for (const registration of registrations) {
178
+ try {
179
+ const entries = readdirSync(registration.path, { withFileTypes: true });
180
+ const roleFiles = entries.filter((entry) => entry.isFile() && extname(entry.name) === ".md");
181
+ if (!roleFiles.length) empty.push(registration);
182
+ for (const entry of roleFiles) files.push({ name: basename(entry.name, ".md"), path: join(registration.path, entry.name), directory: registration.path, extension: registration.extension });
183
+ } catch (error) { errors.push({ registration, error }); }
184
+ }
185
+ files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
186
+ return { files, empty, errors };
187
+ }
188
+ function roleProvenance(source?: { directory: string; extension: WorkflowExtensionMetadata }): string {
189
+ return source ? `${extensionLabel(source.extension)} role directory "${source.directory}"` : "Role";
190
+ }
155
191
  function diagnostic(severity: DoctorSeverity, code: string, message: string, source?: string, hint?: string): DoctorDiagnostic {
156
192
  return { severity, code, message, ...(source ? { source } : {}), ...(hint ? { hint } : {}) };
157
193
  }
@@ -159,7 +195,8 @@ function emptyResourcePolicy(globalSettingsPath: string, cwd: string, projectTru
159
195
  const empty: AgentResourceExclusions = { skills: [], extensions: [] };
160
196
  return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: empty, unmatchedSkills: [], unmatchedExtensions: [] };
161
197
  }
162
- function validateModel(value: string, known: ReadonlySet<string>, available: ReadonlySet<string>, source: string, diagnostics: DoctorDiagnostic[], aliases: Readonly<Record<string, string>>, settingsPath: string): void {
198
+ function validateModel(value: string, known: ReadonlySet<string>, available: ReadonlySet<string>, source: string, diagnostics: DoctorDiagnostic[], aliases: Readonly<Record<string, string>>, dynamicAliases: ReadonlySet<string>, settingsPath: string): void {
199
+ if (isDynamicModelAlias(value, dynamicAliases)) return;
163
200
  try {
164
201
  const parsed = resolveModelReference(value, aliases, known, settingsPath);
165
202
  const name = `${parsed.provider}/${parsed.model}`;
@@ -169,30 +206,29 @@ function validateModel(value: string, known: ReadonlySet<string>, available: Rea
169
206
  }
170
207
  }
171
208
 
172
- function inspectRole(path: string, activeTools: ReadonlySet<string>, knownModels: ReadonlySet<string>, availableModels: ReadonlySet<string>, diagnostics: DoctorDiagnostic[], aliases: Readonly<Record<string, string>>, settingsPath: string): AgentDefinition | undefined {
209
+ function inspectRole(path: string, activeTools: ReadonlySet<string>, knownModels: ReadonlySet<string>, availableModels: ReadonlySet<string>, diagnostics: DoctorDiagnostic[], aliases: Readonly<Record<string, string>>, dynamicAliases: ReadonlySet<string>, settingsPath: string, source?: { directory: string; extension: WorkflowExtensionMetadata }): AgentDefinition | undefined {
173
210
  let definition: AgentDefinition;
174
211
  try { definition = parseRoleMarkdown(readFileSync(path, "utf8"), true, path); }
175
212
  catch (error) {
176
- diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", (error as Error).message, path, "Fix the role YAML frontmatter."));
213
+ const message = error instanceof Error ? error.message : String(error);
214
+ diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", source ? `${roleProvenance(source)} contains invalid role at "${path}": ${message}` : message, path, "Fix the role YAML frontmatter."));
177
215
  return undefined;
178
216
  }
179
217
  const body = definition.prompt ?? "";
180
218
  if (body.trim() === "") diagnostics.push(diagnostic("warning", "ROLE_BODY_EMPTY", "Role body is empty", path));
181
219
  if (Buffer.byteLength(body) > 50 * 1024) diagnostics.push(diagnostic("warning", "ROLE_BODY_LARGE", "Role body exceeds 50KB", path));
182
220
  if (/{{\s*[^{}]+\s*}}/.test(body)) diagnostics.push(diagnostic("warning", "ROLE_PLACEHOLDER", "Role body contains an unsupported placeholder-looking token", path));
183
- if (definition.model) validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, settingsPath);
221
+ if (definition.model) validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, dynamicAliases, settingsPath);
184
222
  for (const tool of definition.tools ?? []) if (!activeTools.has(tool)) diagnostics.push(diagnostic("error", "ROLE_TOOL_INACTIVE", `Tool is unknown or inactive: ${tool}`, path, "Use a tool listed under Active tools or enable its Pi extension."));
185
223
  return definition;
186
224
  }
187
225
 
188
226
  function matchResourcePolicy(policy: AgentResourcePolicy, pi: DoctorPiState): AgentResourcePolicy {
189
- const extensions = new Set((pi.extensions ?? []).map(canonical));
190
- const skills = new Set(pi.skills ?? []);
191
- return { ...policy, unmatchedSkills: policy.effective.skills.filter((name) => !skills.has(name)), unmatchedExtensions: policy.effective.extensions.filter((path) => !extensions.has(canonical(path))) };
192
- }
193
- function resourcePolicySource(policy: AgentResourcePolicy, kind: keyof AgentResourceExclusions, selector: string): string {
194
- return policy.project[kind].includes(selector) && !policy.global[kind].includes(selector) ? policy.projectSettingsPath : policy.globalSettingsPath;
227
+ const extensions = [...new Set((pi.extensions ?? []).map(canonical))];
228
+ const skills = [...new Set(pi.skills ?? [])];
229
+ return { ...policy, excludedSkills: disabledResources(policy.effective.skills, skills), excludedExtensions: disabledResources(policy.effective.extensions, extensions), unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, skills), unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, extensions) };
195
230
  }
231
+ function resourcePolicySource(settingsSource: string): string { return settingsSource; }
196
232
  export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport> {
197
233
  const cwd = canonical(options.cwd ?? process.cwd());
198
234
  const agentDir = canonical(options.agentDir ?? getAgentDir());
@@ -201,6 +237,8 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
201
237
  let settings = DEFAULT_SETTINGS;
202
238
  try { settings = loadSettings(settingsPath); }
203
239
  catch (error) { diagnostics.push(diagnostic("error", "SETTINGS_INVALID", (error as Error).message, settingsPath, "Fix or remove the invalid workflow settings file.")); }
240
+ let settingsSources: WorkflowSettingsSources = { concurrency: settingsPath, modelAliases: settingsPath, disabledAgentResources: settingsPath };
241
+ const projectSettingsPath = workflowProjectSettingsPath(cwd);
204
242
 
205
243
  let pi: DoctorPiState;
206
244
  try { pi = await (options.discoverPi ?? discoverPi)(cwd, agentDir); }
@@ -211,43 +249,91 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
211
249
  if (options.activeTools) pi = { ...pi, activeTools: options.activeTools.filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_catalog") };
212
250
  if (pi.trust.required && !pi.trust.trusted) diagnostics.push(diagnostic("warning", "PROJECT_UNTRUSTED", "Pi project resources are inactive because the project is not trusted", cwd, "Open this project in Pi, choose Trust, then rerun doctor."));
213
251
  for (const error of pi.extensionErrors) diagnostics.push(diagnostic("error", "EXTENSION_LOAD", error.message, error.path, "Fix or disable the failing Pi extension."));
252
+ try {
253
+ const resolved = resolveWorkflowSettings(cwd, pi.trust.trusted, settingsPath);
254
+ settings = resolved.effective;
255
+ settingsSources = resolved.sources;
256
+ } catch (error) {
257
+ const message = error instanceof Error ? error.message : String(error);
258
+ const source = message.includes(projectSettingsPath) ? projectSettingsPath : settingsPath;
259
+ if (!diagnostics.some(({ code, source: itemSource }) => code === "SETTINGS_INVALID" && itemSource === source)) diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, source, "Fix or remove the invalid workflow settings file."));
260
+ }
214
261
  let resourcePolicy: AgentResourcePolicy;
215
262
  try {
216
263
  resourcePolicy = matchResourcePolicy(resolveAgentResourcePolicy(cwd, pi.trust.trusted, settingsPath), pi);
217
264
  } catch (error) {
218
265
  const message = error instanceof Error ? error.message : String(error);
219
- if (!diagnostics.some(({ code, source }) => code === "SETTINGS_INVALID" && source === settingsPath)) diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, message.includes(".json") ? message.match(/(?:settings: )?([^ )]+\.json)/)?.[1] : undefined, "Fix or remove the invalid workflow settings file."));
266
+ const source = message.includes(projectSettingsPath) ? projectSettingsPath : settingsPath;
267
+ if (!diagnostics.some(({ code, source: itemSource }) => code === "SETTINGS_INVALID" && itemSource === source)) diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, source, "Fix or remove the invalid workflow settings file."));
220
268
  resourcePolicy = emptyResourcePolicy(settingsPath, cwd, pi.trust.trusted);
221
269
  }
222
- for (const skill of resourcePolicy.unmatchedSkills) diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled skill selector currently matches no discovered skill: ${skill}`, `${resourcePolicySource(resourcePolicy, "skills", skill)}.disabledAgentResources.skills`));
223
- for (const extension of resourcePolicy.unmatchedExtensions) diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled extension selector currently matches no discovered extension source: ${extension}`, `${resourcePolicySource(resourcePolicy, "extensions", extension)}.disabledAgentResources.extensions`));
270
+ for (const skill of resourcePolicy.unmatchedSkills) diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled skill selector currently matches no discovered skill: ${skill}`, `${resourcePolicySource(settingsSources.disabledAgentResources)}.disabledAgentResources.skills`));
271
+ for (const extension of resourcePolicy.unmatchedExtensions) diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled extension selector currently matches no discovered extension source: ${extension}`, `${resourcePolicySource(settingsSources.disabledAgentResources)}.disabledAgentResources.extensions`));
224
272
 
225
273
  const activeTools = new Set(pi.activeTools);
226
274
  const knownModels = new Set(pi.knownModels);
227
275
  const availableModels = new Set(pi.availableModels);
228
276
  const aliases = settings.modelAliases ?? {};
277
+ const registry = options.registry ?? loadingRegistry();
278
+ const registeredModelAliases = registry.modelAliases();
279
+ const dynamicAliases = new Set(registeredModelAliases.map(({ name }) => name).filter((name) => !Object.prototype.hasOwnProperty.call(aliases, name)));
280
+ const modelAliases: WorkflowCatalogModelAlias[] = [
281
+ ...Object.keys(aliases).map((name) => ({ name, kind: "static" as const, provenance: settingsSources.modelAliases })),
282
+ ...registeredModelAliases.map(({ name, version, headline, extensionDescription }) => ({ name, kind: "dynamic" as const, provenance: `extension: ${headline}`, version, headline, extensionDescription })),
283
+ ].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind));
229
284
  const roles: DoctorRole[] = [];
230
285
  const definitions = new Map<string, AgentDefinition>();
286
+ const extensionScan = scanExtensionRoleFiles(registeredWorkflowRoleDirectoryRegistrations());
287
+ for (const { registration, error } of extensionScan.errors) {
288
+ const message = error instanceof Error ? error.message : String(error);
289
+ diagnostics.push(diagnostic("error", "ROLE_DIRECTORY", `${extensionLabel(registration.extension)} role directory "${registration.path}" could not be scanned: ${message}`, registration.path, "Fix or remove the registered role directory."));
290
+ }
291
+ for (const registration of extensionScan.empty) diagnostics.push(diagnostic("warning", "ROLE_DIRECTORY_EMPTY", `${extensionLabel(registration.extension)} role directory "${registration.path}" contains no .md role files`, registration.path, "Add packaged role files or remove the directory registration."));
292
+ const extensionFilesByName = new Map<string, ExtensionRoleFile[]>();
293
+ for (const file of extensionScan.files) extensionFilesByName.set(file.name, [...(extensionFilesByName.get(file.name) ?? []), file]);
294
+ const duplicateExtensionNames = new Set<string>();
295
+ for (const [name, matches] of extensionFilesByName) if (matches.length > 1) {
296
+ duplicateExtensionNames.add(name);
297
+ diagnostics.push(diagnostic("error", "ROLE_DUPLICATE", `Duplicate extension role "${name}": ${matches.map(({ path, directory, extension }) => `${extensionLabel(extension)} role directory "${directory}" (${path})`).join("; ")}`, matches[0]?.path, "Keep one extension role with this name; global and project roles may override packaged defaults."));
298
+ }
299
+ const extensionPaths = new Map<string, string>();
300
+ for (const file of extensionScan.files) {
301
+ roles.push({ name: file.name, path: file.path, scope: "extension", active: true, extension: file.extension });
302
+ const definition = inspectRole(file.path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, { directory: file.directory, extension: file.extension });
303
+ if (duplicateExtensionNames.has(file.name)) continue;
304
+ extensionPaths.set(file.name, file.path);
305
+ if (definition) definitions.set(file.name, definition);
306
+ }
231
307
  const globalPaths = new Map<string, string>();
232
308
  const globalRoleDirs = workflowRoleDirectories(agentDir);
233
309
  for (const path of roleFilesFrom(globalRoleDirs)) {
234
310
  const name = basename(path, ".md");
235
- roles.push({ name, path, scope: "global", active: true });
311
+ const extensionPath = extensionPaths.get(name);
312
+ roles.push({ name, path, scope: "global", active: true, ...(extensionPath ? { overrides: extensionPath } : {}) });
236
313
  globalPaths.set(name, path);
237
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, settingsPath);
238
- if (definition) definitions.set(name, definition);
314
+ if (extensionPath) {
315
+ const extension = roles.find((role) => role.path === extensionPath);
316
+ if (extension) { extension.active = false; extension.overriddenBy = path; }
317
+ }
318
+ const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
319
+ if (definition) definitions.set(name, definition); else definitions.delete(name);
239
320
  }
240
321
  for (const path of roleFilesFrom([join(cwd, ".pi", "pi-extensible-workflows", "roles")])) {
241
322
  const name = basename(path, ".md");
242
323
  const globalPath = globalPaths.get(name);
324
+ const extensionPath = extensionPaths.get(name);
325
+ const overriddenPath = globalPath ?? extensionPath;
243
326
  const active = pi.trust.trusted;
244
- roles.push({ name, path, scope: "project", active, ...(active && globalPath ? { overrides: globalPath } : {}) });
327
+ roles.push({ name, path, scope: "project", active, ...(active && overriddenPath ? { overrides: overriddenPath } : {}) });
245
328
  if (!active) continue;
246
329
  if (globalPath) {
247
330
  const global = roles.find((role) => role.path === globalPath);
248
331
  if (global) { global.active = false; global.overriddenBy = path; }
332
+ } else if (extensionPath) {
333
+ const extension = roles.find((role) => role.path === extensionPath);
334
+ if (extension) { extension.active = false; extension.overriddenBy = path; }
249
335
  }
250
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, settingsPath);
336
+ const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
251
337
  if (definition) definitions.set(name, definition); else definitions.delete(name);
252
338
  }
253
339
 
@@ -259,7 +345,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
259
345
  const severityOrder: Record<DoctorSeverity, number> = { error: 0, warning: 1 };
260
346
  diagnostics.sort((left, right) => severityOrder[left.severity] - severityOrder[right.severity] || (left.source ?? "").localeCompare(right.source ?? "") || left.code.localeCompare(right.code) || left.message.localeCompare(right.message));
261
347
  roles.sort((left, right) => left.name.localeCompare(right.name) || left.scope.localeCompare(right.scope));
262
- return { cwd, agentDir, settingsPath, settings, trust: pi.trust, activeTools: [...activeTools].sort(), roles, functions, resourcePolicy, diagnostics };
348
+ return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), roles, functions, modelAliases, resourcePolicy, diagnostics };
263
349
  }
264
350
 
265
351
  function count(report: DoctorReport, severity: DoctorSeverity): number { return report.diagnostics.filter((item) => item.severity === severity).length; }
@@ -273,7 +359,9 @@ export function formatDoctorReport(report: DoctorReport): string {
273
359
  "## Environment",
274
360
  `- CWD: \`${report.cwd}\``,
275
361
  `- Agent dir: \`${report.agentDir}\``,
276
- `- Workflow settings: \`${report.settingsPath}\``,
362
+ `- Global workflow settings: \`${report.settingsPath}\``,
363
+ `- Project workflow settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
364
+ `- Effective setting sources: concurrency=\`${report.settingsSources.concurrency}\`, modelAliases=\`${report.settingsSources.modelAliases}\`, disabledAgentResources=\`${report.settingsSources.disabledAgentResources}\``,
277
365
  `- Limits: concurrency=${String(report.settings.concurrency)}`,
278
366
  "",
279
367
  "## Trust/resources",
@@ -288,6 +376,8 @@ export function formatDoctorReport(report: DoctorReport): string {
288
376
  `- Project extensions: ${report.resourcePolicy.project.extensions.join(", ") || "(none)"}`,
289
377
  `- Effective skills: ${report.resourcePolicy.effective.skills.join(", ") || "(none)"}`,
290
378
  `- Effective extensions: ${report.resourcePolicy.effective.extensions.join(", ") || "(none)"}`,
379
+ `- Excluded skills: ${report.resourcePolicy.excludedSkills?.join(", ") || "(none)"}`,
380
+ `- Excluded extensions: ${report.resourcePolicy.excludedExtensions?.join(", ") || "(none)"}`,
291
381
  `- Unmatched skills: ${report.resourcePolicy.unmatchedSkills.join(", ") || "(none)"}`,
292
382
  `- Unmatched extensions: ${report.resourcePolicy.unmatchedExtensions.join(", ") || "(none)"}`,
293
383
  "",
@@ -295,7 +385,10 @@ export function formatDoctorReport(report: DoctorReport): string {
295
385
  ...(report.activeTools.length ? report.activeTools.map((tool) => `- \`${tool}\``) : ["- None resolved"]),
296
386
  "",
297
387
  "## Roles",
298
- ...(report.roles.length ? report.roles.map((role) => `- \`${role.name}\` (${role.scope}, ${role.active ? "active" : role.overriddenBy ? `overridden by ${role.overriddenBy}` : "inactive: project untrusted"}) - \`${role.path}\`${role.overrides ? `; overrides \`${role.overrides}\`` : ""}`) : ["- None found"]),
388
+ ...(report.roles.length ? report.roles.map((role) => `- \`${role.name}\` (${role.scope}, ${role.active ? "active" : role.overriddenBy ? `overridden by ${role.overriddenBy}` : "inactive: project untrusted"}) - \`${role.path}\`${role.extension ? `; ${extensionLabel(role.extension)} role directory "${dirname(role.path)}"` : ""}${role.overrides ? `; overrides \`${role.overrides}\`` : ""}`) : ["- None found"]),
389
+ "",
390
+ "## Model aliases",
391
+ ...(report.modelAliases.length ? report.modelAliases.map((alias) => `- [${alias.kind}] \`${alias.name}\`${alias.kind === "static" ? ` -> ${report.settings.modelAliases?.[alias.name] ?? "(unresolved)"}` : ""} (${alias.provenance})`) : ["- None registered"]),
299
392
  "",
300
393
  "## Reusable functions",
301
394
  ...(report.functions.length ? report.functions.map((fn) => `- [${fn.valid ? "ok" : "error"}] \`${fn.name}\` - ${fn.description}`) : ["- None registered"]),