pi-extensible-workflows 0.3.0 → 1.0.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/src/doctor.ts CHANGED
@@ -14,12 +14,17 @@ import {
14
14
  DEFAULT_SETTINGS,
15
15
  loadSettings,
16
16
  parseModelReference,
17
+ resolveAgentResourcePolicy,
18
+ resolveModelReference,
17
19
  parseRoleMarkdown,
18
20
  preflight,
19
21
  registeredWorkflowDefinitions,
20
22
  workflowRoleDirectories,
23
+ workflowProjectSettingsPath,
21
24
  workflowSettingsPath,
22
25
  WorkflowError,
26
+ type AgentResourceExclusions,
27
+ type AgentResourcePolicy,
23
28
  type WorkflowScriptDefinition,
24
29
  type WorkflowSettings,
25
30
  } from "./index.js";
@@ -36,6 +41,8 @@ export interface DoctorPiState {
36
41
  knownModels: readonly string[];
37
42
  availableModels: readonly string[];
38
43
  extensionErrors: readonly { path?: string; message: string }[];
44
+ extensions?: readonly string[];
45
+ skills?: readonly string[];
39
46
  workflows: Readonly<Record<string, WorkflowScriptDefinition>>;
40
47
  }
41
48
  export interface DoctorReport {
@@ -47,6 +54,7 @@ export interface DoctorReport {
47
54
  activeTools: readonly string[];
48
55
  roles: readonly DoctorRole[];
49
56
  workflows: readonly DoctorWorkflow[];
57
+ resourcePolicy: AgentResourcePolicy;
50
58
  diagnostics: readonly DoctorDiagnostic[];
51
59
  }
52
60
  export interface DoctorOptions {
@@ -107,7 +115,7 @@ async function discoverPi(cwd: string, agentDir: string): Promise<DoctorPiState>
107
115
  agentDir,
108
116
  settingsManager,
109
117
  modelRuntime,
110
- resourceLoaderOptions: { noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true },
118
+ resourceLoaderOptions: { noPromptTemplates: true, noThemes: true, noContextFiles: true },
111
119
  resourceLoaderReloadOptions: { resolveProjectTrust: async () => trusted },
112
120
  });
113
121
  const allModels = services.modelRuntime.getModels();
@@ -117,11 +125,14 @@ async function discoverPi(cwd: string, agentDir: string): Promise<DoctorPiState>
117
125
  const { session } = await createAgentSessionFromServices({ services, sessionManager: SessionManager.inMemory(), model });
118
126
  const activeTools = session.agent.state.tools.map(({ name }) => name).filter((name) => name !== "workflow" && name !== "workflow_respond" && name !== "workflow_catalog");
119
127
  const extensions = services.resourceLoader.getExtensions();
128
+ const skills = services.resourceLoader.getSkills().skills;
120
129
  return {
121
130
  trust: { required, trusted, source },
122
131
  activeTools,
123
132
  knownModels: allModels.map(({ provider, id }) => `${provider}/${id}`),
124
133
  availableModels: availableModels.map(({ provider, id }) => `${provider}/${id}`),
134
+ extensions: extensions.extensions.map(({ resolvedPath }) => resolvedPath),
135
+ skills: skills.map(({ name }) => name),
125
136
  extensionErrors: [
126
137
  ...extensions.errors.map(({ path, error }) => ({ path, message: error })),
127
138
  ...services.diagnostics.filter(({ type }) => type === "error").map(({ message }) => ({ message })),
@@ -147,10 +158,13 @@ function roleFilesFrom(dirs: readonly string[]): string[] {
147
158
  function diagnostic(severity: DoctorSeverity, code: string, message: string, source?: string, hint?: string): DoctorDiagnostic {
148
159
  return { severity, code, message, ...(source ? { source } : {}), ...(hint ? { hint } : {}) };
149
160
  }
150
-
151
- function validateModel(value: string, known: ReadonlySet<string>, available: ReadonlySet<string>, source: string, diagnostics: DoctorDiagnostic[]): void {
161
+ function emptyResourcePolicy(globalSettingsPath: string, cwd: string, projectTrusted: boolean): AgentResourcePolicy {
162
+ const empty: AgentResourceExclusions = { skills: [], extensions: [] };
163
+ return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: empty, unmatchedSkills: [], unmatchedExtensions: [] };
164
+ }
165
+ function validateModel(value: string, known: ReadonlySet<string>, available: ReadonlySet<string>, source: string, diagnostics: DoctorDiagnostic[], aliases: Readonly<Record<string, string>>, settingsPath: string): void {
152
166
  try {
153
- const parsed = parseModelReference(value);
167
+ const parsed = resolveModelReference(value, aliases, known, settingsPath);
154
168
  const name = `${parsed.provider}/${parsed.model}`;
155
169
  if (!known.has(name) || !available.has(name)) diagnostics.push(diagnostic("warning", "MODEL_UNAVAILABLE", `Model is valid-shaped but unavailable: ${name}`, source));
156
170
  } catch (error) {
@@ -158,9 +172,9 @@ function validateModel(value: string, known: ReadonlySet<string>, available: Rea
158
172
  }
159
173
  }
160
174
 
161
- function inspectRole(path: string, activeTools: ReadonlySet<string>, knownModels: ReadonlySet<string>, availableModels: ReadonlySet<string>, diagnostics: DoctorDiagnostic[]): AgentDefinition | undefined {
175
+ function inspectRole(path: string, activeTools: ReadonlySet<string>, knownModels: ReadonlySet<string>, availableModels: ReadonlySet<string>, diagnostics: DoctorDiagnostic[], aliases: Readonly<Record<string, string>>, settingsPath: string): AgentDefinition | undefined {
162
176
  let definition: AgentDefinition;
163
- try { definition = parseRoleMarkdown(readFileSync(path, "utf8"), true); }
177
+ try { definition = parseRoleMarkdown(readFileSync(path, "utf8"), true, path); }
164
178
  catch (error) {
165
179
  diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", (error as Error).message, path, "Fix the role YAML frontmatter."));
166
180
  return undefined;
@@ -169,7 +183,7 @@ function inspectRole(path: string, activeTools: ReadonlySet<string>, knownModels
169
183
  if (body.trim() === "") diagnostics.push(diagnostic("warning", "ROLE_BODY_EMPTY", "Role body is empty", path));
170
184
  if (Buffer.byteLength(body) > 50 * 1024) diagnostics.push(diagnostic("warning", "ROLE_BODY_LARGE", "Role body exceeds 50KB", path));
171
185
  if (/{{\s*[^{}]+\s*}}/.test(body)) diagnostics.push(diagnostic("warning", "ROLE_PLACEHOLDER", "Role body contains an unsupported placeholder-looking token", path));
172
- if (definition.model) validateModel(definition.model, knownModels, availableModels, path, diagnostics);
186
+ if (definition.model) validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, settingsPath);
173
187
  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."));
174
188
  return definition;
175
189
  }
@@ -177,7 +191,14 @@ function inspectRole(path: string, activeTools: ReadonlySet<string>, knownModels
177
191
  class DoctorModelSet extends Set<string> {
178
192
  override has(value: string): boolean { parseModelReference(value); return true; }
179
193
  }
180
-
194
+ function matchResourcePolicy(policy: AgentResourcePolicy, pi: DoctorPiState): AgentResourcePolicy {
195
+ const extensions = new Set((pi.extensions ?? []).map(canonical));
196
+ const skills = new Set(pi.skills ?? []);
197
+ return { ...policy, unmatchedSkills: policy.effective.skills.filter((name) => !skills.has(name)), unmatchedExtensions: policy.effective.extensions.filter((path) => !extensions.has(canonical(path))) };
198
+ }
199
+ function resourcePolicySource(policy: AgentResourcePolicy, kind: keyof AgentResourceExclusions, selector: string): string {
200
+ return policy.project[kind].includes(selector) && !policy.global[kind].includes(selector) ? policy.projectSettingsPath : policy.globalSettingsPath;
201
+ }
181
202
  export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport> {
182
203
  const cwd = canonical(options.cwd ?? process.cwd());
183
204
  const agentDir = canonical(options.agentDir ?? getAgentDir());
@@ -196,10 +217,21 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
196
217
  if (options.activeTools) pi = { ...pi, activeTools: options.activeTools.filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_catalog") };
197
218
  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."));
198
219
  for (const error of pi.extensionErrors) diagnostics.push(diagnostic("error", "EXTENSION_LOAD", error.message, error.path, "Fix or disable the failing Pi extension."));
220
+ let resourcePolicy: AgentResourcePolicy;
221
+ try {
222
+ resourcePolicy = matchResourcePolicy(resolveAgentResourcePolicy(cwd, pi.trust.trusted, settingsPath), pi);
223
+ } catch (error) {
224
+ const message = error instanceof Error ? error.message : String(error);
225
+ 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."));
226
+ resourcePolicy = emptyResourcePolicy(settingsPath, cwd, pi.trust.trusted);
227
+ }
228
+ 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`));
229
+ 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`));
199
230
 
200
231
  const activeTools = new Set(pi.activeTools);
201
232
  const knownModels = new Set(pi.knownModels);
202
233
  const availableModels = new Set(pi.availableModels);
234
+ const aliases = settings.modelAliases ?? {};
203
235
  const roles: DoctorRole[] = [];
204
236
  const definitions = new Map<string, AgentDefinition>();
205
237
  const globalPaths = new Map<string, string>();
@@ -208,7 +240,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
208
240
  const name = basename(path, ".md");
209
241
  roles.push({ name, path, scope: "global", active: true });
210
242
  globalPaths.set(name, path);
211
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics);
243
+ const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, settingsPath);
212
244
  if (definition) definitions.set(name, definition);
213
245
  }
214
246
  for (const path of roleFilesFrom([join(cwd, ".pi", "pi-extensible-workflows", "roles")])) {
@@ -221,7 +253,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
221
253
  const global = roles.find((role) => role.path === globalPath);
222
254
  if (global) { global.active = false; global.overriddenBy = path; }
223
255
  }
224
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics);
256
+ const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, settingsPath);
225
257
  if (definition) definitions.set(name, definition); else definitions.delete(name);
226
258
  }
227
259
 
@@ -233,6 +265,9 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
233
265
  models: new DoctorModelSet(pi.knownModels),
234
266
  tools: activeTools,
235
267
  agentTypes: new Set(definitions.keys()),
268
+ modelAliases: aliases,
269
+ knownModels,
270
+ settingsPath,
236
271
  }, [], { name, description: workflow.description });
237
272
  for (const model of checked.referenced.models) if (!knownModels.has(model) || !availableModels.has(model)) diagnostics.push(diagnostic("warning", "MODEL_UNAVAILABLE", `Model is valid-shaped but unavailable: ${model}`, name));
238
273
  } catch (error) {
@@ -246,7 +281,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
246
281
  const severityOrder: Record<DoctorSeverity, number> = { error: 0, warning: 1 };
247
282
  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));
248
283
  roles.sort((left, right) => left.name.localeCompare(right.name) || left.scope.localeCompare(right.scope));
249
- return { cwd, agentDir, settingsPath, settings, trust: pi.trust, activeTools: [...activeTools].sort(), roles, workflows, diagnostics };
284
+ return { cwd, agentDir, settingsPath, settings, trust: pi.trust, activeTools: [...activeTools].sort(), roles, workflows, resourcePolicy, diagnostics };
250
285
  }
251
286
 
252
287
  function count(report: DoctorReport, severity: DoctorSeverity): number { return report.diagnostics.filter((item) => item.severity === severity).length; }
@@ -261,11 +296,23 @@ export function formatDoctorReport(report: DoctorReport): string {
261
296
  `- CWD: \`${report.cwd}\``,
262
297
  `- Agent dir: \`${report.agentDir}\``,
263
298
  `- Workflow settings: \`${report.settingsPath}\``,
264
- `- Limits: concurrency=${String(report.settings.concurrency)}, maxAgentLaunches=${String(report.settings.maxAgentLaunches)}`,
299
+ `- Limits: concurrency=${String(report.settings.concurrency)}`,
265
300
  "",
266
301
  "## Trust/resources",
267
302
  `- [${report.trust.trusted ? "ok" : "warning"}] ${report.trust.source}`,
268
303
  "",
304
+ "## Agent resource exclusions",
305
+ `- Global settings: \`${report.resourcePolicy.globalSettingsPath}\``,
306
+ `- Global skills: ${report.resourcePolicy.global.skills.join(", ") || "(none)"}`,
307
+ `- Global extensions: ${report.resourcePolicy.global.extensions.join(", ") || "(none)"}`,
308
+ `- Project settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
309
+ `- Project skills: ${report.resourcePolicy.project.skills.join(", ") || "(none)"}`,
310
+ `- Project extensions: ${report.resourcePolicy.project.extensions.join(", ") || "(none)"}`,
311
+ `- Effective skills: ${report.resourcePolicy.effective.skills.join(", ") || "(none)"}`,
312
+ `- Effective extensions: ${report.resourcePolicy.effective.extensions.join(", ") || "(none)"}`,
313
+ `- Unmatched skills: ${report.resourcePolicy.unmatchedSkills.join(", ") || "(none)"}`,
314
+ `- Unmatched extensions: ${report.resourcePolicy.unmatchedExtensions.join(", ") || "(none)"}`,
315
+ "",
269
316
  "## Active tools",
270
317
  ...(report.activeTools.length ? report.activeTools.map((tool) => `- \`${tool}\``) : ["- None resolved"]),
271
318
  "",