pi-extensible-workflows 3.2.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.
@@ -6,9 +6,10 @@ export * from "./registry.js";
6
6
  export * from "./execution.js";
7
7
  export * from "./host.js";
8
8
  export * from "./workflow-artifacts.js";
9
+ export * from "./bundles.js";
9
10
  export { default } from "./host.js";
10
- export { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
11
- 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";
12
13
  export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
13
14
  export type { AgentAttempt, AgentBudgetHooks, AgentExecutionOptions, AgentExecutionResult, AgentExecutionRoot, AgentProgress, AgentProviderFailure, AgentProviderRecovery, AgentToolCallProgress, AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./agent-execution.js";
14
15
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
package/dist/src/index.js CHANGED
@@ -6,8 +6,9 @@ export * from "./registry.js";
6
6
  export * from "./execution.js";
7
7
  export * from "./host.js";
8
8
  export * from "./workflow-artifacts.js";
9
+ export * from "./bundles.js";
9
10
  export { default } from "./host.js";
10
- export { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
11
+ export { acquireSessionLease, hasLiveSessionLease, listPersistedSessionIds, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
11
12
  export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
12
13
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
13
14
  export { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
@@ -1,4 +1,4 @@
1
- import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowRunEvent } from "./types.js";
1
+ import type { BudgetApprovalRequest, JsonValue, LaunchSnapshot, RunRecord, WorkflowBudgetUsage, WorkflowRunEvent } from "./types.js";
2
2
  import type { OwnershipRecord } from "./agent-execution.js";
3
3
  export interface NativeSessionReference {
4
4
  sessionId: string;
@@ -14,6 +14,40 @@ export interface EffectiveSystemPrompt {
14
14
  export interface PersistedRun extends RunRecord {
15
15
  nativeSessions: readonly NativeSessionReference[];
16
16
  }
17
+ export interface RunSummaryAgent {
18
+ id: string;
19
+ name: string;
20
+ label?: string;
21
+ state: string;
22
+ role?: string;
23
+ attempts: number;
24
+ }
25
+ export interface RunSummaryArtifacts {
26
+ runDirectory: string;
27
+ statePath: string;
28
+ journalPath: string;
29
+ snapshotPath: string;
30
+ workflowPath: string;
31
+ resultPath: string;
32
+ summaryPath: string;
33
+ }
34
+ export interface RunSummary {
35
+ schemaVersion: 1;
36
+ runId: string;
37
+ sessionId: string;
38
+ workflowName: string;
39
+ state: RunRecord["state"];
40
+ createdAt: string;
41
+ updatedAt: string;
42
+ terminalAt?: string;
43
+ usage: WorkflowBudgetUsage;
44
+ agents: readonly RunSummaryAgent[];
45
+ error?: RunRecord["error"];
46
+ failedAt?: string;
47
+ replayablePaths: readonly string[];
48
+ incompletePaths: readonly string[];
49
+ artifacts: RunSummaryArtifacts;
50
+ }
17
51
  export interface CompletedOperation {
18
52
  path: string;
19
53
  value: JsonValue;
@@ -41,6 +75,7 @@ export interface BorrowedWorktreeBinding {
41
75
  export declare function projectStorageKey(cwd: string): string;
42
76
  export declare function projectSessionsDirectory(cwd: string, home?: string): string;
43
77
  export declare function runsDirectory(cwd: string, sessionId: string, home?: string): string;
78
+ export declare function listPersistedSessionIds(cwd: string, home?: string): Promise<string[]>;
44
79
  export declare function hasLiveSessionLease(cwd: string, sessionId: string, home?: string): Promise<boolean>;
45
80
  export declare class SessionLease {
46
81
  #private;
@@ -63,6 +98,7 @@ export declare class RunStore {
63
98
  readonly directory: string;
64
99
  private journalWrite;
65
100
  private stateWrite;
101
+ private summaryWrite;
66
102
  private worktreeWrite;
67
103
  private borrowedWorktreeWrite;
68
104
  private snapshotWrite;
@@ -70,11 +106,14 @@ export declare class RunStore {
70
106
  private systemPromptWrite;
71
107
  constructor(cwd: string, sessionId: string, runId: string, home?: string);
72
108
  create(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>): Promise<void>;
109
+ private refreshSummary;
110
+ private refreshSummaryBestEffort;
73
111
  isComplete(): Promise<boolean>;
74
112
  load(): Promise<{
75
113
  run: PersistedRun;
76
114
  snapshot: Readonly<LaunchSnapshot>;
77
115
  }>;
116
+ loadSummary(): Promise<RunSummary>;
78
117
  saveState(run: PersistedRun): Promise<void>;
79
118
  updateState(update: (run: PersistedRun) => PersistedRun | Promise<PersistedRun>): Promise<PersistedRun>;
80
119
  saveSnapshot(snapshot: Readonly<LaunchSnapshot>): Promise<void>;
@@ -7,6 +7,16 @@ import { homedir } from "node:os";
7
7
  import { promisify } from "node:util";
8
8
  import { WorkflowError } from "./types.js";
9
9
  import { loadLaunchSnapshot } from "./utils.js";
10
+ const TERMINAL_SUMMARY_STATES = new Set(["completed", "failed", "stopped"]);
11
+ const EMPTY_USAGE = { tokens: 0, costUsd: 0, durationMs: 0, agentLaunches: 0 };
12
+ function summaryArtifacts(directory) { 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") }; }
13
+ function summaryFromRun(run, directory, journal, previous, fallbackCreatedAt, now = new Date().toISOString()) {
14
+ const createdAt = typeof previous?.createdAt === "string" ? previous.createdAt : fallbackCreatedAt;
15
+ const failedAt = run.failedAt ?? run.error?.failedAt;
16
+ const replayablePaths = [...new Set([...(run.retry?.completedPaths ?? []), ...Object.keys(journal.completed)])];
17
+ const incompletePaths = [...new Set([...(run.retry?.incompletePaths ?? []), ...(failedAt ? [failedAt] : [])])];
18
+ 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) };
19
+ }
10
20
  const execute = promisify(execFile);
11
21
  const gitIdentity = {
12
22
  GIT_AUTHOR_NAME: "pi-extensible-workflows", GIT_AUTHOR_EMAIL: "pi-extensible-workflows@localhost", GIT_COMMITTER_NAME: "pi-extensible-workflows", GIT_COMMITTER_EMAIL: "pi-extensible-workflows@localhost",
@@ -24,6 +34,17 @@ export function projectSessionsDirectory(cwd, home = homedir()) {
24
34
  export function runsDirectory(cwd, sessionId, home = homedir()) {
25
35
  return join(projectSessionsDirectory(cwd, home), safePart(sessionId), "runs");
26
36
  }
37
+ export async function listPersistedSessionIds(cwd, home = homedir()) {
38
+ try {
39
+ const entries = await readdir(projectSessionsDirectory(cwd, home), { withFileTypes: true });
40
+ return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map(({ name }) => name);
41
+ }
42
+ catch (error) {
43
+ if (error.code === "ENOENT")
44
+ return [];
45
+ throw error;
46
+ }
47
+ }
27
48
  const SESSION_OWNER_FILE = "owner.json";
28
49
  const SESSION_OWNER_WRITE_GRACE_MS = 30_000;
29
50
  const RUN_CREATE_TEMP = /^\.([a-zA-Z0-9._-]+)\.(\d+)\.[0-9a-f-]+\.tmp$/;
@@ -236,6 +257,7 @@ export class RunStore {
236
257
  journalWrite = Promise.resolve();
237
258
  // ponytail: serializes one RunStore instance; cross-process run sharing remains unsupported.
238
259
  stateWrite = Promise.resolve();
260
+ summaryWrite = Promise.resolve();
239
261
  worktreeWrite = Promise.resolve();
240
262
  borrowedWorktreeWrite = Promise.resolve();
241
263
  snapshotWrite = Promise.resolve();
@@ -265,6 +287,7 @@ export class RunStore {
265
287
  await atomicJson(join(temporary, "borrowed-worktrees.json"), []);
266
288
  await atomicJson(join(temporary, "state.json"), run);
267
289
  await atomicJson(join(temporary, "system-prompts.json"), { version: 1, entries: [] });
290
+ await atomicJson(join(temporary, "summary.json"), summaryFromRun(run, this.directory, { completed: {} }, undefined, new Date().toISOString()));
268
291
  await rename(temporary, this.directory);
269
292
  }
270
293
  catch (error) {
@@ -272,6 +295,18 @@ export class RunStore {
272
295
  throw error;
273
296
  }
274
297
  }
298
+ async refreshSummary() {
299
+ const write = this.summaryWrite.then(async () => {
300
+ const run = await json(join(this.directory, "state.json"));
301
+ const journal = await json(join(this.directory, "journal.json"));
302
+ const previous = await json(join(this.directory, "summary.json")).catch(() => undefined);
303
+ const fallbackCreatedAt = await stat(join(this.directory, "state.json")).then((value) => new Date(value.mtimeMs).toISOString());
304
+ await atomicJson(join(this.directory, "summary.json"), summaryFromRun(run, this.directory, journal, previous, fallbackCreatedAt));
305
+ });
306
+ this.summaryWrite = write.catch(() => undefined);
307
+ await write;
308
+ }
309
+ refreshSummaryBestEffort() { void this.refreshSummary().catch(() => undefined); }
275
310
  async isComplete() {
276
311
  try {
277
312
  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"))]);
@@ -288,11 +323,25 @@ export class RunStore {
288
323
  throw new WorkflowError("RESUME_INCOMPATIBLE", "Persisted run belongs to another cwd or Pi session");
289
324
  return { run, snapshot: loadLaunchSnapshot(await json(join(this.directory, "snapshot.json"))) };
290
325
  }
326
+ async loadSummary() {
327
+ await this.stateWrite;
328
+ await this.journalWrite;
329
+ await this.summaryWrite;
330
+ const run = await json(join(this.directory, "state.json"));
331
+ const journal = await json(join(this.directory, "journal.json"));
332
+ const previous = await json(join(this.directory, "summary.json")).catch(() => undefined);
333
+ const [stateStat, journalStat] = await Promise.all([stat(join(this.directory, "state.json")), stat(join(this.directory, "journal.json"))]);
334
+ const fallbackCreatedAt = new Date(stateStat.mtimeMs).toISOString();
335
+ const previousUpdatedAt = previous?.updatedAt === undefined ? Number.NaN : Date.parse(previous.updatedAt);
336
+ const updatedAt = new Date(Math.max(stateStat.mtimeMs, journalStat.mtimeMs, Number.isNaN(previousUpdatedAt) ? 0 : previousUpdatedAt)).toISOString();
337
+ return summaryFromRun(run, this.directory, journal, previous, fallbackCreatedAt, updatedAt);
338
+ }
291
339
  async saveState(run) {
292
340
  const write = this.stateWrite.then(async () => {
293
341
  if (resolve(run.cwd) !== this.cwd || run.sessionId !== this.sessionId || run.id !== this.runId)
294
342
  throw new WorkflowError("INTERNAL_ERROR", "Run identity does not match its session-scoped store");
295
343
  await atomicJson(join(this.directory, "state.json"), run);
344
+ this.refreshSummaryBestEffort();
296
345
  });
297
346
  this.stateWrite = write.catch(() => undefined);
298
347
  await write;
@@ -307,6 +356,7 @@ export class RunStore {
307
356
  if (resolve(result.cwd) !== this.cwd || result.sessionId !== this.sessionId || result.id !== this.runId)
308
357
  throw new WorkflowError("INTERNAL_ERROR", "Run identity does not match its session-scoped store");
309
358
  await atomicJson(join(this.directory, "state.json"), result);
359
+ this.refreshSummaryBestEffort();
310
360
  });
311
361
  this.stateWrite = write.catch(() => undefined);
312
362
  await write;
@@ -351,6 +401,7 @@ export class RunStore {
351
401
  journal.awaiting ??= {};
352
402
  result = await update(journal);
353
403
  await atomicJson(journalPath, journal);
404
+ this.refreshSummaryBestEffort();
354
405
  });
355
406
  this.journalWrite = write.catch(() => undefined);
356
407
  await write;
@@ -1,6 +1,6 @@
1
1
  import { type SessionEntry, type SessionInfo } from "@earendil-works/pi-coding-agent";
2
2
  import { type AgentSetupSummary, type ModelSpec, type StaticWorkflowCall } from "./index.js";
3
- import { type PersistedRun } from "./persistence.js";
3
+ import { type PersistedRun, type RunSummary } from "./persistence.js";
4
4
  export interface ModelUsage {
5
5
  model: string;
6
6
  cost: number;
@@ -62,11 +62,21 @@ export interface InspectorViewState {
62
62
  selected: number;
63
63
  scroll: number;
64
64
  }
65
+ export type InspectMode = "tui" | "json" | "summary";
66
+ export interface PersistedSessionSummary {
67
+ schemaVersion: 1;
68
+ cwd: string;
69
+ sessionId: string;
70
+ runs: readonly RunSummary[];
71
+ }
65
72
  export declare function transcriptLines(entries: readonly SessionEntry[]): string[];
66
73
  export declare function transcriptFileLines(path: string): string[];
67
74
  export declare function matchSession(query: string, sessions: readonly SessionInfo[]): SessionInfo;
68
75
  export declare function loadSessionReport(path: string, home?: string): Promise<SessionReport>;
69
76
  export declare function renderInspector(report: SessionReport, state: InspectorViewState, width?: number, height?: number, highlighter?: (script: string) => string[]): string[];
77
+ export declare function loadPersistedSessionSummary(cwd: string, sessionId: string, home?: string, failedOnly?: boolean): Promise<PersistedSessionSummary>;
78
+ export declare function loadPersistedSummaries(cwd: string, sessionId: string | undefined, home?: string, failedOnly?: boolean): Promise<readonly PersistedSessionSummary[]>;
79
+ export declare function formatPersistedRunSummary(summary: RunSummary, sessionId?: string): string;
70
80
  export declare function showSessionInspector(report: SessionReport): Promise<void>;
71
81
  export declare function resolveSession(query: string, sessionDir?: string | undefined): Promise<SessionInfo>;
72
- export declare function runSessionInspector(sessionId?: string): Promise<void>;
82
+ export declare function runSessionInspector(sessionId?: string, mode?: InspectMode, cwd?: string, home?: string, write?: (text: string) => void, failedOnly?: boolean): Promise<void>;
@@ -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 } from "@earendil-works/pi-coding-agent";
7
7
  import { formatBudgetStatus, inspectWorkflowScript } from "./index.js";
8
- import { listRunIds, RunStore } from "./persistence.js";
8
+ import { listPersistedSessionIds, listRunIds, RunStore } from "./persistence.js";
9
9
  function text(content) {
10
10
  if (typeof content === "string")
11
11
  return content;
@@ -346,6 +346,28 @@ export function renderInspector(report, state, width = 80, height = 24, highligh
346
346
  const scroll = Math.max(0, Math.min(state.scroll, Math.max(0, fitted.length - room)));
347
347
  return [...header, ...fitted.slice(scroll, scroll + room), ...footer].slice(0, height);
348
348
  }
349
+ export async function loadPersistedSessionSummary(cwd, sessionId, home = homedir(), failedOnly = false) {
350
+ const runs = [];
351
+ for (const runId of (await listRunIds(cwd, sessionId, home)).sort()) {
352
+ try {
353
+ const summary = await new RunStore(cwd, sessionId, runId, home).loadSummary();
354
+ if (!failedOnly || summary.state === "failed")
355
+ runs.push(summary);
356
+ }
357
+ catch { /* Ignore corrupt or concurrently removed runs. */ }
358
+ }
359
+ return { schemaVersion: 1, cwd, sessionId, runs };
360
+ }
361
+ export async function loadPersistedSummaries(cwd, sessionId, home = homedir(), failedOnly = false) {
362
+ const sessionIds = sessionId ? [sessionId] : (await listPersistedSessionIds(cwd, home)).sort();
363
+ const sessions = await Promise.all(sessionIds.map((id) => loadPersistedSessionSummary(cwd, id, home, failedOnly)));
364
+ return sessionId || !failedOnly ? sessions : sessions.filter((session) => session.runs.length > 0);
365
+ }
366
+ export function formatPersistedRunSummary(summary, sessionId = summary.sessionId) {
367
+ const counts = summary.agents.reduce((result, agent) => { result[agent.state] = (result[agent.state] ?? 0) + 1; return result; }, {});
368
+ const status = Object.entries(counts).map(([state, count]) => `${state}=${String(count)}`).join(", ") || "agents=0";
369
+ return `${sessionId} ${summary.runId} ${summary.workflowName} ${summary.state} ${status} updated=${summary.updatedAt}`;
370
+ }
349
371
  function nextState(current, key, workflowCount) {
350
372
  if (current.view === "list") {
351
373
  if (key === "up")
@@ -410,7 +432,19 @@ async function askSessionId() {
410
432
  export async function resolveSession(query, sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR) {
411
433
  return matchSession(query, await SessionManager.listAll(sessionDir));
412
434
  }
413
- export async function runSessionInspector(sessionId) {
435
+ export async function runSessionInspector(sessionId, mode = "tui", cwd = process.cwd(), home = homedir(), write = (text) => { stdout.write(text); }, failedOnly = false) {
436
+ if (mode !== "tui") {
437
+ const sessions = await loadPersistedSummaries(cwd, sessionId?.trim() || undefined, home, failedOnly);
438
+ if (mode === "json") {
439
+ const value = sessionId?.trim() ? sessions[0] : undefined;
440
+ write(`${JSON.stringify(value ?? { schemaVersion: 1, cwd, sessions })}\n`);
441
+ return;
442
+ }
443
+ const emptyLabel = failedOnly ? "no failed runs" : "no persisted runs";
444
+ const lines = sessions.flatMap((session) => session.runs.length ? session.runs.map((run) => formatPersistedRunSummary(run, session.sessionId)) : [`${session.sessionId} (${emptyLabel})`]);
445
+ write(`${lines.length ? lines.join("\n") : failedOnly ? "No failed workflow runs." : "No persisted workflow runs."}\n`);
446
+ return;
447
+ }
414
448
  const query = sessionId?.trim() || await askSessionId();
415
449
  if (!query)
416
450
  throw new Error("Session ID is required.");
@@ -83,6 +83,7 @@ export interface BudgetApprovalRequest {
83
83
  export interface WorkflowErrorShape {
84
84
  code: WorkflowErrorCode;
85
85
  message: string;
86
+ failedAt?: string;
86
87
  }
87
88
  export interface WorkflowEventBase {
88
89
  runId: string;
@@ -201,6 +202,7 @@ export interface AgentActivity {
201
202
  kind: "reasoning" | "tool" | "text";
202
203
  text: string;
203
204
  }
205
+ export declare const WORKFLOW_AGENT_STALL_THRESHOLD_MS: number;
204
206
  export interface AgentAccounting {
205
207
  input: number;
206
208
  output: number;
@@ -279,6 +281,14 @@ export interface AgentRecord {
279
281
  state: "running" | "completed" | "failed";
280
282
  }[];
281
283
  activity?: AgentActivity | undefined;
284
+ lastEventAt?: number;
285
+ }
286
+ export type WorkflowDeliveryMode = "foreground" | "background";
287
+ export type WorkflowDeliveryStatus = "attached" | "pending" | "delivered";
288
+ export interface WorkflowRunDelivery {
289
+ mode: WorkflowDeliveryMode;
290
+ state: WorkflowDeliveryStatus;
291
+ toolCallId?: string;
282
292
  }
283
293
  export interface WorkflowRunEvent {
284
294
  type: string;
@@ -306,26 +316,32 @@ export interface RunRecord {
306
316
  phase?: string;
307
317
  phaseHistory?: readonly WorkflowPhaseRecord[];
308
318
  agents: readonly AgentRecord[];
319
+ activeShells?: number;
309
320
  error?: WorkflowErrorShape;
321
+ failedAt?: string;
310
322
  budget?: WorkflowBudget;
311
323
  budgetVersion?: number;
312
324
  usage?: WorkflowBudgetUsage;
313
325
  budgetEvents?: readonly BudgetEvent[];
314
326
  events?: readonly WorkflowRunEvent[];
327
+ delivery?: WorkflowRunDelivery;
315
328
  }
316
329
  export declare const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
330
+ export type WorkflowLaunchMode = "foreground" | "background";
317
331
  export interface AgentDefinition {
318
332
  prompt?: string;
319
333
  description?: string;
320
334
  model?: string;
321
335
  thinking?: NonNullable<ModelSpec["thinking"]>;
322
336
  tools?: readonly string[];
337
+ overrideSystemPrompt?: boolean;
323
338
  disabledAgentResources?: AgentResourceExclusions;
324
339
  }
325
340
  export interface LaunchSnapshot {
326
341
  identityVersion?: number;
327
342
  launchKind?: "inline" | "function";
328
343
  functionName?: string;
344
+ launchMode?: WorkflowLaunchMode;
329
345
  script: string;
330
346
  args: JsonValue;
331
347
  metadata: WorkflowMetadata;
@@ -449,8 +465,10 @@ export interface SessionInput {
449
465
  agentDir?: string;
450
466
  customTools?: SessionCustomTools;
451
467
  resultTool?: ToolDefinition;
468
+ systemPrompt?: string;
452
469
  systemPromptAppend?: string;
453
470
  extensionFactories?: InlineExtension[];
471
+ additionalSkillPaths?: readonly string[];
454
472
  resourcePolicy?: AgentResourcePolicy;
455
473
  options?: AgentOptions;
456
474
  }
package/dist/src/types.js CHANGED
@@ -16,6 +16,7 @@ export const ERROR_CODES = [
16
16
  "RUN_OWNED", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "SHELL_FAILED", "AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID",
17
17
  "CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
18
18
  ];
19
+ export const WORKFLOW_AGENT_STALL_THRESHOLD_MS = 10 * 60 * 1000;
19
20
  export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
20
21
  export class WorkflowError extends Error {
21
22
  code;
@@ -180,7 +180,7 @@ export function parseRoleMarkdown(content, strict = false, rolePath) {
180
180
  return { prompt: content };
181
181
  const meta = {};
182
182
  for (const line of content.slice(4, end).split("\n")) {
183
- const match = /^(model|thinking|tools|description)\s*:\s*(.+)$/.exec(line.trim());
183
+ const match = /^(model|thinking|tools|description|overrideSystemPrompt|override_system_prompt|is_system_prompt)\s*:\s*(.+)$/.exec(line.trim());
184
184
  if (match?.[1] && match[2])
185
185
  meta[match[1]] = match[2].trim();
186
186
  }
@@ -197,6 +197,9 @@ export function parseRoleMarkdown(content, strict = false, rolePath) {
197
197
  definition.thinking = thinking;
198
198
  if (tools)
199
199
  definition.tools = tools;
200
+ const overrideSystemPrompt = meta.overrideSystemPrompt ?? meta.override_system_prompt ?? meta.is_system_prompt;
201
+ if (overrideSystemPrompt)
202
+ definition.overrideSystemPrompt = overrideSystemPrompt === "true";
200
203
  return definition;
201
204
  }
202
205
  const normalized = content.replace(/\r\n?/g, "\n");
@@ -212,16 +215,19 @@ export function parseRoleMarkdown(content, strict = false, rolePath) {
212
215
  if (!object(parsed.frontmatter))
213
216
  fail("INVALID_METADATA", "Role frontmatter must be an object");
214
217
  const { model, thinking, tools, description, disabledAgentResources } = parsed.frontmatter;
218
+ const overrideSystemPrompt = parsed.frontmatter.overrideSystemPrompt ?? parsed.frontmatter.override_system_prompt ?? parsed.frontmatter.is_system_prompt;
215
219
  if (model !== undefined && (typeof model !== "string" || model.trim() === ""))
216
220
  fail("INVALID_METADATA", "Role model must be a non-empty string");
217
221
  if (thinking !== undefined && (typeof thinking !== "string" || !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(thinking)))
218
222
  fail("INVALID_METADATA", `Invalid role thinking level: ${typeof thinking === "string" ? thinking : typeof thinking}`);
219
223
  if (description !== undefined && (typeof description !== "string" || description.trim() === "" || description.length > 1024 || /[\r\n]/.test(description)))
220
224
  fail("INVALID_METADATA", "Role description must be a non-empty single-line string of at most 1024 characters");
225
+ if (overrideSystemPrompt !== undefined && typeof overrideSystemPrompt !== "boolean")
226
+ fail("INVALID_METADATA", "Role overrideSystemPrompt must be a boolean");
221
227
  if (tools !== undefined && (!Array.isArray(tools) || tools.some((tool) => typeof tool !== "string" || tool.trim() === "")))
222
228
  fail("INVALID_METADATA", "Role tools must be an array of non-empty strings");
223
229
  const normalizedResources = validateAgentResourceExclusions(disabledAgentResources, rolePath ?? "<role>", "INVALID_METADATA");
224
- return { prompt: parsed.body, ...(typeof description === "string" ? { description: description.trim() } : {}), ...(typeof model === "string" ? { model: model.trim() } : {}), ...(typeof thinking === "string" ? { thinking: thinking } : {}), ...(Array.isArray(tools) ? { tools: tools.map((tool) => tool.trim()) } : {}), ...(normalizedResources ? { disabledAgentResources: normalizedResources } : {}) };
230
+ return { prompt: parsed.body, ...(typeof description === "string" ? { description: description.trim() } : {}), ...(typeof model === "string" ? { model: model.trim() } : {}), ...(typeof thinking === "string" ? { thinking: thinking } : {}), ...(Array.isArray(tools) ? { tools: tools.map((tool) => tool.trim()) } : {}), ...(typeof overrideSystemPrompt === "boolean" ? { overrideSystemPrompt } : {}), ...(normalizedResources ? { disabledAgentResources: normalizedResources } : {}) };
225
231
  }
226
232
  const ROLE_DIRECTORY = "pi-extensible-workflows";
227
233
  export function workflowRoleDirectories(agentDir = getAgentDir()) {
@@ -74,6 +74,10 @@ export interface ParentToolResult {
74
74
  isError?: boolean;
75
75
  text?: string;
76
76
  }
77
+ export interface ParentToolCall {
78
+ name: string;
79
+ arguments: JsonValue;
80
+ }
77
81
  export interface ParentUsage {
78
82
  input: number;
79
83
  output: number;
@@ -216,13 +220,14 @@ export interface EvalCaseResult {
216
220
  realWorkflowAgentsLaunched: number | null;
217
221
  };
218
222
  }
219
- export declare const SAFE_PARENT_EVAL_TOOLS: readonly ["read", "grep", "find", "bash", "workflow"];
223
+ export declare const SAFE_PARENT_EVAL_TOOLS: readonly ["read", "grep", "find", "bash", "workflow", "workflow_retry"];
220
224
  export declare function validateWorkflowEvalCases(values: readonly unknown[], source?: string): readonly WorkflowEvalCase[];
221
225
  export declare function loadWorkflowEvalCases(directory?: string): readonly WorkflowEvalCase[];
222
226
  export declare const INITIAL_WORKFLOW_EVAL_CASES: readonly WorkflowEvalCase[];
223
227
  export declare function matchesJsonResult(shape: JsonResultShape, value: JsonValue): boolean;
224
228
  export declare function matchesOutputSchema(shape: OutputSchemaShape, schema: JsonSchema): boolean;
225
229
  export declare function extractParentOracle(entries: readonly unknown[]): ParentOracle;
230
+ export declare function extractParentToolCalls(oracle: ParentOracle): ParentToolCall[];
226
231
  export declare function extractParentOracleFile(path: string): ParentOracle;
227
232
  export declare function extractCapturedWorkflows(oracle: ParentOracle): CapturedWorkflowCall[];
228
233
  export declare function captureValidationReports(oracle: ParentOracle, calls: readonly CapturedWorkflowCall[]): {
@@ -231,6 +236,7 @@ export declare function captureValidationReports(oracle: ParentOracle, calls: re
231
236
  verified: boolean;
232
237
  };
233
238
  export declare function evalExpectationErrors(oracle: ParentOracle, expectations: EvalExpectations): string[];
239
+ export declare function recoverySelectionErrors(evalCase: Pick<WorkflowEvalCase, "id">, oracle: ParentOracle): string[];
234
240
  export declare function replayExpectationErrors(calls: readonly CapturedWorkflowCall[], reports: readonly ReplayReport[], expectations: EvalExpectations): string[];
235
241
  export declare function staticExpectationResults(calls: readonly CapturedWorkflowCall[], expectations: EvalExpectations): CriterionResult[];
236
242
  export declare function selectStaticCandidate(calls: readonly CapturedWorkflowCall[], validations: readonly ProductionValidationReport[], expectations: EvalExpectations, requiredCount?: number): {
@@ -10,7 +10,7 @@ import { CAPTURE_ERROR_PREFIX, CAPTURE_IDENTITY, resolveWorkflowSkillPath } from
10
10
  export { resolveWorkflowSkillPath } from "./eval-capture-extension.js";
11
11
  import { ERROR_CODES, inspectWorkflowScript, isObject, loadAgentDefinitions, runWorkflow, WORKFLOW_CALL_KINDS, WorkflowError } from "./index.js";
12
12
  const CASE_PROCESS_GRACE_MS = 1_000;
13
- export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow"]);
13
+ export const SAFE_PARENT_EVAL_TOOLS = Object.freeze(["read", "grep", "find", "bash", "workflow", "workflow_retry"]);
14
14
  const EVAL_MODEL_TOKEN = "$EVAL_MODEL";
15
15
  const semantic = (description) => [{ id: "intent", description }];
16
16
  const JSON_RESULT_TYPES = ["null", "boolean", "number", "integer", "string", "array", "object"];
@@ -367,6 +367,13 @@ export function extractParentOracle(entries) {
367
367
  }
368
368
  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 })) } };
369
369
  }
370
+ export function extractParentToolCalls(oracle) {
371
+ return oracle.assistantBatches.flatMap(({ parts }) => parts.flatMap((part) => {
372
+ if (!isObject(part) || part.type !== "toolCall" || typeof part.name !== "string")
373
+ return [];
374
+ return [{ name: part.name, arguments: isJson(part.arguments) ? part.arguments : null }];
375
+ }));
376
+ }
370
377
  export function extractParentOracleFile(path) {
371
378
  const entries = readFileSync(path, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
372
379
  return extractParentOracle(entries);
@@ -434,6 +441,19 @@ export function evalExpectationErrors(oracle, expectations) {
434
441
  }
435
442
  return errors;
436
443
  }
444
+ const RECOVERY_SELECTIONS = {
445
+ "recovery-failed-run": { tool: "workflow_retry", arguments: { runId: "failed-run-42" } },
446
+ "recovery-completed-worktree": { tool: "workflow", arguments: { name: "borrow-worktree", script: "return true;", parentRunId: "completed-run-42" } },
447
+ };
448
+ export function recoverySelectionErrors(evalCase, oracle) {
449
+ const expected = RECOVERY_SELECTIONS[evalCase.id];
450
+ if (!expected)
451
+ return [];
452
+ const actual = extractParentToolCalls(oracle);
453
+ if (actual.length === 1 && actual[0]?.name === expected.tool && equalJson(actual[0].arguments, expected.arguments))
454
+ return [];
455
+ return [`${evalCase.id} parent tool calls were ${JSON.stringify(actual)}; expected ${JSON.stringify([expected])}`];
456
+ }
437
457
  export function replayExpectationErrors(calls, reports, expectations) {
438
458
  const errors = [];
439
459
  const staticCalls = calls.flatMap((call) => { try {
@@ -862,7 +882,7 @@ function semanticJudgePrompt(evalCase, calls, cwd, home) {
862
882
  } }));
863
883
  const roleText = [...usedRoles].map((role) => `${role}: ${roles[role]?.description ?? "no description"}`).join("\n") || "none";
864
884
  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.";
865
- 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")}`;
885
+ 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")}`;
866
886
  }
867
887
  async function runSemanticJudge(input, calls, cwd, home, sessionDir, maxCost) {
868
888
  const args = ["--offline", "--no-extensions", "--no-skills", "--no-context-files", "--no-tools", "--mode", "json", "--session-dir", sessionDir, "--session-id", randomUUID()];
@@ -1045,7 +1065,7 @@ export async function captureEvalCase(input) {
1045
1065
  const parentUsageThroughCandidate = usageThroughCandidate(oracle, workflows, selection.callIndices);
1046
1066
  const parentAccounting = parentUsageThroughCandidate ?? oracle.usage;
1047
1067
  const unsafeTool = oracle.parentToolSequence.find((tool) => !SAFE_PARENT_EVAL_TOOLS.includes(tool));
1048
- const errors = [...evalExpectationErrors(oracle, input.case.expectations), ...validation.errors, ...(unsafeTool ? [`parent tool is outside the safe eval allowlist: ${unsafeTool}`] : [])];
1068
+ const errors = [...evalExpectationErrors(oracle, input.case.expectations), ...recoverySelectionErrors(input.case, oracle), ...validation.errors, ...(unsafeTool ? [`parent tool is outside the safe eval allowlist: ${unsafeTool}`] : [])];
1049
1069
  if (requiredCount > 0 && selection.callIndices.length === 0)
1050
1070
  errors.push("Catastrophic validity failure: no production-valid workflow candidate satisfied static expectations.");
1051
1071
  let judge;
@@ -0,0 +1,17 @@
1
+ id: recovery-completed-worktree
2
+ prompt: >-
3
+ The completed workflow run ID is "completed-run-42". You need a new workflow to do named worktree work,
4
+ so launch the workflow with exactly parentRunId "completed-run-42". Use exactly the workflow arguments {"name":"borrow-worktree","script":"return true;","parentRunId":"completed-run-42"}.
5
+ Do not call workflow_retry because the source is completed and parentRunId only borrows the named worktree; it does not replay or resume.
6
+ maxCost: 0.1
7
+ expectations:
8
+ firstTool: workflow
9
+ firstBatchToolSequence:
10
+ startsWith: [workflow]
11
+ parentToolSequence:
12
+ startsWith: [workflow]
13
+ workflowCallCount: 1
14
+ expectedWorkflowCalls: 1
15
+ semanticCriteria:
16
+ - id: parent-run-id
17
+ description: The captured workflow call has exactly name borrow-worktree, script return true;, and parentRunId completed-run-42; it does not call workflow_retry.
@@ -0,0 +1,14 @@
1
+ id: recovery-failed-run
2
+ prompt: >-
3
+ A workflow failure diagnostic identifies the persisted failed run ID "failed-run-42" and says the exact next action is
4
+ workflow_retry({ runId: "failed-run-42" }). Select that recovery tool now with exactly {"runId":"failed-run-42"}.
5
+ Do not launch a new workflow, use parentRunId, or answer with prose.
6
+ maxCost: 0.1
7
+ expectedWorkflowCalls: 0
8
+ expectations:
9
+ firstTool: workflow_retry
10
+ firstBatchToolSequence:
11
+ equals: [workflow_retry]
12
+ parentToolSequence:
13
+ equals: [workflow_retry]
14
+ workflowCallCount: 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extensible-workflows",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Deterministic multi-agent workflow orchestration for Pi",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": {
@@ -32,9 +32,9 @@ Inline launches require a non-empty `name`. Registered function launches must om
32
32
  ```json
33
33
  { "workflow": "workflowName", "args": { "issue": 42 } }
34
34
  ```
35
- Recovery map: `agent(..., { retries })` reruns one agent call in the same run for transient failures; `workflow_retry({ runId })` replays a failed run into a child; `workflow_resume({ runId, budget? })` continues a `budget_exhausted` run; `parentRunId` on a new launch only borrows named worktrees and never replays or resumes. Use each only for its stated case.
35
+ Recovery map: `agent(..., { retries })` reruns one agent call in the same run for transient failures; `workflow_retry({ runId, foreground? })` replays a failed run into a child; `workflow_resume({ runId, budget?, foreground? })` continues a `budget_exhausted` run; recovery inherits the source snapshot's foreground/background launch mode, while legacy snapshots without `launchMode` recover in background; set `foreground: true` or `false` to override it; `parentRunId` on a new launch only borrows named worktrees and never replays or resumes. Use each only for its stated case.
36
36
  For an explicitly failed run, use the exact `runId` from failure diagnostics with `workflow_retry({ runId })`: diagnostics list replayable and incomplete paths, artifacts, and valid named worktrees; the tool creates a linked child, replays completed agent, shell, function, and checkpoint operations, and executes incomplete work. External side effects before failure are not guaranteed exactly once.
37
- Inside the workflow, read `args.issue` (`args` is `null` when omitted). `workflow_stop` requires the exact run ID; foreground results retain their value and completed `runId`, while background launches return `runId` immediately. Retry versus per-agent `retries` and `workflow_resume` is always explicit.
37
+ Inside the workflow, read `args.issue` (`args` is `null` when omitted). `workflow_stop` requires the exact run ID; foreground launches and foreground recovery retain their terminal value and completed `runId`, while background launches and recovery return `runId` immediately and deliver completion or failure as a follow-up. Retry versus per-agent `retries` and `workflow_resume` is always explicit.
38
38
  Inspect tool `workflow_catalog` result at least once before creating the first workflow for a task.
39
39
 
40
40
  Workflow JavaScript has no imports, filesystem, network, process, or timers. Delegate that work to agents. `shell(command, options)` is the trusted host RPC for deterministic gates: it inherits the workflow or active-worktree cwd, merges string `env` overrides, and returns `{ exitCode, stdout, stderr }`; nonzero exits are results, but launch failures and timeouts fail with `SHELL_FAILED`.