pi-workflow-engine 0.10.2 → 0.11.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 (55) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +199 -120
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -0
  3. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +145 -0
  4. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +376 -0
  5. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +87 -0
  6. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +84 -529
  7. package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
  8. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +413 -0
  9. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +43 -1
  10. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +81 -0
  11. package/.pi/extensions/pi-workflow-engine/src/budget.ts +7 -0
  12. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +17 -3
  13. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +164 -61
  14. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +117 -20
  15. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +64 -15
  16. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
  17. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +14 -3
  18. package/.pi/extensions/pi-workflow-engine/src/engine.ts +208 -74
  19. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
  20. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
  21. package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
  22. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +8 -3
  23. package/.pi/extensions/pi-workflow-engine/src/journal.ts +148 -64
  24. package/.pi/extensions/pi-workflow-engine/src/options.ts +9 -6
  25. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
  26. package/.pi/extensions/pi-workflow-engine/src/progress.ts +2 -5
  27. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
  28. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +665 -0
  29. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
  30. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
  31. package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
  32. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
  33. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
  34. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
  35. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
  36. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +17 -22
  37. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +143 -52
  38. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
  39. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +295 -0
  40. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +153 -0
  41. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +404 -0
  42. package/.pi/extensions/pi-workflow-engine/src/types.ts +38 -10
  43. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +5 -69
  44. package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
  45. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +121 -0
  46. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -1
  47. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +28 -3
  48. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +282 -41
  49. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +48 -26
  50. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +8 -1
  51. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -2
  52. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -1
  53. package/README.md +170 -226
  54. package/USAGE.md +92 -12
  55. package/package.json +9 -9
@@ -2,22 +2,28 @@ import { fileURLToPath } from "node:url";
2
2
  import { Type } from "typebox";
3
3
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
4
4
  import { SelectList, Text, truncateToWidth, type Component, type SelectItem, type SelectListTheme, type TUI } from "@earendil-works/pi-tui";
5
+ import { isAdvisoryReport } from "./src/advisory-schema.ts";
5
6
  import type { WorkflowProgressSnapshot } from "./src/progress.ts";
6
- import type { WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
7
+ import type { LoadedWorkflow, WorkflowModule, WorkflowProgressSource, WorkflowRef, WorkflowRunMetadata, WorkflowRunOptions } from "./src/types.ts";
7
8
  import { WorkflowInspector } from "./src/ui/workflow-inspector.ts";
8
- import type { PerfSink, PerfSnapshot } from "./src/perf.ts";
9
+ import type { PerfSink } from "./src/perf.ts";
9
10
  import type { WorkflowUsageSnapshot } from "./src/usage.ts";
10
- import { registerDynamax } from "./src/dynamax.ts";
11
- import { handleReviewViewerAction } from "./src/review/review-actions.ts";
12
- import { decideReviewResultsPresentation, extensionContextMode, maybeShowReviewResultsViewer } from "./src/review/review-results-flow.ts";
11
+ import { ADAPTIVE_WORKFLOW_GUIDANCE, dynamaxSessionKey, registerDynamax } from "./src/dynamax.ts";
12
+ import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./src/dynamax-shortcuts.ts";
13
+ import { ReviewSessionCoordinator } from "./src/review/review-session-coordinator.ts";
13
14
  import {
14
15
  formatWorkflowDetailLines,
15
16
  isWorkflowResult,
16
17
  renderWorkflowResult,
17
- type WorkflowPerfDetails,
18
- type WorkflowResultEnvelope,
19
18
  } from "./src/ui/workflow-result-renderer.ts";
20
- import { parseWorkflowBudgetString, WORKFLOW_BUDGET_MAX, WORKFLOW_BUDGET_MIN } from "./src/options.ts";
19
+ import {
20
+ parseWorkflowBudgetString,
21
+ resolveWorkflowRunOptions,
22
+ type ResolvedWorkflowRunOptions,
23
+ WORKFLOW_BUDGET_MAX,
24
+ WORKFLOW_BUDGET_MIN,
25
+ } from "./src/options.ts";
26
+ import { executeWorkflowInvocation, type WorkflowExecution, type WorkflowPerfDetails } from "./src/workflow-execution.ts";
21
27
 
22
28
  /** Extension root (this file lives in <repo>/.pi/extensions/pi-workflow-engine/index.ts). */
23
29
  const EXTENSION_DIR = fileURLToPath(new URL(".", import.meta.url));
@@ -37,22 +43,39 @@ function formatMessageContent(
37
43
  metadata?: WorkflowRunMetadata,
38
44
  ): string {
39
45
  const details = formatWorkflowDetailLines({ usage, perf, metadata });
40
- return `## Workflow: ${name}\n\n${summarize(result)}${details.length > 0 ? `\n\n${details.join("\n")}` : ""}`;
41
- }
42
-
43
- function workflowEnvelope(
44
- name: string,
45
- result: unknown,
46
- usage?: WorkflowUsageSnapshot,
47
- perf?: WorkflowPerfDetails,
48
- metadata?: WorkflowRunMetadata,
49
- ): WorkflowResultEnvelope {
50
- return { name, result, completedAt: Date.now(), usage, perf, runId: metadata?.runId, resumedFromRunId: metadata?.resumedFromRunId };
46
+ return `## Workflow: ${name}\n\n${formatResultForContext(result)}${details.length > 0 ? `\n\n${details.join("\n")}` : ""}`;
47
+ }
48
+
49
+ function formatResultForContext(result: unknown): string {
50
+ if (!isAdvisoryReport(result)) return summarize(result);
51
+
52
+ const lines = [result.summary];
53
+ if (result.findings.length > 0) {
54
+ lines.push("", "Findings:");
55
+ result.findings.forEach((finding, index) => {
56
+ const id = `R${String(index + 1).padStart(3, "0")}`;
57
+ lines.push(
58
+ `\n### ${id}: ${finding.summary}`,
59
+ `- Severity: ${finding.severity}`,
60
+ `- Confidence: ${finding.confidence}`,
61
+ `- Category: ${finding.category}`,
62
+ `- Location: ${finding.locations.map(formatFindingLocation).join(", ")}`,
63
+ `- Impact: ${finding.impact}`,
64
+ `- Evidence: ${finding.evidence.length > 0 ? finding.evidence.join("; ") : "(none cited)"}`,
65
+ `- Recommendation: ${finding.recommendation}`,
66
+ );
67
+ });
68
+ }
69
+ if (result.nextSteps.length > 0) {
70
+ lines.push("", "Next steps:", ...result.nextSteps.map((step) => `- ${step}`));
71
+ }
72
+ return lines.join("\n");
51
73
  }
52
74
 
53
- function compactPerfSnapshot(snapshot: PerfSnapshot | undefined): WorkflowPerfDetails | undefined {
54
- if (!snapshot?.enabled) return undefined;
55
- return { enabled: true, startedAt: snapshot.startedAt, aggregates: snapshot.aggregates };
75
+ function formatFindingLocation(location: { readonly file: string; readonly line?: number; readonly symbol?: string }): string {
76
+ const line = location.line === undefined ? "" : `:${location.line}`;
77
+ const symbol = location.symbol === undefined ? "" : ` (${location.symbol})`;
78
+ return `${location.file}${line}${symbol}`;
56
79
  }
57
80
 
58
81
  type DiscoveryModule = typeof import("./src/discovery.ts");
@@ -71,9 +94,8 @@ async function loadInlineWorkflow(): Promise<InlineWorkflowModule> {
71
94
  return await import("./src/inline-workflow.ts");
72
95
  }
73
96
 
74
- async function createInvocationPerf(options: WorkflowRunOptions): Promise<PerfSink | undefined> {
75
- const enabled = options.perf ?? process.env.PI_WORKFLOW_PERF === "1";
76
- if (!enabled) return undefined;
97
+ async function createInvocationPerf(options: ResolvedWorkflowRunOptions): Promise<PerfSink | undefined> {
98
+ if (!options.perf) return undefined;
77
99
  const { createPerfRecorder } = await import("./src/perf.ts");
78
100
  return createPerfRecorder(true);
79
101
  }
@@ -81,7 +103,7 @@ async function createInvocationPerf(options: WorkflowRunOptions): Promise<PerfSi
81
103
  /**
82
104
  * Resolve an `api.workflow()` reference to a registered workflow module. Throws on an unknown name.
83
105
  */
84
- export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Promise<WorkflowModule> {
106
+ export async function resolveWorkflowRef(ref: WorkflowRef, perf?: PerfSink): Promise<LoadedWorkflow> {
85
107
  const { discoverWorkflows } = await loadDiscovery();
86
108
  const workflows = await discoverWorkflows(EXTENSION_DIR, { perf });
87
109
  const mod = workflows.get(ref);
@@ -182,15 +204,21 @@ export interface ActiveWorkflowInspection {
182
204
  readonly snapshot: () => WorkflowProgressSnapshot;
183
205
  }
184
206
 
185
- let lastWorkflowInspection: LastWorkflowInspection | undefined;
186
- let activeWorkflowInspection: ActiveWorkflowInspection | undefined;
207
+ interface SessionWorkflowInspections {
208
+ last?: LastWorkflowInspection;
209
+ active?: ActiveWorkflowInspection;
210
+ }
211
+
212
+ const workflowInspections = new WeakMap<ExtensionAPI, Map<string, SessionWorkflowInspections>>();
213
+ let latestWorkflowInspection: LastWorkflowInspection | undefined;
214
+ let latestActiveWorkflowInspection: ActiveWorkflowInspection | undefined;
187
215
 
188
216
  export function getLastWorkflowInspection(): LastWorkflowInspection | undefined {
189
- return lastWorkflowInspection;
217
+ return latestWorkflowInspection;
190
218
  }
191
219
 
192
220
  export function getActiveWorkflowInspection(): ActiveWorkflowInspection | undefined {
193
- return activeWorkflowInspection;
221
+ return latestActiveWorkflowInspection;
194
222
  }
195
223
 
196
224
  export async function openWorkflowInspector(ctx: ExtensionContext, inspection: LastWorkflowInspection | ActiveWorkflowInspection): Promise<void> {
@@ -200,12 +228,22 @@ export async function openWorkflowInspector(ctx: ExtensionContext, inspection: L
200
228
  );
201
229
  }
202
230
 
203
- async function openAvailableWorkflowInspector(ctx: ExtensionContext): Promise<void> {
231
+ function workflowInspectionState(pi: ExtensionAPI, ctx: ExtensionContext): SessionWorkflowInspections {
232
+ const sessions = workflowInspections.get(pi) ?? new Map<string, SessionWorkflowInspections>();
233
+ const key = dynamaxSessionKey(ctx);
234
+ const state = sessions.get(key) ?? {};
235
+ sessions.set(key, state);
236
+ workflowInspections.set(pi, sessions);
237
+ return state;
238
+ }
239
+
240
+ async function openAvailableWorkflowInspector(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
204
241
  if (!ctx.hasUI) {
205
242
  ctx.ui.notify("Workflow inspector requires the TUI", "warning");
206
243
  return;
207
244
  }
208
- const inspection = activeWorkflowInspection ?? lastWorkflowInspection;
245
+ const state = workflowInspectionState(pi, ctx);
246
+ const inspection = state.active ?? state.last;
209
247
  if (!inspection) {
210
248
  ctx.ui.notify("No workflow inspector is available yet", "warning");
211
249
  return;
@@ -362,6 +400,7 @@ Always pass a plain string as the first api.agent() argument; build prompts with
362
400
  If using \`isolation: "worktree"\`, remember api.agent() returns \`{ result, patch, changed }\`; read \`.result\` for the agent answer and \`.patch\` for the diff.
363
401
  When the run is budgeted, guard expensive loops with \`while (api.budget.total && api.budget.remaining() > N) { ... }\`; api.agent() throws once the budget is spent.
364
402
  Subagents receive no skills by default. When the brief asks for a skill or a stage clearly benefits from one, pass \`skills: ["skill-name"]\` on that agent call only.
403
+ ${ADAPTIVE_WORKFLOW_GUIDANCE}
365
404
  Do not edit files unless the user explicitly requested edits.`;
366
405
  }
367
406
 
@@ -425,76 +464,127 @@ export async function pickWorkflow(
425
464
 
426
465
  export async function sendWorkflowResult(
427
466
  pi: ExtensionAPI,
428
- ctx: ExtensionCommandContext,
467
+ ctx: ExtensionContext,
429
468
  name: string,
430
- mod: WorkflowModule,
469
+ mod: LoadedWorkflow,
431
470
  args: string,
432
471
  options: WorkflowRunOptions,
433
472
  perfRecorder?: PerfSink,
473
+ reviewSessions: ReviewSessionCoordinator = createReviewSessionCoordinator(pi),
434
474
  ): Promise<void> {
435
- const { runWorkflow } = await loadEngine();
436
- let perfSnapshot: PerfSnapshot | undefined;
437
- let usageSnapshot: WorkflowUsageSnapshot | undefined;
438
- let runMetadata: WorkflowRunMetadata | undefined;
475
+ await sendResolvedWorkflowResult(
476
+ pi,
477
+ ctx,
478
+ name,
479
+ mod,
480
+ args,
481
+ resolveWorkflowRunOptions(options),
482
+ perfRecorder,
483
+ reviewSessions,
484
+ );
485
+ }
486
+
487
+ async function sendResolvedWorkflowResult(
488
+ pi: ExtensionAPI,
489
+ ctx: ExtensionContext,
490
+ name: string,
491
+ mod: LoadedWorkflow,
492
+ args: string,
493
+ options: ResolvedWorkflowRunOptions,
494
+ perfRecorder: PerfSink | undefined,
495
+ reviewSessions: ReviewSessionCoordinator,
496
+ ): Promise<void> {
497
+ const execution = await executeResolvedWorkflow(pi, ctx, name, mod, args, options, perfRecorder);
498
+ reviewSessions.remember(ctx, execution, options);
499
+ sendWorkflowExecution(pi, execution);
500
+ await reviewSessions.present(ctx, execution, options);
501
+ }
502
+
503
+ async function executeResolvedWorkflow(
504
+ pi: ExtensionAPI,
505
+ ctx: ExtensionContext,
506
+ name: string,
507
+ mod: LoadedWorkflow,
508
+ args: string,
509
+ options: ResolvedWorkflowRunOptions,
510
+ perfRecorder?: PerfSink,
511
+ ): Promise<WorkflowExecution> {
512
+ const { runResolvedWorkflow } = await loadEngine();
439
513
  let liveInspection: ActiveWorkflowInspection | undefined;
440
- const result = await runWorkflow(ctx, mod, args, {
441
- ...options,
442
- perf: options.perf ?? perfRecorder !== undefined,
514
+ const inspections = workflowInspectionState(pi, ctx);
515
+ return await executeWorkflowInvocation({
516
+ ctx,
517
+ name,
518
+ mod,
519
+ args,
520
+ options,
443
521
  perfRecorder,
522
+ runResolvedWorkflow,
444
523
  resolveWorkflow: (ref) => resolveWorkflowRef(ref, perfRecorder),
445
524
  onProgressSource(source) {
446
525
  if (source) {
447
526
  liveInspection = bindActiveWorkflowInspection(name, args, source);
448
- activeWorkflowInspection = liveInspection;
449
- } else if (activeWorkflowInspection === liveInspection) {
450
- activeWorkflowInspection = undefined;
527
+ inspections.active = liveInspection;
528
+ latestActiveWorkflowInspection = liveInspection;
529
+ } else if (inspections.active === liveInspection) {
530
+ inspections.active = undefined;
531
+ if (latestActiveWorkflowInspection === liveInspection) latestActiveWorkflowInspection = undefined;
451
532
  liveInspection = undefined;
452
533
  }
453
- options.onProgressSource?.(source);
454
- },
455
- onPerfSnapshot(snapshot) {
456
- perfSnapshot = snapshot;
457
- options.onPerfSnapshot?.(snapshot);
458
- },
459
- onUsageSnapshot(snapshot) {
460
- usageSnapshot = snapshot;
461
- options.onUsageSnapshot?.(snapshot);
462
- },
463
- onRunMetadata(metadata) {
464
- runMetadata = metadata;
465
- options.onRunMetadata?.(metadata);
466
534
  },
467
535
  onProgressSnapshot(snapshot) {
468
- lastWorkflowInspection = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
469
- options.onProgressSnapshot?.(snapshot);
536
+ const completed = { name, args, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
537
+ inspections.last = completed;
538
+ latestWorkflowInspection = completed;
470
539
  },
471
540
  });
472
- const perf = compactPerfSnapshot(perfSnapshot);
473
- const reviewDecision = decideReviewResultsPresentation({
474
- workflowName: name,
475
- result,
476
- mode: extensionContextMode(ctx),
477
- hasUI: ctx.hasUI,
478
- resultViewer: options.resultViewer,
479
- invocationKind: "command",
480
- });
481
- const reviewAction = await maybeShowReviewResultsViewer(ctx, reviewDecision);
482
- if (reviewDecision.kind !== "send") {
483
- await handleReviewViewerAction(pi, ctx, reviewAction, reviewDecision.issues, reviewDecision.report.reviewContext);
484
- }
541
+ }
542
+
543
+ function sendWorkflowExecution(pi: ExtensionAPI, execution: WorkflowExecution): void {
544
+ const { name } = execution.envelope;
485
545
  pi.sendMessage(
486
546
  {
487
547
  customType: "workflow-result",
488
- content: formatMessageContent(name, result, usageSnapshot, perf, runMetadata),
548
+ content: formatMessageContent(
549
+ name,
550
+ execution.envelope.result,
551
+ execution.envelope.usage,
552
+ execution.envelope.perf,
553
+ execution.metadata,
554
+ ),
489
555
  display: true,
490
- details: workflowEnvelope(name, result, usageSnapshot, perf, runMetadata),
556
+ details: execution.envelope,
491
557
  },
492
558
  { triggerTurn: false },
493
559
  );
494
560
  }
495
561
 
496
- export default function workflowEngine(pi: ExtensionAPI): void {
497
- registerDynamax(pi, undefined, { openInspector: openAvailableWorkflowInspector });
562
+ function createReviewSessionCoordinator(pi: ExtensionAPI): ReviewSessionCoordinator {
563
+ return new ReviewSessionCoordinator(pi, {
564
+ async runFollowUp(ctx, workflow, options) {
565
+ const perfRecorder = await createInvocationPerf(options);
566
+ return await executeResolvedWorkflow(pi, ctx, workflow.meta.name, workflow, "", options, perfRecorder);
567
+ },
568
+ publish: (execution) => sendWorkflowExecution(pi, execution),
569
+ });
570
+ }
571
+
572
+ export default function workflowEngine(pi: ExtensionAPI, shortcuts: DynamaxShortcuts = resolveDynamaxShortcuts()): void {
573
+ const reviewSessions = createReviewSessionCoordinator(pi);
574
+ registerDynamax(pi, shortcuts, { openInspector: (ctx) => openAvailableWorkflowInspector(pi, ctx) });
575
+ pi.on("session_shutdown", (_event, ctx) => {
576
+ const key = dynamaxSessionKey(ctx);
577
+ workflowInspections.get(pi)?.delete(key);
578
+ reviewSessions.dispose(ctx);
579
+ });
580
+ if (shortcuts.results) {
581
+ pi.registerShortcut(shortcuts.results, {
582
+ description: "Open last code-review results",
583
+ handler: async (ctx) => {
584
+ await reviewSessions.reopen(ctx);
585
+ },
586
+ });
587
+ }
498
588
 
499
589
  pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
500
590
  const details = message.details;
@@ -515,7 +605,18 @@ export default function workflowEngine(pi: ExtensionAPI): void {
515
605
  ctx.ui.notify("Usage: /workflow:inspector [last]", "warning");
516
606
  return;
517
607
  }
518
- await openAvailableWorkflowInspector(ctx);
608
+ await openAvailableWorkflowInspector(pi, ctx);
609
+ },
610
+ });
611
+
612
+ pi.registerCommand("workflow:results", {
613
+ description: "Reopen the last code-review findings viewer",
614
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
615
+ if (args.trim()) {
616
+ ctx.ui.notify("Usage: /workflow:results", "warning");
617
+ return;
618
+ }
619
+ await reviewSessions.reopen(ctx);
519
620
  },
520
621
  });
521
622
 
@@ -528,7 +629,8 @@ export default function workflowEngine(pi: ExtensionAPI): void {
528
629
  ctx.ui.notify(`Invalid workflow option: ${direct.optionErrors.join("; ")}`, "warning");
529
630
  return;
530
631
  }
531
- const perfRecorder = await createInvocationPerf(direct.options);
632
+ const directOptions = resolveWorkflowRunOptions(direct.options);
633
+ const perfRecorder = await createInvocationPerf(directOptions);
532
634
  const { discoverWorkflows } = await loadDiscovery();
533
635
  const workflows = await discoverWorkflows(EXTENSION_DIR, { refresh: direct.refreshDiscovery, perf: perfRecorder });
534
636
  const available = [...workflows.keys()].join(", ") || "(none)";
@@ -550,7 +652,8 @@ export default function workflowEngine(pi: ExtensionAPI): void {
550
652
  return;
551
653
  }
552
654
 
553
- await sendWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, invocation.options, perfRecorder);
655
+ const effectiveOptions = invocation === direct ? directOptions : resolveWorkflowRunOptions(invocation.options);
656
+ await sendResolvedWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, effectiveOptions, perfRecorder, reviewSessions);
554
657
  },
555
658
  });
556
659
 
@@ -572,6 +675,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
572
675
  "When using `isolation: \"worktree\"`, `api.agent()` returns `{ result, patch, changed }`; use `.result` for the answer and `.patch` for the isolated diff.",
573
676
  "If an inline subagent needs grep/find/code-search helpers, use `tools: [\"read\", \"bash\", \"grep\", \"find\", \"ls\"]` plus `toolHints: [\"search\"]` so installed tools such as ast-grep, mgrep, ffgrep, or fffind are discovered dynamically.",
574
677
  "`api.budget` exposes `{ total, spent(), remaining() }` (output tokens). When the run is budgeted, scale fleets from `budget.total` and guard loops with `while (budget.total && budget.remaining() > N) { await api.agent(...) }`; `api.agent()` throws once the ceiling is reached.",
678
+ ADAPTIVE_WORKFLOW_GUIDANCE,
575
679
  "Every workflow tool call must provide exactly one of `name` or `script`, never both.",
576
680
  ],
577
681
  parameters: Type.Object({
@@ -623,7 +727,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
623
727
  };
624
728
  }
625
729
 
626
- const runOptions: WorkflowRunOptions = {
730
+ const runOptions = resolveWorkflowRunOptions({
627
731
  inspect: ctx.hasUI,
628
732
  concurrency: params.concurrency,
629
733
  parallelSubmissionLimit: params.parallelSubmissionLimit,
@@ -631,9 +735,9 @@ export default function workflowEngine(pi: ExtensionAPI): void {
631
735
  perf: params.perf,
632
736
  resumeFromRunId,
633
737
  signal,
634
- };
738
+ });
635
739
  const perfRecorder = await createInvocationPerf(runOptions);
636
- let mod: WorkflowModule;
740
+ let mod: LoadedWorkflow;
637
741
  let resultName: string;
638
742
 
639
743
  if (request.kind === "named") {
@@ -660,46 +764,21 @@ export default function workflowEngine(pi: ExtensionAPI): void {
660
764
  resultName = mod.meta.name;
661
765
  }
662
766
 
663
- const { runWorkflow } = await loadEngine();
664
- let perfSnapshot: PerfSnapshot | undefined;
665
- let usageSnapshot: WorkflowUsageSnapshot | undefined;
666
- let runMetadata: WorkflowRunMetadata | undefined;
667
- let liveInspection: ActiveWorkflowInspection | undefined;
668
767
  const resultArgs = params.args ?? "";
669
- const result = await runWorkflow(ctx, mod, resultArgs, {
670
- ...runOptions,
671
- perf: runOptions.perf ?? perfRecorder !== undefined,
672
- perfRecorder,
673
- resolveWorkflow: (ref) => resolveWorkflowRef(ref, perfRecorder),
674
- onProgressSource(source) {
675
- if (source) {
676
- liveInspection = bindActiveWorkflowInspection(resultName, resultArgs, source);
677
- activeWorkflowInspection = liveInspection;
678
- } else if (activeWorkflowInspection === liveInspection) {
679
- activeWorkflowInspection = undefined;
680
- liveInspection = undefined;
681
- }
682
- },
683
- onPerfSnapshot: (snapshot) => {
684
- perfSnapshot = snapshot;
685
- },
686
- onUsageSnapshot: (snapshot) => {
687
- usageSnapshot = snapshot;
688
- },
689
- onRunMetadata: (metadata) => {
690
- runMetadata = metadata;
691
- },
692
- onProgressSnapshot: (snapshot) => {
693
- // Record the run so /workflow:inspector can reopen it — tool-invoked (dynamax) runs were
694
- // previously uninspectable, unlike the /workflow command path.
695
- lastWorkflowInspection = { name: resultName, args: resultArgs, completedAt: snapshot.doneAt ?? Date.now(), snapshot };
696
- },
697
- });
698
- const perf = compactPerfSnapshot(perfSnapshot);
699
- const detailLines = formatWorkflowDetailLines({ usage: usageSnapshot, perf, metadata: runMetadata });
768
+ const execution = await executeResolvedWorkflow(pi, ctx, resultName, mod, resultArgs, runOptions, perfRecorder);
769
+ reviewSessions.remember(ctx, execution, runOptions);
700
770
  return {
701
- content: [{ type: "text", text: `${summarize(result)}${detailLines.length > 0 ? `\n\n${detailLines.join("\n")}` : ""}` }],
702
- details: workflowEnvelope(resultName, result, usageSnapshot, perf, runMetadata),
771
+ content: [{
772
+ type: "text",
773
+ text: formatMessageContent(
774
+ resultName,
775
+ execution.envelope.result,
776
+ execution.envelope.usage,
777
+ execution.envelope.perf,
778
+ execution.metadata,
779
+ ),
780
+ }],
781
+ details: execution.envelope,
703
782
  };
704
783
  },
705
784
  });
@@ -1,4 +1,5 @@
1
1
  import { Type, type Static } from "typebox";
2
+ import { Value } from "typebox/value";
2
3
 
3
4
  export const AdvisorySeveritySchema = Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")], {
4
5
  description: "Impact level of an advisory finding.",
@@ -53,6 +54,11 @@ export const AdvisoryReportSchema = Type.Object({
53
54
  nextSteps: Type.Array(Type.String({ description: "Concrete follow-up commands, inspections, or decisions." })),
54
55
  });
55
56
 
57
+ export const AdvisoryReportWithStatsSchema = Type.Object({
58
+ ...AdvisoryReportSchema.properties,
59
+ stats: Type.Optional(Type.Record(Type.String(), Type.Union([Type.String(), Type.Number()]))),
60
+ });
61
+
56
62
  export type AdvisorySeverity = Static<typeof AdvisorySeveritySchema>;
57
63
  export type AdvisoryConfidence = Static<typeof AdvisoryConfidenceSchema>;
58
64
  export type AdvisoryVerifierVerdict = Static<typeof AdvisoryVerifierVerdictSchema>;
@@ -62,3 +68,8 @@ export type AdvisoryCandidates = Static<typeof AdvisoryCandidatesSchema>;
62
68
  export type AdvisoryVerdict = Static<typeof AdvisoryVerdictSchema>;
63
69
  export type AdvisoryFinding = Static<typeof AdvisoryFindingSchema>;
64
70
  export type AdvisoryReport = Static<typeof AdvisoryReportSchema>;
71
+ export type AdvisoryReportWithStats = Static<typeof AdvisoryReportWithStatsSchema>;
72
+
73
+ export function isAdvisoryReport(value: unknown): value is AdvisoryReportWithStats {
74
+ return Value.Check(AdvisoryReportWithStatsSchema, value);
75
+ }
@@ -0,0 +1,145 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { assertWorkflowBudgetAvailable } from "./budget.ts";
3
+ import {
4
+ type AgentExecutionOptions,
5
+ type AgentRunTags,
6
+ type RunContext,
7
+ } from "./agent-runner-types.ts";
8
+ import {
9
+ captureReplayIdentity,
10
+ captureIsolatedRepositoryAfterSetup,
11
+ captureSharedRepositoryBeforeSetup,
12
+ isReplayEnabled,
13
+ lookupReplayResult,
14
+ validateReplayEvidence,
15
+ validateReplayIdentity,
16
+ type AgentAttemptResult,
17
+ type AgentReplayEvidence,
18
+ type AgentReplayPlan,
19
+ } from "./agent-replay.ts";
20
+ import { openAgentSession, promptAgentSession, type AgentSessionHandle } from "./agent-session.ts";
21
+ import {
22
+ createAgentWorkspace,
23
+ disposeAgentWorkspace,
24
+ type AgentWorkspace,
25
+ } from "./agent-workspace.ts";
26
+ import type { AgentResumeBaseContext, AgentResumeContext, RepositoryResumeContext } from "./resume-context.ts";
27
+ import { captureRepositoryMutationGuard } from "./resume-context.ts";
28
+
29
+ /** Execute one fully bracketed workspace/session attempt. Cleanup always precedes settlement. */
30
+ export async function executeAgentAttempt(input: {
31
+ readonly rc: RunContext;
32
+ readonly prompt: string;
33
+ readonly opts: AgentExecutionOptions;
34
+ readonly resumeBaseContext: AgentResumeBaseContext;
35
+ readonly model: Model<Api> | undefined;
36
+ readonly replay: AgentReplayPlan;
37
+ readonly label: string;
38
+ readonly rowId: number;
39
+ readonly tags: AgentRunTags;
40
+ }): Promise<AgentAttemptResult> {
41
+ const { rc, prompt, opts, resumeBaseContext, model, replay, label, rowId, tags } = input;
42
+ let repositoryBefore: RepositoryResumeContext | undefined;
43
+ let evidence: AgentReplayEvidence | undefined;
44
+ if (isReplayEnabled(replay)) {
45
+ if (replay.kind === "isolated") {
46
+ const mutationGuard = await captureRepositoryMutationGuard(rc.cwd, rc.signal);
47
+ if (mutationGuard.kind === "unverifiable") {
48
+ repositoryBefore = mutationGuard;
49
+ } else {
50
+ evidence = { kind: "isolated", mutationGuard: mutationGuard.fingerprint };
51
+ }
52
+ } else {
53
+ repositoryBefore = await captureSharedRepositoryBeforeSetup(rc, prompt, opts, replay);
54
+ evidence = { kind: "shared" };
55
+ }
56
+ }
57
+ let workspace: AgentWorkspace | undefined;
58
+ let handle: AgentSessionHandle | undefined;
59
+
60
+ try {
61
+ workspace = await createAgentWorkspace(rc, opts, label);
62
+ if (replay.kind === "isolated" && evidence?.kind === "isolated") {
63
+ if (workspace.kind !== "isolated") throw new Error("Isolated replay created a shared workspace.");
64
+ repositoryBefore = await captureIsolatedRepositoryAfterSetup(rc, prompt, opts, workspace);
65
+ }
66
+ handle = await openAgentSession({ rc, prompt, opts, cwd: workspace.cwd, model, label, tags });
67
+
68
+ let identity: AgentResumeContext | undefined;
69
+ if (isReplayEnabled(replay) && repositoryBefore && evidence) {
70
+ const capture = await captureReplayIdentity({
71
+ rc,
72
+ base: resumeBaseContext,
73
+ repository: repositoryBefore,
74
+ selectedSkills: handle.selectedSkills,
75
+ session: handle.session,
76
+ sessionCwd: workspace.cwd,
77
+ });
78
+ if (capture.kind === "unverifiable") {
79
+ rc.progress.log(`${label}: resume disabled for this call (${capture.reason})`);
80
+ } else {
81
+ identity = capture.identity;
82
+ const cached = await lookupReplayResult({ rc, key: replay.key, identity, opts, workspace });
83
+ if (cached.hit) {
84
+ const contract = await validateReplayIdentity({
85
+ rc,
86
+ identity,
87
+ selectedSkills: handle.selectedSkills,
88
+ session: handle.session,
89
+ sessionCwd: workspace.cwd,
90
+ replay,
91
+ workspace,
92
+ });
93
+ const validation = contract.ok
94
+ ? await validateReplayEvidence(rc, evidence)
95
+ : contract;
96
+ if (validation.ok) {
97
+ return { kind: "cache-hit", result: cached.result, identity, evidence };
98
+ }
99
+ rc.progress.log(`${label}: cached result invalidated (${validation.reason})`);
100
+ } else if (cached.reason) {
101
+ rc.progress.log(`${label}: cached result invalidated (${cached.reason})`);
102
+ }
103
+ }
104
+ }
105
+
106
+ assertWorkflowBudgetAvailable(rc.budget);
107
+ const rawResult = await promptAgentSession({
108
+ rc,
109
+ handle,
110
+ prompt,
111
+ opts,
112
+ label,
113
+ rowId,
114
+ tags,
115
+ });
116
+ const result = await workspace.wrapResult(rawResult);
117
+ if (!identity) return { kind: "live-unrecordable", result };
118
+ if (!isReplayEnabled(replay) || !evidence) throw new Error("Replay identity produced without complete replay evidence.");
119
+
120
+ const contract = await validateReplayIdentity({
121
+ rc,
122
+ identity,
123
+ selectedSkills: handle.selectedSkills,
124
+ session: handle.session,
125
+ sessionCwd: workspace.cwd,
126
+ replay,
127
+ workspace,
128
+ });
129
+ const validation = contract.ok
130
+ ? await validateReplayEvidence(rc, evidence)
131
+ : contract;
132
+ if (validation.ok) {
133
+ return { kind: "live-recordable", result, identity, evidence };
134
+ }
135
+ rc.progress.log(`${label}: read-only resume contract was not recorded (${validation.reason})`);
136
+ return { kind: "live-unrecordable", result };
137
+ } finally {
138
+ try {
139
+ const session = handle?.session;
140
+ if (session) rc.perf.timeSync("agent.dispose_ms", () => session.dispose(), tags);
141
+ } finally {
142
+ await disposeAgentWorkspace(rc, label, workspace);
143
+ }
144
+ }
145
+ }