@piewf/cli 5.3.0 → 5.5.0-rc.1

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.
@@ -366,8 +366,10 @@ function roleMarkdown(role) {
366
366
  metadata.push(`overrideSystemPrompt: ${String(role.overrideSystemPrompt)}`);
367
367
  if (role.contextFiles !== undefined)
368
368
  metadata.push(`contextFiles: ${JSON.stringify(role.contextFiles)}`);
369
- if (role.disabledAgentResources !== undefined)
370
- metadata.push(`disabledAgentResources: ${JSON.stringify(role.disabledAgentResources)}`);
369
+ if (role.skills !== undefined)
370
+ metadata.push(`skills: ${JSON.stringify(role.skills)}`);
371
+ if (role.extensions !== undefined)
372
+ metadata.push(`extensions: ${JSON.stringify(role.extensions)}`);
371
373
  metadata.push("---");
372
374
  return `${metadata.join("\n")}\n${role.prompt ?? ""}\n`;
373
375
  }
package/dist/src/cli.d.ts CHANGED
@@ -18,6 +18,7 @@ export declare function parseWorkflowCliArgs(schema: JsonSchema, rawArgs: readon
18
18
  export declare function parseDoctorArgs(rawArgs: readonly string[]): {
19
19
  role?: string;
20
20
  prompt?: string;
21
+ json?: boolean;
21
22
  };
22
23
  export declare function parseDoctorCleanupArgs(rawArgs: readonly string[]): Required<Pick<DoctorCleanupOptions, "olderThanDays" | "yes">>;
23
24
  export declare function runCli(args: readonly string[], options?: CliOptions, write?: (text: string) => void): Promise<number>;
package/dist/src/cli.js CHANGED
@@ -241,10 +241,15 @@ function parseInspectArgs(rawArgs) {
241
241
  export function parseDoctorArgs(rawArgs) {
242
242
  let role;
243
243
  let prompt;
244
+ let json = false;
244
245
  for (let index = 0; index < rawArgs.length; index += 1) {
245
246
  const token = requiredArg(rawArgs, index);
246
247
  const equals = token.indexOf("=");
247
248
  const option = equals >= 0 ? token.slice(0, equals) : token;
249
+ if (option === "--json" && equals < 0) {
250
+ json = true;
251
+ continue;
252
+ }
248
253
  if (option === "--role" || option === "--prompt") {
249
254
  const value = equals >= 0 ? token.slice(equals + 1) : rawArgs[++index];
250
255
  if (!value)
@@ -271,7 +276,7 @@ export function parseDoctorArgs(rawArgs) {
271
276
  }
272
277
  if (prompt !== undefined && role === undefined)
273
278
  throw new Error("--prompt requires --role");
274
- return { ...(role === undefined ? {} : { role }), ...(prompt === undefined ? {} : { prompt }) };
279
+ return { ...(role === undefined ? {} : { role }), ...(prompt === undefined ? {} : { prompt }), ...(json ? { json: true } : {}) };
275
280
  }
276
281
  export function parseDoctorCleanupArgs(rawArgs) {
277
282
  let olderThanDays = 90;
@@ -737,13 +742,13 @@ export async function runCli(args, options = {}, write = (text) => { process.std
737
742
  const stderr = options.stderr ?? ((text) => { process.stderr.write(text); });
738
743
  if (args[0] === "doctor" && args[1] !== "cleanup") {
739
744
  if (args.slice(1).some((arg) => arg === "--help" || arg === "-h")) {
740
- write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>]\n");
745
+ write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] [--json]\n");
741
746
  return 0;
742
747
  }
743
748
  try {
744
- const parsed = parseDoctorArgs(args.slice(1));
749
+ const { json, ...parsed } = parseDoctorArgs(args.slice(1));
745
750
  const report = await doctor({ ...options, ...parsed });
746
- write(formatDoctorReport(report));
751
+ write(json ? `${JSON.stringify(report)}\n` : formatDoctorReport(report));
747
752
  return doctorExitCode(report);
748
753
  }
749
754
  catch (error) {
@@ -808,7 +813,7 @@ export async function runCli(args, options = {}, write = (text) => { process.std
808
813
  return 1;
809
814
  }
810
815
  }
811
- write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] | inspect [session-id] [--json|--summary] [--failed] | transcript <session-file> | bundle <workflow-name> [--name <command>] [--output <path>] [--force] | run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]\n");
816
+ write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] [--json] | inspect [session-id] [--json|--summary] [--failed] | transcript <session-file> | bundle <workflow-name> [--name <command>] [--output <path>] [--force] | run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]\n");
812
817
  return 1;
813
818
  }
814
819
  if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
@@ -48,15 +48,18 @@ function model(value, label) { if (!object(value) || typeof value.provider !== "
48
48
  function accounting(value, label) { if (!object(value))
49
49
  throw new Error(`${label} is invalid`); for (const key of ["input", "output", "cacheRead", "cacheWrite", "cost"])
50
50
  finiteNumber(value[key], `${label}.${key}`); }
51
- function resourceExclusions(value, label) { if (value === undefined)
51
+ function resourceSelectors(value, label) { if (value === undefined)
52
52
  return; if (!object(value))
53
53
  throw new Error(`${label} is invalid`); if (value.skills !== undefined)
54
54
  stringList(value.skills, `${label}.skills`); if (value.extensions !== undefined)
55
- stringList(value.extensions, `${label}.extensions`); }
55
+ stringList(value.extensions, `${label}.extensions`); if (value.tools !== undefined)
56
+ stringList(value.tools, `${label}.tools`); }
56
57
  function agentDefinition(value, label) { if (!object(value))
57
58
  throw new Error(`${label} is invalid`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.description, `${label}.description`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !isThinking(value.thinking))
58
59
  throw new Error(`${label}.thinking is invalid`); if (value.tools !== undefined)
59
- stringList(value.tools, `${label}.tools`); resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`); }
60
+ stringList(value.tools, `${label}.tools`); if (value.skills !== undefined)
61
+ stringList(value.skills, `${label}.skills`); if (value.extensions !== undefined)
62
+ stringList(value.extensions, `${label}.extensions`); }
60
63
  function optionalRole(value, label) {
61
64
  if (value === undefined)
62
65
  return;
@@ -78,11 +81,15 @@ function optionalRole(value, label) {
78
81
  throw new Error(`${label}.overrideSystemPrompt is invalid`);
79
82
  if (value.contextFiles !== undefined && value.contextFiles !== null)
80
83
  stringList(value.contextFiles, `${label}.contextFiles`);
81
- if (value.disabledAgentResources !== undefined && value.disabledAgentResources !== null)
82
- resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
84
+ if (value.skills !== undefined && value.skills !== null)
85
+ stringList(value.skills, `${label}.skills`);
86
+ if (value.extensions !== undefined && value.extensions !== null)
87
+ stringList(value.extensions, `${label}.extensions`);
83
88
  }
84
89
  function validateScheduledOptions(value, label) { if (!object(value) || typeof value.label !== "string" || !value.label || typeof value.cwd !== "string" || !value.cwd)
85
- throw new Error(`${label} is invalid`); optionalString(value.requestedLabel, `${label}.requestedLabel`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); stringList(value.tools, `${label}.tools`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !isThinking(value.thinking))
90
+ throw new Error(`${label} is invalid`); optionalString(value.requestedLabel, `${label}.requestedLabel`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); stringList(value.tools, `${label}.tools`); if (value.skills !== undefined)
91
+ stringList(value.skills, `${label}.skills`); if (value.extensions !== undefined)
92
+ stringList(value.extensions, `${label}.extensions`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !isThinking(value.thinking))
86
93
  throw new Error(`${label}.thinking is invalid`); optionalRole(value.role, `${label}.role`); if (value.schema !== undefined)
87
94
  validateSchema(value.schema, `${label}.schema`); if (value.retries !== undefined)
88
95
  nonNegativeInteger(value.retries, `${label}.retries`); if (value.timeoutMs !== undefined && value.timeoutMs !== null)
@@ -104,7 +111,8 @@ function validateAgentSetup(value, label) {
104
111
  stringList(value.tools, `${label}.tools`);
105
112
  if (typeof value.cwd !== "string" || !value.cwd)
106
113
  throw new Error(`${label}.cwd is invalid`);
107
- resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
114
+ if (value.resourceSelectors !== undefined)
115
+ resourceSelectors(value.resourceSelectors, `${label}.resourceSelectors`);
108
116
  }
109
117
  function validateAgent(value, label) {
110
118
  if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.name !== "string" || !value.name || typeof value.path !== "string" || !value.path || !isAgentState(value.state))
@@ -254,17 +262,18 @@ function validateSnapshot(snapshot) {
254
262
  positiveInteger(snapshot.identityVersion, "Persisted snapshot identity version");
255
263
  optionalString(snapshot.settingsPath, "Persisted snapshot settings path");
256
264
  if (snapshot.settingsSources !== undefined) {
257
- if (!object(snapshot.settingsSources) || typeof snapshot.settingsSources.concurrency !== "string" || typeof snapshot.settingsSources.modelAliases !== "string" || typeof snapshot.settingsSources.disabledAgentResources !== "string")
265
+ if (!object(snapshot.settingsSources) || typeof snapshot.settingsSources.concurrency !== "string" || typeof snapshot.settingsSources.modelAliases !== "string" || snapshot.settingsSources.skills !== undefined && typeof snapshot.settingsSources.skills !== "string" || snapshot.settingsSources.extensions !== undefined && typeof snapshot.settingsSources.extensions !== "string" || snapshot.settingsSources.tools !== undefined && typeof snapshot.settingsSources.tools !== "string")
258
266
  throw new Error("Persisted snapshot settings sources are invalid");
259
267
  }
268
+ const settingsRecord = snapshot.settings;
269
+ resourceSelectors({ skills: settingsRecord.skills, extensions: settingsRecord.extensions, tools: settingsRecord.tools }, "Persisted snapshot selectors");
260
270
  validateBudget(snapshot.budget);
261
271
  if (snapshot.modelAliases !== undefined)
262
272
  validateModelAliases(snapshot.modelAliases);
263
- if (snapshot.settings.modelAliases !== undefined)
264
- validateModelAliases(snapshot.settings.modelAliases);
273
+ if (settingsRecord.modelAliases !== undefined)
274
+ validateModelAliases(settingsRecord.modelAliases);
265
275
  if (snapshot.phases !== undefined)
266
276
  stringList(snapshot.phases, "Persisted snapshot phases");
267
- resourceExclusions(snapshot.settings.disabledAgentResources, "Persisted snapshot disabled resources");
268
277
  if (snapshot.roles !== undefined) {
269
278
  if (!object(snapshot.roles))
270
279
  throw new Error("Persisted snapshot roles are invalid");
@@ -38,12 +38,18 @@ export interface DoctorRoleInspection {
38
38
  };
39
39
  tools: readonly string[];
40
40
  resources: {
41
+ selectors: {
42
+ skills: readonly string[];
43
+ extensions: readonly string[];
44
+ tools: readonly string[];
45
+ };
41
46
  skills: readonly string[];
42
47
  extensions: readonly string[];
43
- excludedSkills: readonly string[];
44
- excludedExtensions: readonly string[];
48
+ tools: readonly string[];
45
49
  unmatchedSkills: readonly string[];
46
50
  unmatchedExtensions: readonly string[];
51
+ unmatchedTools: readonly string[];
52
+ selectorSources?: NonNullable<AgentResourcePolicy["selectorSources"]>;
47
53
  };
48
54
  systemPrompt: {
49
55
  probe: string;
@@ -1,11 +1,30 @@
1
1
  import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
2
  import { basename, dirname, extname, join, resolve } from "node:path";
3
- import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
3
+ import { InMemoryCredentialStore, InMemoryModelsStore } from "@earendil-works/pi-ai";
4
4
  import { ModelRuntime, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
5
- import { DEFAULT_SETTINGS, createLocalPiSession, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
5
+ import { DEFAULT_SETTINGS, createLocalPiSession, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, resourcePatternHasMagic, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
6
6
  import { loadingRegistry } from "pi-extensible-workflows";
7
- import { disabledResources, unmatchedResourcePatterns } from "pi-extensible-workflows";
7
+ import { selectResourcesByLayers, unmatchedResourcePatterns } from "pi-extensible-workflows";
8
8
  const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
9
+ const AGENT_RESOURCE_SELECTOR_MIGRATION_ISSUE = "https://github.com/vekexasia/pi-extensible-workflows/issues/205";
10
+ const AGENT_RESOURCE_SELECTOR_MIGRATION_MESSAGE = `\`disabledAgentResources\` is no longer supported by #205. Migrate to direct \`skills\`, \`extensions\`, and \`tools\` selectors: legacy patterns exclude resources and \`!pattern\` re-enables them, while new selectors include matches and \`!pattern\` excludes them. See ${AGENT_RESOURCE_SELECTOR_MIGRATION_ISSUE}`;
11
+ function usesLegacySettings(path) {
12
+ try {
13
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
14
+ return isObject(parsed) && Object.prototype.hasOwnProperty.call(parsed, "disabledAgentResources");
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ function usesLegacyRoleSelectors(path) {
21
+ try {
22
+ return /^\s*disabledAgentResources\s*:/m.test(readFileSync(path, "utf8"));
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
9
28
  function canonical(path) {
10
29
  const absolute = resolve(path);
11
30
  try {
@@ -73,7 +92,7 @@ async function discoverPi(cwd, agentDir) {
73
92
  const previousOffline = process.env.PI_OFFLINE;
74
93
  process.env.PI_OFFLINE = "1";
75
94
  try {
76
- const modelRuntime = await ModelRuntime.create({ credentials: await readCredentials(agentDir), modelsPath: join(agentDir, "models.json") });
95
+ const modelRuntime = await ModelRuntime.create({ credentials: await readCredentials(agentDir), modelsPath: join(agentDir, "models.json"), modelsStore: new InMemoryModelsStore() });
77
96
  const services = await createAgentSessionServices({
78
97
  cwd,
79
98
  agentDir,
@@ -170,9 +189,17 @@ function roleProvenance(source) {
170
189
  function diagnostic(severity, code, message, source, hint) {
171
190
  return { severity, code, message, ...(source ? { source } : {}), ...(hint ? { hint } : {}) };
172
191
  }
192
+ function legacyAgentResourceSelectorDiagnostic(source) {
193
+ return diagnostic("error", "AGENT_RESOURCE_SELECTOR_MIGRATION", AGENT_RESOURCE_SELECTOR_MIGRATION_MESSAGE, source, "Replace disabledAgentResources with direct selectors and use !* before positive allow-list patterns.");
194
+ }
195
+ function positiveOnlyToolSelectorDiagnostic(source, selectors) {
196
+ if (!selectors?.length || selectors.some((selector) => selector.startsWith("!")))
197
+ return undefined;
198
+ return diagnostic("warning", "AGENT_RESOURCE_TOOL_SELECTOR_ALLOWLIST", "Positive-only tool selectors do not restrict the default-enabled candidate set.", `${source}.tools`, "Prepend !* before positive patterns to make this an allow-list.");
199
+ }
173
200
  function emptyResourcePolicy(globalSettingsPath, cwd, projectTrusted) {
174
- const empty = { skills: [], extensions: [] };
175
- return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: empty, unmatchedSkills: [], unmatchedExtensions: [] };
201
+ const empty = { skills: [], extensions: [], tools: [] };
202
+ return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: empty, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: { global: {}, project: {} } };
176
203
  }
177
204
  function validateModel(value, known, available, source, diagnostics, aliases, dynamicAliases, settingsPath) {
178
205
  if (isDynamicModelAlias(value, dynamicAliases))
@@ -194,10 +221,17 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
194
221
  definition = parseRoleMarkdown(readFileSync(path, "utf8"), true, path);
195
222
  }
196
223
  catch (error) {
224
+ if (usesLegacyRoleSelectors(path)) {
225
+ diagnostics.push(legacyAgentResourceSelectorDiagnostic(path));
226
+ return undefined;
227
+ }
197
228
  const message = error instanceof Error ? error.message : String(error);
198
229
  diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", source ? `${roleProvenance(source)} contains invalid role at "${path}": ${message}` : message, path, "Fix the role YAML frontmatter."));
199
230
  return undefined;
200
231
  }
232
+ const toolSelectorDiagnostic = positiveOnlyToolSelectorDiagnostic(path, definition.tools);
233
+ if (toolSelectorDiagnostic)
234
+ diagnostics.push(toolSelectorDiagnostic);
201
235
  const body = definition.prompt ?? "";
202
236
  if (body.trim() === "")
203
237
  diagnostics.push(diagnostic("warning", "ROLE_BODY_EMPTY", "Role body is empty", path));
@@ -207,15 +241,22 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
207
241
  diagnostics.push(diagnostic("warning", "ROLE_PLACEHOLDER", "Role body contains an unsupported placeholder-looking token", path));
208
242
  if (definition.model)
209
243
  validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, dynamicAliases, settingsPath);
210
- for (const tool of definition.tools ?? [])
211
- if (!activeTools.has(tool))
244
+ for (const selector of definition.tools ?? []) {
245
+ const tool = selector.startsWith("!") ? selector.slice(1) : selector;
246
+ if (!selector.startsWith("!") && !resourcePatternHasMagic(selector) && !activeTools.has(tool))
212
247
  diagnostics.push(diagnostic("error", "ROLE_TOOL_INACTIVE", `Tool is unknown or inactive: ${tool}`, path, "Use a tool listed under Pi active tools or enable its Pi extension."));
248
+ }
213
249
  return definition;
214
250
  }
215
251
  function matchResourcePolicy(policy, pi) {
216
252
  const extensions = [...new Set((pi.extensions ?? []).map(canonical))];
217
253
  const skills = [...new Set(pi.skills ?? [])];
218
- 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) };
254
+ const tools = [...new Set(pi.activeTools)];
255
+ const layers = policy.selectorSources;
256
+ const selectedSkills = selectResourcesByLayers([layers.global.skills, layers.project.skills], skills);
257
+ const selectedExtensions = selectResourcesByLayers([layers.global.extensions, layers.project.extensions], extensions);
258
+ const selectedTools = selectResourcesByLayers([layers.global.tools, layers.project.tools], tools);
259
+ return { ...policy, selectedSkills, selectedExtensions, selectedTools, unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, skills), unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, extensions), unmatchedTools: unmatchedResourcePatterns(policy.effective.tools ?? [], tools) };
219
260
  }
220
261
  async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath, basePolicy, rootModel, activeTools, aliases, knownModels, availableModels, settingsPath, prompt, hooks, diagnostics) {
221
262
  const setupDiagnostics = [];
@@ -260,7 +301,7 @@ async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath,
260
301
  const policy = prepared.setup.sessionInput.resourcePolicy ?? basePolicy;
261
302
  for (const item of [...resources.diagnostics, ...promptResult.diagnostics])
262
303
  setupDiagnostics.push(diagnostic(item.type === "error" ? "error" : "warning", "ROLE_INSPECTION", item.message, item.source));
263
- return { role: roleName, path: rolePath, model: actualModel, tools: state?.tools.map(({ name }) => name) ?? [...prepared.setup.sessionInput.tools], resources: { skills: resources.skills, extensions: resources.extensions, excludedSkills: policy.excludedSkills ?? [], excludedExtensions: policy.excludedExtensions ?? [], unmatchedSkills: policy.unmatchedSkills, unmatchedExtensions: policy.unmatchedExtensions }, systemPrompt: { probe: prompt, expandedProbe: promptResult.expandedPrompt, text: promptResult.systemPrompt, ...(resources.systemPromptSource ? { source: resources.systemPromptSource } : {}) }, setup: { hooks: prepared.summary.hookNames, diagnostics: setupDiagnostics } };
304
+ return { role: roleName, path: rolePath, model: actualModel, tools: state?.tools.map(({ name }) => name) ?? [...prepared.setup.sessionInput.tools], resources: { selectors: { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], tools: [...(policy.effective.tools ?? [])] }, skills: policy.selectedSkills ?? resources.skills, extensions: policy.selectedExtensions ?? resources.extensions, tools: policy.selectedTools ?? prepared.setup.sessionInput.tools, unmatchedSkills: policy.unmatchedSkills, unmatchedExtensions: policy.unmatchedExtensions, unmatchedTools: policy.unmatchedTools ?? [], selectorSources: policy.selectorSources }, systemPrompt: { probe: prompt, expandedProbe: promptResult.expandedPrompt, text: promptResult.systemPrompt, ...(resources.systemPromptSource ? { source: resources.systemPromptSource } : {}) }, setup: { hooks: prepared.summary.hookNames, diagnostics: setupDiagnostics } };
264
305
  }
265
306
  catch (error) {
266
307
  setupDiagnostics.push(diagnostic("error", "ROLE_INSPECTION", error instanceof Error ? error.message : String(error), rolePath));
@@ -276,6 +317,8 @@ export async function doctor(options = {}) {
276
317
  const cwd = canonical(options.cwd ?? process.cwd());
277
318
  const agentDir = canonical(options.agentDir ?? getAgentDir());
278
319
  const settingsPath = canonical(options.settingsPath ?? workflowSettingsPath(agentDir));
320
+ const projectSettingsPath = workflowProjectSettingsPath(cwd);
321
+ const legacyGlobalSettings = usesLegacySettings(settingsPath);
279
322
  const diagnostics = [];
280
323
  let settings = DEFAULT_SETTINGS;
281
324
  try {
@@ -284,8 +327,7 @@ export async function doctor(options = {}) {
284
327
  catch (error) {
285
328
  diagnostics.push(diagnostic("error", "SETTINGS_INVALID", errorText(error), settingsPath, "Fix or remove the invalid workflow settings file."));
286
329
  }
287
- let settingsSources = { concurrency: settingsPath, modelAliases: settingsPath, disabledAgentResources: settingsPath };
288
- const projectSettingsPath = workflowProjectSettingsPath(cwd);
330
+ let settingsSources = { concurrency: settingsPath, modelAliases: settingsPath, skills: settingsPath, extensions: settingsPath, tools: settingsPath };
289
331
  let pi;
290
332
  try {
291
333
  pi = await (options.discoverPi ?? discoverPi)(cwd, agentDir);
@@ -298,6 +340,11 @@ export async function doctor(options = {}) {
298
340
  pi = { ...pi, activeTools: options.activeTools.filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_catalog") };
299
341
  if (pi.trust.required && !pi.trust.trusted)
300
342
  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."));
343
+ const legacyProjectSettings = pi.trust.trusted && usesLegacySettings(projectSettingsPath);
344
+ if (legacyGlobalSettings)
345
+ diagnostics.push(legacyAgentResourceSelectorDiagnostic(`${settingsPath}.disabledAgentResources`));
346
+ if (legacyProjectSettings)
347
+ diagnostics.push(legacyAgentResourceSelectorDiagnostic(`${projectSettingsPath}.disabledAgentResources`));
301
348
  for (const error of pi.extensionErrors)
302
349
  diagnostics.push(diagnostic("error", "EXTENSION_LOAD", error.message, error.path, "Fix or disable the failing Pi extension."));
303
350
  try {
@@ -322,10 +369,17 @@ export async function doctor(options = {}) {
322
369
  diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, source, "Fix or remove the invalid workflow settings file."));
323
370
  resourcePolicy = emptyResourcePolicy(settingsPath, cwd, pi.trust.trusted);
324
371
  }
372
+ for (const [source, selectors] of [[resourcePolicy.globalSettingsPath, resourcePolicy.selectorSources.global.tools], [resourcePolicy.projectSettingsPath, resourcePolicy.selectorSources.project.tools]]) {
373
+ const toolSelectorDiagnostic = positiveOnlyToolSelectorDiagnostic(source, selectors);
374
+ if (toolSelectorDiagnostic)
375
+ diagnostics.push(toolSelectorDiagnostic);
376
+ }
325
377
  for (const skill of resourcePolicy.unmatchedSkills)
326
- diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled skill selector currently matches no discovered skill: ${skill}`, `${resourcePolicySource(settingsSources.disabledAgentResources)}.disabledAgentResources.skills`));
378
+ diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Skill selector currently matches no discovered skill: ${skill}`, `${resourcePolicySource(settingsSources.skills ?? settingsPath)}.skills`));
327
379
  for (const extension of resourcePolicy.unmatchedExtensions)
328
- diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled extension selector currently matches no discovered extension source: ${extension}`, `${resourcePolicySource(settingsSources.disabledAgentResources)}.disabledAgentResources.extensions`));
380
+ diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Extension selector currently matches no discovered extension source: ${extension}`, `${resourcePolicySource(settingsSources.extensions ?? settingsPath)}.extensions`));
381
+ for (const tool of resourcePolicy.unmatchedTools ?? [])
382
+ diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Tool selector currently matches no root tool: ${tool}`, `${resourcePolicySource(settingsSources.tools ?? settingsPath)}.tools`));
329
383
  const activeTools = new Set(pi.activeTools);
330
384
  const knownModels = new Set(pi.knownModels);
331
385
  const availableModels = new Set(pi.availableModels);
@@ -414,6 +468,9 @@ export async function doctor(options = {}) {
414
468
  else
415
469
  definitions.delete(name);
416
470
  }
471
+ const rolePaths = new Set(roles.map(({ path }) => path));
472
+ if (diagnostics.some(({ code, source }) => source !== undefined && rolePaths.has(source) && (code === "ROLE_FRONTMATTER" || code === "AGENT_RESOURCE_SELECTOR_MIGRATION")))
473
+ diagnostics.push(diagnostic("error", "ROLE_LOAD_BLOCKED", "Workflow role loading is blocked because the runtime rejects the complete role set when any active role file is invalid.", undefined, "Fix the reported role file before launching workflows."));
417
474
  let roleInspection;
418
475
  if (options.role !== undefined) {
419
476
  const activeRole = roles.find(({ name, active }) => name === options.role && active);
@@ -459,24 +516,44 @@ export async function doctor(options = {}) {
459
516
  }
460
517
  function count(report, severity) { return report.diagnostics.filter((item) => item.severity === severity).length; }
461
518
  export function doctorExitCode(report) { return count(report, "error") > 0 ? 1 : 0; }
462
- function exposedValues(values, excluded) {
463
- const disabled = new Set(excluded);
464
- return values.filter((value) => !disabled.has(value));
465
- }
466
519
  function nestedValues(label, values) {
467
520
  return [`- ${label}:`, ...(values.length ? values.map((value) => ` - \`${value}\``) : [" - (none)"])];
468
521
  }
522
+ function roleSelectorSourceLines(sources) {
523
+ return [
524
+ ...nestedValues("Global skill selectors", sources.global.skills ?? []),
525
+ ...nestedValues("Global extension selectors", sources.global.extensions ?? []),
526
+ ...nestedValues("Global tool selectors", sources.global.tools ?? []),
527
+ ...nestedValues("Project skill selectors", sources.project.skills ?? []),
528
+ ...nestedValues("Project extension selectors", sources.project.extensions ?? []),
529
+ ...nestedValues("Project tool selectors", sources.project.tools ?? []),
530
+ ...(sources.role === undefined ? [] : [
531
+ ...nestedValues("Role skill selectors", sources.role.skills ?? []),
532
+ ...nestedValues("Role extension selectors", sources.role.extensions ?? []),
533
+ ...nestedValues("Role tool selectors", sources.role.tools ?? []),
534
+ ]),
535
+ ...(sources.call === undefined ? [] : [
536
+ ...nestedValues("Call skill selectors", sources.call.skills ?? []),
537
+ ...nestedValues("Call extension selectors", sources.call.extensions ?? []),
538
+ ...nestedValues("Call tool selectors", sources.call.tools ?? []),
539
+ ]),
540
+ ];
541
+ }
469
542
  function roleInspectionLines(inspection) {
470
543
  return [
471
544
  `- Role: \`${inspection.role}\` - \`${inspection.path}\``,
472
545
  `- Model: \`${inspection.model.provider}/${inspection.model.model}\` (${inspection.model.inherited ? "inherited, " : ""}${inspection.model.thinking ?? "off"})`,
546
+ ...(inspection.resources.selectorSources ? roleSelectorSourceLines(inspection.resources.selectorSources) : []),
473
547
  ...nestedValues("Tools", inspection.tools),
548
+ ...nestedValues("Configured skill selectors", inspection.resources.selectors.skills),
474
549
  ...nestedValues("Effective skills", inspection.resources.skills),
550
+ ...nestedValues("Configured extension selectors", inspection.resources.selectors.extensions),
475
551
  ...nestedValues("Effective extensions", inspection.resources.extensions),
476
- ...nestedValues("Excluded skills", inspection.resources.excludedSkills),
477
- ...nestedValues("Excluded extensions", inspection.resources.excludedExtensions),
552
+ ...nestedValues("Configured tool selectors", inspection.resources.selectors.tools),
553
+ ...nestedValues("Effective tools", inspection.resources.tools),
478
554
  ...nestedValues("Unmatched skills", inspection.resources.unmatchedSkills),
479
555
  ...nestedValues("Unmatched extensions", inspection.resources.unmatchedExtensions),
556
+ ...nestedValues("Unmatched tools", inspection.resources.unmatchedTools),
480
557
  `- Prompt probe: ${inspection.systemPrompt.probe ? JSON.stringify(inspection.systemPrompt.probe) : "empty"}`,
481
558
  `- Expanded probe: ${JSON.stringify(inspection.systemPrompt.expandedProbe)}`,
482
559
  `- System prompt source: ${inspection.systemPrompt.source ?? "(none)"}`,
@@ -504,6 +581,7 @@ export function formatDoctorReport(report) {
504
581
  ];
505
582
  return `${lines.join("\n")}\n`;
506
583
  }
584
+ const roleLoadingFailed = report.diagnostics.some(({ code }) => code === "ROLE_LOAD_BLOCKED");
507
585
  const lines = [
508
586
  "# pi-extensible-workflows doctor",
509
587
  "",
@@ -512,7 +590,7 @@ export function formatDoctorReport(report) {
512
590
  `- Agent dir: \`${report.agentDir}\``,
513
591
  `- Global workflow settings: \`${report.settingsPath}\``,
514
592
  `- Project workflow settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
515
- `- Effective setting sources: concurrency=\`${report.settingsSources.concurrency}\`, modelAliases=\`${report.settingsSources.modelAliases}\`, disabledAgentResources=\`${report.settingsSources.disabledAgentResources}\``,
593
+ `- Effective setting sources: concurrency=\`${report.settingsSources.concurrency}\`, modelAliases=\`${report.settingsSources.modelAliases}\`, skills=\`${report.settingsSources.skills ?? "(none)"}\`, extensions=\`${report.settingsSources.extensions ?? "(none)"}\`, tools=\`${report.settingsSources.tools ?? "(none)"}\``,
516
594
  `- Limits: concurrency=${String(report.settings.concurrency)}`,
517
595
  "",
518
596
  "## Trust/resources",
@@ -527,24 +605,24 @@ export function formatDoctorReport(report) {
527
605
  "## Pi active skills",
528
606
  ...(report.piSkills.length ? report.piSkills.map((skill) => `- \`${skill}\``) : ["- None resolved"]),
529
607
  "",
530
- "## Workflow agent resources (after disabledAgentResources)",
608
+ "## Workflow agent resource selectors",
531
609
  `- Global settings: \`${report.resourcePolicy.globalSettingsPath}\``,
532
610
  `- Global skills: ${report.resourcePolicy.global.skills.join(", ") || "(none)"}`,
533
611
  `- Global extensions: ${report.resourcePolicy.global.extensions.join(", ") || "(none)"}`,
612
+ `- Global tools: ${(report.resourcePolicy.global.tools ?? []).join(", ") || "(none)"}`,
534
613
  `- Project settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
535
614
  `- Project skills: ${report.resourcePolicy.project.skills.join(", ") || "(none)"}`,
536
615
  `- Project extensions: ${report.resourcePolicy.project.extensions.join(", ") || "(none)"}`,
537
- `- Effective skills: ${report.resourcePolicy.effective.skills.join(", ") || "(none)"}`,
538
- `- Effective extensions: ${report.resourcePolicy.effective.extensions.join(", ") || "(none)"}`,
539
- ...nestedValues("Exposed skills", exposedValues(report.piSkills, report.resourcePolicy.excludedSkills ?? [])),
540
- ...nestedValues("Exposed extensions", exposedValues(report.piExtensions, report.resourcePolicy.excludedExtensions ?? [])),
541
- ...nestedValues("Disabled skills", report.resourcePolicy.excludedSkills ?? []),
542
- ...nestedValues("Disabled extensions", report.resourcePolicy.excludedExtensions ?? []),
616
+ `- Project tools: ${(report.resourcePolicy.project.tools ?? []).join(", ") || "(none)"}`,
617
+ `- Effective skills: ${(report.resourcePolicy.selectedSkills ?? []).join(", ") || "(none)"}`,
618
+ `- Effective extensions: ${(report.resourcePolicy.selectedExtensions ?? []).join(", ") || "(none)"}`,
619
+ `- Effective tools: ${(report.resourcePolicy.selectedTools ?? []).join(", ") || "(none)"}`,
543
620
  `- Unmatched skills: ${report.resourcePolicy.unmatchedSkills.join(", ") || "(none)"}`,
544
621
  `- Unmatched extensions: ${report.resourcePolicy.unmatchedExtensions.join(", ") || "(none)"}`,
622
+ `- Unmatched tools: ${(report.resourcePolicy.unmatchedTools ?? []).join(", ") || "(none)"}`,
545
623
  "",
546
624
  "## Roles",
547
- ...(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"]),
625
+ ...(report.roles.length ? report.roles.map((role) => `- \`${role.name}\` (${role.scope}, ${role.active ? roleLoadingFailed ? "unavailable: role loading failed" : "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"]),
548
626
  "",
549
627
  "## Model aliases",
550
628
  ...(report.modelAliases.length ? report.modelAliases.map((alias) => `- [${alias.kind}] \`${alias.name}\`${alias.kind === "static" ? ` -> ${report.settings.modelAliases?.[alias.name] ?? "(unresolved)"}` : ""} (${alias.provenance})`) : ["- None registered"]),
@@ -304,13 +304,17 @@ function detailLines(workflow) {
304
304
  lines.push(`Attempt ${String(attempt.attempt)} · ${attempt.model}${attempt.thinking !== undefined ? `:${attempt.thinking}` : ""} · ${money(attempt.cost)}${attempt.error ? ` · ${attempt.error}` : ""}`, `Prompt: ${attempt.prompt}`, ...(attempt.setup ? [
305
305
  `Hooks: ${attempt.setup.hookNames.join(", ") || "(none)"}`,
306
306
  `Effective: model=${attempt.setup.model.provider}/${attempt.setup.model.model}${attempt.setup.model.thinking ? `:${attempt.setup.model.thinking}` : ""} tools=${attempt.setup.tools.join(",") || "(none)"} cwd=${attempt.setup.cwd}`,
307
- ...(attempt.setup.disabledAgentResources ? [
308
- `Configured skill patterns: ${attempt.setup.disabledAgentResources.skills.join(", ") || "(none)"}`,
309
- `Excluded skills: ${attempt.setup.disabledAgentResources.excludedSkills?.join(", ") || "(none)"}`,
310
- `Configured extension patterns: ${attempt.setup.disabledAgentResources.extensions.join(", ") || "(none)"}`,
311
- `Excluded extensions: ${attempt.setup.disabledAgentResources.excludedExtensions?.join(", ") || "(none)"}`,
312
- `Unmatched skills: ${attempt.setup.disabledAgentResources.unmatchedSkills.join(", ") || "(none)"}`,
313
- `Unmatched extensions: ${attempt.setup.disabledAgentResources.unmatchedExtensions.join(", ") || "(none)"}`,
307
+ ...(attempt.setup.resourceSelectors ? [
308
+ `Selector sources: global/project/role/call are persisted with the attempt`,
309
+ `Configured skill patterns: ${attempt.setup.resourceSelectors.selectors.skills.join(", ") || "(none)"}`,
310
+ `Effective skills: ${attempt.setup.resourceSelectors.skills.join(", ") || "(none)"}`,
311
+ `Configured extension patterns: ${attempt.setup.resourceSelectors.selectors.extensions.join(", ") || "(none)"}`,
312
+ `Effective extensions: ${attempt.setup.resourceSelectors.extensions.join(", ") || "(none)"}`,
313
+ `Configured tool patterns: ${(attempt.setup.resourceSelectors.selectors.tools ?? []).join(", ") || "(none)"}`,
314
+ `Effective tools: ${attempt.setup.resourceSelectors.tools.join(", ") || "(none)"}`,
315
+ `Unmatched skills: ${attempt.setup.resourceSelectors.unmatchedSkills.join(", ") || "(none)"}`,
316
+ `Unmatched extensions: ${attempt.setup.resourceSelectors.unmatchedExtensions.join(", ") || "(none)"}`,
317
+ `Unmatched tools: ${attempt.setup.resourceSelectors.unmatchedTools.join(", ") || "(none)"}`,
314
318
  ] : []),
315
319
  ] : []));
316
320
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piewf/cli",
3
- "version": "5.3.0",
3
+ "version": "5.5.0-rc.1",
4
4
  "description": "CLI for pi-extensible-workflows",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": {
@@ -33,8 +33,8 @@
33
33
  "typebox": "*"
34
34
  },
35
35
  "devDependencies": {
36
- "@earendil-works/pi-ai": "0.82.1",
37
- "@earendil-works/pi-coding-agent": "0.82.1",
36
+ "@earendil-works/pi-ai": "0.84.1",
37
+ "@earendil-works/pi-coding-agent": "0.84.1",
38
38
  "@eslint/js": "10.0.1",
39
39
  "@types/node": "24.12.4",
40
40
  "eslint": "10.0.3",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "license": "MIT",
48
48
  "dependencies": {
49
- "pi-extensible-workflows": "5.3.0"
49
+ "pi-extensible-workflows": "5.5.0-rc.1"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"