@marcoscale98/pi-extensible-workflows 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/CHANGELOG.md +536 -0
- package/README.md +64 -0
- package/dist/src/agent-execution.d.ts +338 -0
- package/dist/src/agent-execution.js +1633 -0
- package/dist/src/background-widget.d.ts +74 -0
- package/dist/src/background-widget.js +1340 -0
- package/dist/src/budget.d.ts +38 -0
- package/dist/src/budget.js +188 -0
- package/dist/src/changelog.d.ts +6 -0
- package/dist/src/changelog.js +112 -0
- package/dist/src/decoders.d.ts +96 -0
- package/dist/src/decoders.js +629 -0
- package/dist/src/execution.d.ts +19 -0
- package/dist/src/execution.js +749 -0
- package/dist/src/herdr.d.ts +53 -0
- package/dist/src/herdr.js +270 -0
- package/dist/src/host-delivery.d.ts +95 -0
- package/dist/src/host-delivery.js +376 -0
- package/dist/src/host-navigator.d.ts +83 -0
- package/dist/src/host-navigator.js +1087 -0
- package/dist/src/host-phases.d.ts +89 -0
- package/dist/src/host-phases.js +320 -0
- package/dist/src/host-recovery.d.ts +141 -0
- package/dist/src/host-recovery.js +356 -0
- package/dist/src/host-runtime.d.ts +78 -0
- package/dist/src/host-runtime.js +378 -0
- package/dist/src/host-view.d.ts +116 -0
- package/dist/src/host-view.js +868 -0
- package/dist/src/host.d.ts +40 -0
- package/dist/src/host.js +1641 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.js +13725 -0
- package/dist/src/index.js.map +6 -0
- package/dist/src/io.d.ts +14 -0
- package/dist/src/io.js +82 -0
- package/dist/src/package-identity.d.ts +4 -0
- package/dist/src/package-identity.js +6 -0
- package/dist/src/paths.d.ts +14 -0
- package/dist/src/paths.js +51 -0
- package/dist/src/persistence.d.ts +5 -0
- package/dist/src/persistence.js +5 -0
- package/dist/src/pi-runtime-adapter.d.ts +27 -0
- package/dist/src/pi-runtime-adapter.js +243 -0
- package/dist/src/pi-runtime-runner.d.ts +42 -0
- package/dist/src/pi-runtime-runner.js +505 -0
- package/dist/src/registry.d.ts +48 -0
- package/dist/src/registry.js +379 -0
- package/dist/src/retention.d.ts +17 -0
- package/dist/src/retention.js +195 -0
- package/dist/src/runtime/agent-runner.d.ts +143 -0
- package/dist/src/runtime/agent-runner.js +12 -0
- package/dist/src/runtime/index.d.ts +2 -0
- package/dist/src/runtime/index.js +2 -0
- package/dist/src/runtime/namespace.d.ts +14 -0
- package/dist/src/runtime/namespace.js +1 -0
- package/dist/src/runtime/workflow-result.d.ts +2 -0
- package/dist/src/runtime/workflow-result.js +6 -0
- package/dist/src/session-handoff.d.ts +2 -0
- package/dist/src/session-handoff.js +82 -0
- package/dist/src/session-lease.d.ts +12 -0
- package/dist/src/session-lease.js +180 -0
- package/dist/src/store.d.ts +95 -0
- package/dist/src/store.js +951 -0
- package/dist/src/subagent-manager-handle.d.ts +4 -0
- package/dist/src/subagent-manager-handle.js +6 -0
- package/dist/src/tool-timing.d.ts +12 -0
- package/dist/src/tool-timing.js +33 -0
- package/dist/src/trajectory-contracts.d.ts +8 -0
- package/dist/src/trajectory-contracts.js +9 -0
- package/dist/src/trajectory-host-handle.d.ts +12 -0
- package/dist/src/trajectory-host-handle.js +6 -0
- package/dist/src/trajectory.d.ts +128 -0
- package/dist/src/trajectory.js +594 -0
- package/dist/src/types.d.ts +960 -0
- package/dist/src/types.js +60 -0
- package/dist/src/utils.d.ts +57 -0
- package/dist/src/utils.js +225 -0
- package/dist/src/validation.d.ts +29 -0
- package/dist/src/validation.js +1100 -0
- package/dist/src/workflow-artifacts.d.ts +14 -0
- package/dist/src/workflow-artifacts.js +40 -0
- package/dist/starter/index.d.ts +1 -0
- package/dist/starter/index.js +103 -0
- package/dist/starter/index.js.map +6 -0
- package/dist/starter/review-loop.d.ts +72 -0
- package/dist/starter/review-loop.js +43 -0
- package/dist/starter/roles/developer.md +6 -0
- package/dist/starter/roles/oracle.md +18 -0
- package/dist/starter/roles/researcher.md +20 -0
- package/dist/starter/roles/reviewer.md +19 -0
- package/dist/starter/roles/scout.md +18 -0
- package/dist/subagents/index.d.ts +8 -0
- package/dist/subagents/index.js +8021 -0
- package/dist/subagents/index.js.map +6 -0
- package/dist/subagents/src/contracts.d.ts +173 -0
- package/dist/subagents/src/contracts.js +68 -0
- package/dist/subagents/src/decode.d.ts +15 -0
- package/dist/subagents/src/decode.js +182 -0
- package/dist/subagents/src/index.d.ts +12 -0
- package/dist/subagents/src/index.js +180 -0
- package/dist/subagents/src/manager.d.ts +3 -0
- package/dist/subagents/src/manager.js +1636 -0
- package/dist/subagents/src/navigator.d.ts +5 -0
- package/dist/subagents/src/navigator.js +627 -0
- package/dist/subagents/src/view.d.ts +70 -0
- package/dist/subagents/src/view.js +387 -0
- package/dist/subagents/src/worktree.d.ts +34 -0
- package/dist/subagents/src/worktree.js +68 -0
- package/dist/trajectory/assets/favicon.png +0 -0
- package/dist/trajectory/assets/index.html +772 -0
- package/dist/trajectory/assets/marked.min.js +6 -0
- package/dist/trajectory/assets/morphdom.min.js +1 -0
- package/dist/trajectory/index.d.ts +5 -0
- package/dist/trajectory/index.js +3459 -0
- package/dist/trajectory/index.js.map +6 -0
- package/dist/trajectory/src/assets/favicon.png +0 -0
- package/dist/trajectory/src/assets/index.html +772 -0
- package/dist/trajectory/src/assets/marked.min.js +6 -0
- package/dist/trajectory/src/assets/morphdom.min.js +1 -0
- package/dist/trajectory/src/export.d.ts +25 -0
- package/dist/trajectory/src/export.js +80 -0
- package/dist/trajectory/src/index.d.ts +26 -0
- package/dist/trajectory/src/index.js +732 -0
- package/dist/trajectory/src/server.d.ts +7 -0
- package/dist/trajectory/src/server.js +2534 -0
- package/dist/trajectory/src/server.js.map +6 -0
- package/examples/advanced-patterns/auto-model-router.mjs +78 -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 +105 -0
- package/skills/pi-extensible-workflows/SKILL.md +160 -0
- package/src/agent-execution.ts +1533 -0
- package/src/background-widget.ts +1496 -0
- package/src/budget.ts +109 -0
- package/src/changelog.ts +114 -0
- package/src/decoders.ts +567 -0
- package/src/execution.ts +652 -0
- package/src/herdr.ts +240 -0
- package/src/host-delivery.ts +346 -0
- package/src/host-navigator.ts +871 -0
- package/src/host-phases.ts +264 -0
- package/src/host-recovery.ts +333 -0
- package/src/host-runtime.ts +373 -0
- package/src/host-view.ts +828 -0
- package/src/host.ts +1502 -0
- package/src/index.ts +29 -0
- package/src/io.ts +73 -0
- package/src/package-identity.ts +7 -0
- package/src/paths.ts +49 -0
- package/src/persistence.ts +11 -0
- package/src/pi-runtime-adapter.ts +247 -0
- package/src/pi-runtime-runner.ts +436 -0
- package/src/registry.ts +321 -0
- package/src/retention.ts +178 -0
- package/src/runtime/agent-runner.ts +158 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/namespace.ts +16 -0
- package/src/runtime/workflow-result.ts +8 -0
- package/src/session-handoff.ts +78 -0
- package/src/session-lease.ts +125 -0
- package/src/store.ts +832 -0
- package/src/subagent-manager-handle.ts +8 -0
- package/src/tool-timing.ts +47 -0
- package/src/trajectory-contracts.ts +13 -0
- package/src/trajectory-host-handle.ts +16 -0
- package/src/trajectory.ts +543 -0
- package/src/types.ts +374 -0
- package/src/utils.ts +192 -0
- package/src/validation.ts +910 -0
- package/src/workflow-artifacts.ts +35 -0
- package/starter/index.ts +35 -0
- package/starter/prompts/council.md +16 -0
- package/starter/prompts/deep-research.md +16 -0
- package/starter/prompts/oracle.md +14 -0
- package/starter/prompts/parallel-review.md +22 -0
- package/starter/prompts/parallel-scout.md +16 -0
- package/starter/prompts/review-loop.md +14 -0
- package/starter/prompts/review.md +14 -0
- package/starter/prompts/scout.md +14 -0
- package/starter/review-loop.ts +73 -0
- package/starter/roles/developer.md +6 -0
- package/starter/roles/oracle.md +18 -0
- package/starter/roles/researcher.md +20 -0
- package/starter/roles/reviewer.md +19 -0
- package/starter/roles/scout.md +18 -0
- package/subagents/README.md +145 -0
- package/subagents/index.ts +8 -0
- package/subagents/src/contracts.ts +176 -0
- package/subagents/src/decode.ts +158 -0
- package/subagents/src/index.ts +194 -0
- package/subagents/src/manager.ts +1560 -0
- package/subagents/src/navigator.ts +488 -0
- package/subagents/src/view.ts +376 -0
- package/subagents/src/worktree.ts +99 -0
- package/trajectory/index.ts +5 -0
- package/trajectory/src/assets/favicon.png +0 -0
- package/trajectory/src/assets/index.html +772 -0
- package/trajectory/src/assets/marked.min.js +6 -0
- package/trajectory/src/assets/morphdom.min.js +1 -0
- package/trajectory/src/export.ts +85 -0
- package/trajectory/src/index.ts +604 -0
- package/trajectory/src/server.ts +447 -0
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
import { atomicWriteFile } from "./persistence.js";
|
|
2
|
+
import { mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import * as acorn from "acorn";
|
|
7
|
+
import { Script } from "node:vm";
|
|
8
|
+
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { registeredWorkflowRoleDirectoryRegistrations } from "./registry.js";
|
|
10
|
+
import { annotateModelAliasError, assertModelThinking, deepFreeze, errorText, fail, isNodeError, jsonObject, jsonValue, mergeWorkflowExtensionSettings, modelAliasName, modelCapability, object, positiveInteger, resolveModelReference, resourcePatternHasMagic, unknownModel, validateModelAliases, validateResourcePattern, validWorkflowExtensionNamespace } from "./utils.js";
|
|
11
|
+
import { WORKFLOW_CALL_KINDS, isContextFileScope } from "./types.js";
|
|
12
|
+
import { canonicalPath, workflowProjectSettingsPath } from "./paths.js";
|
|
13
|
+
export { workflowProjectSettingsPath } from "./paths.js";
|
|
14
|
+
export const DEFAULT_SETTINGS = Object.freeze({ concurrency: 8, backgroundWidget: true });
|
|
15
|
+
export function validateCheckpoint(value) {
|
|
16
|
+
if (!object(value) || Object.keys(value).some((key) => !["name", "prompt", "context"].includes(key)) || typeof value.name !== "string" || value.name.trim() === "" || typeof value.prompt !== "string" || !jsonValue(value.context))
|
|
17
|
+
fail("INVALID_METADATA", "checkpoint requires only name, prompt, and JSON context");
|
|
18
|
+
if (Buffer.byteLength(value.prompt) > 1024)
|
|
19
|
+
fail("INVALID_METADATA", "checkpoint prompt exceeds 1024 UTF-8 bytes");
|
|
20
|
+
if (Buffer.byteLength(JSON.stringify(value.context)) > 4096)
|
|
21
|
+
fail("INVALID_METADATA", "checkpoint context exceeds 4096 UTF-8 bytes");
|
|
22
|
+
return { name: value.name, prompt: value.prompt, context: value.context };
|
|
23
|
+
}
|
|
24
|
+
export function workflowSettingsPath(agentDir = getAgentDir()) { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
|
|
25
|
+
function normalizedResourcePath(value, settingsPath) {
|
|
26
|
+
if (value === "*")
|
|
27
|
+
return value;
|
|
28
|
+
let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
|
|
29
|
+
if (expanded.startsWith("file://"))
|
|
30
|
+
expanded = fileURLToPath(expanded);
|
|
31
|
+
const resolved = resolve(dirname(settingsPath), expanded);
|
|
32
|
+
if (expanded === "**" || expanded.startsWith("**/") || expanded.startsWith("**\\"))
|
|
33
|
+
return expanded;
|
|
34
|
+
if (resourcePatternHasMagic(expanded)) {
|
|
35
|
+
const magicIndex = resolved.search(/[*?\x5b\x5d{}()]/);
|
|
36
|
+
const separatorIndex = Math.max(resolved.lastIndexOf("/", magicIndex), resolved.lastIndexOf("\\", magicIndex));
|
|
37
|
+
const rootBoundary = separatorIndex === 0 || (separatorIndex === 2 && /^[A-Za-z]:[\\/]/.test(resolved));
|
|
38
|
+
const prefix = rootBoundary ? resolved.slice(0, separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(0, separatorIndex) : resolved;
|
|
39
|
+
const suffix = rootBoundary ? resolved.slice(separatorIndex + 1) : separatorIndex >= 0 ? resolved.slice(separatorIndex) : "";
|
|
40
|
+
return `${canonicalPath(prefix)}${suffix}`;
|
|
41
|
+
}
|
|
42
|
+
return canonicalPath(resolved);
|
|
43
|
+
}
|
|
44
|
+
function validateSelectorList(value, path, kind, errorCode = "INVALID_SETTINGS", normalizeExtensions = true) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
if (!Array.isArray(value))
|
|
48
|
+
fail(errorCode, `${path}.${kind} must be an array`);
|
|
49
|
+
const normalized = [];
|
|
50
|
+
for (const [index, entry] of value.entries()) {
|
|
51
|
+
if (typeof entry !== "string" || !entry.trim())
|
|
52
|
+
fail(errorCode, `${path}.${kind}[${String(index)}] must be a non-empty string`);
|
|
53
|
+
let selector = entry.trim();
|
|
54
|
+
if (kind === "extensions" && normalizeExtensions) {
|
|
55
|
+
const negated = selector.startsWith("!");
|
|
56
|
+
const body = negated ? selector.slice(1) : selector;
|
|
57
|
+
if (!body)
|
|
58
|
+
fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: Empty minimatch pattern ${JSON.stringify(selector)}`);
|
|
59
|
+
try {
|
|
60
|
+
selector = `${negated ? "!" : ""}${normalizedResourcePath(body, path)}`;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
validateResourcePattern(selector);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
fail(errorCode, `${path}.${kind}[${String(index)}] must be a valid minimatch pattern: ${errorText(error)}`);
|
|
71
|
+
}
|
|
72
|
+
normalized.push(selector);
|
|
73
|
+
}
|
|
74
|
+
return Object.freeze(normalized);
|
|
75
|
+
}
|
|
76
|
+
function validateWorktreePostCreateCommand(value, settingsPath) {
|
|
77
|
+
if (value === undefined)
|
|
78
|
+
return undefined;
|
|
79
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
80
|
+
fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand must be a non-empty array`);
|
|
81
|
+
return Object.freeze(value.map((entry, index) => {
|
|
82
|
+
if (typeof entry !== "string" || !entry.trim())
|
|
83
|
+
fail("INVALID_SETTINGS", `${settingsPath}.worktreePostCreateCommand[${String(index)}] must be a non-empty string`);
|
|
84
|
+
return entry;
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
function selectorsFromSettings(settings) {
|
|
88
|
+
return {
|
|
89
|
+
...(settings.skills === undefined ? {} : { skills: settings.skills }),
|
|
90
|
+
...(settings.extensions === undefined ? {} : { extensions: settings.extensions }),
|
|
91
|
+
...(settings.tools === undefined ? {} : { tools: settings.tools }),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function selectorSet(value) { return { skills: [...(value?.skills ?? [])], extensions: [...(value?.extensions ?? [])], ...(value?.tools === undefined ? {} : { tools: [...value.tools] }) }; }
|
|
95
|
+
function validateContextFileScopes(value, rolePath) {
|
|
96
|
+
if (value === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (!Array.isArray(value) || !value.every(isContextFileScope))
|
|
99
|
+
fail("INVALID_METADATA", `${rolePath}.contextFiles must be an array containing only global, project, or cwd`);
|
|
100
|
+
return [...value];
|
|
101
|
+
}
|
|
102
|
+
export function validateWorkflowExtensionSettings(value, settingsPath, errorCode = "INVALID_SETTINGS", applyDefaults = true) {
|
|
103
|
+
if (value === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
const base = `${settingsPath}.extensionSettings`;
|
|
106
|
+
if (!object(value))
|
|
107
|
+
fail(errorCode, `${base} must be an object`);
|
|
108
|
+
const normalized = {};
|
|
109
|
+
for (const [namespace, raw] of Object.entries(value)) {
|
|
110
|
+
if (!validWorkflowExtensionNamespace(namespace))
|
|
111
|
+
fail(errorCode, `${base} contains an invalid namespace: ${namespace}`);
|
|
112
|
+
if (namespace === "herdr") {
|
|
113
|
+
if (!object(raw))
|
|
114
|
+
fail(errorCode, `${base}.herdr must be an object`);
|
|
115
|
+
if (Object.keys(raw).some((key) => key !== "enableFullyInspectableMode"))
|
|
116
|
+
fail(errorCode, `${base}.herdr contains an unsupported setting`);
|
|
117
|
+
if (raw.enableFullyInspectableMode !== undefined && typeof raw.enableFullyInspectableMode !== "boolean")
|
|
118
|
+
fail(errorCode, `${base}.herdr.enableFullyInspectableMode must be a boolean`);
|
|
119
|
+
normalized.herdr = Object.freeze({ ...(raw.enableFullyInspectableMode === undefined ? {} : { enableFullyInspectableMode: raw.enableFullyInspectableMode }) });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (namespace === "trajectory") {
|
|
123
|
+
if (!object(raw))
|
|
124
|
+
fail(errorCode, `${base}.trajectory must be an object`);
|
|
125
|
+
if (Object.keys(raw).some((key) => key !== "port" && key !== "themes"))
|
|
126
|
+
fail(errorCode, `${base}.trajectory contains an unsupported setting`);
|
|
127
|
+
if (raw.port !== undefined && (!positiveInteger(raw.port) || raw.port > 65535))
|
|
128
|
+
fail(errorCode, `${base}.trajectory.port must be an integer from 1 to 65535`);
|
|
129
|
+
if (raw.themes !== undefined && typeof raw.themes !== "boolean")
|
|
130
|
+
fail(errorCode, `${base}.trajectory.themes must be a boolean`);
|
|
131
|
+
normalized.trajectory = Object.freeze({ ...(raw.port === undefined ? {} : { port: raw.port }), ...(raw.themes === undefined ? (applyDefaults ? { themes: false } : {}) : { themes: raw.themes }) });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!jsonValue(raw))
|
|
135
|
+
fail(errorCode, `${base}.${namespace} must be JSON-compatible`);
|
|
136
|
+
normalized[namespace] = structuredClone(raw);
|
|
137
|
+
}
|
|
138
|
+
return deepFreeze(normalized);
|
|
139
|
+
}
|
|
140
|
+
function positiveRetentionInteger(value) { return positiveInteger(value) && Number.isSafeInteger(value); }
|
|
141
|
+
function validateRetention(value, settingsPath) {
|
|
142
|
+
if (value === undefined)
|
|
143
|
+
return undefined;
|
|
144
|
+
const base = `${settingsPath}.retention`;
|
|
145
|
+
if (!object(value))
|
|
146
|
+
fail("INVALID_SETTINGS", `${base} must be an object`);
|
|
147
|
+
const unknown = Object.keys(value).find((key) => key !== "olderThanDays" && key !== "maxTerminalRuns");
|
|
148
|
+
if (unknown)
|
|
149
|
+
fail("INVALID_SETTINGS", `Unknown retention setting at ${base}: ${unknown}`);
|
|
150
|
+
if (value.olderThanDays !== undefined && !positiveRetentionInteger(value.olderThanDays))
|
|
151
|
+
fail("INVALID_SETTINGS", `${base}.olderThanDays must be a positive integer`);
|
|
152
|
+
if (value.maxTerminalRuns !== undefined && !positiveRetentionInteger(value.maxTerminalRuns))
|
|
153
|
+
fail("INVALID_SETTINGS", `${base}.maxTerminalRuns must be a positive integer`);
|
|
154
|
+
return Object.freeze({ ...(value.olderThanDays === undefined ? {} : { olderThanDays: value.olderThanDays }), ...(value.maxTerminalRuns === undefined ? {} : { maxTerminalRuns: value.maxTerminalRuns }) });
|
|
155
|
+
}
|
|
156
|
+
function parseSettings(path, partial) {
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (isNodeError(error, "ENOENT"))
|
|
163
|
+
return partial ? Object.freeze({}) : DEFAULT_SETTINGS;
|
|
164
|
+
fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
|
|
165
|
+
}
|
|
166
|
+
if (!object(parsed))
|
|
167
|
+
fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
|
|
168
|
+
const allowed = new Set(["concurrency", "modelAliases", "skills", "extensions", "extensionSettings", "tools", "retention", "worktreePostCreateCommand", ...(partial ? [] : ["backgroundWidget"])]);
|
|
169
|
+
const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
|
|
170
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "disabledAgentResources"))
|
|
171
|
+
fail("INVALID_SETTINGS", `disabledAgentResources is no longer supported; use skills, extensions, and tools selectors (settings: ${path})`);
|
|
172
|
+
if (unknown)
|
|
173
|
+
fail("INVALID_SETTINGS", `Unknown workflow setting at ${path}: ${unknown}`);
|
|
174
|
+
const concurrency = parsed.concurrency === undefined ? (partial ? undefined : DEFAULT_SETTINGS.concurrency) : parsed.concurrency;
|
|
175
|
+
if (concurrency !== undefined && (!positiveInteger(concurrency) || concurrency > 16))
|
|
176
|
+
fail("INVALID_SETTINGS", `${path}.concurrency must be an integer from 1 to 16`);
|
|
177
|
+
const backgroundWidget = parsed.backgroundWidget === undefined ? (partial ? undefined : DEFAULT_SETTINGS.backgroundWidget) : parsed.backgroundWidget;
|
|
178
|
+
if (backgroundWidget !== undefined && typeof backgroundWidget !== "boolean")
|
|
179
|
+
fail("INVALID_SETTINGS", `${path}.backgroundWidget must be a boolean`);
|
|
180
|
+
const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
|
|
181
|
+
const skills = validateSelectorList(parsed.skills, path, "skills");
|
|
182
|
+
const tools = validateSelectorList(parsed.tools, path, "tools");
|
|
183
|
+
const extensions = validateSelectorList(parsed.extensions, path, "extensions");
|
|
184
|
+
const extensionSettings = parsed.extensionSettings === undefined ? undefined : validateWorkflowExtensionSettings(parsed.extensionSettings, path, "INVALID_SETTINGS", !partial);
|
|
185
|
+
const retention = validateRetention(parsed.retention, path);
|
|
186
|
+
const worktreePostCreateCommand = validateWorktreePostCreateCommand(parsed.worktreePostCreateCommand, path);
|
|
187
|
+
return Object.freeze({
|
|
188
|
+
...(concurrency === undefined ? {} : { concurrency }), ...(backgroundWidget === undefined ? {} : { backgroundWidget }), ...(modelAliases === undefined ? {} : { modelAliases }),
|
|
189
|
+
...(skills === undefined ? {} : { skills }), ...(extensions === undefined ? {} : { extensions }),
|
|
190
|
+
...(extensionSettings === undefined ? {} : { extensionSettings }), ...(tools === undefined ? {} : { tools }), ...(retention === undefined ? {} : { retention }),
|
|
191
|
+
...(worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand }),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
export function loadSettings(path = workflowSettingsPath()) { return parseSettings(path, false); }
|
|
195
|
+
export function loadSettingsOverrides(path) { return parseSettings(path, true); }
|
|
196
|
+
export function resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath = workflowSettingsPath()) {
|
|
197
|
+
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
198
|
+
const global = loadSettings(globalSettingsPath);
|
|
199
|
+
const project = projectTrusted ? loadSettingsOverrides(projectSettingsPath) : Object.freeze({});
|
|
200
|
+
const projectHas = (key) => Object.prototype.hasOwnProperty.call(project, key);
|
|
201
|
+
const sourceFor = (key) => projectHas(key) ? projectSettingsPath : globalSettingsPath;
|
|
202
|
+
const globalSelectors = selectorsFromSettings(global);
|
|
203
|
+
const projectSelectors = selectorsFromSettings(project);
|
|
204
|
+
const effectiveSelectors = selectorSet({
|
|
205
|
+
skills: [...(globalSelectors.skills ?? []), ...(projectSelectors.skills ?? [])],
|
|
206
|
+
extensions: [...(globalSelectors.extensions ?? []), ...(projectSelectors.extensions ?? [])],
|
|
207
|
+
...(globalSelectors.tools === undefined && projectSelectors.tools === undefined ? {} : { tools: [...(globalSelectors.tools ?? []), ...(projectSelectors.tools ?? [])] }),
|
|
208
|
+
});
|
|
209
|
+
const hasExtensionSelectors = global.extensions !== undefined || project.extensions !== undefined;
|
|
210
|
+
const extensionSettings = projectHas("extensionSettings") ? project.extensionSettings : global.extensionSettings;
|
|
211
|
+
const sources = {
|
|
212
|
+
concurrency: projectHas("concurrency") ? projectSettingsPath : globalSettingsPath,
|
|
213
|
+
modelAliases: projectHas("modelAliases") ? projectSettingsPath : globalSettingsPath,
|
|
214
|
+
skills: sourceFor("skills"), extensions: sourceFor("extensions"), tools: sourceFor("tools"),
|
|
215
|
+
...(extensionSettings === undefined ? {} : { extensionSettings: projectHas("extensionSettings") ? projectSettingsPath : globalSettingsPath }),
|
|
216
|
+
...(project.retention === undefined && global.retention === undefined ? {} : { retention: project.retention === undefined ? globalSettingsPath : projectSettingsPath }),
|
|
217
|
+
...(projectHas("worktreePostCreateCommand") || global.worktreePostCreateCommand !== undefined ? { worktreePostCreateCommand: projectHas("worktreePostCreateCommand") ? projectSettingsPath : globalSettingsPath } : {}),
|
|
218
|
+
};
|
|
219
|
+
const effective = Object.freeze({
|
|
220
|
+
concurrency: project.concurrency ?? global.concurrency,
|
|
221
|
+
backgroundWidget: global.backgroundWidget ?? true,
|
|
222
|
+
...(projectHas("modelAliases") ? { modelAliases: project.modelAliases } : global.modelAliases === undefined ? {} : { modelAliases: global.modelAliases }),
|
|
223
|
+
...(effectiveSelectors.skills.length ? { skills: effectiveSelectors.skills } : global.skills !== undefined || project.skills !== undefined ? { skills: effectiveSelectors.skills } : {}),
|
|
224
|
+
...(hasExtensionSelectors ? { extensions: effectiveSelectors.extensions } : {}),
|
|
225
|
+
...(extensionSettings === undefined ? {} : { extensionSettings }),
|
|
226
|
+
...(effectiveSelectors.tools?.length ? { tools: effectiveSelectors.tools } : global.tools !== undefined || project.tools !== undefined ? { tools: effectiveSelectors.tools } : {}),
|
|
227
|
+
...((project.retention ?? global.retention) === undefined ? {} : { retention: project.retention ?? global.retention }),
|
|
228
|
+
...(projectHas("worktreePostCreateCommand") ? { worktreePostCreateCommand: project.worktreePostCreateCommand } : global.worktreePostCreateCommand === undefined ? {} : { worktreePostCreateCommand: global.worktreePostCreateCommand }),
|
|
229
|
+
});
|
|
230
|
+
return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, sources };
|
|
231
|
+
}
|
|
232
|
+
export function validateModelAliasAvailability(aliases, names, availableModels, knownModels, settingsPath) {
|
|
233
|
+
for (const name of names) {
|
|
234
|
+
try {
|
|
235
|
+
const target = modelCapability(name, aliases, knownModels, settingsPath);
|
|
236
|
+
if (!availableModels.has(target))
|
|
237
|
+
unknownModel(name, target, settingsPath);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
throw annotateModelAliasError(error, name);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export function resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath = workflowSettingsPath()) {
|
|
245
|
+
const resolved = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
|
|
246
|
+
const global = selectorSet(selectorsFromSettings(resolved.global));
|
|
247
|
+
const project = selectorSet(selectorsFromSettings(resolved.project));
|
|
248
|
+
const effective = selectorSet(selectorsFromSettings(resolved.effective));
|
|
249
|
+
return { globalSettingsPath: resolved.globalSettingsPath, projectSettingsPath: resolved.projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [], unmatchedTools: [], selectorSources: { global: selectorsFromSettings(resolved.global), project: selectorsFromSettings(resolved.project) } };
|
|
250
|
+
}
|
|
251
|
+
export function saveModelAliases(path = workflowSettingsPath(), aliases = {}) {
|
|
252
|
+
const normalized = validateModelAliases(aliases, path);
|
|
253
|
+
let parsed = {};
|
|
254
|
+
try {
|
|
255
|
+
loadSettings(path);
|
|
256
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (!isNodeError(error, "ENOENT"))
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
if (!object(parsed))
|
|
263
|
+
fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
|
|
264
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
265
|
+
atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
|
|
266
|
+
}
|
|
267
|
+
export function parseRoleMarkdown(content, strict = false, rolePath) {
|
|
268
|
+
if (!strict) {
|
|
269
|
+
if (!content.startsWith("---\n"))
|
|
270
|
+
return { prompt: content };
|
|
271
|
+
const end = content.indexOf("\n---", 4);
|
|
272
|
+
if (end < 0)
|
|
273
|
+
return { prompt: content };
|
|
274
|
+
const meta = {};
|
|
275
|
+
for (const line of content.slice(4, end).split("\n")) {
|
|
276
|
+
const match = /^(model|tools|skills|extensions|description|overrideSystemPrompt|override_system_prompt|is_system_prompt|contextFiles|extensionSettings|disabledAgentResources|thinking)\s*:\s*(.+)$/.exec(line.trim());
|
|
277
|
+
if (match?.[1] === "disabledAgentResources")
|
|
278
|
+
fail("INVALID_METADATA", "disabledAgentResources is no longer supported; use skills, extensions, and tools selectors");
|
|
279
|
+
if (match?.[1] === "thinking")
|
|
280
|
+
fail("INVALID_METADATA", "Role thinking is not supported; put it on model as provider/model:thinking");
|
|
281
|
+
if (match?.[1] && match[2])
|
|
282
|
+
meta[match[1]] = match[2].trim();
|
|
283
|
+
}
|
|
284
|
+
const unquote = (v) => v.replace(/^['"]|['"]$/g, "");
|
|
285
|
+
const parseList = (value) => value === undefined ? undefined : value.replace(/^\[|\]$/g, "").split(",").map((entry) => unquote(entry.trim())).filter(Boolean);
|
|
286
|
+
const tools = parseList(meta.tools);
|
|
287
|
+
const skills = parseList(meta.skills);
|
|
288
|
+
const extensions = parseList(meta.extensions);
|
|
289
|
+
let extensionSettings;
|
|
290
|
+
if (meta.extensionSettings !== undefined) {
|
|
291
|
+
try {
|
|
292
|
+
extensionSettings = JSON.parse(meta.extensionSettings);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
fail("INVALID_METADATA", `Invalid role extensionSettings: ${errorText(error)}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const rolePathValue = rolePath ?? "<role>";
|
|
299
|
+
const normalizedExtensionSettings = validateWorkflowExtensionSettings(extensionSettings, rolePathValue, "INVALID_METADATA", false);
|
|
300
|
+
const definition = { prompt: content.slice(end + 4).replace(/^\n/, "") };
|
|
301
|
+
if (meta.model) {
|
|
302
|
+
const model = unquote(meta.model);
|
|
303
|
+
assertModelThinking(model, "Role model");
|
|
304
|
+
definition.model = model;
|
|
305
|
+
}
|
|
306
|
+
if (meta.description)
|
|
307
|
+
definition.description = unquote(meta.description);
|
|
308
|
+
if (tools)
|
|
309
|
+
definition.tools = tools;
|
|
310
|
+
if (skills)
|
|
311
|
+
definition.skills = skills;
|
|
312
|
+
if (extensions)
|
|
313
|
+
definition.extensions = extensions;
|
|
314
|
+
if (normalizedExtensionSettings !== undefined)
|
|
315
|
+
definition.extensionSettings = normalizedExtensionSettings;
|
|
316
|
+
const overrideSystemPrompt = meta.overrideSystemPrompt ?? meta.override_system_prompt ?? meta.is_system_prompt;
|
|
317
|
+
const contextFiles = meta.contextFiles ? meta.contextFiles.replace(/^\[|\]$/g, "").split(",").map((scope) => unquote(scope.trim())).filter(Boolean) : undefined;
|
|
318
|
+
const normalizedContextFiles = validateContextFileScopes(contextFiles, "role");
|
|
319
|
+
if (overrideSystemPrompt)
|
|
320
|
+
definition.overrideSystemPrompt = overrideSystemPrompt === "true";
|
|
321
|
+
if (normalizedContextFiles)
|
|
322
|
+
definition.contextFiles = normalizedContextFiles;
|
|
323
|
+
return definition;
|
|
324
|
+
}
|
|
325
|
+
const normalized = content.replace(/\r\n?/g, "\n");
|
|
326
|
+
if (normalized.startsWith("---\n") && normalized.indexOf("\n---", 3) < 0)
|
|
327
|
+
fail("INVALID_METADATA", "Role frontmatter is missing its closing delimiter");
|
|
328
|
+
let parsed;
|
|
329
|
+
try {
|
|
330
|
+
parsed = parseFrontmatter(content);
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
fail("INVALID_METADATA", `Invalid role frontmatter: ${errorText(error)}`);
|
|
334
|
+
}
|
|
335
|
+
if (!object(parsed.frontmatter))
|
|
336
|
+
fail("INVALID_METADATA", "Role frontmatter must be an object");
|
|
337
|
+
const { model, tools, skills, extensions, extensionSettings, description, contextFiles } = parsed.frontmatter;
|
|
338
|
+
if (Object.prototype.hasOwnProperty.call(parsed.frontmatter, "disabledAgentResources"))
|
|
339
|
+
fail("INVALID_METADATA", "disabledAgentResources is no longer supported; use skills, extensions, and tools selectors");
|
|
340
|
+
if (Object.prototype.hasOwnProperty.call(parsed.frontmatter, "thinking"))
|
|
341
|
+
fail("INVALID_METADATA", "Role thinking is not supported; put it on model as provider/model:thinking");
|
|
342
|
+
const overrideSystemPrompt = parsed.frontmatter.overrideSystemPrompt ?? parsed.frontmatter.override_system_prompt ?? parsed.frontmatter.is_system_prompt;
|
|
343
|
+
if (model !== undefined && (typeof model !== "string" || model.trim() === ""))
|
|
344
|
+
fail("INVALID_METADATA", "Role model must be a non-empty string");
|
|
345
|
+
if (description !== undefined && (typeof description !== "string" || description.trim() === "" || description.length > 1024 || /[\r\n]/.test(description)))
|
|
346
|
+
fail("INVALID_METADATA", "Role description must be a non-empty single-line string of at most 1024 characters");
|
|
347
|
+
if (overrideSystemPrompt !== undefined && typeof overrideSystemPrompt !== "boolean")
|
|
348
|
+
fail("INVALID_METADATA", "Role overrideSystemPrompt must be a boolean");
|
|
349
|
+
const normalizedContextFiles = validateContextFileScopes(contextFiles, rolePath ?? "<role>");
|
|
350
|
+
const rolePathValue = rolePath ?? "<role>";
|
|
351
|
+
const normalizedTools = validateSelectorList(tools, rolePathValue, "tools", "INVALID_METADATA");
|
|
352
|
+
const normalizedSkills = validateSelectorList(skills, rolePathValue, "skills", "INVALID_METADATA");
|
|
353
|
+
const normalizedExtensions = validateSelectorList(extensions, rolePathValue, "extensions", "INVALID_METADATA");
|
|
354
|
+
const normalizedExtensionSettings = validateWorkflowExtensionSettings(extensionSettings, rolePathValue, "INVALID_METADATA", false);
|
|
355
|
+
const normalizedDescription = typeof description === "string" ? description.trim() : undefined;
|
|
356
|
+
const normalizedModel = typeof model === "string" ? model.trim() : undefined;
|
|
357
|
+
if (normalizedModel !== undefined)
|
|
358
|
+
assertModelThinking(normalizedModel, "Role model");
|
|
359
|
+
const definition = { prompt: parsed.body };
|
|
360
|
+
if (normalizedDescription !== undefined)
|
|
361
|
+
definition.description = normalizedDescription;
|
|
362
|
+
if (normalizedModel !== undefined)
|
|
363
|
+
definition.model = normalizedModel;
|
|
364
|
+
if (normalizedTools !== undefined)
|
|
365
|
+
definition.tools = normalizedTools;
|
|
366
|
+
if (normalizedSkills !== undefined)
|
|
367
|
+
definition.skills = normalizedSkills;
|
|
368
|
+
if (normalizedExtensions !== undefined)
|
|
369
|
+
definition.extensions = normalizedExtensions;
|
|
370
|
+
if (normalizedExtensionSettings !== undefined)
|
|
371
|
+
definition.extensionSettings = normalizedExtensionSettings;
|
|
372
|
+
if (typeof overrideSystemPrompt === "boolean")
|
|
373
|
+
definition.overrideSystemPrompt = overrideSystemPrompt;
|
|
374
|
+
if (normalizedContextFiles !== undefined)
|
|
375
|
+
definition.contextFiles = normalizedContextFiles;
|
|
376
|
+
return definition;
|
|
377
|
+
}
|
|
378
|
+
const ROLE_DIRECTORY = "pi-extensible-workflows";
|
|
379
|
+
export function workflowRoleDirectories(agentDir = getAgentDir()) {
|
|
380
|
+
return [join(agentDir, ROLE_DIRECTORY, "roles")];
|
|
381
|
+
}
|
|
382
|
+
function projectRoleDirectories(root) {
|
|
383
|
+
return [join(root, ROLE_DIRECTORY, "roles")];
|
|
384
|
+
}
|
|
385
|
+
function roleDirectorySources(dirs) {
|
|
386
|
+
const seen = new Set();
|
|
387
|
+
return dirs.flatMap((value) => {
|
|
388
|
+
const source = typeof value === "string" ? { path: value } : { path: value.path, extension: value.extension, ...(value.builtin === true ? { builtin: true } : {}) };
|
|
389
|
+
if (seen.has(source.path))
|
|
390
|
+
return [];
|
|
391
|
+
seen.add(source.path);
|
|
392
|
+
return [source];
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function roleDirectoryLabel(source, extension = true) {
|
|
396
|
+
return source.extension ? `Extension "${source.extension.headline}" (${source.extension.version})` : extension ? "Registered workflow extension" : "Standard workflow";
|
|
397
|
+
}
|
|
398
|
+
function isRoleFile(dir, entry) {
|
|
399
|
+
if (extname(entry.name) !== ".md")
|
|
400
|
+
return false;
|
|
401
|
+
if (entry.isFile())
|
|
402
|
+
return true;
|
|
403
|
+
if (!entry.isSymbolicLink())
|
|
404
|
+
return false;
|
|
405
|
+
try {
|
|
406
|
+
return statSync(join(dir, entry.name)).isFile();
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
if (isNodeError(error, "ENOENT"))
|
|
410
|
+
return false;
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function scanRoleFiles(dirs, extension) {
|
|
415
|
+
const files = [];
|
|
416
|
+
for (const source of roleDirectorySources(dirs)) {
|
|
417
|
+
let entries;
|
|
418
|
+
try {
|
|
419
|
+
entries = readdirSync(source.path, { withFileTypes: true });
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (!extension && isNodeError(error, "ENOENT"))
|
|
423
|
+
continue;
|
|
424
|
+
fail("INVALID_METADATA", `${roleDirectoryLabel(source, extension)} role directory "${source.path}" could not be scanned: ${errorText(error)}`);
|
|
425
|
+
}
|
|
426
|
+
for (const entry of entries)
|
|
427
|
+
if (isRoleFile(source.path, entry))
|
|
428
|
+
files.push({ name: basename(entry.name, ".md"), path: join(source.path, entry.name), source });
|
|
429
|
+
}
|
|
430
|
+
return files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
|
|
431
|
+
}
|
|
432
|
+
function readRoleDefinitions(dirs, extension = false) {
|
|
433
|
+
const files = scanRoleFiles(dirs, extension);
|
|
434
|
+
const starterFiles = files.filter(({ source }) => source.builtin === true);
|
|
435
|
+
const regularFiles = files.filter(({ source }) => source.builtin !== true);
|
|
436
|
+
if (extension) {
|
|
437
|
+
const byName = new Map();
|
|
438
|
+
for (const file of regularFiles)
|
|
439
|
+
byName.set(file.name, [...(byName.get(file.name) ?? []), file]);
|
|
440
|
+
for (const [name, matches] of byName)
|
|
441
|
+
if (matches.length > 1)
|
|
442
|
+
fail("INVALID_METADATA", `Duplicate extension role "${name}": ${matches.map(({ path, source }) => `${roleDirectoryLabel(source)} role directory "${source.path}" (${path})`).join("; ")}`);
|
|
443
|
+
}
|
|
444
|
+
const read = (roleFiles) => Object.fromEntries(roleFiles.map(({ name, path, source }) => {
|
|
445
|
+
try {
|
|
446
|
+
return [name, parseRoleMarkdown(readFileSync(path, "utf8"), true, path)];
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
if (extension)
|
|
450
|
+
fail("INVALID_METADATA", `${roleDirectoryLabel(source)} role directory "${source.path}" contains invalid role "${name}" at "${path}": ${errorText(error)}`);
|
|
451
|
+
throw error;
|
|
452
|
+
}
|
|
453
|
+
}));
|
|
454
|
+
return extension ? { ...read(starterFiles), ...read(regularFiles) } : read(files);
|
|
455
|
+
}
|
|
456
|
+
export function loadProjectAgentDefinitions(cwd) { return readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))); }
|
|
457
|
+
export function loadAgentDefinitions(cwd, agentDir = getAgentDir(), projectTrusted = true, extensionRoleDirectories = registeredWorkflowRoleDirectoryRegistrations()) {
|
|
458
|
+
return deepFreeze({ ...readRoleDefinitions(extensionRoleDirectories, true), ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
|
|
459
|
+
}
|
|
460
|
+
function validateRolePolicies(definitions, roles, availableModels, aliases = {}, knownModels = availableModels, settingsPath) {
|
|
461
|
+
for (const role of roles) {
|
|
462
|
+
const definition = definitions[role];
|
|
463
|
+
if (!definition)
|
|
464
|
+
continue;
|
|
465
|
+
if (definition.model !== undefined) {
|
|
466
|
+
const resolved = modelCapability(definition.model, aliases, knownModels, settingsPath);
|
|
467
|
+
if (!availableModels.has(resolved)) {
|
|
468
|
+
if (modelAliasName(definition.model, aliases))
|
|
469
|
+
unknownModel(definition.model, resolved, settingsPath);
|
|
470
|
+
fail("UNKNOWN_MODEL", `Unknown model for role ${role}: ${resolved}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// Role tools absent from the session are tolerated at launch: the runtime resolve emits a
|
|
474
|
+
// warning and the agent runs without them. Doctor still reports unmatched patterns.
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function validateWorkflowMetadata(value) {
|
|
478
|
+
if (!object(value) || typeof value.name !== "string" || value.name.trim() === "")
|
|
479
|
+
fail("INVALID_METADATA", "Workflow metadata requires a non-empty name");
|
|
480
|
+
if (value.description !== undefined && (typeof value.description !== "string" || value.description.trim() === ""))
|
|
481
|
+
fail("INVALID_METADATA", "Workflow description must be a non-empty string when provided");
|
|
482
|
+
if (Object.keys(value).some((key) => !["name", "description"].includes(key)))
|
|
483
|
+
fail("INVALID_METADATA", "Unknown workflow metadata");
|
|
484
|
+
return Object.freeze({ name: value.name.trim(), ...(typeof value.description === "string" ? { description: value.description.trim() } : {}) });
|
|
485
|
+
}
|
|
486
|
+
function workflowBody(script) {
|
|
487
|
+
if (typeof script !== "string" || script.trim() === "")
|
|
488
|
+
fail("INVALID_SYNTAX", "Workflow script must be non-empty");
|
|
489
|
+
try {
|
|
490
|
+
const program = acorn.parse(script, { ecmaVersion: "latest", sourceType: "module", allowReturnOutsideFunction: true });
|
|
491
|
+
const first = program.body[0];
|
|
492
|
+
if (first?.type === "ExportNamedDeclaration" && first.declaration?.type === "VariableDeclaration") {
|
|
493
|
+
const declarator = first.declaration.declarations[0];
|
|
494
|
+
if (declarator?.id.type === "Identifier" && declarator.id.name === "meta")
|
|
495
|
+
return script.slice(first.end).replace(/^\s*/, "");
|
|
496
|
+
}
|
|
497
|
+
return script;
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
fail("INVALID_SYNTAX", `Invalid workflow syntax: ${errorText(error)}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function parseWorkflow(script) {
|
|
504
|
+
const body = workflowBody(script);
|
|
505
|
+
try {
|
|
506
|
+
new Script(`(async()=>{${body}\n})`);
|
|
507
|
+
return acorn.parse(body, { ecmaVersion: "latest", sourceType: "module", allowReturnOutsideFunction: true });
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
fail("INVALID_SYNTAX", `Invalid workflow syntax: ${errorText(error)}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function isAcornNode(value) {
|
|
514
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
515
|
+
}
|
|
516
|
+
/** The static name of an object property or record key; computed keys and non-literal expressions have none. */
|
|
517
|
+
function propertyKeyName(property) {
|
|
518
|
+
return property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
|
|
519
|
+
}
|
|
520
|
+
function astChildren(node) {
|
|
521
|
+
const children = [];
|
|
522
|
+
for (const value of Object.values(node)) {
|
|
523
|
+
if (Array.isArray(value)) {
|
|
524
|
+
for (const child of value)
|
|
525
|
+
if (isAcornNode(child))
|
|
526
|
+
children.push(child);
|
|
527
|
+
}
|
|
528
|
+
else if (isAcornNode(value))
|
|
529
|
+
children.push(value);
|
|
530
|
+
}
|
|
531
|
+
return children;
|
|
532
|
+
}
|
|
533
|
+
function isWorkflowCallKind(value) {
|
|
534
|
+
return typeof value === "string" && WORKFLOW_CALL_KINDS.some((kind) => kind === value);
|
|
535
|
+
}
|
|
536
|
+
function isWorkflowCall(node) {
|
|
537
|
+
return node.type === "CallExpression" && node.callee.type === "Identifier" && isWorkflowCallKind(node.callee.name);
|
|
538
|
+
}
|
|
539
|
+
function workflowCallKind(node) {
|
|
540
|
+
return isWorkflowCall(node) ? node.callee.name : undefined;
|
|
541
|
+
}
|
|
542
|
+
function workflowCalls(program) {
|
|
543
|
+
const calls = [];
|
|
544
|
+
const visit = (node) => {
|
|
545
|
+
if (isWorkflowCall(node))
|
|
546
|
+
calls.push(node);
|
|
547
|
+
for (const child of astChildren(node))
|
|
548
|
+
visit(child);
|
|
549
|
+
};
|
|
550
|
+
visit(program);
|
|
551
|
+
return calls.sort((left, right) => left.start - right.start);
|
|
552
|
+
}
|
|
553
|
+
function workflowCallsWithStructure(program) {
|
|
554
|
+
const calls = [];
|
|
555
|
+
const visit = (node, context) => {
|
|
556
|
+
let current = context;
|
|
557
|
+
if (node.type === "Property" && current.structure.length) {
|
|
558
|
+
const scope = current.structure.at(-1);
|
|
559
|
+
const key = propertyKeyName(node);
|
|
560
|
+
if (scope?.key === null && key)
|
|
561
|
+
current = { ...current, structure: [...current.structure.slice(0, -1), { ...scope, key }] };
|
|
562
|
+
}
|
|
563
|
+
if (isWorkflowCall(node)) {
|
|
564
|
+
const call = node;
|
|
565
|
+
const operation = call.callee.name;
|
|
566
|
+
const execution = operation === "parallel" ? "parallel" : operation === "pipeline" ? "sequential" : current.execution;
|
|
567
|
+
calls.push({ call, execution, structure: current.structure });
|
|
568
|
+
for (const [index, argument] of call.arguments.entries()) {
|
|
569
|
+
if (argument.type === "SpreadElement")
|
|
570
|
+
continue;
|
|
571
|
+
const scopeKind = operation === "parallel" && index === 1 ? "parallel" : operation === "pipeline" && index === 2 ? "pipeline" : undefined;
|
|
572
|
+
visit(argument, scopeKind ? { execution, structure: [...current.structure, { kind: scopeKind, name: staticString(callArgument(call, 0)), key: null }] } : current);
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
for (const child of astChildren(node))
|
|
577
|
+
visit(child, current);
|
|
578
|
+
};
|
|
579
|
+
visit(program, { execution: "sequential", structure: [] });
|
|
580
|
+
return calls.sort((left, right) => left.call.start - right.call.start);
|
|
581
|
+
}
|
|
582
|
+
function memberCall(node, objectName, propertyName) {
|
|
583
|
+
if (node?.type !== "CallExpression" || node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.object.type !== "Identifier" || node.callee.object.name !== objectName || node.callee.property.type !== "Identifier")
|
|
584
|
+
return false;
|
|
585
|
+
return node.callee.property.name === propertyName;
|
|
586
|
+
}
|
|
587
|
+
function mapCallback(node) {
|
|
588
|
+
if (!memberCall(node, "Promise", "all") && !memberCall(node, "Promise", "allSettled"))
|
|
589
|
+
return undefined;
|
|
590
|
+
if (node.type !== "CallExpression")
|
|
591
|
+
return undefined;
|
|
592
|
+
const source = node.arguments[0];
|
|
593
|
+
if (source?.type !== "CallExpression" || source.callee.type !== "MemberExpression" || source.callee.computed || source.callee.property.type !== "Identifier" || !["map", "flatMap"].includes(source.callee.property.name))
|
|
594
|
+
return undefined;
|
|
595
|
+
const callback = source.arguments[0];
|
|
596
|
+
return callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression" ? callback : undefined;
|
|
597
|
+
}
|
|
598
|
+
function hasUnscopedAgent(node, scoped = false) {
|
|
599
|
+
const operation = workflowCallKind(node);
|
|
600
|
+
if (operation === "agent")
|
|
601
|
+
return !scoped;
|
|
602
|
+
const nestedScope = scoped || operation === "parallel" || operation === "pipeline";
|
|
603
|
+
return astChildren(node).some((child) => hasUnscopedAgent(child, nestedScope));
|
|
604
|
+
}
|
|
605
|
+
function validateObviousConcurrentAgentCalls(program) {
|
|
606
|
+
const visit = (node) => {
|
|
607
|
+
const callback = mapCallback(node);
|
|
608
|
+
if (callback && hasUnscopedAgent(callback))
|
|
609
|
+
fail("INVALID_METADATA", "Promise.all/map agent fan-out cannot prove stable call-site identity; use parallel(...) or pipeline(...)");
|
|
610
|
+
for (const child of astChildren(node))
|
|
611
|
+
visit(child);
|
|
612
|
+
};
|
|
613
|
+
visit(program);
|
|
614
|
+
}
|
|
615
|
+
function validateDirectPrimitiveReferences(program, name) {
|
|
616
|
+
const visit = (node, parent) => {
|
|
617
|
+
if (node.type === "Identifier" && node.name === name) {
|
|
618
|
+
const directCall = parent?.type === "CallExpression" && parent.callee === node;
|
|
619
|
+
const propertyKey = parent?.type === "Property" && parent.key === node && !parent.computed && !parent.shorthand;
|
|
620
|
+
if (!directCall && !propertyKey)
|
|
621
|
+
fail("INVALID_METADATA", `${name} calls must use a direct ${name}(...) call; aliases and indirect calls are unsupported`);
|
|
622
|
+
}
|
|
623
|
+
for (const child of astChildren(node))
|
|
624
|
+
visit(child, node);
|
|
625
|
+
};
|
|
626
|
+
visit(program);
|
|
627
|
+
}
|
|
628
|
+
function validateRemovedWorkflowPrimitives(program, code) {
|
|
629
|
+
const visit = (node) => {
|
|
630
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "conversation")
|
|
631
|
+
fail(code, "conversation() was removed; pass prior agent results explicitly");
|
|
632
|
+
for (const child of astChildren(node))
|
|
633
|
+
visit(child);
|
|
634
|
+
};
|
|
635
|
+
visit(program);
|
|
636
|
+
}
|
|
637
|
+
function validateDirectAgentReferences(program, code) {
|
|
638
|
+
const visit = (node, parent, grandparent) => {
|
|
639
|
+
if (node.type === "Identifier" && node.name === "agent") {
|
|
640
|
+
const directCall = parent?.type === "CallExpression" && parent.callee === node;
|
|
641
|
+
const createCall = parent?.type === "MemberExpression" && parent.object === node && !parent.computed && parent.property.type === "Identifier" && parent.property.name === "create" && grandparent?.type === "CallExpression" && grandparent.callee === parent;
|
|
642
|
+
const propertyName = parent?.type === "Property" && parent.key === node && !parent.computed && !parent.shorthand || parent?.type === "MemberExpression" && parent.property === node && !parent.computed;
|
|
643
|
+
if (!directCall && !createCall && !propertyName)
|
|
644
|
+
fail(code, "agent calls must use a direct agent(...) or agent.create(...) call; aliases and indirect calls are unsupported");
|
|
645
|
+
}
|
|
646
|
+
for (const child of astChildren(node))
|
|
647
|
+
visit(child, node, parent);
|
|
648
|
+
};
|
|
649
|
+
visit(program);
|
|
650
|
+
}
|
|
651
|
+
function agentCreateCalls(program) {
|
|
652
|
+
const calls = [];
|
|
653
|
+
const visit = (node) => {
|
|
654
|
+
if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.object.type === "Identifier" && node.callee.object.name === "agent" && node.callee.property.type === "Identifier" && node.callee.property.name === "create")
|
|
655
|
+
calls.push(node);
|
|
656
|
+
for (const child of astChildren(node))
|
|
657
|
+
visit(child);
|
|
658
|
+
};
|
|
659
|
+
visit(program);
|
|
660
|
+
return calls;
|
|
661
|
+
}
|
|
662
|
+
function hasIdentifier(node, name) {
|
|
663
|
+
if (node.type === "Identifier" && node.name === name)
|
|
664
|
+
return true;
|
|
665
|
+
return astChildren(node).some((child) => hasIdentifier(child, name));
|
|
666
|
+
}
|
|
667
|
+
const INTERNAL_AGENT_NAME = "__pi_extensible_workflows_agent";
|
|
668
|
+
const INTERNAL_WORKTREE_NAME = "__pi_extensible_workflows_withWorktree";
|
|
669
|
+
const INTERNAL_SHELL_NAME = "__pi_extensible_workflows_shell";
|
|
670
|
+
const RESERVED_IDENTIFIERS = [[INTERNAL_AGENT_NAME, "agent"], [INTERNAL_WORKTREE_NAME, "withWorktree"], [INTERNAL_SHELL_NAME, "shell"]];
|
|
671
|
+
/** Workflow source must not mention the identifiers instrumentation rewrites primitive calls into. */
|
|
672
|
+
function assertNoReservedIdentifiers(program) {
|
|
673
|
+
for (const [name, primitive] of RESERVED_IDENTIFIERS)
|
|
674
|
+
if (hasIdentifier(program, name))
|
|
675
|
+
fail("INVALID_METADATA", `${name} is reserved for workflow ${primitive} instrumentation`);
|
|
676
|
+
}
|
|
677
|
+
function callHasTrailingComma(source, call) {
|
|
678
|
+
let previous;
|
|
679
|
+
let current;
|
|
680
|
+
for (const token of acorn.tokenizer(source.slice(call.start, call.end), { ecmaVersion: "latest", sourceType: "module" })) {
|
|
681
|
+
previous = current;
|
|
682
|
+
current = token;
|
|
683
|
+
}
|
|
684
|
+
return current?.type.label === ")" && previous?.type.label === ",";
|
|
685
|
+
}
|
|
686
|
+
export function instrumentWorkflow(script) {
|
|
687
|
+
const body = workflowBody(script);
|
|
688
|
+
if (!body.trim())
|
|
689
|
+
return body;
|
|
690
|
+
const program = parseWorkflow(body);
|
|
691
|
+
assertNoReservedIdentifiers(program);
|
|
692
|
+
validateRemovedWorkflowPrimitives(program, "INVALID_METADATA");
|
|
693
|
+
const calls = workflowCalls(program).filter((call) => ["agent", "withWorktree", "shell"].includes(call.callee.name));
|
|
694
|
+
const edits = calls.flatMap((call) => {
|
|
695
|
+
const replacement = { start: call.callee.start, end: call.callee.end, text: call.callee.name === "agent" ? INTERNAL_AGENT_NAME : call.callee.name === "withWorktree" ? INTERNAL_WORKTREE_NAME : INTERNAL_SHELL_NAME };
|
|
696
|
+
if (call.callee.name === "withWorktree")
|
|
697
|
+
return [replacement];
|
|
698
|
+
const callSite = `${String(call.start)}:${String(call.end)}`;
|
|
699
|
+
const hiddenArgument = call.arguments.length === 0 || callHasTrailingComma(body, call) ? "" : ", ";
|
|
700
|
+
return [replacement, { start: call.end - 1, end: call.end - 1, text: `${hiddenArgument}${JSON.stringify(callSite)}` }];
|
|
701
|
+
}).sort((left, right) => right.start - left.start);
|
|
702
|
+
let instrumented = body;
|
|
703
|
+
for (const edit of edits)
|
|
704
|
+
instrumented = instrumented.slice(0, edit.start) + edit.text + instrumented.slice(edit.end);
|
|
705
|
+
return instrumented;
|
|
706
|
+
}
|
|
707
|
+
function literalString(node) {
|
|
708
|
+
return node?.type === "Literal" && typeof node.value === "string" ? node.value : undefined;
|
|
709
|
+
}
|
|
710
|
+
function propertyNode(node, name) {
|
|
711
|
+
if (node?.type !== "ObjectExpression")
|
|
712
|
+
return undefined;
|
|
713
|
+
for (let index = node.properties.length - 1; index >= 0; index -= 1) {
|
|
714
|
+
const property = node.properties[index];
|
|
715
|
+
if (!property || property.type === "SpreadElement" || property.computed)
|
|
716
|
+
return undefined;
|
|
717
|
+
const key = propertyKeyName(property);
|
|
718
|
+
if (key === name)
|
|
719
|
+
return property.value;
|
|
720
|
+
}
|
|
721
|
+
return undefined;
|
|
722
|
+
}
|
|
723
|
+
function stableName(node) {
|
|
724
|
+
if (!node)
|
|
725
|
+
return false;
|
|
726
|
+
if (node.type !== "ObjectExpression") {
|
|
727
|
+
if (["Literal", "ArrayExpression", "ArrowFunctionExpression", "FunctionExpression", "ClassExpression", "TemplateLiteral", "UnaryExpression", "UpdateExpression", "BinaryExpression"].includes(node.type))
|
|
728
|
+
return false;
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
let result = false;
|
|
732
|
+
for (const property of node.properties) {
|
|
733
|
+
if (property.type === "SpreadElement" || property.computed) {
|
|
734
|
+
result = undefined;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const key = propertyKeyName(property);
|
|
738
|
+
if (key !== "name")
|
|
739
|
+
continue;
|
|
740
|
+
const value = literalString(property.value);
|
|
741
|
+
result = value === undefined ? property.value.type === "Literal" ? false : undefined : value.trim() !== "";
|
|
742
|
+
}
|
|
743
|
+
return result;
|
|
744
|
+
}
|
|
745
|
+
export function workflowPrompt(template, values) {
|
|
746
|
+
if (typeof template !== "string")
|
|
747
|
+
fail("INVALID_METADATA", "prompt() template must be a string");
|
|
748
|
+
if (!object(values) || Array.isArray(values) || !jsonValue(values))
|
|
749
|
+
fail("INVALID_METADATA", "prompt() values must be a plain JSON-compatible object");
|
|
750
|
+
const placeholders = [...template.matchAll(/{{|}}|{([A-Za-z_$][\w$]*)}/g)].flatMap((match) => match[1] === undefined ? [] : [match[1]]);
|
|
751
|
+
const used = new Set(placeholders);
|
|
752
|
+
const keys = Object.keys(values);
|
|
753
|
+
const missing = placeholders.find((key) => !Object.prototype.hasOwnProperty.call(values, key));
|
|
754
|
+
if (missing)
|
|
755
|
+
fail("INVALID_METADATA", `Missing prompt value "${missing}"`);
|
|
756
|
+
const unused = keys.find((key) => !used.has(key));
|
|
757
|
+
if (unused !== undefined)
|
|
758
|
+
fail("INVALID_METADATA", `Unused prompt value "${unused}"`);
|
|
759
|
+
return template.replace(/{{|}}|{([A-Za-z_$][\w$]*)}/g, (match, key) => {
|
|
760
|
+
if (match === "{{")
|
|
761
|
+
return "{";
|
|
762
|
+
if (match === "}}")
|
|
763
|
+
return "}";
|
|
764
|
+
if (typeof key !== "string")
|
|
765
|
+
return match;
|
|
766
|
+
const value = values[key];
|
|
767
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
export function validateSchema(schema, at = "schema") {
|
|
771
|
+
if (!object(schema) || Object.getPrototypeOf(schema) !== Object.prototype || !jsonValue(schema))
|
|
772
|
+
fail("INVALID_SCHEMA", `${at} must be a plain JSON-compatible Schema object`);
|
|
773
|
+
if (typeof schema.type !== "string" && !Array.isArray(schema.type) && schema.$ref === undefined && schema.anyOf === undefined && schema.oneOf === undefined && schema.allOf === undefined && schema.const === undefined && schema.enum === undefined)
|
|
774
|
+
fail("INVALID_SCHEMA", `${at} has no JSON Schema shape`);
|
|
775
|
+
if (schema.required !== undefined && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string")))
|
|
776
|
+
fail("INVALID_SCHEMA", `${at}.required must be an array of strings`);
|
|
777
|
+
if (schema.properties !== undefined && !object(schema.properties))
|
|
778
|
+
fail("INVALID_SCHEMA", `${at}.properties must be an object`);
|
|
779
|
+
}
|
|
780
|
+
const AGENT_OPTION_KEYS = new Set(["label", "model", "tools", "skills", "extensions", "contextFiles", "role", "outputSchema", "retries", "timeoutMs"]);
|
|
781
|
+
function validateAgentOption(key, value, aliases, knownModels, settingsPath) {
|
|
782
|
+
switch (key) {
|
|
783
|
+
case "label":
|
|
784
|
+
if (typeof value !== "string" || !value.trim())
|
|
785
|
+
fail("INVALID_METADATA", "agent label must be a non-empty string");
|
|
786
|
+
break;
|
|
787
|
+
case "model":
|
|
788
|
+
if (typeof value !== "string" || !value.trim())
|
|
789
|
+
fail("INVALID_METADATA", "agent model must be a non-empty string");
|
|
790
|
+
assertModelThinking(value, "agent model");
|
|
791
|
+
if (aliases !== undefined)
|
|
792
|
+
resolveModelReference(value, aliases, knownModels, settingsPath);
|
|
793
|
+
break;
|
|
794
|
+
case "tools":
|
|
795
|
+
case "skills":
|
|
796
|
+
case "extensions":
|
|
797
|
+
validateSelectorList(value, "agent options", key, "INVALID_METADATA", key !== "extensions");
|
|
798
|
+
break;
|
|
799
|
+
case "contextFiles":
|
|
800
|
+
validateContextFileScopes(value, "agent options");
|
|
801
|
+
break;
|
|
802
|
+
case "role":
|
|
803
|
+
if (typeof value !== "string" || !value.trim())
|
|
804
|
+
fail("INVALID_METADATA", "agent role must be a non-empty string");
|
|
805
|
+
break;
|
|
806
|
+
case "outputSchema":
|
|
807
|
+
validateSchema(value, "agent outputSchema");
|
|
808
|
+
break;
|
|
809
|
+
case "retries":
|
|
810
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0)
|
|
811
|
+
fail("INVALID_METADATA", "agent retries must be a non-negative integer");
|
|
812
|
+
break;
|
|
813
|
+
case "timeoutMs":
|
|
814
|
+
if (value !== null && !positiveInteger(value))
|
|
815
|
+
fail("INVALID_METADATA", "agent timeoutMs must be null or a positive integer");
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
export function validateAgentOptions(value) {
|
|
820
|
+
if (!object(value) || !jsonValue(value))
|
|
821
|
+
fail("INVALID_METADATA", "agent options must be a JSON object");
|
|
822
|
+
if (Object.prototype.hasOwnProperty.call(value, "thinking"))
|
|
823
|
+
fail("INVALID_METADATA", "agent thinking is not supported; use model provider/model:thinking");
|
|
824
|
+
for (const [key, option] of Object.entries(value))
|
|
825
|
+
if (AGENT_OPTION_KEYS.has(key))
|
|
826
|
+
validateAgentOption(key, option);
|
|
827
|
+
return value;
|
|
828
|
+
}
|
|
829
|
+
const SHELL_OPTION_KEYS = new Set(["timeoutMs", "env"]);
|
|
830
|
+
function isStringRecord(value) { return object(value) && Object.values(value).every((entry) => typeof entry === "string"); }
|
|
831
|
+
export function validateShellOptions(value) {
|
|
832
|
+
if (value === undefined)
|
|
833
|
+
return {};
|
|
834
|
+
if (!object(value) || !jsonValue(value) || Object.keys(value).some((key) => !SHELL_OPTION_KEYS.has(key)))
|
|
835
|
+
fail("INVALID_METADATA", "shell options must contain only timeoutMs and env");
|
|
836
|
+
if (value.timeoutMs !== undefined && !positiveInteger(value.timeoutMs))
|
|
837
|
+
fail("INVALID_METADATA", "shell timeoutMs must be a positive integer");
|
|
838
|
+
const env = value.env;
|
|
839
|
+
if (env !== undefined && !isStringRecord(env))
|
|
840
|
+
fail("INVALID_METADATA", "shell env must be an object of strings");
|
|
841
|
+
return { ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }), ...(env === undefined ? {} : { env }) };
|
|
842
|
+
}
|
|
843
|
+
export function validateShellCommand(value) {
|
|
844
|
+
if (typeof value !== "string")
|
|
845
|
+
fail("INVALID_METADATA", "shell command must be a string");
|
|
846
|
+
return value;
|
|
847
|
+
}
|
|
848
|
+
function staticValue(node) {
|
|
849
|
+
if (!node)
|
|
850
|
+
return { known: false };
|
|
851
|
+
if (node.type === "Literal")
|
|
852
|
+
return { known: true, value: node.value };
|
|
853
|
+
if (node.type === "UnaryExpression" && (node.operator === "-" || node.operator === "+")) {
|
|
854
|
+
const argument = staticValue(node.argument);
|
|
855
|
+
return argument.known && typeof argument.value === "number" ? { known: true, value: node.operator === "-" ? -argument.value : argument.value } : { known: false };
|
|
856
|
+
}
|
|
857
|
+
if (node.type === "ArrayExpression") {
|
|
858
|
+
const values = [];
|
|
859
|
+
for (const element of node.elements) {
|
|
860
|
+
if (!element || element.type === "SpreadElement")
|
|
861
|
+
return { known: false };
|
|
862
|
+
const value = staticValue(element);
|
|
863
|
+
if (!value.known)
|
|
864
|
+
return { known: false };
|
|
865
|
+
values.push(value.value);
|
|
866
|
+
}
|
|
867
|
+
return { known: true, value: values };
|
|
868
|
+
}
|
|
869
|
+
if (node.type === "ObjectExpression") {
|
|
870
|
+
const value = {};
|
|
871
|
+
for (const property of node.properties) {
|
|
872
|
+
if (property.type === "SpreadElement" || property.computed)
|
|
873
|
+
return { known: false };
|
|
874
|
+
const key = propertyKeyName(property);
|
|
875
|
+
const child = staticValue(property.value);
|
|
876
|
+
if (!key || !child.known)
|
|
877
|
+
return { known: false };
|
|
878
|
+
value[key] = child.value;
|
|
879
|
+
}
|
|
880
|
+
return { known: true, value };
|
|
881
|
+
}
|
|
882
|
+
return { known: false };
|
|
883
|
+
}
|
|
884
|
+
function callArgument(call, index) {
|
|
885
|
+
const argument = call.arguments[index];
|
|
886
|
+
return argument?.type === "SpreadElement" ? undefined : argument;
|
|
887
|
+
}
|
|
888
|
+
function staticString(node) {
|
|
889
|
+
const value = staticValue(node);
|
|
890
|
+
return value.known && typeof value.value === "string" ? value.value : null;
|
|
891
|
+
}
|
|
892
|
+
export function inspectWorkflowScript(script) {
|
|
893
|
+
return workflowCallsWithStructure(parseWorkflow(script)).map(({ call, execution, structure }) => {
|
|
894
|
+
const kind = call.callee.name;
|
|
895
|
+
const first = callArgument(call, 0);
|
|
896
|
+
const options = callArgument(call, 1);
|
|
897
|
+
const placement = { execution, structure };
|
|
898
|
+
if (kind === "agent") {
|
|
899
|
+
const retries = staticValue(propertyNode(options, "retries"));
|
|
900
|
+
const outputSchema = staticValue(propertyNode(options, "outputSchema"));
|
|
901
|
+
const staticOutputSchema = outputSchema.known && jsonObject(outputSchema.value) ? outputSchema.value : undefined;
|
|
902
|
+
const optionKeys = options?.type === "ObjectExpression" ? options.properties.flatMap((property) => {
|
|
903
|
+
if (property.type === "SpreadElement" || property.computed)
|
|
904
|
+
return [];
|
|
905
|
+
const key = propertyKeyName(property);
|
|
906
|
+
return key ? [key] : [];
|
|
907
|
+
}) : [];
|
|
908
|
+
const knownOptionEntries = [];
|
|
909
|
+
for (const key of optionKeys) {
|
|
910
|
+
const value = staticValue(propertyNode(options, key));
|
|
911
|
+
if (value.known && jsonValue(value.value))
|
|
912
|
+
knownOptionEntries.push([key, value.value]);
|
|
913
|
+
}
|
|
914
|
+
const knownOptions = Object.fromEntries(knownOptionEntries);
|
|
915
|
+
const base = { ...placement, kind, start: call.start, end: call.end, name: null, prompt: staticString(first), model: staticString(propertyNode(options, "model")), label: staticString(propertyNode(options, "label")), role: staticString(propertyNode(options, "role")) };
|
|
916
|
+
return { ...base, ...(retries.known && typeof retries.value === "number" ? { retries: retries.value } : {}), ...(staticOutputSchema === undefined ? {} : { outputSchema: staticOutputSchema }), ...(optionKeys.length ? { options: knownOptions, optionKeys } : {}) };
|
|
917
|
+
}
|
|
918
|
+
if (kind === "checkpoint")
|
|
919
|
+
return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null, role: null };
|
|
920
|
+
if (kind === "shell")
|
|
921
|
+
return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
|
|
922
|
+
return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function validateStaticAgentOptions(node, aliases = {}, knownModels, settingsPath) {
|
|
926
|
+
if (node?.type !== "ObjectExpression")
|
|
927
|
+
return;
|
|
928
|
+
for (const key of AGENT_OPTION_KEYS) {
|
|
929
|
+
const value = staticValue(propertyNode(node, key));
|
|
930
|
+
if (value.known)
|
|
931
|
+
validateAgentOption(key, value.value, aliases, knownModels, settingsPath);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
function hasDynamicAgentRole(node) {
|
|
935
|
+
if (!node)
|
|
936
|
+
return false;
|
|
937
|
+
if (node.type !== "ObjectExpression")
|
|
938
|
+
return true;
|
|
939
|
+
for (let index = node.properties.length - 1; index >= 0; index -= 1) {
|
|
940
|
+
const property = node.properties[index];
|
|
941
|
+
if (!property || property.type === "SpreadElement" || property.computed)
|
|
942
|
+
return true;
|
|
943
|
+
const key = propertyKeyName(property);
|
|
944
|
+
if (key === "role") {
|
|
945
|
+
const roleValue = staticValue(property.value);
|
|
946
|
+
if (roleValue.known && typeof roleValue.value === "string")
|
|
947
|
+
return false;
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
function validateStaticShellOptions(call) {
|
|
954
|
+
if (call.arguments.some((argument) => argument.type === "SpreadElement"))
|
|
955
|
+
return;
|
|
956
|
+
if (call.arguments.length !== 1 && call.arguments.length !== 2)
|
|
957
|
+
fail("INVALID_METADATA", "shell requires a command string and optional options");
|
|
958
|
+
const command = staticValue(callArgument(call, 0));
|
|
959
|
+
if (command.known)
|
|
960
|
+
validateShellCommand(command.value);
|
|
961
|
+
const options = staticValue(callArgument(call, 1));
|
|
962
|
+
if (options.known)
|
|
963
|
+
validateShellOptions(options.value);
|
|
964
|
+
}
|
|
965
|
+
function validateStaticWithWorktree(call, compatibility) {
|
|
966
|
+
if (call.arguments.some((argument) => argument.type === "SpreadElement"))
|
|
967
|
+
return;
|
|
968
|
+
if (call.arguments.length !== 2)
|
|
969
|
+
fail(compatibility ? "RESUME_INCOMPATIBLE" : "INVALID_METADATA", "withWorktree requires a name and callback");
|
|
970
|
+
const callback = call.arguments[1];
|
|
971
|
+
if (staticValue(callback).known)
|
|
972
|
+
fail("INVALID_METADATA", "withWorktree callback must be a function");
|
|
973
|
+
const name = staticValue(callArgument(call, 0));
|
|
974
|
+
if (name.known && (typeof name.value !== "string" || !name.value.trim()))
|
|
975
|
+
fail("INVALID_METADATA", "withWorktree name must be a non-empty string");
|
|
976
|
+
}
|
|
977
|
+
export function preflight(script, capabilities, schemas = [], metadata = { name: "workflow" }, compatibility = false) {
|
|
978
|
+
const checkedMetadata = validateWorkflowMetadata(metadata);
|
|
979
|
+
const program = parseWorkflow(script);
|
|
980
|
+
assertNoReservedIdentifiers(program);
|
|
981
|
+
validateDirectPrimitiveReferences(program, "withWorktree");
|
|
982
|
+
validateRemovedWorkflowPrimitives(program, compatibility ? "RESUME_INCOMPATIBLE" : "INVALID_METADATA");
|
|
983
|
+
validateDirectPrimitiveReferences(program, "shell");
|
|
984
|
+
validateDirectAgentReferences(program, compatibility ? "RESUME_INCOMPATIBLE" : "INVALID_METADATA");
|
|
985
|
+
const checkedSchemas = [];
|
|
986
|
+
for (const [index, schema] of schemas.entries()) {
|
|
987
|
+
validateSchema(schema, `schema[${String(index)}]`);
|
|
988
|
+
checkedSchemas.push(schema);
|
|
989
|
+
}
|
|
990
|
+
const calls = workflowCalls(program);
|
|
991
|
+
validateObviousConcurrentAgentCalls(program);
|
|
992
|
+
const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase) => phase !== undefined);
|
|
993
|
+
for (const call of calls) {
|
|
994
|
+
const operation = call.callee.name;
|
|
995
|
+
if (operation === "agent")
|
|
996
|
+
validateStaticAgentOptions(call.arguments[1], capabilities.modelAliases ?? {}, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath);
|
|
997
|
+
if (operation === "withWorktree")
|
|
998
|
+
validateStaticWithWorktree(call, compatibility);
|
|
999
|
+
if (operation === "shell")
|
|
1000
|
+
validateStaticShellOptions(call);
|
|
1001
|
+
if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement"))
|
|
1002
|
+
continue;
|
|
1003
|
+
if (operation === "checkpoint" && stableName(call.arguments[0]) === false)
|
|
1004
|
+
fail("INVALID_METADATA", `${operation} requires a stable explicit name`);
|
|
1005
|
+
if (operation === "parallel" && (call.arguments.length !== 2 || !literalString(call.arguments[0])?.trim() || call.arguments[1]?.type !== "ObjectExpression"))
|
|
1006
|
+
fail("INVALID_METADATA", "parallel requires an operation name string and tasks record");
|
|
1007
|
+
if (operation === "pipeline" && (call.arguments.length !== 3 || !literalString(call.arguments[0])?.trim() || call.arguments[1]?.type !== "ObjectExpression" || call.arguments[2]?.type !== "ObjectExpression"))
|
|
1008
|
+
fail("INVALID_METADATA", "pipeline requires an operation name string, items record, and stages record");
|
|
1009
|
+
}
|
|
1010
|
+
const handleOptions = agentCreateCalls(program).map((call) => {
|
|
1011
|
+
const options = call.arguments.length === 1 ? callArgument(call, 0) : undefined;
|
|
1012
|
+
if (options?.type !== "ObjectExpression" || !literalString(propertyNode(options, "name"))?.trim())
|
|
1013
|
+
fail("INVALID_METADATA", "agent.create requires an options object with a stable explicit name");
|
|
1014
|
+
validateStaticAgentOptions(options, capabilities.modelAliases ?? {}, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath);
|
|
1015
|
+
return options;
|
|
1016
|
+
});
|
|
1017
|
+
const agentOptions = [...calls.filter((call) => call.callee.name === "agent").map((call) => callArgument(call, 1)), ...handleOptions];
|
|
1018
|
+
const dynamicAgentRoles = agentOptions.some((options) => hasDynamicAgentRole(options));
|
|
1019
|
+
const staticSchemas = [];
|
|
1020
|
+
for (const options of agentOptions) {
|
|
1021
|
+
const value = staticValue(propertyNode(options, "outputSchema"));
|
|
1022
|
+
if (!value.known)
|
|
1023
|
+
continue;
|
|
1024
|
+
const schema = value.value;
|
|
1025
|
+
validateSchema(schema, `agent outputSchema[${String(staticSchemas.length)}]`);
|
|
1026
|
+
staticSchemas.push(schema);
|
|
1027
|
+
}
|
|
1028
|
+
checkedSchemas.push(...staticSchemas);
|
|
1029
|
+
const modelRefs = agentOptions.flatMap((options) => { const requested = literalString(propertyNode(options, "model")); return requested === undefined ? [] : [{ requested, resolved: modelCapability(requested, capabilities.modelAliases, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath) }]; });
|
|
1030
|
+
const models = modelRefs.map(({ resolved }) => resolved);
|
|
1031
|
+
const tools = agentOptions.flatMap((options) => {
|
|
1032
|
+
const value = propertyNode(options, "tools");
|
|
1033
|
+
return value?.type === "ArrayExpression" ? value.elements.flatMap((element) => { const tool = element && element.type !== "SpreadElement" ? literalString(element) : undefined; return tool === undefined ? [] : [tool]; }) : [];
|
|
1034
|
+
});
|
|
1035
|
+
const agentTypes = agentOptions.flatMap((options) => { const value = staticString(propertyNode(options, "role")); return value === null ? [] : [value]; });
|
|
1036
|
+
for (const pattern of tools) {
|
|
1037
|
+
const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
|
|
1038
|
+
if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !capabilities.tools.has(body))
|
|
1039
|
+
fail("UNKNOWN_TOOL", `Unknown tool: ${body}`);
|
|
1040
|
+
}
|
|
1041
|
+
const missingModel = capabilities.skipModelAvailability ? undefined : modelRefs.find(({ resolved }) => !capabilities.models.has(resolved));
|
|
1042
|
+
if (missingModel) {
|
|
1043
|
+
if (modelAliasName(missingModel.requested, capabilities.modelAliases ?? {}))
|
|
1044
|
+
unknownModel(missingModel.requested, missingModel.resolved, capabilities.settingsPath);
|
|
1045
|
+
fail("UNKNOWN_MODEL", `Unknown model: ${missingModel.resolved}`);
|
|
1046
|
+
}
|
|
1047
|
+
const missingType = agentTypes.find((type) => !capabilities.agentTypes.has(type));
|
|
1048
|
+
if (missingType)
|
|
1049
|
+
fail("UNKNOWN_AGENT_TYPE", `Unknown agent type: ${missingType}`);
|
|
1050
|
+
return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools, agentTypes }), schemas: deepFreeze(checkedSchemas), dynamicAgentRoles });
|
|
1051
|
+
}
|
|
1052
|
+
export function validateWorkflowLaunch(params, context, registry) {
|
|
1053
|
+
return validateWorkflowLaunchWithRegistry(params, context, registry);
|
|
1054
|
+
}
|
|
1055
|
+
export function validateWorkflowLaunchWithRegistry(params, context, registry) {
|
|
1056
|
+
if (Object.prototype.hasOwnProperty.call(params, "maxAgentLaunches"))
|
|
1057
|
+
fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
|
|
1058
|
+
const hasScript = params.script !== undefined;
|
|
1059
|
+
const hasScriptPath = params.scriptPath !== undefined;
|
|
1060
|
+
if (hasScript && hasScriptPath)
|
|
1061
|
+
fail("INVALID_METADATA", "Provide either script or scriptPath, not more than one");
|
|
1062
|
+
const scriptPath = typeof params.scriptPath === "string" ? params.scriptPath.trim() : undefined;
|
|
1063
|
+
if (hasScriptPath && !scriptPath)
|
|
1064
|
+
fail("INVALID_METADATA", "scriptPath must be a non-empty path");
|
|
1065
|
+
let fileScript;
|
|
1066
|
+
if (scriptPath !== undefined) {
|
|
1067
|
+
try {
|
|
1068
|
+
fileScript = readFileSync(resolve(context.cwd, scriptPath), "utf8");
|
|
1069
|
+
}
|
|
1070
|
+
catch (error) {
|
|
1071
|
+
fail("INVALID_SYNTAX", `Cannot read workflow script file ${scriptPath}: ${errorText(error)}`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const rawName = params.name;
|
|
1075
|
+
const explicitName = typeof rawName === "string" ? rawName.trim() : "";
|
|
1076
|
+
if (!explicitName)
|
|
1077
|
+
fail("INVALID_METADATA", "Workflow name must be non-empty");
|
|
1078
|
+
const script = typeof params.script === "string" && params.script.trim() ? params.script : fileScript ?? "";
|
|
1079
|
+
if (!script)
|
|
1080
|
+
fail("INVALID_SYNTAX", "Provide script or scriptPath");
|
|
1081
|
+
const metadata = validateWorkflowMetadata({ name: explicitName, ...(typeof params.description === "string" ? { description: params.description } : {}) });
|
|
1082
|
+
const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false, registry && typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : registry && typeof registry.roleDirectories === "function" ? registry.roleDirectories() : undefined);
|
|
1083
|
+
const projectAgentDefinitions = context.projectTrusted ? loadProjectAgentDefinitions(context.cwd) : {};
|
|
1084
|
+
const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
|
|
1085
|
+
const aliases = context.modelAliases ?? {};
|
|
1086
|
+
const knownModels = context.knownModels ?? context.availableModels;
|
|
1087
|
+
const checked = preflight(script, { models: context.availableModels, tools: context.rootTools, agentTypes: new Set(Object.keys(agentDefinitions)), modelAliases: aliases, knownModels, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) }, [], metadata);
|
|
1088
|
+
const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
|
|
1089
|
+
validateRolePolicies(agentDefinitions, roleNames, context.availableModels, aliases, knownModels, context.settingsPath);
|
|
1090
|
+
if (registry?.validateExtensionSettings) {
|
|
1091
|
+
const validate = registry.validateExtensionSettings.bind(registry);
|
|
1092
|
+
const validatorContext = (source, role) => ({ source, cwd: context.cwd, projectTrusted: context.projectTrusted, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}), ...(role === undefined ? {} : { role }) });
|
|
1093
|
+
validate(context.extensionSettings, validatorContext("effective"));
|
|
1094
|
+
for (const role of roleNames) {
|
|
1095
|
+
validate(mergeWorkflowExtensionSettings(context.extensionSettings, agentDefinitions[role]?.extensionSettings), validatorContext("role", role));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames };
|
|
1099
|
+
}
|
|
1100
|
+
export { createLaunchSnapshot, loadLaunchSnapshot } from "./utils.js";
|