pi-extensible-workflows 1.0.1 → 3.0.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.
Files changed (47) hide show
  1. package/README.md +9 -2
  2. package/dist/src/agent-execution.d.ts +13 -14
  3. package/dist/src/agent-execution.js +110 -197
  4. package/dist/src/budget.d.ts +38 -0
  5. package/dist/src/budget.js +160 -0
  6. package/dist/src/cli.d.ts +9 -0
  7. package/dist/src/cli.js +536 -6
  8. package/dist/src/doctor.d.ts +4 -4
  9. package/dist/src/doctor.js +9 -29
  10. package/dist/src/execution.d.ts +17 -0
  11. package/dist/src/execution.js +630 -0
  12. package/dist/src/herdr.d.ts +12 -0
  13. package/dist/src/herdr.js +74 -0
  14. package/dist/src/host.d.ts +62 -0
  15. package/dist/src/host.js +2696 -0
  16. package/dist/src/index.d.ts +11 -557
  17. package/dist/src/index.js +8 -3974
  18. package/dist/src/persistence.d.ts +32 -19
  19. package/dist/src/persistence.js +310 -70
  20. package/dist/src/registry.d.ts +31 -0
  21. package/dist/src/registry.js +169 -0
  22. package/dist/src/session-inspector.d.ts +1 -0
  23. package/dist/src/session-inspector.js +4 -1
  24. package/dist/src/types.d.ts +565 -0
  25. package/dist/src/types.js +27 -0
  26. package/dist/src/utils.d.ts +26 -0
  27. package/dist/src/utils.js +128 -0
  28. package/dist/src/validation.d.ts +24 -0
  29. package/dist/src/validation.js +740 -0
  30. package/dist/src/workflow-evals.js +2 -1
  31. package/package.json +4 -3
  32. package/skills/pi-extensible-workflows/SKILL.md +52 -67
  33. package/src/agent-execution.ts +84 -147
  34. package/src/budget.ts +75 -0
  35. package/src/cli.ts +427 -6
  36. package/src/doctor.ts +13 -32
  37. package/src/execution.ts +540 -0
  38. package/src/herdr.ts +73 -0
  39. package/src/host.ts +2265 -0
  40. package/src/index.ts +11 -3453
  41. package/src/persistence.ts +255 -47
  42. package/src/registry.ts +157 -0
  43. package/src/session-inspector.ts +5 -1
  44. package/src/types.ts +113 -0
  45. package/src/utils.ts +108 -0
  46. package/src/validation.ts +616 -0
  47. package/src/workflow-evals.ts +2 -1
@@ -0,0 +1,616 @@
1
+ import { atomicWriteFile } from "./persistence.js";
2
+ import { mkdirSync, readFileSync, readdirSync, realpathSync } 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 { Value } from "typebox/value";
9
+ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
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";
12
+ import type { WorkflowRegistryApi } from "./registry.js";
13
+ import { deepFreeze, errorText, fail, jsonValue, mergeAgentResourceExclusions, modelAliasName, modelCapability, object, parseThinking, positiveInteger, resolveModelReference, unknownModel, validateModelAliases } from "./utils.js";
14
+ import { WORKFLOW_CALL_KINDS } from "./types.js";
15
+
16
+ export const DEFAULT_SETTINGS: Readonly<WorkflowSettings> = Object.freeze({ concurrency: 8 });
17
+
18
+ export function validateCheckpoint(value: unknown): CheckpointInput {
19
+ 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)) fail("INVALID_METADATA", "checkpoint requires only name, prompt, and JSON context");
20
+ if (Buffer.byteLength(value.prompt) > 1024) fail("INVALID_METADATA", "checkpoint prompt exceeds 1024 UTF-8 bytes");
21
+ if (Buffer.byteLength(JSON.stringify(value.context)) > 4096) fail("INVALID_METADATA", "checkpoint context exceeds 4096 UTF-8 bytes");
22
+ return { name: value.name, prompt: value.prompt, context: value.context };
23
+ }
24
+
25
+ export function workflowSettingsPath(agentDir = getAgentDir()): string { return join(agentDir, ROLE_DIRECTORY, "settings.json"); }
26
+ export function workflowProjectSettingsPath(cwd: string): string { return join(cwd, ".pi", ROLE_DIRECTORY, "settings.json"); }
27
+ const EMPTY_AGENT_RESOURCE_EXCLUSIONS: AgentResourceExclusions = Object.freeze({ skills: [], extensions: [] });
28
+ function normalizedResourcePath(value: string, settingsPath: string): string {
29
+ let expanded = value === "~" ? homedir() : value.startsWith("~/") || value.startsWith("~\\") ? join(homedir(), value.slice(2)) : value;
30
+ if (expanded.startsWith("file://")) expanded = fileURLToPath(expanded);
31
+ const resolved = resolve(dirname(settingsPath), expanded);
32
+ try { return realpathSync(resolved); } catch { return resolved; }
33
+ }
34
+ function validateAgentResourceExclusions(value: unknown, settingsPath: string, errorCode: "INVALID_SETTINGS" | "INVALID_METADATA" = "INVALID_SETTINGS"): AgentResourceExclusions | undefined {
35
+ if (value === undefined) return undefined;
36
+ const base = `${settingsPath}.disabledAgentResources`;
37
+ if (!object(value)) fail(errorCode, `${base} must be an object`);
38
+ for (const key of Object.keys(value)) if (key !== "skills" && key !== "extensions") fail(errorCode, `${base}.${key} is not supported`);
39
+ const normalized: { skills: string[]; extensions: string[] } = { skills: [], extensions: [] };
40
+ for (const kind of ["skills", "extensions"] as const) {
41
+ const entries = value[kind];
42
+ if (entries === undefined) continue;
43
+ if (!Array.isArray(entries)) fail(errorCode, `${base}.${kind} must be an array`);
44
+ const seen = new Set<string>();
45
+ for (const [index, entry] of entries.entries()) {
46
+ if (typeof entry !== "string" || !entry.trim()) fail(errorCode, `${base}.${kind}[${String(index)}] must be a non-empty string`);
47
+ let selector = entry.trim();
48
+ if (kind === "extensions") {
49
+ try { selector = normalizedResourcePath(selector, settingsPath); } catch (error) { fail(errorCode, `${base}.${kind}[${String(index)}] must be a valid path: ${errorText(error)}`); }
50
+ }
51
+ if (!seen.has(selector)) { seen.add(selector); normalized[kind].push(selector); }
52
+ }
53
+ }
54
+ return Object.freeze({ skills: Object.freeze(normalized.skills), extensions: Object.freeze(normalized.extensions) });
55
+ }
56
+ export function loadSettings(path = workflowSettingsPath()): Readonly<WorkflowSettings> {
57
+ let parsed: unknown;
58
+ try { parsed = JSON.parse(readFileSync(path, "utf8")); }
59
+ catch (error) {
60
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return DEFAULT_SETTINGS;
61
+ fail("CONFIG_ERROR", `Invalid workflow settings JSON at ${path}: ${errorText(error)}`);
62
+ }
63
+ if (!object(parsed)) fail("INVALID_SETTINGS", `Workflow settings at ${path} must be an object`);
64
+ const allowed = new Set(["concurrency", "modelAliases", "disabledAgentResources"]);
65
+ const unknown = Object.keys(parsed).find((key) => !allowed.has(key));
66
+ 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`);
69
+ const modelAliases = parsed.modelAliases === undefined ? undefined : validateModelAliases(parsed.modelAliases, path);
70
+ const disabledAgentResources = validateAgentResourceExclusions(parsed.disabledAgentResources, path);
71
+ return Object.freeze({ concurrency, ...(modelAliases ? { modelAliases } : {}), ...(disabledAgentResources ? { disabledAgentResources } : {}) });
72
+ }
73
+ export function resolveAgentResourcePolicy(cwd: string, projectTrusted: boolean, globalSettingsPath = workflowSettingsPath()): AgentResourcePolicy {
74
+ const projectSettingsPath = workflowProjectSettingsPath(cwd);
75
+ const global = loadSettings(globalSettingsPath).disabledAgentResources ?? EMPTY_AGENT_RESOURCE_EXCLUSIONS;
76
+ const project = projectTrusted ? loadSettings(projectSettingsPath).disabledAgentResources ?? EMPTY_AGENT_RESOURCE_EXCLUSIONS : EMPTY_AGENT_RESOURCE_EXCLUSIONS;
77
+ const effective = mergeAgentResourceExclusions(global, project);
78
+ return { globalSettingsPath, projectSettingsPath, projectTrusted, global, project, effective, unmatchedSkills: [], unmatchedExtensions: [] };
79
+ }
80
+ export function saveModelAliases(path = workflowSettingsPath(), aliases: Readonly<Record<string, string>> = {}): void {
81
+ const normalized = validateModelAliases(aliases, path);
82
+ let parsed: Record<string, unknown> = {};
83
+ try {
84
+ loadSettings(path);
85
+ parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
86
+ } catch (error) {
87
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
88
+ }
89
+ mkdirSync(dirname(path), { recursive: true });
90
+ atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
91
+ }
92
+
93
+ export function parseRoleMarkdown(content: string, strict = false, rolePath?: string): AgentDefinition {
94
+ if (!strict) {
95
+ if (!content.startsWith("---\n")) return { prompt: content };
96
+ const end = content.indexOf("\n---", 4);
97
+ if (end < 0) return { prompt: content };
98
+ const meta: Record<string, string> = {};
99
+ for (const line of content.slice(4, end).split("\n")) {
100
+ const match = /^(model|thinking|tools|description)\s*:\s*(.+)$/.exec(line.trim());
101
+ if (match?.[1] && match[2]) meta[match[1]] = match[2].trim();
102
+ }
103
+ const tools = meta.tools ? meta.tools.replace(/^\[|\]$/g, "").split(",").map((tool) => tool.trim().replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "")).filter(Boolean) : undefined;
104
+ const thinking = meta.thinking?.replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "");
105
+ if (thinking && !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(thinking)) fail("INVALID_METADATA", `Invalid role thinking level: ${thinking}`);
106
+ const definition: AgentDefinition = { prompt: content.slice(end + 4).replace(/^\n/, "") };
107
+ if (meta.model) definition.model = meta.model.replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "");
108
+ if (meta.description) definition.description = meta.description.replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "");
109
+ if (thinking) definition.thinking = thinking as NonNullable<AgentDefinition["thinking"]>;
110
+ if (tools) definition.tools = tools;
111
+ return definition;
112
+ }
113
+ const normalized = content.replace(/\r\n?/g, "\n");
114
+ if (normalized.startsWith("---\n") && normalized.indexOf("\n---", 3) < 0) fail("INVALID_METADATA", "Role frontmatter is missing its closing delimiter");
115
+ let parsed: ReturnType<typeof parseFrontmatter>;
116
+ try { parsed = parseFrontmatter(content); }
117
+ catch (error) { fail("INVALID_METADATA", `Invalid role frontmatter: ${errorText(error)}`); }
118
+ if (!object(parsed.frontmatter)) fail("INVALID_METADATA", "Role frontmatter must be an object");
119
+ const { model, thinking, tools, description, disabledAgentResources } = parsed.frontmatter;
120
+ if (model !== undefined && (typeof model !== "string" || model.trim() === "")) fail("INVALID_METADATA", "Role model must be a non-empty string");
121
+ if (thinking !== undefined && (typeof thinking !== "string" || !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(thinking))) fail("INVALID_METADATA", `Invalid role thinking level: ${typeof thinking === "string" ? thinking : typeof thinking}`);
122
+ if (description !== undefined && (typeof description !== "string" || description.trim() === "" || description.length > 1024 || /[\r\n]/.test(description))) fail("INVALID_METADATA", "Role description must be a non-empty single-line string of at most 1024 characters");
123
+ if (tools !== undefined && (!Array.isArray(tools) || tools.some((tool) => typeof tool !== "string" || tool.trim() === ""))) fail("INVALID_METADATA", "Role tools must be an array of non-empty strings");
124
+ const normalizedResources = validateAgentResourceExclusions(disabledAgentResources, rolePath ?? "<role>", "INVALID_METADATA");
125
+ return { prompt: parsed.body, ...(typeof description === "string" ? { description: description.trim() } : {}), ...(typeof model === "string" ? { model: model.trim() } : {}), ...(typeof thinking === "string" ? { thinking: thinking as NonNullable<AgentDefinition["thinking"]> } : {}), ...(Array.isArray(tools) ? { tools: tools.map((tool) => (tool as string).trim()) } : {}), ...(normalizedResources ? { disabledAgentResources: normalizedResources } : {}) };
126
+ }
127
+
128
+ const ROLE_DIRECTORY = "pi-extensible-workflows";
129
+
130
+ export function workflowRoleDirectories(agentDir = getAgentDir()): readonly string[] {
131
+ return [join(agentDir, ROLE_DIRECTORY, "roles")];
132
+ }
133
+
134
+ function projectRoleDirectories(root: string): readonly string[] {
135
+ return [join(root, ROLE_DIRECTORY, "roles")];
136
+ }
137
+
138
+ function readAgentDefinitions(dir: string): Record<string, AgentDefinition> {
139
+ try {
140
+ return Object.fromEntries(readdirSync(dir, { withFileTypes: true })
141
+ .filter((entry) => entry.isFile() && extname(entry.name) === ".md")
142
+ .map((entry) => { const path = join(dir, entry.name); return [basename(entry.name, ".md"), parseRoleMarkdown(readFileSync(path, "utf8"), true, path)]; }));
143
+ } catch (error) {
144
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ function readRoleDefinitions(dirs: readonly string[]): Record<string, AgentDefinition> {
150
+ return Object.fromEntries(dirs.flatMap((dir) => Object.entries(readAgentDefinitions(dir))));
151
+ }
152
+
153
+ export function loadAgentDefinitions(cwd: string, agentDir = getAgentDir(), projectTrusted = true): Readonly<Record<string, AgentDefinition>> {
154
+ return deepFreeze({ ...readRoleDefinitions(workflowRoleDirectories(agentDir)), ...(projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(cwd, ".pi"))) : {}) });
155
+ }
156
+ 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
+ for (const role of roles) {
158
+ const definition = definitions[role];
159
+ if (!definition) continue;
160
+ if (definition.model !== undefined) {
161
+ const resolved = modelCapability(definition.model, aliases, knownModels, settingsPath);
162
+ if (!availableModels.has(resolved)) {
163
+ if (modelAliasName(definition.model, aliases)) unknownModel(definition.model, resolved, settingsPath);
164
+ fail("UNKNOWN_MODEL", `Unknown model for role ${role}: ${resolved}`);
165
+ }
166
+ }
167
+ const missingTool = (definition.tools ?? [...rootTools]).find((tool) => !rootTools.has(tool));
168
+ if (missingTool) fail("UNKNOWN_TOOL", `Unknown tool for role ${role}: ${missingTool}`);
169
+ }
170
+ }
171
+
172
+ function validateWorkflowMetadata(value: unknown): WorkflowMetadata {
173
+ if (!object(value) || typeof value.name !== "string" || value.name.trim() === "") fail("INVALID_METADATA", "Workflow metadata requires a non-empty name");
174
+ if (value.description !== undefined && (typeof value.description !== "string" || value.description.trim() === "")) fail("INVALID_METADATA", "Workflow description must be a non-empty string when provided");
175
+ if (Object.keys(value).some((key) => !["name", "description"].includes(key))) fail("INVALID_METADATA", "Unknown workflow metadata");
176
+ return Object.freeze({ name: value.name.trim(), ...(typeof value.description === "string" ? { description: value.description.trim() } : {}) });
177
+ }
178
+
179
+ function workflowBody(script: string): string {
180
+ if (typeof script !== "string" || script.trim() === "") fail("INVALID_SYNTAX", "Workflow script must be non-empty");
181
+ try {
182
+ const program = acorn.parse(script, { ecmaVersion: "latest", sourceType: "module", allowReturnOutsideFunction: true });
183
+ const first = program.body[0];
184
+ if (first?.type === "ExportNamedDeclaration" && first.declaration?.type === "VariableDeclaration") {
185
+ const declarator = first.declaration.declarations[0];
186
+ if (declarator?.id.type === "Identifier" && declarator.id.name === "meta") return script.slice(first.end).replace(/^\s*/, "");
187
+ }
188
+ return script;
189
+ } catch (error) { fail("INVALID_SYNTAX", `Invalid workflow syntax: ${errorText(error)}`); }
190
+ }
191
+
192
+ function parseWorkflow(script: string): acorn.Program {
193
+ const body = workflowBody(script);
194
+ try {
195
+ new Script(`(async()=>{${body}\n})`);
196
+ return acorn.parse(body, { ecmaVersion: "latest", sourceType: "module", allowReturnOutsideFunction: true });
197
+ } catch (error) { fail("INVALID_SYNTAX", `Invalid workflow syntax: ${errorText(error)}`); }
198
+ }
199
+
200
+ type WorkflowCall = acorn.CallExpression & { callee: acorn.Identifier };
201
+
202
+ function astNode(value: unknown): value is acorn.AnyNode {
203
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
204
+ }
205
+ function astChildren(node: acorn.AnyNode): acorn.AnyNode[] {
206
+ const children: acorn.AnyNode[] = [];
207
+ for (const value of Object.values(node) as unknown[]) {
208
+ if (Array.isArray(value)) {
209
+ for (const child of value) if (astNode(child)) children.push(child);
210
+ } else if (astNode(value)) children.push(value);
211
+ }
212
+ return children;
213
+ }
214
+ function workflowCallKind(node: acorn.AnyNode): WorkflowCallKind | undefined {
215
+ if (node.type !== "CallExpression" || node.callee.type !== "Identifier") return undefined;
216
+ const kind = node.callee.name as WorkflowCallKind;
217
+ return WORKFLOW_CALL_KINDS.includes(kind) ? kind : undefined;
218
+ }
219
+ function workflowCalls(program: acorn.Program): WorkflowCall[] {
220
+ const calls: WorkflowCall[] = [];
221
+ const visit = (node: acorn.AnyNode): void => {
222
+ if (workflowCallKind(node)) calls.push(node as WorkflowCall);
223
+ for (const child of astChildren(node)) visit(child);
224
+ };
225
+ visit(program);
226
+ return calls.sort((left, right) => left.start - right.start);
227
+ }
228
+
229
+ function workflowCallsWithStructure(program: acorn.Program): Array<{ call: WorkflowCall; execution: StaticWorkflowExecution; structure: readonly StaticWorkflowScope[] }> {
230
+ const calls: Array<{ call: WorkflowCall; execution: StaticWorkflowExecution; structure: readonly StaticWorkflowScope[] }> = [];
231
+ const visit = (node: acorn.AnyNode, context: StaticWorkflowContext): void => {
232
+ let current = context;
233
+ if (node.type === "Property" && current.structure.length) {
234
+ const scope = current.structure.at(-1);
235
+ const key = node.key.type === "Identifier" ? node.key.name : node.key.type === "Literal" ? String(node.key.value) : undefined;
236
+ if (scope?.key === null && key) current = { ...current, structure: [...current.structure.slice(0, -1), { ...scope, key }] };
237
+ }
238
+ const operation = workflowCallKind(node);
239
+ if (operation) {
240
+ const call = node as WorkflowCall;
241
+ const execution = operation === "parallel" ? "parallel" : operation === "pipeline" ? "sequential" : current.execution;
242
+ calls.push({ call, execution, structure: current.structure });
243
+ for (const [index, argument] of call.arguments.entries()) {
244
+ if (argument.type === "SpreadElement") continue;
245
+ const scopeKind = operation === "parallel" && index === 1 ? "parallel" : operation === "pipeline" && index === 2 ? "pipeline" : undefined;
246
+ visit(argument, scopeKind ? { execution, structure: [...current.structure, { kind: scopeKind, name: staticString(callArgument(call, 0)), key: null }] } : current);
247
+ }
248
+ return;
249
+ }
250
+ for (const child of astChildren(node)) visit(child, current);
251
+ };
252
+ visit(program, { execution: "sequential", structure: [] });
253
+ return calls.sort((left, right) => left.call.start - right.call.start);
254
+ }
255
+ function validateDirectPrimitiveReferences(program: acorn.AnyNode, name: string): void {
256
+ const visit = (node: acorn.AnyNode, parent?: acorn.AnyNode): void => {
257
+ if (node.type === "Identifier" && node.name === name) {
258
+ const directCall = parent?.type === "CallExpression" && parent.callee === node;
259
+ const propertyKey = parent?.type === "Property" && parent.key === node && !parent.computed && !parent.shorthand;
260
+ if (!directCall && !propertyKey) fail("INVALID_METADATA", `${name} calls must use a direct ${name}(...) call; aliases and indirect calls are unsupported`);
261
+ }
262
+ for (const child of astChildren(node)) visit(child, node);
263
+ };
264
+ visit(program);
265
+ }
266
+ function validateRemovedWorkflowPrimitives(program: acorn.AnyNode, code: WorkflowErrorCode): void {
267
+ const visit = (node: acorn.AnyNode): void => {
268
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "conversation") fail(code, "conversation() was removed; pass prior agent results explicitly");
269
+ for (const child of astChildren(node)) visit(child);
270
+ };
271
+ visit(program);
272
+ }
273
+ function hasIdentifier(node: acorn.AnyNode, name: string): boolean {
274
+ if (node.type === "Identifier" && node.name === name) return true;
275
+ return astChildren(node).some((child) => hasIdentifier(child, name));
276
+ }
277
+
278
+ type StaticWorkflowContext = { execution: StaticWorkflowExecution; structure: readonly StaticWorkflowScope[] };
279
+
280
+ const INTERNAL_AGENT_NAME = "__pi_extensible_workflows_agent";
281
+ const INTERNAL_WORKTREE_NAME = "__pi_extensible_workflows_withWorktree";
282
+ const INTERNAL_SHELL_NAME = "__pi_extensible_workflows_shell";
283
+
284
+ function callHasTrailingComma(source: string, call: WorkflowCall): boolean {
285
+ let previous: acorn.Token | undefined;
286
+ let current: acorn.Token | undefined;
287
+ for (const token of acorn.tokenizer(source.slice(call.start, call.end), { ecmaVersion: "latest", sourceType: "module" })) {
288
+ previous = current;
289
+ current = token;
290
+ }
291
+ return current?.type.label === ")" && previous?.type.label === ",";
292
+ }
293
+
294
+ export function instrumentWorkflow(script: string): string {
295
+ const body = workflowBody(script);
296
+ if (!body.trim()) return body;
297
+ const program = parseWorkflow(body);
298
+ if (hasIdentifier(program, INTERNAL_AGENT_NAME)) fail("INVALID_METADATA", `${INTERNAL_AGENT_NAME} is reserved for workflow agent instrumentation`);
299
+ if (hasIdentifier(program, INTERNAL_WORKTREE_NAME)) fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
300
+ if (hasIdentifier(program, INTERNAL_SHELL_NAME)) fail("INVALID_METADATA", `${INTERNAL_SHELL_NAME} is reserved for workflow shell instrumentation`);
301
+ validateRemovedWorkflowPrimitives(program, "INVALID_METADATA");
302
+ const calls = workflowCalls(program).filter((call) => ["agent", "withWorktree", "shell"].includes(call.callee.name));
303
+ const edits = calls.flatMap((call) => {
304
+ 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 };
305
+ if (call.callee.name === "withWorktree") return [replacement];
306
+ const callSite = `${String(call.start)}:${String(call.end)}`;
307
+ const hiddenArgument = call.arguments.length === 0 || callHasTrailingComma(body, call) ? "" : ", ";
308
+ return [replacement, { start: call.end - 1, end: call.end - 1, text: `${hiddenArgument}${JSON.stringify(callSite)}` }];
309
+ }).sort((left, right) => right.start - left.start);
310
+ let instrumented = body;
311
+ for (const edit of edits) instrumented = instrumented.slice(0, edit.start) + edit.text + instrumented.slice(edit.end);
312
+ return instrumented;
313
+ }
314
+
315
+ function literalString(node: acorn.AnyNode | undefined): string | undefined {
316
+ return node?.type === "Literal" && typeof node.value === "string" ? node.value : undefined;
317
+ }
318
+
319
+ function propertyNode(node: acorn.AnyNode | undefined, name: string): acorn.AnyNode | undefined {
320
+ if (node?.type !== "ObjectExpression") return undefined;
321
+ for (let index = node.properties.length - 1; index >= 0; index -= 1) {
322
+ const property = node.properties[index];
323
+ if (!property || property.type === "SpreadElement" || property.computed) return undefined;
324
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
325
+ if (key === name) return property.value;
326
+ }
327
+ return undefined;
328
+ }
329
+
330
+ function stableName(node: acorn.AnyNode | undefined): boolean | undefined {
331
+ if (!node) return false;
332
+ if (node.type !== "ObjectExpression") {
333
+ if (["Literal", "ArrayExpression", "ArrowFunctionExpression", "FunctionExpression", "ClassExpression", "TemplateLiteral", "UnaryExpression", "UpdateExpression", "BinaryExpression"].includes(node.type)) return false;
334
+ return undefined;
335
+ }
336
+ let result: boolean | undefined = false;
337
+ for (const property of node.properties) {
338
+ if (property.type === "SpreadElement" || property.computed) { result = undefined; continue; }
339
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
340
+ if (key !== "name") continue;
341
+ const value = literalString(property.value);
342
+ result = value === undefined ? property.value.type === "Literal" ? false : undefined : value.trim() !== "";
343
+ }
344
+ return result;
345
+ }
346
+
347
+
348
+
349
+ export function workflowPrompt(template: string, values: Readonly<Record<string, JsonValue>>): string {
350
+ if (typeof template !== "string") fail("INVALID_METADATA", "prompt() template must be a string");
351
+ if (!object(values) || Array.isArray(values) || !jsonValue(values)) fail("INVALID_METADATA", "prompt() values must be a plain JSON-compatible object");
352
+ const placeholders = [...template.matchAll(/{{|}}|{([A-Za-z_$][\w$]*)}/g)].flatMap((match) => match[1] === undefined ? [] : [match[1]]);
353
+ const used = new Set(placeholders);
354
+ const keys = Object.keys(values);
355
+ const missing = placeholders.find((key) => !Object.prototype.hasOwnProperty.call(values, key));
356
+ if (missing) fail("INVALID_METADATA", `Missing prompt value "${missing}"`);
357
+ const unused = keys.find((key) => !used.has(key));
358
+ if (unused !== undefined) fail("INVALID_METADATA", `Unused prompt value "${unused}"`);
359
+ return template.replace(/{{|}}|{([A-Za-z_$][\w$]*)}/g, (match, key: string | undefined) => match === "{{" ? "{" : match === "}}" ? "}" : typeof values[key as string] === "string" ? values[key as string] as string : JSON.stringify(values[key as string], null, 2));
360
+ }
361
+
362
+ export function validateSchema(schema: unknown, at = "schema"): asserts schema is JsonSchema {
363
+ if (!object(schema) || Object.getPrototypeOf(schema) !== Object.prototype || !jsonValue(schema)) fail("INVALID_SCHEMA", `${at} must be a plain JSON-compatible Schema object`);
364
+ 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) fail("INVALID_SCHEMA", `${at} has no JSON Schema shape`);
365
+ if (schema.required !== undefined && (!Array.isArray(schema.required) || schema.required.some((key) => typeof key !== "string"))) fail("INVALID_SCHEMA", `${at}.required must be an array of strings`);
366
+ if (schema.properties !== undefined && !object(schema.properties)) fail("INVALID_SCHEMA", `${at}.properties must be an object`);
367
+ }
368
+
369
+ const AGENT_OPTION_KEYS = new Set(["label", "model", "thinking", "tools", "role", "outputSchema", "retries", "timeoutMs"]);
370
+ function validateAgentOption(key: string, value: unknown, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): void {
371
+ switch (key) {
372
+ case "label":
373
+ if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent label must be a non-empty string");
374
+ break;
375
+ case "model":
376
+ if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent model must be a non-empty string");
377
+ if (aliases !== undefined) resolveModelReference(value, aliases, knownModels, settingsPath);
378
+ break;
379
+ case "thinking":
380
+ if (typeof value !== "string" || !parseThinking(value)) fail("INVALID_METADATA", "agent thinking must be off, minimal, low, medium, high, xhigh, or max");
381
+ break;
382
+ case "tools":
383
+ if (!Array.isArray(value) || value.some((tool) => typeof tool !== "string")) fail("INVALID_METADATA", "agent tools must be an array of strings");
384
+ break;
385
+ case "role":
386
+ if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent role must be a non-empty string");
387
+ break;
388
+ case "outputSchema":
389
+ validateSchema(value, "agent outputSchema");
390
+ break;
391
+ case "retries":
392
+ if (!Number.isInteger(value) || (value as number) < 0) fail("INVALID_METADATA", "agent retries must be a non-negative integer");
393
+ break;
394
+ case "timeoutMs":
395
+ if (value !== null && !positiveInteger(value)) fail("INVALID_METADATA", "agent timeoutMs must be null or a positive integer");
396
+ break;
397
+ }
398
+ }
399
+ export function validateAgentOptions(value: unknown): Readonly<Record<string, JsonValue>> {
400
+ if (!object(value) || !jsonValue(value)) fail("INVALID_METADATA", "agent options must be a JSON object");
401
+ for (const [key, option] of Object.entries(value)) if (AGENT_OPTION_KEYS.has(key)) validateAgentOption(key, option);
402
+ if (typeof value.role === "string" && ["model", "thinking", "tools"].some((key) => Object.prototype.hasOwnProperty.call(value, key))) fail("INVALID_METADATA", "Role agents must not specify model, thinking, or tools");
403
+ return value;
404
+ }
405
+ const SHELL_OPTION_KEYS = new Set(["timeoutMs", "env"]);
406
+ export function validateShellOptions(value: unknown): ShellOptions {
407
+ if (value === undefined) return {};
408
+ if (!object(value) || !jsonValue(value) || Object.keys(value).some((key) => !SHELL_OPTION_KEYS.has(key))) fail("INVALID_METADATA", "shell options must contain only timeoutMs and env");
409
+ if (value.timeoutMs !== undefined && !positiveInteger(value.timeoutMs)) fail("INVALID_METADATA", "shell timeoutMs must be a positive integer");
410
+ if (value.env !== undefined && (!object(value.env) || Object.values(value.env).some((entry) => typeof entry !== "string"))) fail("INVALID_METADATA", "shell env must be an object of strings");
411
+ return { ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }), ...(value.env === undefined ? {} : { env: value.env as Record<string, string> }) };
412
+ }
413
+ export function validateShellCommand(value: unknown): string {
414
+ if (typeof value !== "string") fail("INVALID_METADATA", "shell command must be a string");
415
+ return value;
416
+ }
417
+
418
+ type StaticValue = { known: true; value: unknown } | { known: false };
419
+
420
+ function staticValue(node: acorn.AnyNode | undefined): StaticValue {
421
+ if (!node) return { known: false };
422
+ if (node.type === "Literal") return { known: true, value: node.value };
423
+ if (node.type === "UnaryExpression" && (node.operator === "-" || node.operator === "+")) {
424
+ const argument = staticValue(node.argument);
425
+ return argument.known && typeof argument.value === "number" ? { known: true, value: node.operator === "-" ? -argument.value : argument.value } : { known: false };
426
+ }
427
+ if (node.type === "ArrayExpression") {
428
+ const values: unknown[] = [];
429
+ for (const element of node.elements) {
430
+ if (!element || element.type === "SpreadElement") return { known: false };
431
+ const value = staticValue(element);
432
+ if (!value.known) return { known: false };
433
+ values.push(value.value);
434
+ }
435
+ return { known: true, value: values };
436
+ }
437
+ if (node.type === "ObjectExpression") {
438
+ const value: Record<string, unknown> = {};
439
+ for (const property of node.properties) {
440
+ if (property.type === "SpreadElement" || property.computed) return { known: false };
441
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
442
+ const child = staticValue(property.value);
443
+ if (!key || !child.known) return { known: false };
444
+ value[key] = child.value;
445
+ }
446
+ return { known: true, value };
447
+ }
448
+ return { known: false };
449
+ }
450
+
451
+
452
+
453
+ function callArgument(call: WorkflowCall, index: number): acorn.AnyNode | undefined {
454
+ const argument = call.arguments[index];
455
+ return argument?.type === "SpreadElement" ? undefined : argument;
456
+ }
457
+
458
+ function staticString(node: acorn.AnyNode | undefined): string | null {
459
+ const value = staticValue(node);
460
+ return value.known && typeof value.value === "string" ? value.value : null;
461
+ }
462
+
463
+ export function inspectWorkflowScript(script: string): StaticWorkflowCall[] {
464
+ return workflowCallsWithStructure(parseWorkflow(script)).map(({ call, execution, structure }) => {
465
+ const kind = call.callee.name as StaticWorkflowCall["kind"];
466
+ const first = callArgument(call, 0);
467
+ const options = callArgument(call, 1);
468
+ const placement = { execution, structure };
469
+ if (kind === "agent") {
470
+ const retries = staticValue(propertyNode(options, "retries"));
471
+ const outputSchema = staticValue(propertyNode(options, "outputSchema"));
472
+ const optionKeys = options?.type === "ObjectExpression" ? options.properties.flatMap((property) => {
473
+ if (property.type === "SpreadElement" || property.computed) return [];
474
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
475
+ return key ? [key] : [];
476
+ }) : [];
477
+ const knownOptions = Object.fromEntries(optionKeys.flatMap((key) => { const value = staticValue(propertyNode(options, key)); return value.known && jsonValue(value.value) ? [[key, value.value]] : []; })) as Record<string, JsonValue>;
478
+ 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")) };
479
+ return { ...base, ...(retries.known && typeof retries.value === "number" ? { retries: retries.value } : {}), ...(outputSchema.known && object(outputSchema.value) ? { outputSchema: outputSchema.value as JsonSchema } : {}), ...(optionKeys.length ? { options: knownOptions, optionKeys } : {}) };
480
+ }
481
+ if (kind === "checkpoint") return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null, role: null };
482
+ if (kind === "shell") return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
483
+ return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
484
+ });
485
+ }
486
+
487
+ function validateStaticAgentOptions(node: acorn.AnyNode | undefined, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): void {
488
+ if (node?.type !== "ObjectExpression") return;
489
+ const options = staticValue(node);
490
+ if (options.known && object(options.value) && typeof options.value.role === "string" && ["model", "thinking", "tools"].some((key) => Object.prototype.hasOwnProperty.call(options.value as Record<string, unknown>, key))) fail("INVALID_METADATA", "Role agents must not specify model, thinking, or tools");
491
+ for (const key of AGENT_OPTION_KEYS) {
492
+ const value = staticValue(propertyNode(node, key));
493
+ if (value.known) validateAgentOption(key, value.value, aliases, knownModels, settingsPath);
494
+ }
495
+ }
496
+ function validateStaticShellOptions(call: WorkflowCall): void {
497
+ if (call.arguments.some((argument) => argument.type === "SpreadElement")) return;
498
+ if (call.arguments.length !== 1 && call.arguments.length !== 2) fail("INVALID_METADATA", "shell requires a command string and optional options");
499
+ const command = staticValue(callArgument(call, 0));
500
+ if (command.known) validateShellCommand(command.value);
501
+ const options = staticValue(callArgument(call, 1));
502
+ if (options.known) validateShellOptions(options.value);
503
+ }
504
+
505
+ function validateStaticWithWorktree(call: WorkflowCall, compatibility: boolean): void {
506
+ if (call.arguments.some((argument) => argument.type === "SpreadElement")) return;
507
+ if (call.arguments.length !== 2) fail(compatibility ? "RESUME_INCOMPATIBLE" : "INVALID_METADATA", "withWorktree requires a name and callback");
508
+ const callback = call.arguments[1];
509
+ if (staticValue(callback).known) fail("INVALID_METADATA", "withWorktree callback must be a function");
510
+ const name = staticValue(callArgument(call, 0));
511
+ if (name.known && (typeof name.value !== "string" || !name.value.trim())) fail("INVALID_METADATA", "withWorktree name must be a non-empty string");
512
+ }
513
+
514
+
515
+ function hasDynamicAgentRole(node: acorn.AnyNode | undefined): boolean {
516
+ if (!node) return false;
517
+ if (node.type !== "ObjectExpression") return true;
518
+ for (let index = node.properties.length - 1; index >= 0; index -= 1) {
519
+ const property = node.properties[index];
520
+ if (!property || property.type === "SpreadElement" || property.computed) return true;
521
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
522
+ if (key === "role") return literalString(property.value) === undefined;
523
+ }
524
+ return false;
525
+ }
526
+ export function preflight(script: string, capabilities: PreflightCapabilities, schemas: readonly unknown[] = [], metadata: WorkflowMetadata = { name: "workflow" }, compatibility = false): PreflightResult {
527
+ const checkedMetadata = validateWorkflowMetadata(metadata);
528
+ const program = parseWorkflow(script);
529
+ if (hasIdentifier(program, INTERNAL_AGENT_NAME)) fail("INVALID_METADATA", `${INTERNAL_AGENT_NAME} is reserved for workflow agent instrumentation`);
530
+ if (hasIdentifier(program, INTERNAL_WORKTREE_NAME)) fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
531
+ if (hasIdentifier(program, INTERNAL_SHELL_NAME)) fail("INVALID_METADATA", `${INTERNAL_SHELL_NAME} is reserved for workflow shell instrumentation`);
532
+ validateDirectPrimitiveReferences(program, "withWorktree");
533
+ validateRemovedWorkflowPrimitives(program, compatibility ? "RESUME_INCOMPATIBLE" : "INVALID_METADATA");
534
+ validateDirectPrimitiveReferences(program, "shell");
535
+ for (const [index, schema] of schemas.entries()) validateSchema(schema, `schema[${String(index)}]`);
536
+ const calls = workflowCalls(program);
537
+ const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase): phase is string => phase !== undefined);
538
+ for (const call of calls) {
539
+ const operation = call.callee.name;
540
+ if (operation === "agent") validateStaticAgentOptions(call.arguments[1], capabilities.modelAliases ?? {}, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath);
541
+ if (operation === "withWorktree") validateStaticWithWorktree(call, compatibility);
542
+ if (operation === "shell") validateStaticShellOptions(call);
543
+ if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement")) continue;
544
+ if (operation === "checkpoint" && stableName(call.arguments[0]) === false) fail("INVALID_METADATA", `${operation} requires a stable explicit name`);
545
+ if (operation === "parallel" && (call.arguments.length !== 2 || !literalString(call.arguments[0])?.trim() || call.arguments[1]?.type !== "ObjectExpression")) fail("INVALID_METADATA", "parallel requires an operation name string and tasks record");
546
+ if (operation === "pipeline" && (call.arguments.length !== 3 || !literalString(call.arguments[0])?.trim() || call.arguments[1]?.type !== "ObjectExpression" || call.arguments[2]?.type !== "ObjectExpression")) fail("INVALID_METADATA", "pipeline requires an operation name string, items record, and stages record");
547
+ }
548
+ const agentCalls = calls.filter((call) => call.callee.name === "agent");
549
+ const dynamicAgentRoles = agentCalls.some((call) => hasDynamicAgentRole(call.arguments[1]));
550
+ const staticSchemas = agentCalls.flatMap((call) => { const value = staticValue(propertyNode(call.arguments[1], "outputSchema")); return value.known ? [value.value] : []; });
551
+ for (const [index, schema] of staticSchemas.entries()) validateSchema(schema, `agent outputSchema[${String(index)}]`);
552
+ const checkedSchemas = [...schemas, ...staticSchemas];
553
+ const modelRefs = agentCalls.flatMap((call) => { const requested = literalString(propertyNode(call.arguments[1], "model")); return requested === undefined ? [] : [{ requested, resolved: modelCapability(requested, capabilities.modelAliases, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath) }]; });
554
+ const models = modelRefs.map(({ resolved }) => resolved);
555
+ const tools = agentCalls.flatMap((call) => {
556
+ const value = propertyNode(call.arguments[1], "tools");
557
+ return value?.type === "ArrayExpression" ? value.elements.flatMap((element) => { const tool = element && element.type !== "SpreadElement" ? literalString(element) : undefined; return tool === undefined ? [] : [tool]; }) : [];
558
+ });
559
+ const agentTypes = agentCalls.flatMap((call) => { const value = literalString(propertyNode(call.arguments[1], "role")); return value === undefined ? [] : [value]; });
560
+ const missingModel = capabilities.skipModelAvailability ? undefined : modelRefs.find(({ resolved }) => !capabilities.models.has(resolved));
561
+ if (missingModel) {
562
+ if (modelAliasName(missingModel.requested, capabilities.modelAliases ?? {})) unknownModel(missingModel.requested, missingModel.resolved, capabilities.settingsPath);
563
+ fail("UNKNOWN_MODEL", `Unknown model: ${missingModel.resolved}`);
564
+ }
565
+ const missingTool = tools.find((tool) => !capabilities.tools.has(tool));
566
+ if (missingTool) fail("UNKNOWN_TOOL", `Unknown tool: ${missingTool}`);
567
+ const missingType = agentTypes.find((type) => !capabilities.agentTypes.has(type));
568
+ if (missingType) fail("UNKNOWN_AGENT_TYPE", `Unknown agent type: ${missingType}`);
569
+ return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools, agentTypes }), schemas: deepFreeze(checkedSchemas) as readonly JsonSchema[], dynamicAgentRoles });
570
+ }
571
+
572
+
573
+
574
+ function functionLaunchScript(name: string): string { return `return await ${name}(args);`; }
575
+
576
+ export function validateWorkflowLaunch(params: WorkflowValidationParameters, context: WorkflowValidationContext, registry?: WorkflowRegistryApi): ValidatedWorkflowLaunch {
577
+ return validateWorkflowLaunchWithRegistry(params, context, registry);
578
+ }
579
+ export function validateWorkflowLaunchWithRegistry(params: WorkflowValidationParameters, context: WorkflowValidationContext, registry?: WorkflowRegistryApi): ValidatedWorkflowLaunch {
580
+ if (Object.prototype.hasOwnProperty.call(params, "maxAgentLaunches")) fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
581
+ if (params.script !== undefined && params.workflow !== undefined) fail("INVALID_METADATA", "Provide either script or workflow, not both");
582
+ const functionName = typeof params.workflow === "string" ? params.workflow : undefined;
583
+ if (functionName !== undefined && params.name !== undefined) fail("INVALID_METADATA", "Registered function launches do not accept name; workflow is the run name");
584
+ const workflowName = functionName ?? (typeof params.name === "string" ? params.name.trim() : "");
585
+ if (functionName === undefined && !workflowName) fail("INVALID_METADATA", "Inline workflow launches require a non-empty name");
586
+ const fn = functionName === undefined ? undefined : registry?.function(functionName);
587
+ if (functionName !== undefined && !registry) fail("MISSING_WORKFLOW", `Registered function is unavailable: ${functionName}`);
588
+ const args = params.args === undefined ? null : params.args;
589
+ if (functionName !== undefined && fn && (!object(args) || !jsonValue(args) || !Value.Check(fn.input, args))) fail("RESULT_INVALID", `Invalid input for ${functionName}`);
590
+ const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : "";
591
+ if (!script) fail("INVALID_SYNTAX", "Provide script or registered function");
592
+ 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);
594
+ const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
595
+ const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
596
+ const aliases = context.modelAliases ?? {};
597
+ const knownModels = context.knownModels ?? context.availableModels;
598
+ 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);
599
+ const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
600
+ validateRolePolicies(agentDefinitions, roleNames, context.availableModels, context.rootTools, aliases, knownModels, context.settingsPath);
601
+ return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, ...(functionName ? { functionName } : {}) };
602
+ }
603
+
604
+
605
+
606
+ export function launchScriptForSnapshot(snapshot: Readonly<LaunchSnapshot>, registry: WorkflowRegistryApi): string {
607
+ if (snapshot.launchKind === "function") {
608
+ if (!snapshot.functionName) fail("RESUME_INCOMPATIBLE", "Persisted registered function launch is missing its function name");
609
+ try { registry.function(snapshot.functionName); } catch (error) { if (error instanceof WorkflowError && error.code === "MISSING_WORKFLOW") throw new WorkflowError("RESUME_INCOMPATIBLE", `Persisted registered function is unavailable: ${snapshot.functionName}`); throw error; }
610
+ return functionLaunchScript(snapshot.functionName);
611
+ }
612
+ if (snapshot.launchKind === "inline") return snapshot.script;
613
+ fail("RESUME_INCOMPATIBLE", "This persisted run uses the removed registered-workflow format; launch it again as a registered function or inline script");
614
+ }
615
+
616
+ export { createLaunchSnapshot, loadLaunchSnapshot } from "./utils.js";
@@ -485,6 +485,7 @@ export async function replayWorkflowScript(script: string, args: JsonValue = nul
485
485
  return `fake:${prompt}`;
486
486
  } finally { active -= 1; }
487
487
  },
488
+ worktree: async () => ({ path: "/worktrees/eval", branch: "eval-branch" }),
488
489
  phase: (name) => { phases.push(name); },
489
490
  log: (message) => { logs.push(message); },
490
491
  }, signal);
@@ -593,7 +594,7 @@ function semanticJudgePrompt(evalCase: WorkflowEvalCase, calls: readonly Capture
593
594
  const roles = loadAgentDefinitions(cwd, join(home, ".pi", "agent"));
594
595
  const usedRoles = new Set(calls.flatMap(({ script }) => { try { return script ? inspectWorkflowScript(script).flatMap((call) => call.kind === "agent" && call.role ? [call.role] : []) : []; } catch { return []; } }));
595
596
  const roleText = [...usedRoles].map((role) => `${role}: ${roles[role]?.description ?? "no description"}`).join("\n") || "none";
596
- const docs = "agent(prompt, options) delegates; parallel(name, tasks) runs independent tasks concurrently; pipeline(name, items, stages) applies ordered stages; prompt(template, data) carries values into prompts. A role owns model/thinking/tools policy.";
597
+ const docs = "agent(prompt, options) delegates; shell(command, options) runs a deterministic host command and returns exitCode/stdout/stderr; parallel(name, tasks) runs independent tasks concurrently; pipeline(name, items, stages) applies ordered stages; prompt(template, data) carries values into prompts. A role owns model/thinking/tools policy.";
597
598
  return `Judge whether the captured workflow design satisfies each criterion. Do not execute it. Return only JSON: {"criteria":[{"id":"criterion id","pass":true,"evidence":"specific script evidence"}]}.\n\nOriginal request:\n${evalCase.prompt}\n\nCriteria:\n${JSON.stringify(evalCase.semanticCriteria ?? [])}\n\nDSL:\n${docs}\n\nRelevant roles:\n${roleText}\n\nCaptured workflow script(s):\n${calls.map((call, index) => `--- ${String(index)} ---\n${call.script ?? "<missing>"}`).join("\n")}`;
598
599
  }
599
600