@piewf/cli 3.4.2
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 +53 -0
- package/dist/src/bundles.js +458 -0
- package/dist/src/cli.d.ts +19 -0
- package/dist/src/cli.js +746 -0
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +639 -0
- package/dist/src/doctor.d.ts +67 -0
- package/dist/src/doctor.js +397 -0
- package/dist/src/session-inspector.d.ts +82 -0
- package/dist/src/session-inspector.js +445 -0
- package/package.json +53 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type AgentResourcePolicy, type WorkflowCatalogModelAlias, type WorkflowExtensionMetadata, type WorkflowFunction, type WorkflowSettings, type WorkflowSettingsSources } from "pi-extensible-workflows";
|
|
2
|
+
import { type WorkflowRegistryApi } from "pi-extensible-workflows";
|
|
3
|
+
export type DoctorSeverity = "error" | "warning";
|
|
4
|
+
export interface DoctorDiagnostic {
|
|
5
|
+
severity: DoctorSeverity;
|
|
6
|
+
code: string;
|
|
7
|
+
message: string;
|
|
8
|
+
source?: string;
|
|
9
|
+
hint?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DoctorRole {
|
|
12
|
+
name: string;
|
|
13
|
+
path: string;
|
|
14
|
+
scope: "extension" | "global" | "project";
|
|
15
|
+
active: boolean;
|
|
16
|
+
overrides?: string;
|
|
17
|
+
overriddenBy?: string;
|
|
18
|
+
extension?: WorkflowExtensionMetadata;
|
|
19
|
+
}
|
|
20
|
+
export interface DoctorFunction {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
valid: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface DoctorTrust {
|
|
26
|
+
required: boolean;
|
|
27
|
+
trusted: boolean;
|
|
28
|
+
source: string;
|
|
29
|
+
}
|
|
30
|
+
export interface DoctorPiState {
|
|
31
|
+
trust: DoctorTrust;
|
|
32
|
+
activeTools: readonly string[];
|
|
33
|
+
knownModels: readonly string[];
|
|
34
|
+
availableModels: readonly string[];
|
|
35
|
+
extensionErrors: readonly {
|
|
36
|
+
path?: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}[];
|
|
39
|
+
extensions?: readonly string[];
|
|
40
|
+
skills?: readonly string[];
|
|
41
|
+
functions: Readonly<Record<string, WorkflowFunction>>;
|
|
42
|
+
}
|
|
43
|
+
export interface DoctorReport {
|
|
44
|
+
cwd: string;
|
|
45
|
+
agentDir: string;
|
|
46
|
+
settingsPath: string;
|
|
47
|
+
settings: Readonly<WorkflowSettings>;
|
|
48
|
+
settingsSources: WorkflowSettingsSources;
|
|
49
|
+
trust: DoctorTrust;
|
|
50
|
+
activeTools: readonly string[];
|
|
51
|
+
roles: readonly DoctorRole[];
|
|
52
|
+
functions: readonly DoctorFunction[];
|
|
53
|
+
resourcePolicy: AgentResourcePolicy;
|
|
54
|
+
modelAliases: readonly WorkflowCatalogModelAlias[];
|
|
55
|
+
diagnostics: readonly DoctorDiagnostic[];
|
|
56
|
+
}
|
|
57
|
+
export interface DoctorOptions {
|
|
58
|
+
cwd?: string;
|
|
59
|
+
agentDir?: string;
|
|
60
|
+
settingsPath?: string;
|
|
61
|
+
discoverPi?: (cwd: string, agentDir: string) => Promise<DoctorPiState>;
|
|
62
|
+
activeTools?: readonly string[];
|
|
63
|
+
registry?: WorkflowRegistryApi;
|
|
64
|
+
}
|
|
65
|
+
export declare function doctor(options?: DoctorOptions): Promise<DoctorReport>;
|
|
66
|
+
export declare function doctorExitCode(report: DoctorReport): 0 | 1;
|
|
67
|
+
export declare function formatDoctorReport(report: DoctorReport): string;
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
|
+
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
4
|
+
import { ModelRuntime, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { DEFAULT_SETTINGS, loadSettings, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
|
|
6
|
+
import { loadingRegistry } from "pi-extensible-workflows";
|
|
7
|
+
import { disabledResources, unmatchedResourcePatterns } from "pi-extensible-workflows";
|
|
8
|
+
const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
|
|
9
|
+
function canonical(path) {
|
|
10
|
+
const absolute = resolve(path);
|
|
11
|
+
try {
|
|
12
|
+
return realpathSync(absolute);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return absolute;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function isDynamicModelAlias(value, aliases) {
|
|
19
|
+
const match = /^([^/\s:]+)(?::([^\s]+))?$/.exec(value);
|
|
20
|
+
const name = match?.[1];
|
|
21
|
+
return Boolean(name && (match[2] === undefined || parseThinking(match[2]) !== undefined) && aliases.has(name));
|
|
22
|
+
}
|
|
23
|
+
async function readCredentials(agentDir) {
|
|
24
|
+
const credentials = new InMemoryCredentialStore();
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(readFileSync(join(agentDir, "auth.json"), "utf8"));
|
|
27
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
28
|
+
throw new Error("Pi auth.json must be an object");
|
|
29
|
+
await Promise.all(Object.entries(parsed).map(([provider, credential]) => credentials.modify(provider, async () => credential)));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== "ENOENT")
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
return credentials;
|
|
36
|
+
}
|
|
37
|
+
function savedTrust(cwd, agentDir) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(readFileSync(join(agentDir, "trust.json"), "utf8"));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code === "ENOENT")
|
|
44
|
+
return undefined;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
48
|
+
throw new Error("Pi trust.json must be an object");
|
|
49
|
+
let current = canonical(cwd);
|
|
50
|
+
while (current !== dirname(current)) {
|
|
51
|
+
const value = parsed[current];
|
|
52
|
+
if (value === true || value === false)
|
|
53
|
+
return value;
|
|
54
|
+
current = dirname(current);
|
|
55
|
+
}
|
|
56
|
+
const value = parsed[current];
|
|
57
|
+
return value === true || value === false ? value : undefined;
|
|
58
|
+
}
|
|
59
|
+
async function discoverPi(cwd, agentDir) {
|
|
60
|
+
const required = hasTrustRequiringProjectResources(cwd);
|
|
61
|
+
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: false });
|
|
62
|
+
const saved = required ? savedTrust(cwd, agentDir) : true;
|
|
63
|
+
const fallback = settingsManager.getDefaultProjectTrust();
|
|
64
|
+
const trusted = !required || saved !== undefined ? Boolean(saved) : fallback === "always";
|
|
65
|
+
const source = !required ? "no trust-gated project resources" : saved !== undefined ? "saved Pi trust decision" : `headless defaultProjectTrust=${fallback}`;
|
|
66
|
+
const previousOffline = process.env.PI_OFFLINE;
|
|
67
|
+
process.env.PI_OFFLINE = "1";
|
|
68
|
+
try {
|
|
69
|
+
const modelRuntime = await ModelRuntime.create({ credentials: await readCredentials(agentDir), modelsPath: join(agentDir, "models.json") });
|
|
70
|
+
const services = await createAgentSessionServices({
|
|
71
|
+
cwd,
|
|
72
|
+
agentDir,
|
|
73
|
+
settingsManager,
|
|
74
|
+
modelRuntime,
|
|
75
|
+
resourceLoaderOptions: { noPromptTemplates: true, noThemes: true, noContextFiles: true },
|
|
76
|
+
resourceLoaderReloadOptions: { resolveProjectTrust: async () => trusted },
|
|
77
|
+
});
|
|
78
|
+
const allModels = services.modelRuntime.getModels();
|
|
79
|
+
const availableModels = await services.modelRuntime.getAvailable();
|
|
80
|
+
const model = availableModels[0] ?? allModels[0];
|
|
81
|
+
if (!model)
|
|
82
|
+
throw new Error("Pi has no models registered");
|
|
83
|
+
const { session } = await createAgentSessionFromServices({ services, sessionManager: SessionManager.inMemory(), model });
|
|
84
|
+
const activeTools = session.agent.state.tools.map(({ name }) => name).filter((name) => name !== "workflow" && name !== "workflow_respond" && name !== "workflow_catalog");
|
|
85
|
+
const extensions = services.resourceLoader.getExtensions();
|
|
86
|
+
const skills = services.resourceLoader.getSkills().skills;
|
|
87
|
+
return {
|
|
88
|
+
trust: { required, trusted, source },
|
|
89
|
+
activeTools,
|
|
90
|
+
knownModels: allModels.map(({ provider, id }) => `${provider}/${id}`),
|
|
91
|
+
availableModels: availableModels.map(({ provider, id }) => `${provider}/${id}`),
|
|
92
|
+
extensions: extensions.extensions.map(({ resolvedPath }) => resolvedPath),
|
|
93
|
+
skills: skills.map(({ name }) => name),
|
|
94
|
+
extensionErrors: [
|
|
95
|
+
...extensions.errors.map(({ path, error }) => ({ path, message: error })),
|
|
96
|
+
...services.diagnostics.filter(({ type }) => type === "error").map(({ message }) => ({ message })),
|
|
97
|
+
],
|
|
98
|
+
functions: registeredWorkflowFunctions(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (previousOffline === undefined)
|
|
103
|
+
delete process.env.PI_OFFLINE;
|
|
104
|
+
else
|
|
105
|
+
process.env.PI_OFFLINE = previousOffline;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function roleFiles(dir) {
|
|
109
|
+
try {
|
|
110
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && extname(entry.name) === ".md").map((entry) => join(dir, entry.name)).sort();
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error.code === "ENOENT")
|
|
114
|
+
return [];
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function roleFilesFrom(dirs) {
|
|
119
|
+
const paths = dirs.flatMap((dir) => roleFiles(dir));
|
|
120
|
+
return [...new Map(paths.map((path) => [basename(path, ".md"), path])).values()].sort();
|
|
121
|
+
}
|
|
122
|
+
function extensionLabel(extension) { return `Extension "${extension.headline}" (${extension.version})`; }
|
|
123
|
+
function scanExtensionRoleFiles(registrations) {
|
|
124
|
+
const files = [];
|
|
125
|
+
const empty = [];
|
|
126
|
+
const errors = [];
|
|
127
|
+
for (const registration of registrations) {
|
|
128
|
+
try {
|
|
129
|
+
const entries = readdirSync(registration.path, { withFileTypes: true });
|
|
130
|
+
const roleFiles = entries.filter((entry) => entry.isFile() && extname(entry.name) === ".md");
|
|
131
|
+
if (!roleFiles.length)
|
|
132
|
+
empty.push(registration);
|
|
133
|
+
for (const entry of roleFiles)
|
|
134
|
+
files.push({ name: basename(entry.name, ".md"), path: join(registration.path, entry.name), directory: registration.path, extension: registration.extension });
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
errors.push({ registration, error });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
|
|
141
|
+
return { files, empty, errors };
|
|
142
|
+
}
|
|
143
|
+
function roleProvenance(source) {
|
|
144
|
+
return source ? `${extensionLabel(source.extension)} role directory "${source.directory}"` : "Role";
|
|
145
|
+
}
|
|
146
|
+
function diagnostic(severity, code, message, source, hint) {
|
|
147
|
+
return { severity, code, message, ...(source ? { source } : {}), ...(hint ? { hint } : {}) };
|
|
148
|
+
}
|
|
149
|
+
function emptyResourcePolicy(globalSettingsPath, cwd, projectTrusted) {
|
|
150
|
+
const empty = { skills: [], extensions: [] };
|
|
151
|
+
return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: empty, unmatchedSkills: [], unmatchedExtensions: [] };
|
|
152
|
+
}
|
|
153
|
+
function validateModel(value, known, available, source, diagnostics, aliases, dynamicAliases, settingsPath) {
|
|
154
|
+
if (isDynamicModelAlias(value, dynamicAliases))
|
|
155
|
+
return;
|
|
156
|
+
try {
|
|
157
|
+
const parsed = resolveModelReference(value, aliases, known, settingsPath);
|
|
158
|
+
const name = `${parsed.provider}/${parsed.model}`;
|
|
159
|
+
if (!known.has(name) || !available.has(name))
|
|
160
|
+
diagnostics.push(diagnostic("warning", "MODEL_UNAVAILABLE", `Model is valid-shaped but unavailable: ${name}`, source));
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
diagnostics.push(diagnostic("error", "MODEL_INVALID", error.message, source, error.message.includes("thinking") ? THINKING_HINT : "Use provider/model or provider/model:thinking."));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
|
|
167
|
+
let definition;
|
|
168
|
+
try {
|
|
169
|
+
definition = parseRoleMarkdown(readFileSync(path, "utf8"), true, path);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
+
diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", source ? `${roleProvenance(source)} contains invalid role at "${path}": ${message}` : message, path, "Fix the role YAML frontmatter."));
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const body = definition.prompt ?? "";
|
|
177
|
+
if (body.trim() === "")
|
|
178
|
+
diagnostics.push(diagnostic("warning", "ROLE_BODY_EMPTY", "Role body is empty", path));
|
|
179
|
+
if (Buffer.byteLength(body) > 50 * 1024)
|
|
180
|
+
diagnostics.push(diagnostic("warning", "ROLE_BODY_LARGE", "Role body exceeds 50KB", path));
|
|
181
|
+
if (/{{\s*[^{}]+\s*}}/.test(body))
|
|
182
|
+
diagnostics.push(diagnostic("warning", "ROLE_PLACEHOLDER", "Role body contains an unsupported placeholder-looking token", path));
|
|
183
|
+
if (definition.model)
|
|
184
|
+
validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, dynamicAliases, settingsPath);
|
|
185
|
+
for (const tool of definition.tools ?? [])
|
|
186
|
+
if (!activeTools.has(tool))
|
|
187
|
+
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."));
|
|
188
|
+
return definition;
|
|
189
|
+
}
|
|
190
|
+
function matchResourcePolicy(policy, pi) {
|
|
191
|
+
const extensions = [...new Set((pi.extensions ?? []).map(canonical))];
|
|
192
|
+
const skills = [...new Set(pi.skills ?? [])];
|
|
193
|
+
return { ...policy, excludedSkills: disabledResources(policy.effective.skills, skills), excludedExtensions: disabledResources(policy.effective.extensions, extensions), unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, skills), unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, extensions) };
|
|
194
|
+
}
|
|
195
|
+
function resourcePolicySource(settingsSource) { return settingsSource; }
|
|
196
|
+
export async function doctor(options = {}) {
|
|
197
|
+
const cwd = canonical(options.cwd ?? process.cwd());
|
|
198
|
+
const agentDir = canonical(options.agentDir ?? getAgentDir());
|
|
199
|
+
const settingsPath = canonical(options.settingsPath ?? workflowSettingsPath(agentDir));
|
|
200
|
+
const diagnostics = [];
|
|
201
|
+
let settings = DEFAULT_SETTINGS;
|
|
202
|
+
try {
|
|
203
|
+
settings = loadSettings(settingsPath);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", error.message, settingsPath, "Fix or remove the invalid workflow settings file."));
|
|
207
|
+
}
|
|
208
|
+
let settingsSources = { concurrency: settingsPath, modelAliases: settingsPath, disabledAgentResources: settingsPath };
|
|
209
|
+
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
210
|
+
let pi;
|
|
211
|
+
try {
|
|
212
|
+
pi = await (options.discoverPi ?? discoverPi)(cwd, agentDir);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
diagnostics.push(diagnostic("error", "PI_DISCOVERY", `Pi headless discovery failed: ${error.message}`, undefined, "Open and trust the project in Pi, fix extension errors, then rerun doctor."));
|
|
216
|
+
pi = { trust: { required: false, trusted: false, source: "discovery failed" }, activeTools: [], knownModels: [], availableModels: [], extensionErrors: [], functions: {} };
|
|
217
|
+
}
|
|
218
|
+
if (options.activeTools)
|
|
219
|
+
pi = { ...pi, activeTools: options.activeTools.filter((tool) => tool !== "workflow" && tool !== "workflow_respond" && tool !== "workflow_catalog") };
|
|
220
|
+
if (pi.trust.required && !pi.trust.trusted)
|
|
221
|
+
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."));
|
|
222
|
+
for (const error of pi.extensionErrors)
|
|
223
|
+
diagnostics.push(diagnostic("error", "EXTENSION_LOAD", error.message, error.path, "Fix or disable the failing Pi extension."));
|
|
224
|
+
try {
|
|
225
|
+
const resolved = resolveWorkflowSettings(cwd, pi.trust.trusted, settingsPath);
|
|
226
|
+
settings = resolved.effective;
|
|
227
|
+
settingsSources = resolved.sources;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
231
|
+
const source = message.includes(projectSettingsPath) ? projectSettingsPath : settingsPath;
|
|
232
|
+
if (!diagnostics.some(({ code, source: itemSource }) => code === "SETTINGS_INVALID" && itemSource === source))
|
|
233
|
+
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, source, "Fix or remove the invalid workflow settings file."));
|
|
234
|
+
}
|
|
235
|
+
let resourcePolicy;
|
|
236
|
+
try {
|
|
237
|
+
resourcePolicy = matchResourcePolicy(resolveAgentResourcePolicy(cwd, pi.trust.trusted, settingsPath), pi);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
241
|
+
const source = message.includes(projectSettingsPath) ? projectSettingsPath : settingsPath;
|
|
242
|
+
if (!diagnostics.some(({ code, source: itemSource }) => code === "SETTINGS_INVALID" && itemSource === source))
|
|
243
|
+
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, source, "Fix or remove the invalid workflow settings file."));
|
|
244
|
+
resourcePolicy = emptyResourcePolicy(settingsPath, cwd, pi.trust.trusted);
|
|
245
|
+
}
|
|
246
|
+
for (const skill of resourcePolicy.unmatchedSkills)
|
|
247
|
+
diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled skill selector currently matches no discovered skill: ${skill}`, `${resourcePolicySource(settingsSources.disabledAgentResources)}.disabledAgentResources.skills`));
|
|
248
|
+
for (const extension of resourcePolicy.unmatchedExtensions)
|
|
249
|
+
diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled extension selector currently matches no discovered extension source: ${extension}`, `${resourcePolicySource(settingsSources.disabledAgentResources)}.disabledAgentResources.extensions`));
|
|
250
|
+
const activeTools = new Set(pi.activeTools);
|
|
251
|
+
const knownModels = new Set(pi.knownModels);
|
|
252
|
+
const availableModels = new Set(pi.availableModels);
|
|
253
|
+
const aliases = settings.modelAliases ?? {};
|
|
254
|
+
const registry = options.registry ?? loadingRegistry();
|
|
255
|
+
const registeredModelAliases = registry.modelAliases();
|
|
256
|
+
const dynamicAliases = new Set(registeredModelAliases.map(({ name }) => name).filter((name) => !Object.prototype.hasOwnProperty.call(aliases, name)));
|
|
257
|
+
const modelAliases = [
|
|
258
|
+
...Object.keys(aliases).map((name) => ({ name, kind: "static", provenance: settingsSources.modelAliases })),
|
|
259
|
+
...registeredModelAliases.map(({ name, version, headline, extensionDescription }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline, extensionDescription })),
|
|
260
|
+
].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind));
|
|
261
|
+
const roles = [];
|
|
262
|
+
const definitions = new Map();
|
|
263
|
+
const extensionScan = scanExtensionRoleFiles(registeredWorkflowRoleDirectoryRegistrations());
|
|
264
|
+
for (const { registration, error } of extensionScan.errors) {
|
|
265
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
266
|
+
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."));
|
|
267
|
+
}
|
|
268
|
+
for (const registration of extensionScan.empty)
|
|
269
|
+
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."));
|
|
270
|
+
const extensionFilesByName = new Map();
|
|
271
|
+
for (const file of extensionScan.files)
|
|
272
|
+
extensionFilesByName.set(file.name, [...(extensionFilesByName.get(file.name) ?? []), file]);
|
|
273
|
+
const duplicateExtensionNames = new Set();
|
|
274
|
+
for (const [name, matches] of extensionFilesByName)
|
|
275
|
+
if (matches.length > 1) {
|
|
276
|
+
duplicateExtensionNames.add(name);
|
|
277
|
+
diagnostics.push(diagnostic("error", "ROLE_DUPLICATE", `Duplicate extension role "${name}": ${matches.map(({ path, directory, extension }) => `${extensionLabel(extension)} role directory "${directory}" (${path})`).join("; ")}`, matches[0]?.path, "Keep one extension role with this name; global and project roles may override packaged defaults."));
|
|
278
|
+
}
|
|
279
|
+
const extensionPaths = new Map();
|
|
280
|
+
for (const file of extensionScan.files) {
|
|
281
|
+
roles.push({ name: file.name, path: file.path, scope: "extension", active: true, extension: file.extension });
|
|
282
|
+
const definition = inspectRole(file.path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, { directory: file.directory, extension: file.extension });
|
|
283
|
+
if (duplicateExtensionNames.has(file.name))
|
|
284
|
+
continue;
|
|
285
|
+
extensionPaths.set(file.name, file.path);
|
|
286
|
+
if (definition)
|
|
287
|
+
definitions.set(file.name, definition);
|
|
288
|
+
}
|
|
289
|
+
const globalPaths = new Map();
|
|
290
|
+
const globalRoleDirs = workflowRoleDirectories(agentDir);
|
|
291
|
+
for (const path of roleFilesFrom(globalRoleDirs)) {
|
|
292
|
+
const name = basename(path, ".md");
|
|
293
|
+
const extensionPath = extensionPaths.get(name);
|
|
294
|
+
roles.push({ name, path, scope: "global", active: true, ...(extensionPath ? { overrides: extensionPath } : {}) });
|
|
295
|
+
globalPaths.set(name, path);
|
|
296
|
+
if (extensionPath) {
|
|
297
|
+
const extension = roles.find((role) => role.path === extensionPath);
|
|
298
|
+
if (extension) {
|
|
299
|
+
extension.active = false;
|
|
300
|
+
extension.overriddenBy = path;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
|
|
304
|
+
if (definition)
|
|
305
|
+
definitions.set(name, definition);
|
|
306
|
+
else
|
|
307
|
+
definitions.delete(name);
|
|
308
|
+
}
|
|
309
|
+
for (const path of roleFilesFrom([join(cwd, ".pi", "pi-extensible-workflows", "roles")])) {
|
|
310
|
+
const name = basename(path, ".md");
|
|
311
|
+
const globalPath = globalPaths.get(name);
|
|
312
|
+
const extensionPath = extensionPaths.get(name);
|
|
313
|
+
const overriddenPath = globalPath ?? extensionPath;
|
|
314
|
+
const active = pi.trust.trusted;
|
|
315
|
+
roles.push({ name, path, scope: "project", active, ...(active && overriddenPath ? { overrides: overriddenPath } : {}) });
|
|
316
|
+
if (!active)
|
|
317
|
+
continue;
|
|
318
|
+
if (globalPath) {
|
|
319
|
+
const global = roles.find((role) => role.path === globalPath);
|
|
320
|
+
if (global) {
|
|
321
|
+
global.active = false;
|
|
322
|
+
global.overriddenBy = path;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
else if (extensionPath) {
|
|
326
|
+
const extension = roles.find((role) => role.path === extensionPath);
|
|
327
|
+
if (extension) {
|
|
328
|
+
extension.active = false;
|
|
329
|
+
extension.overriddenBy = path;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
|
|
333
|
+
if (definition)
|
|
334
|
+
definitions.set(name, definition);
|
|
335
|
+
else
|
|
336
|
+
definitions.delete(name);
|
|
337
|
+
}
|
|
338
|
+
const functions = [];
|
|
339
|
+
for (const [name, fn] of Object.entries(pi.functions).sort(([left], [right]) => left.localeCompare(right))) {
|
|
340
|
+
functions.push({ name, description: fn.description, valid: true });
|
|
341
|
+
}
|
|
342
|
+
const severityOrder = { error: 0, warning: 1 };
|
|
343
|
+
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));
|
|
344
|
+
roles.sort((left, right) => left.name.localeCompare(right.name) || left.scope.localeCompare(right.scope));
|
|
345
|
+
return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), roles, functions, modelAliases, resourcePolicy, diagnostics };
|
|
346
|
+
}
|
|
347
|
+
function count(report, severity) { return report.diagnostics.filter((item) => item.severity === severity).length; }
|
|
348
|
+
export function doctorExitCode(report) { return count(report, "error") > 0 ? 1 : 0; }
|
|
349
|
+
export function formatDoctorReport(report) {
|
|
350
|
+
const lines = [
|
|
351
|
+
"# pi-extensible-workflows doctor",
|
|
352
|
+
"",
|
|
353
|
+
"## Environment",
|
|
354
|
+
`- CWD: \`${report.cwd}\``,
|
|
355
|
+
`- Agent dir: \`${report.agentDir}\``,
|
|
356
|
+
`- Global workflow settings: \`${report.settingsPath}\``,
|
|
357
|
+
`- Project workflow settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
|
|
358
|
+
`- Effective setting sources: concurrency=\`${report.settingsSources.concurrency}\`, modelAliases=\`${report.settingsSources.modelAliases}\`, disabledAgentResources=\`${report.settingsSources.disabledAgentResources}\``,
|
|
359
|
+
`- Limits: concurrency=${String(report.settings.concurrency)}`,
|
|
360
|
+
"",
|
|
361
|
+
"## Trust/resources",
|
|
362
|
+
`- [${report.trust.trusted ? "ok" : "warning"}] ${report.trust.source}`,
|
|
363
|
+
"",
|
|
364
|
+
"## Agent resource exclusions",
|
|
365
|
+
`- Global settings: \`${report.resourcePolicy.globalSettingsPath}\``,
|
|
366
|
+
`- Global skills: ${report.resourcePolicy.global.skills.join(", ") || "(none)"}`,
|
|
367
|
+
`- Global extensions: ${report.resourcePolicy.global.extensions.join(", ") || "(none)"}`,
|
|
368
|
+
`- Project settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
|
|
369
|
+
`- Project skills: ${report.resourcePolicy.project.skills.join(", ") || "(none)"}`,
|
|
370
|
+
`- Project extensions: ${report.resourcePolicy.project.extensions.join(", ") || "(none)"}`,
|
|
371
|
+
`- Effective skills: ${report.resourcePolicy.effective.skills.join(", ") || "(none)"}`,
|
|
372
|
+
`- Effective extensions: ${report.resourcePolicy.effective.extensions.join(", ") || "(none)"}`,
|
|
373
|
+
`- Excluded skills: ${report.resourcePolicy.excludedSkills?.join(", ") || "(none)"}`,
|
|
374
|
+
`- Excluded extensions: ${report.resourcePolicy.excludedExtensions?.join(", ") || "(none)"}`,
|
|
375
|
+
`- Unmatched skills: ${report.resourcePolicy.unmatchedSkills.join(", ") || "(none)"}`,
|
|
376
|
+
`- Unmatched extensions: ${report.resourcePolicy.unmatchedExtensions.join(", ") || "(none)"}`,
|
|
377
|
+
"",
|
|
378
|
+
"## Active tools",
|
|
379
|
+
...(report.activeTools.length ? report.activeTools.map((tool) => `- \`${tool}\``) : ["- None resolved"]),
|
|
380
|
+
"",
|
|
381
|
+
"## Roles",
|
|
382
|
+
...(report.roles.length ? report.roles.map((role) => `- \`${role.name}\` (${role.scope}, ${role.active ? "active" : role.overriddenBy ? `overridden by ${role.overriddenBy}` : "inactive: project untrusted"}) - \`${role.path}\`${role.extension ? `; ${extensionLabel(role.extension)} role directory "${dirname(role.path)}"` : ""}${role.overrides ? `; overrides \`${role.overrides}\`` : ""}`) : ["- None found"]),
|
|
383
|
+
"",
|
|
384
|
+
"## Model aliases",
|
|
385
|
+
...(report.modelAliases.length ? report.modelAliases.map((alias) => `- [${alias.kind}] \`${alias.name}\`${alias.kind === "static" ? ` -> ${report.settings.modelAliases?.[alias.name] ?? "(unresolved)"}` : ""} (${alias.provenance})`) : ["- None registered"]),
|
|
386
|
+
"",
|
|
387
|
+
"## Reusable functions",
|
|
388
|
+
...(report.functions.length ? report.functions.map((fn) => `- [${fn.valid ? "ok" : "error"}] \`${fn.name}\` - ${fn.description}`) : ["- None registered"]),
|
|
389
|
+
"",
|
|
390
|
+
"## Diagnostics",
|
|
391
|
+
...(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"]),
|
|
392
|
+
"",
|
|
393
|
+
"## Summary",
|
|
394
|
+
`- ${String(count(report, "error"))} error(s), ${String(count(report, "warning"))} warning(s)`,
|
|
395
|
+
];
|
|
396
|
+
return `${lines.join("\n")}\n`;
|
|
397
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type SessionEntry, type SessionInfo } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type AgentSetupSummary, type ModelSpec, type StaticWorkflowCall } from "pi-extensible-workflows";
|
|
3
|
+
import { type PersistedRun, type RunSummary } from "pi-extensible-workflows/persistence";
|
|
4
|
+
export interface ModelUsage {
|
|
5
|
+
model: string;
|
|
6
|
+
cost: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AttemptReport {
|
|
9
|
+
attempt: number;
|
|
10
|
+
prompt: string;
|
|
11
|
+
model: string;
|
|
12
|
+
thinking?: ModelSpec["thinking"];
|
|
13
|
+
cost: number;
|
|
14
|
+
models: readonly ModelUsage[];
|
|
15
|
+
error?: string;
|
|
16
|
+
setup?: AgentSetupSummary;
|
|
17
|
+
}
|
|
18
|
+
export interface AgentReport {
|
|
19
|
+
name: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
state: string;
|
|
22
|
+
role?: string;
|
|
23
|
+
requestedModel?: string;
|
|
24
|
+
model: string;
|
|
25
|
+
thinking?: ModelSpec["thinking"];
|
|
26
|
+
cost: number;
|
|
27
|
+
attempts: readonly AttemptReport[];
|
|
28
|
+
setup?: AgentSetupSummary;
|
|
29
|
+
}
|
|
30
|
+
export interface WorkflowReport {
|
|
31
|
+
name: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
status: string;
|
|
34
|
+
runId?: string;
|
|
35
|
+
script?: string;
|
|
36
|
+
calls: readonly StaticWorkflowCall[];
|
|
37
|
+
parseError?: string;
|
|
38
|
+
cost: number;
|
|
39
|
+
models: readonly ModelUsage[];
|
|
40
|
+
agents: readonly AgentReport[];
|
|
41
|
+
budget?: PersistedRun["budget"];
|
|
42
|
+
budgetVersion?: number;
|
|
43
|
+
usage?: PersistedRun["usage"];
|
|
44
|
+
budgetEvents?: PersistedRun["budgetEvents"];
|
|
45
|
+
events?: readonly {
|
|
46
|
+
type: string;
|
|
47
|
+
message: string;
|
|
48
|
+
}[];
|
|
49
|
+
}
|
|
50
|
+
export interface SessionReport {
|
|
51
|
+
id: string;
|
|
52
|
+
cwd: string;
|
|
53
|
+
path: string;
|
|
54
|
+
cost: number;
|
|
55
|
+
models: readonly ModelUsage[];
|
|
56
|
+
workflows: readonly WorkflowReport[];
|
|
57
|
+
totalCost: number;
|
|
58
|
+
totalModels: readonly ModelUsage[];
|
|
59
|
+
}
|
|
60
|
+
export interface InspectorViewState {
|
|
61
|
+
view: "list" | "detail" | "script";
|
|
62
|
+
selected: number;
|
|
63
|
+
scroll: number;
|
|
64
|
+
}
|
|
65
|
+
export type InspectMode = "tui" | "json" | "summary";
|
|
66
|
+
export interface PersistedSessionSummary {
|
|
67
|
+
schemaVersion: 1;
|
|
68
|
+
cwd: string;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
runs: readonly RunSummary[];
|
|
71
|
+
}
|
|
72
|
+
export declare function transcriptLines(entries: readonly SessionEntry[]): string[];
|
|
73
|
+
export declare function transcriptFileLines(path: string): string[];
|
|
74
|
+
export declare function matchSession(query: string, sessions: readonly SessionInfo[]): SessionInfo;
|
|
75
|
+
export declare function loadSessionReport(path: string, home?: string): Promise<SessionReport>;
|
|
76
|
+
export declare function renderInspector(report: SessionReport, state: InspectorViewState, width?: number, height?: number, highlighter?: (script: string) => string[]): string[];
|
|
77
|
+
export declare function loadPersistedSessionSummary(cwd: string, sessionId: string, home?: string, failedOnly?: boolean): Promise<PersistedSessionSummary>;
|
|
78
|
+
export declare function loadPersistedSummaries(cwd: string, sessionId: string | undefined, home?: string, failedOnly?: boolean): Promise<readonly PersistedSessionSummary[]>;
|
|
79
|
+
export declare function formatPersistedRunSummary(summary: RunSummary, sessionId?: string): string;
|
|
80
|
+
export declare function showSessionInspector(report: SessionReport): Promise<void>;
|
|
81
|
+
export declare function resolveSession(query: string, sessionDir?: string | undefined): Promise<SessionInfo>;
|
|
82
|
+
export declare function runSessionInspector(sessionId?: string, mode?: InspectMode, cwd?: string, home?: string, write?: (text: string) => void, failedOnly?: boolean): Promise<void>;
|