pi-extensible-workflows 0.3.2 → 1.0.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.
@@ -1,12 +1,13 @@
1
1
  import { spawn, type ChildProcess } from "node:child_process";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { join, relative } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { CAPTURE_IDENTITY } from "./eval-capture-extension.js";
9
- import { extractCapturedWorkflows, extractParentOracleFile } from "./workflow-evals.js";
9
+ import { isObject } from "./index.js";
10
+ import { extractCapturedWorkflows, extractParentOracleFile, findSessionFile } from "./workflow-evals.js";
10
11
 
11
12
  export const AMBIENT_OPT_IN = "PI_WORKFLOW_EVAL_AMBIENT";
12
13
  export const AMBIENT_CAPTURE_NOTE = "Ambient Tier D uses the explicit capture extension. Pi 0.80.6 orders CLI extensions before discovered extensions and the first tool registration wins, so ambient tools and skills remain available while workflow execution stays capture-only.";
@@ -327,26 +328,13 @@ function ambientAgentDir(environment: NodeJS.ProcessEnv): string {
327
328
  return environment.PI_CODING_AGENT_DIR ?? process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
328
329
  }
329
330
 
330
- function isObject(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
331
-
332
- function findSessionFile(directory: string, sessionId: string): string | undefined {
333
- if (!existsSync(directory)) return undefined;
334
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
335
- const path = join(directory, entry.name);
336
- if (entry.isDirectory()) { const found = findSessionFile(path, sessionId); if (found) return found; }
337
- else if (entry.name.endsWith(".jsonl")) {
338
- try { const header = JSON.parse(readFileSync(path, "utf8").split("\n")[0] ?? "{}") as unknown; if (isObject(header) && header.id === sessionId) return path; } catch { /* Ignore incomplete sessions. */ }
339
- }
340
- }
341
- return undefined;
342
- }
343
-
344
- function emptyResult(candidate: AmbientEvalCase, repository: AmbientFixtureRepository, worktree: AmbientCaseWorktree, environment: NodeJS.ProcessEnv, error: string): AmbientCaseResult {
331
+ type AmbientCaseResultDraft = Omit<AmbientCaseResult, "manifest"> & { manifest: Omit<AmbientManifest, "gitStatusAfter"> };
332
+ function emptyResult(candidate: AmbientEvalCase, repository: AmbientFixtureRepository, worktree: AmbientCaseWorktree, environment: NodeJS.ProcessEnv, error: string): AmbientCaseResultDraft {
345
333
  const accounting = emptyAccounting();
346
334
  return {
347
335
  id: candidate.id, status: "failed", workflows: [], accounting, accountingTrustworthy: false, diagnostics: [], errors: [error],
348
336
  manifest: {
349
- invocationMode: AMBIENT_INVOCATION_MODE, fixtureRoot: repository.fixtureRoot, worktreePath: worktree.path, ambientAgentDir: ambientAgentDir(environment), fixtureFileList: worktree.fixtureFiles, gitStatusBefore: worktree.gitStatusBefore, gitStatusAfter: worktree.gitStatusBefore, parentToolSequence: [], skillReads: [], workflowCalls: [], workflowCallCount: 0, tokenCost: accounting,
337
+ invocationMode: AMBIENT_INVOCATION_MODE, fixtureRoot: repository.fixtureRoot, worktreePath: worktree.path, ambientAgentDir: ambientAgentDir(environment), fixtureFileList: worktree.fixtureFiles, gitStatusBefore: worktree.gitStatusBefore, parentToolSequence: [], skillReads: [], workflowCalls: [], workflowCallCount: 0, tokenCost: accounting,
350
338
  cleanup: { processExited: false, processGroupTerminated: false, worktreeRemoved: false, fixtureRepoRemoved: false, tempRootRemoved: false, captureIdentityVerified: false, realWorkflowAgentsLaunched: 0 },
351
339
  },
352
340
  };
@@ -356,12 +344,13 @@ async function runAmbientCase(repository: AmbientFixtureRepository, candidate: A
356
344
  const worktree = createAmbientCaseWorktree(repository, candidate.id);
357
345
  const sessionId = randomUUID();
358
346
  const sessionDir = join(repository.root, "sessions", candidate.id);
359
- let result: AmbientCaseResult | undefined;
360
- let gitStatusAfter: readonly string[] = worktree.gitStatusBefore;
347
+ let result: AmbientCaseResultDraft | undefined;
348
+ let failure: string | undefined;
349
+ let gitStatusAfter: readonly string[];
361
350
  try {
362
351
  const pi = await runAmbientPiProcess({ worktree: worktree.path, sessionDir, sessionId, prompt: candidate.prompt, provider, model, ...(thinking ? { thinking } : {}), ...(piCommand ? { piCommand } : {}), timeoutMs: candidate.timeoutMs, maxCost: candidate.maxCost, environment });
363
352
  const sessionFile = findSessionFile(sessionDir, sessionId);
364
- if (!sessionFile) result = emptyResult(candidate, repository, worktree, environment, "Ambient parent session was not written.");
353
+ if (!sessionFile) failure = "Ambient parent session was not written.";
365
354
  else {
366
355
  const oracle = extractParentOracleFile(sessionFile);
367
356
  const workflows = extractCapturedWorkflows(oracle);
@@ -371,24 +360,25 @@ async function runAmbientCase(repository: AmbientFixtureRepository, candidate: A
371
360
  result = {
372
361
  id: candidate.id, status, workflows, accounting: oracle.usage, accountingTrustworthy: !pi.timedOut && !pi.budgetExceeded && pi.exitCode === 0, diagnostics: [pi.stderr].filter(Boolean), errors,
373
362
  manifest: {
374
- invocationMode: AMBIENT_INVOCATION_MODE, fixtureRoot: repository.fixtureRoot, worktreePath: worktree.path, ambientAgentDir: ambientAgentDir(environment), fixtureFileList: worktree.fixtureFiles, gitStatusBefore: worktree.gitStatusBefore, gitStatusAfter, parentToolSequence: oracle.parentToolSequence, skillReads: oracle.skillReads, workflowCalls: workflows, workflowCallCount: oracle.workflowCallCount, tokenCost: oracle.usage,
363
+ invocationMode: AMBIENT_INVOCATION_MODE, fixtureRoot: repository.fixtureRoot, worktreePath: worktree.path, ambientAgentDir: ambientAgentDir(environment), fixtureFileList: worktree.fixtureFiles, gitStatusBefore: worktree.gitStatusBefore, parentToolSequence: oracle.parentToolSequence, skillReads: oracle.skillReads, workflowCalls: workflows, workflowCallCount: oracle.workflowCallCount, tokenCost: oracle.usage,
375
364
  cleanup: { processExited: pi.exitCode !== null, processGroupTerminated: pi.processGroupTerminated, worktreeRemoved: false, fixtureRepoRemoved: false, tempRootRemoved: false, captureIdentityVerified, realWorkflowAgentsLaunched: 0 },
376
365
  },
377
366
  };
378
367
  }
379
368
  } catch (error) {
380
- result = emptyResult(candidate, repository, worktree, environment, error instanceof Error ? error.message : String(error));
369
+ failure = error instanceof Error ? error.message : String(error);
381
370
  } finally {
382
371
  try { gitStatusAfter = gitStatus(worktree.path); } catch { gitStatusAfter = ["<unavailable>"]; }
383
372
  const worktreeRemoved = removeAmbientCaseWorktree(repository, worktree);
384
- if (!result) result = emptyResult(candidate, repository, worktree, environment, "Ambient case produced no result.");
373
+ if (!result) result = emptyResult(candidate, repository, worktree, environment, failure ?? "Ambient case produced no result.");
385
374
  const cleanup = { ...result.manifest.cleanup, worktreeRemoved };
386
- result = { ...result, manifest: { ...result.manifest, gitStatusAfter, cleanup } };
387
- writeFileSync(join(artifactsDir, `${candidate.id}.json`), `${JSON.stringify(result, null, 2)}\n`, { mode: 0o600 });
375
+ result = { ...result, manifest: { ...result.manifest, cleanup } };
388
376
  }
389
- return result;
390
- }
391
377
 
378
+ const finalized: AmbientCaseResult = { ...result, manifest: { ...result.manifest, gitStatusAfter } };
379
+ writeFileSync(join(artifactsDir, `${candidate.id}.json`), `${JSON.stringify(finalized, null, 2)}\n`, { mode: 0o600 });
380
+ return finalized;
381
+ }
392
382
  export function assertAmbientOptIn(environment: NodeJS.ProcessEnv = process.env): void {
393
383
  if (environment[AMBIENT_OPT_IN] !== "1") throw new Error(`Ambient Tier D evals are opt-in. Set ${AMBIENT_OPT_IN}=1 to run them.`);
394
384
  }
package/src/doctor.ts CHANGED
@@ -13,13 +13,17 @@ import {
13
13
  import {
14
14
  DEFAULT_SETTINGS,
15
15
  loadSettings,
16
- parseModelReference,
16
+ resolveAgentResourcePolicy,
17
+ resolveModelReference,
17
18
  parseRoleMarkdown,
18
19
  preflight,
19
20
  registeredWorkflowDefinitions,
20
21
  workflowRoleDirectories,
22
+ workflowProjectSettingsPath,
21
23
  workflowSettingsPath,
22
24
  WorkflowError,
25
+ type AgentResourceExclusions,
26
+ type AgentResourcePolicy,
23
27
  type WorkflowScriptDefinition,
24
28
  type WorkflowSettings,
25
29
  } from "./index.js";
@@ -36,6 +40,8 @@ export interface DoctorPiState {
36
40
  knownModels: readonly string[];
37
41
  availableModels: readonly string[];
38
42
  extensionErrors: readonly { path?: string; message: string }[];
43
+ extensions?: readonly string[];
44
+ skills?: readonly string[];
39
45
  workflows: Readonly<Record<string, WorkflowScriptDefinition>>;
40
46
  }
41
47
  export interface DoctorReport {
@@ -47,6 +53,7 @@ export interface DoctorReport {
47
53
  activeTools: readonly string[];
48
54
  roles: readonly DoctorRole[];
49
55
  workflows: readonly DoctorWorkflow[];
56
+ resourcePolicy: AgentResourcePolicy;
50
57
  diagnostics: readonly DoctorDiagnostic[];
51
58
  }
52
59
  export interface DoctorOptions {
@@ -107,7 +114,7 @@ async function discoverPi(cwd: string, agentDir: string): Promise<DoctorPiState>
107
114
  agentDir,
108
115
  settingsManager,
109
116
  modelRuntime,
110
- resourceLoaderOptions: { noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true },
117
+ resourceLoaderOptions: { noPromptTemplates: true, noThemes: true, noContextFiles: true },
111
118
  resourceLoaderReloadOptions: { resolveProjectTrust: async () => trusted },
112
119
  });
113
120
  const allModels = services.modelRuntime.getModels();
@@ -117,11 +124,14 @@ async function discoverPi(cwd: string, agentDir: string): Promise<DoctorPiState>
117
124
  const { session } = await createAgentSessionFromServices({ services, sessionManager: SessionManager.inMemory(), model });
118
125
  const activeTools = session.agent.state.tools.map(({ name }) => name).filter((name) => name !== "workflow" && name !== "workflow_respond" && name !== "workflow_catalog");
119
126
  const extensions = services.resourceLoader.getExtensions();
127
+ const skills = services.resourceLoader.getSkills().skills;
120
128
  return {
121
129
  trust: { required, trusted, source },
122
130
  activeTools,
123
131
  knownModels: allModels.map(({ provider, id }) => `${provider}/${id}`),
124
132
  availableModels: availableModels.map(({ provider, id }) => `${provider}/${id}`),
133
+ extensions: extensions.extensions.map(({ resolvedPath }) => resolvedPath),
134
+ skills: skills.map(({ name }) => name),
125
135
  extensionErrors: [
126
136
  ...extensions.errors.map(({ path, error }) => ({ path, message: error })),
127
137
  ...services.diagnostics.filter(({ type }) => type === "error").map(({ message }) => ({ message })),
@@ -147,10 +157,13 @@ function roleFilesFrom(dirs: readonly string[]): string[] {
147
157
  function diagnostic(severity: DoctorSeverity, code: string, message: string, source?: string, hint?: string): DoctorDiagnostic {
148
158
  return { severity, code, message, ...(source ? { source } : {}), ...(hint ? { hint } : {}) };
149
159
  }
150
-
151
- function validateModel(value: string, known: ReadonlySet<string>, available: ReadonlySet<string>, source: string, diagnostics: DoctorDiagnostic[]): void {
160
+ function emptyResourcePolicy(globalSettingsPath: string, cwd: string, projectTrusted: boolean): AgentResourcePolicy {
161
+ const empty: AgentResourceExclusions = { skills: [], extensions: [] };
162
+ return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: empty, unmatchedSkills: [], unmatchedExtensions: [] };
163
+ }
164
+ function validateModel(value: string, known: ReadonlySet<string>, available: ReadonlySet<string>, source: string, diagnostics: DoctorDiagnostic[], aliases: Readonly<Record<string, string>>, settingsPath: string): void {
152
165
  try {
153
- const parsed = parseModelReference(value);
166
+ const parsed = resolveModelReference(value, aliases, known, settingsPath);
154
167
  const name = `${parsed.provider}/${parsed.model}`;
155
168
  if (!known.has(name) || !available.has(name)) diagnostics.push(diagnostic("warning", "MODEL_UNAVAILABLE", `Model is valid-shaped but unavailable: ${name}`, source));
156
169
  } catch (error) {
@@ -158,9 +171,9 @@ function validateModel(value: string, known: ReadonlySet<string>, available: Rea
158
171
  }
159
172
  }
160
173
 
161
- function inspectRole(path: string, activeTools: ReadonlySet<string>, knownModels: ReadonlySet<string>, availableModels: ReadonlySet<string>, diagnostics: DoctorDiagnostic[]): AgentDefinition | undefined {
174
+ 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
175
  let definition: AgentDefinition;
163
- try { definition = parseRoleMarkdown(readFileSync(path, "utf8"), true); }
176
+ try { definition = parseRoleMarkdown(readFileSync(path, "utf8"), true, path); }
164
177
  catch (error) {
165
178
  diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", (error as Error).message, path, "Fix the role YAML frontmatter."));
166
179
  return undefined;
@@ -169,15 +182,19 @@ function inspectRole(path: string, activeTools: ReadonlySet<string>, knownModels
169
182
  if (body.trim() === "") diagnostics.push(diagnostic("warning", "ROLE_BODY_EMPTY", "Role body is empty", path));
170
183
  if (Buffer.byteLength(body) > 50 * 1024) diagnostics.push(diagnostic("warning", "ROLE_BODY_LARGE", "Role body exceeds 50KB", path));
171
184
  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);
185
+ if (definition.model) validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, settingsPath);
173
186
  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
187
  return definition;
175
188
  }
176
189
 
177
- class DoctorModelSet extends Set<string> {
178
- override has(value: string): boolean { parseModelReference(value); return true; }
190
+ function matchResourcePolicy(policy: AgentResourcePolicy, pi: DoctorPiState): AgentResourcePolicy {
191
+ const extensions = new Set((pi.extensions ?? []).map(canonical));
192
+ const skills = new Set(pi.skills ?? []);
193
+ return { ...policy, unmatchedSkills: policy.effective.skills.filter((name) => !skills.has(name)), unmatchedExtensions: policy.effective.extensions.filter((path) => !extensions.has(canonical(path))) };
194
+ }
195
+ function resourcePolicySource(policy: AgentResourcePolicy, kind: keyof AgentResourceExclusions, selector: string): string {
196
+ return policy.project[kind].includes(selector) && !policy.global[kind].includes(selector) ? policy.projectSettingsPath : policy.globalSettingsPath;
179
197
  }
180
-
181
198
  export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport> {
182
199
  const cwd = canonical(options.cwd ?? process.cwd());
183
200
  const agentDir = canonical(options.agentDir ?? getAgentDir());
@@ -196,10 +213,21 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
196
213
  if (options.activeTools) pi = { ...pi, activeTools: options.activeTools.filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_catalog") };
197
214
  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
215
  for (const error of pi.extensionErrors) diagnostics.push(diagnostic("error", "EXTENSION_LOAD", error.message, error.path, "Fix or disable the failing Pi extension."));
216
+ let resourcePolicy: AgentResourcePolicy;
217
+ try {
218
+ resourcePolicy = matchResourcePolicy(resolveAgentResourcePolicy(cwd, pi.trust.trusted, settingsPath), pi);
219
+ } catch (error) {
220
+ const message = error instanceof Error ? error.message : String(error);
221
+ 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."));
222
+ resourcePolicy = emptyResourcePolicy(settingsPath, cwd, pi.trust.trusted);
223
+ }
224
+ 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`));
225
+ 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
226
 
200
227
  const activeTools = new Set(pi.activeTools);
201
228
  const knownModels = new Set(pi.knownModels);
202
229
  const availableModels = new Set(pi.availableModels);
230
+ const aliases = settings.modelAliases ?? {};
203
231
  const roles: DoctorRole[] = [];
204
232
  const definitions = new Map<string, AgentDefinition>();
205
233
  const globalPaths = new Map<string, string>();
@@ -208,7 +236,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
208
236
  const name = basename(path, ".md");
209
237
  roles.push({ name, path, scope: "global", active: true });
210
238
  globalPaths.set(name, path);
211
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics);
239
+ const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, settingsPath);
212
240
  if (definition) definitions.set(name, definition);
213
241
  }
214
242
  for (const path of roleFilesFrom([join(cwd, ".pi", "pi-extensible-workflows", "roles")])) {
@@ -221,7 +249,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
221
249
  const global = roles.find((role) => role.path === globalPath);
222
250
  if (global) { global.active = false; global.overriddenBy = path; }
223
251
  }
224
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics);
252
+ const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, settingsPath);
225
253
  if (definition) definitions.set(name, definition); else definitions.delete(name);
226
254
  }
227
255
 
@@ -230,9 +258,13 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
230
258
  let valid = true;
231
259
  try {
232
260
  const checked = preflight(workflow.script, {
233
- models: new DoctorModelSet(pi.knownModels),
261
+ models: new Set(pi.knownModels),
234
262
  tools: activeTools,
235
263
  agentTypes: new Set(definitions.keys()),
264
+ modelAliases: aliases,
265
+ knownModels,
266
+ settingsPath,
267
+ skipModelAvailability: true,
236
268
  }, [], { name, description: workflow.description });
237
269
  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
270
  } catch (error) {
@@ -246,7 +278,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
246
278
  const severityOrder: Record<DoctorSeverity, number> = { error: 0, warning: 1 };
247
279
  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
280
  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 };
281
+ return { cwd, agentDir, settingsPath, settings, trust: pi.trust, activeTools: [...activeTools].sort(), roles, workflows, resourcePolicy, diagnostics };
250
282
  }
251
283
 
252
284
  function count(report: DoctorReport, severity: DoctorSeverity): number { return report.diagnostics.filter((item) => item.severity === severity).length; }
@@ -261,11 +293,23 @@ export function formatDoctorReport(report: DoctorReport): string {
261
293
  `- CWD: \`${report.cwd}\``,
262
294
  `- Agent dir: \`${report.agentDir}\``,
263
295
  `- Workflow settings: \`${report.settingsPath}\``,
264
- `- Limits: concurrency=${String(report.settings.concurrency)}, maxAgentLaunches=${String(report.settings.maxAgentLaunches)}`,
296
+ `- Limits: concurrency=${String(report.settings.concurrency)}`,
265
297
  "",
266
298
  "## Trust/resources",
267
299
  `- [${report.trust.trusted ? "ok" : "warning"}] ${report.trust.source}`,
268
300
  "",
301
+ "## Agent resource exclusions",
302
+ `- Global settings: \`${report.resourcePolicy.globalSettingsPath}\``,
303
+ `- Global skills: ${report.resourcePolicy.global.skills.join(", ") || "(none)"}`,
304
+ `- Global extensions: ${report.resourcePolicy.global.extensions.join(", ") || "(none)"}`,
305
+ `- Project settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
306
+ `- Project skills: ${report.resourcePolicy.project.skills.join(", ") || "(none)"}`,
307
+ `- Project extensions: ${report.resourcePolicy.project.extensions.join(", ") || "(none)"}`,
308
+ `- Effective skills: ${report.resourcePolicy.effective.skills.join(", ") || "(none)"}`,
309
+ `- Effective extensions: ${report.resourcePolicy.effective.extensions.join(", ") || "(none)"}`,
310
+ `- Unmatched skills: ${report.resourcePolicy.unmatchedSkills.join(", ") || "(none)"}`,
311
+ `- Unmatched extensions: ${report.resourcePolicy.unmatchedExtensions.join(", ") || "(none)"}`,
312
+ "",
269
313
  "## Active tools",
270
314
  ...(report.activeTools.length ? report.activeTools.map((tool) => `- \`${tool}\``) : ["- None resolved"]),
271
315
  "",