pi-extensible-workflows 3.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.
- package/README.md +15 -10
- package/dist/src/agent-execution.d.ts +4 -80
- package/dist/src/agent-execution.js +20 -10
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +48 -2
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +597 -0
- package/dist/src/doctor.d.ts +7 -2
- package/dist/src/doctor.js +127 -22
- package/dist/src/host.d.ts +40 -1
- package/dist/src/host.js +517 -129
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -1
- package/dist/src/persistence.d.ts +3 -0
- package/dist/src/persistence.js +49 -1
- package/dist/src/registry.d.ts +21 -9
- package/dist/src/registry.js +131 -21
- package/dist/src/session-inspector.js +4 -2
- package/dist/src/types.d.ts +134 -7
- package/dist/src/utils.d.ts +6 -0
- package/dist/src/utils.js +45 -5
- package/dist/src/validation.d.ts +6 -2
- package/dist/src/validation.js +123 -31
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +50 -16
- package/src/agent-execution.ts +23 -37
- package/src/cli.ts +33 -2
- package/src/doctor-cleanup.ts +337 -0
- package/src/doctor.ts +117 -24
- package/src/host.ts +455 -122
- package/src/index.ts +4 -2
- package/src/persistence.ts +35 -1
- package/src/registry.ts +108 -25
- package/src/session-inspector.ts +4 -2
- package/src/types.ts +52 -7
- package/src/utils.ts +39 -5
- package/src/validation.ts +103 -31
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
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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 &&
|
|
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
|
-
`-
|
|
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"]),
|