@marcoscale98/piewf-cli 5.14.1-fork.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/dist/src/bundles.d.ts +63 -0
- package/dist/src/bundles.js +619 -0
- package/dist/src/cli.d.ts +34 -0
- package/dist/src/cli.js +912 -0
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +659 -0
- package/dist/src/doctor.d.ts +113 -0
- package/dist/src/doctor.js +668 -0
- package/dist/src/session-inspector.d.ts +82 -0
- package/dist/src/session-inspector.js +454 -0
- package/package.json +52 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
3
|
+
import { InMemoryCredentialStore, InMemoryModelsStore } from "@earendil-works/pi-ai";
|
|
4
|
+
import { ModelRuntime, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { DEFAULT_SETTINGS, canonicalPath, createLocalPiSession, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, resourcePatternHasMagic, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "@marcoscale98/pi-extensible-workflows";
|
|
6
|
+
import { loadingRegistry } from "@marcoscale98/pi-extensible-workflows";
|
|
7
|
+
import { selectResourcesByLayers, unmatchedResourcePatterns, mergeWorkflowExtensionSettings } from "@marcoscale98/pi-extensible-workflows";
|
|
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
|
+
}
|
|
28
|
+
function isDynamicModelAlias(value, aliases) {
|
|
29
|
+
const match = /^([^/\s:]+)(?::([^\s]+))?$/.exec(value);
|
|
30
|
+
const name = match?.[1];
|
|
31
|
+
return Boolean(name && (match[2] === undefined || parseThinking(match[2]) !== undefined) && aliases.has(name));
|
|
32
|
+
}
|
|
33
|
+
function isCredential(value) {
|
|
34
|
+
if (!isObject(value))
|
|
35
|
+
return false;
|
|
36
|
+
if (value.type === "api_key")
|
|
37
|
+
return (value.key === undefined || typeof value.key === "string") && (value.env === undefined || isObject(value.env) && Object.values(value.env).every((entry) => typeof entry === "string"));
|
|
38
|
+
return value.type === "oauth" && typeof value.refresh === "string" && typeof value.access === "string" && typeof value.expires === "number";
|
|
39
|
+
}
|
|
40
|
+
async function readCredentials(agentDir) {
|
|
41
|
+
const credentials = new InMemoryCredentialStore();
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(readFileSync(join(agentDir, "auth.json"), "utf8"));
|
|
44
|
+
if (!isObject(parsed))
|
|
45
|
+
throw new Error("Pi auth.json must be an object");
|
|
46
|
+
await Promise.all(Object.entries(parsed).flatMap(([provider, credential]) => isCredential(credential) ? [credentials.modify(provider, async () => credential)] : []));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (!isNodeError(error, "ENOENT"))
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
return credentials;
|
|
53
|
+
}
|
|
54
|
+
function savedTrust(cwd, agentDir) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(readFileSync(join(agentDir, "trust.json"), "utf8"));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (isNodeError(error, "ENOENT"))
|
|
61
|
+
return undefined;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (!isObject(parsed))
|
|
65
|
+
throw new Error("Pi trust.json must be an object");
|
|
66
|
+
let current = canonicalPath(cwd);
|
|
67
|
+
while (current !== dirname(current)) {
|
|
68
|
+
const value = parsed[current];
|
|
69
|
+
if (value === true || value === false)
|
|
70
|
+
return value;
|
|
71
|
+
current = dirname(current);
|
|
72
|
+
}
|
|
73
|
+
const value = parsed[current];
|
|
74
|
+
return value === true || value === false ? value : undefined;
|
|
75
|
+
}
|
|
76
|
+
async function discoverPi(cwd, agentDir) {
|
|
77
|
+
const required = hasTrustRequiringProjectResources(cwd);
|
|
78
|
+
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
|
|
79
|
+
const saved = required ? savedTrust(cwd, agentDir) : true;
|
|
80
|
+
const fallback = settingsManager.getDefaultProjectTrust();
|
|
81
|
+
const trusted = !required || saved !== undefined ? Boolean(saved) : fallback === "always";
|
|
82
|
+
const source = !required ? "no trust-gated project resources" : saved !== undefined ? "saved Pi trust decision" : `headless defaultProjectTrust=${fallback}`;
|
|
83
|
+
const previousOffline = process.env.PI_OFFLINE;
|
|
84
|
+
process.env.PI_OFFLINE = "1";
|
|
85
|
+
try {
|
|
86
|
+
const modelRuntime = await ModelRuntime.create({ credentials: await readCredentials(agentDir), modelsPath: join(agentDir, "models.json"), modelsStore: new InMemoryModelsStore() });
|
|
87
|
+
const services = await createAgentSessionServices({
|
|
88
|
+
cwd,
|
|
89
|
+
agentDir,
|
|
90
|
+
settingsManager,
|
|
91
|
+
modelRuntime,
|
|
92
|
+
resourceLoaderOptions: { noPromptTemplates: true, noThemes: true, noContextFiles: true },
|
|
93
|
+
resourceLoaderReloadOptions: { resolveProjectTrust: async () => trusted },
|
|
94
|
+
});
|
|
95
|
+
const allModels = services.modelRuntime.getModels();
|
|
96
|
+
const availableModels = await services.modelRuntime.getAvailable();
|
|
97
|
+
const model = availableModels[0] ?? allModels[0];
|
|
98
|
+
if (!model)
|
|
99
|
+
throw new Error("Pi has no models registered");
|
|
100
|
+
const { session } = await createAgentSessionFromServices({ services, sessionManager: SessionManager.inMemory(), model });
|
|
101
|
+
const activeTools = session.agent.state.tools.map(({ name }) => name).filter((name) => name !== "workflow" && name !== "workflow_respond" && name !== "workflow_catalog");
|
|
102
|
+
const extensions = services.resourceLoader.getExtensions();
|
|
103
|
+
const skills = services.resourceLoader.getSkills().skills;
|
|
104
|
+
return {
|
|
105
|
+
trust: { required, trusted, source },
|
|
106
|
+
model: { provider: model.provider, model: model.id, thinking: session.thinkingLevel },
|
|
107
|
+
activeTools,
|
|
108
|
+
knownModels: allModels.map(({ provider, id }) => `${provider}/${id}`),
|
|
109
|
+
availableModels: availableModels.map(({ provider, id }) => `${provider}/${id}`),
|
|
110
|
+
extensions: extensions.extensions.map(({ resolvedPath }) => resolvedPath),
|
|
111
|
+
skills: skills.map(({ name }) => name),
|
|
112
|
+
extensionErrors: [
|
|
113
|
+
...extensions.errors.map(({ path, error }) => ({ path, message: error })),
|
|
114
|
+
...services.diagnostics.filter(({ type }) => type === "error").map(({ message }) => ({ message })),
|
|
115
|
+
],
|
|
116
|
+
functions: registeredWorkflowFunctions(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
if (previousOffline === undefined)
|
|
121
|
+
delete process.env.PI_OFFLINE;
|
|
122
|
+
else
|
|
123
|
+
process.env.PI_OFFLINE = previousOffline;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function isRoleFile(dir, entry) {
|
|
127
|
+
if (extname(entry.name) !== ".md")
|
|
128
|
+
return false;
|
|
129
|
+
if (entry.isFile())
|
|
130
|
+
return true;
|
|
131
|
+
if (!entry.isSymbolicLink())
|
|
132
|
+
return false;
|
|
133
|
+
try {
|
|
134
|
+
return statSync(join(dir, entry.name)).isFile();
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (isNodeError(error, "ENOENT"))
|
|
138
|
+
return false;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function roleFiles(dir) {
|
|
143
|
+
try {
|
|
144
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => isRoleFile(dir, entry)).map((entry) => join(dir, entry.name)).sort();
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (isNodeError(error, "ENOENT"))
|
|
148
|
+
return [];
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function roleFilesFrom(dirs) {
|
|
153
|
+
const paths = dirs.flatMap((dir) => roleFiles(dir));
|
|
154
|
+
return [...new Map(paths.map((path) => [basename(path, ".md"), path])).values()].sort();
|
|
155
|
+
}
|
|
156
|
+
function extensionLabel(extension) { return `Extension "${extension.headline}" (${extension.version})`; }
|
|
157
|
+
function scanExtensionRoleFiles(registrations) {
|
|
158
|
+
const files = [];
|
|
159
|
+
const empty = [];
|
|
160
|
+
const errors = [];
|
|
161
|
+
for (const registration of registrations) {
|
|
162
|
+
try {
|
|
163
|
+
const entries = readdirSync(registration.path, { withFileTypes: true });
|
|
164
|
+
const roleFiles = entries.filter((entry) => isRoleFile(registration.path, entry));
|
|
165
|
+
if (!roleFiles.length)
|
|
166
|
+
empty.push(registration);
|
|
167
|
+
for (const entry of roleFiles)
|
|
168
|
+
files.push({ name: basename(entry.name, ".md"), path: join(registration.path, entry.name), directory: registration.path, extension: registration.extension, ...(registration.builtin === true ? { builtin: true } : {}) });
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
errors.push({ registration, error });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
|
|
175
|
+
return { files, empty, errors };
|
|
176
|
+
}
|
|
177
|
+
function roleProvenance(source) {
|
|
178
|
+
return source ? `${extensionLabel(source.extension)} role directory "${source.directory}"` : "Role";
|
|
179
|
+
}
|
|
180
|
+
function diagnostic(severity, code, message, source, hint) {
|
|
181
|
+
return { severity, code, message, ...(source ? { source } : {}), ...(hint ? { hint } : {}) };
|
|
182
|
+
}
|
|
183
|
+
function legacyAgentResourceSelectorDiagnostic(source) {
|
|
184
|
+
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.");
|
|
185
|
+
}
|
|
186
|
+
function positiveOnlyToolSelectorDiagnostic(source, selectors) {
|
|
187
|
+
if (!selectors?.length || selectors.some((selector) => selector.startsWith("!")))
|
|
188
|
+
return undefined;
|
|
189
|
+
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.");
|
|
190
|
+
}
|
|
191
|
+
function emptyResourcePolicy(globalSettingsPath, cwd, projectTrusted) {
|
|
192
|
+
const empty = { skills: [], extensions: [], tools: [] };
|
|
193
|
+
return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: empty, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: { global: {}, project: {} } };
|
|
194
|
+
}
|
|
195
|
+
function validateModel(value, known, available, source, diagnostics, aliases, dynamicAliases, settingsPath) {
|
|
196
|
+
if (isDynamicModelAlias(value, dynamicAliases))
|
|
197
|
+
return;
|
|
198
|
+
try {
|
|
199
|
+
const parsed = resolveModelReference(value, aliases, known, settingsPath);
|
|
200
|
+
const name = `${parsed.provider}/${parsed.model}`;
|
|
201
|
+
if (!known.has(name) || !available.has(name))
|
|
202
|
+
diagnostics.push(diagnostic("warning", "MODEL_UNAVAILABLE", `Model is valid-shaped but unavailable: ${name}`, source));
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
const message = errorText(error);
|
|
206
|
+
diagnostics.push(diagnostic("error", "MODEL_INVALID", message, source, message.includes("thinking") ? THINKING_HINT : "Use provider/model:thinking."));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
|
|
210
|
+
let definition;
|
|
211
|
+
try {
|
|
212
|
+
definition = parseRoleMarkdown(readFileSync(path, "utf8"), true, path);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (usesLegacyRoleSelectors(path)) {
|
|
216
|
+
diagnostics.push(legacyAgentResourceSelectorDiagnostic(path));
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const message = errorText(error);
|
|
220
|
+
diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", source ? `${roleProvenance(source)} contains invalid role at "${path}": ${message}` : message, path, "Fix the role YAML frontmatter."));
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
const toolSelectorDiagnostic = positiveOnlyToolSelectorDiagnostic(path, definition.tools);
|
|
224
|
+
if (toolSelectorDiagnostic)
|
|
225
|
+
diagnostics.push(toolSelectorDiagnostic);
|
|
226
|
+
const body = definition.prompt ?? "";
|
|
227
|
+
if (body.trim() === "")
|
|
228
|
+
diagnostics.push(diagnostic("warning", "ROLE_BODY_EMPTY", "Role body is empty", path));
|
|
229
|
+
if (Buffer.byteLength(body) > 50 * 1024)
|
|
230
|
+
diagnostics.push(diagnostic("warning", "ROLE_BODY_LARGE", "Role body exceeds 50KB", path));
|
|
231
|
+
if (/{{\s*[^{}]+\s*}}/.test(body))
|
|
232
|
+
diagnostics.push(diagnostic("warning", "ROLE_PLACEHOLDER", "Role body contains an unsupported placeholder-looking token", path));
|
|
233
|
+
if (definition.model)
|
|
234
|
+
validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, dynamicAliases, settingsPath);
|
|
235
|
+
for (const selector of definition.tools ?? []) {
|
|
236
|
+
const tool = selector.startsWith("!") ? selector.slice(1) : selector;
|
|
237
|
+
if (!selector.startsWith("!") && !resourcePatternHasMagic(selector) && !activeTools.has(tool))
|
|
238
|
+
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."));
|
|
239
|
+
}
|
|
240
|
+
return definition;
|
|
241
|
+
}
|
|
242
|
+
function matchResourcePolicy(policy, pi) {
|
|
243
|
+
const extensions = [...new Set((pi.extensions ?? []).map(canonicalPath))];
|
|
244
|
+
const skills = [...new Set(pi.skills ?? [])];
|
|
245
|
+
const tools = [...new Set(pi.activeTools)];
|
|
246
|
+
const layers = policy.selectorSources;
|
|
247
|
+
const selectedSkills = selectResourcesByLayers([layers.global.skills, layers.project.skills], skills);
|
|
248
|
+
const selectedExtensions = selectResourcesByLayers([layers.global.extensions, layers.project.extensions], extensions);
|
|
249
|
+
const selectedTools = selectResourcesByLayers([layers.global.tools, layers.project.tools], tools);
|
|
250
|
+
return { ...policy, selectedSkills, selectedExtensions, selectedTools, unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, skills), unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, extensions), unmatchedTools: unmatchedResourcePatterns(policy.effective.tools ?? [], tools) };
|
|
251
|
+
}
|
|
252
|
+
async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath, basePolicy, rootModel, activeTools, aliases, knownModels, availableModels, settingsPath, extensionSettings, prompt, hooks, diagnostics) {
|
|
253
|
+
const setupDiagnostics = [];
|
|
254
|
+
const signal = new AbortController().signal;
|
|
255
|
+
const transport = { id: "doctor-local", createSession: async () => { throw new Error("Doctor inspection does not create transport sessions"); } };
|
|
256
|
+
const run = { cwd, sessionId: "doctor", runId: "doctor", workflow: { name: "doctor" }, args: null, signal };
|
|
257
|
+
const root = { cwd, model: { ...rootModel }, tools: new Set(activeTools), agentDefinitions: { [roleName]: definition }, agentDir, extensionSettings, modelAliases: aliases, knownModels, availableModels, settingsPath, agentSetupHooks: hooks, agentResourcePolicy: () => structuredClone(basePolicy), runContext: run };
|
|
258
|
+
const options = { label: roleName, workflowName: "doctor", role: roleName };
|
|
259
|
+
let prepared;
|
|
260
|
+
try {
|
|
261
|
+
prepared = await prepareAgentSetupForInspection(root, prompt, options, transport);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
setupDiagnostics.push(diagnostic("error", "ROLE_INSPECTION", errorText(error), rolePath));
|
|
265
|
+
diagnostics.push(...setupDiagnostics);
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
if (prepared.failure) {
|
|
269
|
+
const error = prepared.failure.error;
|
|
270
|
+
const code = prepared.failure.hook ? "ROLE_SETUP_HOOK" : "ROLE_INSPECTION";
|
|
271
|
+
setupDiagnostics.push(diagnostic("error", code, `${prepared.failure.hook ? `Role setup hook ${prepared.failure.hook} failed: ` : ""}${errorText(error)}`, prepared.failure.hook ?? rolePath));
|
|
272
|
+
diagnostics.push(...setupDiagnostics);
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
const session = await (async () => { try {
|
|
276
|
+
return await createLocalPiSession({ ...prepared.setup.sessionInput, sessionManager: SessionManager.inMemory() });
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
setupDiagnostics.push(diagnostic("error", "ROLE_INSPECTION", errorText(error), rolePath));
|
|
280
|
+
return undefined;
|
|
281
|
+
} })();
|
|
282
|
+
if (!session) {
|
|
283
|
+
diagnostics.push(...setupDiagnostics);
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const promptResult = await session.preparePrompt(prompt);
|
|
288
|
+
const resources = session.getResourceInspection();
|
|
289
|
+
const state = session.agent?.state;
|
|
290
|
+
const inherited = prepared.setup.sessionInput.model.provider === rootModel.provider && prepared.setup.sessionInput.model.model === rootModel.model && prepared.setup.sessionInput.model.thinking === rootModel.thinking;
|
|
291
|
+
const actualModel = session.model?.provider && (session.model.model ?? session.model.id) ? { provider: session.model.provider, model: session.model.model ?? session.model.id ?? prepared.setup.sessionInput.model.model, ...(session.thinkingLevel ? { thinking: session.thinkingLevel } : {}), ...(inherited ? { inherited: true } : {}) } : { ...prepared.setup.sessionInput.model, ...(inherited ? { inherited: true } : {}) };
|
|
292
|
+
const policy = prepared.setup.sessionInput.resourcePolicy ?? basePolicy;
|
|
293
|
+
for (const item of [...resources.diagnostics, ...promptResult.diagnostics])
|
|
294
|
+
setupDiagnostics.push(diagnostic(item.type === "error" ? "error" : "warning", "ROLE_INSPECTION", item.message, item.source));
|
|
295
|
+
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 } };
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
setupDiagnostics.push(diagnostic("error", "ROLE_INSPECTION", errorText(error), rolePath));
|
|
299
|
+
diagnostics.push(...setupDiagnostics);
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
await session.dispose();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function resourcePolicySource(settingsSource) { return settingsSource; }
|
|
307
|
+
function validateDoctorExtensionSettings(registry, value, source, cwd, projectTrusted, settingsPath, diagnostics, role) {
|
|
308
|
+
try {
|
|
309
|
+
registry.validateExtensionSettings(value, { source, cwd, projectTrusted, settingsPath, ...(role === undefined ? {} : { role }) });
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", errorText(error), `${settingsPath}.extensionSettings`, "Fix the extension-owned settings reported in this error."));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export async function doctor(options = {}) {
|
|
316
|
+
const cwd = canonicalPath(options.cwd ?? process.cwd());
|
|
317
|
+
const agentDir = canonicalPath(options.agentDir ?? getAgentDir());
|
|
318
|
+
const settingsPath = canonicalPath(options.settingsPath ?? workflowSettingsPath(agentDir));
|
|
319
|
+
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
320
|
+
const legacyGlobalSettings = usesLegacySettings(settingsPath);
|
|
321
|
+
const diagnostics = [];
|
|
322
|
+
const registry = options.registry ?? loadingRegistry();
|
|
323
|
+
let settings = DEFAULT_SETTINGS;
|
|
324
|
+
try {
|
|
325
|
+
settings = loadSettings(settingsPath);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", errorText(error), settingsPath, "Fix or remove the invalid workflow settings file."));
|
|
329
|
+
}
|
|
330
|
+
let settingsSources = { concurrency: settingsPath, modelAliases: settingsPath, skills: settingsPath, extensions: settingsPath, tools: settingsPath };
|
|
331
|
+
let pi;
|
|
332
|
+
try {
|
|
333
|
+
pi = await (options.discoverPi ?? discoverPi)(cwd, agentDir);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
diagnostics.push(diagnostic("error", "PI_DISCOVERY", `Pi headless discovery failed: ${errorText(error)}`, undefined, "Open and trust the project in Pi, fix extension errors, then rerun doctor."));
|
|
337
|
+
pi = { trust: { required: false, trusted: false, source: "discovery failed" }, activeTools: [], knownModels: [], availableModels: [], extensionErrors: [], functions: {} };
|
|
338
|
+
}
|
|
339
|
+
if (options.activeTools)
|
|
340
|
+
pi = { ...pi, activeTools: options.activeTools.filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_catalog") };
|
|
341
|
+
if (pi.trust.required && !pi.trust.trusted)
|
|
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`));
|
|
348
|
+
for (const error of pi.extensionErrors)
|
|
349
|
+
diagnostics.push(diagnostic("error", "EXTENSION_LOAD", error.message, error.path, "Fix or disable the failing Pi extension."));
|
|
350
|
+
try {
|
|
351
|
+
const resolved = resolveWorkflowSettings(cwd, pi.trust.trusted, settingsPath);
|
|
352
|
+
settings = resolved.effective;
|
|
353
|
+
settingsSources = resolved.sources;
|
|
354
|
+
if (resolved.global.extensionSettings !== undefined)
|
|
355
|
+
validateDoctorExtensionSettings(registry, resolved.global.extensionSettings, "global", cwd, pi.trust.trusted, resolved.globalSettingsPath, diagnostics);
|
|
356
|
+
if (pi.trust.trusted && resolved.project.extensionSettings !== undefined)
|
|
357
|
+
validateDoctorExtensionSettings(registry, resolved.project.extensionSettings, "project", cwd, true, resolved.projectSettingsPath, diagnostics);
|
|
358
|
+
validateDoctorExtensionSettings(registry, resolved.effective.extensionSettings, "effective", cwd, pi.trust.trusted, settingsSources.extensionSettings ?? settingsPath, diagnostics);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
const message = errorText(error);
|
|
362
|
+
const source = message.includes(projectSettingsPath) ? projectSettingsPath : settingsPath;
|
|
363
|
+
if (!diagnostics.some(({ code, source: itemSource }) => code === "SETTINGS_INVALID" && itemSource === source))
|
|
364
|
+
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, source, "Fix or remove the invalid workflow settings file."));
|
|
365
|
+
}
|
|
366
|
+
let resourcePolicy;
|
|
367
|
+
try {
|
|
368
|
+
resourcePolicy = matchResourcePolicy(resolveAgentResourcePolicy(cwd, pi.trust.trusted, settingsPath), pi);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
const message = errorText(error);
|
|
372
|
+
const source = message.includes(projectSettingsPath) ? projectSettingsPath : settingsPath;
|
|
373
|
+
if (!diagnostics.some(({ code, source: itemSource }) => code === "SETTINGS_INVALID" && itemSource === source))
|
|
374
|
+
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, source, "Fix or remove the invalid workflow settings file."));
|
|
375
|
+
resourcePolicy = emptyResourcePolicy(settingsPath, cwd, pi.trust.trusted);
|
|
376
|
+
}
|
|
377
|
+
for (const [source, selectors] of [[resourcePolicy.globalSettingsPath, resourcePolicy.selectorSources.global.tools], [resourcePolicy.projectSettingsPath, resourcePolicy.selectorSources.project.tools]]) {
|
|
378
|
+
const toolSelectorDiagnostic = positiveOnlyToolSelectorDiagnostic(source, selectors);
|
|
379
|
+
if (toolSelectorDiagnostic)
|
|
380
|
+
diagnostics.push(toolSelectorDiagnostic);
|
|
381
|
+
}
|
|
382
|
+
for (const skill of resourcePolicy.unmatchedSkills)
|
|
383
|
+
diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Skill selector currently matches no discovered skill: ${skill}`, `${resourcePolicySource(settingsSources.skills ?? settingsPath)}.skills`));
|
|
384
|
+
for (const extension of resourcePolicy.unmatchedExtensions)
|
|
385
|
+
diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Extension selector currently matches no discovered extension source: ${extension}`, `${resourcePolicySource(settingsSources.extensions ?? settingsPath)}.extensions`));
|
|
386
|
+
for (const tool of resourcePolicy.unmatchedTools ?? [])
|
|
387
|
+
diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Tool selector currently matches no root tool: ${tool}`, `${resourcePolicySource(settingsSources.tools ?? settingsPath)}.tools`));
|
|
388
|
+
const activeTools = new Set(pi.activeTools);
|
|
389
|
+
const knownModels = new Set(pi.knownModels);
|
|
390
|
+
const availableModels = new Set(pi.availableModels);
|
|
391
|
+
const aliases = settings.modelAliases ?? {};
|
|
392
|
+
const registeredModelAliases = registry.modelAliases();
|
|
393
|
+
const dynamicAliases = new Set(registeredModelAliases.map(({ name }) => name).filter((name) => !Object.prototype.hasOwnProperty.call(aliases, name)));
|
|
394
|
+
const modelAliases = [
|
|
395
|
+
...Object.keys(aliases).map((name) => ({ name, kind: "static", provenance: settingsSources.modelAliases })),
|
|
396
|
+
...registeredModelAliases.map(({ name, version, headline }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline })),
|
|
397
|
+
].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind));
|
|
398
|
+
const roles = [];
|
|
399
|
+
const definitions = new Map();
|
|
400
|
+
const extensionScan = scanExtensionRoleFiles(registeredWorkflowRoleDirectoryRegistrations());
|
|
401
|
+
for (const { registration, error } of extensionScan.errors) {
|
|
402
|
+
const message = errorText(error);
|
|
403
|
+
diagnostics.push(diagnostic("error", "ROLE_DIRECTORY", `${extensionLabel(registration.extension)} role directory "${registration.path}" could not be scanned: ${message}`, registration.path, "Fix or remove the registered role directory."));
|
|
404
|
+
}
|
|
405
|
+
for (const registration of extensionScan.empty)
|
|
406
|
+
diagnostics.push(diagnostic("warning", "ROLE_DIRECTORY_EMPTY", `${extensionLabel(registration.extension)} role directory "${registration.path}" contains no .md role files`, registration.path, "Add packaged role files or remove the directory registration."));
|
|
407
|
+
const extensionFilesByName = new Map();
|
|
408
|
+
for (const file of extensionScan.files)
|
|
409
|
+
extensionFilesByName.set(file.name, [...(extensionFilesByName.get(file.name) ?? []), file]);
|
|
410
|
+
const duplicateExtensionNames = new Set();
|
|
411
|
+
const extensionPaths = new Map();
|
|
412
|
+
const starterOverrides = new Map();
|
|
413
|
+
const starterOverriddenBy = new Map();
|
|
414
|
+
for (const [name, matches] of extensionFilesByName) {
|
|
415
|
+
const regularMatches = matches.filter(({ builtin }) => builtin !== true);
|
|
416
|
+
const starterMatches = matches.filter(({ builtin }) => builtin === true);
|
|
417
|
+
if (regularMatches.length > 1) {
|
|
418
|
+
duplicateExtensionNames.add(name);
|
|
419
|
+
diagnostics.push(diagnostic("error", "ROLE_DUPLICATE", `Duplicate extension role "${name}": ${regularMatches.map(({ path, directory, extension }) => `${extensionLabel(extension)} role directory "${directory}" (${path})`).join("; ")}`, regularMatches[0]?.path, "Keep one extension role with this name; global and project roles may override packaged defaults."));
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const extension = regularMatches[0] ?? starterMatches[0];
|
|
423
|
+
if (extension)
|
|
424
|
+
extensionPaths.set(name, extension.path);
|
|
425
|
+
const regular = regularMatches[0];
|
|
426
|
+
const starter = starterMatches[0];
|
|
427
|
+
if (regular && starter) {
|
|
428
|
+
starterOverrides.set(regular.path, starter.path);
|
|
429
|
+
starterOverriddenBy.set(starter.path, regular.path);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
for (const file of extensionScan.files) {
|
|
433
|
+
const starterPath = starterOverrides.get(file.path);
|
|
434
|
+
const overriddenBy = starterOverriddenBy.get(file.path);
|
|
435
|
+
roles.push({ name: file.name, path: file.path, scope: "extension", active: overriddenBy === undefined, extension: file.extension, ...(starterPath ? { overrides: starterPath } : {}), ...(overriddenBy ? { overriddenBy } : {}) });
|
|
436
|
+
const definition = inspectRole(file.path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, { directory: file.directory, extension: file.extension });
|
|
437
|
+
if (definition)
|
|
438
|
+
validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, file.path, diagnostics, file.name);
|
|
439
|
+
if (duplicateExtensionNames.has(file.name))
|
|
440
|
+
continue;
|
|
441
|
+
if (extensionPaths.get(file.name) !== file.path)
|
|
442
|
+
continue;
|
|
443
|
+
if (definition)
|
|
444
|
+
definitions.set(file.name, definition);
|
|
445
|
+
}
|
|
446
|
+
const globalPaths = new Map();
|
|
447
|
+
const globalRoleDirs = workflowRoleDirectories(agentDir);
|
|
448
|
+
for (const path of roleFilesFrom(globalRoleDirs)) {
|
|
449
|
+
const name = basename(path, ".md");
|
|
450
|
+
const extensionPath = extensionPaths.get(name);
|
|
451
|
+
roles.push({ name, path, scope: "global", active: true, ...(extensionPath ? { overrides: extensionPath } : {}) });
|
|
452
|
+
globalPaths.set(name, path);
|
|
453
|
+
if (extensionPath) {
|
|
454
|
+
const extension = roles.find((role) => role.path === extensionPath);
|
|
455
|
+
if (extension) {
|
|
456
|
+
extension.active = false;
|
|
457
|
+
extension.overriddenBy = path;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
|
|
461
|
+
if (definition)
|
|
462
|
+
validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
|
|
463
|
+
if (definition)
|
|
464
|
+
definitions.set(name, definition);
|
|
465
|
+
else
|
|
466
|
+
definitions.delete(name);
|
|
467
|
+
}
|
|
468
|
+
for (const path of roleFilesFrom([join(cwd, ".pi", "pi-extensible-workflows", "roles")])) {
|
|
469
|
+
const name = basename(path, ".md");
|
|
470
|
+
const globalPath = globalPaths.get(name);
|
|
471
|
+
const extensionPath = extensionPaths.get(name);
|
|
472
|
+
const overriddenPath = globalPath ?? extensionPath;
|
|
473
|
+
const active = pi.trust.trusted;
|
|
474
|
+
roles.push({ name, path, scope: "project", active, ...(active && overriddenPath ? { overrides: overriddenPath } : {}) });
|
|
475
|
+
if (!active)
|
|
476
|
+
continue;
|
|
477
|
+
if (globalPath) {
|
|
478
|
+
const global = roles.find((role) => role.path === globalPath);
|
|
479
|
+
if (global) {
|
|
480
|
+
global.active = false;
|
|
481
|
+
global.overriddenBy = path;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
else if (extensionPath) {
|
|
485
|
+
const extension = roles.find((role) => role.path === extensionPath);
|
|
486
|
+
if (extension) {
|
|
487
|
+
extension.active = false;
|
|
488
|
+
extension.overriddenBy = path;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
|
|
492
|
+
if (definition)
|
|
493
|
+
validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
|
|
494
|
+
if (definition)
|
|
495
|
+
definitions.set(name, definition);
|
|
496
|
+
else
|
|
497
|
+
definitions.delete(name);
|
|
498
|
+
}
|
|
499
|
+
const rolePaths = new Set(roles.map(({ path }) => path));
|
|
500
|
+
if (diagnostics.some(({ code, source }) => source !== undefined && rolePaths.has(source) && (code === "ROLE_FRONTMATTER" || code === "AGENT_RESOURCE_SELECTOR_MIGRATION")))
|
|
501
|
+
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."));
|
|
502
|
+
let roleInspection;
|
|
503
|
+
if (options.role !== undefined) {
|
|
504
|
+
const activeRole = roles.find(({ name, active }) => name === options.role && active);
|
|
505
|
+
const definition = activeRole ? definitions.get(options.role) : undefined;
|
|
506
|
+
if (!activeRole || !definition)
|
|
507
|
+
diagnostics.push(diagnostic("error", "ROLE_NOT_FOUND", `Active role not found: ${options.role}`, options.role));
|
|
508
|
+
else {
|
|
509
|
+
const rootReference = pi.model ? `${pi.model.provider}/${pi.model.model}` : pi.availableModels[0] ?? pi.knownModels[0];
|
|
510
|
+
if (!rootReference)
|
|
511
|
+
diagnostics.push(diagnostic("error", "ROLE_INSPECTION_MODEL", "Cannot inspect a role because Pi has no registered model"));
|
|
512
|
+
else {
|
|
513
|
+
let rootModel;
|
|
514
|
+
try {
|
|
515
|
+
if (pi.model) {
|
|
516
|
+
const thinking = parseThinking(pi.model.thinking);
|
|
517
|
+
rootModel = { provider: pi.model.provider, model: pi.model.model, ...(thinking ? { thinking } : {}) };
|
|
518
|
+
}
|
|
519
|
+
else
|
|
520
|
+
rootModel = resolveModelReference(rootReference, aliases, knownModels, settingsPath);
|
|
521
|
+
let roleAliases = aliases;
|
|
522
|
+
if (definition.model && isDynamicModelAlias(definition.model, dynamicAliases)) {
|
|
523
|
+
const dynamic = await registry.resolveModelAliases({ cwd, projectTrusted: pi.trust.trusted, rootModel, knownModels, availableModels, signal: new AbortController().signal });
|
|
524
|
+
roleAliases = { ...aliases, ...dynamic };
|
|
525
|
+
}
|
|
526
|
+
roleInspection = await inspectRoleSession(cwd, agentDir, options.role, definition, activeRole.path, resourcePolicy, rootModel, [...activeTools], roleAliases, knownModels, availableModels, settingsPath, settings.extensionSettings, options.prompt ?? "", registry.agentSetupHooks(), diagnostics);
|
|
527
|
+
if (roleInspection)
|
|
528
|
+
diagnostics.push(...roleInspection.setup.diagnostics);
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
diagnostics.push(diagnostic("error", "ROLE_INSPECTION_MODEL", errorText(error), activeRole.path));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const functions = [];
|
|
537
|
+
for (const [name, fn] of Object.entries(pi.functions).sort(([left], [right]) => left.localeCompare(right))) {
|
|
538
|
+
functions.push({ name, description: fn.description, valid: true });
|
|
539
|
+
}
|
|
540
|
+
const severityOrder = { error: 0, warning: 1 };
|
|
541
|
+
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));
|
|
542
|
+
roles.sort((left, right) => left.name.localeCompare(right.name) || left.scope.localeCompare(right.scope));
|
|
543
|
+
return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), piExtensions: [...new Set((pi.extensions ?? []).map(canonicalPath))].sort(), piSkills: [...new Set(pi.skills ?? [])].sort(), roles, functions, modelAliases, resourcePolicy, ...(options.role !== undefined ? { roleTarget: options.role } : {}), ...(roleInspection ? { roleInspection } : {}), diagnostics };
|
|
544
|
+
}
|
|
545
|
+
function count(report, severity) { return report.diagnostics.filter((item) => item.severity === severity).length; }
|
|
546
|
+
export function doctorExitCode(report) { return count(report, "error") > 0 ? 1 : 0; }
|
|
547
|
+
function nestedValues(label, values) {
|
|
548
|
+
return [`- ${label}:`, ...(values.length ? values.map((value) => ` - \`${value}\``) : [" - (none)"])];
|
|
549
|
+
}
|
|
550
|
+
function roleSelectorSourceLines(sources) {
|
|
551
|
+
return [
|
|
552
|
+
...nestedValues("Global skill selectors", sources.global.skills ?? []),
|
|
553
|
+
...nestedValues("Global extension selectors", sources.global.extensions ?? []),
|
|
554
|
+
...nestedValues("Global tool selectors", sources.global.tools ?? []),
|
|
555
|
+
...nestedValues("Project skill selectors", sources.project.skills ?? []),
|
|
556
|
+
...nestedValues("Project extension selectors", sources.project.extensions ?? []),
|
|
557
|
+
...nestedValues("Project tool selectors", sources.project.tools ?? []),
|
|
558
|
+
...(sources.role === undefined ? [] : [
|
|
559
|
+
...nestedValues("Role skill selectors", sources.role.skills ?? []),
|
|
560
|
+
...nestedValues("Role extension selectors", sources.role.extensions ?? []),
|
|
561
|
+
...nestedValues("Role tool selectors", sources.role.tools ?? []),
|
|
562
|
+
]),
|
|
563
|
+
...(sources.call === undefined ? [] : [
|
|
564
|
+
...nestedValues("Call skill selectors", sources.call.skills ?? []),
|
|
565
|
+
...nestedValues("Call extension selectors", sources.call.extensions ?? []),
|
|
566
|
+
...nestedValues("Call tool selectors", sources.call.tools ?? []),
|
|
567
|
+
]),
|
|
568
|
+
];
|
|
569
|
+
}
|
|
570
|
+
function roleInspectionLines(inspection) {
|
|
571
|
+
return [
|
|
572
|
+
`- Role: \`${inspection.role}\` - \`${inspection.path}\``,
|
|
573
|
+
`- Model: \`${inspection.model.provider}/${inspection.model.model}\` (${inspection.model.inherited ? "inherited, " : ""}${inspection.model.thinking ?? "off"})`,
|
|
574
|
+
...(inspection.resources.selectorSources ? roleSelectorSourceLines(inspection.resources.selectorSources) : []),
|
|
575
|
+
...nestedValues("Tools", inspection.tools),
|
|
576
|
+
...nestedValues("Configured skill selectors", inspection.resources.selectors.skills),
|
|
577
|
+
...nestedValues("Effective skills", inspection.resources.skills),
|
|
578
|
+
...nestedValues("Configured extension selectors", inspection.resources.selectors.extensions),
|
|
579
|
+
...nestedValues("Effective extensions", inspection.resources.extensions),
|
|
580
|
+
...nestedValues("Configured tool selectors", inspection.resources.selectors.tools),
|
|
581
|
+
...nestedValues("Effective tools", inspection.resources.tools),
|
|
582
|
+
...nestedValues("Unmatched skills", inspection.resources.unmatchedSkills),
|
|
583
|
+
...nestedValues("Unmatched extensions", inspection.resources.unmatchedExtensions),
|
|
584
|
+
...nestedValues("Unmatched tools", inspection.resources.unmatchedTools),
|
|
585
|
+
`- Prompt probe: ${inspection.systemPrompt.probe ? JSON.stringify(inspection.systemPrompt.probe) : "empty"}`,
|
|
586
|
+
`- Expanded probe: ${JSON.stringify(inspection.systemPrompt.expandedProbe)}`,
|
|
587
|
+
`- System prompt source: ${inspection.systemPrompt.source ?? "(none)"}`,
|
|
588
|
+
"### Final system prompt",
|
|
589
|
+
"```",
|
|
590
|
+
inspection.systemPrompt.text,
|
|
591
|
+
"```",
|
|
592
|
+
...nestedValues("Applied setup hooks", inspection.setup.hooks),
|
|
593
|
+
`- Setup diagnostics: ${String(inspection.setup.diagnostics.length)}`,
|
|
594
|
+
];
|
|
595
|
+
}
|
|
596
|
+
export function formatDoctorReport(report) {
|
|
597
|
+
if (report.roleInspection || report.roleTarget !== undefined) {
|
|
598
|
+
const lines = [
|
|
599
|
+
"# pi-extensible-workflows doctor",
|
|
600
|
+
"",
|
|
601
|
+
"## Role inspection",
|
|
602
|
+
...(report.roleInspection ? roleInspectionLines(report.roleInspection) : [`- Role: \`${report.roleTarget ?? "(unknown)"}\``, "- Inspection unavailable"]),
|
|
603
|
+
"",
|
|
604
|
+
"## Diagnostics",
|
|
605
|
+
...(report.diagnostics.length ? report.diagnostics.map((item) => `- [${item.severity}] ${item.code}${item.source ? ` \`${item.source}\`` : ""}: ${item.message}${item.hint ? ` Fix: ${item.hint}` : ""}`) : ["- [ok] No diagnostics"]),
|
|
606
|
+
"",
|
|
607
|
+
"## Summary",
|
|
608
|
+
`- ${String(count(report, "error"))} error(s), ${String(count(report, "warning"))} warning(s)`,
|
|
609
|
+
];
|
|
610
|
+
return `${lines.join("\n")}\n`;
|
|
611
|
+
}
|
|
612
|
+
const roleLoadingFailed = report.diagnostics.some(({ code }) => code === "ROLE_LOAD_BLOCKED");
|
|
613
|
+
const lines = [
|
|
614
|
+
"# pi-extensible-workflows doctor",
|
|
615
|
+
"",
|
|
616
|
+
"## Environment",
|
|
617
|
+
`- CWD: \`${report.cwd}\``,
|
|
618
|
+
`- Agent dir: \`${report.agentDir}\``,
|
|
619
|
+
`- Global workflow settings: \`${report.settingsPath}\``,
|
|
620
|
+
`- Project workflow settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
|
|
621
|
+
`- Effective setting sources: concurrency=\`${report.settingsSources.concurrency}\`, modelAliases=\`${report.settingsSources.modelAliases}\`, skills=\`${report.settingsSources.skills ?? "(none)"}\`, extensions=\`${report.settingsSources.extensions ?? "(none)"}\`, extensionSettings=\`${report.settingsSources.extensionSettings ?? "(none)"}\`, tools=\`${report.settingsSources.tools ?? "(none)"}\``,
|
|
622
|
+
`- Limits: concurrency=${String(report.settings.concurrency)}`,
|
|
623
|
+
"",
|
|
624
|
+
"## Trust/resources",
|
|
625
|
+
`- [${report.trust.trusted ? "ok" : "warning"}] ${report.trust.source}`,
|
|
626
|
+
"",
|
|
627
|
+
"## Pi active tools",
|
|
628
|
+
...(report.activeTools.length ? report.activeTools.map((tool) => `- \`${tool}\``) : ["- None resolved"]),
|
|
629
|
+
"",
|
|
630
|
+
"## Pi active extensions",
|
|
631
|
+
...(report.piExtensions.length ? report.piExtensions.map((extension) => `- \`${extension}\``) : ["- None resolved"]),
|
|
632
|
+
"",
|
|
633
|
+
"## Pi active skills",
|
|
634
|
+
...(report.piSkills.length ? report.piSkills.map((skill) => `- \`${skill}\``) : ["- None resolved"]),
|
|
635
|
+
"",
|
|
636
|
+
"## Workflow agent resource selectors",
|
|
637
|
+
`- Global settings: \`${report.resourcePolicy.globalSettingsPath}\``,
|
|
638
|
+
`- Global skills: ${report.resourcePolicy.global.skills.join(", ") || "(none)"}`,
|
|
639
|
+
`- Global extensions: ${report.resourcePolicy.global.extensions.join(", ") || "(none)"}`,
|
|
640
|
+
`- Global tools: ${(report.resourcePolicy.global.tools ?? []).join(", ") || "(none)"}`,
|
|
641
|
+
`- Project settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
|
|
642
|
+
`- Project skills: ${report.resourcePolicy.project.skills.join(", ") || "(none)"}`,
|
|
643
|
+
`- Project extensions: ${report.resourcePolicy.project.extensions.join(", ") || "(none)"}`,
|
|
644
|
+
`- Project tools: ${(report.resourcePolicy.project.tools ?? []).join(", ") || "(none)"}`,
|
|
645
|
+
`- Effective skills: ${(report.resourcePolicy.selectedSkills ?? []).join(", ") || "(none)"}`,
|
|
646
|
+
`- Effective extensions: ${(report.resourcePolicy.selectedExtensions ?? []).join(", ") || "(none)"}`,
|
|
647
|
+
`- Effective tools: ${(report.resourcePolicy.selectedTools ?? []).join(", ") || "(none)"}`,
|
|
648
|
+
`- Unmatched skills: ${report.resourcePolicy.unmatchedSkills.join(", ") || "(none)"}`,
|
|
649
|
+
`- Unmatched extensions: ${report.resourcePolicy.unmatchedExtensions.join(", ") || "(none)"}`,
|
|
650
|
+
`- Unmatched tools: ${(report.resourcePolicy.unmatchedTools ?? []).join(", ") || "(none)"}`,
|
|
651
|
+
"",
|
|
652
|
+
"## Roles",
|
|
653
|
+
...(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"]),
|
|
654
|
+
"",
|
|
655
|
+
"## Model aliases",
|
|
656
|
+
...(report.modelAliases.length ? report.modelAliases.map((alias) => `- [${alias.kind}] \`${alias.name}\`${alias.kind === "static" ? ` -> ${report.settings.modelAliases?.[alias.name] ?? "(unresolved)"}` : ""} (${alias.provenance})`) : ["- None registered"]),
|
|
657
|
+
"",
|
|
658
|
+
"## Reusable functions",
|
|
659
|
+
...(report.functions.length ? report.functions.map((fn) => `- [${fn.valid ? "ok" : "error"}] \`${fn.name}\` - ${fn.description}`) : ["- None registered"]),
|
|
660
|
+
"",
|
|
661
|
+
"## Diagnostics",
|
|
662
|
+
...(report.diagnostics.length ? report.diagnostics.map((item) => `- [${item.severity}] ${item.code}${item.source ? ` \`${item.source}\`` : ""}: ${item.message}${item.hint ? ` Fix: ${item.hint}` : ""}`) : ["- [ok] No diagnostics"]),
|
|
663
|
+
"",
|
|
664
|
+
"## Summary",
|
|
665
|
+
`- ${String(count(report, "error"))} error(s), ${String(count(report, "warning"))} warning(s)`,
|
|
666
|
+
];
|
|
667
|
+
return `${lines.join("\n")}\n`;
|
|
668
|
+
}
|