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.
- package/README.md +9 -0
- package/dist/src/agent-execution.d.ts +81 -8
- package/dist/src/agent-execution.js +465 -75
- package/dist/src/ambient-workflow-evals.js +14 -33
- package/dist/src/doctor.d.ts +4 -1
- package/dist/src/doctor.js +57 -14
- package/dist/src/index.d.ts +222 -24
- package/dist/src/index.js +1548 -410
- package/dist/src/persistence.d.ts +28 -1
- package/dist/src/persistence.js +126 -9
- package/dist/src/session-inspector.d.ts +13 -1
- package/dist/src/session-inspector.js +22 -4
- package/dist/src/workflow-evals.d.ts +1 -0
- package/dist/src/workflow-evals.js +23 -25
- package/package.json +5 -2
- package/skills/pi-extensible-workflows/SKILL.md +14 -6
- package/src/agent-execution.ts +391 -86
- package/src/ambient-workflow-evals.ts +18 -28
- package/src/doctor.ts +60 -16
- package/src/index.ts +1332 -395
- package/src/persistence.ts +98 -10
- package/src/session-inspector.ts +25 -7
- package/src/workflow-evals.ts +13 -16
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
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,
|
|
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:
|
|
360
|
-
let
|
|
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)
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
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)}
|
|
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
|
"",
|