pi-extensible-workflows 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -1,22 +1,22 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import { fork } from "node:child_process";
2
+ import { fork, spawn } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { basename, dirname, extname, join, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
+ import { StringDecoder } from "node:string_decoder";
8
9
  import * as acorn from "acorn";
9
10
  import { Script } from "node:vm";
10
11
  import { Type } from "@earendil-works/pi-ai";
11
12
  import { Value } from "typebox/value";
12
- import { copyToClipboard, getAgentDir, parseFrontmatter, highlightCode, SessionManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
13
+ import { copyToClipboard, getAgentDir, parseFrontmatter, highlightCode, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
13
14
  import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
14
- import { transcriptLines } from "./session-inspector.js";
15
- import { acquireSessionLease, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
15
+ import { herdrPaneId, openHerdrPane } from "./herdr.js";
16
+ import { acquireSessionLease, atomicWriteFile, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
16
17
  export const RUN_STATES = ["queued", "running", "pausing", "paused", "awaiting_input", "completed", "failed", "stopped", "interrupted", "budget_exhausted"];
17
18
  export const AGENT_STATES = ["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"];
18
- export const WORKFLOW_ASYNC_STARTED_EVENT = "workflow:async-started";
19
- export const WORKFLOW_ASYNC_COMPLETE_EVENT = "workflow:async-complete";
19
+ export const WORKFLOW_CALL_KINDS = ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree", "shell"];
20
20
  export const WORKFLOW_RUN_STARTED_EVENT = "workflow:run-started";
21
21
  export const WORKFLOW_RUN_RESUMED_EVENT = "workflow:run-resumed";
22
22
  export const WORKFLOW_RUN_STATE_CHANGED_EVENT = "workflow:run-state-changed";
@@ -30,10 +30,10 @@ export const WORKFLOW_WORKTREE_CREATED_EVENT = "workflow:worktree-created";
30
30
  const SETTLED_AGENT_STATES = new Set(["completed", "failed", "cancelled"]);
31
31
  export const ERROR_CODES = [
32
32
  "CONFIG_ERROR", "INVALID_SETTINGS", "INVALID_SYNTAX", "INVALID_METADATA", "DUPLICATE_NAME", "INVALID_SCHEMA", "UNKNOWN_MODEL", "UNKNOWN_TOOL", "UNKNOWN_AGENT_TYPE",
33
- "RUN_OWNED", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID",
33
+ "RUN_OWNED", "REGISTRY_FROZEN", "GLOBAL_COLLISION", "MISSING_WORKFLOW", "RPC_LIMIT_EXCEEDED", "SHELL_FAILED", "AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID",
34
34
  "CANCELLED", "WORKER_UNRESPONSIVE", "WORKTREE_FAILED", "RESUME_INCOMPATIBLE", "BUDGET_EXHAUSTED", "INTERNAL_ERROR",
35
35
  ];
36
- export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 3;
36
+ export const LAUNCH_SNAPSHOT_IDENTITY_VERSION = 4;
37
37
  export class WorkflowError extends Error {
38
38
  code;
39
39
  constructor(code, message) {
@@ -42,6 +42,7 @@ export class WorkflowError extends Error {
42
42
  this.name = "WorkflowError";
43
43
  }
44
44
  }
45
+ const WORKFLOW_FAILURE_DIAGNOSTICS = Symbol("workflowFailureDiagnostics");
45
46
  const WORKFLOW_AUTHORED_ERROR = Symbol("workflowAuthoredError");
46
47
  function errorText(error) { return error && typeof error === "object" && typeof error.message === "string" ? error.message : error instanceof Error ? error.message : String(error); }
47
48
  function errorCode(error) {
@@ -71,12 +72,13 @@ const WORKFLOW_ERROR_PROSE = {
71
72
  INVALID_SCHEMA: (detail) => `The workflow schema is invalid: ${detail}.`,
72
73
  REGISTRY_FROZEN: (detail) => `Workflow extension registration is closed: ${detail}.`,
73
74
  GLOBAL_COLLISION: (detail) => `The workflow global name is already in use: ${detail}.`,
74
- MISSING_WORKFLOW: (detail) => `The workflow primitive is unavailable: ${detail}.`,
75
+ MISSING_WORKFLOW: (detail) => `The registered workflow function is unavailable: ${detail}.`,
75
76
  UNKNOWN_MODEL: (detail) => `The workflow requested the unavailable model ${detail.replace(/^(?:Unknown model(?: for role [^:]+)?|Invalid model spec):\s*/, "")}.`,
76
77
  UNKNOWN_TOOL: (detail) => `The workflow requested the unavailable tool ${detail.replace(/^Unknown tool:\s*/, "")}.`,
77
78
  UNKNOWN_AGENT_TYPE: (detail) => `The workflow requested the unavailable agent role ${detail.replace(/^Unknown agent role:\s*/, "")}.`,
78
79
  RUN_OWNED: (detail) => /already owned|active ownership/.test(detail) ? "The workflow session is already in use." : `The workflow session is already in use: ${detail}.`,
79
80
  RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
81
+ SHELL_FAILED: (detail) => `The workflow shell command failed: ${detail}.`,
80
82
  AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
81
83
  AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
82
84
  RESULT_INVALID: (detail) => `The workflow produced an invalid result: ${detail}.`,
@@ -188,6 +190,7 @@ export class RunLifecycle {
188
190
  export const DEFAULT_SETTINGS = Object.freeze({ concurrency: 8 });
189
191
  function fail(code, message) { throw new WorkflowError(code, message); }
190
192
  function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
193
+ export { object as isObject };
191
194
  function nonNegativeInteger(value) { return Number.isInteger(value) && value >= 0; }
192
195
  function nonNegativeFinite(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
193
196
  export function validateBudget(value) {
@@ -562,9 +565,7 @@ export function saveModelAliases(path = workflowSettingsPath(), aliases = {}) {
562
565
  throw error;
563
566
  }
564
567
  mkdirSync(dirname(path), { recursive: true });
565
- const temporary = `${path}.${String(process.pid)}.${randomUUID()}.tmp`;
566
- writeFileSync(temporary, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, { mode: 0o600 });
567
- renameSync(temporary, path);
568
+ atomicWriteFile(path, `${JSON.stringify({ ...parsed, modelAliases: normalized }, null, 2)}\n`, true);
568
569
  }
569
570
  export function parseRoleMarkdown(content, strict = false, rolePath) {
570
571
  if (!strict) {
@@ -700,20 +701,32 @@ function parseWorkflow(script) {
700
701
  function astNode(value) {
701
702
  return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
702
703
  }
704
+ function astChildren(node) {
705
+ const children = [];
706
+ for (const value of Object.values(node)) {
707
+ if (Array.isArray(value)) {
708
+ for (const child of value)
709
+ if (astNode(child))
710
+ children.push(child);
711
+ }
712
+ else if (astNode(value))
713
+ children.push(value);
714
+ }
715
+ return children;
716
+ }
717
+ function workflowCallKind(node) {
718
+ if (node.type !== "CallExpression" || node.callee.type !== "Identifier")
719
+ return undefined;
720
+ const kind = node.callee.name;
721
+ return WORKFLOW_CALL_KINDS.includes(kind) ? kind : undefined;
722
+ }
703
723
  function workflowCalls(program) {
704
724
  const calls = [];
705
725
  const visit = (node) => {
706
- if (node.type === "CallExpression" && node.callee.type === "Identifier" && ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"].includes(node.callee.name))
726
+ if (workflowCallKind(node))
707
727
  calls.push(node);
708
- for (const value of Object.values(node)) {
709
- if (Array.isArray(value)) {
710
- for (const child of value)
711
- if (astNode(child))
712
- visit(child);
713
- }
714
- else if (astNode(value))
715
- visit(value);
716
- }
728
+ for (const child of astChildren(node))
729
+ visit(child);
717
730
  };
718
731
  visit(program);
719
732
  return calls.sort((left, right) => left.start - right.start);
@@ -728,9 +741,9 @@ function workflowCallsWithStructure(program) {
728
741
  if (scope?.key === null && key)
729
742
  current = { ...current, structure: [...current.structure.slice(0, -1), { ...scope, key }] };
730
743
  }
731
- if (node.type === "CallExpression" && node.callee.type === "Identifier" && ["agent", "conversation", "parallel", "pipeline", "checkpoint", "phase", "withWorktree"].includes(node.callee.name)) {
744
+ const operation = workflowCallKind(node);
745
+ if (operation) {
732
746
  const call = node;
733
- const operation = call.callee.name;
734
747
  const execution = operation === "parallel" ? "parallel" : operation === "pipeline" ? "sequential" : current.execution;
735
748
  calls.push({ call, execution, structure: current.structure });
736
749
  for (const [index, argument] of call.arguments.entries()) {
@@ -741,15 +754,8 @@ function workflowCallsWithStructure(program) {
741
754
  }
742
755
  return;
743
756
  }
744
- for (const value of Object.values(node)) {
745
- if (Array.isArray(value)) {
746
- for (const child of value)
747
- if (astNode(child))
748
- visit(child, current);
749
- }
750
- else if (astNode(value))
751
- visit(value, current);
752
- }
757
+ for (const child of astChildren(node))
758
+ visit(child, current);
753
759
  };
754
760
  visit(program, { execution: "sequential", structure: [] });
755
761
  return calls.sort((left, right) => left.call.start - right.call.start);
@@ -762,26 +768,20 @@ function validateDirectPrimitiveReferences(program, name) {
762
768
  if (!directCall && !propertyKey)
763
769
  fail("INVALID_METADATA", `${name} calls must use a direct ${name}(...) call; aliases and indirect calls are unsupported`);
764
770
  }
765
- for (const value of Object.values(node)) {
766
- if (Array.isArray(value)) {
767
- for (const child of value)
768
- if (astNode(child))
769
- visit(child, node);
770
- }
771
- else if (astNode(value))
772
- visit(value, node);
773
- }
771
+ for (const child of astChildren(node))
772
+ visit(child, node);
774
773
  };
775
774
  visit(program);
776
775
  }
777
776
  function hasIdentifier(node, name) {
778
777
  if (node.type === "Identifier" && node.name === name)
779
778
  return true;
780
- return Object.values(node).some((value) => Array.isArray(value) ? value.some((child) => astNode(child) && hasIdentifier(child, name)) : astNode(value) && hasIdentifier(value, name));
779
+ return astChildren(node).some((child) => hasIdentifier(child, name));
781
780
  }
782
781
  const INTERNAL_AGENT_NAME = "__pi_extensible_workflows_agent";
783
782
  const INTERNAL_CONVERSATION_NAME = "__pi_extensible_workflows_conversation";
784
783
  const INTERNAL_WORKTREE_NAME = "__pi_extensible_workflows_withWorktree";
784
+ const INTERNAL_SHELL_NAME = "__pi_extensible_workflows_shell";
785
785
  function callHasTrailingComma(source, call) {
786
786
  let previous;
787
787
  let current;
@@ -802,12 +802,14 @@ function instrumentWorkflow(script) {
802
802
  fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
803
803
  if (hasIdentifier(program, INTERNAL_WORKTREE_NAME))
804
804
  fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
805
- const calls = workflowCalls(program).filter((call) => ["agent", "conversation", "withWorktree"].includes(call.callee.name));
805
+ if (hasIdentifier(program, INTERNAL_SHELL_NAME))
806
+ fail("INVALID_METADATA", `${INTERNAL_SHELL_NAME} is reserved for workflow shell instrumentation`);
807
+ const calls = workflowCalls(program).filter((call) => ["agent", "conversation", "withWorktree", "shell"].includes(call.callee.name));
806
808
  const edits = calls.flatMap((call) => {
807
809
  const callSite = `${String(call.start)}:${String(call.end)}`;
808
810
  const hiddenArgument = call.arguments.length === 0 || callHasTrailingComma(body, call) ? "" : ", ";
809
811
  return [
810
- { start: call.callee.start, end: call.callee.end, text: call.callee.name === "agent" ? INTERNAL_AGENT_NAME : call.callee.name === "conversation" ? INTERNAL_CONVERSATION_NAME : INTERNAL_WORKTREE_NAME },
812
+ { start: call.callee.start, end: call.callee.end, text: call.callee.name === "agent" ? INTERNAL_AGENT_NAME : call.callee.name === "conversation" ? INTERNAL_CONVERSATION_NAME : call.callee.name === "withWorktree" ? INTERNAL_WORKTREE_NAME : INTERNAL_SHELL_NAME },
811
813
  { start: call.end - 1, end: call.end - 1, text: `${hiddenArgument}${JSON.stringify(callSite)}` },
812
814
  ];
813
815
  }).sort((left, right) => right.start - left.start);
@@ -946,6 +948,23 @@ function validateAgentOptions(value) {
946
948
  fail("INVALID_METADATA", "Role agents must not specify model, thinking, or tools");
947
949
  return value;
948
950
  }
951
+ const SHELL_OPTION_KEYS = new Set(["timeoutMs", "env"]);
952
+ function validateShellOptions(value) {
953
+ if (value === undefined)
954
+ return {};
955
+ if (!object(value) || !jsonValue(value) || Object.keys(value).some((key) => !SHELL_OPTION_KEYS.has(key)))
956
+ fail("INVALID_METADATA", "shell options must contain only timeoutMs and env");
957
+ if (value.timeoutMs !== undefined && !positiveInteger(value.timeoutMs))
958
+ fail("INVALID_METADATA", "shell timeoutMs must be a positive integer");
959
+ if (value.env !== undefined && (!object(value.env) || Object.values(value.env).some((entry) => typeof entry !== "string")))
960
+ fail("INVALID_METADATA", "shell env must be an object of strings");
961
+ return { ...(value.timeoutMs === undefined ? {} : { timeoutMs: value.timeoutMs }), ...(value.env === undefined ? {} : { env: value.env }) };
962
+ }
963
+ function validateShellCommand(value) {
964
+ if (typeof value !== "string")
965
+ fail("INVALID_METADATA", "shell command must be a string");
966
+ return value;
967
+ }
949
968
  function staticValue(node) {
950
969
  if (!node)
951
970
  return { known: false };
@@ -1011,6 +1030,8 @@ export function inspectWorkflowScript(script) {
1011
1030
  }
1012
1031
  if (kind === "checkpoint")
1013
1032
  return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null, role: null };
1033
+ if (kind === "shell")
1034
+ return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
1014
1035
  return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
1015
1036
  });
1016
1037
  }
@@ -1026,6 +1047,18 @@ function validateStaticAgentOptions(node, aliases = {}, knownModels, settingsPat
1026
1047
  validateAgentOption(key, value.value, aliases, knownModels, settingsPath);
1027
1048
  }
1028
1049
  }
1050
+ function validateStaticShellOptions(call) {
1051
+ if (call.arguments.some((argument) => argument.type === "SpreadElement"))
1052
+ return;
1053
+ if (call.arguments.length !== 1 && call.arguments.length !== 2)
1054
+ fail("INVALID_METADATA", "shell requires a command string and optional options");
1055
+ const command = staticValue(callArgument(call, 0));
1056
+ if (command.known)
1057
+ validateShellCommand(command.value);
1058
+ const options = staticValue(callArgument(call, 1));
1059
+ if (options.known)
1060
+ validateShellOptions(options.value);
1061
+ }
1029
1062
  function validateStaticWithWorktree(call) {
1030
1063
  if (call.arguments.some((argument) => argument.type === "SpreadElement"))
1031
1064
  return;
@@ -1040,13 +1073,12 @@ function validateStaticWithWorktree(call) {
1040
1073
  fail("INVALID_METADATA", "withWorktree name must be a non-empty string");
1041
1074
  }
1042
1075
  }
1043
- const RESERVED_GLOBALS = new Set(["agent", "conversation", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
1076
+ const RESERVED_GLOBALS = new Set(["agent", "conversation", "shell", "prompt", "checkpoint", "parallel", "pipeline", "phase", "withWorktree", "log", "args", "Promise", "JSON", "Math", "Date", "eval", "Function", "WebAssembly", "process", "require", "module", "exports", "console", "fetch", "XMLHttpRequest", "WebSocket", "performance", "crypto", "setTimeout", "setInterval", "setImmediate", "queueMicrotask", "Intl", "SharedArrayBuffer", "Atomics", "globalThis", "global", "undefined", "NaN", "Infinity", "extensions", "workflow_catalog"]);
1044
1077
  const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
1045
1078
  const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
1046
1079
  export class WorkflowRegistry {
1047
1080
  #extensions = new Set();
1048
1081
  #globals = new Map();
1049
- #workflows = new Map();
1050
1082
  #hooks = new Map();
1051
1083
  #frozen = false;
1052
1084
  get frozen() { return this.#frozen; }
@@ -1054,14 +1086,15 @@ export class WorkflowRegistry {
1054
1086
  register(extension) {
1055
1087
  if (this.#frozen)
1056
1088
  fail("REGISTRY_FROZEN", "Workflow extension registration is closed after session_start");
1057
- if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "workflows", "agentSetupHooks"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
1089
+ if (object(extension) && Object.prototype.hasOwnProperty.call(extension, "workflows"))
1090
+ fail("INVALID_METADATA", "Separate registered workflow definitions were removed; register a function with input and output schemas instead");
1091
+ if (!object(extension) || Object.keys(extension).some((key) => !["version", "headline", "description", "functions", "variables", "agentSetupHooks"].includes(key)) || typeof extension.version !== "string" || !SEMVER.test(extension.version) || typeof extension.headline !== "string" || !extension.headline.trim() || typeof extension.description !== "string" || !extension.description.trim())
1058
1092
  fail("INVALID_METADATA", "Workflow extensions require a semantic version, headline, and description");
1059
1093
  const functions = extension.functions ?? {};
1060
1094
  const variables = extension.variables ?? {};
1061
- const workflows = extension.workflows ?? {};
1062
1095
  const agentSetupHooks = extension.agentSetupHooks ?? {};
1063
- if (!object(functions) || !object(variables) || !object(workflows) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(workflows).length === 0 && Object.keys(agentSetupHooks).length === 0))
1064
- fail("INVALID_METADATA", "Workflow extensions require functions, variables, workflows, or agent setup hooks");
1096
+ if (!object(functions) || !object(variables) || !object(agentSetupHooks) || (Object.keys(functions).length === 0 && Object.keys(variables).length === 0 && Object.keys(agentSetupHooks).length === 0))
1097
+ fail("INVALID_METADATA", "Workflow extensions require functions, variables, or agent setup hooks");
1065
1098
  const names = [...Object.keys(functions), ...Object.keys(variables)];
1066
1099
  if (new Set(names).size !== names.length)
1067
1100
  fail("GLOBAL_COLLISION", "Global name collision inside extension");
@@ -1086,50 +1119,38 @@ export class WorkflowRegistry {
1086
1119
  fail("INVALID_METADATA", `Invalid workflow variable: ${name}`);
1087
1120
  validateSchema(variable.schema, `${name} schema`);
1088
1121
  }
1089
- for (const [name, workflow] of Object.entries(workflows)) {
1090
- if (!IDENTIFIER.test(name) || !object(workflow) || Object.keys(workflow).some((key) => !["description", "script"].includes(key)) || typeof workflow.description !== "string" || !workflow.description.trim() || typeof workflow.script !== "string" || !workflow.script.trim())
1091
- fail("INVALID_METADATA", `Invalid workflow script: ${name}`);
1092
- if (this.#workflows.has(name))
1093
- fail("DUPLICATE_NAME", `Reusable workflow already registered: ${name}`);
1094
- parseWorkflow(workflow.script);
1095
- }
1096
1122
  for (const [name, hook] of Object.entries(agentSetupHooks)) {
1097
1123
  if (!IDENTIFIER.test(name) || !object(hook) || Object.keys(hook).some((key) => !["priority", "setup"].includes(key)) || typeof hook.setup !== "function" || hook.priority !== undefined && (typeof hook.priority !== "number" || !Number.isFinite(hook.priority)))
1098
1124
  fail("INVALID_METADATA", `Invalid agent setup hook: ${name}`);
1099
1125
  if (this.#hooks.has(name))
1100
1126
  fail("DUPLICATE_NAME", `Agent setup hook already registered: ${name}`);
1101
1127
  }
1102
- const stored = deepFreeze({ ...extension, functions, variables, workflows, agentSetupHooks });
1128
+ const stored = deepFreeze({ ...extension, functions, variables, agentSetupHooks });
1103
1129
  this.#extensions.add(stored);
1104
1130
  for (const name of names)
1105
1131
  this.#globals.set(name, name);
1106
- for (const [name, workflow] of Object.entries(workflows))
1107
- this.#workflows.set(name, workflow);
1108
1132
  for (const [name, hook] of Object.entries(agentSetupHooks))
1109
1133
  this.#hooks.set(name, { name, priority: hook.priority ?? 10, setup: hook.setup });
1110
1134
  }
1111
- workflow(name) {
1135
+ function(name) {
1112
1136
  if (!IDENTIFIER.test(name))
1113
- fail("MISSING_WORKFLOW", `Registered workflows require an unqualified name: ${name}`);
1114
- const workflow = this.#workflows.get(name);
1115
- if (!workflow)
1116
- fail("MISSING_WORKFLOW", `Workflow is unavailable: ${name}`);
1117
- return workflow;
1137
+ fail("MISSING_WORKFLOW", `Registered functions require an unqualified name: ${name}`);
1138
+ const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
1139
+ if (!fn)
1140
+ fail("MISSING_WORKFLOW", `Registered function is unavailable: ${name}; the separate registered-workflow format was removed`);
1141
+ return fn;
1118
1142
  }
1119
- workflows() {
1120
- return Object.freeze(Object.fromEntries(this.#workflows));
1143
+ functions() {
1144
+ return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.entries(extension.functions ?? {}))));
1121
1145
  }
1122
1146
  catalog() {
1123
1147
  const functions = [];
1124
1148
  const variables = [];
1125
- const workflows = [];
1126
1149
  for (const extension of this.#extensions) {
1127
1150
  for (const [name, fn] of Object.entries(extension.functions ?? {}))
1128
1151
  functions.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: fn.description, input: structuredClone(fn.input), output: structuredClone(fn.output) });
1129
1152
  for (const [name, variable] of Object.entries(extension.variables ?? {}))
1130
1153
  variables.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: variable.description, schema: structuredClone(variable.schema) });
1131
- for (const [name, workflow] of Object.entries(extension.workflows ?? {}))
1132
- workflows.push({ name, version: extension.version, headline: extension.headline, extensionDescription: extension.description, description: workflow.description });
1133
1154
  }
1134
1155
  let aliases;
1135
1156
  try {
@@ -1139,15 +1160,28 @@ export class WorkflowRegistry {
1139
1160
  aliases = undefined;
1140
1161
  }
1141
1162
  const sort = (left, right) => left.name.localeCompare(right.name);
1142
- return deepFreeze({ functions: functions.sort(sort), variables: variables.sort(sort), workflows: workflows.sort(sort), ...(aliases ? { modelAliases: structuredClone(aliases) } : {}) });
1163
+ return deepFreeze({ functions: functions.sort(sort), variables: variables.sort(sort), ...(aliases ? { modelAliases: structuredClone(aliases) } : {}) });
1164
+ }
1165
+ catalogIndex() {
1166
+ const catalog = this.catalog();
1167
+ return deepFreeze({
1168
+ functions: catalog.functions.map(({ name, description, input }) => ({ name, description, input: structuredClone(input) })),
1169
+ variables: catalog.variables.map(({ name, description, schema }) => ({ name, description, schema: structuredClone(schema) })),
1170
+ ...(catalog.modelAliases ? { modelAliases: structuredClone(catalog.modelAliases) } : {}),
1171
+ });
1172
+ }
1173
+ catalogDetail(name) {
1174
+ const catalog = this.catalog();
1175
+ const entry = catalog.functions.find((candidate) => candidate.name === name) ?? catalog.variables.find((candidate) => candidate.name === name);
1176
+ if (entry)
1177
+ return entry;
1178
+ return deepFreeze({ error: { code: "NOT_FOUND", name, message: `No registered workflow function or variable is available: ${name}` } });
1143
1179
  }
1144
1180
  globals() {
1145
1181
  return Object.freeze(Object.fromEntries([...this.#extensions].flatMap((extension) => Object.keys(extension.functions ?? {}).map((name) => [name, { name }]))));
1146
1182
  }
1147
1183
  async invokeFunction(name, input, context, path, journal) {
1148
- const fn = [...this.#extensions].find((extension) => extension.functions?.[name])?.functions?.[name];
1149
- if (!fn)
1150
- fail("MISSING_WORKFLOW", `Workflow function is unavailable: ${name}`);
1184
+ const fn = this.function(name);
1151
1185
  if (!object(input) || !jsonValue(input) || !Value.Check(fn.input, input))
1152
1186
  fail("RESULT_INVALID", `Invalid input for ${name}`);
1153
1187
  const replayed = journal.get(path);
@@ -1156,7 +1190,7 @@ export class WorkflowRegistry {
1156
1190
  fail("RESULT_INVALID", `Invalid replay for ${name}`);
1157
1191
  return structuredClone(replayed);
1158
1192
  }
1159
- const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, agent: context.agent, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
1193
+ const result = await fn.run(deepFreeze(structuredClone(input)), Object.freeze({ run: context.run, invoke: context.invoke, agent: context.agent, shell: context.shell, prompt: context.prompt, parallel: context.parallel, pipeline: context.pipeline, withWorktree: context.withWorktree, checkpoint: context.checkpoint, phase: context.phase, log: context.log }));
1160
1194
  if (!jsonValue(result) || !Value.Check(fn.output, result))
1161
1195
  fail("RESULT_INVALID", `Invalid output from ${name}`);
1162
1196
  const stored = structuredClone(result);
@@ -1177,9 +1211,11 @@ function createWorkflowRegistryApi(registry) {
1177
1211
  get frozen() { return registry.frozen; },
1178
1212
  freeze: () => { registry.freeze(); },
1179
1213
  register: (extension) => { registry.register(extension); },
1180
- workflow: (name) => registry.workflow(name),
1181
- workflows: () => registry.workflows(),
1214
+ function: (name) => registry.function(name),
1215
+ functions: () => registry.functions(),
1182
1216
  catalog: () => registry.catalog(),
1217
+ catalogIndex: () => registry.catalogIndex(),
1218
+ catalogDetail: (name) => registry.catalogDetail(name),
1183
1219
  globals: () => registry.globals(),
1184
1220
  invokeFunction: (...args) => registry.invokeFunction(...args),
1185
1221
  variables: () => registry.variables(),
@@ -1200,25 +1236,28 @@ function loadingRegistry() { return workflowRegistryHost().api; }
1200
1236
  beginWorkflowExtensionLoading();
1201
1237
  export function registerWorkflowExtension(extension) { loadingRegistry().register(extension); }
1202
1238
  export function workflowCatalog() { return loadingRegistry().catalog(); }
1203
- export function registeredWorkflowDefinitions() { return loadingRegistry().workflows(); }
1239
+ export function workflowCatalogIndex() { return loadingRegistry().catalogIndex(); }
1240
+ export function workflowCatalogDetail(name) { return loadingRegistry().catalogDetail(name); }
1241
+ export function registeredWorkflowFunctions() { return loadingRegistry().functions(); }
1204
1242
  export function formatWorkflowPreview(args) {
1205
1243
  const name = typeof args.name === "string" && args.name.trim() ? args.name.trim() : typeof args.workflow === "string" && args.workflow.trim() ? args.workflow : "workflow";
1206
1244
  if (typeof args.script !== "string" || !args.script.trim())
1207
- return `workflow ${name}${typeof args.workflow === "string" ? "\nRegistered workflow" : ""}`;
1245
+ return `workflow ${name}${typeof args.workflow === "string" ? "\nRegistered function" : ""}`;
1208
1246
  return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
1209
1247
  }
1210
1248
  export const WORKFLOW_TOOL_LABEL = "Workflow";
1211
1249
  export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow";
1212
- export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow that orchestrates subagents. Inline scripts require a name; registered workflows use their workflow name. Runs in the background by default; completion arrives as a follow-up message.";
1250
+ export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow that orchestrates subagents. Inline scripts require a name; registered functions use their function name. Runs in the background by default; completion arrives as a follow-up message. Foreground results include the completed run ID, and parentRunId reuses matching named worktrees from a terminal run.";
1213
1251
  export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
1214
1252
  name: Type.Optional(Type.String({ description: "Workflow name for inline scripts" })),
1215
1253
  description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
1216
1254
  script: Type.Optional(Type.String({ description: "Immutable workflow source without metadata" })),
1217
- workflow: Type.Optional(Type.String({ description: "Registered reusable workflow as an unqualified name" })),
1255
+ workflow: Type.Optional(Type.String({ description: "Registered reusable function as an unqualified name" })),
1218
1256
  args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
1219
1257
  foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of running in the background" })),
1220
1258
  concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16 })),
1221
1259
  budget: Type.Optional(Type.Unknown({ description: "Optional aggregate soft and hard run budgets" })),
1260
+ parentRunId: Type.Optional(Type.String({ description: "Terminal run whose named worktrees may be reused" })),
1222
1261
  });
1223
1262
  function hasDynamicAgentRole(node) {
1224
1263
  if (!node)
@@ -1244,8 +1283,11 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
1244
1283
  fail("INVALID_METADATA", `${INTERNAL_CONVERSATION_NAME} is reserved for workflow conversation instrumentation`);
1245
1284
  if (hasIdentifier(program, INTERNAL_WORKTREE_NAME))
1246
1285
  fail("INVALID_METADATA", `${INTERNAL_WORKTREE_NAME} is reserved for workflow withWorktree instrumentation`);
1286
+ if (hasIdentifier(program, INTERNAL_SHELL_NAME))
1287
+ fail("INVALID_METADATA", `${INTERNAL_SHELL_NAME} is reserved for workflow shell instrumentation`);
1247
1288
  validateDirectPrimitiveReferences(program, "withWorktree");
1248
1289
  validateDirectPrimitiveReferences(program, "conversation");
1290
+ validateDirectPrimitiveReferences(program, "shell");
1249
1291
  for (const [index, schema] of schemas.entries())
1250
1292
  validateSchema(schema, `schema[${String(index)}]`);
1251
1293
  const calls = workflowCalls(program);
@@ -1259,6 +1301,8 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
1259
1301
  }
1260
1302
  if (operation === "withWorktree")
1261
1303
  validateStaticWithWorktree(call);
1304
+ if (operation === "shell")
1305
+ validateStaticShellOptions(call);
1262
1306
  if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement"))
1263
1307
  continue;
1264
1308
  if (operation === "checkpoint" && stableName(call.arguments[0]) === false)
@@ -1295,6 +1339,7 @@ export function preflight(script, capabilities, schemas = [], metadata = { name:
1295
1339
  fail("UNKNOWN_AGENT_TYPE", `Unknown agent type: ${missingType}`);
1296
1340
  return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools, agentTypes }), schemas: deepFreeze(checkedSchemas), dynamicAgentRoles });
1297
1341
  }
1342
+ function functionLaunchScript(name) { return `return await ${name}(args);`; }
1298
1343
  export function validateWorkflowLaunch(params, context) {
1299
1344
  return validateWorkflowLaunchWithRegistry(params, context, loadingRegistry());
1300
1345
  }
@@ -1303,15 +1348,19 @@ function validateWorkflowLaunchWithRegistry(params, context, registry) {
1303
1348
  fail("INVALID_METADATA", "maxAgentLaunches has been removed; use budget.agentLaunches");
1304
1349
  if (params.script !== undefined && params.workflow !== undefined)
1305
1350
  fail("INVALID_METADATA", "Provide either script or workflow, not both");
1306
- const definition = typeof params.workflow === "string" ? registry.workflow(params.workflow) : undefined;
1307
- const script = typeof params.script === "string" && params.script.trim() ? params.script : definition?.script ?? "";
1351
+ const functionName = typeof params.workflow === "string" ? params.workflow : undefined;
1352
+ const fn = functionName === undefined ? undefined : registry.function(functionName);
1353
+ const args = params.args === undefined ? null : params.args;
1354
+ if (functionName !== undefined && fn && (!object(args) || !jsonValue(args) || !Value.Check(fn.input, args)))
1355
+ fail("RESULT_INVALID", `Invalid input for ${functionName}`);
1356
+ const script = functionName !== undefined && fn ? functionLaunchScript(functionName) : typeof params.script === "string" && params.script.trim() ? params.script : "";
1308
1357
  if (!script)
1309
- fail("INVALID_SYNTAX", "Provide script or registered workflow");
1310
- const workflowName = typeof params.name === "string" && params.name.trim() ? params.name.trim() : typeof params.workflow === "string" ? params.workflow : "";
1358
+ fail("INVALID_SYNTAX", "Provide script or registered function");
1359
+ const workflowName = functionName ?? (typeof params.name === "string" && params.name.trim() ? params.name.trim() : "");
1311
1360
  if (!workflowName)
1312
1361
  fail("INVALID_METADATA", "Inline workflows require name");
1313
- const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : definition?.description ? { description: definition.description } : {}) });
1314
- const globalAgentDefinitions = loadAgentDefinitions(context.cwd, undefined, false);
1362
+ const metadata = validateWorkflowMetadata({ name: workflowName, ...(typeof params.description === "string" ? { description: params.description } : fn?.description ? { description: fn.description } : {}) });
1363
+ const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false);
1315
1364
  const projectAgentDefinitions = context.projectTrusted ? readRoleDefinitions(projectRoleDirectories(join(context.cwd, ".pi"))) : {};
1316
1365
  const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
1317
1366
  const aliases = context.modelAliases ?? {};
@@ -1319,7 +1368,7 @@ function validateWorkflowLaunchWithRegistry(params, context, registry) {
1319
1368
  const checked = preflight(script, { models: context.availableModels, tools: context.rootTools, agentTypes: new Set(Object.keys(agentDefinitions)), modelAliases: aliases, knownModels, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) }, [], metadata);
1320
1369
  const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
1321
1370
  validateRolePolicies(agentDefinitions, roleNames, context.availableModels, context.rootTools, aliases, knownModels, context.settingsPath);
1322
- return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames };
1371
+ return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, ...(functionName ? { functionName } : {}) };
1323
1372
  }
1324
1373
  function deepFreeze(value) {
1325
1374
  if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
@@ -1330,13 +1379,29 @@ function deepFreeze(value) {
1330
1379
  return value;
1331
1380
  }
1332
1381
  export function createLaunchSnapshot(input) {
1333
- return deepFreeze(structuredClone({ ...input, identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION }));
1382
+ return deepFreeze(structuredClone({ ...input, launchKind: input.launchKind ?? (input.functionName ? "function" : "inline"), identityVersion: input.identityVersion ?? LAUNCH_SNAPSHOT_IDENTITY_VERSION }));
1334
1383
  }
1335
1384
  export function loadLaunchSnapshot(input) {
1336
- if (object(input.settings) && Object.prototype.hasOwnProperty.call(input.settings, "maxAgentLaunches"))
1337
- fail("RESUME_INCOMPATIBLE", "Persisted workflow settings contain removed maxAgentLaunches");
1338
1385
  return deepFreeze(structuredClone(input));
1339
1386
  }
1387
+ function launchScriptForSnapshot(snapshot, registry) {
1388
+ if (snapshot.launchKind === "function") {
1389
+ if (!snapshot.functionName)
1390
+ fail("RESUME_INCOMPATIBLE", "Persisted registered function launch is missing its function name");
1391
+ try {
1392
+ registry.function(snapshot.functionName);
1393
+ }
1394
+ catch (error) {
1395
+ if (error instanceof WorkflowError && error.code === "MISSING_WORKFLOW")
1396
+ throw new WorkflowError("RESUME_INCOMPATIBLE", `Persisted registered function is unavailable: ${snapshot.functionName}`);
1397
+ throw error;
1398
+ }
1399
+ return functionLaunchScript(snapshot.functionName);
1400
+ }
1401
+ if (snapshot.launchKind === "inline")
1402
+ return snapshot.script;
1403
+ fail("RESUME_INCOMPATIBLE", "This persisted run uses the removed registered-workflow format; launch it again as a registered function or inline script");
1404
+ }
1340
1405
  export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
1341
1406
  export const HEARTBEAT_TIMEOUT_MS = 5000;
1342
1407
  const OUTCOME_ERRORS = new Set(["AGENT_TIMEOUT", "AGENT_FAILED", "RESULT_INVALID"]);
@@ -1402,10 +1467,12 @@ const named = (value, kind) => { if (typeof value !== "string" || !value.trim())
1402
1467
  const path = (...names) => names.map(encodeURIComponent).join("/");
1403
1468
  const inheritedAgentPath = new AsyncLocalStorage();
1404
1469
  const agentOccurrences = new Map();
1470
+ const shellOccurrences = new Map();
1405
1471
  const conversationOccurrences = new Map();
1406
1472
  const worktreeOwners = new AsyncLocalStorage();
1407
1473
  const worktreeOccurrences = new Map();
1408
1474
  const rejectAgent = () => { throw workError("INVALID_METADATA", "Workflow agent calls must use a direct agent(...) call; aliases and indirect calls are unsupported"); };
1475
+ const rejectShell = () => { throw workError("INVALID_METADATA", "Workflow shell calls must use a direct shell(...) call; aliases and indirect calls are unsupported"); };
1409
1476
  const rejectWorktree = () => { throw workError("INVALID_METADATA", "withWorktree calls must use a direct withWorktree(...) call; aliases and indirect calls are unsupported"); };
1410
1477
  const internalWithWorktree = async (...values) => {
1411
1478
  const callSite = values.pop();
@@ -1424,7 +1491,9 @@ const internalWithWorktree = async (...values) => {
1424
1491
  worktreeOccurrences.set(occurrenceKey, occurrence);
1425
1492
  owner = path("worktree", "unnamed", ...inherited, "callsite:" + callSite, "occurrence:" + String(occurrence));
1426
1493
  }
1427
- return await worktreeOwners.run(owner, callback);
1494
+ const reference = await rpc("worktree", [owner]);
1495
+ if (!reference || typeof reference !== "object" || typeof reference.path !== "string" || typeof reference.branch !== "string") throw workError("WORKTREE_FAILED", "Worktree reference is invalid");
1496
+ return await worktreeOwners.run(owner, () => callback(Object.freeze({ path: reference.path, branch: reference.branch })));
1428
1497
  };
1429
1498
  const internalConversation = (...values) => {
1430
1499
  const callSite = values.pop();
@@ -1483,6 +1552,31 @@ const internalAgent = (...values) => {
1483
1552
  });
1484
1553
  return result;
1485
1554
  };
1555
+ const internalShell = (...values) => {
1556
+ const callSite = values.pop();
1557
+ if (typeof callSite !== "string") throw workError("INTERNAL_ERROR", "Missing workflow shell call-site identity");
1558
+ if (values.length !== 1 && values.length !== 2) throw workError("INVALID_METADATA", "shell requires a command string and optional options");
1559
+ const command = values[0];
1560
+ if (typeof command !== "string") throw workError("INVALID_METADATA", "shell command must be a string");
1561
+ const options = values.length < 2 || values[1] === undefined ? {} : values[1];
1562
+ if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some(key => key !== "timeoutMs" && key !== "env")) throw workError("INVALID_METADATA", "shell options must contain only timeoutMs and env");
1563
+ if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs <= 0)) throw workError("INVALID_METADATA", "shell timeoutMs must be a positive integer");
1564
+ if (options.env !== undefined && (!options.env || typeof options.env !== "object" || Array.isArray(options.env) || Object.values(options.env).some(value => typeof value !== "string"))) throw workError("INVALID_METADATA", "shell env must be an object of strings");
1565
+ const inherited = inheritedAgentPath.getStore() || [];
1566
+ const worktreeOwner = worktreeOwners.getStore();
1567
+ const occurrenceKey = JSON.stringify([inherited, callSite, worktreeOwner || null]);
1568
+ const occurrence = (shellOccurrences.get(occurrenceKey) || 0) + 1;
1569
+ shellOccurrences.set(occurrenceKey, occurrence);
1570
+ const identity = { structuralPath: [...inherited], callSite, occurrence, ...(worktreeOwner ? { worktreeOwner } : {}) };
1571
+ const result = rpc("shell", [command, options, identity]);
1572
+ Object.defineProperties(result, {
1573
+ toJSON: { value() { throw workError("INVALID_METADATA", "Workflow shell result is a Promise; await it before serialization"); } },
1574
+ toString: { value() { throw workError("INVALID_METADATA", "Workflow shell result is a Promise; await it before interpolation"); } },
1575
+ [Symbol.toPrimitive]: { value() { throw workError("INVALID_METADATA", "Workflow shell result is a Promise; await it before interpolation"); } },
1576
+ });
1577
+ return result;
1578
+ };
1579
+ const shell = rejectShell;
1486
1580
  const agent = rejectAgent;
1487
1581
  const promptPath = (at, key) => /^[A-Za-z_$][\w$]*$/.test(key) ? at + "." + key : at + "[" + JSON.stringify(key) + "]";
1488
1582
  const plainPromptObject = value => {
@@ -1604,13 +1698,13 @@ const pipeline = async (operationName, items, stages) => {
1604
1698
  return Object.fromEntries(results.map(result => [result.name, result.value]));
1605
1699
  };
1606
1700
  const safeMath = Object.fromEntries(Object.getOwnPropertyNames(Math).filter(name => name !== "random").map(name => [name, Math[name]]));
1607
- const sandbox = { agent, conversation: internalConversation, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
1701
+ const sandbox = { agent, conversation: internalConversation, shell, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
1608
1702
  for (const [name, fn] of Object.entries(functions)) Object.defineProperty(sandbox, name, { value: fn, writable: false, configurable: false });
1609
1703
  for (const [name, value] of Object.entries(config.variables || {})) Object.defineProperty(sandbox, name, { value: freeze(value), writable: false, configurable: false });
1610
1704
  for (const name of ["Date","eval","Function","WebAssembly","process","require","module","exports","console","fetch","XMLHttpRequest","WebSocket","performance","crypto","setTimeout","setInterval","setImmediate","queueMicrotask","Intl","SharedArrayBuffer","Atomics"]) sandbox[name] = undefined;
1611
1705
  const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
1612
1706
  const body = config.script;
1613
- Promise.resolve().then(() => new vm.Script("(async(__pi_extensible_workflows_agent,__pi_extensible_workflows_conversation,__pi_extensible_workflows_withWorktree)=>{" + body + "\n})", { filename: "workflow.js" }).runInContext(context)(internalAgent, internalConversation, internalWithWorktree))
1707
+ Promise.resolve().then(() => new vm.Script("(async(__pi_extensible_workflows_agent,__pi_extensible_workflows_conversation,__pi_extensible_workflows_withWorktree,__pi_extensible_workflows_shell)=>{" + body + "\n})", { filename: "workflow.js" }).runInContext(context)(internalAgent, internalConversation, internalWithWorktree, internalShell))
1614
1708
  .then(async value => { await Promise.all(inflight); send({ type: "result", value: value === undefined ? null : value }); })
1615
1709
  .catch(error => send({ type: "error", error: workerError(error) }))
1616
1710
  .finally(() => clearInterval(heartbeat));
@@ -1623,6 +1717,9 @@ function encoded(value) {
1623
1717
  fail("RPC_LIMIT_EXCEEDED", "RPC value exceeds the 10 MB JSON boundary");
1624
1718
  return json;
1625
1719
  }
1720
+ function encodedRpcResult(id, value) {
1721
+ return encoded({ type: "rpcResult", id, ok: true, value });
1722
+ }
1626
1723
  function readAgentIdentity(value) {
1627
1724
  if (!object(value))
1628
1725
  fail("INTERNAL_ERROR", "Invalid workflow agent identity");
@@ -1637,9 +1734,18 @@ function readAgentIdentity(value) {
1637
1734
  fail("INTERNAL_ERROR", "Invalid workflow agent identity");
1638
1735
  return { structuralPath: [...structuralPath], callSite, occurrence, ...(typeof parentBreadcrumb === "string" ? { parentBreadcrumb } : {}), ...(typeof worktreeOwner === "string" ? { worktreeOwner } : {}), ...(parsedConversation ? { conversation: parsedConversation } : {}) };
1639
1736
  }
1737
+ function readShellIdentity(value) {
1738
+ const identity = readAgentIdentity(value);
1739
+ if (identity.conversation)
1740
+ fail("INTERNAL_ERROR", "Shell identity cannot include a conversation");
1741
+ return { structuralPath: identity.structuralPath, callSite: identity.callSite, occurrence: identity.occurrence, ...(identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {}) };
1742
+ }
1640
1743
  function agentIdentityPath(identity) {
1641
1744
  return operationPath("agent", ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
1642
1745
  }
1746
+ function shellIdentityPath(identity) {
1747
+ return operationPath("shell", ...(identity.worktreeOwner ? ["worktree", identity.worktreeOwner] : []), ...identity.structuralPath, `callsite:${identity.callSite}`, `occurrence:${String(identity.occurrence)}`);
1748
+ }
1643
1749
  function conversationIdentityPath(identity) {
1644
1750
  if (!identity.conversation)
1645
1751
  throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
@@ -1650,9 +1756,122 @@ function conversationTurnPath(identity) {
1650
1756
  throw new WorkflowError("INTERNAL_ERROR", "Missing conversation identity");
1651
1757
  return operationPath(conversationIdentityPath(identity), `turn:${String(identity.conversation.turn)}`);
1652
1758
  }
1759
+ function readShellResult(value) {
1760
+ if (!object(value) || (value.exitCode !== null && !Number.isInteger(value.exitCode)) || typeof value.stdout !== "string" || typeof value.stderr !== "string")
1761
+ fail("SHELL_FAILED", "Shell bridge returned an invalid result");
1762
+ return { exitCode: value.exitCode, stdout: value.stdout, stderr: value.stderr };
1763
+ }
1653
1764
  function agentWorktree(identity) {
1654
1765
  return identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {};
1655
1766
  }
1767
+ function shellProcessKill(child) {
1768
+ let forceKill;
1769
+ const killProcessTree = (signal) => {
1770
+ try {
1771
+ if (child.pid && process.platform !== "win32")
1772
+ process.kill(-child.pid, signal);
1773
+ else if (child.pid && process.platform === "win32") {
1774
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore", windowsHide: true });
1775
+ killer.unref();
1776
+ }
1777
+ else
1778
+ child.kill(signal);
1779
+ }
1780
+ catch {
1781
+ try {
1782
+ child.kill(signal);
1783
+ }
1784
+ catch { /* The process may already have exited. */ }
1785
+ }
1786
+ };
1787
+ child.once("close", () => { if (forceKill)
1788
+ clearTimeout(forceKill); });
1789
+ killProcessTree("SIGTERM");
1790
+ forceKill = setTimeout(() => { forceKill = undefined; killProcessTree("SIGKILL"); }, 1000);
1791
+ forceKill.unref();
1792
+ }
1793
+ function executeShellCommand(command, options, signal, cwd = process.cwd()) {
1794
+ return new Promise((resolve, reject) => {
1795
+ let child;
1796
+ try {
1797
+ child = spawn(command, { shell: true, cwd, env: { ...process.env, ...(options.env ?? {}) }, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"] });
1798
+ }
1799
+ catch (error) {
1800
+ reject(new WorkflowError("SHELL_FAILED", errorText(error)));
1801
+ return;
1802
+ }
1803
+ let settled = false;
1804
+ let timedOut = false;
1805
+ let outputBytes = 0;
1806
+ let timeout;
1807
+ const stdoutDecoder = new StringDecoder("utf8");
1808
+ const stderrDecoder = new StringDecoder("utf8");
1809
+ let stdout = "";
1810
+ let stderr = "";
1811
+ const cleanup = () => {
1812
+ if (timeout)
1813
+ clearTimeout(timeout);
1814
+ signal.removeEventListener("abort", onAbort);
1815
+ };
1816
+ const failShell = (error) => {
1817
+ if (settled)
1818
+ return;
1819
+ settled = true;
1820
+ cleanup();
1821
+ shellProcessKill(child);
1822
+ reject(error);
1823
+ };
1824
+ const onAbort = () => { failShell(new WorkflowError("CANCELLED", "Workflow cancelled")); };
1825
+ const capture = (target, chunk) => {
1826
+ if (settled)
1827
+ return;
1828
+ outputBytes += chunk.byteLength;
1829
+ if (outputBytes > RPC_LIMIT_BYTES) {
1830
+ failShell(new WorkflowError("RPC_LIMIT_EXCEEDED", "Shell result exceeds the 10 MB JSON boundary"));
1831
+ return;
1832
+ }
1833
+ if (target === "stdout")
1834
+ stdout += stdoutDecoder.write(chunk);
1835
+ else
1836
+ stderr += stderrDecoder.write(chunk);
1837
+ };
1838
+ child.stdout?.on("data", (chunk) => { capture("stdout", chunk); });
1839
+ child.stderr?.on("data", (chunk) => { capture("stderr", chunk); });
1840
+ child.once("error", (error) => { failShell(new WorkflowError("SHELL_FAILED", errorText(error))); });
1841
+ child.once("close", (exitCode) => {
1842
+ if (settled)
1843
+ return;
1844
+ settled = true;
1845
+ cleanup();
1846
+ stdout += stdoutDecoder.end();
1847
+ stderr += stderrDecoder.end();
1848
+ if (signal.aborted) {
1849
+ reject(new WorkflowError("CANCELLED", "Workflow cancelled"));
1850
+ return;
1851
+ }
1852
+ if (timedOut) {
1853
+ reject(new WorkflowError("SHELL_FAILED", `Shell command timed out after ${String(options.timeoutMs)}ms`));
1854
+ return;
1855
+ }
1856
+ const result = { exitCode: exitCode === null ? null : exitCode, stdout, stderr };
1857
+ try {
1858
+ encodedRpcResult(Number.MAX_SAFE_INTEGER, result);
1859
+ }
1860
+ catch (error) {
1861
+ reject(error instanceof WorkflowError ? error : new WorkflowError("RPC_LIMIT_EXCEEDED", errorText(error)));
1862
+ return;
1863
+ }
1864
+ resolve(result);
1865
+ });
1866
+ if (signal.aborted) {
1867
+ onAbort();
1868
+ return;
1869
+ }
1870
+ signal.addEventListener("abort", onAbort, { once: true });
1871
+ if (options.timeoutMs !== undefined)
1872
+ timeout = setTimeout(() => { timedOut = true; shellProcessKill(child); }, options.timeoutMs);
1873
+ });
1874
+ }
1656
1875
  export function runWorkflow(script, args = null, bridge = {}, signal) {
1657
1876
  encoded(args);
1658
1877
  const config = JSON.stringify({ script: instrumentWorkflow(script), args: structuredClone(args), functions: bridge.functions ?? {}, variables: bridge.variables ?? {} });
@@ -1756,6 +1975,14 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
1756
1975
  value = branded({ name: label, ok: false, failedAt: path, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
1757
1976
  }
1758
1977
  }
1978
+ else if (method === "shell") {
1979
+ if (!bridge.shell)
1980
+ fail("SHELL_FAILED", "No shell bridge is available");
1981
+ const command = validateShellCommand(values[0]);
1982
+ const options = validateShellOptions(values[1]);
1983
+ const identity = readShellIdentity(values[2]);
1984
+ value = readShellResult(await bridge.shell(command, options, controller.signal, identity));
1985
+ }
1759
1986
  else if (method === "checkpoint") {
1760
1987
  if (!bridge.checkpoint || !object(values[0]))
1761
1988
  fail("INTERNAL_ERROR", "checkpoint requires an available bridge and object input");
@@ -1792,6 +2019,11 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
1792
2019
  value = branded({ name, ok: false, failedAt: name, error: { code: typed.code, message: typed.message, ...(isWorkflowAuthored(typed) ? { authored: true } : {}) } });
1793
2020
  }
1794
2021
  }
2022
+ else if (method === "worktree") {
2023
+ if (!bridge.worktree || typeof values[0] !== "string" || !values[0])
2024
+ fail("INTERNAL_ERROR", "worktree requires an active host bridge and scope");
2025
+ value = await bridge.worktree(values[0], controller.signal);
2026
+ }
1795
2027
  else if (method === "phase") {
1796
2028
  if (typeof values[0] !== "string")
1797
2029
  fail("INTERNAL_ERROR", "phase name must be a string");
@@ -1805,7 +2037,7 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
1805
2037
  else
1806
2038
  fail("INTERNAL_ERROR", `Unknown worker RPC method: ${method}`);
1807
2039
  encoded(value);
1808
- child.send(encoded({ type: "rpcResult", id, ok: true, value }));
2040
+ child.send(encodedRpcResult(id, value));
1809
2041
  }
1810
2042
  catch (error) {
1811
2043
  const typed = asWorkflowError(error);
@@ -2005,13 +2237,9 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
2005
2237
  const render = ({ agent, depth }, grouped) => {
2006
2238
  const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? "⠦" : "○";
2007
2239
  const breadcrumb = grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId);
2008
- const setup = agent.attemptDetails?.at(-1)?.setup;
2009
- const thinking = setup?.model.thinking ?? agent.model.thinking;
2010
- const model = `${setup?.model.provider ?? agent.model.provider}/${setup?.model.model ?? agent.model.model}${thinking ? `:${thinking}` : ""}`;
2011
- const policy = [`model=${model}`, agent.requestedModel ? `requested=${agent.requestedModel}` : "", `tools=${(setup?.tools ?? agent.tools).join(",") || "(none)"}`, agent.role ? `role=${agent.role}` : ""];
2012
2240
  const tokens = agent.accounting ? formatAccounting(agent.accounting) : "";
2013
2241
  const indent = " ".repeat((grouped ? 2 : 1) + depth);
2014
- const result = [`${indent}${icon} ${breadcrumb} · ${agent.state} · ${policy.filter(Boolean).join(" · ")}${tokens ? ` · ${tokens}` : ""}`];
2242
+ const result = [`${indent}${icon} ${breadcrumb} · ${agent.state}${tokens ? ` · ${tokens}` : ""}`];
2015
2243
  if (agent.state === "failed" && agent.attemptDetails?.length) {
2016
2244
  const last = agent.attemptDetails[agent.attemptDetails.length - 1];
2017
2245
  if (last?.error)
@@ -2075,7 +2303,8 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
2075
2303
  return lines.join("\n");
2076
2304
  }
2077
2305
  function formatCheckpointReview(checkpoint) {
2078
- return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, "Context:", JSON.stringify(checkpoint.context, null, 2)].join("\n");
2306
+ const context = JSON.stringify(checkpoint.context, null, 2);
2307
+ return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, context === "null" ? "Context: null" : "Context:", ...(context === "null" ? [] : [context])].join("\n");
2079
2308
  }
2080
2309
  const DELIVERY_LIMIT_BYTES = 4 * 1024;
2081
2310
  const WORKFLOW_LOG_ENTRY = "workflow-log";
@@ -2097,11 +2326,144 @@ function utf8Prefix(value, maxBytes) {
2097
2326
  end -= 1;
2098
2327
  return bytes.subarray(0, end).toString("utf8");
2099
2328
  }
2329
+ const DIAGNOSTIC_LIMIT_BYTES = DELIVERY_LIMIT_BYTES - 512;
2330
+ function failureDiagnosticsFrom(error) {
2331
+ if (!error || typeof error !== "object")
2332
+ return undefined;
2333
+ return error[WORKFLOW_FAILURE_DIAGNOSTICS];
2334
+ }
2335
+ function boundedWorkflowFailureDiagnostics(value) {
2336
+ let bounded = {
2337
+ runId: utf8Prefix(value.runId, 128),
2338
+ workflowName: utf8Prefix(value.workflowName, 256),
2339
+ state: value.state,
2340
+ failedAt: value.failedAt === null ? null : utf8Prefix(value.failedAt, 1024),
2341
+ error: { code: value.error.code, message: utf8Prefix(value.error.message, 1024) },
2342
+ ...(value.failedAgent ? { failedAgent: {
2343
+ id: utf8Prefix(value.failedAgent.id, 128),
2344
+ ...(value.failedAgent.label ? { label: utf8Prefix(value.failedAgent.label, 128) } : {}),
2345
+ ...(value.failedAgent.role ? { role: utf8Prefix(value.failedAgent.role, 128) } : {}),
2346
+ structuralPath: value.failedAgent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
2347
+ attempt: value.failedAgent.attempt,
2348
+ ...(value.failedAgent.sessionId ? { sessionId: utf8Prefix(value.failedAgent.sessionId, 256) } : {}),
2349
+ ...(value.failedAgent.sessionFile ? { sessionFile: utf8Prefix(value.failedAgent.sessionFile, 1024) } : {}),
2350
+ } } : {}),
2351
+ completedSiblingAgents: (value.completedSiblingAgents ?? []).slice(0, 16).map((agent) => ({
2352
+ id: utf8Prefix(agent.id, 128),
2353
+ ...(agent.label ? { label: utf8Prefix(agent.label, 128) } : {}),
2354
+ ...(agent.role ? { role: utf8Prefix(agent.role, 128) } : {}),
2355
+ structuralPath: agent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
2356
+ })),
2357
+ completedSiblingPaths: value.completedSiblingPaths.slice(0, 16).map((path) => path.slice(0, 8).map((part) => utf8Prefix(part, 128))),
2358
+ artifacts: { runDirectory: utf8Prefix(value.artifacts.runDirectory, 1024), statePath: utf8Prefix(value.artifacts.statePath, 1024), journalPath: utf8Prefix(value.artifacts.journalPath, 1024) },
2359
+ };
2360
+ const size = () => Buffer.byteLength(JSON.stringify(bounded));
2361
+ while (size() > DIAGNOSTIC_LIMIT_BYTES) {
2362
+ if (bounded.completedSiblingAgents?.length || bounded.completedSiblingPaths.length) {
2363
+ bounded = { ...bounded, completedSiblingAgents: bounded.completedSiblingAgents?.slice(0, -1) ?? [], completedSiblingPaths: bounded.completedSiblingPaths.slice(0, -1) };
2364
+ continue;
2365
+ }
2366
+ if (bounded.failedAgent?.sessionFile) {
2367
+ const failedAgent = { ...bounded.failedAgent };
2368
+ delete failedAgent.sessionFile;
2369
+ bounded = { ...bounded, failedAgent };
2370
+ continue;
2371
+ }
2372
+ if (bounded.failedAgent?.sessionId) {
2373
+ const failedAgent = { ...bounded.failedAgent };
2374
+ delete failedAgent.sessionId;
2375
+ bounded = { ...bounded, failedAgent };
2376
+ continue;
2377
+ }
2378
+ if (Buffer.byteLength(bounded.artifacts.runDirectory) > 256) {
2379
+ bounded = { ...bounded, artifacts: { ...bounded.artifacts, runDirectory: utf8Prefix(bounded.artifacts.runDirectory, 256) } };
2380
+ continue;
2381
+ }
2382
+ if (Buffer.byteLength(bounded.error.message) > 256) {
2383
+ bounded = { ...bounded, error: { ...bounded.error, message: utf8Prefix(bounded.error.message, 256) } };
2384
+ continue;
2385
+ }
2386
+ if (bounded.failedAt !== null && Buffer.byteLength(bounded.failedAt) > 256) {
2387
+ bounded = { ...bounded, failedAt: utf8Prefix(bounded.failedAt, 256) };
2388
+ continue;
2389
+ }
2390
+ if (bounded.failedAgent && bounded.failedAgent.structuralPath.length > 4) {
2391
+ bounded = { ...bounded, failedAgent: { ...bounded.failedAgent, structuralPath: bounded.failedAgent.structuralPath.slice(0, 4) } };
2392
+ continue;
2393
+ }
2394
+ if (bounded.failedAgent?.structuralPath.some((part) => Buffer.byteLength(part) > 64)) {
2395
+ bounded = { ...bounded, failedAgent: { ...bounded.failedAgent, structuralPath: bounded.failedAgent.structuralPath.map((part) => utf8Prefix(part, 64)) } };
2396
+ continue;
2397
+ }
2398
+ if (Buffer.byteLength(bounded.artifacts.statePath) > 512 || Buffer.byteLength(bounded.artifacts.journalPath) > 512) {
2399
+ bounded = { ...bounded, artifacts: { ...bounded.artifacts, statePath: utf8Prefix(bounded.artifacts.statePath, 512), journalPath: utf8Prefix(bounded.artifacts.journalPath, 512) } };
2400
+ continue;
2401
+ }
2402
+ if (Buffer.byteLength(bounded.workflowName) > 128) {
2403
+ bounded = { ...bounded, workflowName: utf8Prefix(bounded.workflowName, 128) };
2404
+ continue;
2405
+ }
2406
+ break;
2407
+ }
2408
+ return bounded;
2409
+ }
2410
+ function createWorkflowFailureDiagnostics(store, metadata, error, run) {
2411
+ const rawFailedAt = error && typeof error === "object" ? error.failedAt : undefined;
2412
+ const failedAt = typeof rawFailedAt === "string" && rawFailedAt ? rawFailedAt : null;
2413
+ const failedAgents = run.agents.filter((agent) => agent.state === "failed");
2414
+ const failedAgentRecord = failedAgents.find((agent) => {
2415
+ if (failedAt === null)
2416
+ return false;
2417
+ try {
2418
+ return failedAt.includes(`${operationPath("agent", ...(agent.structuralPath ?? []))}/`);
2419
+ }
2420
+ catch {
2421
+ return false;
2422
+ }
2423
+ }) ?? failedAgents.at(-1);
2424
+ const failedAttempt = failedAgentRecord ? [...(failedAgentRecord.attemptDetails ?? [])].reverse().find((attempt) => attempt.error) ?? failedAgentRecord.attemptDetails?.at(-1) : undefined;
2425
+ const failedAgent = failedAgentRecord ? {
2426
+ id: failedAgentRecord.id,
2427
+ ...(failedAgentRecord.label ?? failedAgentRecord.name ? { label: failedAgentRecord.label ?? failedAgentRecord.name } : {}),
2428
+ ...(failedAgentRecord.role ? { role: failedAgentRecord.role } : {}),
2429
+ structuralPath: [...(failedAgentRecord.structuralPath ?? [])],
2430
+ attempt: Math.max(1, failedAttempt?.attempt ?? failedAgentRecord.attempts),
2431
+ ...(failedAttempt?.sessionId ? { sessionId: failedAttempt.sessionId } : {}),
2432
+ ...(failedAttempt?.sessionFile ? { sessionFile: failedAttempt.sessionFile } : {}),
2433
+ } : undefined;
2434
+ const completedSiblingAgents = run.agents.filter((agent) => {
2435
+ if (agent.state !== "completed" || agent.id === failedAgentRecord?.id)
2436
+ return false;
2437
+ return failedAgentRecord?.parentId === undefined ? agent.parentId === undefined : agent.parentId === failedAgentRecord.parentId;
2438
+ }).map((agent) => ({
2439
+ id: agent.id,
2440
+ ...(agent.label ?? agent.name ? { label: agent.label ?? agent.name } : {}),
2441
+ ...(agent.role ? { role: agent.role } : {}),
2442
+ structuralPath: [...(agent.structuralPath ?? [])],
2443
+ }));
2444
+ const completedSiblingPaths = completedSiblingAgents.map((agent) => [...agent.structuralPath]);
2445
+ return boundedWorkflowFailureDiagnostics({
2446
+ runId: run.id, workflowName: metadata.name, state: run.state, failedAt,
2447
+ error: { code: errorCode(error) ?? "INTERNAL_ERROR", message: errorText(error) || "The workflow failed without an error message." },
2448
+ ...(failedAgent ? { failedAgent } : {}), completedSiblingAgents, completedSiblingPaths,
2449
+ artifacts: { runDirectory: store.directory, statePath: join(store.directory, "state.json"), journalPath: join(store.directory, "journal.json") },
2450
+ });
2451
+ }
2452
+ export function formatWorkflowFailureDiagnostics(diagnostic) {
2453
+ const failedAgent = diagnostic.failedAgent ? `${diagnostic.failedAgent.label ?? diagnostic.failedAgent.id}${diagnostic.failedAgent.role ? ` role=${diagnostic.failedAgent.role}` : ""} attempt=${String(diagnostic.failedAgent.attempt)} path=${diagnostic.failedAgent.structuralPath.join(" > ") || "(root)"}${diagnostic.failedAgent.sessionFile ? ` session=${diagnostic.failedAgent.sessionFile}` : ""}` : "(not persisted)";
2454
+ const siblingAgents = diagnostic.completedSiblingAgents;
2455
+ const siblings = siblingAgents ? siblingAgents.map((agent) => `${agent.label ?? agent.id}${agent.role ? ` role=${agent.role}` : ""} path=${agent.structuralPath.join(" > ") || "(root)"}`).join(", ") || "(none)" : diagnostic.completedSiblingPaths.map((path) => path.join(" > ") || "(root)").join(", ") || "(none)";
2456
+ return [`✗ Workflow: ${diagnostic.workflowName}`, ` Run: ${diagnostic.runId}`, ` State: ${diagnostic.state}`, ` Error: ${diagnostic.error.code}: ${diagnostic.error.message}`, ` Failed at: ${diagnostic.failedAt ?? "(unknown)"}`, ` Failed agent: ${failedAgent}`, ` Completed sibling ${siblingAgents ? "agents" : "paths"}: ${siblings}`, ` Artifacts: state=${diagnostic.artifacts.statePath} journal=${diagnostic.artifacts.journalPath}`].join("\n");
2457
+ }
2458
+ function serializeWorkflowFailureDiagnostics(diagnostic) { return JSON.stringify(diagnostic); }
2459
+ function isWorkflowFailureDiagnostics(value) {
2460
+ return object(value) && typeof value.runId === "string" && typeof value.workflowName === "string" && typeof value.state === "string" && "failedAt" in value && object(value.error) && object(value.artifacts);
2461
+ }
2100
2462
  function deliver(pi, content) {
2101
2463
  pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
2102
2464
  }
2103
- function deliverFailure(pi, name, error) {
2104
- deliver(pi, `Workflow ${name} failed: ${formatWorkflowFailure(error)}`);
2465
+ function deliverFailure(pi, diagnostic) {
2466
+ deliver(pi, `Workflow ${utf8Prefix(diagnostic.workflowName, 128)} failure diagnostics: ${serializeWorkflowFailureDiagnostics(diagnostic)}`);
2105
2467
  }
2106
2468
  function safeEventError(error) {
2107
2469
  const code = errorCode(error) ?? "INTERNAL_ERROR";
@@ -2115,36 +2477,28 @@ class WorkflowEventPublisher {
2115
2477
  constructor(sink) {
2116
2478
  this.sink = sink;
2117
2479
  }
2480
+ removeRun(runId) {
2481
+ this.#queues.delete(runId);
2482
+ this.#budgetEvents.delete(runId);
2483
+ this.#worktrees.delete(runId);
2484
+ }
2118
2485
  seedBudget(runId, events) {
2119
2486
  const seen = this.#budgetEvents.get(runId) ?? new Set();
2120
2487
  for (const event of events ?? [])
2121
2488
  seen.add(this.budgetKey(event));
2122
2489
  this.#budgetEvents.set(runId, seen);
2123
2490
  }
2124
- async runStarted(store, metadata, background) {
2125
- await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {});
2126
- if (background)
2127
- await this.legacyStarted(store, metadata);
2128
- }
2129
- async legacyStarted(store, metadata) { await this.#publish(store, metadata, WORKFLOW_ASYNC_STARTED_EVENT, { id: store.runId, runId: store.runId, pid: process.pid, sessionId: store.sessionId, asyncDir: store.directory, agent: metadata.name }); }
2491
+ async runStarted(store, metadata) { await this.#publish(store, metadata, WORKFLOW_RUN_STARTED_EVENT, {}); }
2130
2492
  async runResumed(store, metadata) { await this.#publish(store, metadata, WORKFLOW_RUN_RESUMED_EVENT, {}); }
2131
2493
  async runState(store, metadata, previousState, state, reason) {
2132
2494
  await this.#publish(store, metadata, WORKFLOW_RUN_STATE_CHANGED_EVENT, { previousState, state, ...(reason ? { reason } : {}), ...(ERROR_CODES.includes(reason) ? { errorCode: reason } : {}) });
2133
2495
  if ((previousState === "paused" || previousState === "interrupted" || previousState === "budget_exhausted") && state === "running")
2134
2496
  await this.runResumed(store, metadata);
2135
2497
  }
2136
- async runCompleted(store, metadata, resultPath, background) {
2137
- await this.#publish(store, metadata, WORKFLOW_RUN_COMPLETED_EVENT, { resultPath });
2138
- if (background)
2139
- await this.#publish(store, metadata, WORKFLOW_ASYNC_COMPLETE_EVENT, { id: store.runId, runId: store.runId, sessionId: store.sessionId, asyncDir: store.directory, success: true, state: "complete" });
2140
- }
2141
- async runFailed(store, metadata, error, background, state) {
2498
+ async runCompleted(store, metadata, resultPath) { await this.#publish(store, metadata, WORKFLOW_RUN_COMPLETED_EVENT, { resultPath }); }
2499
+ async runFailed(store, metadata, error, state) {
2142
2500
  if (state === "failed")
2143
2501
  await this.#publish(store, metadata, WORKFLOW_RUN_FAILED_EVENT, { error: safeEventError(error) });
2144
- if (background) {
2145
- const legacyState = state === "interrupted" ? "failed" : state;
2146
- await this.#publish(store, metadata, WORKFLOW_ASYNC_COMPLETE_EVENT, { id: store.runId, runId: store.runId, sessionId: store.sessionId, asyncDir: store.directory, success: false, state: legacyState, ...(state === "stopped" ? { stopped: true } : {}), ...(legacyState === "budget_exhausted" || legacyState === "failed" ? { error: safeEventError(error).message } : {}) });
2147
- }
2148
2502
  }
2149
2503
  async agentState(store, metadata, previous, agent) {
2150
2504
  await this.#publish(store, metadata, WORKFLOW_AGENT_STATE_CHANGED_EVENT, { agentId: agent.id, displayLabel: agent.label ?? agent.name, ...(agent.role ? { role: agent.role } : {}), structuralPath: [...(agent.structuralPath ?? [])], ...(agent.parentId ? { parentId: agent.parentId } : {}), ...(agent.parentBreadcrumb ? { parentBreadcrumb: agent.parentBreadcrumb } : {}), ...(agent.worktreeOwner ? { worktreeOwner: agent.worktreeOwner } : {}), ...(previous ? { previousState: previous.state } : {}), state: agent.state, attempt: agent.attempts });
@@ -2202,7 +2556,12 @@ function namedRecord(value, kind) {
2202
2556
  fail("INVALID_METADATA", `${kind} must be a record`);
2203
2557
  return Object.entries(value);
2204
2558
  }
2205
- function hostWithWorktree(args, identity, occurrences) {
2559
+ function publicWorktreeReference(reference) {
2560
+ if (!object(reference) || typeof reference.path !== "string" || typeof reference.branch !== "string")
2561
+ fail("WORKTREE_FAILED", "Worktree reference is invalid");
2562
+ return Object.freeze({ path: reference.path, branch: reference.branch });
2563
+ }
2564
+ async function hostWithWorktree(args, identity, occurrences, resolveWorktree, signal) {
2206
2565
  if (args.length !== 1 && args.length !== 2)
2207
2566
  fail("INVALID_METADATA", "withWorktree requires a callback or a name and callback");
2208
2567
  const callback = args[args.length - 1];
@@ -2221,7 +2580,10 @@ function hostWithWorktree(args, identity, occurrences) {
2221
2580
  occurrences.set(key, occurrence);
2222
2581
  owner = operationPath("worktree", "unnamed", "function", identity, ...structuralPath, `occurrence:${String(occurrence)}`);
2223
2582
  }
2224
- return inheritedHostWorktreeOwner.run(owner, async () => await callback());
2583
+ if (!resolveWorktree)
2584
+ fail("WORKTREE_FAILED", "No worktree bridge is available");
2585
+ const reference = publicWorktreeReference(await resolveWorktree(owner, signal));
2586
+ return inheritedHostWorktreeOwner.run(owner, async () => await callback(reference));
2225
2587
  }
2226
2588
  function workflowRunContext(cwd, sessionId, runId, workflow, args, signal) {
2227
2589
  return Object.freeze({ cwd, sessionId, runId, workflow: deepFreeze(structuredClone(workflow)), args: deepFreeze(structuredClone(args)), signal });
@@ -2320,47 +2682,125 @@ function nextNamedOccurrence(counters, label) {
2320
2682
  }
2321
2683
  function withWorkflowFunctions(bridge, store, runContext, variables, registry) {
2322
2684
  const functionAgentOccurrences = new Map();
2685
+ const functionShellOccurrences = new Map();
2323
2686
  const functionWorktreeOccurrences = new Map();
2324
- return { ...bridge, functions: registry.globals(), variables, function: async (name, input, path, signal, worktreeOwner, structuralPath = []) => {
2325
- const replayed = await store.replay(path);
2326
- let stored;
2327
- const sideEffects = [];
2328
- const context = {
2329
- run: runContext,
2330
- agent: async (...args) => {
2331
- if (!bridge.agent || typeof args[0] !== "string")
2332
- fail("AGENT_FAILED", "No agent bridge is available");
2333
- const options = validateAgentOptions(args[1] === undefined ? {} : args[1]);
2334
- const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
2335
- const inherited = inheritedHostAgentPath.getStore() ?? [];
2336
- const key = `${path}\0${JSON.stringify(inherited)}`;
2337
- const occurrence = (functionAgentOccurrences.get(key) ?? 0) + 1;
2338
- functionAgentOccurrences.set(key, occurrence);
2339
- return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb: name, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
2340
- },
2341
- prompt: workflowPrompt,
2342
- parallel: (...args) => hostParallel(args[0], args[1]),
2343
- pipeline: (...args) => hostPipeline(args[0], args[1], args[2]),
2344
- withWorktree: (...args) => hostWithWorktree(args, path, functionWorktreeOccurrences),
2345
- checkpoint: async (...args) => {
2346
- if (!bridge.checkpoint || !object(args[0]) || !jsonValue(args[0]))
2347
- fail("INTERNAL_ERROR", "No checkpoint bridge is available");
2348
- return bridge.checkpoint(args[0], signal);
2349
- },
2350
- phase: (name) => { sideEffects.push(Promise.resolve(bridge.phase?.(name))); },
2351
- log: (message) => { sideEffects.push(Promise.resolve(bridge.log?.(message))); },
2352
- };
2353
- const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } }));
2354
- await Promise.all(sideEffects);
2355
- if (!replayed)
2356
- await store.complete(path, stored ?? result);
2357
- return result;
2358
- } };
2687
+ const functionInvokeOccurrences = new Map();
2688
+ const invokeFunction = async (name, input, path, signal, worktreeOwner, structuralPath = [], breadcrumb) => {
2689
+ const replayed = await store.replay(path);
2690
+ let stored;
2691
+ const sideEffects = [];
2692
+ const functionBreadcrumb = breadcrumb ?? name;
2693
+ const context = {
2694
+ run: runContext,
2695
+ invoke: async (targetName, targetInput) => {
2696
+ const inherited = inheritedHostAgentPath.getStore() ?? structuralPath;
2697
+ const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
2698
+ const key = JSON.stringify([path, inherited, targetName]);
2699
+ const occurrence = (functionInvokeOccurrences.get(key) ?? 0) + 1;
2700
+ functionInvokeOccurrences.set(key, occurrence);
2701
+ const nestedPath = operationPath("function", "nested", path, ...inherited, targetName, `occurrence:${String(occurrence)}`);
2702
+ return invokeFunction(targetName, targetInput, nestedPath, signal, scopedWorktreeOwner, inherited, `${functionBreadcrumb} > ${targetName}`);
2703
+ },
2704
+ agent: async (...args) => {
2705
+ if (!bridge.agent || typeof args[0] !== "string")
2706
+ fail("AGENT_FAILED", "No agent bridge is available");
2707
+ const options = validateAgentOptions(args[1] === undefined ? {} : args[1]);
2708
+ const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
2709
+ const inherited = inheritedHostAgentPath.getStore() ?? [];
2710
+ const key = `${path}\0${JSON.stringify(inherited)}`;
2711
+ const occurrence = (functionAgentOccurrences.get(key) ?? 0) + 1;
2712
+ functionAgentOccurrences.set(key, occurrence);
2713
+ return bridge.agent(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb: functionBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
2714
+ },
2715
+ shell: async (...args) => {
2716
+ if (!bridge.shell)
2717
+ fail("SHELL_FAILED", "No shell bridge is available");
2718
+ if (typeof args[0] !== "string")
2719
+ fail("INVALID_METADATA", "shell command must be a string");
2720
+ const options = validateShellOptions(args[1] === undefined ? {} : args[1]);
2721
+ const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
2722
+ const inherited = inheritedHostAgentPath.getStore() ?? [];
2723
+ const key = `${path}\0${JSON.stringify([inherited, scopedWorktreeOwner ?? null])}`;
2724
+ const occurrence = (functionShellOccurrences.get(key) ?? 0) + 1;
2725
+ functionShellOccurrences.set(key, occurrence);
2726
+ return bridge.shell(args[0], options, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
2727
+ },
2728
+ prompt: workflowPrompt,
2729
+ parallel: (...args) => hostParallel(args[0], args[1]),
2730
+ pipeline: (...args) => hostPipeline(args[0], args[1], args[2]),
2731
+ withWorktree: (...args) => hostWithWorktree(args, path, functionWorktreeOccurrences, bridge.worktree, signal),
2732
+ checkpoint: async (...args) => {
2733
+ if (!bridge.checkpoint || !object(args[0]) || !jsonValue(args[0]))
2734
+ fail("INTERNAL_ERROR", "No checkpoint bridge is available");
2735
+ return bridge.checkpoint(args[0], signal);
2736
+ },
2737
+ phase: (name) => { sideEffects.push(Promise.resolve(bridge.phase?.(name))); },
2738
+ log: (message) => { sideEffects.push(Promise.resolve(bridge.log?.(message))); },
2739
+ };
2740
+ const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } }));
2741
+ await Promise.all(sideEffects);
2742
+ if (!replayed)
2743
+ await store.complete(path, stored ?? result);
2744
+ return result;
2745
+ };
2746
+ return { ...bridge, functions: registry.globals(), variables, function: invokeFunction };
2359
2747
  }
2360
2748
  function projectTrusted(ctx) {
2361
2749
  const check = object(ctx) ? ctx.isProjectTrusted : undefined;
2362
2750
  return typeof check === "function" ? Boolean(Reflect.apply(check, ctx, [])) : true;
2363
2751
  }
2752
+ function isEntryRenderer(value) { return typeof value === "function"; }
2753
+ function isWorkflowEventSink(value) { return object(value) && typeof value.emit === "function"; }
2754
+ function piHostCapabilities(pi) {
2755
+ if (!object(pi))
2756
+ return {};
2757
+ const registerEntryRenderer = pi.registerEntryRenderer;
2758
+ const events = pi.events;
2759
+ return { ...(isEntryRenderer(registerEntryRenderer) ? { registerEntryRenderer } : {}), ...(isWorkflowEventSink(events) ? { events } : {}) };
2760
+ }
2761
+ function isModelRegistryGetter(value) { return typeof value === "function"; }
2762
+ function contextHostCapabilities(ctx) {
2763
+ if (!object(ctx) || !object(ctx.modelRegistry))
2764
+ return {};
2765
+ const registry = ctx.modelRegistry;
2766
+ const getAll = registry.getAll;
2767
+ const getAvailable = registry.getAvailable;
2768
+ return { modelRegistry: { ...(isModelRegistryGetter(getAll) ? { getAll: () => getAll.call(registry) } : {}), ...(isModelRegistryGetter(getAvailable) ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
2769
+ }
2770
+ function isUiSelect(value) { return typeof value === "function"; }
2771
+ function isUiInput(value) { return typeof value === "function"; }
2772
+ function isUiSetStatus(value) { return typeof value === "function"; }
2773
+ function uiHostCapabilities(ui) {
2774
+ if (!object(ui))
2775
+ return undefined;
2776
+ const select = ui.select;
2777
+ const input = ui.input;
2778
+ const setStatus = ui.setStatus;
2779
+ return { ...(isUiSelect(select) ? { select } : {}), ...(isUiInput(input) ? { input } : {}), ...(isUiSetStatus(setStatus) ? { setStatus } : {}) };
2780
+ }
2781
+ function tuiHostCapabilities(tui) {
2782
+ if (!object(tui) || !object(tui.terminal))
2783
+ return {};
2784
+ return { terminal: { ...(tui.terminal.rows === undefined ? {} : { rows: tui.terminal.rows }) } };
2785
+ }
2786
+ function tuiRows(tui) { const rows = tuiHostCapabilities(tui).terminal?.rows; return typeof rows === "number" && Number.isFinite(rows) ? rows : 24; }
2787
+ const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
2788
+ function borderWorkflowOverlay(component, theme) {
2789
+ return {
2790
+ ...component,
2791
+ render(width) {
2792
+ const border = theme.fg("border", "─".repeat(Math.max(1, width)));
2793
+ return [border, ...component.render(width), border];
2794
+ },
2795
+ };
2796
+ }
2797
+ function isKeybindingGetter(value) { return typeof value === "function"; }
2798
+ function keybindingsHostCapabilities(keybindings) {
2799
+ if (!object(keybindings) || !isKeybindingGetter(keybindings.getKeys))
2800
+ return {};
2801
+ return { getKeys: keybindings.getKeys };
2802
+ }
2803
+ function keybindingKeys(keybindings, name) { const getKeys = keybindingsHostCapabilities(keybindings).getKeys; return typeof getKeys === "function" ? getKeys.call(keybindings, name) : undefined; }
2364
2804
  function parseThinking(value) {
2365
2805
  switch (value) {
2366
2806
  case "off":
@@ -2373,10 +2813,11 @@ function parseThinking(value) {
2373
2813
  default: return undefined;
2374
2814
  }
2375
2815
  }
2376
- export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession) {
2816
+ export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir) {
2377
2817
  beginWorkflowExtensionLoading();
2378
2818
  const registry = loadingRegistry();
2379
- const registerEntryRenderer = pi.registerEntryRenderer;
2819
+ const extensionAgentDir = agentDir ?? getAgentDir();
2820
+ const registerEntryRenderer = piHostCapabilities(pi).registerEntryRenderer;
2380
2821
  registerEntryRenderer?.(WORKFLOW_LOG_ENTRY, (entry) => {
2381
2822
  const data = entry.data;
2382
2823
  return textBlock(data ? `Workflow ${data.workflowName}: ${data.message}` : "");
@@ -2390,7 +2831,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2390
2831
  await lifecycle.leave();
2391
2832
  }
2392
2833
  };
2393
- const eventPublisher = new WorkflowEventPublisher(pi.events);
2834
+ const eventPublisher = new WorkflowEventPublisher(piHostCapabilities(pi).events);
2394
2835
  pi.on("resources_discover", () => {
2395
2836
  if (!pi.getActiveTools().includes("workflow"))
2396
2837
  return;
@@ -2399,6 +2840,63 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2399
2840
  return skillPath ? { skillPaths: [skillPath] } : undefined;
2400
2841
  });
2401
2842
  const runs = new Map();
2843
+ let providerRecoveryQueue = Promise.resolve();
2844
+ const enqueueProviderRecovery = (task) => { const next = providerRecoveryQueue.then(task, task); providerRecoveryQueue = next.then(() => undefined, () => undefined); return next; };
2845
+ const createProviderErrorRecovery = (host, fallbackModels, abort) => {
2846
+ if (!object(host) || host.mode !== "tui" || host.hasUI !== true)
2847
+ return undefined;
2848
+ const ui = object(host.ui) ? host.ui : undefined;
2849
+ const select = uiHostCapabilities(ui)?.select;
2850
+ if (!select)
2851
+ return undefined;
2852
+ const hostModels = contextHostCapabilities(host).modelRegistry;
2853
+ const choose = (title, options) => select.call(ui, title, options);
2854
+ return (failure) => enqueueProviderRecovery(async () => {
2855
+ const action = await choose(`Subagent "${failure.label}" failed\nCurrent provider/model: ${failure.provider}/${failure.model}\nProvider error: ${failure.error}\nChoose what to do`, ["Retry", "Change model", "Abort workflow"]);
2856
+ if (action === "Retry")
2857
+ return "retry";
2858
+ if (action === "Change model") {
2859
+ const available = hostModels?.getAvailable?.().map((model) => `${model.provider}/${model.id}`) ?? [...fallbackModels];
2860
+ const selected = await choose(`Available models for subagent "${failure.label}"`, [...new Set(available)].sort());
2861
+ if (selected)
2862
+ return { model: selected };
2863
+ }
2864
+ abort();
2865
+ return "abort";
2866
+ });
2867
+ };
2868
+ const pendingFailureDiagnostics = new Map();
2869
+ pi.on("tool_result", (event) => {
2870
+ if (event.toolName !== "workflow" || !event.isError)
2871
+ return;
2872
+ const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
2873
+ if (!diagnostic)
2874
+ return;
2875
+ pendingFailureDiagnostics.delete(event.toolCallId);
2876
+ return { content: [{ type: "text", text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
2877
+ });
2878
+ const liveActivities = new Map();
2879
+ const setLiveActivity = (runId, agentId, activity) => {
2880
+ const activities = liveActivities.get(runId);
2881
+ if (activity) {
2882
+ if (activities)
2883
+ activities.set(agentId, activity);
2884
+ else
2885
+ liveActivities.set(runId, new Map([[agentId, activity]]));
2886
+ }
2887
+ else {
2888
+ activities?.delete(agentId);
2889
+ if (activities?.size === 0)
2890
+ liveActivities.delete(runId);
2891
+ }
2892
+ };
2893
+ const withLiveActivities = (run) => {
2894
+ const activities = liveActivities.get(run.id);
2895
+ return activities?.size ? { ...run, agents: run.agents.map((agent) => {
2896
+ const activity = activities.get(agent.id);
2897
+ return activity ? { ...agent, activity } : agent;
2898
+ }) } : run;
2899
+ };
2402
2900
  const conversationLocks = new Set();
2403
2901
  const terminalRunStates = new Map();
2404
2902
  let sessionLease;
@@ -2429,10 +2927,39 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2429
2927
  const persistWorktree = async (store, metadata, owner) => {
2430
2928
  const existing = (await store.worktrees()).some((worktree) => worktree.owner === owner);
2431
2929
  const worktree = await store.worktree(owner);
2432
- if (!existing)
2930
+ if (!existing && await store.ownsWorktree(owner))
2433
2931
  await eventPublisher.worktree(store, metadata, worktree);
2434
2932
  return worktree;
2435
2933
  };
2934
+ const resolveWorktree = async (store, metadata, owner) => {
2935
+ const run = runs.get(store.runId);
2936
+ if (!run)
2937
+ fail("INTERNAL_ERROR", `Unknown production run: ${store.runId}`);
2938
+ await run.lifecycle.enter();
2939
+ try {
2940
+ const worktree = await persistWorktree(store, metadata, owner);
2941
+ return { path: worktree.path, branch: worktree.branch };
2942
+ }
2943
+ finally {
2944
+ await run.lifecycle.leave();
2945
+ }
2946
+ };
2947
+ const shellForRun = async (store, metadata, lifecycle, command, options, signal, identity) => {
2948
+ await lifecycle.enter();
2949
+ try {
2950
+ const path = shellIdentityPath(identity);
2951
+ const replayed = await store.replay(path);
2952
+ if (replayed)
2953
+ return readShellResult(replayed.value);
2954
+ const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
2955
+ const result = await executeShellCommand(command, options, signal, cwd);
2956
+ await store.complete(path, result);
2957
+ return result;
2958
+ }
2959
+ finally {
2960
+ await lifecycle.leave();
2961
+ }
2962
+ };
2436
2963
  const lifecycleFor = (store, state, budget, metadata) => new RunLifecycle(state, async (next, previous, reason) => {
2437
2964
  if (next !== "pausing")
2438
2965
  budget.transition(next);
@@ -2443,7 +2970,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2443
2970
  return nextRun;
2444
2971
  });
2445
2972
  await eventPublisher.runState(store, metadata, previous, next, reason);
2446
- runs.get(store.runId)?.update?.(workflowToolUpdate(persisted));
2973
+ runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2447
2974
  });
2448
2975
  const scheduler = new FairAgentScheduler(async ({ id, runId, parentId, prompt, options, signal, setSteer }) => {
2449
2976
  const run = runs.get(runId);
@@ -2464,7 +2991,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2464
2991
  }
2465
2992
  if (!runState.agents.some((agent) => agent.id === id))
2466
2993
  return;
2467
- run.update?.(workflowToolUpdate(runState));
2994
+ setLiveActivity(runId, id, progress.activity);
2995
+ run.update?.(workflowToolUpdate(withLiveActivities(runState)));
2468
2996
  };
2469
2997
  const onAttempt = async (attempt) => {
2470
2998
  await scheduler.flush();
@@ -2475,15 +3003,16 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2475
3003
  const active = (await run.store.load()).run;
2476
3004
  await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
2477
3005
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2478
- run.update?.(workflowToolUpdate(persisted));
3006
+ run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2479
3007
  };
2480
- const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}), ...(options.conversation ? { conversation: options.conversation } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
3008
+ const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}), ...(options.conversation ? { conversation: options.conversation } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
2481
3009
  const before = (await run.store.load()).run;
2482
3010
  await persistAgentAttempts(run.store, id, result.attempts);
2483
3011
  const completed = (await run.store.load()).run;
2484
3012
  await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
2485
3013
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2486
- run.update?.(workflowToolUpdate(persisted));
3014
+ setLiveActivity(runId, id);
3015
+ run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2487
3016
  return result.value;
2488
3017
  }
2489
3018
  catch (error) {
@@ -2495,7 +3024,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2495
3024
  await eventPublisher.agentStates(run.store, run.metadata, before.agents, failed.agents);
2496
3025
  }
2497
3026
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2498
- run.update?.(workflowToolUpdate(persisted));
3027
+ setLiveActivity(runId, id);
3028
+ run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
2499
3029
  throw error;
2500
3030
  }
2501
3031
  }, 16, async (runId, ownership) => {
@@ -2522,8 +3052,23 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2522
3052
  return { ...current, agents };
2523
3053
  });
2524
3054
  await eventPublisher.agentStates(run.store, run.metadata, previousAgents, runState.agents);
2525
- run.update?.(workflowToolUpdate(runState));
3055
+ run.update?.(workflowToolUpdate(withLiveActivities(runState)));
2526
3056
  });
3057
+ const cleanupTerminalRun = async (runId) => {
3058
+ const run = runs.get(runId);
3059
+ if (!run || !["completed", "failed", "stopped"].includes(run.lifecycle.state))
3060
+ return;
3061
+ await scheduler.cancelRun(runId);
3062
+ await scheduler.flush();
3063
+ if (runs.get(runId) !== run)
3064
+ return;
3065
+ scheduler.removeRun(runId);
3066
+ terminalRunStates.set(runId, run.lifecycle.state);
3067
+ run.checkpointResolvers.clear();
3068
+ liveActivities.delete(runId);
3069
+ eventPublisher.removeRun(runId);
3070
+ runs.delete(runId);
3071
+ };
2527
3072
  const stopWorkflowRun = async (runId) => {
2528
3073
  const run = runs.get(runId);
2529
3074
  const terminalState = terminalRunStates.get(runId);
@@ -2537,6 +3082,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2537
3082
  run.execution?.cancel();
2538
3083
  await scheduler.cancelRun(run.store.runId);
2539
3084
  await scheduler.flush();
3085
+ await cleanupTerminalRun(runId);
2540
3086
  return { runId, state: "stopped", stopped: true };
2541
3087
  };
2542
3088
  const answerCheckpoint = async (runId, name, approved, silent = false) => {
@@ -2569,18 +3115,18 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2569
3115
  return undefined;
2570
3116
  await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
2571
3117
  const result = await applyBudgetDecision(request, approved);
2572
- run.budgetResolvers.get(proposalId)?.(result);
2573
- run.budgetResolvers.delete(proposalId);
2574
3118
  if (!silent)
2575
3119
  deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
2576
3120
  return result;
2577
3121
  };
2578
- const checkpointBridge = (runId, store, metadata, foreground, ui) => {
3122
+ const checkpointBridge = (runId, store, metadata, foreground, ui, headless = false) => {
2579
3123
  const checkpointCounters = new Map();
2580
3124
  return async (raw, signal) => {
2581
3125
  const input = validateCheckpoint(raw);
2582
3126
  const label = nextNamedOccurrence(checkpointCounters, input.name);
2583
3127
  const path = operationPath("checkpoint", label);
3128
+ if (headless)
3129
+ fail("RESUME_INCOMPATIBLE", "Headless CLI checkpoints are unsupported");
2584
3130
  if (foreground && !ui?.select)
2585
3131
  fail("RESUME_INCOMPATIBLE", "Foreground checkpoints require UI");
2586
3132
  const alreadyAwaiting = (await store.awaitingCheckpoints()).some((checkpoint) => checkpoint.path === path);
@@ -2671,14 +3217,17 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2671
3217
  return;
2672
3218
  const catalog = registry.catalog();
2673
3219
  const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0;
2674
- if (!catalog.functions.length && !catalog.variables.length && !catalog.workflows.length && !hasAliases)
3220
+ if (!catalog.functions.length && !catalog.variables.length && !hasAliases)
2675
3221
  return;
2676
3222
  pi.registerTool({
2677
3223
  name: "workflow_catalog",
2678
3224
  label: "Workflow Catalog",
2679
- description: "List global workflow functions, variables, and reusable workflows",
2680
- parameters: Type.Object({}, { additionalProperties: false }),
2681
- async execute() { return { content: [{ type: "text", text: JSON.stringify(registry.catalog()) }], details: {} }; }
3225
+ description: "List reusable workflow functions, variables, and model aliases, or load one entry in full",
3226
+ parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or variable name for full detail" })) }, { additionalProperties: false }),
3227
+ async execute(_id, params = {}) {
3228
+ const result = params.name === undefined ? registry.catalogIndex() : registry.catalogDetail(params.name);
3229
+ return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
3230
+ }
2682
3231
  });
2683
3232
  catalogRegistered = true;
2684
3233
  };
@@ -2688,7 +3237,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2688
3237
  const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
2689
3238
  if (missing)
2690
3239
  throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
2691
- const settingsPath = workflowSettingsPath();
3240
+ const settingsPath = workflowSettingsPath(extensionAgentDir);
2692
3241
  const currentSettings = loadSettings(settingsPath);
2693
3242
  resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath);
2694
3243
  const currentAliases = currentSettings.modelAliases ?? {};
@@ -2702,7 +3251,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2702
3251
  const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
2703
3252
  const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
2704
3253
  await run.store.saveSnapshot(snapshot);
2705
- run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath) }, createSession);
3254
+ run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, agentDir: extensionAgentDir, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath) }, createSession);
2706
3255
  run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
2707
3256
  const drift = aliasDrift(previousAliases, currentAliases);
2708
3257
  if (drift.length)
@@ -2710,6 +3259,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2710
3259
  };
2711
3260
  const coldResumeRun = async (run, hasUI, ui, trustedProject, context) => {
2712
3261
  const loaded = await run.store.load();
3262
+ await run.store.validateBorrowedWorktrees();
2713
3263
  if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
2714
3264
  throw new WorkflowError("RESUME_INCOMPATIBLE", "Workflow launch snapshot identity version is incompatible");
2715
3265
  if (loaded.snapshot.roles === undefined)
@@ -2723,7 +3273,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2723
3273
  const missing = loaded.snapshot.tools.filter((tool) => tool !== "workflow_catalog").find((tool) => !active.has(tool));
2724
3274
  if (missing)
2725
3275
  throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
2726
- const settingsPath = workflowSettingsPath();
3276
+ const settingsPath = workflowSettingsPath(extensionAgentDir);
2727
3277
  const currentSettings = loadSettings(settingsPath);
2728
3278
  resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
2729
3279
  const currentAliases = currentSettings.modelAliases ?? {};
@@ -2736,10 +3286,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2736
3286
  const resumeAliases = { ...previousAliases, ...currentAliases };
2737
3287
  const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
2738
3288
  const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
2739
- preflight(loaded.snapshot.script, { models: resumeModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels: resumeModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata);
3289
+ const script = launchScriptForSnapshot(loaded.snapshot, registry);
3290
+ preflight(script, { models: resumeModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels: resumeModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata);
2740
3291
  const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
2741
3292
  await run.store.saveSnapshot(snapshot);
2742
- run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath) }, createSession);
3293
+ run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, agentDir: extensionAgentDir, model: run.model, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath) }, createSession);
2743
3294
  const drift = aliasDrift(previousAliases, currentAliases);
2744
3295
  if (drift.length)
2745
3296
  await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
@@ -2756,14 +3307,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2756
3307
  if (!["completed", "failed", "stopped"].includes(run.lifecycle.state)) {
2757
3308
  await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
2758
3309
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, error: { code: typed.code, message: typed.message } }));
2759
- await eventPublisher.runFailed(run.store, run.metadata, typed, true, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
3310
+ await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
2760
3311
  run.update?.(workflowToolUpdate(persisted));
2761
3312
  }
3313
+ await cleanupTerminalRun(run.store.runId);
2762
3314
  throw typed;
2763
3315
  }
2764
3316
  await scheduler.cancelRun(run.store.runId);
2765
3317
  await run.lifecycle.resume();
2766
- const execution = runWorkflow(loaded.snapshot.script, loaded.snapshot.args, withWorkflowFunctions({ agent: async (prompt, options, signal, identity) => {
3318
+ const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: async (prompt, options, signal, identity) => {
2767
3319
  await run.lifecycle.enter();
2768
3320
  const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
2769
3321
  const conversationLock = conversationId ? `${run.store.runId}:${conversationId}` : "";
@@ -2810,7 +3362,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2810
3362
  conversationLocks.delete(conversationLock);
2811
3363
  await run.lifecycle.leave();
2812
3364
  }
2813
- }, checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, false, hasUI ? ui : undefined), phase: async (phase) => { await run.lifecycle.enter(); try {
3365
+ }, worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, false, hasUI ? ui : undefined), phase: async (phase) => { await run.lifecycle.enter(); try {
2814
3366
  let previousPhase;
2815
3367
  const persisted = await persistRunState(run.store, run.metadata, (current) => { previousPhase = current.phase; return { ...current, phase }; });
2816
3368
  await eventPublisher.phase(run.store, run.metadata, previousPhase, phase);
@@ -2826,7 +3378,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2826
3378
  throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
2827
3379
  const resultPath = await run.store.saveResult(value);
2828
3380
  await run.lifecycle.terminal("completed", "completed");
2829
- await eventPublisher.runCompleted(run.store, run.metadata, resultPath, true);
3381
+ await eventPublisher.runCompleted(run.store, run.metadata, resultPath);
2830
3382
  return { value, resultPath };
2831
3383
  }).catch(async (error) => {
2832
3384
  await scheduler.flush();
@@ -2835,11 +3387,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2835
3387
  await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
2836
3388
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), error: { code: typed.code, message: typed.message } }));
2837
3389
  const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
2838
- await eventPublisher.runFailed(run.store, run.metadata, typed, true, state);
3390
+ await eventPublisher.runFailed(run.store, run.metadata, typed, state);
2839
3391
  run.update?.(workflowToolUpdate(persisted));
2840
3392
  if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
2841
- deliverFailure(pi, run.metadata.name, error);
2842
- });
3393
+ deliverFailure(pi, createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted));
3394
+ }).finally(() => cleanupTerminalRun(run.store.runId));
3395
+ run.completion = completion;
2843
3396
  void completion;
2844
3397
  };
2845
3398
  const applyBudgetDecision = async (request, approved) => {
@@ -2875,18 +3428,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2875
3428
  if (budgetRelaxed(currentBudget, nextBudget)) {
2876
3429
  const proposalId = randomUUID();
2877
3430
  const request = { kind: "budget", proposalId, runId, consumed: usage, previous: currentBudget ?? {}, proposed: nextBudget ?? {}, budgetVersion: loaded.run.budgetVersion ?? 1 };
2878
- const decision = new Promise((resolve) => { run.budgetResolvers.set(proposalId, resolve); });
2879
- try {
2880
- await run.store.requestWorkflowDecision(request);
2881
- await appendBudgetDecisionEvent(run, request, "adjustment_requested");
2882
- }
2883
- catch (error) {
2884
- run.budgetResolvers.delete(proposalId);
2885
- throw error;
2886
- }
3431
+ await run.store.requestWorkflowDecision(request);
3432
+ await appendBudgetDecisionEvent(run, request, "adjustment_requested");
2887
3433
  deliver(pi, budgetDecisionDelivery(run.metadata, request));
2888
- const decisionResult = await decision;
2889
- return { state: decisionResult.state };
3434
+ return { state: "awaiting_approval", proposalId };
2890
3435
  }
2891
3436
  const changed = JSON.stringify(currentBudget ?? {}) !== JSON.stringify(nextBudget ?? {});
2892
3437
  if (changed) {
@@ -2955,14 +3500,16 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2955
3500
  const lifecycle = lifecycleFor(store, loaded.run.state, budgetRuntime, loaded.snapshot.metadata);
2956
3501
  const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
2957
3502
  const roleDefinitions = loaded.snapshot.roles ?? {};
2958
- runs.set(runId, { executor: new WorkflowAgentExecutor({ cwd: ctx.cwd, model, tools: new Set(loaded.snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}), agentDefinitions: roleDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(store.cwd, projectTrusted(ctx)) }, createSession), store, metadata: loaded.snapshot.metadata, model, lifecycle, budget: budgetRuntime, abortController: new AbortController(), projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), budgetResolvers: new Map() });
3503
+ const abortController = new AbortController();
3504
+ const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
3505
+ runs.set(runId, { executor: new WorkflowAgentExecutor({ cwd: ctx.cwd, agentDir: extensionAgentDir, model, tools: new Set(loaded.snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels: new Set(loaded.snapshot.models), knownModels: new Set(loaded.snapshot.models), ...(loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ? { modelAliases: loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases } : {}), ...(loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}), agentDefinitions: roleDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(store.cwd, projectTrusted(ctx), loaded.snapshot.settingsPath ?? workflowSettingsPath(extensionAgentDir)) }, createSession), store, metadata: loaded.snapshot.metadata, model, lifecycle, budget: budgetRuntime, abortController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) });
2959
3506
  for (const checkpoint of await store.awaitingCheckpoints())
2960
3507
  deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
2961
3508
  for (const decision of await store.pendingWorkflowDecisions())
2962
3509
  deliver(pi, budgetDecisionDelivery(loaded.snapshot.metadata, decision));
2963
3510
  scheduler.restoreRun(runId, loaded.snapshot.settings.concurrency, loaded.snapshot.identityVersion === LAUNCH_SNAPSHOT_IDENTITY_VERSION ? await store.loadOwnership() : [], () => runs.get(runId)?.budget.checkAgentLaunch());
2964
3511
  }
2965
- const resumeSelect = ctx.ui?.select;
3512
+ const resumeSelect = uiHostCapabilities(ctx.ui)?.select;
2966
3513
  if (ctx.hasUI && resumeSelect) {
2967
3514
  const interrupted = [...runs.values()].filter((r) => r.lifecycle.state === "interrupted");
2968
3515
  if (interrupted.length > 0) {
@@ -2992,7 +3539,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2992
3539
  pi.on("before_agent_start", (event, ctx) => {
2993
3540
  if (!pi.getActiveTools().includes("workflow"))
2994
3541
  return;
2995
- const roles = Object.entries(loadAgentDefinitions(ctx.cwd, undefined, projectTrusted(ctx))).filter(([, definition]) => definition.description);
3542
+ const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx))).filter(([, definition]) => definition.description);
2996
3543
  if (!roles.length)
2997
3544
  return;
2998
3545
  const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
@@ -3004,9 +3551,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3004
3551
  description: WORKFLOW_TOOL_DESCRIPTION,
3005
3552
  promptSnippet: WORKFLOW_TOOL_PROMPT_SNIPPET,
3006
3553
  parameters: WORKFLOW_TOOL_PARAMETERS,
3007
- async execute(_id, params, signal, onUpdate, ctx) {
3554
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
3008
3555
  try {
3009
- const settingsPath = workflowSettingsPath();
3556
+ const headless = object(ctx) && ctx.headless === true;
3557
+ const settingsPath = workflowSettingsPath(extensionAgentDir);
3010
3558
  const defaults = loadSettings(settingsPath);
3011
3559
  if (!ctx.model)
3012
3560
  throw new WorkflowError("UNKNOWN_MODEL", "A launching model is required");
@@ -3014,7 +3562,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3014
3562
  const budget = validateBudget(params.budget);
3015
3563
  const rootModel = { provider: ctx.model.provider, model: ctx.model.id, thinking: pi.getThinkingLevel() };
3016
3564
  const rootModelName = `${rootModel.provider}/${rootModel.model}`;
3017
- const modelRegistry = ctx.modelRegistry;
3565
+ const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
3018
3566
  const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? [ctx.model]).map((model) => `${model.provider}/${model.id}`));
3019
3567
  knownModels.add(rootModelName);
3020
3568
  const availableModels = knownModels;
@@ -3022,8 +3570,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3022
3570
  const trustedProject = projectTrusted(ctx);
3023
3571
  if (typeof ctx.cwd === "string")
3024
3572
  resolveAgentResourcePolicy(ctx.cwd, trustedProject, settingsPath);
3025
- const validated = validateWorkflowLaunchWithRegistry(params, { cwd: ctx.cwd, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases: defaults.modelAliases ?? {}, knownModels, settingsPath }, registry);
3026
- const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames } = validated;
3573
+ const validated = validateWorkflowLaunchWithRegistry({ ...params, args: params.args }, { cwd: ctx.cwd, agentDir: extensionAgentDir, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases: defaults.modelAliases ?? {}, knownModels, settingsPath }, registry);
3574
+ const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, functionName } = validated;
3027
3575
  await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
3028
3576
  const runId = randomUUID();
3029
3577
  const args = (params.args ?? null);
@@ -3036,24 +3584,28 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3036
3584
  const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
3037
3585
  const variables = await resolveWorkflowVariables(runContext, runController, registry);
3038
3586
  const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
3587
+ const parentRunId = params.parentRunId;
3588
+ if (parentRunId !== undefined)
3589
+ await store.validateParentRun(parentRunId);
3039
3590
  const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]]));
3040
3591
  const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
3041
3592
  const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, defaults.modelAliases, knownModels, settingsPath)] : []; });
3042
3593
  const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
3043
- const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}), ...(budget ? { budget } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
3594
+ const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, ...(functionName ? { launchKind: "function", functionName } : {}), ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}), ...(budget ? { budget } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
3044
3595
  const budgetRuntime = new WorkflowBudgetRuntime(budget);
3045
3596
  const initialBudget = budgetRuntime.snapshot();
3046
- await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
3597
+ await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
3047
3598
  const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
3048
3599
  const background = !params.foreground;
3049
3600
  const providerPause = async () => { if (background)
3050
3601
  deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
3051
- const executor = new WorkflowAgentExecutor({ cwd: ctx.cwd, model: rootModel, tools: new Set(rootTools), availableModels, knownModels, modelAliases: defaults.modelAliases ?? {}, settingsPath, agentDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(ctx.cwd, projectTrusted(ctx)), runContext }, createSession);
3052
- runs.set(runId, { executor, store, metadata: checked.metadata, model: rootModel, lifecycle, budget: budgetRuntime, abortController: runController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), budgetResolvers: new Map(), ...(params.foreground && onUpdate ? { update: onUpdate } : {}) });
3602
+ const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
3603
+ const executor = new WorkflowAgentExecutor({ cwd: ctx.cwd, agentDir: extensionAgentDir, model: rootModel, tools: new Set(rootTools), availableModels, knownModels, modelAliases: defaults.modelAliases ?? {}, settingsPath, agentDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(ctx.cwd, projectTrusted(ctx), settingsPath), runContext }, createSession);
3604
+ runs.set(runId, { executor, store, metadata: checked.metadata, model: rootModel, lifecycle, budget: budgetRuntime, abortController: runController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}), ...(params.foreground && onUpdate ? { update: onUpdate } : {}) });
3053
3605
  if (params.foreground && onUpdate)
3054
3606
  onUpdate(workflowToolUpdate((await store.load()).run));
3055
3607
  scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
3056
- const execution = runWorkflow(script, args, withWorkflowFunctions({ agent: async (prompt, options, agentSignal, identity) => {
3608
+ const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: async (prompt, options, agentSignal, identity) => {
3057
3609
  await lifecycle.enter();
3058
3610
  const conversationId = identity.conversation ? conversationIdentityPath(identity) : undefined;
3059
3611
  const conversationLock = conversationId ? `${runId}:${conversationId}` : "";
@@ -3103,7 +3655,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3103
3655
  conversationLocks.delete(conversationLock);
3104
3656
  await lifecycle.leave();
3105
3657
  }
3106
- }, checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined), phase: async (phase) => {
3658
+ }, worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined, headless), phase: async (phase) => {
3107
3659
  await lifecycle.enter();
3108
3660
  try {
3109
3661
  let previousPhase;
@@ -3116,35 +3668,46 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3116
3668
  }
3117
3669
  }, log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
3118
3670
  runs.get(runId).execution = execution;
3119
- await eventPublisher.runStarted(store, checked.metadata, background);
3671
+ await eventPublisher.runStarted(store, checked.metadata);
3120
3672
  const finish = execution.result.then(async (value) => {
3121
3673
  await scheduler.flush();
3122
3674
  if (budgetRuntime.hardExhausted)
3123
3675
  throw new WorkflowError("BUDGET_EXHAUSTED", "Budgeted work was attempted after hard exhaustion");
3124
3676
  const resultPath = await store.saveResult(value);
3125
3677
  await lifecycle.terminal("completed", "completed");
3126
- await eventPublisher.runCompleted(store, checked.metadata, resultPath, background);
3678
+ await eventPublisher.runCompleted(store, checked.metadata, resultPath);
3127
3679
  return { value, resultPath };
3128
3680
  }).catch(async (error) => {
3129
3681
  await scheduler.flush();
3130
3682
  const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
3131
3683
  if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state))
3132
3684
  await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
3133
- await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot(), error: { code: typed.code, message: typed.message } }));
3685
+ const persisted = await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot(), error: { code: typed.code, message: typed.message } }));
3134
3686
  const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
3135
- await eventPublisher.runFailed(store, checked.metadata, typed, background, state);
3687
+ await eventPublisher.runFailed(store, checked.metadata, typed, state);
3688
+ const diagnostic = createWorkflowFailureDiagnostics(store, checked.metadata, typed, persisted);
3689
+ Object.defineProperty(typed, WORKFLOW_FAILURE_DIAGNOSTICS, { value: diagnostic });
3690
+ if (params.foreground)
3691
+ pendingFailureDiagnostics.set(toolCallId, diagnostic);
3136
3692
  throw typed;
3137
3693
  });
3138
- runs.get(runId).completion = finish;
3694
+ const completion = finish.finally(() => cleanupTerminalRun(runId));
3695
+ runs.get(runId).completion = completion;
3139
3696
  if (background) {
3140
- void finish.then(async ({ value, resultPath }) => {
3697
+ void completion.then(async ({ value, resultPath }) => {
3141
3698
  deliver(pi, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
3142
- }, (error) => { deliverFailure(pi, checked.metadata.name, error); });
3699
+ }, (error) => {
3700
+ const diagnostic = failureDiagnosticsFrom(error);
3701
+ if (diagnostic)
3702
+ deliverFailure(pi, diagnostic);
3703
+ else
3704
+ deliver(pi, `Workflow ${checked.metadata.name} failed: ${formatWorkflowFailure(error)}`);
3705
+ });
3143
3706
  return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
3144
3707
  }
3145
- const { value } = await finish;
3708
+ const { value } = await completion;
3146
3709
  const run = (await store.load()).run;
3147
- return { content: [{ type: "text", text: JSON.stringify(value) }], details: { runId, value, run } };
3710
+ return { content: [{ type: "text", text: JSON.stringify(value) }, { type: "text", text: `Workflow run ID: ${runId}` }], details: { runId, value, run } };
3148
3711
  }
3149
3712
  catch (error) {
3150
3713
  throw mainAgentError(error);
@@ -3155,19 +3718,22 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3155
3718
  },
3156
3719
  renderResult(result, { isPartial }, _theme, context) {
3157
3720
  const details = result.details;
3721
+ if (isWorkflowFailureDiagnostics(details))
3722
+ return textBlock(formatWorkflowFailureDiagnostics(details));
3723
+ const runDetails = details;
3158
3724
  const state = context.state;
3159
- if (details?.run && isPartial && details.run.state === "running" && !state.workflowSpinner) {
3725
+ if (runDetails?.run && isPartial && runDetails.run.state === "running" && !state.workflowSpinner) {
3160
3726
  state.workflowSpinner = setInterval(context.invalidate, 80);
3161
3727
  state.workflowSpinner.unref();
3162
3728
  }
3163
- else if ((!isPartial || details?.run?.state !== "running") && state.workflowSpinner) {
3729
+ else if ((!isPartial || runDetails?.run?.state !== "running") && state.workflowSpinner) {
3164
3730
  clearInterval(state.workflowSpinner);
3165
3731
  delete state.workflowSpinner;
3166
3732
  }
3167
- if (details?.run)
3168
- return workflowProgressBlock(details.run);
3733
+ if (runDetails?.run)
3734
+ return workflowProgressBlock(runDetails.run);
3169
3735
  const content = result.content[0];
3170
- return textBlock(isPartial ? "Workflow starting..." : details?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
3736
+ return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
3171
3737
  },
3172
3738
  });
3173
3739
  pi.registerCommand("workflow", {
@@ -3198,7 +3764,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3198
3764
  let stores = await loadStores();
3199
3765
  const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint or proposal-id]. Use workflow_resume for budget patches.";
3200
3766
  const setWorkflowStatus = (text) => {
3201
- const setStatus = ctx.ui.setStatus;
3767
+ const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
3202
3768
  setStatus?.call(ctx.ui, "workflow-stop", text);
3203
3769
  };
3204
3770
  const runAction = async (actionCommand, keepContext, status = setWorkflowStatus) => {
@@ -3254,7 +3820,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3254
3820
  return keepContext ? "dashboard" : "done";
3255
3821
  }
3256
3822
  if (action === "adjust" && run?.lifecycle.state === "budget_exhausted") {
3257
- const input = await ctx.ui.input?.("Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
3823
+ const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
3258
3824
  if (input === undefined)
3259
3825
  return keepContext ? "dashboard" : "done";
3260
3826
  const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input));
@@ -3291,8 +3857,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3291
3857
  }
3292
3858
  };
3293
3859
  const manageAliases = async () => {
3294
- const settingsPath = workflowSettingsPath();
3295
- const modelRegistry = ctx.modelRegistry;
3860
+ const settingsPath = workflowSettingsPath(extensionAgentDir);
3861
+ const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
3296
3862
  const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
3297
3863
  const selectTarget = async () => {
3298
3864
  const models = available();
@@ -3409,10 +3975,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3409
3975
  const labels = navigatorRunLabels(sorted);
3410
3976
  const terminalStates = new Set(["completed", "failed", "stopped"]);
3411
3977
  const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
3412
- const pickerOptions = [...labels, "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : [])];
3978
+ const pickerOptions = [...labels, ...(herdrPaneId() ? ["Inspect session in pane"] : []), "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : [])];
3413
3979
  const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
3414
3980
  if (!runChoice || runChoice === "Close")
3415
3981
  return;
3982
+ if (runChoice === "Inspect session in pane") {
3983
+ try {
3984
+ await openHerdrPane({ action: "inspect", cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId() });
3985
+ ctx.ui.notify("Opened session inspector in pane.", "info");
3986
+ }
3987
+ catch (error) {
3988
+ ctx.ui.notify(`Cannot open session inspector in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
3989
+ }
3990
+ continue;
3991
+ }
3416
3992
  if (runChoice === "Model aliases") {
3417
3993
  await manageAliases();
3418
3994
  stores = await loadStores();
@@ -3448,49 +4024,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3448
4024
  ctx.ui.notify(`Failed to copy ${artifact}: ${error instanceof Error ? error.message : String(error)}`, "error");
3449
4025
  }
3450
4026
  };
3451
- const openTranscript = async (transcript) => {
3452
- try {
3453
- const entries = SessionManager.open(transcript).buildContextEntries();
3454
- if (ctx.mode !== "tui") {
3455
- ctx.ui.notify(`Transcript: ${transcript}`, "info");
3456
- return;
3457
- }
3458
- await ctx.ui.custom((tui, theme, keybindings, done) => {
3459
- let offset = 0;
3460
- let renderedLines = [];
3461
- const viewport = () => Math.max(1, ((tui.terminal?.rows) ?? 24) - 3);
3462
- const move = (delta) => { offset = Math.max(0, Math.min(Math.max(0, renderedLines.length - viewport()), offset + delta)); };
3463
- return {
3464
- render(width) {
3465
- renderedLines = transcriptLines(entries).flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
3466
- offset = Math.min(offset, Math.max(0, renderedLines.length - viewport()));
3467
- return [theme.fg("accent", "Native Pi transcript"), ...renderedLines.slice(offset, offset + viewport()), "", theme.fg("dim", "↑↓/pgup/pgdn scroll · esc close")];
3468
- },
3469
- invalidate() { },
3470
- handleInput(data) {
3471
- if (keybindings.matches(data, "tui.select.up"))
3472
- move(-1);
3473
- else if (keybindings.matches(data, "tui.select.down"))
3474
- move(1);
3475
- else if (keybindings.matches(data, "tui.select.pageUp"))
3476
- move(-viewport());
3477
- else if (keybindings.matches(data, "tui.select.pageDown"))
3478
- move(viewport());
3479
- else if (keybindings.matches(data, "tui.editor.cursorLineStart"))
3480
- offset = 0;
3481
- else if (keybindings.matches(data, "tui.editor.cursorLineEnd"))
3482
- offset = Math.max(0, renderedLines.length - viewport());
3483
- else if (keybindings.matches(data, "tui.select.cancel"))
3484
- done(undefined);
3485
- tui.requestRender();
3486
- },
3487
- };
3488
- }, { overlay: true });
3489
- }
3490
- catch (error) {
3491
- ctx.ui.notify(`Cannot open transcript: ${error instanceof Error ? error.message : String(error)}`, "warning");
3492
- }
3493
- };
3494
4027
  const loadDashboard = async () => {
3495
4028
  const loaded = await store.load();
3496
4029
  const checkpoints = await store.awaitingCheckpoints();
@@ -3530,20 +4063,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3530
4063
  actions.set("Refresh", "refresh");
3531
4064
  else
3532
4065
  actions.set("View script", "view-script");
3533
- const transcripts = [...new Set([...loaded.run.agents.flatMap((agent) => (agent.attemptDetails ?? []).map((attempt) => attempt.sessionFile)), ...loaded.run.nativeSessions.map(({ sessionFile }) => sessionFile)])];
3534
4066
  if (loaded.run.agents.length)
3535
4067
  actions.set("Agents...", "agents");
3536
- if (!loaded.run.agents.length && ctx.mode === "tui" && transcripts.length)
3537
- actions.set("View transcript", "view-transcript");
3538
- if (!loaded.run.agents.length && transcripts.length)
3539
- actions.set("Transcript paths", "transcripts");
3540
4068
  if (terminalStates.has(loaded.run.state))
3541
4069
  add("Delete", "delete");
3542
4070
  if (ctx.mode === "tui") {
3543
4071
  addCopy("Copy run path", store.directory, "run path");
3544
4072
  addCopy("Copy run ID", store.runId, "run ID");
3545
4073
  }
3546
- return { dashboard: formatNavigatorDashboard(loaded.run, checkpoints, worktrees), actions, copies, reviews, transcripts, script: loaded.snapshot.script, agents: loaded.run.agents, worktrees };
4074
+ return { dashboard: formatNavigatorDashboard(loaded.run, checkpoints, worktrees), actions, copies, reviews, script: loaded.snapshot.script, agents: loaded.run.agents, worktrees, cwd: loaded.run.cwd };
3547
4075
  };
3548
4076
  const selectAgent = async (dashboard) => {
3549
4077
  const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
@@ -3566,14 +4094,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3566
4094
  const attempts = [...(selected.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
3567
4095
  const worktree = selected.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === selected.worktreeOwner) : undefined;
3568
4096
  const actions = [
3569
- ...(attempts.length ? ["View transcript", "Copy transcript path"] : []),
4097
+ ...(attempts.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
3570
4098
  ...(worktree ? ["Copy branch", "Copy worktree path"] : []),
3571
4099
  "Copy agent ID",
3572
4100
  "Back",
3573
4101
  ];
3574
4102
  const chooseAttempt = async () => {
3575
4103
  const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
3576
- const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Transcript attempts", [...choices, "Back"]);
4104
+ const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Fork attempts", [...choices, "Back"]);
3577
4105
  const index = choice ? choices.indexOf(choice) : -1;
3578
4106
  return index >= 0 ? attempts[index] : undefined;
3579
4107
  };
@@ -3593,14 +4121,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3593
4121
  await copyArtifact(worktree.path, "worktree path");
3594
4122
  continue;
3595
4123
  }
3596
- if (action === "View transcript" || action === "Copy transcript path") {
4124
+ if (action === "Fork as Pi session in pane") {
3597
4125
  const attempt = await chooseAttempt();
3598
4126
  if (!attempt)
3599
4127
  continue;
3600
- if (action === "Copy transcript path")
3601
- await copyArtifact(attempt.sessionFile, "transcript path");
3602
- else
3603
- await openTranscript(attempt.sessionFile);
4128
+ const running = !SETTLED_AGENT_STATES.has(selected.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
4129
+ if (running && !await ctx.ui.confirm("Fork running attempt?", "This attempt is still running. The snapshot may end mid-turn and will not receive later updates. It opens read-only to avoid concurrent changes to the workflow agent's working directory. Continue?"))
4130
+ continue;
4131
+ try {
4132
+ await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
4133
+ ctx.ui.notify("Forked Pi session in pane.", "info");
4134
+ }
4135
+ catch (error) {
4136
+ ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
4137
+ }
3604
4138
  }
3605
4139
  }
3606
4140
  };
@@ -3615,19 +4149,18 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3615
4149
  let disposed = false;
3616
4150
  let stopRequested = false;
3617
4151
  let stopStatus;
3618
- const terminalRows = () => Math.max(1, (tui.terminal?.rows) ?? 24);
4152
+ const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
3619
4153
  const keyLabels = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
3620
4154
  const keyLabel = (binding, fallback) => {
3621
- const getKeys = keybindings.getKeys;
3622
- const keys = getKeys?.call(keybindings, binding);
4155
+ const keys = keybindingKeys(keybindings, binding);
3623
4156
  return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
3624
4157
  };
3625
4158
  const dashboardLayout = () => {
3626
4159
  const rows = terminalRows();
3627
4160
  const hintRows = rows >= 4 ? 1 : 0;
3628
- const separatorRows = rows >= 6 ? 1 : 0;
4161
+ const separatorRows = rows >= 8 ? 1 : 0;
3629
4162
  const available = Math.max(1, rows - hintRows - separatorRows);
3630
- const actionViewport = Math.min(options.length, Math.max(1, Math.floor(available / 2)));
4163
+ const actionViewport = Math.min(options.length, Math.max(1, Math.ceil(available / 2)));
3631
4164
  return { rows, hintRows, separatorRows, actionViewport, dashboardViewport: available - actionViewport };
3632
4165
  };
3633
4166
  const updateDashboard = async (selectedOption) => {
@@ -3670,7 +4203,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3670
4203
  void updateDashboard(selectedOption).catch(() => undefined).finally(() => { refreshing = false; });
3671
4204
  }, 1000);
3672
4205
  timer.unref();
3673
- return {
4206
+ return borderWorkflowOverlay({
3674
4207
  render(width) {
3675
4208
  const dashboard = stopStatus ? `${view.dashboard}\n\n${stopStatus}` : view.dashboard;
3676
4209
  const dashboardLines = truncateToVisualLines(theme.fg("accent", dashboard), Number.MAX_SAFE_INTEGER, width, 1).visualLines;
@@ -3717,7 +4250,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3717
4250
  tui.requestRender();
3718
4251
  },
3719
4252
  dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
3720
- };
4253
+ }, theme);
3721
4254
  }, { overlay: true })
3722
4255
  : await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Close"]);
3723
4256
  if (!actionChoice || actionChoice === "Close")
@@ -3733,12 +4266,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3733
4266
  const highlighted = highlightCode(view.script, "javascript");
3734
4267
  let offset = 0;
3735
4268
  let renderedLines = [];
3736
- const viewport = () => Math.max(1, ((tui.terminal?.rows) ?? 24) - 3);
4269
+ const viewport = () => Math.max(1, tuiRows(tui) - 3 - WORKFLOW_OVERLAY_BORDER_ROWS);
3737
4270
  const move = (delta) => {
3738
4271
  const maxOffset = Math.max(0, renderedLines.length - viewport());
3739
4272
  offset = Math.max(0, Math.min(maxOffset, offset + delta));
3740
4273
  };
3741
- return {
4274
+ return borderWorkflowOverlay({
3742
4275
  render(width) {
3743
4276
  renderedLines = highlighted.flatMap((line) => line ? truncateToVisualLines(line, Number.MAX_SAFE_INTEGER, width, 0).visualLines : [""]);
3744
4277
  const maxOffset = Math.max(0, renderedLines.length - viewport());
@@ -3764,24 +4297,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3764
4297
  done(undefined);
3765
4298
  tui.requestRender();
3766
4299
  },
3767
- };
3768
- });
3769
- continue;
3770
- }
3771
- if (actionChoice === "View transcript") {
3772
- const transcript = await ctx.ui.select("Native Pi transcripts", [...view.transcripts, "Back"]);
3773
- if (transcript && transcript !== "Back")
3774
- await openTranscript(transcript);
3775
- continue;
3776
- }
3777
- if (actionChoice === "Transcript paths") {
3778
- const transcript = await ctx.ui.select("Native Pi transcript paths", [...view.transcripts, "Back"]);
3779
- if (transcript && transcript !== "Back") {
3780
- if (ctx.mode === "tui")
3781
- await copyArtifact(transcript, "transcript path");
3782
- else
3783
- ctx.ui.notify(transcript, "info");
3784
- }
4300
+ }, theme);
4301
+ }, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
3785
4302
  continue;
3786
4303
  }
3787
4304
  const copy = view.copies.get(actionChoice);
@@ -3799,11 +4316,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3799
4316
  let offset = 0;
3800
4317
  let renderedLines = [];
3801
4318
  const layout = () => {
3802
- const rows = Math.max(1, ((tui.terminal?.rows) ?? 24));
4319
+ const rows = Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
3803
4320
  const compactControls = rows < 4;
3804
4321
  const titleRows = rows >= 5 ? 1 : 0;
3805
4322
  const hintRows = rows >= 8 ? 1 : 0;
3806
- const separatorRows = rows >= 8 ? 2 : 0;
4323
+ const separatorRows = rows >= 8 ? 1 : 0;
3807
4324
  const controlRows = compactControls ? 1 : options.length;
3808
4325
  const contentViewport = Math.max(0, rows - titleRows - hintRows - separatorRows - controlRows);
3809
4326
  return { rows, compactControls, titleRows, hintRows, separatorRows, contentViewport };
@@ -3812,7 +4329,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3812
4329
  const maxOffset = Math.max(0, renderedLines.length - layout().contentViewport);
3813
4330
  offset = Math.max(0, Math.min(maxOffset, offset + delta));
3814
4331
  };
3815
- return {
4332
+ return borderWorkflowOverlay({
3816
4333
  render(width) {
3817
4334
  renderedLines = truncateToVisualLines(formatCheckpointReview(checkpoint), Number.MAX_SAFE_INTEGER, width, 0).visualLines;
3818
4335
  const currentLayout = layout();
@@ -3827,7 +4344,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3827
4344
  ...renderedLines.slice(offset, offset + currentLayout.contentViewport),
3828
4345
  ...(currentLayout.separatorRows ? [""] : []),
3829
4346
  ...controls,
3830
- ...(currentLayout.separatorRows ? [""] : []),
3831
4347
  ...(currentLayout.hintRows ? [hint] : []),
3832
4348
  ];
3833
4349
  },
@@ -3847,8 +4363,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3847
4363
  done(undefined);
3848
4364
  tui.requestRender();
3849
4365
  },
3850
- };
3851
- });
4366
+ }, theme);
4367
+ }, { overlay: true, overlayOptions: { anchor: "top-left", width: "100%", maxHeight: "100%" } });
3852
4368
  if (decision) {
3853
4369
  const accepted = await answerCheckpoint(store.runId, checkpoint.name, decision === "Approve", true);
3854
4370
  if (!accepted)