pi-extensible-workflows 3.0.0 → 3.1.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 -10
- 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/host.d.ts +40 -1
- package/dist/src/host.js +517 -129
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +2 -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 +134 -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 +123 -31
- package/package.json +3 -2
- package/skills/pi-extensible-workflows/SKILL.md +50 -16
- 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/host.ts +455 -122
- package/src/index.ts +4 -2
- package/src/persistence.ts +35 -1
- package/src/registry.ts +108 -25
- package/src/session-inspector.ts +4 -2
- package/src/types.ts +52 -7
- package/src/utils.ts +39 -5
- package/src/validation.ts +103 -31
package/src/validation.ts
CHANGED
|
@@ -8,9 +8,10 @@ import { Script } from "node:vm";
|
|
|
8
8
|
import { Value } from "typebox/value";
|
|
9
9
|
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { WorkflowError } from "./types.js";
|
|
11
|
-
import type { AgentDefinition, AgentResourceExclusions, AgentResourcePolicy, CheckpointInput, JsonSchema, JsonValue, LaunchSnapshot, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowMetadata, WorkflowSettings, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
|
|
11
|
+
import type { AgentDefinition, AgentResourceExclusions, AgentResourcePolicy, CheckpointInput, JsonSchema, JsonValue, LaunchSnapshot, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowExtensionMetadata, WorkflowMetadata, WorkflowRoleDirectoryRegistration, WorkflowSettings, WorkflowSettingsOverrides, WorkflowSettingsResolution, WorkflowSettingsSources, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
|
|
12
12
|
import type { WorkflowRegistryApi } from "./registry.js";
|
|
13
|
-
import {
|
|
13
|
+
import { registeredWorkflowRoleDirectoryRegistrations } from "./registry.js";
|
|
14
|
+
import { annotateModelAliasError, deepFreeze, errorText, fail, jsonValue, modelAliasName, modelCapability, object, parseThinking, positiveInteger, resolveModelReference, unknownModel, validateModelAliases, validateResourcePattern } from "./utils.js";
|
|
14
15
|
import { WORKFLOW_CALL_KINDS } from "./types.js";
|
|
15
16
|
|
|
16
17
|
export const DEFAULT_SETTINGS: Readonly<WorkflowSettings> = Object.freeze({ concurrency: 8 });
|
|
@@ -25,10 +26,20 @@ export function validateCheckpoint(value: unknown): CheckpointInput {
|
|
|
25
26
|
export function workflowSettingsPath(agentDir = getAgentDir()): string { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
|
|
26
27
|
export function workflowProjectSettingsPath(cwd: string): string { return join(cwd, ".pi", ROLE_DIRECTORY, "settings.json"); }
|
|
27
28
|
const EMPTY_AGENT_RESOURCE_EXCLUSIONS: AgentResourceExclusions = Object.freeze({ skills: [], extensions: [] });
|
|
29
|
+
function resourcePatternHasMagic(value: string): boolean { return /[*?\x5b\x5d{}()]/.test(value); }
|
|
28
30
|
function normalizedResourcePath(value: string, settingsPath: string): string {
|
|
29
31
|
let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
|
|
30
32
|
if (expanded.startsWith("file://")) expanded = fileURLToPath(expanded);
|
|
31
33
|
const resolved = resolve(dirname(settingsPath), expanded);
|
|
34
|
+
if (expanded === "**" || expanded.startsWith("**/") || expanded.startsWith("**\\")) return expanded;
|
|
35
|
+
if (resourcePatternHasMagic(expanded)) {
|
|
36
|
+
const magicIndex = resolved.search(/[*?\x5b\x5d{}()]/);
|
|
37
|
+
const separatorIndex = Math.max(resolved.lastIndexOf("/", magicIndex), resolved.lastIndexOf("\\", magicIndex));
|
|
38
|
+
const rootBoundary = separatorIndex === 0 || (separatorIndex === 2 && /^[A-Za-z]:[\\/]/.test(resolved));
|
|
39
|
+
const prefix = rootBoundary ? resolved.slice(0, separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(0, separatorIndex) : resolved;
|
|
40
|
+
const suffix = rootBoundary ? resolved.slice(separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(separatorIndex) : "";
|
|
41
|
+
try { return `${realpathSync(prefix)}${suffix}`; } catch { return resolved; }
|
|
42
|
+
}
|
|
32
43
|
try { return realpathSync(resolved); } catch { return resolved; }
|
|
33
44
|
}
|
|
34
45
|
function validateAgentResourceExclusions(value: unknown, settingsPath: string, errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS"): AgentResourceExclusions | undefined {
|
|
@@ -41,41 +52,74 @@ function validateAgentResourceExclusions(value: unknown, settingsPath: string, e
|
|
|
41
52
|
const entries = value[kind];
|
|
42
53
|
if (entries === undefined) continue;
|
|
43
54
|
if (!Array.isArray(entries)) fail(errorCode, `${base}.${kind} must be an array`);
|
|
44
|
-
const seen = new Set<string>();
|
|
45
55
|
for (const [index, entry] of entries.entries()) {
|
|
46
56
|
if (typeof entry !== "string" || !entry.trim()) fail(errorCode, `${base}.${kind}[${String(index)}] must be a non-empty string`);
|
|
47
57
|
let selector = entry.trim();
|
|
48
58
|
if (kind === "extensions") {
|
|
49
|
-
|
|
59
|
+
const negated = selector.startsWith("!");
|
|
60
|
+
const body = negated ? selector.slice(1) : selector;
|
|
61
|
+
if (!body) fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid minimatch pattern: Empty minimatch pattern ${JSON.stringify(selector)}`);
|
|
62
|
+
try { selector = `${negated ? "!" : ""}${normalizedResourcePath(body, settingsPath)}`; } catch (error) { fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`); }
|
|
50
63
|
}
|
|
51
|
-
|
|
64
|
+
try { validateResourcePattern(selector); } catch (error) { fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid minimatch pattern: ${errorText(error)}`); }
|
|
65
|
+
normalized[kind].push(selector);
|
|
52
66
|
}
|
|
53
67
|
}
|
|
54
68
|
return Object.freeze({ skills: Object.freeze(normalized.skills), extensions: Object.freeze(normalized.extensions) });
|
|
55
69
|
}
|
|
56
|
-
|
|
70
|
+
function parseSettings(path: string, partial: false): Readonly<WorkflowSettings>;
|
|
71
|
+
function parseSettings(path: string, partial: true): Readonly<WorkflowSettingsOverrides>;
|
|
72
|
+
function parseSettings(path: string, partial: boolean): Readonly<WorkflowSettings | WorkflowSettingsOverrides> {
|
|
57
73
|
let parsed: unknown;
|
|
58
74
|
try { parsed = JSON.parse(readFileSync(path, "utf8")); }
|
|
59
75
|
catch (error) {
|
|
60
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return DEFAULT_SETTINGS;
|
|
76
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return partial ? Object.freeze({}) : DEFAULT_SETTINGS;
|
|
61
77
|
fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
|
|
62
78
|
}
|
|
63
79
|
if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
|
|
64
80
|
const allowed = new Set(["concurrency", "modelAliases", "disabledAgentResources"]);
|
|
65
81
|
const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
|
|
66
82
|
if (unknown) fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
|
|
67
|
-
const concurrency = parsed.concurrency === undefined ? DEFAULT_SETTINGS.concurrency : parsed.concurrency;
|
|
68
|
-
if (!positiveInteger(concurrency) || concurrency > 16) fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
|
|
83
|
+
const concurrency = parsed.concurrency === undefined ? (partial ? undefined : DEFAULT_SETTINGS.concurrency) : parsed.concurrency;
|
|
84
|
+
if (concurrency !== undefined && (!positiveInteger(concurrency) || concurrency > 16)) fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
|
|
69
85
|
const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
|
|
70
86
|
const disabledAgentResources = validateAgentResourceExclusions(parsed.disabledAgentResources, path);
|
|
71
|
-
return Object.freeze({ concurrency, ...(modelAliases ? {
|
|
87
|
+
return Object.freeze({ ...(concurrency === undefined ? {} : { concurrency }), ...(modelAliases === undefined ? {} : { modelAliases }), ...(disabledAgentResources === undefined ? {} : { disabledAgentResources }) });
|
|
72
88
|
}
|
|
73
|
-
export function
|
|
89
|
+
export function loadSettings(path = workflowSettingsPath()): Readonly<WorkflowSettings> { return parseSettings(path, false); }
|
|
90
|
+
export function loadSettingsOverrides(path: string): Readonly<WorkflowSettingsOverrides> { return parseSettings(path, true); }
|
|
91
|
+
export function resolveWorkflowSettings(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): WorkflowSettingsResolution {
|
|
74
92
|
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
75
|
-
const global = loadSettings(globalSettingsPath)
|
|
76
|
-
const project = projectTrusted ?
|
|
77
|
-
const
|
|
78
|
-
|
|
93
|
+
const global = loadSettings(globalSettingsPath);
|
|
94
|
+
const project: Readonly<WorkflowSettingsOverrides> = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
|
|
95
|
+
const projectHas = (key: keyof WorkflowSettingsOverrides): boolean => Object.prototype.hasOwnProperty.call(project, key);
|
|
96
|
+
const sources: WorkflowSettingsSources = {
|
|
97
|
+
concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
|
|
98
|
+
modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
|
|
99
|
+
disabledAgentResources: projectHas("disabledAgentResources") ? projectSettingsPath : globalSettingsPath,
|
|
100
|
+
};
|
|
101
|
+
const effective = Object.freeze({
|
|
102
|
+
concurrency: project.concurrency ?? global.concurrency,
|
|
103
|
+
...(projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === undefined ? {} : { modelAliases: global.modelAliases }),
|
|
104
|
+
...(projectHas("disabledAgentResources") ? { disabledAgentResources: project.disabledAgentResources } : global.disabledAgentResources === undefined ? {} : { disabledAgentResources: global.disabledAgentResources }),
|
|
105
|
+
});
|
|
106
|
+
return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
|
|
107
|
+
}
|
|
108
|
+
export function validateModelAliasAvailability(aliases: Readonly<Record<string, string>>, names: readonly string[], availableModels: ReadonlySet<string>, knownModels: ReadonlySet<string>, settingsPath?: string): void {
|
|
109
|
+
for (const name of names) {
|
|
110
|
+
try {
|
|
111
|
+
const target = modelCapability(name, aliases, knownModels, settingsPath);
|
|
112
|
+
if (!availableModels.has(target)) unknownModel(name, target, settingsPath);
|
|
113
|
+
} catch (error) { throw annotateModelAliasError(error, name); }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): AgentResourcePolicy {
|
|
117
|
+
const resolved = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
|
|
118
|
+
const empty = EMPTY_AGENT_RESOURCE_EXCLUSIONS;
|
|
119
|
+
const global = resolved.global.disabledAgentResources ?? empty;
|
|
120
|
+
const project = resolved.project.disabledAgentResources ?? empty;
|
|
121
|
+
const effective = resolved.effective.disabledAgentResources ?? empty;
|
|
122
|
+
return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [] };
|
|
79
123
|
}
|
|
80
124
|
export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonly<Record<string, string>> = {}): void {
|
|
81
125
|
const normalized = validateModelAliases(aliases, path);
|
|
@@ -135,23 +179,51 @@ function projectRoleDirectories(root: string): readonly string[] {
|
|
|
135
179
|
return [join(root, ROLE_DIRECTORY, "roles")];
|
|
136
180
|
}
|
|
137
181
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
182
|
+
type RoleDirectorySource = { path: string; extension?: WorkflowExtensionMetadata };
|
|
183
|
+
export type WorkflowRoleDirectoryInput = string | WorkflowRoleDirectoryRegistration;
|
|
184
|
+
type RoleFile = { name: string; path: string; source: RoleDirectorySource };
|
|
185
|
+
function roleDirectorySources(dirs: readonly WorkflowRoleDirectoryInput[]): RoleDirectorySource[] {
|
|
186
|
+
const seen = new Set<string>();
|
|
187
|
+
return dirs.flatMap((value) => {
|
|
188
|
+
const source = typeof value === "string" ? { path: value } : { path: value.path, extension: value.extension };
|
|
189
|
+
if (seen.has(source.path)) return [];
|
|
190
|
+
seen.add(source.path);
|
|
191
|
+
return [source];
|
|
192
|
+
});
|
|
147
193
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
194
|
+
function roleDirectoryLabel(source: RoleDirectorySource, extension = true): string {
|
|
195
|
+
return source.extension ? `Extension "${source.extension.headline}" (${source.extension.version})` : extension ? "Registered workflow extension" : "Standard workflow";
|
|
196
|
+
}
|
|
197
|
+
function scanRoleFiles(dirs: readonly WorkflowRoleDirectoryInput[], extension: boolean): RoleFile[] {
|
|
198
|
+
const files: RoleFile[] = [];
|
|
199
|
+
for (const source of roleDirectorySources(dirs)) {
|
|
200
|
+
let entries: import("node:fs").Dirent[];
|
|
201
|
+
try { entries = readdirSync(source.path, { withFileTypes: true }); }
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (!extension && (error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
204
|
+
fail("INVALID_METADATA", `${roleDirectoryLabel(source, extension)} role directory "${source.path}" could not be scanned: ${errorText(error)}`);
|
|
205
|
+
}
|
|
206
|
+
for (const entry of entries) if (entry.isFile() && extname(entry.name) === ".md") files.push({ name: basename(entry.name, ".md"), path: join(source.path, entry.name), source });
|
|
207
|
+
}
|
|
208
|
+
return files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
|
|
209
|
+
}
|
|
210
|
+
function readRoleDefinitions(dirs: readonly WorkflowRoleDirectoryInput[], extension = false): Record<string, AgentDefinition> {
|
|
211
|
+
const files = scanRoleFiles(dirs, extension);
|
|
212
|
+
if (extension) {
|
|
213
|
+
const byName = new Map<string, RoleFile[]>();
|
|
214
|
+
for (const file of files) byName.set(file.name, [...(byName.get(file.name) ?? []), file]);
|
|
215
|
+
for (const [name, matches] of byName) if (matches.length > 1) fail("INVALID_METADATA", `Duplicate extension role "${name}": ${matches.map(({ path, source }) => `${roleDirectoryLabel(source)} role directory "${source.path}" (${path})`).join("; ")}`);
|
|
216
|
+
}
|
|
217
|
+
return Object.fromEntries(files.map(({ name, path, source }) => {
|
|
218
|
+
try { return [name, parseRoleMarkdown(readFileSync(path, "utf8"), true, path)]; }
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (extension) fail("INVALID_METADATA", `${roleDirectoryLabel(source)} role directory "${source.path}" contains invalid role "${name}" at "${path}": ${errorText(error)}`);
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}));
|
|
151
224
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return deepFreeze({ ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
|
|
225
|
+
export function loadAgentDefinitions(cwd: string, agentDir = getAgentDir(), projectTrusted = true, extensionRoleDirectories: readonly WorkflowRoleDirectoryInput[] = registeredWorkflowRoleDirectoryRegistrations()): Readonly<Record<string, AgentDefinition>> {
|
|
226
|
+
return deepFreeze({ ...readRoleDefinitions(extensionRoleDirectories, true), ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
|
|
155
227
|
}
|
|
156
228
|
function validateRolePolicies(definitions: Readonly<Record<string, AgentDefinition>>, roles: readonly string[], availableModels: ReadonlySet<string>, rootTools: ReadonlySet<string>, aliases: Readonly<Record<string, string>> = {}, knownModels = availableModels, settingsPath?: string): void {
|
|
157
229
|
for (const role of roles) {
|
|
@@ -590,7 +662,7 @@ export function validateWorkflowLaunchWithRegistry(params: WorkflowValidationPar
|
|
|
590
662
|
const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : "";
|
|
591
663
|
if (!script) fail("INVALID_SYNTAX", "Provide script or registered function");
|
|
592
664
|
const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : fn?.description ? { description: fn.description } : {}) });
|
|
593
|
-
const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false);
|
|
665
|
+
const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false, registry && typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : registry && typeof registry.roleDirectories === "function" ? registry.roleDirectories() : undefined);
|
|
594
666
|
const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
|
|
595
667
|
const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
|
|
596
668
|
const aliases = context.modelAliases ?? {};
|