pi-workflow-engine 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +256 -0
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +64 -0
  3. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +238 -0
  4. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +28 -0
  5. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +171 -0
  6. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +152 -0
  7. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +114 -0
  8. package/{src → .pi/extensions/pi-workflow-engine/src}/engine.ts +36 -8
  9. package/.pi/extensions/pi-workflow-engine/src/options.ts +46 -0
  10. package/.pi/extensions/pi-workflow-engine/src/perf.ts +139 -0
  11. package/{src → .pi/extensions/pi-workflow-engine/src}/progress.ts +82 -35
  12. package/{src → .pi/extensions/pi-workflow-engine/src}/types.ts +12 -4
  13. package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-format.ts +59 -6
  14. package/{src → .pi/extensions/pi-workflow-engine/src}/ui/workflow-inspector.ts +8 -6
  15. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +198 -0
  16. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +182 -0
  17. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +263 -0
  18. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +24 -0
  19. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/code-review.ts +89 -145
  20. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +244 -0
  21. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +188 -0
  22. package/{workflows → .pi/extensions/pi-workflow-engine/workflows}/ping.ts +1 -0
  23. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +181 -0
  24. package/.pi/settings.json +3 -0
  25. package/README.md +163 -63
  26. package/assets/preview-candidate-core.png +0 -0
  27. package/assets/preview-candidate-relic-emblem.png +0 -0
  28. package/assets/preview-candidate-robot-squad.png +0 -0
  29. package/assets/preview.png +0 -0
  30. package/package.json +32 -6
  31. package/src/agent-runner.ts +0 -122
  32. package/src/concurrency.ts +0 -55
  33. package/src/discovery.ts +0 -54
  34. package/src/index.ts +0 -141
  35. package/src/ui/workflow-result-renderer.ts +0 -149
  36. package/src/ui/workflow-widget.ts +0 -143
  37. package/src/workflows.ts +0 -14
package/src/index.ts DELETED
@@ -1,141 +0,0 @@
1
- import { fileURLToPath } from "node:url";
2
- import { Type } from "typebox";
3
- import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
- import { Text } from "@earendil-works/pi-tui";
5
- import { discoverWorkflows } from "./discovery.ts";
6
- import { runWorkflow } from "./engine.ts";
7
- import type { WorkflowModule, WorkflowRunOptions } from "./types.ts";
8
- import { isWorkflowResult, renderWorkflowResult, type WorkflowResultEnvelope } from "./ui/workflow-result-renderer.ts";
9
-
10
- /** Repo root (this file lives in <repo>/src/index.ts). */
11
- const REPO_DIR = fileURLToPath(new URL("..", import.meta.url));
12
-
13
- function summarize(result: unknown): string {
14
- if (result && typeof result === "object" && typeof (result as { summary?: unknown }).summary === "string") {
15
- return (result as { summary: string }).summary;
16
- }
17
- return typeof result === "string" ? result : "Workflow finished.";
18
- }
19
-
20
- function formatReport(name: string, result: unknown): string {
21
- return [`## Workflow: ${name}`, "", "```json", JSON.stringify(result, null, 2), "```"].join("\n");
22
- }
23
-
24
- function workflowEnvelope(name: string, result: unknown): WorkflowResultEnvelope {
25
- return { name, result, completedAt: Date.now() };
26
- }
27
-
28
- interface WorkflowInvocation {
29
- name: string;
30
- args: string;
31
- options: WorkflowRunOptions;
32
- }
33
-
34
- function parseWorkflowInvocation(input: string): WorkflowInvocation {
35
- const trimmed = input.trim();
36
- const space = trimmed.indexOf(" ");
37
- const name = space === -1 ? trimmed : trimmed.slice(0, space);
38
- const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
39
- const inspect = /(?:^|\s)--inspect(?:\s|$)/.test(rest);
40
- const cleanedArgs = rest.replace(/(?:^|\s)--inspect(?=\s|$)/g, " ").trim();
41
- return { name, args: cleanedArgs, options: inspect ? { inspect: true } : {} };
42
- }
43
-
44
- async function pickWorkflow(
45
- workflows: ReadonlyMap<string, WorkflowModule>,
46
- ctx: ExtensionCommandContext,
47
- ): Promise<WorkflowInvocation | undefined> {
48
- const choices = [...workflows.values()].map((mod) => `${mod.meta.name} — ${mod.meta.description}`);
49
- const choice = await ctx.ui.select("Run workflow", choices);
50
- if (!choice) return undefined;
51
-
52
- const separator = choice.indexOf(" — ");
53
- const name = separator === -1 ? choice : choice.slice(0, separator);
54
- const args = name === "code-review" ? (await ctx.ui.input("Code-review target/instructions", "Blank = auto-detect diff"))?.trim() ?? "" : "";
55
- const inspect = await ctx.ui.confirm("Open inspector?", "Open a live workflow inspector while this workflow runs?");
56
- return { name, args, options: { inspect } };
57
- }
58
-
59
- async function sendWorkflowResult(
60
- pi: ExtensionAPI,
61
- ctx: ExtensionCommandContext,
62
- name: string,
63
- mod: WorkflowModule,
64
- args: string,
65
- options: WorkflowRunOptions,
66
- ): Promise<void> {
67
- const result = await runWorkflow(ctx, mod, args, options);
68
- pi.sendMessage(
69
- { customType: "workflow-result", content: formatReport(name, result), display: true, details: workflowEnvelope(name, result) },
70
- { triggerTurn: false },
71
- );
72
- }
73
-
74
- export default function workflowEngine(pi: ExtensionAPI): void {
75
- pi.registerMessageRenderer("workflow-result", (message, { expanded }, theme) => {
76
- const details = message.details;
77
- if (isWorkflowResult(details)) return renderWorkflowResult(details.name, details.result, expanded, theme);
78
- return renderWorkflowResult("workflow", details ?? message.content, expanded, theme);
79
- });
80
-
81
- // /workflow <name> [args] — user-invoked.
82
- pi.registerCommand("workflow", {
83
- description: "Run a multi-agent workflow: /workflow <name> [args]",
84
- handler: async (args: string, ctx: ExtensionCommandContext) => {
85
- const workflows = await discoverWorkflows(REPO_DIR);
86
- const available = [...workflows.keys()].join(", ") || "(none)";
87
- const direct = parseWorkflowInvocation(args);
88
- const invocation = direct.name ? direct : ctx.hasUI ? await pickWorkflow(workflows, ctx) : undefined;
89
-
90
- if (!invocation) {
91
- ctx.ui.notify(`Usage: /workflow <name> [args]. Available: ${available}`, "warning");
92
- return;
93
- }
94
-
95
- const mod = workflows.get(invocation.name);
96
- if (!mod) {
97
- ctx.ui.notify(`Unknown workflow "${invocation.name}". Available: ${available}`, "error");
98
- return;
99
- }
100
-
101
- await sendWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, invocation.options);
102
- },
103
- });
104
-
105
- // workflow tool — lets the host agent fan out mid-conversation.
106
- pi.registerTool({
107
- name: "workflow",
108
- label: "Workflow",
109
- description:
110
- "Run a named multi-agent workflow (fan-out → verify → synthesize) and return its structured result. Use for thorough reviews or audits.",
111
- parameters: Type.Object({
112
- name: Type.String({ description: "Workflow name, e.g. code-review" }),
113
- args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
114
- }),
115
- renderCall(args, theme) {
116
- const suffix = args.args ? ` ${theme.fg("dim", args.args)}` : "";
117
- return new Text(`▸ ${theme.fg("toolTitle", theme.bold("workflow"))} ${theme.fg("accent", args.name)}${suffix}`, 0, 0);
118
- },
119
- renderResult(result, { expanded, isPartial }, theme) {
120
- if (isPartial) return new Text(theme.fg("accent", "Running workflow…"), 0, 0);
121
- const details = result.details;
122
- if (isWorkflowResult(details)) return renderWorkflowResult(details.name, details.result, expanded, theme);
123
- const first = result.content[0];
124
- const text = first?.type === "text" ? first.text : "Workflow finished.";
125
- return new Text(theme.fg("muted", text), 0, 0);
126
- },
127
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
128
- const workflows = await discoverWorkflows(REPO_DIR);
129
- const mod = workflows.get(params.name);
130
- if (!mod) {
131
- const available = [...workflows.keys()].join(", ") || "(none)";
132
- return {
133
- content: [{ type: "text", text: `Unknown workflow "${params.name}". Available: ${available}` }],
134
- details: { error: "unknown_workflow", available },
135
- };
136
- }
137
- const result = await runWorkflow(ctx, mod, params.args ?? "");
138
- return { content: [{ type: "text", text: summarize(result) }], details: workflowEnvelope(params.name, result) };
139
- },
140
- });
141
- }
@@ -1,149 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { Box, type Component, Text } from "@earendil-works/pi-tui";
3
- import { badge, formatCount } from "./workflow-format.ts";
4
-
5
- export interface WorkflowResultEnvelope {
6
- name: string;
7
- result: unknown;
8
- completedAt: number;
9
- }
10
-
11
- export interface CodeReviewFinding {
12
- file: string;
13
- line?: number;
14
- severity?: string;
15
- verdict?: string;
16
- summary: string;
17
- evidence?: string;
18
- failure_scenario?: string;
19
- }
20
-
21
- export interface CodeReviewResult {
22
- summary: string;
23
- findings: CodeReviewFinding[];
24
- stats?: Record<string, string | number>;
25
- }
26
-
27
- export function isWorkflowResult(value: unknown): value is WorkflowResultEnvelope {
28
- if (!isRecord(value)) return false;
29
- return typeof value.name === "string" && "result" in value && typeof value.completedAt === "number";
30
- }
31
-
32
- export function isFinding(value: unknown): value is CodeReviewFinding {
33
- if (!isRecord(value)) return false;
34
- if (typeof value.file !== "string" || typeof value.summary !== "string") return false;
35
- if (value.line !== undefined && typeof value.line !== "number") return false;
36
- if (value.severity !== undefined && typeof value.severity !== "string") return false;
37
- if (value.verdict !== undefined && typeof value.verdict !== "string") return false;
38
- if (value.evidence !== undefined && typeof value.evidence !== "string") return false;
39
- if (value.failure_scenario !== undefined && typeof value.failure_scenario !== "string") return false;
40
- return true;
41
- }
42
-
43
- export function isCodeReviewResult(value: unknown): value is CodeReviewResult {
44
- if (!isRecord(value)) return false;
45
- if (typeof value.summary !== "string" || !Array.isArray(value.findings)) return false;
46
- if (!value.findings.every(isFinding)) return false;
47
- if (value.stats !== undefined && !isStats(value.stats)) return false;
48
- return true;
49
- }
50
-
51
- export function renderWorkflowResult(name: string, result: unknown, expanded: boolean, theme: Theme): Component {
52
- const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
53
- box.addChild(new Text(renderWorkflowResultText(name, result, expanded, theme), 0, 0));
54
- return box;
55
- }
56
-
57
- export function renderWorkflowResultText(name: string, result: unknown, expanded: boolean, theme: Theme): string {
58
- if (name === "code-review" && isCodeReviewResult(result)) {
59
- return renderCodeReviewResult(name, result, expanded, theme);
60
- }
61
- return renderGenericWorkflowResult(name, result, expanded, theme);
62
- }
63
-
64
- function renderCodeReviewResult(name: string, result: CodeReviewResult, expanded: boolean, theme: Theme): string {
65
- const icon = theme.fg("success", "✓");
66
- const title = theme.fg("accent", theme.bold(`Workflow: ${name}`));
67
- const lines = [`${icon} ${title}`, theme.fg("muted", result.summary)];
68
- const stats = statsLine(result.stats, theme);
69
- if (stats) lines.push(stats);
70
-
71
- if (result.findings.length === 0) {
72
- lines.push(theme.fg("success", "No findings."));
73
- return lines.join("\n");
74
- }
75
-
76
- const findings = expanded ? result.findings : result.findings.slice(0, 3);
77
- lines.push(theme.fg("dim", expanded ? "Findings:" : "Top findings:"));
78
- for (const finding of findings) {
79
- lines.push(renderFinding(finding, expanded, theme));
80
- }
81
- if (!expanded && result.findings.length > findings.length) {
82
- lines.push(theme.fg("dim", `… ${result.findings.length - findings.length} more finding(s)`));
83
- }
84
- return lines.join("\n");
85
- }
86
-
87
- function renderFinding(finding: CodeReviewFinding, expanded: boolean, theme: Theme): string {
88
- const location = `${finding.file}${finding.line != null ? `:${finding.line}` : ""}`;
89
- const severityColor = finding.severity === "bug" ? "error" : "warning";
90
- const verdictColor = finding.verdict === "CONFIRMED" ? "success" : finding.verdict === "PLAUSIBLE" ? "warning" : "muted";
91
- let text =
92
- ` ${badge(finding.severity ?? "finding", severityColor, theme)} ` +
93
- `${badge(finding.verdict ?? "", verdictColor, theme)} ` +
94
- `${theme.fg("accent", location)} ${theme.fg("muted", finding.summary)}`;
95
- if (expanded) {
96
- if (finding.failure_scenario) text += `\n ${theme.fg("dim", `Failure: ${finding.failure_scenario}`)}`;
97
- if (finding.evidence) text += `\n ${theme.fg("dim", `Evidence: ${finding.evidence}`)}`;
98
- }
99
- return text;
100
- }
101
-
102
- function renderGenericWorkflowResult(name: string, result: unknown, expanded: boolean, theme: Theme): string {
103
- const lines = [`${theme.fg("success", "✓")} ${theme.fg("accent", theme.bold(`Workflow: ${name}`))}`];
104
- const summary = extractSummary(result);
105
- if (summary) lines.push(theme.fg("muted", summary));
106
- if (expanded) lines.push(theme.fg("dim", safeJson(result)));
107
- else if (!summary) lines.push(theme.fg("dim", compactJson(result)));
108
- return lines.join("\n");
109
- }
110
-
111
- function statsLine(stats: Record<string, string | number> | undefined, theme: Theme): string | undefined {
112
- if (!stats) return undefined;
113
- const ordered = ["files", "candidates", "dropped", "verified", "kept"];
114
- const parts = ordered.flatMap((key) => {
115
- const value = stats[key];
116
- if (value === undefined) return [];
117
- return [`${key} ${typeof value === "number" ? formatCount(value) : value}`];
118
- });
119
- if (parts.length === 0) return undefined;
120
- return theme.fg("dim", parts.join(" · "));
121
- }
122
-
123
- function extractSummary(value: unknown): string | undefined {
124
- if (typeof value === "string") return value;
125
- if (isRecord(value) && typeof value.summary === "string") return value.summary;
126
- return undefined;
127
- }
128
-
129
- function compactJson(value: unknown): string {
130
- const json = safeJson(value).replace(/\s+/g, " ").trim();
131
- return json.length > 240 ? `${json.slice(0, 237)}...` : json;
132
- }
133
-
134
- function safeJson(value: unknown): string {
135
- try {
136
- return JSON.stringify(value, null, 2) ?? String(value);
137
- } catch (error) {
138
- return error instanceof Error ? error.message : String(error);
139
- }
140
- }
141
-
142
- function isStats(value: unknown): value is Record<string, string | number> {
143
- if (!isRecord(value)) return false;
144
- return Object.values(value).every((entry) => typeof entry === "string" || typeof entry === "number");
145
- }
146
-
147
- function isRecord(value: unknown): value is Record<string, unknown> {
148
- return typeof value === "object" && value !== null;
149
- }
@@ -1,143 +0,0 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import type { AgentRowSnapshot, PhaseSnapshot, WorkflowProgressSnapshot } from "../progress.ts";
3
- import { formatCount, formatDuration, statusIcon, truncateDisplay } from "./workflow-format.ts";
4
-
5
- const MAX_WIDGET_LINES = 12;
6
- const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
7
-
8
- export interface WorkflowWidget {
9
- nextFrame(): void;
10
- render(width: number, theme: Theme): string[];
11
- invalidate(): void;
12
- }
13
-
14
- export function createWorkflowWidget(snapshotProvider: () => WorkflowProgressSnapshot): WorkflowWidget {
15
- return new LiveWorkflowWidget(snapshotProvider);
16
- }
17
-
18
- export function renderWorkflowWidgetLines(
19
- snapshot: WorkflowProgressSnapshot,
20
- frame: number,
21
- width: number,
22
- theme: Theme,
23
- ): string[] {
24
- const safeWidth = Math.max(1, width);
25
- const agents = snapshot.phases.flatMap((phase) => phase.agents);
26
- const running = agents.filter((agent) => agent.status === "running").length;
27
- const queued = agents.filter((agent) => agent.status === "queued").length;
28
- const done = agents.filter((agent) => agent.status === "done").length;
29
- const failed = agents.filter((agent) => agent.status === "failed").length;
30
- const active = running + queued;
31
- const elapsed = formatDuration((snapshot.doneAt ?? Date.now()) - snapshot.startedAt);
32
- const spinner = active > 0 ? theme.fg("accent", SPINNER[frame % SPINNER.length]) : theme.fg("success", "✓");
33
-
34
- const headingParts = [`${done}/${agents.length} done`, elapsed];
35
- if (running > 0) headingParts.unshift(`${running} running`);
36
- if (queued > 0) headingParts.unshift(`${queued} queued`);
37
- if (failed > 0) headingParts.unshift(theme.fg("error", `${failed} failed`));
38
-
39
- const lines: string[] = [
40
- truncateDisplay(
41
- `${spinner} ${theme.bold(snapshot.title)} ${theme.fg("dim", "·")} ${theme.fg("muted", snapshot.currentPhase)} ${theme.fg("dim", "·")} ${headingParts.join(` ${theme.fg("dim", "·")} `)}`,
42
- safeWidth,
43
- ),
44
- ];
45
-
46
- const footer = footerLine(snapshot, theme);
47
- const bodyBudget = Math.max(0, MAX_WIDGET_LINES - lines.length - (footer ? 1 : 0));
48
- const bodyLines = prioritizedBodyLines(snapshot.phases, theme);
49
- const visibleBody = bodyLines.slice(0, bodyBudget);
50
- const hidden = bodyLines.length - visibleBody.length;
51
-
52
- for (const line of visibleBody) lines.push(truncateDisplay(line, safeWidth));
53
- if (hidden > 0 && lines.length < MAX_WIDGET_LINES) {
54
- lines.push(truncateDisplay(`${theme.fg("dim", "└─")} ${theme.fg("dim", `+${hidden} more`)}`, safeWidth));
55
- }
56
- if (footer && lines.length < MAX_WIDGET_LINES) lines.push(truncateDisplay(footer, safeWidth));
57
-
58
- return lines.slice(0, MAX_WIDGET_LINES).map((line) => truncateDisplay(line, safeWidth));
59
- }
60
-
61
- class LiveWorkflowWidget implements WorkflowWidget {
62
- private frame = 0;
63
- private cachedWidth: number | undefined;
64
- private cachedFrame: number | undefined;
65
- private cachedDoneAt: number | undefined;
66
- private cachedLines: string[] | undefined;
67
-
68
- constructor(private readonly snapshotProvider: () => WorkflowProgressSnapshot) {}
69
-
70
- nextFrame(): void {
71
- this.frame = (this.frame + 1) % SPINNER.length;
72
- this.invalidate();
73
- }
74
-
75
- render(width: number, theme: Theme): string[] {
76
- const snapshot = this.snapshotProvider();
77
- if (
78
- this.cachedLines &&
79
- this.cachedWidth === width &&
80
- this.cachedFrame === this.frame &&
81
- this.cachedDoneAt === snapshot.doneAt
82
- ) {
83
- return this.cachedLines;
84
- }
85
-
86
- this.cachedLines = renderWorkflowWidgetLines(snapshot, this.frame, width, theme);
87
- this.cachedWidth = width;
88
- this.cachedFrame = this.frame;
89
- this.cachedDoneAt = snapshot.doneAt;
90
- return this.cachedLines;
91
- }
92
-
93
- invalidate(): void {
94
- this.cachedWidth = undefined;
95
- this.cachedFrame = undefined;
96
- this.cachedDoneAt = undefined;
97
- this.cachedLines = undefined;
98
- }
99
- }
100
-
101
- function prioritizedBodyLines(phases: readonly PhaseSnapshot[], theme: Theme): string[] {
102
- const activePhases = phases.filter((phase) => phase.agents.some((agent) => agent.status === "running" || agent.status === "queued"));
103
- const inactivePhases = phases.filter((phase) => !activePhases.includes(phase));
104
- return [...activePhases, ...inactivePhases].flatMap((phase) => phaseLines(phase, theme));
105
- }
106
-
107
- function phaseLines(phase: PhaseSnapshot, theme: Theme): string[] {
108
- if (phase.agents.length === 0 && phase.title === "Workflow") return [];
109
- const running = phase.agents.filter((agent) => agent.status === "running").length;
110
- const done = phase.agents.filter((agent) => agent.status === "done").length;
111
- const failed = phase.agents.filter((agent) => agent.status === "failed").length;
112
- const parts: string[] = [];
113
- if (running > 0) parts.push(`${running} running`);
114
- if (done > 0) parts.push(`${done} done`);
115
- if (failed > 0) parts.push(`${failed} failed`);
116
-
117
- const lines = [`${theme.fg("dim", "├─")} ${theme.fg(running > 0 ? "accent" : "muted", phase.title)}${parts.length ? ` ${theme.fg("dim", `(${parts.join(" · ")})`)}` : ""}`];
118
- const active = phase.agents.filter((agent) => agent.status === "running" || agent.status === "queued");
119
- const failedAgents = phase.agents.filter((agent) => agent.status === "failed");
120
- const completed = phase.agents.filter((agent) => agent.status === "done");
121
- for (const agent of [...active, ...failedAgents, ...completed]) lines.push(agentLine(agent, theme));
122
- return lines;
123
- }
124
-
125
- function agentLine(agent: AgentRowSnapshot, theme: Theme): string {
126
- const detailParts: string[] = [];
127
- if (agent.toolUses > 0) detailParts.push(`${agent.toolUses} tool${agent.toolUses === 1 ? "" : "s"}`);
128
- if (agent.status === "running" && agent.lastTool) detailParts.push(agent.lastTool);
129
- if (agent.startedAt !== undefined) detailParts.push(formatDuration((agent.doneAt ?? Date.now()) - agent.startedAt));
130
- if (agent.status === "failed" && agent.error) detailParts.push(agent.error);
131
-
132
- const activity = detailParts.length > 0 ? ` ${theme.fg("dim", `· ${detailParts.join(" · ")}`)}` : "";
133
- return `${theme.fg("dim", "│ ")} ${statusIcon(agent.status, theme)} ${theme.fg(agent.status === "running" ? "text" : "muted", agent.label)}${activity}`;
134
- }
135
-
136
- function footerLine(snapshot: WorkflowProgressSnapshot, theme: Theme): string | undefined {
137
- const counters = snapshot.counters.slice(0, 4).map((counter) => `${counter.label} ${formatCount(counter.value)}`);
138
- const latestLog = snapshot.logs[snapshot.logs.length - 1];
139
- const parts = counters.length > 0 ? counters : [];
140
- if (latestLog) parts.push(latestLog);
141
- if (parts.length === 0) return undefined;
142
- return `${theme.fg("dim", "└─")} ${theme.fg("dim", parts.join(" · "))}`;
143
- }
package/src/workflows.ts DELETED
@@ -1,14 +0,0 @@
1
- import type { WorkflowModule } from "./types.ts";
2
- import * as codeReview from "../workflows/code-review.ts";
3
- import * as ping from "../workflows/ping.ts";
4
-
5
- /**
6
- * Workflows statically imported by the extension. These are loaded through pi's own
7
- * jiti instance (because they're part of the extension's module graph), so they share
8
- * pi's bundled `typebox` / SDK — guaranteeing schema validation works.
9
- *
10
- * To add a guaranteed workflow: drop a `.ts` in `workflows/`, import it here, and add
11
- * it to this array. (Drop-in workflows discovered at runtime are also supported — see
12
- * discovery.ts — but the static registry is the always-correct path.)
13
- */
14
- export const BUILTIN_WORKFLOWS: WorkflowModule[] = [codeReview, ping];