pi-extensible-workflows 3.1.0 → 3.3.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 +11 -47
  2. package/dist/src/agent-execution.d.ts +4 -0
  3. package/dist/src/agent-execution.js +146 -60
  4. package/dist/src/bundles.d.ts +53 -0
  5. package/dist/src/bundles.js +457 -0
  6. package/dist/src/cli.d.ts +3 -1
  7. package/dist/src/cli.js +146 -21
  8. package/dist/src/doctor-cleanup.js +4 -2
  9. package/dist/src/eval-capture-extension.js +16 -1
  10. package/dist/src/execution.js +13 -4
  11. package/dist/src/host.d.ts +51 -8
  12. package/dist/src/host.js +1181 -487
  13. package/dist/src/index.d.ts +4 -2
  14. package/dist/src/index.js +3 -1
  15. package/dist/src/persistence.d.ts +40 -1
  16. package/dist/src/persistence.js +51 -0
  17. package/dist/src/session-inspector.d.ts +12 -2
  18. package/dist/src/session-inspector.js +36 -2
  19. package/dist/src/types.d.ts +19 -0
  20. package/dist/src/types.js +1 -0
  21. package/dist/src/validation.js +42 -2
  22. package/dist/src/workflow-artifacts.d.ts +13 -0
  23. package/dist/src/workflow-artifacts.js +39 -0
  24. package/dist/src/workflow-evals.d.ts +7 -1
  25. package/dist/src/workflow-evals.js +23 -3
  26. package/evals/cases/recovery-completed-worktree.yaml +17 -0
  27. package/evals/cases/recovery-failed-run.yaml +14 -0
  28. package/examples/workflow-extension-template/README.md +37 -0
  29. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  30. package/examples/workflow-extension-template/index.js +51 -0
  31. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  32. package/package.json +3 -2
  33. package/skills/pi-extensible-workflows/SKILL.md +21 -28
  34. package/src/agent-execution.ts +102 -30
  35. package/src/bundles.ts +471 -0
  36. package/src/cli.ts +118 -21
  37. package/src/doctor-cleanup.ts +3 -2
  38. package/src/eval-capture-extension.ts +15 -1
  39. package/src/execution.ts +13 -4
  40. package/src/host.ts +992 -447
  41. package/src/index.ts +4 -2
  42. package/src/persistence.ts +53 -1
  43. package/src/session-inspector.ts +36 -4
  44. package/src/types.ts +12 -5
  45. package/src/validation.ts +33 -2
  46. package/src/workflow-artifacts.ts +34 -0
  47. package/src/workflow-evals.ts +24 -3
package/src/index.ts CHANGED
@@ -5,9 +5,11 @@ export * from "./validation.js";
5
5
  export * from "./registry.js";
6
6
  export * from "./execution.js";
7
7
  export * from "./host.js";
8
+ export * from "./workflow-artifacts.js";
9
+ export * from "./bundles.js";
8
10
  export { default } from "./host.js";
9
- export { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
10
- export type { AwaitingCheckpoint, CompletedOperation, NativeSessionReference, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, WorktreeReference } from "./persistence.js";
11
+ export { acquireSessionLease, hasLiveSessionLease, listPersistedSessionIds, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
12
+ export type { AwaitingCheckpoint, CompletedOperation, NativeSessionReference, PendingWorkflowDecision, PersistedOwnershipNode, PersistedRun, RunSummary, RunSummaryAgent, RunSummaryArtifacts, WorktreeReference } from "./persistence.js";
11
13
  export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
12
14
  export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./agent-execution.js";
13
15
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
@@ -5,7 +5,7 @@ import { access, link, mkdir, open, readFile, readdir, rename, rm, stat, writeFi
5
5
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { promisify } from "node:util";
8
- import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./types.js";
8
+ import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowBudgetUsage, WorkflowRunEvent } from "./types.js";
9
9
  import type { OwnershipRecord } from "./agent-execution.js";
10
10
  import { WorkflowError } from "./types.js";
11
11
  import { loadLaunchSnapshot } from "./utils.js";
@@ -13,11 +13,24 @@ import { loadLaunchSnapshot } from "./utils.js";
13
13
  export interface NativeSessionReference { sessionId: string; sessionFile: string }
14
14
  export interface EffectiveSystemPrompt { sessionId: string; attempt: number; turn: number; sha256: string; prompt: string }
15
15
  export interface PersistedRun extends RunRecord { nativeSessions: readonly NativeSessionReference[] }
16
+ export interface RunSummaryAgent { id: string; name: string; label?: string; state: string; role?: string; attempts: number }
17
+ export interface RunSummaryArtifacts { runDirectory: string; statePath: string; journalPath: string; snapshotPath: string; workflowPath: string; resultPath: string; summaryPath: string }
18
+ export interface RunSummary { schemaVersion: 1; runId: string; sessionId: string; workflowName: string; state: RunRecord["state"]; createdAt: string; updatedAt: string; terminalAt?: string; usage: WorkflowBudgetUsage; agents: readonly RunSummaryAgent[]; error?: RunRecord["error"]; failedAt?: string; replayablePaths: readonly string[]; incompletePaths: readonly string[]; artifacts: RunSummaryArtifacts }
16
19
  export interface CompletedOperation { path: string; value: JsonValue }
17
20
  export interface AwaitingCheckpoint { path: string; name: string; prompt: string; context: JsonValue }
18
21
  export type PendingWorkflowDecision = BudgetApprovalRequest
19
22
  export type PersistedOwnershipNode = OwnershipRecord
20
23
  type Journal = { completed: Record<string, CompletedOperation>; awaiting?: Record<string, AwaitingCheckpoint>; decisions?: Record<string, PendingWorkflowDecision> };
24
+ const TERMINAL_SUMMARY_STATES = new Set(["completed", "failed", "stopped"]);
25
+ const EMPTY_USAGE: WorkflowBudgetUsage = { tokens: 0, costUsd: 0, durationMs: 0, agentLaunches: 0 };
26
+ function summaryArtifacts(directory: string): RunSummaryArtifacts { return { runDirectory: directory, statePath: join(directory, "state.json"), journalPath: join(directory, "journal.json"), snapshotPath: join(directory, "snapshot.json"), workflowPath: join(directory, "workflow.js"), resultPath: join(directory, "result.json"), summaryPath: join(directory, "summary.json") }; }
27
+ function summaryFromRun(run: PersistedRun, directory: string, journal: Journal, previous: Partial<RunSummary> | undefined, fallbackCreatedAt: string, now = new Date().toISOString()): RunSummary {
28
+ const createdAt = typeof previous?.createdAt === "string" ? previous.createdAt : fallbackCreatedAt;
29
+ const failedAt = run.failedAt ?? run.error?.failedAt;
30
+ const replayablePaths = [...new Set([...(run.retry?.completedPaths ?? []), ...Object.keys(journal.completed)])];
31
+ const incompletePaths = [...new Set([...(run.retry?.incompletePaths ?? []), ...(failedAt ? [failedAt] : [])])];
32
+ return { schemaVersion: 1, runId: run.id, sessionId: run.sessionId, workflowName: run.workflowName, state: run.state, createdAt, updatedAt: now, ...(previous?.terminalAt || TERMINAL_SUMMARY_STATES.has(run.state) ? { terminalAt: previous?.terminalAt ?? now } : {}), usage: { ...EMPTY_USAGE, ...(run.usage ?? {}) }, agents: run.agents.map(({ id, name, label, state, role, attempts }) => ({ id, name, ...(label ? { label } : {}), state, ...(role ? { role } : {}), attempts })), ...(run.error ? { error: run.error } : {}), ...(failedAt ? { failedAt } : {}), replayablePaths, incompletePaths, artifacts: summaryArtifacts(directory) };
33
+ }
21
34
  export interface WorktreeReference { owner: string; path: string; branch: string; cwd: string; base: string }
22
35
  export interface BorrowedWorktreeBinding { name: string; sourceRunId: string; owner: string }
23
36
 
@@ -41,6 +54,15 @@ export function projectSessionsDirectory(cwd: string, home = homedir()): string
41
54
  export function runsDirectory(cwd: string, sessionId: string, home = homedir()): string {
42
55
  return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
43
56
  }
57
+ export async function listPersistedSessionIds(cwd: string, home = homedir()): Promise<string[]> {
58
+ try {
59
+ const entries = await readdir(projectSessionsDirectory(cwd, home), { withFileTypes: true });
60
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map(({ name }) => name);
61
+ } catch (error) {
62
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
63
+ throw error;
64
+ }
65
+ }
44
66
 
45
67
  const SESSION_OWNER_FILE = "owner.json";
46
68
  const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
@@ -197,6 +219,7 @@ export class RunStore {
197
219
  private journalWrite: Promise<void> = Promise.resolve();
198
220
  // ponytail: serializes one RunStore instance; cross-process run sharing remains unsupported.
199
221
  private stateWrite: Promise<void> = Promise.resolve();
222
+ private summaryWrite: Promise<void> = Promise.resolve();
200
223
  private worktreeWrite: Promise<void> = Promise.resolve();
201
224
  private borrowedWorktreeWrite: Promise<void> = Promise.resolve();
202
225
  private snapshotWrite: Promise<void> = Promise.resolve();
@@ -222,12 +245,25 @@ export class RunStore {
222
245
  await atomicJson(join(temporary, "borrowed-worktrees.json"), []);
223
246
  await atomicJson(join(temporary, "state.json"), run);
224
247
  await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
248
+ await atomicJson(join(temporary, "summary.json"), summaryFromRun(run, this.directory, { completed: {} }, undefined, new Date().toISOString()));
225
249
  await rename(temporary, this.directory);
226
250
  } catch (error) {
227
251
  await rm(temporary, { recursive: true, force: true });
228
252
  throw error;
229
253
  }
230
254
  }
255
+ private async refreshSummary(): Promise<void> {
256
+ const write = this.summaryWrite.then(async () => {
257
+ const run = await json<PersistedRun>(join(this.directory, "state.json"));
258
+ const journal = await json<Journal>(join(this.directory, "journal.json"));
259
+ const previous = await json<Partial<RunSummary>>(join(this.directory, "summary.json")).catch(() => undefined);
260
+ const fallbackCreatedAt = await stat(join(this.directory, "state.json")).then((value) => new Date(value.mtimeMs).toISOString());
261
+ await atomicJson(join(this.directory, "summary.json"), summaryFromRun(run, this.directory, journal, previous, fallbackCreatedAt));
262
+ });
263
+ this.summaryWrite = write.catch(() => undefined);
264
+ await write;
265
+ }
266
+ private refreshSummaryBestEffort(): void { void this.refreshSummary().catch(() => undefined); }
231
267
 
232
268
  async isComplete(): Promise<boolean> {
233
269
  try { await Promise.all([access(join(this.directory, "snapshot.json")), access(join(this.directory, "journal.json")), access(join(this.directory, "ownership.json")), access(join(this.directory, "state.json"))]); return true; }
@@ -240,11 +276,25 @@ export class RunStore {
240
276
  if (resolve(run.cwd) !== this.cwd || run.sessionId !== this.sessionId || run.id !== this.runId) throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run belongs to another cwd or Pi session");
241
277
  return { run, snapshot: loadLaunchSnapshot(await json<LaunchSnapshot>(join(this.directory, "snapshot.json"))) };
242
278
  }
279
+ async loadSummary(): Promise<RunSummary> {
280
+ await this.stateWrite;
281
+ await this.journalWrite;
282
+ await this.summaryWrite;
283
+ const run = await json<PersistedRun>(join(this.directory, "state.json"));
284
+ const journal = await json<Journal>(join(this.directory, "journal.json"));
285
+ const previous = await json<RunSummary>(join(this.directory, "summary.json")).catch(() => undefined);
286
+ const [stateStat, journalStat] = await Promise.all([stat(join(this.directory, "state.json")), stat(join(this.directory, "journal.json"))]);
287
+ const fallbackCreatedAt = new Date(stateStat.mtimeMs).toISOString();
288
+ const previousUpdatedAt = previous?.updatedAt === undefined ? Number.NaN : Date.parse(previous.updatedAt);
289
+ const updatedAt = new Date(Math.max(stateStat.mtimeMs, journalStat.mtimeMs, Number.isNaN(previousUpdatedAt) ? 0 : previousUpdatedAt)).toISOString();
290
+ return summaryFromRun(run, this.directory, journal, previous, fallbackCreatedAt, updatedAt);
291
+ }
243
292
 
244
293
  async saveState(run: PersistedRun): Promise<void> {
245
294
  const write = this.stateWrite.then(async () => {
246
295
  if (resolve(run.cwd) !== this.cwd || run.sessionId !== this.sessionId || run.id !== this.runId) throw new WorkflowError("INTERNAL_ERROR", "Run identity does not match its session-scoped store");
247
296
  await atomicJson(join(this.directory, "state.json"), run);
297
+ this.refreshSummaryBestEffort();
248
298
  });
249
299
  this.stateWrite = write.catch(() => undefined);
250
300
  await write;
@@ -258,6 +308,7 @@ export class RunStore {
258
308
  result = await update(current);
259
309
  if (resolve(result.cwd) !== this.cwd || result.sessionId !== this.sessionId || result.id !== this.runId) throw new WorkflowError("INTERNAL_ERROR", "Run identity does not match its session-scoped store");
260
310
  await atomicJson(join(this.directory, "state.json"), result);
311
+ this.refreshSummaryBestEffort();
261
312
  });
262
313
  this.stateWrite = write.catch(() => undefined);
263
314
  await write;
@@ -308,6 +359,7 @@ export class RunStore {
308
359
  journal.awaiting ??= {};
309
360
  result = await update(journal);
310
361
  await atomicJson(journalPath, journal);
362
+ this.refreshSummaryBestEffort();
311
363
  });
312
364
  this.journalWrite = write.catch(() => undefined);
313
365
  await write;
@@ -5,7 +5,7 @@ import { createInterface } from "node:readline/promises";
5
5
  import { stdin, stdout } from "node:process";
6
6
  import { highlightCode, initTheme, SessionManager, truncateToVisualLines, type SessionEntry, type SessionInfo } from "@earendil-works/pi-coding-agent";
7
7
  import { formatBudgetStatus, inspectWorkflowScript, type AgentSetupSummary, type ModelSpec, type StaticWorkflowCall } from "./index.js";
8
- import { listRunIds, RunStore, type PersistedRun } from "./persistence.js";
8
+ import { listPersistedSessionIds, listRunIds, RunStore, type PersistedRun, type RunSummary } from "./persistence.js";
9
9
 
10
10
  export interface ModelUsage { model: string; cost: number }
11
11
  export interface AttemptReport { attempt: number; prompt: string; model: string; thinking?: ModelSpec["thinking"]; cost: number; models: readonly ModelUsage[]; error?: string; setup?: AgentSetupSummary }
@@ -13,6 +13,8 @@ export interface AgentReport { name: string; label?: string; state: string; role
13
13
  export interface WorkflowReport { name: string; description?: string; status: string; runId?: string; script?: string; calls: readonly StaticWorkflowCall[]; parseError?: string; cost: number; models: readonly ModelUsage[]; agents: readonly AgentReport[]; budget?: PersistedRun["budget"]; budgetVersion?: number; usage?: PersistedRun["usage"]; budgetEvents?: PersistedRun["budgetEvents"]; events?: readonly { type: string; message: string }[] }
14
14
  export interface SessionReport { id: string; cwd: string; path: string; cost: number; models: readonly ModelUsage[]; workflows: readonly WorkflowReport[]; totalCost: number; totalModels: readonly ModelUsage[] }
15
15
  export interface InspectorViewState { view: "list" | "detail" | "script"; selected: number; scroll: number }
16
+ export type InspectMode = "tui" | "json" | "summary";
17
+ export interface PersistedSessionSummary { schemaVersion: 1; cwd: string; sessionId: string; runs: readonly RunSummary[] }
16
18
 
17
19
  type TranscriptSummary = { prompt?: string; cost: number; models: readonly ModelUsage[]; model?: string; thinking?: ModelSpec["thinking"] };
18
20
  type ToolResult = { toolCallId: string; isError: boolean; content: unknown; details?: unknown };
@@ -334,7 +336,26 @@ export function renderInspector(report: SessionReport, state: InspectorViewState
334
336
  const scroll = Math.max(0, Math.min(state.scroll, Math.max(0, fitted.length - room)));
335
337
  return [...header, ...fitted.slice(scroll, scroll + room), ...footer].slice(0, height);
336
338
  }
337
-
339
+ export async function loadPersistedSessionSummary(cwd: string, sessionId: string, home = homedir(), failedOnly = false): Promise<PersistedSessionSummary> {
340
+ const runs: RunSummary[] = [];
341
+ for (const runId of (await listRunIds(cwd, sessionId, home)).sort()) {
342
+ try {
343
+ const summary = await new RunStore(cwd, sessionId, runId, home).loadSummary();
344
+ if (!failedOnly || summary.state === "failed") runs.push(summary);
345
+ } catch { /* Ignore corrupt or concurrently removed runs. */ }
346
+ }
347
+ return { schemaVersion: 1, cwd, sessionId, runs };
348
+ }
349
+ export async function loadPersistedSummaries(cwd: string, sessionId: string | undefined, home = homedir(), failedOnly = false): Promise<readonly PersistedSessionSummary[]> {
350
+ const sessionIds = sessionId ? [sessionId] : (await listPersistedSessionIds(cwd, home)).sort();
351
+ const sessions = await Promise.all(sessionIds.map((id) => loadPersistedSessionSummary(cwd, id, home, failedOnly)));
352
+ return sessionId || !failedOnly ? sessions : sessions.filter((session) => session.runs.length > 0);
353
+ }
354
+ export function formatPersistedRunSummary(summary: RunSummary, sessionId = summary.sessionId): string {
355
+ const counts = summary.agents.reduce<Record<string, number>>((result, agent) => { result[agent.state] = (result[agent.state] ?? 0) + 1; return result; }, {});
356
+ const status = Object.entries(counts).map(([state, count]) => `${state}=${String(count)}`).join(", ") || "agents=0";
357
+ return `${sessionId} ${summary.runId} ${summary.workflowName} ${summary.state} ${status} updated=${summary.updatedAt}`;
358
+ }
338
359
  function nextState(current: InspectorViewState, key: string, workflowCount: number): InspectorViewState {
339
360
  if (current.view === "list") {
340
361
  if (key === "up") return { ...current, selected: Math.max(0, current.selected - 1) };
@@ -385,8 +406,19 @@ async function askSessionId(): Promise<string> {
385
406
  export async function resolveSession(query: string, sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR): Promise<SessionInfo> {
386
407
  return matchSession(query, await SessionManager.listAll(sessionDir));
387
408
  }
388
-
389
- export async function runSessionInspector(sessionId?: string): Promise<void> {
409
+ export async function runSessionInspector(sessionId?: string, mode: InspectMode = "tui", cwd = process.cwd(), home = homedir(), write: (text: string) => void = (text) => { stdout.write(text); }, failedOnly = false): Promise<void> {
410
+ if (mode !== "tui") {
411
+ const sessions = await loadPersistedSummaries(cwd, sessionId?.trim() || undefined, home, failedOnly);
412
+ if (mode === "json") {
413
+ const value = sessionId?.trim() ? sessions[0] : undefined;
414
+ write(`${JSON.stringify(value ?? { schemaVersion: 1, cwd, sessions })}\n`);
415
+ return;
416
+ }
417
+ const emptyLabel = failedOnly ? "no failed runs" : "no persisted runs";
418
+ const lines = sessions.flatMap((session) => session.runs.length ? session.runs.map((run) => formatPersistedRunSummary(run, session.sessionId)) : [`${session.sessionId} (${emptyLabel})`]);
419
+ write(`${lines.length ? lines.join("\n") : failedOnly ? "No failed workflow runs." : "No persisted workflow runs."}\n`);
420
+ return;
421
+ }
390
422
  const query = sessionId?.trim() || await askSessionId();
391
423
  if (!query) throw new Error("Session ID is required.");
392
424
  const session = await resolveSession(query);
package/src/types.ts CHANGED
@@ -45,7 +45,7 @@ export interface WorkflowBudgetUsage { tokens: number; costUsd: number; duration
45
45
  export type BudgetEventType = "soft_crossed" | "hard_overrun" | "hard_exhausted" | "adjustment_requested" | "adjustment_approved" | "adjustment_rejected";
46
46
  export interface BudgetEvent { type: BudgetEventType; budgetVersion: number; dimensions: readonly BudgetDimension[]; usage: WorkflowBudgetUsage; limits: WorkflowBudget; at: number; proposalId?: string; previous?: WorkflowBudget; proposed?: WorkflowBudget }
47
47
  export interface BudgetApprovalRequest { kind: "budget"; proposalId: string; runId: string; consumed: WorkflowBudgetUsage; previous: WorkflowBudget; proposed: WorkflowBudget; budgetVersion: number }
48
- export interface WorkflowErrorShape { code: WorkflowErrorCode; message: string }
48
+ export interface WorkflowErrorShape { code: WorkflowErrorCode; message: string; failedAt?: string }
49
49
  export interface WorkflowEventBase { runId: string; sessionId: string; workflowName: string; cwd: string; runDirectory: string; timestamp: number }
50
50
  export type WorkflowRunStartedEvent = WorkflowEventBase;
51
51
  export type WorkflowRunResumedEvent = WorkflowEventBase;
@@ -68,19 +68,24 @@ export interface WorkflowSettingsResolution { globalSettingsPath: string; projec
68
68
  export interface AgentResourceExclusions { skills: readonly string[]; extensions: readonly string[] }
69
69
  export interface AgentResourcePolicy { globalSettingsPath: string; projectSettingsPath: string; projectTrusted: boolean; global: AgentResourceExclusions; project: AgentResourceExclusions; effective: AgentResourceExclusions; unmatchedSkills: string[]; unmatchedExtensions: string[]; excludedSkills?: string[]; excludedExtensions?: string[] }
70
70
  export interface AgentActivity { kind: "reasoning" | "tool" | "text"; text: string }
71
+ export const WORKFLOW_AGENT_STALL_THRESHOLD_MS = 10 * 60 * 1000;
71
72
  export interface AgentAccounting { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }
72
73
  export interface AgentSetupSummary { hookNames: readonly string[]; model: ModelSpec; tools: readonly string[]; cwd: string; disabledAgentResources?: { skills: readonly string[]; extensions: readonly string[]; excludedSkills?: readonly string[]; excludedExtensions?: readonly string[]; unmatchedSkills: readonly string[]; unmatchedExtensions: readonly string[] } }
73
74
  export interface AgentAttemptSummary { attempt: number; sessionId: string; sessionFile: string; error?: { code: string; message: string }; accounting: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; setup?: AgentSetupSummary }
74
75
  export interface WorkflowWorktreeCreatedEvent extends WorkflowEventBase { owner: string; branch: string; path: string; base: string }
75
76
  export interface WorkflowWorktreeReference { readonly path: string; readonly branch: string }
76
- export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined }
77
+ export interface AgentRecord { id: string; name: string; label?: string; path: string; state: AgentState; parentId?: string; structuralPath?: readonly string[]; resultPath?: string; parentBreadcrumb?: string; worktreeOwner?: string; role?: string; requestedModel?: string; model: ModelSpec; tools: readonly string[]; attempts: number; attemptDetails?: readonly AgentAttemptSummary[]; accounting?: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number }; toolCalls?: readonly { id: string; name: string; state: "running" | "completed" | "failed" }[]; activity?: AgentActivity | undefined; lastEventAt?: number }
78
+ export type WorkflowDeliveryMode = "foreground" | "background";
79
+ export type WorkflowDeliveryStatus = "attached" | "pending" | "delivered";
80
+ export interface WorkflowRunDelivery { mode: WorkflowDeliveryMode; state: WorkflowDeliveryStatus; toolCallId?: string }
77
81
  export interface WorkflowRunEvent { type: string; message: string }
78
82
  export interface WorkflowRetryProvenance { sourceRunId: string; lineageRootRunId: string; completedPaths: readonly string[]; incompletePaths: readonly string[]; namedWorktrees: readonly string[] }
79
83
  export interface WorkflowPhaseRecord { phase: string; afterAgent: number }
80
- export interface RunRecord { id: string; workflowName: string; cwd: string; sessionId: string; state: RunState; parentRunId?: string; retry?: WorkflowRetryProvenance; phase?: string; phaseHistory?: readonly WorkflowPhaseRecord[]; agents: readonly AgentRecord[]; error?: WorkflowErrorShape; budget?: WorkflowBudget; budgetVersion?: number; usage?: WorkflowBudgetUsage; budgetEvents?: readonly BudgetEvent[]; events?: readonly WorkflowRunEvent[] }
84
+ export interface RunRecord { id: string; workflowName: string; cwd: string; sessionId: string; state: RunState; parentRunId?: string; retry?: WorkflowRetryProvenance; phase?: string; phaseHistory?: readonly WorkflowPhaseRecord[]; agents: readonly AgentRecord[]; activeShells?: number; error?: WorkflowErrorShape; failedAt?: string; budget?: WorkflowBudget; budgetVersion?: number; usage?: WorkflowBudgetUsage; budgetEvents?: readonly BudgetEvent[]; events?: readonly WorkflowRunEvent[]; delivery?: WorkflowRunDelivery }
81
85
  export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
82
- export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: NonNullable<ModelSpec["thinking"]>; tools?: readonly string[]; disabledAgentResources?: AgentResourceExclusions }
83
- export interface LaunchSnapshot { identityVersion?: number; launchKind?: "inline" | "function"; functionName?: string; script: string; args: JsonValue; metadata: WorkflowMetadata; settings: WorkflowSettings; settingsSources?: WorkflowSettingsSources; budget?: WorkflowBudget; settingsPath?: string; modelAliases?: Readonly<Record<string, string>>; phases?: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[]; roles?: Readonly<Record<string, AgentDefinition>>; projectRoles?: readonly string[]; schemas: readonly JsonSchema[] }
86
+ export type WorkflowLaunchMode = "foreground" | "background";
87
+ export interface AgentDefinition { prompt?: string; description?: string; model?: string; thinking?: NonNullable<ModelSpec["thinking"]>; tools?: readonly string[]; overrideSystemPrompt?: boolean; disabledAgentResources?: AgentResourceExclusions }
88
+ export interface LaunchSnapshot { identityVersion?: number; launchKind?: "inline" | "function"; functionName?: string; launchMode?: WorkflowLaunchMode; script: string; args: JsonValue; metadata: WorkflowMetadata; settings: WorkflowSettings; settingsSources?: WorkflowSettingsSources; budget?: WorkflowBudget; settingsPath?: string; modelAliases?: Readonly<Record<string, string>>; phases?: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[]; roles?: Readonly<Record<string, AgentDefinition>>; projectRoles?: readonly string[]; schemas: readonly JsonSchema[] }
84
89
  export interface PreflightCapabilities { models: ReadonlySet<string>; tools: ReadonlySet<string>; agentTypes: ReadonlySet<string>; modelAliases?: Readonly<Record<string, string>>; knownModels?: ReadonlySet<string>; settingsPath?: string; skipModelAvailability?: boolean }
85
90
  export interface PreflightResult { metadata: WorkflowMetadata; referenced: { phases: readonly string[]; models: readonly string[]; tools: readonly string[]; agentTypes: readonly string[] }; schemas: readonly JsonSchema[]; dynamicAgentRoles: boolean }
86
91
  export interface WorkflowOrchestrationContext { agent: (prompt: string, options?: Readonly<AgentOptions>) => Promise<JsonValue>; shell: (command: string, options?: ShellOptions) => Promise<ShellResult>; prompt: (template: string, values: Readonly<Record<string, JsonValue>>) => string; parallel: (...args: readonly unknown[]) => Promise<JsonValue>; pipeline: (...args: readonly unknown[]) => Promise<JsonValue>; withWorktree: (name: string, callback: WorkflowWorktreeCallback) => Promise<JsonValue>; checkpoint: (...args: readonly unknown[]) => Promise<boolean>; phase: (name: string) => void; log: (message: string) => void }
@@ -117,8 +122,10 @@ export interface SessionInput {
117
122
  agentDir?: string;
118
123
  customTools?: SessionCustomTools;
119
124
  resultTool?: ToolDefinition;
125
+ systemPrompt?: string;
120
126
  systemPromptAppend?: string;
121
127
  extensionFactories?: InlineExtension[];
128
+ additionalSkillPaths?: readonly string[];
122
129
  resourcePolicy?: AgentResourcePolicy;
123
130
  options?: AgentOptions;
124
131
  }
package/src/validation.ts CHANGED
@@ -141,7 +141,7 @@ export function parseRoleMarkdown(content: string, strict = false, rolePath?: st
141
141
  if (end < 0) return { prompt: content };
142
142
  const meta: Record<string, string> = {};
143
143
  for (const line of content.slice(4, end).split("\n")) {
144
- const match = /^(model|thinking|tools|description)\s*:\s*(.+)$/.exec(line.trim());
144
+ const match = /^(model|thinking|tools|description|overrideSystemPrompt|override_system_prompt|is_system_prompt)\s*:\s*(.+)$/.exec(line.trim());
145
145
  if (match?.[1] && match[2]) meta[match[1]] = match[2].trim();
146
146
  }
147
147
  const tools = meta.tools ? meta.tools.replace(/^\[|\]$/g, "").split(",").map((tool) => tool.trim().replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "")).filter(Boolean) : undefined;
@@ -152,6 +152,8 @@ export function parseRoleMarkdown(content: string, strict = false, rolePath?: st
152
152
  if (meta.description) definition.description = meta.description.replace(/^[']|[']$/g, "").replace(/^["]|["]$/g, "");
153
153
  if (thinking) definition.thinking = thinking as NonNullable<AgentDefinition["thinking"]>;
154
154
  if (tools) definition.tools = tools;
155
+ const overrideSystemPrompt = meta.overrideSystemPrompt ?? meta.override_system_prompt ?? meta.is_system_prompt;
156
+ if (overrideSystemPrompt) definition.overrideSystemPrompt = overrideSystemPrompt === "true";
155
157
  return definition;
156
158
  }
157
159
  const normalized = content.replace(/\r\n?/g, "\n");
@@ -161,12 +163,14 @@ export function parseRoleMarkdown(content: string, strict = false, rolePath?: st
161
163
  catch (error) { fail("INVALID_METADATA", `Invalid role frontmatter: ${errorText(error)}`); }
162
164
  if (!object(parsed.frontmatter)) fail("INVALID_METADATA", "Role frontmatter must be an object");
163
165
  const { model, thinking, tools, description, disabledAgentResources } = parsed.frontmatter;
166
+ const overrideSystemPrompt = parsed.frontmatter.overrideSystemPrompt ?? parsed.frontmatter.override_system_prompt ?? parsed.frontmatter.is_system_prompt;
164
167
  if (model !== undefined && (typeof model !== "string" || model.trim() === "")) fail("INVALID_METADATA", "Role model must be a non-empty string");
165
168
  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}`);
166
169
  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");
170
+ if (overrideSystemPrompt !== undefined && typeof overrideSystemPrompt !== "boolean") fail("INVALID_METADATA", "Role overrideSystemPrompt must be a boolean");
167
171
  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");
168
172
  const normalizedResources = validateAgentResourceExclusions(disabledAgentResources, rolePath ?? "<role>", "INVALID_METADATA");
169
- 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 } : {}) };
173
+ 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()) } : {}), ...(typeof overrideSystemPrompt === "boolean" ? { overrideSystemPrompt } : {}), ...(normalizedResources ? { disabledAgentResources: normalizedResources } : {}) };
170
174
  }
171
175
 
172
176
  const ROLE_DIRECTORY = "pi-extensible-workflows";
@@ -324,6 +328,32 @@ function workflowCallsWithStructure(program: acorn.Program): Array<{ call: Workf
324
328
  visit(program, { execution: "sequential", structure: [] });
325
329
  return calls.sort((left, right) => left.call.start - right.call.start);
326
330
  }
331
+ function memberCall(node: acorn.AnyNode | undefined, objectName: string, propertyName: string): boolean {
332
+ 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") return false;
333
+ return node.callee.property.name === propertyName;
334
+ }
335
+ function mapCallback(node: acorn.AnyNode): acorn.AnyNode | undefined {
336
+ if (!memberCall(node, "Promise", "all") && !memberCall(node, "Promise", "allSettled")) return undefined;
337
+ if (node.type !== "CallExpression") return undefined;
338
+ const source = node.arguments[0];
339
+ if (source?.type !== "CallExpression" || source.callee.type !== "MemberExpression" || source.callee.computed || source.callee.property.type !== "Identifier" || !["map", "flatMap"].includes(source.callee.property.name)) return undefined;
340
+ const callback = source.arguments[0];
341
+ return callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression" ? callback : undefined;
342
+ }
343
+ function hasUnscopedAgent(node: acorn.AnyNode, scoped = false): boolean {
344
+ const operation = workflowCallKind(node);
345
+ if (operation === "agent") return !scoped;
346
+ const nestedScope = scoped || operation === "parallel" || operation === "pipeline";
347
+ return astChildren(node).some((child) => hasUnscopedAgent(child, nestedScope));
348
+ }
349
+ function validateObviousConcurrentAgentCalls(program: acorn.Program): void {
350
+ const visit = (node: acorn.AnyNode): void => {
351
+ const callback = mapCallback(node);
352
+ if (callback && hasUnscopedAgent(callback)) fail("INVALID_METADATA", "Promise.all/map agent fan-out cannot prove stable call-site identity; use parallel(...) or pipeline(...)");
353
+ for (const child of astChildren(node)) visit(child);
354
+ };
355
+ visit(program);
356
+ }
327
357
  function validateDirectPrimitiveReferences(program: acorn.AnyNode, name: string): void {
328
358
  const visit = (node: acorn.AnyNode, parent?: acorn.AnyNode): void => {
329
359
  if (node.type === "Identifier" && node.name === name) {
@@ -606,6 +636,7 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
606
636
  validateDirectPrimitiveReferences(program, "shell");
607
637
  for (const [index, schema] of schemas.entries()) validateSchema(schema, `schema[${String(index)}]`);
608
638
  const calls = workflowCalls(program);
639
+ validateObviousConcurrentAgentCalls(program);
609
640
  const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase): phase is string => phase !== undefined);
610
641
  for (const call of calls) {
611
642
  const operation = call.callee.name;
@@ -0,0 +1,34 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { JsonValue } from "./types.js";
6
+
7
+ export interface WorkflowArtifact { extension: ".js" | ".json" | ".md"; content: string }
8
+ export type WorkflowTui = { stop(): void; start(): void; requestRender(force?: boolean): void };
9
+
10
+ export function workflowScriptArtifact(script: string): WorkflowArtifact { return { extension: ".js", content: script }; }
11
+ export function workflowResultArtifact(value: JsonValue): WorkflowArtifact { return typeof value === "string" ? { extension: ".md", content: value } : { extension: ".json", content: `${JSON.stringify(value, null, 2)}\n` }; }
12
+
13
+ async function spawnWorkflowEditor(command: string, path: string): Promise<number | null> {
14
+ const [editor, ...editorArgs] = command.split(" ");
15
+ if (!editor) return null;
16
+ return new Promise((resolve) => {
17
+ try {
18
+ const child = spawn(editor, [...editorArgs, path], { stdio: "inherit", shell: process.platform === "win32" });
19
+ child.once("error", () => { resolve(null); });
20
+ child.once("close", (code) => { resolve(code); });
21
+ } catch { resolve(null); }
22
+ });
23
+ }
24
+
25
+ export async function openWorkflowArtifact(tui: WorkflowTui, command: string, artifact: WorkflowArtifact): Promise<number | null> {
26
+ const directory = await mkdtemp(join(tmpdir(), "pi-workflow-editor-"));
27
+ const path = join(directory, `artifact${artifact.extension}`);
28
+ try {
29
+ await writeFile(path, artifact.content, { encoding: "utf8", mode: 0o600 });
30
+ tui.stop();
31
+ try { return await spawnWorkflowEditor(command, path); }
32
+ finally { tui.start(); tui.requestRender(true); }
33
+ } finally { await rm(directory, { recursive: true, force: true }); }
34
+ }
@@ -28,6 +28,7 @@ export interface AgentOrderExpectation { role?: string; model?: string; promptIn
28
28
  export interface DataFlowExpectation { binding: string; toAgentIndex: number }
29
29
  export interface ParentAssistantBatch { index: number; parts: readonly JsonValue[]; tools: readonly string[]; usage?: ParentUsage }
30
30
  export interface ParentToolResult { toolCallId?: string; details?: JsonValue; isError?: boolean; text?: string }
31
+ export interface ParentToolCall { name: string; arguments: JsonValue }
31
32
  export interface ParentUsage { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens: number; cost: number; models: readonly { model: string; cost: number }[] }
32
33
  export interface ParentOracle { assistantBatches: readonly ParentAssistantBatch[]; workflowToolResults: readonly ParentToolResult[]; skillReads: readonly string[]; firstSignificantAction?: SignificantAction; firstTool?: string; firstBatchToolSequence: readonly string[]; toolsBeforeFirstWorkflow: readonly string[]; firstWorkflowBatchToolSequence: readonly string[]; parentToolSequence: readonly string[]; workflowCallCount: number; usage: ParentUsage }
33
34
  export interface CapturedWorkflowCall { batch: number; toolCallId?: string; arguments: JsonValue; script?: string }
@@ -91,7 +92,7 @@ export interface EvalCaseResult {
91
92
  }
92
93
 
93
94
  const CASE_PROCESS_GRACE_MS = 1_000;
94
- export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow"] as const);
95
+ export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow", "workflow_retry"] as const);
95
96
  const EVAL_MODEL_TOKEN = "$EVAL_MODEL";
96
97
  const semantic = (description: string): readonly SemanticCriterion[] => [{ id: "intent", description }];
97
98
  const JSON_RESULT_TYPES = ["null", "boolean", "number", "integer", "string", "array", "object"] as const;
@@ -216,6 +217,13 @@ export function extractParentOracle(entries: readonly unknown[]): ParentOracle {
216
217
  return { assistantBatches: batches, workflowToolResults, skillReads, ...(firstSignificantAction ? { firstSignificantAction } : {}), ...(parentToolSequence[0] ? { firstTool: parentToolSequence[0] } : {}), firstBatchToolSequence: firstBatch?.tools ?? [], toolsBeforeFirstWorkflow: firstWorkflowIndex < 0 ? parentToolSequence : parentToolSequence.slice(0, firstWorkflowIndex), firstWorkflowBatchToolSequence: firstWorkflowBatch?.tools ?? [], parentToolSequence, workflowCallCount: parentToolSequence.filter((name) => name === "workflow").length, usage: { ...totals, models: [...modelCosts].map(([model, cost]) => ({ model, cost })) } };
217
218
  }
218
219
 
220
+ export function extractParentToolCalls(oracle: ParentOracle): ParentToolCall[] {
221
+ return oracle.assistantBatches.flatMap(({ parts }) => parts.flatMap((part) => {
222
+ if (!isObject(part) || part.type !== "toolCall" || typeof part.name !== "string") return [];
223
+ return [{ name: part.name, arguments: isJson(part.arguments) ? part.arguments : null }];
224
+ }));
225
+ }
226
+
219
227
  export function extractParentOracleFile(path: string): ParentOracle {
220
228
  const entries = readFileSync(path, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line) as unknown);
221
229
  return extractParentOracle(entries);
@@ -280,6 +288,19 @@ export function evalExpectationErrors(oracle: ParentOracle, expectations: EvalEx
280
288
  return errors;
281
289
  }
282
290
 
291
+ const RECOVERY_SELECTIONS: Readonly<Record<string, { tool: string; arguments: JsonValue }>> = {
292
+ "recovery-failed-run": { tool: "workflow_retry", arguments: { runId: "failed-run-42" } },
293
+ "recovery-completed-worktree": { tool: "workflow", arguments: { name: "borrow-worktree", script: "return true;", parentRunId: "completed-run-42" } },
294
+ };
295
+
296
+ export function recoverySelectionErrors(evalCase: Pick<WorkflowEvalCase, "id">, oracle: ParentOracle): string[] {
297
+ const expected = RECOVERY_SELECTIONS[evalCase.id];
298
+ if (!expected) return [];
299
+ const actual = extractParentToolCalls(oracle);
300
+ if (actual.length === 1 && actual[0]?.name === expected.tool && equalJson(actual[0].arguments, expected.arguments)) return [];
301
+ return [`${evalCase.id} parent tool calls were ${JSON.stringify(actual)}; expected ${JSON.stringify([expected])}`];
302
+ }
303
+
283
304
  export function replayExpectationErrors(calls: readonly CapturedWorkflowCall[], reports: readonly ReplayReport[], expectations: EvalExpectations): string[] {
284
305
  const errors: string[] = [];
285
306
  const staticCalls = calls.flatMap((call) => { try { return call.script ? inspectWorkflowScript(call.script) : []; } catch { return []; } });
@@ -595,7 +616,7 @@ function semanticJudgePrompt(evalCase: WorkflowEvalCase, calls: readonly Capture
595
616
  const usedRoles = new Set(calls.flatMap(({ script }) => { try { return script ? inspectWorkflowScript(script).flatMap((call) => call.kind === "agent" && call.role ? [call.role] : []) : []; } catch { return []; } }));
596
617
  const roleText = [...usedRoles].map((role) => `${role}: ${roles[role]?.description ?? "no description"}`).join("\n") || "none";
597
618
  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.";
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")}`;
619
+ 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 call(s):\n${calls.map((call, index) => `--- ${String(index)} ---\nArguments:\n${JSON.stringify(call.arguments)}\nScript:\n${call.script ?? "<missing>"}`).join("\n")}`;
599
620
  }
600
621
 
601
622
  async function runSemanticJudge(input: CaptureCaseInput, calls: readonly CapturedWorkflowCall[], cwd: string, home: string, sessionDir: string, maxCost: number): Promise<JudgeProcessResult> {
@@ -720,7 +741,7 @@ export async function captureEvalCase(input: CaptureCaseInput): Promise<EvalCase
720
741
  const parentUsageThroughCandidate = usageThroughCandidate(oracle, workflows, selection.callIndices);
721
742
  const parentAccounting = parentUsageThroughCandidate ?? oracle.usage;
722
743
  const unsafeTool = oracle.parentToolSequence.find((tool) => !SAFE_PARENT_EVAL_TOOLS.includes(tool as (typeof SAFE_PARENT_EVAL_TOOLS)[number]));
723
- const errors = [...evalExpectationErrors(oracle, input.case.expectations), ...validation.errors, ...(unsafeTool ? [`parent tool is outside the safe eval allowlist: ${unsafeTool}`] : [])];
744
+ const errors = [...evalExpectationErrors(oracle, input.case.expectations), ...recoverySelectionErrors(input.case, oracle), ...validation.errors, ...(unsafeTool ? [`parent tool is outside the safe eval allowlist: ${unsafeTool}`] : [])];
724
745
  if (requiredCount > 0 && selection.callIndices.length === 0) errors.push("Catastrophic validity failure: no production-valid workflow candidate satisfied static expectations.");
725
746
  let judge: SemanticJudgeReport | undefined;
726
747
  let judgeProcess: JudgeProcessResult | undefined;