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
@@ -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";
package/dist/src/index.js CHANGED
@@ -5,8 +5,10 @@ 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";
11
+ export { acquireSessionLease, hasLiveSessionLease, listPersistedSessionIds, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
10
12
  export { FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
11
13
  export { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
12
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;
@@ -257,6 +259,7 @@ export interface AgentRecord {
257
259
  state: AgentState;
258
260
  parentId?: string;
259
261
  structuralPath?: readonly string[];
262
+ resultPath?: string;
260
263
  parentBreadcrumb?: string;
261
264
  worktreeOwner?: string;
262
265
  role?: string;
@@ -278,6 +281,14 @@ export interface AgentRecord {
278
281
  state: "running" | "completed" | "failed";
279
282
  }[];
280
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;
281
292
  }
282
293
  export interface WorkflowRunEvent {
283
294
  type: string;
@@ -305,26 +316,32 @@ export interface RunRecord {
305
316
  phase?: string;
306
317
  phaseHistory?: readonly WorkflowPhaseRecord[];
307
318
  agents: readonly AgentRecord[];
319
+ activeShells?: number;
308
320
  error?: WorkflowErrorShape;
321
+ failedAt?: string;
309
322
  budget?: WorkflowBudget;
310
323
  budgetVersion?: number;
311
324
  usage?: WorkflowBudgetUsage;
312
325
  budgetEvents?: readonly BudgetEvent[];
313
326
  events?: readonly WorkflowRunEvent[];
327
+ delivery?: WorkflowRunDelivery;
314
328
  }
315
329
  export declare const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 5;
330
+ export type WorkflowLaunchMode = "foreground" | "background";
316
331
  export interface AgentDefinition {
317
332
  prompt?: string;
318
333
  description?: string;
319
334
  model?: string;
320
335
  thinking?: NonNullable<ModelSpec["thinking"]>;
321
336
  tools?: readonly string[];
337
+ overrideSystemPrompt?: boolean;
322
338
  disabledAgentResources?: AgentResourceExclusions;
323
339
  }
324
340
  export interface LaunchSnapshot {
325
341
  identityVersion?: number;
326
342
  launchKind?: "inline" | "function";
327
343
  functionName?: string;
344
+ launchMode?: WorkflowLaunchMode;
328
345
  script: string;
329
346
  args: JsonValue;
330
347
  metadata: WorkflowMetadata;
@@ -448,8 +465,10 @@ export interface SessionInput {
448
465
  agentDir?: string;
449
466
  customTools?: SessionCustomTools;
450
467
  resultTool?: ToolDefinition;
468
+ systemPrompt?: string;
451
469
  systemPromptAppend?: string;
452
470
  extensionFactories?: InlineExtension[];
471
+ additionalSkillPaths?: readonly string[];
453
472
  resourcePolicy?: AgentResourcePolicy;
454
473
  options?: AgentOptions;
455
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()) {
@@ -401,6 +407,39 @@ function workflowCallsWithStructure(program) {
401
407
  visit(program, { execution: "sequential", structure: [] });
402
408
  return calls.sort((left, right) => left.call.start - right.call.start);
403
409
  }
410
+ function memberCall(node, objectName, propertyName) {
411
+ 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")
412
+ return false;
413
+ return node.callee.property.name === propertyName;
414
+ }
415
+ function mapCallback(node) {
416
+ if (!memberCall(node, "Promise", "all") && !memberCall(node, "Promise", "allSettled"))
417
+ return undefined;
418
+ if (node.type !== "CallExpression")
419
+ return undefined;
420
+ const source = node.arguments[0];
421
+ if (source?.type !== "CallExpression" || source.callee.type !== "MemberExpression" || source.callee.computed || source.callee.property.type !== "Identifier" || !["map", "flatMap"].includes(source.callee.property.name))
422
+ return undefined;
423
+ const callback = source.arguments[0];
424
+ return callback?.type === "ArrowFunctionExpression" || callback?.type === "FunctionExpression" ? callback : undefined;
425
+ }
426
+ function hasUnscopedAgent(node, scoped = false) {
427
+ const operation = workflowCallKind(node);
428
+ if (operation === "agent")
429
+ return !scoped;
430
+ const nestedScope = scoped || operation === "parallel" || operation === "pipeline";
431
+ return astChildren(node).some((child) => hasUnscopedAgent(child, nestedScope));
432
+ }
433
+ function validateObviousConcurrentAgentCalls(program) {
434
+ const visit = (node) => {
435
+ const callback = mapCallback(node);
436
+ if (callback && hasUnscopedAgent(callback))
437
+ fail("INVALID_METADATA", "Promise.all/map agent fan-out cannot prove stable call-site identity; use parallel(...) or pipeline(...)");
438
+ for (const child of astChildren(node))
439
+ visit(child);
440
+ };
441
+ visit(program);
442
+ }
404
443
  function validateDirectPrimitiveReferences(program, name) {
405
444
  const visit = (node, parent) => {
406
445
  if (node.type === "Identifier" && node.name === name) {
@@ -731,6 +770,7 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
731
770
  for (const [index, schema] of schemas.entries())
732
771
  validateSchema(schema, `schema[${String(index)}]`);
733
772
  const calls = workflowCalls(program);
773
+ validateObviousConcurrentAgentCalls(program);
734
774
  const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase) => phase !== undefined);
735
775
  for (const call of calls) {
736
776
  const operation = call.callee.name;
@@ -0,0 +1,13 @@
1
+ import type { JsonValue } from "./types.js";
2
+ export interface WorkflowArtifact {
3
+ extension: ".js" | ".json" | ".md";
4
+ content: string;
5
+ }
6
+ export type WorkflowTui = {
7
+ stop(): void;
8
+ start(): void;
9
+ requestRender(force?: boolean): void;
10
+ };
11
+ export declare function workflowScriptArtifact(script: string): WorkflowArtifact;
12
+ export declare function workflowResultArtifact(value: JsonValue): WorkflowArtifact;
13
+ export declare function openWorkflowArtifact(tui: WorkflowTui, command: string, artifact: WorkflowArtifact): Promise<number | null>;
@@ -0,0 +1,39 @@
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
+ export function workflowScriptArtifact(script) { return { extension: ".js", content: script }; }
6
+ export function workflowResultArtifact(value) { return typeof value === "string" ? { extension: ".md", content: value } : { extension: ".json", content: `${JSON.stringify(value, null, 2)}\n` }; }
7
+ async function spawnWorkflowEditor(command, path) {
8
+ const [editor, ...editorArgs] = command.split(" ");
9
+ if (!editor)
10
+ return null;
11
+ return new Promise((resolve) => {
12
+ try {
13
+ const child = spawn(editor, [...editorArgs, path], { stdio: "inherit", shell: process.platform === "win32" });
14
+ child.once("error", () => { resolve(null); });
15
+ child.once("close", (code) => { resolve(code); });
16
+ }
17
+ catch {
18
+ resolve(null);
19
+ }
20
+ });
21
+ }
22
+ export async function openWorkflowArtifact(tui, command, artifact) {
23
+ const directory = await mkdtemp(join(tmpdir(), "pi-workflow-editor-"));
24
+ const path = join(directory, `artifact${artifact.extension}`);
25
+ try {
26
+ await writeFile(path, artifact.content, { encoding: "utf8", mode: 0o600 });
27
+ tui.stop();
28
+ try {
29
+ return await spawnWorkflowEditor(command, path);
30
+ }
31
+ finally {
32
+ tui.start();
33
+ tui.requestRender(true);
34
+ }
35
+ }
36
+ finally {
37
+ await rm(directory, { recursive: true, force: true });
38
+ }
39
+ }
@@ -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;