pi-extensible-workflows 3.0.0 → 3.1.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/host.js CHANGED
@@ -10,13 +10,32 @@ import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor } f
10
10
  import { herdrPaneId, openHerdrPane } from "./herdr.js";
11
11
  import { acquireSessionLease, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
12
12
  import { budgetRelaxed, budgetUsage, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
13
- import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCode, errorText, fail, isWorkflowAuthored, jsonValue, modelCapability, object, parseModelReference, parseThinking, positiveInteger, resolveModelReference, validateModelAliases } from "./utils.js";
14
- import { launchScriptForSnapshot, loadAgentDefinitions, loadSettings, preflight, resolveAgentResourcePolicy, saveModelAliases, validateAgentOptions, validateCheckpoint, validateShellOptions, validateWorkflowLaunchWithRegistry, workflowPrompt, workflowSettingsPath } from "./validation.js";
13
+ import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCode, errorText, fail, isWorkflowAuthored, jsonValue, modelAliasErrorName, modelCapability, object, parseModelReference, parseThinking, positiveInteger, resolveModelReference, validateModelAliases } from "./utils.js";
14
+ import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateAgentOptions, validateCheckpoint, validateModelAliasAvailability, validateShellOptions, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowPrompt, workflowSettingsPath } from "./validation.js";
15
15
  import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistry } from "./registry.js";
16
16
  import { agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
17
17
  import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_WORKTREE_CREATED_EVENT, WorkflowError } from "./types.js";
18
18
  const SETTLED_AGENT_STATES = new Set(["completed", "failed", "cancelled"]);
19
+ function snapshotResourcePolicy(snapshot, cwd, projectTrusted, globalSettingsPath) {
20
+ const empty = { skills: [], extensions: [] };
21
+ return { globalSettingsPath, projectSettingsPath: join(cwd, ".pi", "pi-extensible-workflows", "settings.json"), projectTrusted, global: empty, project: empty, effective: snapshot.settings.disabledAgentResources ?? empty, unmatchedSkills: [], unmatchedExtensions: [] };
22
+ }
19
23
  const PLAIN_WORKFLOW_PROGRESS_STYLES = { accent: (text) => text, success: (text) => text, error: (text) => text, warning: (text) => text, muted: (text) => text, dim: (text) => text, bold: (text) => text };
24
+ function workflowLaunchSettings(cwd, projectTrusted, globalSettingsPath, concurrency) {
25
+ const resolution = resolveWorkflowSettings(cwd, projectTrusted, globalSettingsPath);
26
+ const settings = Object.freeze({ ...resolution.effective, ...(concurrency === undefined ? {} : { concurrency }) });
27
+ return { settings, resolution, resourcePolicy: resolveAgentResourcePolicy(cwd, projectTrusted, globalSettingsPath), modelSettingsPath: resolution.sources.modelAliases };
28
+ }
29
+ function frozenResourcePolicy(policy) { return () => structuredClone(policy); }
30
+ function resumedSnapshotSettings(snapshot, resolution, modelAliases) {
31
+ const settings = { ...snapshot.settings, concurrency: snapshot.settingsSources === undefined || snapshot.settingsSources.concurrency === "per-run options" ? snapshot.settings.concurrency : resolution.effective.concurrency, modelAliases };
32
+ if (resolution.effective.disabledAgentResources === undefined)
33
+ delete settings.disabledAgentResources;
34
+ else
35
+ settings.disabledAgentResources = resolution.effective.disabledAgentResources;
36
+ const settingsSources = snapshot.settingsSources === undefined ? undefined : { ...snapshot.settingsSources, modelAliases: resolution.sources.modelAliases, disabledAgentResources: resolution.sources.disabledAgentResources, concurrency: snapshot.settingsSources.concurrency === "per-run options" ? "per-run options" : resolution.sources.concurrency };
37
+ return { settings, ...(settingsSources === undefined ? {} : { settingsSources }) };
38
+ }
20
39
  const WORKFLOW_FAILURE_DIAGNOSTICS = Symbol("workflowFailureDiagnostics");
21
40
  function workflowDetail(message) {
22
41
  const detail = message.trim().replace(new RegExp(`\\b(?:${ERROR_CODES.join("|")})\\b:?\\s*`, "g"), "").replace(/^\s*[A-Z][A-Z0-9_]+:\s*/, "").split("\n").filter((line) => !/^\s*at\s/.test(line)).join("\n").replace(/^Run \S+(?=\s(?:exceeded|is))/i, "Run").replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, "the workflow").replace(/^(?:Pi )session \S+(?=\s(?:is|has))/i, "session").replace(/^(Unknown scheduler run|Missing production ownership record|Persisted agent belongs to another run):\s*\S+/i, "$1").replace(/\b(?:runId|sessionId|callSite|occurrence|failedAt|id)[:=]\s*\S+/gi, "").replace(/\s{2,}/g, " ").trim();
@@ -160,6 +179,116 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
160
179
  parentRunId: Type.Optional(Type.String({ description: "Terminal run whose named worktrees may be reused" })),
161
180
  });
162
181
  export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
182
+ function phaseNames(source) {
183
+ const phases = source === undefined ? [] : Array.isArray(source) ? source : source.phases ?? [];
184
+ return phases.filter((phase) => typeof phase === "string" && phase.trim() !== "").map((phase) => phase.trim());
185
+ }
186
+ function phaseAgentCounts(agents) {
187
+ const counts = { total: agents.length, completed: 0, running: 0, failed: 0, cancelled: 0, pending: 0 };
188
+ for (const agent of agents) {
189
+ if (agent.state === "completed")
190
+ counts.completed += 1;
191
+ else if (agent.state === "running")
192
+ counts.running += 1;
193
+ else if (agent.state === "failed")
194
+ counts.failed += 1;
195
+ else if (agent.state === "cancelled")
196
+ counts.cancelled += 1;
197
+ else
198
+ counts.pending += 1;
199
+ }
200
+ return counts;
201
+ }
202
+ function phaseState(runState, counts, isLatest) {
203
+ if (!isLatest)
204
+ return "completed";
205
+ if (runState === "failed")
206
+ return "failed";
207
+ if (runState === "stopped")
208
+ return "cancelled";
209
+ if (runState === "interrupted")
210
+ return "interrupted";
211
+ if (runState === "budget_exhausted")
212
+ return "budget_exhausted";
213
+ if (counts.failed > 0)
214
+ return "failed";
215
+ if (counts.cancelled > 0)
216
+ return "cancelled";
217
+ if (counts.running > 0 || counts.pending > 0)
218
+ return "running";
219
+ return runState === "completed" ? "completed" : "running";
220
+ }
221
+ export function buildWorkflowPhaseModel(run, source) {
222
+ const declaredPhases = phaseNames(source);
223
+ const rawHistory = Array.isArray(run.phaseHistory) ? run.phaseHistory : [];
224
+ const observed = [];
225
+ let boundary = 0;
226
+ for (const record of rawHistory) {
227
+ if (!object(record) || typeof record.phase !== "string" || !record.phase.trim() || typeof record.afterAgent !== "number" || !Number.isSafeInteger(record.afterAgent))
228
+ continue;
229
+ boundary = Math.max(boundary, Math.min(run.agents.length, Math.max(0, record.afterAgent)));
230
+ observed.push({ name: record.phase.trim(), afterAgent: boundary });
231
+ }
232
+ if (!observed.length && typeof run.phase === "string" && run.phase.trim())
233
+ observed.push({ name: run.phase.trim(), afterAgent: 0 });
234
+ const observedEntries = observed.map((entry, index) => ({ ...entry, index, agents: run.agents.slice(entry.afterAgent, observed[index + 1]?.afterAgent ?? run.agents.length) }));
235
+ const matchedDeclarations = new Set();
236
+ const declarationIndices = observedEntries.map((entry) => {
237
+ const index = declaredPhases.findIndex((name, candidate) => !matchedDeclarations.has(candidate) && name === entry.name);
238
+ if (index >= 0)
239
+ matchedDeclarations.add(index);
240
+ return index >= 0 ? index : undefined;
241
+ });
242
+ const entries = observedEntries.map((entry, index) => ({ name: entry.name, observedIndex: index, ...(declarationIndices[index] === undefined ? {} : { declarationIndex: declarationIndices[index] }) }));
243
+ for (const [declarationIndex, name] of declaredPhases.entries()) {
244
+ if (matchedDeclarations.has(declarationIndex))
245
+ continue;
246
+ const insertion = entries.findIndex((entry) => entry.declarationIndex !== undefined && entry.declarationIndex > declarationIndex);
247
+ const pending = { name };
248
+ if (insertion < 0)
249
+ entries.push(pending);
250
+ else
251
+ entries.splice(insertion, 0, pending);
252
+ }
253
+ const occurrences = new Map();
254
+ const phases = entries.map((entry) => {
255
+ const occurrence = (occurrences.get(entry.name) ?? 0) + 1;
256
+ occurrences.set(entry.name, occurrence);
257
+ const observation = entry.observedIndex === undefined ? undefined : observedEntries[entry.observedIndex];
258
+ const agents = observation?.agents ?? [];
259
+ const counts = phaseAgentCounts(agents);
260
+ const state = observation ? phaseState(run.state, counts, entry.observedIndex === observedEntries.length - 1) : "not started";
261
+ return { id: `${entry.name}#${String(occurrence)}`, name: entry.name, occurrence, state, status: state, observed: observation !== undefined, ...(observation ? { afterAgent: observation.afterAgent } : {}), agents, counts };
262
+ });
263
+ let currentPhaseIndex;
264
+ for (let index = phases.length - 1; index >= 0; index -= 1) {
265
+ if (phases[index]?.observed) {
266
+ currentPhaseIndex = index;
267
+ break;
268
+ }
269
+ }
270
+ const counts = {};
271
+ for (const phase of phases)
272
+ counts[phase.state] = (counts[phase.state] ?? 0) + 1;
273
+ const current = currentPhaseIndex === undefined ? undefined : phases[currentPhaseIndex];
274
+ const assigned = new Set(observedEntries.flatMap(({ agents }) => agents.map((agent) => agent.id)));
275
+ const unassignedAgents = run.agents.filter((agent) => !assigned.has(agent.id));
276
+ const result = { declaredPhases, phases, counts };
277
+ if (current !== undefined && currentPhaseIndex !== undefined) {
278
+ result.currentPhaseIndex = currentPhaseIndex;
279
+ result.currentPhaseId = current.id;
280
+ }
281
+ if (unassignedAgents.length)
282
+ result.unassignedAgents = unassignedAgents;
283
+ return result;
284
+ }
285
+ export function preserveWorkflowPhaseSelection(model, selection) {
286
+ const phase = model.phases.find((candidate) => candidate.id === selection.phaseId) ?? (model.currentPhaseIndex === undefined ? undefined : model.phases[model.currentPhaseIndex]) ?? model.phases[0];
287
+ if (!phase)
288
+ return {};
289
+ const agentId = phase.agents.some((agent) => agent.id === selection.agentId) ? selection.agentId : phase.agents[0]?.id;
290
+ return { phaseId: phase.id, ...(agentId === undefined ? {} : { agentId }) };
291
+ }
163
292
  function agentGroupKey(agent) { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
164
293
  function agentGroupLabel(agents) {
165
294
  const structural = agents[0]?.structuralPath ?? [];
@@ -171,8 +300,13 @@ function agentGroups(agents, allAgents = agents) {
171
300
  const groups = new Map();
172
301
  for (const [index, agent] of agents.entries()) {
173
302
  let depth = 0;
174
- for (let parent = agent.parentId; parent && byId.has(parent); parent = byId.get(parent)?.parentId)
303
+ const seen = new Set([agent.id]);
304
+ for (let parent = agent.parentId; parent && byId.has(parent); parent = byId.get(parent)?.parentId) {
305
+ if (seen.has(parent))
306
+ break;
307
+ seen.add(parent);
175
308
  depth += 1;
309
+ }
176
310
  const key = agentGroupKey(agent);
177
311
  const group = groups.get(key) ?? { agents: [] };
178
312
  group.agents.push({ agent, index, depth });
@@ -239,6 +373,101 @@ function textBlock(text) {
239
373
  invalidate() { },
240
374
  };
241
375
  }
376
+ function styledTextBlock(text) {
377
+ return {
378
+ render(width) {
379
+ return truncateWorkflowProgress(text, width);
380
+ },
381
+ invalidate() { },
382
+ };
383
+ }
384
+ function workflowCatalogBlock(text, expanded) {
385
+ return {
386
+ render(width) {
387
+ const safeWidth = Math.max(1, width);
388
+ if (!expanded)
389
+ return truncateWorkflowProgress(text, safeWidth);
390
+ return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, safeWidth, 0).visualLines.map((line) => line.trimEnd());
391
+ },
392
+ invalidate() { },
393
+ };
394
+ }
395
+ function catalogText(value) { return value.replace(/\s+/g, " ").trim(); }
396
+ function catalogResultValue(result) {
397
+ if (result.details !== undefined)
398
+ return result.details;
399
+ const text = result.content?.find((entry) => entry.type === "text")?.text;
400
+ if (!text)
401
+ return undefined;
402
+ try {
403
+ return JSON.parse(text);
404
+ }
405
+ catch {
406
+ return text;
407
+ }
408
+ }
409
+ function isCatalogIndex(value) {
410
+ return object(value) && Array.isArray(value.functions) && Array.isArray(value.variables);
411
+ }
412
+ function isCatalogFunction(value) {
413
+ return object(value) && typeof value.name === "string" && typeof value.description === "string" && object(value.input) && object(value.output);
414
+ }
415
+ function isCatalogVariable(value) {
416
+ return object(value) && typeof value.name === "string" && typeof value.description === "string" && object(value.schema);
417
+ }
418
+ function isCatalogError(value) {
419
+ return object(value) && object(value.error) && typeof value.error.message === "string";
420
+ }
421
+ function catalogSectionTitle(label, count, theme) {
422
+ return theme.fg("accent", theme.bold(`${label} (${String(count)})`));
423
+ }
424
+ function catalogIndexEntries(entries, theme) {
425
+ const width = Math.max(0, ...entries.map((entry) => entry.name.length));
426
+ return entries.map((entry) => ` ${theme.fg("accent", entry.name.padEnd(width))} ${theme.fg("toolOutput", catalogText(entry.description))}`);
427
+ }
428
+ function formatCatalogIndex(catalog, theme) {
429
+ const aliases = Object.prototype.propertyIsEnumerable.call(catalog, "modelAliases") ? Object.keys(catalog.modelAliases ?? {}).sort().map((name) => ({ name, kind: "static", provenance: "settings" })) : catalog.modelAliasEntries ?? Object.keys(catalog.modelAliases ?? {}).sort().map((name) => ({ name, kind: "static", provenance: "settings" }));
430
+ const aliasWidth = Math.max(0, ...aliases.map(({ name }) => name.length));
431
+ const aliasLines = aliases.map(({ name, kind, provenance }) => ` ${theme.fg("accent", name.padEnd(aliasWidth))} ${theme.fg("toolOutput", `${kind} · ${provenance}`)}`);
432
+ return [
433
+ catalogSectionTitle("Functions", catalog.functions.length, theme),
434
+ ...catalogIndexEntries(catalog.functions, theme),
435
+ "",
436
+ catalogSectionTitle("Variables", catalog.variables.length, theme),
437
+ ...catalogIndexEntries(catalog.variables, theme),
438
+ "",
439
+ catalogSectionTitle("Model aliases", aliases.length, theme),
440
+ ...aliasLines,
441
+ ].join("\n");
442
+ }
443
+ function catalogSchemaLines(schema, theme) {
444
+ const json = JSON.stringify(schema, null, 2);
445
+ return json.split("\n").map((line) => ` ${theme.fg("toolOutput", line)}`);
446
+ }
447
+ function formatCatalogDetail(value, expanded, theme) {
448
+ if ("kind" in value)
449
+ return [theme.fg("accent", theme.bold("Model alias")), ` ${theme.fg("accent", value.name)} ${theme.fg("toolOutput", `${value.kind} · ${value.provenance}`)}`].join("\n");
450
+ const kind = "input" in value ? "Function" : "Variable";
451
+ if (!expanded)
452
+ return [theme.fg("accent", theme.bold(kind)), ` ${theme.fg("accent", value.name)} ${theme.fg("toolOutput", catalogText(value.description))}`, ` ${theme.fg("muted", "version")}: ${theme.fg("toolOutput", value.version)} ${theme.fg("muted", "headline")}: ${theme.fg("toolOutput", catalogText(value.headline))}`].join("\n");
453
+ const lines = [theme.fg("accent", theme.bold(`${kind}: ${value.name}`)), `${theme.fg("muted", "description")}: ${theme.fg("toolOutput", value.description)}`, "", theme.fg("accent", theme.bold("Extension")), ` ${theme.fg("muted", "version")}: ${theme.fg("toolOutput", value.version)}`, ` ${theme.fg("muted", "headline")}: ${theme.fg("toolOutput", value.headline)}`, ` ${theme.fg("muted", "description")}: ${theme.fg("toolOutput", value.extensionDescription)}`, "", theme.fg("accent", theme.bold("Schema"))];
454
+ if ("input" in value)
455
+ lines.push(theme.fg("muted", "Input schema"), ...catalogSchemaLines(value.input, theme), "", theme.fg("muted", "Output schema"), ...catalogSchemaLines(value.output, theme));
456
+ else
457
+ lines.push(theme.fg("muted", "Variable schema"), ...catalogSchemaLines(value.schema, theme));
458
+ return lines.join("\n");
459
+ }
460
+ function formatWorkflowCatalog(value, expanded, theme) {
461
+ if (isCatalogIndex(value))
462
+ return formatCatalogIndex(value, theme);
463
+ if (isCatalogFunction(value) || isCatalogVariable(value))
464
+ return formatCatalogDetail(value, expanded, theme);
465
+ if (object(value) && typeof value.name === "string" && (value.kind === "static" || value.kind === "dynamic"))
466
+ return formatCatalogDetail(value, expanded, theme);
467
+ if (isCatalogError(value))
468
+ return theme.fg("error", value.error.message);
469
+ return theme.fg("error", "The workflow catalog returned an invalid result.");
470
+ }
242
471
  const ANSI_SGR = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`);
243
472
  export function truncateWorkflowProgress(text, width) {
244
473
  const safeWidth = Math.max(1, width);
@@ -263,7 +492,7 @@ function themeWorkflowProgressStyles(theme) {
263
492
  warning: (text) => theme.fg("warning", text),
264
493
  muted: (text) => theme.fg("muted", text),
265
494
  dim: (text) => theme.fg("dim", text),
266
- bold: (text) => theme.bold(text),
495
+ bold: (text) => typeof theme.bold === "function" ? theme.bold(text) : text,
267
496
  };
268
497
  }
269
498
  function workflowProgressBlock(run, theme) {
@@ -317,32 +546,33 @@ function navigatorRunLabels(entries) {
317
546
  return `${glyph} ${run.workflowName}${suffix} ${run.state} ${run.phase ?? ""} ${String(done)}/${String(run.agents.length)} agents${costStr}`;
318
547
  });
319
548
  }
320
- function agentBreadcrumbParts(agent, byId) {
321
- const name = agent.label ?? agent.name;
322
- const parts = agent.parentBreadcrumb ? [agent.parentBreadcrumb] : [];
549
+ export function agentBreadcrumbParts(agent, byId, includeStructuralPath = false) {
550
+ const leaf = agent.label ?? agent.name;
551
+ const parts = includeStructuralPath && agent.structuralPath?.length ? [agent.structuralPath.join(" > ")] : [];
552
+ if (agent.parentBreadcrumb)
553
+ parts.push(agent.parentBreadcrumb);
554
+ const ancestors = [];
323
555
  const seen = new Set([agent.id]);
324
556
  for (let parentId = agent.parentId; parentId; parentId = byId.get(parentId)?.parentId) {
325
557
  if (seen.has(parentId))
326
- break; // ponytail: cycle guard for corrupt data
558
+ break;
327
559
  seen.add(parentId);
328
560
  const parent = byId.get(parentId);
329
- if (parent)
330
- parts.push(parent.label ?? parent.name);
331
- else
561
+ if (!parent)
332
562
  break;
563
+ ancestors.push(parent.label ?? parent.name);
333
564
  }
334
- parts.push(name);
565
+ parts.push(...ancestors.reverse(), leaf);
335
566
  return parts;
336
567
  }
337
- function agentBreadcrumb(agent, byId) {
338
- const parts = agentBreadcrumbParts(agent, byId);
339
- return parts.length > 1 ? parts.join(" > ") : parts[0] ?? "";
568
+ export function agentBreadcrumb(agent, byId, includeStructuralPath = false) {
569
+ return agentBreadcrumbParts(agent, byId, includeStructuralPath).join(" > ");
340
570
  }
341
571
  function styledAgentBreadcrumb(agent, byId, styles) {
342
572
  const parts = agentBreadcrumbParts(agent, byId);
343
573
  if (parts.length <= 1)
344
574
  return parts[0] ?? "";
345
- return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${parts[parts.length - 1] ?? ""}`;
575
+ return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
346
576
  }
347
577
  function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
348
578
  const label = agent.activity?.kind === "reasoning" ? "reasoning" : agent.activity?.kind === "text" ? "responding" : agent.activity?.kind === "tool" ? agent.activity.text : [...(agent.toolCalls ?? [])].reverse().find(({ state }) => state === "running")?.name ?? "";
@@ -401,6 +631,7 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
401
631
  `Launch cwd: ${run.cwd}`,
402
632
  ...formatCompactBudgetStatus(run),
403
633
  `Launch models: ${snapshot.models.join(", ") || "(none)"}`,
634
+ `Settings: concurrency=${String(snapshot.settings.concurrency)}`,
404
635
  ];
405
636
  if (run.error)
406
637
  lines.push(`Run error: ${run.error.code}: ${run.error.message}`);
@@ -409,6 +640,8 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
409
640
  const aliases = snapshot.modelAliases ?? snapshot.settings.modelAliases;
410
641
  if (aliases && Object.keys(aliases).length)
411
642
  lines.push(`Model aliases: ${Object.entries(aliases).map(([name, target]) => `${name}=${target}`).join(", ")}`);
643
+ if (snapshot.settingsSources)
644
+ lines.push(`Settings sources: concurrency=${snapshot.settingsSources.concurrency}, modelAliases=${snapshot.settingsSources.modelAliases}, disabledAgentResources=${snapshot.settingsSources.disabledAgentResources}`);
412
645
  lines.push("Agents / ownership:");
413
646
  if (!run.agents.length)
414
647
  lines.push(" (none)");
@@ -435,6 +668,69 @@ export function formatNavigatorRun(loaded, checkpoints, _worktrees) {
435
668
  lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
436
669
  return lines.join("\n");
437
670
  }
671
+ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
672
+ const safeWidth = Math.max(1, width);
673
+ const model = buildWorkflowPhaseModel(run, snapshot);
674
+ const wrap = (text, limit = safeWidth) => truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, limit), 0).visualLines.map((line) => line.trimEnd());
675
+ const phaseStyle = (state) => state === "completed" ? (text) => styles.success(text) : state === "failed" || state === "cancelled" ? (text) => styles.error(text) : state === "running" ? (text) => styles.accent(text) : state === "interrupted" || state === "budget_exhausted" ? (text) => styles.warning(text) : (text) => styles.muted(text);
676
+ const phaseName = (phase) => `${phase.name}${phase.occurrence > 1 ? ` #${String(phase.occurrence)}` : ""}`;
677
+ const phaseCounts = (phase) => `agents completed=${String(phase.counts.completed)} running=${String(phase.counts.running)} failed=${String(phase.counts.failed)} cancelled=${String(phase.counts.cancelled)} pending=${String(phase.counts.pending)}`;
678
+ const phaseLine = (phase, selected) => `${selected ? "→" : " "} ${phaseName(phase)} · ${phaseStyle(phase.state)(phase.state)} · ${phaseCounts(phase)}`;
679
+ const stateNames = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
680
+ const statusSummary = stateNames.filter((state) => (model.counts[state] ?? 0) > 0).map((state) => `${String(model.counts[state])} ${state}`).join(" · ") || "0 phases";
681
+ const selectedIndex = selection.phaseId ? model.phases.findIndex((phase) => phase.id === selection.phaseId) : -1;
682
+ const activeIndex = selectedIndex >= 0 ? selectedIndex : model.currentPhaseIndex ?? (model.phases.length ? 0 : -1);
683
+ const selectedPhase = activeIndex >= 0 ? model.phases[activeIndex] : undefined;
684
+ const lines = [styles.bold(styles.accent(`Workflow: ${run.workflowName}`))];
685
+ if (run.error)
686
+ lines.push(styles.error(`ERROR ${run.error.code}: ${run.error.message}`));
687
+ lines.push(`phase: ${run.phase ?? selectedPhase?.name ?? "none"}`, `Run state: ${run.state}`, `Phases: ${statusSummary}`);
688
+ for (const event of run.events ?? [])
689
+ lines.push(styles.warning(`Warning: ${event.message}`));
690
+ lines.push(...formatCompactBudgetStatus(run));
691
+ const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
692
+ const renderAgentLines = (agents, selectedAgentId) => {
693
+ if (!agents.length)
694
+ return [styles.muted("Agents: none")];
695
+ const rendered = [];
696
+ for (const agent of agents) {
697
+ const selected = agent.id === selectedAgentId;
698
+ const stateStyle = progressStyleForState(agent.state, styles);
699
+ const icon = agent.state === "completed" ? "✓" : agent.state === "failed" || agent.state === "cancelled" ? "✗" : agent.state === "running" ? "⠦" : "○";
700
+ rendered.push(`${selected ? "→" : " "} ${stateStyle(icon)} ${styledAgentBreadcrumb(agent, byId, styles)} · ${stateStyle(agent.state)}`);
701
+ if (agent.state === "failed") {
702
+ const error = agent.attemptDetails?.at(-1)?.error;
703
+ if (error)
704
+ rendered.push(styles.error(` error: ${error.code}: ${error.message}`));
705
+ }
706
+ }
707
+ return rendered;
708
+ };
709
+ if (!model.phases.length) {
710
+ lines.push(styles.muted("Phases: no declared or observed phases (legacy snapshot)"), styles.bold("Agents"), ...renderAgentLines(run.agents, selection.agentId));
711
+ return lines.flatMap((line) => wrap(line));
712
+ }
713
+ const selectedAgent = selectedPhase?.agents.find((agent) => agent.id === selection.agentId) ?? selectedPhase?.agents[0];
714
+ if (safeWidth >= 80) {
715
+ const sidebarWidth = Math.min(38, Math.max(24, Math.floor(safeWidth * 0.36)));
716
+ const detailWidth = Math.max(1, safeWidth - sidebarWidth - 3);
717
+ const sidebar = [styles.bold("Phases"), ...model.phases.flatMap((phase, index) => wrap(phaseLine(phase, index === activeIndex), sidebarWidth))];
718
+ const detail = selectedPhase ? [styles.bold(`Phase: ${phaseName(selectedPhase)}`), ...wrap(`Status: ${phaseStyle(selectedPhase.state)(selectedPhase.state)}`, detailWidth), ...wrap(phaseCounts(selectedPhase), detailWidth), "Agents", ...renderAgentLines(selectedPhase.agents, selectedAgent?.id).flatMap((line) => wrap(line, detailWidth))] : [styles.muted("No phase is selected")];
719
+ const rows = Math.max(sidebar.length, detail.length);
720
+ for (let index = 0; index < rows; index += 1)
721
+ lines.push(`${sidebar[index] ?? ""} | ${detail[index] ?? ""}`);
722
+ }
723
+ else {
724
+ lines.push(styles.bold("Phases"));
725
+ for (const [index, phase] of model.phases.entries())
726
+ lines.push(...wrap(phaseLine(phase, index === activeIndex)));
727
+ if (selectedPhase)
728
+ lines.push("", styles.bold(`Selected phase: ${phaseName(selectedPhase)}`), ...wrap(`Status: ${phaseStyle(selectedPhase.state)(selectedPhase.state)}`), ...wrap(phaseCounts(selectedPhase)), styles.bold("Agents"), ...renderAgentLines(selectedPhase.agents, selectedAgent?.id).flatMap((line) => wrap(line)));
729
+ }
730
+ if (model.unassignedAgents?.length)
731
+ lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
732
+ return lines.flatMap((line) => wrap(line));
733
+ }
438
734
  function formatCheckpointReview(checkpoint) {
439
735
  const context = JSON.stringify(checkpoint.context, null, 2);
440
736
  return [`Name: ${checkpoint.name}`, "Prompt:", checkpoint.prompt, context === "null" ? "Context: null" : "Context:", ...(context === "null" ? [] : [context])].join("\n");
@@ -938,6 +1234,38 @@ function contextHostCapabilities(ctx) {
938
1234
  const getAvailable = registry.getAvailable;
939
1235
  return { modelRegistry: { ...(isModelRegistryGetter(getAll) ? { getAll: () => getAll.call(registry) } : {}), ...(isModelRegistryGetter(getAvailable) ? { getAvailable: () => getAvailable.call(registry) } : {}) } };
940
1236
  }
1237
+ function modelInventory(root, registry) {
1238
+ const all = registry?.getAll?.() ?? registry?.getAvailable?.() ?? [];
1239
+ const available = registry?.getAvailable?.() ?? registry?.getAll?.() ?? [];
1240
+ const knownModels = new Set(all.map((model) => `${model.provider}/${model.id}`));
1241
+ const availableModels = new Set(available.map((model) => `${model.provider}/${model.id}`));
1242
+ const rootName = root?.provider && root.model ? `${root.provider}/${root.model}` : undefined;
1243
+ if (rootName) {
1244
+ knownModels.add(rootName);
1245
+ availableModels.add(rootName);
1246
+ }
1247
+ return { knownModels, availableModels };
1248
+ }
1249
+ function resumeHostContext(ctx) {
1250
+ const model = object(ctx) && object(ctx.model) && typeof ctx.model.provider === "string" && typeof ctx.model.id === "string" ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
1251
+ return { model, modelRegistry: contextHostCapabilities(ctx).modelRegistry };
1252
+ }
1253
+ async function resolveLaunchAliases(registry, staticAliases, context, availableModels, knownModels, settingsPath) {
1254
+ const dynamic = typeof registry.resolveModelAliases === "function" ? await registry.resolveModelAliases(context, new Set(Object.keys(staticAliases))) : {};
1255
+ const dynamicNames = Object.keys(dynamic);
1256
+ try {
1257
+ const aliases = validateModelAliases({ ...dynamic, ...staticAliases }, settingsPath);
1258
+ validateModelAliasAvailability(aliases, dynamicNames, availableModels, knownModels, settingsPath);
1259
+ return { aliases, dynamicNames };
1260
+ }
1261
+ catch (error) {
1262
+ const name = modelAliasErrorName(error);
1263
+ const descriptor = name && typeof registry.modelAliases === "function" ? registry.modelAliases().find((candidate) => candidate.name === name) : undefined;
1264
+ if (descriptor && errorCode(error) !== "CANCELLED")
1265
+ throw new WorkflowError(errorCode(error) ?? "CONFIG_ERROR", `${errorText(error)} (extension: ${descriptor.headline})`);
1266
+ throw error;
1267
+ }
1268
+ }
941
1269
  function isUiSelect(value) { return typeof value === "function"; }
942
1270
  function isUiInput(value) { return typeof value === "function"; }
943
1271
  function isUiSetStatus(value) { return typeof value === "function"; }
@@ -1084,11 +1412,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1084
1412
  };
1085
1413
  const phaseBridge = (store, metadata, lifecycle) => {
1086
1414
  let cursor = 0;
1087
- let executionPhase;
1088
1415
  return async (phase) => {
1089
- if (phase === executionPhase)
1090
- return;
1091
- executionPhase = phase;
1092
1416
  await scheduler.flush();
1093
1417
  await lifecycle.enter();
1094
1418
  try {
@@ -1293,7 +1617,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1293
1617
  run.budget.recordEvent({ type, budgetVersion: request.budgetVersion, dimensions: [], usage: structuredClone(request.consumed), limits: structuredClone(request.proposed), at: Date.now(), proposalId: request.proposalId, previous: structuredClone(request.previous), proposed: structuredClone(request.proposed) });
1294
1618
  await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
1295
1619
  };
1296
- const answerBudgetDecision = async (runId, proposalId, approved, silent = false) => {
1620
+ const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal) => {
1297
1621
  const run = runs.get(runId);
1298
1622
  if (!run)
1299
1623
  return undefined;
@@ -1301,7 +1625,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1301
1625
  if (!request)
1302
1626
  return undefined;
1303
1627
  await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
1304
- const result = await applyBudgetDecision(request, approved);
1628
+ const result = await applyBudgetDecision(request, approved, context, signal);
1305
1629
  if (!silent)
1306
1630
  deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
1307
1631
  return result;
@@ -1362,10 +1686,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1362
1686
  label: "Workflow Respond",
1363
1687
  description: "Approve or reject one pending workflow checkpoint or budget decision",
1364
1688
  parameters: Type.Object({ runId: Type.String(), name: Type.Optional(Type.String()), proposalId: Type.Optional(Type.String()), approved: Type.Boolean() }, { additionalProperties: false }),
1365
- async execute(_id, params) {
1689
+ async execute(_id, params, signal, _onUpdate, ctx) {
1366
1690
  try {
1367
1691
  if (params.proposalId) {
1368
- const result = await answerBudgetDecision(params.runId, params.proposalId, params.approved);
1692
+ const result = await answerBudgetDecision(params.runId, params.proposalId, params.approved, false, ctx, signal);
1369
1693
  if (!result) {
1370
1694
  const denied = { state: "budget_exhausted", approved: false, reason: "proposal_not_pending" };
1371
1695
  return { content: [{ type: "text", text: JSON.stringify(denied) }], details: denied };
@@ -1399,22 +1723,31 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1399
1723
  });
1400
1724
  let catalogRegistered = false;
1401
1725
  let sessionStarted = false;
1402
- const registerCatalog = () => {
1726
+ const registerCatalog = (cwd, trustedProject) => {
1403
1727
  if (catalogRegistered || !pi.getActiveTools().includes("workflow"))
1404
1728
  return;
1405
- const catalog = registry.catalog();
1406
- const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0;
1407
- if (!catalog.functions.length && !catalog.variables.length && !hasAliases)
1729
+ const catalog = registry.catalog({ cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) });
1730
+ const hasAliases = Object.keys(catalog.modelAliases ?? {}).length > 0 || Boolean(catalog.modelAliasEntries?.length);
1731
+ const hasSettings = catalog.settings !== undefined && [catalog.settings.globalSettingsPath, catalog.settings.projectSettingsPath].some((path) => existsSync(path));
1732
+ if (!catalog.functions.length && !catalog.variables.length && !hasAliases && !hasSettings)
1408
1733
  return;
1409
1734
  pi.registerTool({
1410
1735
  name: "workflow_catalog",
1411
1736
  label: "Workflow Catalog",
1412
1737
  description: "List reusable workflow functions, variables, and model aliases, or load one entry in full",
1413
- parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function or variable name for full detail" })) }, { additionalProperties: false }),
1738
+ parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function, variable, or model alias name for full detail" })) }, { additionalProperties: false }),
1414
1739
  async execute(_id, params = {}) {
1415
- const result = params.name === undefined ? registry.catalogIndex() : registry.catalogDetail(params.name);
1740
+ const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
1741
+ const result = params.name === undefined ? registry.catalogIndex(context) : registry.catalogDetail(params.name, context);
1416
1742
  return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
1417
- }
1743
+ },
1744
+ renderCall(args, theme) {
1745
+ const title = theme.fg("toolTitle", theme.bold("workflow_catalog"));
1746
+ return styledTextBlock(args.name === undefined ? title : `${title} ${theme.fg("accent", args.name)}`);
1747
+ },
1748
+ renderResult(result, options, theme) {
1749
+ return workflowCatalogBlock(formatWorkflowCatalog(catalogResultValue(result), options.expanded, theme), options.expanded);
1750
+ },
1418
1751
  });
1419
1752
  catalogRegistered = true;
1420
1753
  };
@@ -1425,20 +1758,23 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1425
1758
  if (missing)
1426
1759
  throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
1427
1760
  const settingsPath = workflowSettingsPath(extensionAgentDir);
1428
- const currentSettings = loadSettings(settingsPath);
1429
- resolveAgentResourcePolicy(run.store.cwd, run.projectTrusted(), settingsPath);
1430
- const currentAliases = currentSettings.modelAliases ?? {};
1761
+ const trustedProject = context?.projectTrusted ?? run.projectTrusted();
1762
+ const resolution = resolveWorkflowSettings(run.store.cwd, trustedProject, settingsPath);
1763
+ const currentPolicy = resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
1764
+ const staticAliases = resolution.effective.modelAliases ?? {};
1431
1765
  const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
1432
- const modelRegistry = context?.modelRegistry;
1433
- const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`));
1434
- if (context?.model)
1435
- knownModels.add(`${context.model.provider}/${context.model.id}`);
1436
- const resumeModels = modelRegistry ? knownModels : new Set([...loaded.snapshot.models, ...knownModels]);
1766
+ const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
1767
+ const inventory = modelInventory(rootModel, context?.modelRegistry);
1768
+ const knownModels = context?.modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
1769
+ const availableModels = context?.modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
1770
+ const currentAliases = (await resolveLaunchAliases(registry, staticAliases, { cwd: run.store.cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: run.abortController.signal }, availableModels, knownModels, settingsPath)).aliases;
1437
1771
  const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
1438
1772
  const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
1439
- const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
1773
+ const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
1774
+ const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
1440
1775
  await run.store.saveSnapshot(snapshot);
1441
- 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);
1776
+ scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
1777
+ 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, knownModels, 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: frozenResourcePolicy(currentPolicy) }, createSession);
1442
1778
  run.executor.setRunContext(workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, run.abortController.signal));
1443
1779
  const drift = aliasDrift(previousAliases, currentAliases);
1444
1780
  if (drift.length)
@@ -1462,28 +1798,35 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1462
1798
  if (missing)
1463
1799
  throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
1464
1800
  const settingsPath = workflowSettingsPath(extensionAgentDir);
1465
- const currentSettings = loadSettings(settingsPath);
1466
- resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
1467
- const currentAliases = currentSettings.modelAliases ?? {};
1801
+ const resolution = resolveWorkflowSettings(run.store.cwd, trustedProject, settingsPath);
1802
+ const currentPolicy = resolveAgentResourcePolicy(run.store.cwd, trustedProject, settingsPath);
1803
+ const staticAliases = resolution.effective.modelAliases ?? {};
1468
1804
  const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
1469
- const modelRegistry = context?.modelRegistry;
1470
- const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`));
1471
- if (context?.model)
1472
- knownModels.add(`${context.model.provider}/${context.model.id}`);
1473
- const resumeModels = modelRegistry ? knownModels : new Set([...loaded.snapshot.models, ...knownModels]);
1805
+ const rootModel = context?.model ? { ...run.model, provider: context.model.provider, model: context.model.id } : run.model;
1806
+ const inventory = modelInventory(rootModel, context?.modelRegistry);
1807
+ const knownModels = context?.modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
1808
+ const availableModels = context?.modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
1809
+ const controller = new AbortController();
1810
+ if (context?.signal?.aborted)
1811
+ controller.abort();
1812
+ else {
1813
+ context?.signal?.addEventListener("abort", () => { controller.abort(); }, { once: true });
1814
+ }
1815
+ run.abortController = controller;
1816
+ const currentAliases = context?.resolvedAliases ?? (await resolveLaunchAliases(registry, staticAliases, { cwd: run.store.cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: controller.signal }, availableModels, knownModels, settingsPath)).aliases;
1474
1817
  const resumeAliases = { ...previousAliases, ...currentAliases };
1475
- const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
1476
- const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
1818
+ const blockedAliases = context?.blockedAliases ?? new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
1819
+ const blockedAliasTargets = context?.blockedAliasTargets ?? Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
1477
1820
  const script = launchScriptForSnapshot(loaded.snapshot, registry);
1478
- 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, true);
1479
- const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, settings: { ...loaded.snapshot.settings, modelAliases: currentAliases }, modelAliases: currentAliases });
1821
+ preflight(script, { models: availableModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata, true);
1822
+ const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
1823
+ const snapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
1480
1824
  await run.store.saveSnapshot(snapshot);
1481
- 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);
1825
+ scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
1826
+ run.executor = new WorkflowAgentExecutor({ cwd: run.store.cwd, agentDir: extensionAgentDir, model: rootModel, tools: new Set(snapshot.tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog")), availableModels, knownModels, 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: frozenResourcePolicy(currentPolicy) }, createSession);
1482
1827
  const drift = aliasDrift(previousAliases, currentAliases);
1483
1828
  if (drift.length)
1484
1829
  await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
1485
- const controller = new AbortController();
1486
- run.abortController = controller;
1487
1830
  const runContext = workflowRunContext(run.store.cwd, run.store.sessionId, run.store.runId, loaded.snapshot.metadata, loaded.snapshot.args, controller.signal);
1488
1831
  run.executor.setRunContext(runContext);
1489
1832
  let variables;
@@ -1562,7 +1905,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1562
1905
  run.completion = completion;
1563
1906
  void completion;
1564
1907
  };
1565
- const applyBudgetDecision = async (request, approved) => {
1908
+ const applyBudgetDecision = async (request, approved, context, signal) => {
1566
1909
  const run = runs.get(request.runId);
1567
1910
  if (!run)
1568
1911
  throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
@@ -1576,10 +1919,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1576
1919
  next.budget = nextBudget;
1577
1920
  else
1578
1921
  delete next.budget; return next; });
1579
- await coldResumeRun(run, false, {}, true);
1922
+ await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
1580
1923
  return { state: "running", approved: true };
1581
1924
  };
1582
- const resumeWorkflowRun = async (runId, rawPatch) => {
1925
+ const resumeWorkflowRun = async (runId, rawPatch, context, signal) => {
1583
1926
  const run = runs.get(runId);
1584
1927
  if (!run)
1585
1928
  throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${runId}`);
@@ -1610,11 +1953,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1610
1953
  else
1611
1954
  delete next.budget; return next; });
1612
1955
  }
1613
- await coldResumeRun(run, false, {}, true);
1956
+ await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
1614
1957
  return { state: "running" };
1615
1958
  };
1616
1959
  const retryReservations = new Set();
1617
- const retryWorkflowRun = async (runId, context) => {
1960
+ const retryWorkflowRun = async (runId, context, signal) => {
1618
1961
  if (typeof runId !== "string" || !runId.trim())
1619
1962
  throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
1620
1963
  const host = object(context) ? context : {};
@@ -1674,19 +2017,23 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1674
2017
  if (missing)
1675
2018
  throw new WorkflowError("RESUME_INCOMPATIBLE", `Required tool is unavailable: ${missing}`);
1676
2019
  const settingsPath = workflowSettingsPath(extensionAgentDir);
1677
- const currentSettings = loadSettings(settingsPath);
1678
- resolveAgentResourcePolicy(cwd, trustedProject, settingsPath);
1679
- const currentAliases = currentSettings.modelAliases ?? {};
2020
+ const resolution = resolveWorkflowSettings(cwd, trustedProject, settingsPath);
2021
+ const currentPolicy = resolveAgentResourcePolicy(cwd, trustedProject, settingsPath);
2022
+ const staticAliases = resolution.effective.modelAliases ?? {};
1680
2023
  const previousAliases = loaded.snapshot.modelAliases ?? loaded.snapshot.settings.modelAliases ?? {};
1681
2024
  const modelRegistry = contextHostCapabilities(context).modelRegistry;
1682
- const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`));
1683
2025
  const hostModel = object(host.model) && typeof host.model.provider === "string" && typeof host.model.id === "string" ? { provider: host.model.provider, id: host.model.id } : { provider: "", id: "" };
1684
- if (hostModel.provider && hostModel.id)
1685
- knownModels.add(`${hostModel.provider}/${hostModel.id}`);
1686
- const resumeModels = modelRegistry ? knownModels : new Set([...loaded.snapshot.models, ...knownModels]);
2026
+ const rootModel = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
2027
+ const inventory = modelInventory(rootModel, modelRegistry);
2028
+ const knownModels = modelRegistry ? inventory.knownModels : new Set([...loaded.snapshot.models, ...inventory.knownModels]);
2029
+ const availableModels = modelRegistry ? inventory.availableModels : new Set([...loaded.snapshot.models, ...inventory.availableModels]);
2030
+ const resolvedAliases = await resolveLaunchAliases(registry, staticAliases, { cwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: signal ?? new AbortController().signal }, availableModels, knownModels, settingsPath);
2031
+ const currentAliases = resolvedAliases.aliases;
1687
2032
  const resumeAliases = { ...previousAliases, ...currentAliases };
2033
+ const blockedAliases = new Set(Object.keys(previousAliases).filter((name) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
2034
+ const blockedAliasTargets = Object.fromEntries(Object.entries(previousAliases).filter(([name]) => !Object.prototype.hasOwnProperty.call(currentAliases, name)));
1688
2035
  const script = launchScriptForSnapshot(loaded.snapshot, registry);
1689
- 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, true);
2036
+ preflight(script, { models: availableModels, tools: active, agentTypes: new Set(loaded.snapshot.agentTypes), modelAliases: resumeAliases, knownModels, settingsPath, skipModelAvailability: true }, loaded.snapshot.schemas, loaded.snapshot.metadata, true);
1690
2037
  await sourceStore.validateNamedWorktrees();
1691
2038
  for (const name of loaded.run.retry?.namedWorktrees ?? [])
1692
2039
  await sourceStore.resolveNamedWorktree(name);
@@ -1696,7 +2043,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1696
2043
  const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
1697
2044
  const childRunId = randomUUID();
1698
2045
  const childStore = new RunStore(cwd, sessionId, childRunId, home);
1699
- const childSnapshot = createLaunchSnapshot(loaded.snapshot);
2046
+ const refreshed = resumedSnapshotSettings(loaded.snapshot, resolution, currentAliases);
2047
+ const childSnapshot = createLaunchSnapshot({ ...loaded.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
1700
2048
  const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
1701
2049
  const childInitialBudget = childBudget.snapshot();
1702
2050
  const retry = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
@@ -1705,13 +2053,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1705
2053
  const model = modelSpec(loaded.snapshot.models[0] ?? "", fallbackModel);
1706
2054
  const lifecycle = lifecycleFor(childStore, "interrupted", childBudget, loaded.snapshot.metadata);
1707
2055
  const abortController = new AbortController();
1708
- const providerErrorRecovery = createProviderErrorRecovery(context, resumeModels, () => { abortController.abort(); });
2056
+ const providerErrorRecovery = createProviderErrorRecovery(context, availableModels, () => { abortController.abort(); });
1709
2057
  const providerPause = async () => { deliver(pi, `Workflow ${loaded.snapshot.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
1710
- const childRun = { executor: new WorkflowAgentExecutor({ cwd, agentDir: extensionAgentDir, model, tools: new Set(loaded.snapshot.tools.filter((tool) => active.has(tool) || tool === "workflow_catalog")), availableModels: resumeModels, knownModels: resumeModels, modelAliases: currentAliases, settingsPath, agentDefinitions: loaded.snapshot.roles ?? {}, runStore: childStore, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: () => resolveAgentResourcePolicy(cwd, projectTrusted(context), settingsPath) }, createSession), store: childStore, metadata: loaded.snapshot.metadata, model, lifecycle, budget: childBudget, abortController, projectTrusted: () => projectTrusted(context), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) };
2058
+ const childRun = { executor: new WorkflowAgentExecutor({ cwd, agentDir: extensionAgentDir, model, tools: new Set(loaded.snapshot.tools.filter((tool) => active.has(tool) || tool === "workflow_catalog")), availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: loaded.snapshot.roles ?? {}, runStore: childStore, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: frozenResourcePolicy(currentPolicy) }, createSession), store: childStore, metadata: loaded.snapshot.metadata, model, lifecycle, budget: childBudget, abortController, projectTrusted: () => projectTrusted(context), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) };
1711
2059
  runs.set(childRunId, childRun);
1712
2060
  scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
1713
2061
  await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
1714
- await coldResumeRun(childRun, false, {}, trustedProject, { model: hostModel, modelRegistry: modelRegistry ? { getAll: () => [...(modelRegistry.getAll?.() ?? [])], getAvailable: () => [...(modelRegistry.getAvailable?.() ?? [])] } : undefined });
2062
+ await coldResumeRun(childRun, false, {}, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) });
1715
2063
  const completion = runs.get(childRunId)?.completion;
1716
2064
  if (completion) {
1717
2065
  childStarted = true;
@@ -1729,9 +2077,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1729
2077
  label: "Workflow Retry",
1730
2078
  description: "Retry a failed workflow run by replaying its completed structural operations",
1731
2079
  parameters: WORKFLOW_RETRY_PARAMETERS,
1732
- async execute(_id, params, _signal, _onUpdate, ctx) {
2080
+ async execute(_id, params, signal, _onUpdate, ctx) {
1733
2081
  try {
1734
- const result = await retryWorkflowRun(params.runId, ctx);
2082
+ const result = await retryWorkflowRun(params.runId, ctx, signal);
1735
2083
  return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
1736
2084
  }
1737
2085
  catch (error) {
@@ -1744,9 +2092,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1744
2092
  label: "Workflow Resume",
1745
2093
  description: "Resume an exhausted workflow with unchanged or patched aggregate budgets",
1746
2094
  parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
1747
- async execute(_id, params) {
2095
+ async execute(_id, params, signal, _onUpdate, ctx) {
1748
2096
  try {
1749
- const result = await resumeWorkflowRun(params.runId, params.budget);
2097
+ const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal);
1750
2098
  return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
1751
2099
  }
1752
2100
  catch (error) {
@@ -1759,7 +2107,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1759
2107
  return;
1760
2108
  sessionStarted = true;
1761
2109
  registry.freeze();
1762
- registerCatalog();
2110
+ registerCatalog(ctx.cwd, projectTrusted(ctx));
1763
2111
  await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
1764
2112
  try {
1765
2113
  for (const runId of await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)) {
@@ -1795,7 +2143,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1795
2143
  const roleDefinitions = loaded.snapshot.roles ?? {};
1796
2144
  const abortController = new AbortController();
1797
2145
  const providerErrorRecovery = createProviderErrorRecovery(ctx, new Set(loaded.snapshot.models), () => { abortController.abort(); });
1798
- 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 } : {}) });
2146
+ 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.settingsSources?.modelAliases ? { settingsPath: loaded.snapshot.settingsSources.modelAliases } : loaded.snapshot.settingsPath ? { settingsPath: loaded.snapshot.settingsPath } : {}), agentDefinitions: roleDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: frozenResourcePolicy(snapshotResourcePolicy(loaded.snapshot, store.cwd, projectTrusted(ctx), workflowSettingsPath(extensionAgentDir))) }, createSession), store, metadata: loaded.snapshot.metadata, model, lifecycle, budget: budgetRuntime, abortController, projectTrusted: () => projectTrusted(ctx), checkpointResolvers: new Map(), ...(providerErrorRecovery ? { providerErrorRecovery } : {}) });
1799
2147
  for (const checkpoint of await store.awaitingCheckpoints())
1800
2148
  deliver(pi, `Workflow ${loaded.snapshot.metadata.name} checkpoint ${checkpoint.name}: ${checkpoint.prompt}\nContext: ${JSON.stringify(checkpoint.context)}\nRespond with workflow_respond.`);
1801
2149
  for (const decision of await store.pendingWorkflowDecisions())
@@ -1832,7 +2180,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1832
2180
  pi.on("before_agent_start", (event, ctx) => {
1833
2181
  if (!pi.getActiveTools().includes("workflow"))
1834
2182
  return;
1835
- const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx))).filter(([, definition]) => definition.description);
2183
+ const roles = Object.entries(loadAgentDefinitions(ctx.cwd, extensionAgentDir, projectTrusted(ctx), typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : undefined)).filter(([, definition]) => definition.description);
1836
2184
  if (!roles.length)
1837
2185
  return;
1838
2186
  const content = `Workflow role descriptions:\n${roles.map(([name, definition]) => `- \`${name}\`: ${String(definition.description)}`).join("\n")}`;
@@ -1848,32 +2196,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1848
2196
  try {
1849
2197
  const headless = object(ctx) && ctx.headless === true;
1850
2198
  const settingsPath = workflowSettingsPath(extensionAgentDir);
1851
- const defaults = loadSettings(settingsPath);
1852
2199
  if (!ctx.model)
1853
2200
  throw new WorkflowError("UNKNOWN_MODEL", "A launching model is required");
1854
- const settings = Object.freeze({ concurrency: params.concurrency ?? defaults.concurrency, ...(defaults.modelAliases ? { modelAliases: defaults.modelAliases } : {}) });
1855
2201
  const budget = validateBudget(params.budget);
1856
2202
  const rootModel = { provider: ctx.model.provider, model: ctx.model.id, thinking: pi.getThinkingLevel() };
1857
2203
  const rootModelName = `${rootModel.provider}/${rootModel.model}`;
1858
2204
  const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
1859
- const knownModels = new Set((modelRegistry?.getAll?.() ?? modelRegistry?.getAvailable?.() ?? [ctx.model]).map((model) => `${model.provider}/${model.id}`));
1860
- knownModels.add(rootModelName);
1861
- const availableModels = knownModels;
2205
+ const inventory = modelInventory(rootModel, modelRegistry);
2206
+ const knownModels = inventory.knownModels;
2207
+ const availableModels = inventory.availableModels;
1862
2208
  const rootTools = pi.getActiveTools().filter((name) => !["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"].includes(name));
1863
2209
  const trustedProject = projectTrusted(ctx);
1864
- if (typeof ctx.cwd === "string")
1865
- resolveAgentResourcePolicy(ctx.cwd, trustedProject, settingsPath);
1866
- 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);
1867
- const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, functionName } = validated;
1868
- await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
1869
- const runId = randomUUID();
1870
- const args = (params.args ?? null);
1871
- encoded(args);
2210
+ const launchCwd = typeof ctx.cwd === "string" ? ctx.cwd : process.cwd();
2211
+ const launch = workflowLaunchSettings(launchCwd, trustedProject, settingsPath, params.concurrency);
1872
2212
  const runController = new AbortController();
1873
2213
  if (signal?.aborted)
1874
2214
  runController.abort();
1875
2215
  else
1876
2216
  signal?.addEventListener("abort", () => { runController.abort(); }, { once: true });
2217
+ const resolvedAliases = await resolveLaunchAliases(registry, launch.settings.modelAliases ?? {}, { cwd: launchCwd, projectTrusted: trustedProject, rootModel, knownModels, availableModels, signal: runController.signal }, availableModels, knownModels, settingsPath);
2218
+ const modelAliases = resolvedAliases.aliases;
2219
+ const settings = Object.freeze({ ...launch.settings, ...(Object.keys(modelAliases).length ? { modelAliases } : {}) });
2220
+ const validated = validateWorkflowLaunchWithRegistry({ ...params, args: params.args }, { cwd: ctx.cwd, agentDir: extensionAgentDir, projectTrusted: trustedProject, availableModels, rootTools: new Set(rootTools), modelAliases, knownModels, settingsPath }, registry);
2221
+ const { script, checked, agentDefinitions, projectAgentDefinitions, roleNames, functionName } = validated;
2222
+ await ensureSessionLease(ctx.cwd, ctx.sessionManager.getSessionId());
2223
+ const runId = randomUUID();
2224
+ const args = (params.args ?? null);
2225
+ encoded(args);
1877
2226
  const runContext = workflowRunContext(ctx.cwd, ctx.sessionManager.getSessionId(), runId, checked.metadata, args, runController.signal);
1878
2227
  const variables = await resolveWorkflowVariables(runContext, runController, registry);
1879
2228
  const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
@@ -1882,9 +2231,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1882
2231
  await store.validateParentRun(parentRunId);
1883
2232
  const roles = Object.fromEntries(roleNames.map((role) => [role, agentDefinitions[role]]));
1884
2233
  const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
1885
- const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, defaults.modelAliases, knownModels, settingsPath)] : []; });
2234
+ const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
1886
2235
  const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
1887
- 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 });
2236
+ const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(functionName ? { launchKind: "function", functionName } : {}), ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
1888
2237
  const budgetRuntime = new WorkflowBudgetRuntime(budget);
1889
2238
  const initialBudget = budgetRuntime.snapshot();
1890
2239
  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);
@@ -1893,7 +2242,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1893
2242
  const providerPause = async () => { if (background)
1894
2243
  deliver(pi, `Workflow ${checked.metadata.name} paused: provider limit.`); await lifecycle.providerPause(); };
1895
2244
  const providerErrorRecovery = createProviderErrorRecovery(ctx, availableModels, () => { runController.abort(); });
1896
- 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);
2245
+ const executor = new WorkflowAgentExecutor({ cwd: ctx.cwd, agentDir: extensionAgentDir, model: rootModel, tools: new Set(rootTools), availableModels, knownModels, modelAliases, settingsPath, agentDefinitions, runStore: store, providerPause, agentSetupHooks: registry.agentSetupHooks(), agentResourcePolicy: frozenResourcePolicy(launch.resourcePolicy), runContext }, createSession);
1897
2246
  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 } : {}) });
1898
2247
  if (params.foreground && onUpdate)
1899
2248
  onUpdate(workflowToolUpdate((await store.load()).run));
@@ -2027,7 +2376,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2027
2376
  return entries.filter((entry) => entry !== undefined);
2028
2377
  };
2029
2378
  let stores = await loadStores();
2030
- 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.";
2379
+ const usage = "Usage: /workflow [doctor|model-aliases], or /workflow pause|resume|stop|approve|reject|delete <run-id> [checkpoint-name]. Approve/reject are for checkpoints only; use workflow_respond with a proposalId or the navigator's budget controls for budget decisions. Use workflow_resume for budget patches.";
2031
2380
  const setWorkflowStatus = (text) => {
2032
2381
  const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
2033
2382
  setStatus?.call(ctx.ui, "workflow-stop", text);
@@ -2044,7 +2393,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2044
2393
  return keepContext ? "dashboard" : "done";
2045
2394
  }
2046
2395
  if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
2047
- const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true);
2396
+ const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
2048
2397
  ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
2049
2398
  return keepContext ? "dashboard" : "done";
2050
2399
  }
@@ -2069,7 +2418,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2069
2418
  if (action === "resume" && run) {
2070
2419
  if (run.lifecycle.state === "budget_exhausted") {
2071
2420
  const patch = rest.length ? JSON.parse(rest.join(" ")) : undefined;
2072
- const result = await resumeWorkflowRun(run.store.runId, patch);
2421
+ const result = await resumeWorkflowRun(run.store.runId, patch, ctx);
2073
2422
  ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
2074
2423
  }
2075
2424
  else {
@@ -2077,7 +2426,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2077
2426
  await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
2078
2427
  else {
2079
2428
  if (run.lifecycle.state === "paused")
2080
- await refreshPausedRunAliases(run, ctx);
2429
+ await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
2081
2430
  await run.lifecycle.resume();
2082
2431
  }
2083
2432
  ctx.ui.notify(`Resumed workflow ${run.store.runId}.`, "info");
@@ -2088,7 +2437,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2088
2437
  const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
2089
2438
  if (input === undefined)
2090
2439
  return keepContext ? "dashboard" : "done";
2091
- const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input));
2440
+ const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx);
2092
2441
  ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
2093
2442
  return keepContext ? "dashboard" : "done";
2094
2443
  }
@@ -2123,6 +2472,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2123
2472
  };
2124
2473
  const manageAliases = async () => {
2125
2474
  const settingsPath = workflowSettingsPath(extensionAgentDir);
2475
+ let aliasSettingsPath = settingsPath;
2476
+ const trustedProject = projectTrusted(ctx);
2126
2477
  const modelRegistry = contextHostCapabilities(ctx).modelRegistry;
2127
2478
  const available = () => [...new Set((modelRegistry?.getAvailable?.() ?? []).map((model) => `${model.provider}/${model.id}`))].sort();
2128
2479
  const selectTarget = async (aliases) => {
@@ -2136,22 +2487,24 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2136
2487
  };
2137
2488
  const save = (aliases) => {
2138
2489
  try {
2139
- saveModelAliases(settingsPath, aliases);
2140
- ctx.ui.notify(`Saved model aliases to ${settingsPath}.`, "info");
2490
+ saveModelAliases(aliasSettingsPath, aliases);
2491
+ ctx.ui.notify(`Saved model aliases to ${aliasSettingsPath}.`, "info");
2141
2492
  return true;
2142
2493
  }
2143
2494
  catch (error) {
2144
- ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
2495
+ ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
2145
2496
  return false;
2146
2497
  }
2147
2498
  };
2148
2499
  for (;;) {
2149
2500
  let aliases;
2150
2501
  try {
2151
- aliases = loadSettings(settingsPath).modelAliases ?? {};
2502
+ const resolution = resolveWorkflowSettings(ctx.cwd, trustedProject, settingsPath);
2503
+ aliases = resolution.effective.modelAliases ?? {};
2504
+ aliasSettingsPath = resolution.sources.modelAliases;
2152
2505
  }
2153
2506
  catch (error) {
2154
- ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
2507
+ ctx.ui.notify(`${trustedProject ? workflowProjectSettingsPath(ctx.cwd) : settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
2155
2508
  return;
2156
2509
  }
2157
2510
  const names = Object.keys(aliases).sort();
@@ -2173,13 +2526,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2173
2526
  continue;
2174
2527
  const next = { ...aliases, [name]: target };
2175
2528
  try {
2176
- validateModelAliases(next, settingsPath);
2529
+ validateModelAliases(next, aliasSettingsPath);
2177
2530
  }
2178
2531
  catch (error) {
2179
- ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
2532
+ ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
2180
2533
  continue;
2181
2534
  }
2182
- const parsed = resolveModelReference(target, next, new Set(available()), settingsPath);
2535
+ const parsed = resolveModelReference(target, next, new Set(available()), aliasSettingsPath);
2183
2536
  if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
2184
2537
  ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
2185
2538
  if (!await ctx.ui.confirm("Save unknown model?", "Save this target for cross-machine portability?"))
@@ -2195,13 +2548,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2195
2548
  continue;
2196
2549
  const next = { ...aliases, [edit[1]]: target };
2197
2550
  try {
2198
- validateModelAliases(next, settingsPath);
2551
+ validateModelAliases(next, aliasSettingsPath);
2199
2552
  }
2200
2553
  catch (error) {
2201
- ctx.ui.notify(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
2554
+ ctx.ui.notify(`${aliasSettingsPath}: ${error instanceof Error ? error.message : String(error)}`, "error");
2202
2555
  continue;
2203
2556
  }
2204
- const parsed = resolveModelReference(target, next, new Set(available()), settingsPath);
2557
+ const parsed = resolveModelReference(target, next, new Set(available()), aliasSettingsPath);
2205
2558
  if (!available().includes(`${parsed.provider}/${parsed.model}`)) {
2206
2559
  ctx.ui.notify(`Warning: ${target} is not currently available in Pi.`, "warning");
2207
2560
  if (!await ctx.ui.confirm("Save unknown model?", "Save this target for cross-machine portability?"))
@@ -2240,7 +2593,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2240
2593
  const labels = navigatorRunLabels(sorted);
2241
2594
  const terminalStates = new Set(["completed", "failed", "stopped"]);
2242
2595
  const hasCompleted = sorted.some(({ loaded: { run } }) => run.state === "completed");
2243
- const pickerOptions = [...labels, ...(herdrPaneId() ? ["Inspect session in pane"] : []), "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : [])];
2596
+ const hasFailed = sorted.some(({ loaded: { run } }) => run.state === "failed");
2597
+ const pickerOptions = [...labels, ...(herdrPaneId() ? ["Inspect session in pane"] : []), "Model aliases", "Close", ...(hasCompleted ? ["Delete all completed"] : []), ...(hasFailed ? ["Delete all failed"] : [])];
2244
2598
  const runChoice = await ctx.ui.select("Workflows\n", pickerOptions);
2245
2599
  if (!runChoice || runChoice === "Close")
2246
2600
  return;
@@ -2273,6 +2627,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2273
2627
  stores = await loadStores();
2274
2628
  continue;
2275
2629
  }
2630
+ if (runChoice === "Delete all failed") {
2631
+ if (!await ctx.ui.confirm("Delete failed runs?", "Delete all failed workflow runs and their artifacts? This cannot be undone."))
2632
+ continue;
2633
+ for (const entry of sorted) {
2634
+ if (entry.loaded.run.state === "failed") {
2635
+ await entry.store.delete(true);
2636
+ runs.delete(entry.store.runId);
2637
+ terminalRunStates.delete(entry.store.runId);
2638
+ }
2639
+ }
2640
+ ctx.ui.notify("Deleted all failed workflow runs.", "info");
2641
+ stores = await loadStores();
2642
+ continue;
2643
+ }
2276
2644
  const runIndex = labels.indexOf(runChoice);
2277
2645
  if (runIndex < 0)
2278
2646
  return;
@@ -2336,20 +2704,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2336
2704
  addCopy("Copy run path", store.directory, "run path");
2337
2705
  addCopy("Copy run ID", store.runId, "run ID");
2338
2706
  }
2339
- return { dashboard: formatNavigatorDashboard(loaded.run, checkpoints, worktrees), actions, copies, reviews, script: loaded.snapshot.script, agents: loaded.run.agents, worktrees, cwd: loaded.run.cwd };
2707
+ return { dashboard: formatWorkflowPhaseDashboard(loaded.run, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(loaded.run, loaded.snapshot), run: loaded.run, snapshot: loaded.snapshot, actions, copies, reviews, script: loaded.snapshot.script, agents: loaded.run.agents, worktrees, cwd: loaded.run.cwd };
2340
2708
  };
2341
2709
  const selectAgent = async (dashboard) => {
2342
2710
  const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
2343
- const title = (agent) => {
2344
- const parents = [];
2345
- for (let parentId = agent.parentId; parentId; parentId = byId.get(parentId)?.parentId) {
2346
- const parent = byId.get(parentId);
2347
- if (!parent)
2348
- break;
2349
- parents.unshift(parent.label ?? parent.name);
2350
- }
2351
- return [...((agent.structuralPath ?? []).length ? [(agent.structuralPath ?? []).join(" > ")] : []), ...(agent.parentBreadcrumb ? [agent.parentBreadcrumb] : []), ...parents, agent.label ?? agent.name].join(" > ");
2352
- };
2711
+ const title = (agent) => agentBreadcrumb(agent, byId, true);
2353
2712
  const labels = dashboard.agents.map((agent, index) => `#${String(index + 1)} ${title(agent)} [${agent.state}]`);
2354
2713
  const selectedLabel = await ctx.ui.select("Agents", [...labels, "Back"]);
2355
2714
  const selectedIndex = selectedLabel ? labels.indexOf(selectedLabel) : -1;
@@ -2414,8 +2773,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2414
2773
  let disposed = false;
2415
2774
  let stopRequested = false;
2416
2775
  let stopStatus;
2776
+ const initialSelection = preserveWorkflowPhaseSelection(view.phaseModel, {});
2777
+ let selectedPhaseId = initialSelection.phaseId;
2778
+ let selectedAgentId = initialSelection.agentId;
2417
2779
  const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
2418
- const keyLabels = { up: "↑", down: "↓", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
2780
+ const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", tab: "tab", pageUp: "pgup", pageDown: "pgdn", escape: "esc" };
2419
2781
  const keyLabel = (binding, fallback) => {
2420
2782
  const keys = keybindingKeys(keybindings, binding);
2421
2783
  return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
@@ -2429,12 +2791,17 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2429
2791
  return { rows, hintRows, separatorRows, actionViewport, dashboardViewport: available - actionViewport };
2430
2792
  };
2431
2793
  const updateDashboard = async (selectedOption) => {
2794
+ const phaseId = selectedPhaseId;
2795
+ const agentId = selectedAgentId;
2432
2796
  const next = await loadDashboard();
2433
2797
  if (disposed)
2434
2798
  return;
2435
2799
  view = next;
2436
2800
  options = [...view.actions.keys(), "Close"];
2437
2801
  selectedIndex = Math.max(0, options.indexOf(selectedOption ?? ""));
2802
+ const preserved = preserveWorkflowPhaseSelection(view.phaseModel, { phaseId, agentId });
2803
+ selectedPhaseId = preserved.phaseId;
2804
+ selectedAgentId = preserved.agentId;
2438
2805
  tui.requestRender();
2439
2806
  };
2440
2807
  const requestStop = () => {
@@ -2470,11 +2837,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2470
2837
  timer.unref();
2471
2838
  return borderWorkflowOverlay({
2472
2839
  render(width) {
2473
- const dashboard = stopStatus ? `${view.dashboard}\n\n${stopStatus}` : view.dashboard;
2474
- const dashboardLines = truncateToVisualLines(theme.fg("accent", dashboard), Number.MAX_SAFE_INTEGER, width, 1).visualLines;
2840
+ const styles = themeWorkflowProgressStyles(theme);
2841
+ const phaseLines = formatWorkflowPhaseDashboard(view.run, view.snapshot, width, { phaseId: selectedPhaseId, agentId: selectedAgentId }, styles);
2842
+ const dashboardLines = stopStatus ? [styles.error(stopStatus), ...phaseLines] : phaseLines;
2475
2843
  const actionLines = options.map((option, index) => truncateToVisualLines(`${index === selectedIndex ? "→ " : " "}${option}`, Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "");
2476
2844
  const layout = dashboardLayout();
2477
- const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} navigate${dashboardLines.length > layout.dashboardViewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · ${keyLabel("tui.select.confirm", "enter")} select · ${keyLabel("tui.select.cancel", "esc")} close · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
2845
+ const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} phases · ${keyLabel("tui.input.tab", "tab")} agents${dashboardLines.length > layout.dashboardViewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · ${keyLabel("tui.select.confirm", "enter")} select · ${keyLabel("tui.select.cancel", "esc")} close · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
2478
2846
  const compact = [...dashboardLines, "", ...actionLines, "", hint];
2479
2847
  if (compact.length <= layout.rows) {
2480
2848
  dashboardOffset = 0;
@@ -2494,7 +2862,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2494
2862
  handleInput(data) {
2495
2863
  if (stopRequested)
2496
2864
  return;
2497
- if (keybindings.matches(data, "tui.select.up"))
2865
+ const currentPhase = () => view.phaseModel.phases.find((phase) => phase.id === selectedPhaseId);
2866
+ const left = keybindings.matches(data, "tui.editor.cursorLeft");
2867
+ const right = keybindings.matches(data, "tui.editor.cursorRight");
2868
+ if (left || right) {
2869
+ const phases = view.phaseModel.phases;
2870
+ if (phases.length) {
2871
+ const currentIndex = Math.max(0, phases.findIndex((phase) => phase.id === selectedPhaseId));
2872
+ const delta = left ? -1 : 1;
2873
+ const nextPhase = phases[(currentIndex + delta + phases.length) % phases.length];
2874
+ selectedPhaseId = nextPhase?.id;
2875
+ selectedAgentId = nextPhase?.agents[0]?.id;
2876
+ }
2877
+ }
2878
+ else if (keybindings.matches(data, "tui.input.tab")) {
2879
+ const agents = currentPhase()?.agents ?? [];
2880
+ if (agents.length) {
2881
+ const currentIndex = Math.max(0, agents.findIndex((agent) => agent.id === selectedAgentId));
2882
+ selectedAgentId = agents[(currentIndex + 1) % agents.length]?.id;
2883
+ }
2884
+ }
2885
+ else if (keybindings.matches(data, "tui.select.up"))
2498
2886
  selectedIndex = (selectedIndex + options.length - 1) % options.length;
2499
2887
  else if (keybindings.matches(data, "tui.select.down"))
2500
2888
  selectedIndex = (selectedIndex + 1) % options.length;