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.
@@ -2,7 +2,9 @@ import { readFileSync, readdirSync, realpathSync } from "node:fs";
2
2
  import { basename, dirname, extname, join, resolve } from "node:path";
3
3
  import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
4
4
  import { ModelRuntime, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
5
- import { DEFAULT_SETTINGS, loadSettings, resolveAgentResourcePolicy, resolveModelReference, parseRoleMarkdown, registeredWorkflowFunctions, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "./index.js";
5
+ import { DEFAULT_SETTINGS, loadSettings, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "./index.js";
6
+ import { loadingRegistry } from "./registry.js";
7
+ import { disabledResources, unmatchedResourcePatterns } from "./utils.js";
6
8
  const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
7
9
  function canonical(path) {
8
10
  const absolute = resolve(path);
@@ -13,6 +15,11 @@ function canonical(path) {
13
15
  return absolute;
14
16
  }
15
17
  }
18
+ function isDynamicModelAlias(value, aliases) {
19
+ const match = /^([^/\s:]+)(?::([^\s]+))?$/.exec(value);
20
+ const name = match?.[1];
21
+ return Boolean(name && (match[2] === undefined || parseThinking(match[2]) !== undefined) && aliases.has(name));
22
+ }
16
23
  async function readCredentials(agentDir) {
17
24
  const credentials = new InMemoryCredentialStore();
18
25
  try {
@@ -112,6 +119,30 @@ function roleFilesFrom(dirs) {
112
119
  const paths = dirs.flatMap((dir) => roleFiles(dir));
113
120
  return [...new Map(paths.map((path) => [basename(path, ".md"), path])).values()].sort();
114
121
  }
122
+ function extensionLabel(extension) { return `Extension "${extension.headline}" (${extension.version})`; }
123
+ function scanExtensionRoleFiles(registrations) {
124
+ const files = [];
125
+ const empty = [];
126
+ const errors = [];
127
+ for (const registration of registrations) {
128
+ try {
129
+ const entries = readdirSync(registration.path, { withFileTypes: true });
130
+ const roleFiles = entries.filter((entry) => entry.isFile() && extname(entry.name) === ".md");
131
+ if (!roleFiles.length)
132
+ empty.push(registration);
133
+ for (const entry of roleFiles)
134
+ files.push({ name: basename(entry.name, ".md"), path: join(registration.path, entry.name), directory: registration.path, extension: registration.extension });
135
+ }
136
+ catch (error) {
137
+ errors.push({ registration, error });
138
+ }
139
+ }
140
+ files.sort((left, right) => left.name.localeCompare(right.name) || left.path.localeCompare(right.path));
141
+ return { files, empty, errors };
142
+ }
143
+ function roleProvenance(source) {
144
+ return source ? `${extensionLabel(source.extension)} role directory "${source.directory}"` : "Role";
145
+ }
115
146
  function diagnostic(severity, code, message, source, hint) {
116
147
  return { severity, code, message, ...(source ? { source } : {}), ...(hint ? { hint } : {}) };
117
148
  }
@@ -119,7 +150,9 @@ function emptyResourcePolicy(globalSettingsPath, cwd, projectTrusted) {
119
150
  const empty = { skills: [], extensions: [] };
120
151
  return { globalSettingsPath, projectSettingsPath: workflowProjectSettingsPath(cwd), projectTrusted, global: empty, project: empty, effective: empty, unmatchedSkills: [], unmatchedExtensions: [] };
121
152
  }
122
- function validateModel(value, known, available, source, diagnostics, aliases, settingsPath) {
153
+ function validateModel(value, known, available, source, diagnostics, aliases, dynamicAliases, settingsPath) {
154
+ if (isDynamicModelAlias(value, dynamicAliases))
155
+ return;
123
156
  try {
124
157
  const parsed = resolveModelReference(value, aliases, known, settingsPath);
125
158
  const name = `${parsed.provider}/${parsed.model}`;
@@ -130,13 +163,14 @@ function validateModel(value, known, available, source, diagnostics, aliases, se
130
163
  diagnostics.push(diagnostic("error", "MODEL_INVALID", error.message, source, error.message.includes("thinking") ? THINKING_HINT : "Use provider/model or provider/model:thinking."));
131
164
  }
132
165
  }
133
- function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, settingsPath) {
166
+ function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
134
167
  let definition;
135
168
  try {
136
169
  definition = parseRoleMarkdown(readFileSync(path, "utf8"), true, path);
137
170
  }
138
171
  catch (error) {
139
- diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", error.message, path, "Fix the role YAML frontmatter."));
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", source ? `${roleProvenance(source)} contains invalid role at "${path}": ${message}` : message, path, "Fix the role YAML frontmatter."));
140
174
  return undefined;
141
175
  }
142
176
  const body = definition.prompt ?? "";
@@ -147,20 +181,18 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
147
181
  if (/{{\s*[^{}]+\s*}}/.test(body))
148
182
  diagnostics.push(diagnostic("warning", "ROLE_PLACEHOLDER", "Role body contains an unsupported placeholder-looking token", path));
149
183
  if (definition.model)
150
- validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, settingsPath);
184
+ validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, dynamicAliases, settingsPath);
151
185
  for (const tool of definition.tools ?? [])
152
186
  if (!activeTools.has(tool))
153
187
  diagnostics.push(diagnostic("error", "ROLE_TOOL_INACTIVE", `Tool is unknown or inactive: ${tool}`, path, "Use a tool listed under Active tools or enable its Pi extension."));
154
188
  return definition;
155
189
  }
156
190
  function matchResourcePolicy(policy, pi) {
157
- const extensions = new Set((pi.extensions ?? []).map(canonical));
158
- const skills = new Set(pi.skills ?? []);
159
- return { ...policy, unmatchedSkills: policy.effective.skills.filter((name) => !skills.has(name)), unmatchedExtensions: policy.effective.extensions.filter((path) => !extensions.has(canonical(path))) };
160
- }
161
- function resourcePolicySource(policy, kind, selector) {
162
- return policy.project[kind].includes(selector) && !policy.global[kind].includes(selector) ? policy.projectSettingsPath : policy.globalSettingsPath;
191
+ const extensions = [...new Set((pi.extensions ?? []).map(canonical))];
192
+ const skills = [...new Set(pi.skills ?? [])];
193
+ return { ...policy, excludedSkills: disabledResources(policy.effective.skills, skills), excludedExtensions: disabledResources(policy.effective.extensions, extensions), unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, skills), unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, extensions) };
163
194
  }
195
+ function resourcePolicySource(settingsSource) { return settingsSource; }
164
196
  export async function doctor(options = {}) {
165
197
  const cwd = canonical(options.cwd ?? process.cwd());
166
198
  const agentDir = canonical(options.agentDir ?? getAgentDir());
@@ -173,6 +205,8 @@ export async function doctor(options = {}) {
173
205
  catch (error) {
174
206
  diagnostics.push(diagnostic("error", "SETTINGS_INVALID", error.message, settingsPath, "Fix or remove the invalid workflow settings file."));
175
207
  }
208
+ let settingsSources = { concurrency: settingsPath, modelAliases: settingsPath, disabledAgentResources: settingsPath };
209
+ const projectSettingsPath = workflowProjectSettingsPath(cwd);
176
210
  let pi;
177
211
  try {
178
212
  pi = await (options.discoverPi ?? discoverPi)(cwd, agentDir);
@@ -187,41 +221,98 @@ export async function doctor(options = {}) {
187
221
  diagnostics.push(diagnostic("warning", "PROJECT_UNTRUSTED", "Pi project resources are inactive because the project is not trusted", cwd, "Open this project in Pi, choose Trust, then rerun doctor."));
188
222
  for (const error of pi.extensionErrors)
189
223
  diagnostics.push(diagnostic("error", "EXTENSION_LOAD", error.message, error.path, "Fix or disable the failing Pi extension."));
224
+ try {
225
+ const resolved = resolveWorkflowSettings(cwd, pi.trust.trusted, settingsPath);
226
+ settings = resolved.effective;
227
+ settingsSources = resolved.sources;
228
+ }
229
+ catch (error) {
230
+ const message = error instanceof Error ? error.message : String(error);
231
+ const source = message.includes(projectSettingsPath) ? projectSettingsPath : settingsPath;
232
+ if (!diagnostics.some(({ code, source: itemSource }) => code === "SETTINGS_INVALID" && itemSource === source))
233
+ diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, source, "Fix or remove the invalid workflow settings file."));
234
+ }
190
235
  let resourcePolicy;
191
236
  try {
192
237
  resourcePolicy = matchResourcePolicy(resolveAgentResourcePolicy(cwd, pi.trust.trusted, settingsPath), pi);
193
238
  }
194
239
  catch (error) {
195
240
  const message = error instanceof Error ? error.message : String(error);
196
- if (!diagnostics.some(({ code, source }) => code === "SETTINGS_INVALID" && source === settingsPath))
197
- diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, message.includes(".json") ? message.match(/(?:settings: )?([^ )]+\.json)/)?.[1] : undefined, "Fix or remove the invalid workflow settings file."));
241
+ const source = message.includes(projectSettingsPath) ? projectSettingsPath : settingsPath;
242
+ if (!diagnostics.some(({ code, source: itemSource }) => code === "SETTINGS_INVALID" && itemSource === source))
243
+ diagnostics.push(diagnostic("error", "SETTINGS_INVALID", message, source, "Fix or remove the invalid workflow settings file."));
198
244
  resourcePolicy = emptyResourcePolicy(settingsPath, cwd, pi.trust.trusted);
199
245
  }
200
246
  for (const skill of resourcePolicy.unmatchedSkills)
201
- diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled skill selector currently matches no discovered skill: ${skill}`, `${resourcePolicySource(resourcePolicy, "skills", skill)}.disabledAgentResources.skills`));
247
+ diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled skill selector currently matches no discovered skill: ${skill}`, `${resourcePolicySource(settingsSources.disabledAgentResources)}.disabledAgentResources.skills`));
202
248
  for (const extension of resourcePolicy.unmatchedExtensions)
203
- diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled extension selector currently matches no discovered extension source: ${extension}`, `${resourcePolicySource(resourcePolicy, "extensions", extension)}.disabledAgentResources.extensions`));
249
+ diagnostics.push(diagnostic("warning", "AGENT_RESOURCE_UNMATCHED", `Disabled extension selector currently matches no discovered extension source: ${extension}`, `${resourcePolicySource(settingsSources.disabledAgentResources)}.disabledAgentResources.extensions`));
204
250
  const activeTools = new Set(pi.activeTools);
205
251
  const knownModels = new Set(pi.knownModels);
206
252
  const availableModels = new Set(pi.availableModels);
207
253
  const aliases = settings.modelAliases ?? {};
254
+ const registry = options.registry ?? loadingRegistry();
255
+ const registeredModelAliases = registry.modelAliases();
256
+ const dynamicAliases = new Set(registeredModelAliases.map(({ name }) => name).filter((name) => !Object.prototype.hasOwnProperty.call(aliases, name)));
257
+ const modelAliases = [
258
+ ...Object.keys(aliases).map((name) => ({ name, kind: "static", provenance: settingsSources.modelAliases })),
259
+ ...registeredModelAliases.map(({ name, version, headline, extensionDescription }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline, extensionDescription })),
260
+ ].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind));
208
261
  const roles = [];
209
262
  const definitions = new Map();
263
+ const extensionScan = scanExtensionRoleFiles(registeredWorkflowRoleDirectoryRegistrations());
264
+ for (const { registration, error } of extensionScan.errors) {
265
+ const message = error instanceof Error ? error.message : String(error);
266
+ diagnostics.push(diagnostic("error", "ROLE_DIRECTORY", `${extensionLabel(registration.extension)} role directory "${registration.path}" could not be scanned: ${message}`, registration.path, "Fix or remove the registered role directory."));
267
+ }
268
+ for (const registration of extensionScan.empty)
269
+ diagnostics.push(diagnostic("warning", "ROLE_DIRECTORY_EMPTY", `${extensionLabel(registration.extension)} role directory "${registration.path}" contains no .md role files`, registration.path, "Add packaged role files or remove the directory registration."));
270
+ const extensionFilesByName = new Map();
271
+ for (const file of extensionScan.files)
272
+ extensionFilesByName.set(file.name, [...(extensionFilesByName.get(file.name) ?? []), file]);
273
+ const duplicateExtensionNames = new Set();
274
+ for (const [name, matches] of extensionFilesByName)
275
+ if (matches.length > 1) {
276
+ duplicateExtensionNames.add(name);
277
+ diagnostics.push(diagnostic("error", "ROLE_DUPLICATE", `Duplicate extension role "${name}": ${matches.map(({ path, directory, extension }) => `${extensionLabel(extension)} role directory "${directory}" (${path})`).join("; ")}`, matches[0]?.path, "Keep one extension role with this name; global and project roles may override packaged defaults."));
278
+ }
279
+ const extensionPaths = new Map();
280
+ for (const file of extensionScan.files) {
281
+ roles.push({ name: file.name, path: file.path, scope: "extension", active: true, extension: file.extension });
282
+ const definition = inspectRole(file.path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, { directory: file.directory, extension: file.extension });
283
+ if (duplicateExtensionNames.has(file.name))
284
+ continue;
285
+ extensionPaths.set(file.name, file.path);
286
+ if (definition)
287
+ definitions.set(file.name, definition);
288
+ }
210
289
  const globalPaths = new Map();
211
290
  const globalRoleDirs = workflowRoleDirectories(agentDir);
212
291
  for (const path of roleFilesFrom(globalRoleDirs)) {
213
292
  const name = basename(path, ".md");
214
- roles.push({ name, path, scope: "global", active: true });
293
+ const extensionPath = extensionPaths.get(name);
294
+ roles.push({ name, path, scope: "global", active: true, ...(extensionPath ? { overrides: extensionPath } : {}) });
215
295
  globalPaths.set(name, path);
216
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, settingsPath);
296
+ if (extensionPath) {
297
+ const extension = roles.find((role) => role.path === extensionPath);
298
+ if (extension) {
299
+ extension.active = false;
300
+ extension.overriddenBy = path;
301
+ }
302
+ }
303
+ const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
217
304
  if (definition)
218
305
  definitions.set(name, definition);
306
+ else
307
+ definitions.delete(name);
219
308
  }
220
309
  for (const path of roleFilesFrom([join(cwd, ".pi", "pi-extensible-workflows", "roles")])) {
221
310
  const name = basename(path, ".md");
222
311
  const globalPath = globalPaths.get(name);
312
+ const extensionPath = extensionPaths.get(name);
313
+ const overriddenPath = globalPath ?? extensionPath;
223
314
  const active = pi.trust.trusted;
224
- roles.push({ name, path, scope: "project", active, ...(active && globalPath ? { overrides: globalPath } : {}) });
315
+ roles.push({ name, path, scope: "project", active, ...(active && overriddenPath ? { overrides: overriddenPath } : {}) });
225
316
  if (!active)
226
317
  continue;
227
318
  if (globalPath) {
@@ -231,7 +322,14 @@ export async function doctor(options = {}) {
231
322
  global.overriddenBy = path;
232
323
  }
233
324
  }
234
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, settingsPath);
325
+ else if (extensionPath) {
326
+ const extension = roles.find((role) => role.path === extensionPath);
327
+ if (extension) {
328
+ extension.active = false;
329
+ extension.overriddenBy = path;
330
+ }
331
+ }
332
+ const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
235
333
  if (definition)
236
334
  definitions.set(name, definition);
237
335
  else
@@ -244,7 +342,7 @@ export async function doctor(options = {}) {
244
342
  const severityOrder = { error: 0, warning: 1 };
245
343
  diagnostics.sort((left, right) => severityOrder[left.severity] - severityOrder[right.severity] || (left.source ?? "").localeCompare(right.source ?? "") || left.code.localeCompare(right.code) || left.message.localeCompare(right.message));
246
344
  roles.sort((left, right) => left.name.localeCompare(right.name) || left.scope.localeCompare(right.scope));
247
- return { cwd, agentDir, settingsPath, settings, trust: pi.trust, activeTools: [...activeTools].sort(), roles, functions, resourcePolicy, diagnostics };
345
+ return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), roles, functions, modelAliases, resourcePolicy, diagnostics };
248
346
  }
249
347
  function count(report, severity) { return report.diagnostics.filter((item) => item.severity === severity).length; }
250
348
  export function doctorExitCode(report) { return count(report, "error") > 0 ? 1 : 0; }
@@ -255,7 +353,9 @@ export function formatDoctorReport(report) {
255
353
  "## Environment",
256
354
  `- CWD: \`${report.cwd}\``,
257
355
  `- Agent dir: \`${report.agentDir}\``,
258
- `- Workflow settings: \`${report.settingsPath}\``,
356
+ `- Global workflow settings: \`${report.settingsPath}\``,
357
+ `- Project workflow settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
358
+ `- Effective setting sources: concurrency=\`${report.settingsSources.concurrency}\`, modelAliases=\`${report.settingsSources.modelAliases}\`, disabledAgentResources=\`${report.settingsSources.disabledAgentResources}\``,
259
359
  `- Limits: concurrency=${String(report.settings.concurrency)}`,
260
360
  "",
261
361
  "## Trust/resources",
@@ -270,6 +370,8 @@ export function formatDoctorReport(report) {
270
370
  `- Project extensions: ${report.resourcePolicy.project.extensions.join(", ") || "(none)"}`,
271
371
  `- Effective skills: ${report.resourcePolicy.effective.skills.join(", ") || "(none)"}`,
272
372
  `- Effective extensions: ${report.resourcePolicy.effective.extensions.join(", ") || "(none)"}`,
373
+ `- Excluded skills: ${report.resourcePolicy.excludedSkills?.join(", ") || "(none)"}`,
374
+ `- Excluded extensions: ${report.resourcePolicy.excludedExtensions?.join(", ") || "(none)"}`,
273
375
  `- Unmatched skills: ${report.resourcePolicy.unmatchedSkills.join(", ") || "(none)"}`,
274
376
  `- Unmatched extensions: ${report.resourcePolicy.unmatchedExtensions.join(", ") || "(none)"}`,
275
377
  "",
@@ -277,7 +379,10 @@ export function formatDoctorReport(report) {
277
379
  ...(report.activeTools.length ? report.activeTools.map((tool) => `- \`${tool}\``) : ["- None resolved"]),
278
380
  "",
279
381
  "## Roles",
280
- ...(report.roles.length ? report.roles.map((role) => `- \`${role.name}\` (${role.scope}, ${role.active ? "active" : role.overriddenBy ? `overridden by ${role.overriddenBy}` : "inactive: project untrusted"}) - \`${role.path}\`${role.overrides ? `; overrides \`${role.overrides}\`` : ""}`) : ["- None found"]),
382
+ ...(report.roles.length ? report.roles.map((role) => `- \`${role.name}\` (${role.scope}, ${role.active ? "active" : role.overriddenBy ? `overridden by ${role.overriddenBy}` : "inactive: project untrusted"}) - \`${role.path}\`${role.extension ? `; ${extensionLabel(role.extension)} role directory "${dirname(role.path)}"` : ""}${role.overrides ? `; overrides \`${role.overrides}\`` : ""}`) : ["- None found"]),
383
+ "",
384
+ "## Model aliases",
385
+ ...(report.modelAliases.length ? report.modelAliases.map((alias) => `- [${alias.kind}] \`${alias.name}\`${alias.kind === "static" ? ` -> ${report.settings.modelAliases?.[alias.name] ?? "(unresolved)"}` : ""} (${alias.provenance})`) : ["- None registered"]),
281
386
  "",
282
387
  "## Reusable functions",
283
388
  ...(report.functions.length ? report.functions.map((fn) => `- [${fn.valid ? "ok" : "error"}] \`${fn.name}\` - ${fn.description}`) : ["- None registered"]),
@@ -2,7 +2,7 @@ import { Type } from "@earendil-works/pi-ai";
2
2
  import { copyToClipboard, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { type SessionFactory } from "./agent-execution.js";
4
4
  import type { AwaitingCheckpoint, PersistedRun, WorktreeReference } from "./persistence.js";
5
- import { type LaunchSnapshot, type RunState, type WorkflowFailureDiagnostics } from "./types.js";
5
+ import { type AgentRecord, type LaunchSnapshot, type RunState, type WorkflowFailureDiagnostics } from "./types.js";
6
6
  export interface WorkflowProgressStyles {
7
7
  accent(text: string): string;
8
8
  success(text: string): string;
@@ -50,13 +50,52 @@ export declare const WORKFLOW_TOOL_PARAMETERS: Type.TObject<{
50
50
  export declare const WORKFLOW_RETRY_PARAMETERS: Type.TObject<{
51
51
  runId: Type.TString;
52
52
  }>;
53
+ export type WorkflowPhaseState = "not started" | "running" | "completed" | "failed" | "cancelled" | "interrupted" | "budget_exhausted";
54
+ export interface WorkflowPhaseAgentCounts {
55
+ total: number;
56
+ completed: number;
57
+ running: number;
58
+ failed: number;
59
+ cancelled: number;
60
+ pending: number;
61
+ }
62
+ export interface WorkflowPhaseView {
63
+ id: string;
64
+ name: string;
65
+ occurrence: number;
66
+ state: WorkflowPhaseState;
67
+ status: WorkflowPhaseState;
68
+ observed: boolean;
69
+ afterAgent?: number;
70
+ agents: readonly AgentRecord[];
71
+ counts: WorkflowPhaseAgentCounts;
72
+ }
73
+ export interface WorkflowPhaseModel {
74
+ declaredPhases: readonly string[];
75
+ phases: readonly WorkflowPhaseView[];
76
+ currentPhaseIndex?: number;
77
+ currentPhaseId?: string;
78
+ counts: Readonly<Partial<Record<WorkflowPhaseState, number>>>;
79
+ unassignedAgents?: readonly AgentRecord[];
80
+ }
81
+ type WorkflowPhaseSource = readonly string[] | Pick<LaunchSnapshot, "phases"> | undefined;
82
+ export declare function buildWorkflowPhaseModel(run: Pick<PersistedRun, "state" | "phase" | "phaseHistory" | "agents">, source?: WorkflowPhaseSource): WorkflowPhaseModel;
83
+ export interface WorkflowPhaseSelection {
84
+ phaseId?: string | undefined;
85
+ agentId?: string | undefined;
86
+ }
87
+ export declare function preserveWorkflowPhaseSelection(model: WorkflowPhaseModel, selection: WorkflowPhaseSelection): WorkflowPhaseSelection;
53
88
  export declare function formatWorkflowProgress(run: PersistedRun, spinner?: string, styles?: WorkflowProgressStyles): string;
54
89
  export declare function truncateWorkflowProgress(text: string, width: number): string[];
55
90
  export declare function formatBudgetStatus(run: Pick<PersistedRun, "budget" | "budgetVersion" | "usage" | "budgetEvents">): string[];
91
+ export declare function agentBreadcrumbParts(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string[];
92
+ export declare function agentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string;
56
93
  export declare function formatNavigatorDashboard(run: PersistedRun, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[]): string;
57
94
  export declare function formatNavigatorRun(loaded: {
58
95
  run: PersistedRun;
59
96
  snapshot: Readonly<LaunchSnapshot>;
60
97
  }, checkpoints: readonly AwaitingCheckpoint[], _worktrees: readonly WorktreeReference[]): string;
98
+ export declare function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection?: WorkflowPhaseSelection, styles?: WorkflowProgressStyles): string[];
61
99
  export declare function formatWorkflowFailureDiagnostics(diagnostic: WorkflowFailureDiagnostics): string;
62
100
  export default function workflowExtension(pi: ExtensionAPI, home?: string, clipboard?: typeof copyToClipboard, createSession?: SessionFactory, agentDir?: string): void;
101
+ export {};