pi-extensible-workflows 3.0.0 → 3.2.0
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 +15 -17
- package/dist/src/agent-execution.d.ts +4 -80
- package/dist/src/agent-execution.js +20 -10
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +48 -2
- package/dist/src/doctor-cleanup.d.ts +41 -0
- package/dist/src/doctor-cleanup.js +597 -0
- package/dist/src/doctor.d.ts +7 -2
- package/dist/src/doctor.js +127 -22
- package/dist/src/execution.js +13 -4
- package/dist/src/host.d.ts +83 -4
- package/dist/src/host.js +1246 -410
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.js +3 -1
- package/dist/src/persistence.d.ts +3 -0
- package/dist/src/persistence.js +49 -1
- package/dist/src/registry.d.ts +21 -9
- package/dist/src/registry.js +131 -21
- package/dist/src/session-inspector.js +4 -2
- package/dist/src/types.d.ts +135 -7
- package/dist/src/utils.d.ts +6 -0
- package/dist/src/utils.js +45 -5
- package/dist/src/validation.d.ts +6 -2
- package/dist/src/validation.js +157 -31
- package/dist/src/workflow-artifacts.d.ts +13 -0
- package/dist/src/workflow-artifacts.js +39 -0
- package/examples/workflow-extension-template/README.md +37 -0
- package/examples/workflow-extension-template/extension.test.mjs +59 -0
- package/examples/workflow-extension-template/index.js +51 -0
- package/examples/workflow-extension-template/roles/reviewer.md +4 -0
- package/package.json +5 -3
- package/skills/pi-extensible-workflows/SKILL.md +45 -18
- package/src/agent-execution.ts +23 -37
- package/src/cli.ts +33 -2
- package/src/doctor-cleanup.ts +337 -0
- package/src/doctor.ts +117 -24
- package/src/execution.ts +13 -4
- package/src/host.ts +1039 -366
- package/src/index.ts +5 -2
- package/src/persistence.ts +35 -1
- package/src/registry.ts +108 -25
- package/src/session-inspector.ts +4 -2
- package/src/types.ts +53 -8
- package/src/utils.ts +39 -5
- package/src/validation.ts +130 -31
- package/src/workflow-artifacts.ts +34 -0
package/dist/src/doctor.js
CHANGED
|
@@ -2,7 +2,9 @@ import { readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
3
|
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
4
4
|
import { ModelRuntime, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { DEFAULT_SETTINGS, loadSettings, resolveAgentResourcePolicy, resolveModelReference, parseRoleMarkdown, registeredWorkflowFunctions, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "./index.js";
|
|
5
|
+
import { DEFAULT_SETTINGS, loadSettings, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "./index.js";
|
|
6
|
+
import { loadingRegistry } from "./registry.js";
|
|
7
|
+
import { disabledResources, unmatchedResourcePatterns } from "./utils.js";
|
|
6
8
|
const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
|
|
7
9
|
function canonical(path) {
|
|
8
10
|
const absolute = resolve(path);
|
|
@@ -13,6 +15,11 @@ function canonical(path) {
|
|
|
13
15
|
return absolute;
|
|
14
16
|
}
|
|
15
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
|
+
}
|
|
16
23
|
async function readCredentials(agentDir) {
|
|
17
24
|
const credentials = new InMemoryCredentialStore();
|
|
18
25
|
try {
|
|
@@ -112,6 +119,30 @@ function roleFilesFrom(dirs) {
|
|
|
112
119
|
const paths = dirs.flatMap((dir) => roleFiles(dir));
|
|
113
120
|
return [...new Map(paths.map((path) => [basename(path, ".md"), path])).values()].sort();
|
|
114
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
|
+
}
|
|
115
146
|
function diagnostic(severity, code, message, source, hint) {
|
|
116
147
|
return { severity, code, message, ...(source ? { source } : {}), ...(hint ? { hint } : {}) };
|
|
117
148
|
}
|
|
@@ -119,7 +150,9 @@ function emptyResourcePolicy(globalSettingsPath, cwd, projectTrusted) {
|
|
|
119
150
|
const empty = { skills: [], extensions: [] };
|
|
120
151
|
return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: empty, unmatchedSkills: [], unmatchedExtensions: [] };
|
|
121
152
|
}
|
|
122
|
-
function validateModel(value, known, available, source, diagnostics, aliases, settingsPath) {
|
|
153
|
+
function validateModel(value, known, available, source, diagnostics, aliases, dynamicAliases, settingsPath) {
|
|
154
|
+
if (isDynamicModelAlias(value, dynamicAliases))
|
|
155
|
+
return;
|
|
123
156
|
try {
|
|
124
157
|
const parsed = resolveModelReference(value, aliases, known, settingsPath);
|
|
125
158
|
const name = `${parsed.provider}/${parsed.model}`;
|
|
@@ -130,13 +163,14 @@ function validateModel(value, known, available, source, diagnostics, aliases, se
|
|
|
130
163
|
diagnostics.push(diagnostic("error", "MODEL_INVALID", error.message, source, error.message.includes("thinking") ? THINKING_HINT : "Use provider/model or provider/model:thinking."));
|
|
131
164
|
}
|
|
132
165
|
}
|
|
133
|
-
function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, settingsPath) {
|
|
166
|
+
function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
|
|
134
167
|
let definition;
|
|
135
168
|
try {
|
|
136
169
|
definition = parseRoleMarkdown(readFileSync(path, "utf8"), true, path);
|
|
137
170
|
}
|
|
138
171
|
catch (error) {
|
|
139
|
-
|
|
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."));
|
|
140
174
|
return undefined;
|
|
141
175
|
}
|
|
142
176
|
const body = definition.prompt ?? "";
|
|
@@ -147,20 +181,18 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
|
|
|
147
181
|
if (/{{\s*[^{}]+\s*}}/.test(body))
|
|
148
182
|
diagnostics.push(diagnostic("warning", "ROLE_PLACEHOLDER", "Role body contains an unsupported placeholder-looking token", path));
|
|
149
183
|
if (definition.model)
|
|
150
|
-
validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, settingsPath);
|
|
184
|
+
validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, dynamicAliases, settingsPath);
|
|
151
185
|
for (const tool of definition.tools ?? [])
|
|
152
186
|
if (!activeTools.has(tool))
|
|
153
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."));
|
|
154
188
|
return definition;
|
|
155
189
|
}
|
|
156
190
|
function matchResourcePolicy(policy, pi) {
|
|
157
|
-
const extensions = new Set((pi.extensions ?? []).map(canonical));
|
|
158
|
-
const skills = new Set(pi.skills ?? []);
|
|
159
|
-
return { ...policy,
|
|
160
|
-
}
|
|
161
|
-
function resourcePolicySource(policy, kind, selector) {
|
|
162
|
-
return policy.project[kind].includes(selector) && !policy.global[kind].includes(selector) ? policy.projectSettingsPath : policy.globalSettingsPath;
|
|
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) };
|
|
163
194
|
}
|
|
195
|
+
function resourcePolicySource(settingsSource) { return settingsSource; }
|
|
164
196
|
export async function doctor(options = {}) {
|
|
165
197
|
const cwd = canonical(options.cwd ?? process.cwd());
|
|
166
198
|
const agentDir = canonical(options.agentDir ?? getAgentDir());
|
|
@@ -173,6 +205,8 @@ export async function doctor(options = {}) {
|
|
|
173
205
|
catch (error) {
|
|
174
206
|
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", error.message, settingsPath, "Fix or remove the invalid workflow settings file."));
|
|
175
207
|
}
|
|
208
|
+
let settingsSources = { concurrency: settingsPath, modelAliases: settingsPath, disabledAgentResources: settingsPath };
|
|
209
|
+
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
176
210
|
let pi;
|
|
177
211
|
try {
|
|
178
212
|
pi = await (options.discoverPi ?? discoverPi)(cwd, agentDir);
|
|
@@ -187,41 +221,98 @@ export async function doctor(options = {}) {
|
|
|
187
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."));
|
|
188
222
|
for (const error of pi.extensionErrors)
|
|
189
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
|
+
}
|
|
190
235
|
let resourcePolicy;
|
|
191
236
|
try {
|
|
192
237
|
resourcePolicy = matchResourcePolicy(resolveAgentResourcePolicy(cwd, pi.trust.trusted, settingsPath), pi);
|
|
193
238
|
}
|
|
194
239
|
catch (error) {
|
|
195
240
|
const message = error instanceof Error ? error.message : String(error);
|
|
196
|
-
|
|
197
|
-
|
|
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."));
|
|
198
244
|
resourcePolicy = emptyResourcePolicy(settingsPath, cwd, pi.trust.trusted);
|
|
199
245
|
}
|
|
200
246
|
for (const skill of resourcePolicy.unmatchedSkills)
|
|
201
|
-
diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled skill selector currently matches no discovered skill: ${skill}`, `${resourcePolicySource(
|
|
247
|
+
diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled skill selector currently matches no discovered skill: ${skill}`, `${resourcePolicySource(settingsSources.disabledAgentResources)}.disabledAgentResources.skills`));
|
|
202
248
|
for (const extension of resourcePolicy.unmatchedExtensions)
|
|
203
|
-
diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled extension selector currently matches no discovered extension source: ${extension}`, `${resourcePolicySource(
|
|
249
|
+
diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled extension selector currently matches no discovered extension source: ${extension}`, `${resourcePolicySource(settingsSources.disabledAgentResources)}.disabledAgentResources.extensions`));
|
|
204
250
|
const activeTools = new Set(pi.activeTools);
|
|
205
251
|
const knownModels = new Set(pi.knownModels);
|
|
206
252
|
const availableModels = new Set(pi.availableModels);
|
|
207
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));
|
|
208
261
|
const roles = [];
|
|
209
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
|
+
}
|
|
210
289
|
const globalPaths = new Map();
|
|
211
290
|
const globalRoleDirs = workflowRoleDirectories(agentDir);
|
|
212
291
|
for (const path of roleFilesFrom(globalRoleDirs)) {
|
|
213
292
|
const name = basename(path, ".md");
|
|
214
|
-
|
|
293
|
+
const extensionPath = extensionPaths.get(name);
|
|
294
|
+
roles.push({ name, path, scope: "global", active: true, ...(extensionPath ? { overrides: extensionPath } : {}) });
|
|
215
295
|
globalPaths.set(name, path);
|
|
216
|
-
|
|
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);
|
|
217
304
|
if (definition)
|
|
218
305
|
definitions.set(name, definition);
|
|
306
|
+
else
|
|
307
|
+
definitions.delete(name);
|
|
219
308
|
}
|
|
220
309
|
for (const path of roleFilesFrom([join(cwd, ".pi", "pi-extensible-workflows", "roles")])) {
|
|
221
310
|
const name = basename(path, ".md");
|
|
222
311
|
const globalPath = globalPaths.get(name);
|
|
312
|
+
const extensionPath = extensionPaths.get(name);
|
|
313
|
+
const overriddenPath = globalPath ?? extensionPath;
|
|
223
314
|
const active = pi.trust.trusted;
|
|
224
|
-
roles.push({ name, path, scope: "project", active, ...(active &&
|
|
315
|
+
roles.push({ name, path, scope: "project", active, ...(active && overriddenPath ? { overrides: overriddenPath } : {}) });
|
|
225
316
|
if (!active)
|
|
226
317
|
continue;
|
|
227
318
|
if (globalPath) {
|
|
@@ -231,7 +322,14 @@ export async function doctor(options = {}) {
|
|
|
231
322
|
global.overriddenBy = path;
|
|
232
323
|
}
|
|
233
324
|
}
|
|
234
|
-
|
|
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);
|
|
235
333
|
if (definition)
|
|
236
334
|
definitions.set(name, definition);
|
|
237
335
|
else
|
|
@@ -244,7 +342,7 @@ export async function doctor(options = {}) {
|
|
|
244
342
|
const severityOrder = { error: 0, warning: 1 };
|
|
245
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));
|
|
246
344
|
roles.sort((left, right) => left.name.localeCompare(right.name) || left.scope.localeCompare(right.scope));
|
|
247
|
-
return { cwd, agentDir, settingsPath, settings, trust: pi.trust, activeTools: [...activeTools].sort(), roles, functions, resourcePolicy, diagnostics };
|
|
345
|
+
return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), roles, functions, modelAliases, resourcePolicy, diagnostics };
|
|
248
346
|
}
|
|
249
347
|
function count(report, severity) { return report.diagnostics.filter((item) => item.severity === severity).length; }
|
|
250
348
|
export function doctorExitCode(report) { return count(report, "error") > 0 ? 1 : 0; }
|
|
@@ -255,7 +353,9 @@ export function formatDoctorReport(report) {
|
|
|
255
353
|
"## Environment",
|
|
256
354
|
`- CWD: \`${report.cwd}\``,
|
|
257
355
|
`- Agent dir: \`${report.agentDir}\``,
|
|
258
|
-
`-
|
|
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}\``,
|
|
259
359
|
`- Limits: concurrency=${String(report.settings.concurrency)}`,
|
|
260
360
|
"",
|
|
261
361
|
"## Trust/resources",
|
|
@@ -270,6 +370,8 @@ export function formatDoctorReport(report) {
|
|
|
270
370
|
`- Project extensions: ${report.resourcePolicy.project.extensions.join(", ") || "(none)"}`,
|
|
271
371
|
`- Effective skills: ${report.resourcePolicy.effective.skills.join(", ") || "(none)"}`,
|
|
272
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)"}`,
|
|
273
375
|
`- Unmatched skills: ${report.resourcePolicy.unmatchedSkills.join(", ") || "(none)"}`,
|
|
274
376
|
`- Unmatched extensions: ${report.resourcePolicy.unmatchedExtensions.join(", ") || "(none)"}`,
|
|
275
377
|
"",
|
|
@@ -277,7 +379,10 @@ export function formatDoctorReport(report) {
|
|
|
277
379
|
...(report.activeTools.length ? report.activeTools.map((tool) => `- \`${tool}\``) : ["- None resolved"]),
|
|
278
380
|
"",
|
|
279
381
|
"## Roles",
|
|
280
|
-
...(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.overrides ? `; overrides \`${role.overrides}\`` : ""}`) : ["- None found"]),
|
|
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"]),
|
|
281
386
|
"",
|
|
282
387
|
"## Reusable functions",
|
|
283
388
|
...(report.functions.length ? report.functions.map((fn) => `- [${fn.valid ? "ok" : "error"}] \`${fn.name}\` - ${fn.description}`) : ["- None registered"]),
|
package/dist/src/execution.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fork, spawn } from "node:child_process";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -72,6 +72,7 @@ const named = (value, kind) => { if (typeof value !== "string" || !value.trim())
|
|
|
72
72
|
const path = (...names) => names.map(encodeURIComponent).join("/");
|
|
73
73
|
const inheritedAgentPath = new AsyncLocalStorage();
|
|
74
74
|
const agentOccurrences = new Map();
|
|
75
|
+
const agentInflight = new Set();
|
|
75
76
|
const shellOccurrences = new Map();
|
|
76
77
|
const worktreeOwners = new AsyncLocalStorage();
|
|
77
78
|
const rejectAgent = () => { throw workError("INVALID_METADATA", "Workflow agent calls must use a direct agent(...) call; aliases and indirect calls are unsupported"); };
|
|
@@ -92,14 +93,22 @@ const internalAgent = (...values) => {
|
|
|
92
93
|
const callSite = values.pop();
|
|
93
94
|
if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow agent call-site identity");
|
|
94
95
|
const inherited = inheritedAgentPath.getStore() || [];
|
|
95
|
-
// ponytail: same-callsite races outside parallel/pipeline lack a stable structural scope and are unsupported.
|
|
96
96
|
const occurrenceKey = JSON.stringify([inherited, callSite]);
|
|
97
|
+
if (agentInflight.has(occurrenceKey)) throw workError("INVALID_METADATA", "Concurrent agent calls from the same source call site are unsupported; use parallel(...) or pipeline(...)");
|
|
98
|
+
agentInflight.add(occurrenceKey);
|
|
97
99
|
const occurrence = (agentOccurrences.get(occurrenceKey) || 0) + 1;
|
|
98
100
|
agentOccurrences.set(occurrenceKey, occurrence);
|
|
99
101
|
const options = values.length < 2 || values[1] === undefined ? {} : values[1];
|
|
100
102
|
const worktreeOwner = worktreeOwners.getStore();
|
|
101
103
|
const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
|
|
102
|
-
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
result = rpc("agent", [values[0], options, identity]).then(unwrap);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
agentInflight.delete(occurrenceKey);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
void result.then(() => agentInflight.delete(occurrenceKey), () => agentInflight.delete(occurrenceKey));
|
|
103
112
|
Object.defineProperties(result, {
|
|
104
113
|
toJSON: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before serialization"); } },
|
|
105
114
|
toString: { value() { throw workError("INVALID_METADATA", "Workflow agent result is a Promise; await it before interpolation"); } },
|
|
@@ -421,7 +430,7 @@ function workflowErrorFromWorker(error) {
|
|
|
421
430
|
export function runWorkflow(script, args = null, bridge = {}, signal) {
|
|
422
431
|
encoded(args);
|
|
423
432
|
const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {}, variables: bridge.variables ?? {} });
|
|
424
|
-
const childDir = mkdtempSync(join(tmpdir(), "pi-wf-"));
|
|
433
|
+
const childDir = realpathSync(mkdtempSync(join(tmpdir(), "pi-wf-")));
|
|
425
434
|
const childFile = join(childDir, "child.cjs");
|
|
426
435
|
writeFileSync(childFile, childSource);
|
|
427
436
|
const child = fork(childFile, [String(RPC_LIMIT_BYTES), config], {
|
package/dist/src/host.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Type } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import { copyToClipboard, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type SessionFactory } from "./agent-execution.js";
|
|
4
4
|
import type { AwaitingCheckpoint, PersistedRun, WorktreeReference } from "./persistence.js";
|
|
5
|
-
import { type LaunchSnapshot, type RunState, type WorkflowFailureDiagnostics } from "./types.js";
|
|
5
|
+
import { type AgentRecord, type LaunchSnapshot, type RunState, type WorkflowFailureDiagnostics } from "./types.js";
|
|
6
6
|
export interface WorkflowProgressStyles {
|
|
7
7
|
accent(text: string): string;
|
|
8
8
|
success(text: string): string;
|
|
@@ -34,8 +34,8 @@ export declare function formatWorkflowPreview(args: {
|
|
|
34
34
|
description?: unknown;
|
|
35
35
|
}): string;
|
|
36
36
|
export declare const WORKFLOW_TOOL_LABEL = "Workflow";
|
|
37
|
-
export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
|
|
38
|
-
export declare const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow that
|
|
37
|
+
export declare const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline parallel-to-summary path by default";
|
|
38
|
+
export declare const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. Foreground results include the completed run ID. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId }) replays a failed run into a child; workflow_resume({ runId, budget? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
|
|
39
39
|
export declare const WORKFLOW_TOOL_PARAMETERS: Type.TObject<{
|
|
40
40
|
name: Type.TOptional<Type.TString>;
|
|
41
41
|
description: Type.TOptional<Type.TString>;
|
|
@@ -50,13 +50,92 @@ export declare const WORKFLOW_TOOL_PARAMETERS: Type.TObject<{
|
|
|
50
50
|
export declare const WORKFLOW_RETRY_PARAMETERS: Type.TObject<{
|
|
51
51
|
runId: Type.TString;
|
|
52
52
|
}>;
|
|
53
|
+
export type WorkflowPhaseState = "not started" | "running" | "completed" | "failed" | "cancelled" | "interrupted" | "budget_exhausted";
|
|
54
|
+
export interface WorkflowPhaseAgentCounts {
|
|
55
|
+
total: number;
|
|
56
|
+
completed: number;
|
|
57
|
+
running: number;
|
|
58
|
+
failed: number;
|
|
59
|
+
cancelled: number;
|
|
60
|
+
pending: number;
|
|
61
|
+
}
|
|
62
|
+
export interface WorkflowPhaseView {
|
|
63
|
+
id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
occurrence: number;
|
|
66
|
+
state: WorkflowPhaseState;
|
|
67
|
+
observed: boolean;
|
|
68
|
+
afterAgent?: number;
|
|
69
|
+
agents: readonly AgentRecord[];
|
|
70
|
+
counts: WorkflowPhaseAgentCounts;
|
|
71
|
+
}
|
|
72
|
+
export interface WorkflowPhaseModel {
|
|
73
|
+
declaredPhases: readonly string[];
|
|
74
|
+
phases: readonly WorkflowPhaseView[];
|
|
75
|
+
currentPhaseIndex?: number;
|
|
76
|
+
currentPhaseId?: string;
|
|
77
|
+
counts: Readonly<Partial<Record<WorkflowPhaseState, number>>>;
|
|
78
|
+
unassignedAgents?: readonly AgentRecord[];
|
|
79
|
+
}
|
|
80
|
+
type WorkflowPhaseSource = readonly string[] | Pick<LaunchSnapshot, "phases"> | undefined;
|
|
81
|
+
export declare function buildWorkflowPhaseModel(run: Pick<PersistedRun, "state" | "phase" | "phaseHistory" | "agents">, source?: WorkflowPhaseSource): WorkflowPhaseModel;
|
|
82
|
+
export interface WorkflowPhaseSelection {
|
|
83
|
+
phaseId?: string | undefined;
|
|
84
|
+
agentId?: string | undefined;
|
|
85
|
+
nodeId?: string | undefined;
|
|
86
|
+
expandedNodeIds?: readonly string[] | undefined;
|
|
87
|
+
treeOnly?: boolean | undefined;
|
|
88
|
+
detailsOnly?: boolean | undefined;
|
|
89
|
+
actions?: {
|
|
90
|
+
title: string;
|
|
91
|
+
options: readonly string[];
|
|
92
|
+
index: number;
|
|
93
|
+
} | undefined;
|
|
94
|
+
}
|
|
95
|
+
export type WorkflowPhaseTreeNodeKind = "phase" | "operation" | "agent";
|
|
96
|
+
export interface WorkflowPhaseTreeNode {
|
|
97
|
+
id: string;
|
|
98
|
+
kind: WorkflowPhaseTreeNodeKind;
|
|
99
|
+
label: string;
|
|
100
|
+
depth: number;
|
|
101
|
+
phaseId: string;
|
|
102
|
+
operationPath: readonly string[];
|
|
103
|
+
parentId?: string;
|
|
104
|
+
children: readonly string[];
|
|
105
|
+
state: WorkflowPhaseState | AgentRecord["state"];
|
|
106
|
+
agentId?: string;
|
|
107
|
+
agent?: AgentRecord;
|
|
108
|
+
phase?: WorkflowPhaseView;
|
|
109
|
+
}
|
|
110
|
+
export interface WorkflowPhaseTree {
|
|
111
|
+
roots: readonly string[];
|
|
112
|
+
nodes: readonly WorkflowPhaseTreeNode[];
|
|
113
|
+
byId: ReadonlyMap<string, WorkflowPhaseTreeNode>;
|
|
114
|
+
}
|
|
115
|
+
export interface WorkflowPhaseTreeSelection {
|
|
116
|
+
nodeId?: string | undefined;
|
|
117
|
+
}
|
|
118
|
+
export type WorkflowPhaseTreeDirection = "up" | "down" | "left" | "right";
|
|
119
|
+
export declare function buildWorkflowPhaseTree(model: WorkflowPhaseModel): WorkflowPhaseTree;
|
|
120
|
+
export declare function workflowPhaseTreeVisibleNodes(tree: WorkflowPhaseTree, expanded?: ReadonlySet<string>): readonly WorkflowPhaseTreeNode[];
|
|
121
|
+
export declare function workflowPhaseTreeInitialExpanded(tree: WorkflowPhaseTree): ReadonlySet<string>;
|
|
122
|
+
export declare function preserveWorkflowPhaseTreeSelection(tree: WorkflowPhaseTree, selection: WorkflowPhaseTreeSelection): WorkflowPhaseTreeSelection;
|
|
123
|
+
export declare function navigateWorkflowPhaseTree(tree: WorkflowPhaseTree, selectedNodeId: string | undefined, expandedNodeIds: ReadonlySet<string>, direction: WorkflowPhaseTreeDirection): {
|
|
124
|
+
nodeId?: string;
|
|
125
|
+
expandedNodeIds: ReadonlySet<string>;
|
|
126
|
+
};
|
|
127
|
+
export declare function preserveWorkflowPhaseSelection(model: WorkflowPhaseModel, selection: WorkflowPhaseSelection): WorkflowPhaseSelection;
|
|
53
128
|
export declare function formatWorkflowProgress(run: PersistedRun, spinner?: string, styles?: WorkflowProgressStyles): string;
|
|
54
129
|
export declare function truncateWorkflowProgress(text: string, width: number): string[];
|
|
55
130
|
export declare function formatBudgetStatus(run: Pick<PersistedRun, "budget" | "budgetVersion" | "usage" | "budgetEvents">): string[];
|
|
131
|
+
export declare function agentBreadcrumbParts(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string[];
|
|
132
|
+
export declare function agentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string;
|
|
56
133
|
export declare function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[]): string;
|
|
57
134
|
export declare function formatNavigatorRun(loaded: {
|
|
58
135
|
run: PersistedRun;
|
|
59
136
|
snapshot: Readonly<LaunchSnapshot>;
|
|
60
|
-
}, checkpoints: readonly AwaitingCheckpoint[],
|
|
137
|
+
}, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[]): string;
|
|
138
|
+
export declare function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection?: WorkflowPhaseSelection, styles?: WorkflowProgressStyles): string[];
|
|
61
139
|
export declare function formatWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string;
|
|
62
140
|
export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard?: typeof copyToClipboard, createSession?: SessionFactory, agentDir?: string): void;
|
|
141
|
+
export {};
|