pi-workflow-engine 0.1.1 → 0.3.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/README.md CHANGED
@@ -1,10 +1,27 @@
1
- <p align="center">
2
- <img src="assets/preview.png" width="200" alt="pi-workflow-engine — a central agent orchestrating a ring of subagents" />
3
- </p>
1
+ ![agents](assets/preview.png)
4
2
 
5
3
  # pi-workflow-engine
6
4
 
7
- A customisable multi-agent workflow-orchestration engine for the [pi](https://pi.dev) coding agent — your own version of Claude Code's built-in `/code-review`, built on pi's SDK. Workflows fan out to many subagents, pass validated structured data between stages, and synthesise a result.
5
+ Claude Code style workflows for the [pi](https://pi.dev) coding agent.
6
+
7
+ This is not just a way to run a prompt. It is a way to turn an agentic procedure into code: scope the task, fork isolated subagents, fan out across review lenses, validate every handoff with schemas, verify candidate findings, and synthesize one ranked result.
8
+
9
+ The built-in workflows are advisory: they inspect, fan out, verify, and report. They do not edit files. Start with `/workflow code-review`, or use the focused scouts for refactors, diagnosis, and performance.
10
+
11
+ ## Why workflows matter
12
+
13
+ Claude Code popularised a useful pattern: the best agentic coding work is not a single chat turn. It is a loop of context gathering, delegated action, verification, and synthesis. Its docs describe the [agentic loop](https://code.claude.com/docs/en/glossary#agentic-loop), [skills and commands](https://code.claude.com/docs/en/skills), and [subagents](https://code.claude.com/docs/en/glossary#subagent) as composable building blocks for repeatable engineering work.
14
+
15
+ `pi-workflow-engine` brings that shape to pi with static TypeScript workflows:
16
+
17
+ - **Procedures, not prompts**: write the workflow once, then invoke it with `/workflow` or let the host agent call the `workflow` tool.
18
+ - **Isolated subagents**: each `agent()` runs in its own in-memory pi session, so exploratory work does not pollute the main conversation.
19
+ - **Parallel cognition**: run many focused agents at once, with a shared concurrency cap so large workflows stay bounded.
20
+ - **Typed handoffs**: pass structured data between stages using typebox schemas instead of asking the model to emit parseable prose.
21
+ - **Verifier stages**: make verification part of the control flow, not an optional final instruction.
22
+ - **Live progress**: surface phases, agent status, counters, and lane items in the TUI while the run is still moving.
23
+
24
+ The result is closer to an executable review playbook than a chatbot shortcut.
8
25
 
9
26
  ## Install
10
27
 
@@ -18,118 +35,154 @@ Or from npm:
18
35
  pi install npm:pi-workflow-engine
19
36
  ```
20
37
 
21
- That's all — pi fetches the package and serves its dependencies from its own bundle, so there's no clone, install, or build step. Restart pi (or run `/reload` in an open session), then confirm it's registered:
38
+ That's all. pi fetches the package and serves its core dependencies from its own bundle, so there is no clone, install, or build step. Restart pi, or run `/reload` in an open session, then confirm it is registered:
22
39
 
23
40
  ```bash
24
41
  pi list
25
42
  ```
26
43
 
27
- Scope it to a single repo instead of globally (writes to that repo's `.pi/settings.json`):
44
+ Scope it to a single repo instead of globally. This writes to that repo's `.pi/settings.json`:
28
45
 
29
46
  ```bash
30
47
  pi install git:github.com/timbrinded/pi-workflow-engine -l
31
48
  ```
32
49
 
33
- Uninstall with `pi remove git:github.com/timbrinded/pi-workflow-engine`.
50
+ Uninstall with:
51
+
52
+ ```bash
53
+ pi remove git:github.com/timbrinded/pi-workflow-engine
54
+ ```
34
55
 
35
56
  ## Usage
36
57
 
37
58
  In a pi session, from inside a git repo with changes:
38
59
 
39
- ```
40
- /workflow code-review # review the current branch
41
- /workflow code-review HEAD~3 # review a ref range / target / focus area
42
- /workflow ping # quick engine smoke test (one agent)
60
+ ```text
61
+ /workflow code-review # review the current branch or open PR
62
+ /workflow code-review HEAD~3 # review a ref range, target, or focus area
63
+ /workflow refactor-scout src/ # find safe refactor opportunities
64
+ /workflow diagnose "typecheck fails after the schema change"
65
+ /workflow perf-review "workflow startup latency"
66
+ /workflow code-review --inspect # open the live workflow inspector
67
+ /workflow ping # quick engine smoke test
43
68
  ```
44
69
 
45
- The host agent can also invoke the `workflow` tool itself mid-conversation (e.g. "run the code-review workflow") and fold the structured result back into its reasoning.
46
-
47
- `code-review` returns a verified, ranked report:
48
-
49
- ```json
50
- {
51
- "summary": "1 confirmed bug: off-by-one loop boundary in sum.js ...",
52
- "findings": [
53
- {
54
- "file": "sum.js",
55
- "line": 3,
56
- "severity": "bug",
57
- "verdict": "CONFIRMED",
58
- "summary": "Off-by-one: `i <= arr.length` should be `i < arr.length`; accesses arr[arr.length] (undefined) → NaN."
59
- }
60
- ]
61
- }
70
+ The host agent can also invoke the `workflow` tool mid-conversation:
71
+
72
+ ```text
73
+ Run the code-review workflow on this PR and use the result before deciding what to fix.
62
74
  ```
63
75
 
64
- ## How it works
76
+ The advisory workflows inspect and report only; they do not edit files. They return the same shape: summary, ranked findings, evidence, impact, recommendations, and next steps.
77
+
78
+ ## Built-in workflows
79
+
80
+ - `code-review`: Reviews the current branch, open PR, ref range, or target. It looks for correctness bugs and cleanup issues, then independently verifies candidates before ranking them.
81
+ - `refactor-scout`: Looks for small, safe refactor opportunities: duplication, complexity, weak types, boundary leaks, dead code, and convention drift.
82
+ - `diagnose`: Investigates a symptom, failing command, or regression. It generates competing root-cause hypotheses, verifies them, and returns the most likely causes with next validation steps.
83
+ - `perf-review`: Reviews a slow path or workload for bottleneck hypotheses, measurement gaps, and safe optimization directions. It avoids claiming certainty when measurement evidence is missing.
84
+ - `ping`: Minimal smoke workflow for checking that the engine can run one structured agent call.
85
+
86
+ ## The code-review workflow
87
+
88
+ The bundled review workflow is deliberately shaped like a serious review process:
89
+
90
+ 1. **Scope**: detect the open PR or branch diff, list changed files, and read relevant project conventions.
91
+ 2. **Find**: fan out across focused review lenses such as logic bugs, error paths, edge cases, simplification, and conventions.
92
+ 3. **Gate and dedupe**: bound candidates to changed lines and collapse duplicate findings before spending verifier tokens.
93
+ 4. **Verify**: send each survivor to an independent verifier that must confirm, mark plausible, or refute with evidence.
94
+ 5. **Synthesize**: produce one ranked report with stats, verdicts, and concrete locations.
65
95
 
66
- A **workflow** is a TypeScript module that exports `meta` plus a default `async (api) => result`. The injected `api` gives you five primitives:
96
+ The review lenses live in `workflows/code-review.ts`. That is where your repo's real failure modes belong.
97
+
98
+ ## Authoring workflows
99
+
100
+ A workflow is a TypeScript module that exports `meta` plus a default `async (api) => result`. The injected `api` gives you the primitives:
67
101
 
68
102
  | Primitive | Behaviour |
69
103
  |-----------|-----------|
70
- | `agent(prompt, { schema })` | Runs a subagent in an isolated session; with a typebox `schema` it returns validated structured data, otherwise the final text. |
71
- | `parallel(thunks)` | Runs every thunk concurrently and waits for all (a barrier). |
72
- | `pipeline(items, ...stages)` | Runs each item through all stages independently — no barrier between stages. |
73
- | `phase(title)` / `log(msg)` | Drive the live progress tree shown in the TUI. |
104
+ | `agent(prompt, { schema })` | Runs a subagent in an isolated session. With a typebox `schema`, returns validated structured data; otherwise returns final text. |
105
+ | `parallel(thunks)` | Runs every thunk concurrently and waits for all results. |
106
+ | `pipeline(items, ...stages)` | Runs each item through all stages independently, with no barrier between stages. |
107
+ | `phase(title)` / `log(msg)` | Drives the live progress tree shown in the TUI and stderr breadcrumbs when headless. |
108
+ | `progress(event)` | Emits structured progress for richer workflow UI surfaces. |
74
109
 
75
110
  ```ts
76
111
  import { Type } from "typebox";
77
112
  import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
78
113
 
79
- export const meta: WorkflowMeta = { name: "my-workflow", description: "..." };
114
+ export const meta: WorkflowMeta = {
115
+ name: "my-workflow",
116
+ description: "Find, verify, and summarize something important.",
117
+ };
118
+
119
+ const FindingSchema = Type.Object({
120
+ summary: Type.String(),
121
+ file: Type.String(),
122
+ line: Type.Optional(Type.Number()),
123
+ });
80
124
 
81
- export default async function run({ agent, parallel, pipeline, phase, log, args }: WorkflowApi) {
125
+ export default async function run({ agent, parallel, phase }: WorkflowApi) {
82
126
  phase("Find");
83
- const result = await agent("find issues ...", { schema: Type.Object({ /* ... */ }) });
84
- return result; // a typed object, validated against the schema
127
+
128
+ const findings = await parallel([
129
+ () => agent("Find correctness bugs in the diff.", { schema: FindingSchema, thinkingLevel: "low" }),
130
+ () => agent("Find error-handling bugs in the diff.", { schema: FindingSchema, thinkingLevel: "low" }),
131
+ ]);
132
+
133
+ phase("Synthesize");
134
+ return agent(`Summarize these findings: ${JSON.stringify(findings)}`, { thinkingLevel: "medium" });
85
135
  }
86
136
  ```
87
137
 
88
138
  Under the hood:
89
139
 
90
- - **Each `agent()` is an in-process pi `AgentSession`** (`createAgentSession` + `SessionManager.inMemory()`), inheriting the host's model unless you set one.
91
- - **Structured output is a terminating tool**: the engine registers one tool whose `parameters` *is* your schema; pi validates the call and the engine captures the args — no parsing.
92
- - **A single global semaphore caps concurrent agents**, so `parallel`/`pipeline` nest freely while the cap holds inside every `agent()` call.
93
- - **Surfaces**: a `/workflow <name> [args]` command, plus a `workflow` tool the host agent can call.
140
+ - **Each `agent()` is an in-process pi `AgentSession`** using `createAgentSession` and `SessionManager.inMemory()`.
141
+ - **Structured output is a terminating tool**. The engine registers one tool whose `parameters` is your schema; pi validates the call and the engine captures the args. There is no JSON scraping.
142
+ - **A single global semaphore caps concurrent agents**, so `parallel` and `pipeline` can nest freely while every `agent()` call still respects the run cap.
143
+ - **Two surfaces are registered**: `/workflow <name> [args]` for direct use, and a `workflow` tool for host-agent delegation.
94
144
 
95
145
  ## Local development
96
146
 
97
- Only needed if you want to **add or customise workflows, or contribute** — using the bundled workflows requires none of this.
147
+ Only needed if you want to add workflows, customise workflows, or contribute. Using the bundled workflows requires none of this.
98
148
 
99
149
  ```bash
100
150
  git clone https://github.com/timbrinded/pi-workflow-engine
101
151
  cd pi-workflow-engine
102
- bun install # dev deps for typechecking only; pi serves bundled copies at runtime
103
- bun run typecheck # tsc --noEmit
104
- bun scripts/smoke.ts # no-LLM check: module graph loads + workflow discovery resolves
152
+ bun install
153
+ bun run typecheck
154
+ bun scripts/smoke.ts
105
155
  ```
106
156
 
107
- Load your working copy into a session without installing it (ephemeral — overrides nothing):
157
+ Load your working copy into a session without installing it. This is ephemeral and overrides nothing:
108
158
 
109
159
  ```bash
110
160
  pi -e ./src/index.ts -p "/workflow ping"
111
161
  ```
112
162
 
113
- **Add a workflow:** drop a `.ts` in `workflows/`, import it in `src/workflows.ts`, and add it to `BUILTIN_WORKFLOWS`. Statically-imported workflows share pi's bundled `typebox`, which guarantees schema validation. (Files in `workflows/` and `~/.pi/agent/workflows/` are also discovered dynamically at runtime, best-effort.)
163
+ Add a workflow by creating `workflows/<name>.ts`, importing it in `src/workflows.ts`, and adding it to `BUILTIN_WORKFLOWS`. Statically imported workflows share pi's bundled `typebox`, which guarantees schema validation. Files in `workflows/` and `~/.pi/agent/workflows/` are also discovered dynamically at runtime on a best-effort basis.
114
164
 
115
- **Customise the review:** edit the `ANGLES` array in `workflows/code-review.ts` — the lenses are where your codebase's real failure modes and conventions belong. Tune `model` / `thinkingLevel` / `tools` per `agent()` call, and `DEFAULT_CONCURRENCY` in `src/engine.ts`.
165
+ Tune the built-in review workflow by editing the `ANGLES` array in `workflows/code-review.ts`. Tune `model`, `thinkingLevel`, and `tools` per `agent()` call, and tune `DEFAULT_CONCURRENCY` in `src/engine.ts`.
116
166
 
117
167
  ### Layout
118
168
 
119
- ```
169
+ ```text
120
170
  src/
121
- index.ts extension entry (registers the command + tool)
171
+ index.ts extension entry; registers the command and tool
122
172
  types.ts WorkflowApi / WorkflowModule contracts
123
- agent-runner.ts the createAgentSession + terminating-tool schema bridge
173
+ agent-runner.ts createAgentSession + terminating-tool schema bridge
124
174
  concurrency.ts Semaphore, parallel(), pipeline()
125
- engine.ts runWorkflow() — binds the primitives to a run
175
+ engine.ts runWorkflow(); binds primitives to one run
126
176
  progress.ts live phase/agent tree via ctx.ui.setWidget
127
177
  discovery.ts static registry + best-effort drop-in loading
128
178
  workflows/
129
- code-review.ts the example: scope → find → verify → synthesize
130
- ping.ts minimal smoke workflow
179
+ code-review.ts scope -> find -> verify -> synthesize
180
+ refactor-scout.ts advisory refactor opportunities
181
+ diagnose.ts advisory bug diagnosis
182
+ perf-review.ts advisory performance investigation
183
+ ping.ts minimal smoke workflow
131
184
  ```
132
185
 
133
186
  ## License
134
187
 
135
- MIT — see [LICENSE](LICENSE).
188
+ MIT. See [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-workflow-engine",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "A customisable multi-agent workflow-orchestration engine for the pi coding agent.",
6
6
  "keywords": ["pi-package"],
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "scripts": {
18
18
  "typecheck": "tsc --noEmit",
19
+ "test:ui": "bun scripts/test-workflow-ui.ts",
19
20
  "prepublishOnly": "tsc --noEmit"
20
21
  },
21
22
  "peerDependencies": {
@@ -0,0 +1,64 @@
1
+ import { Type, type Static } from "typebox";
2
+
3
+ export const AdvisorySeveritySchema = Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")], {
4
+ description: "Impact level of an advisory finding.",
5
+ });
6
+
7
+ export const AdvisoryConfidenceSchema = Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")], {
8
+ description: "Confidence that the finding is actionable.",
9
+ });
10
+
11
+ export const AdvisoryVerifierVerdictSchema = Type.Union([Type.Literal("CONFIRMED"), Type.Literal("PLAUSIBLE"), Type.Literal("REFUTED")], {
12
+ description: "Verifier judgment for a candidate finding.",
13
+ });
14
+
15
+ export const AdvisoryLocationSchema = Type.Object({
16
+ file: Type.String({ description: "Repository-relative file path." }),
17
+ line: Type.Optional(Type.Number({ description: "Relevant 1-based line number when known." })),
18
+ symbol: Type.Optional(Type.String({ description: "Relevant function, class, type, or other symbol when known." })),
19
+ });
20
+
21
+ export const AdvisoryCandidateSchema = Type.Object({
22
+ summary: Type.String({ description: "One-line candidate finding or hypothesis." }),
23
+ category: Type.String({ description: "Workflow-specific category such as bug, duplication, root-cause, or io." }),
24
+ locations: Type.Array(AdvisoryLocationSchema, { description: "Relevant code or configuration locations." }),
25
+ impact: Type.String({ description: "Concrete consequence if this candidate is real." }),
26
+ recommendation: Type.Optional(Type.String({ description: "Optional first-step recommendation before synthesis." })),
27
+ });
28
+
29
+ export const AdvisoryCandidatesSchema = Type.Object({
30
+ candidates: Type.Array(AdvisoryCandidateSchema, { description: "Candidate advisory findings or hypotheses." }),
31
+ });
32
+
33
+ export const AdvisoryVerdictSchema = Type.Object({
34
+ verdict: AdvisoryVerifierVerdictSchema,
35
+ evidence: Type.Array(Type.String({ description: "Quoted or cited evidence supporting the verdict." })),
36
+ confidence: Type.Optional(AdvisoryConfidenceSchema),
37
+ });
38
+
39
+ export const AdvisoryFindingSchema = Type.Object({
40
+ summary: Type.String({ description: "One-line final finding." }),
41
+ category: Type.String({ description: "Workflow-specific category for grouping and display." }),
42
+ severity: AdvisorySeveritySchema,
43
+ confidence: AdvisoryConfidenceSchema,
44
+ locations: Type.Array(AdvisoryLocationSchema, { description: "Relevant code or configuration locations." }),
45
+ evidence: Type.Array(Type.String({ description: "Quoted or cited evidence." })),
46
+ impact: Type.String({ description: "Concrete user, runtime, maintenance, or performance impact." }),
47
+ recommendation: Type.String({ description: "Advisory next action; not an automatic edit." }),
48
+ });
49
+
50
+ export const AdvisoryReportSchema = Type.Object({
51
+ summary: Type.String({ description: "Short overall advisory summary." }),
52
+ findings: Type.Array(AdvisoryFindingSchema, { description: "Verified and ranked advisory findings." }),
53
+ nextSteps: Type.Array(Type.String({ description: "Concrete follow-up commands, inspections, or decisions." })),
54
+ });
55
+
56
+ export type AdvisorySeverity = Static<typeof AdvisorySeveritySchema>;
57
+ export type AdvisoryConfidence = Static<typeof AdvisoryConfidenceSchema>;
58
+ export type AdvisoryVerifierVerdict = Static<typeof AdvisoryVerifierVerdictSchema>;
59
+ export type AdvisoryLocation = Static<typeof AdvisoryLocationSchema>;
60
+ export type AdvisoryCandidate = Static<typeof AdvisoryCandidateSchema>;
61
+ export type AdvisoryCandidates = Static<typeof AdvisoryCandidatesSchema>;
62
+ export type AdvisoryVerdict = Static<typeof AdvisoryVerdictSchema>;
63
+ export type AdvisoryFinding = Static<typeof AdvisoryFindingSchema>;
64
+ export type AdvisoryReport = Static<typeof AdvisoryReportSchema>;
@@ -49,11 +49,13 @@ function lastAssistantText(state: AgentState): string {
49
49
  export async function runAgent(rc: RunContext, prompt: string, opts: AgentOptions = {}): Promise<unknown> {
50
50
  const label = opts.label ?? "agent";
51
51
 
52
- // Acquire a global concurrency slot before spawning the session.
52
+ // Track queued agents before acquiring a global concurrency slot.
53
+ const rowId = rc.progress.agentQueued(opts.phase, label);
53
54
  return rc.semaphore.run(async () => {
54
- rc.progress.agentStart(opts.phase, label);
55
+ rc.progress.agentStart(opts.phase, label, rowId);
55
56
 
56
57
  let captured: unknown = null;
58
+ let failed = false;
57
59
  const customTools: ToolDefinition[] = opts.schema
58
60
  ? [
59
61
  defineTool({
@@ -92,7 +94,7 @@ export async function runAgent(rc: RunContext, prompt: string, opts: AgentOption
92
94
 
93
95
  const unsubscribe = session.subscribe((event) => {
94
96
  if (event.type === "tool_execution_start" && event.toolName !== FINAL_TOOL) {
95
- rc.progress.agentTool(label, event.toolName);
97
+ rc.progress.agentTool(label, event.toolName, rowId);
96
98
  }
97
99
  });
98
100
 
@@ -107,12 +109,14 @@ export async function runAgent(rc: RunContext, prompt: string, opts: AgentOption
107
109
  }
108
110
  return lastAssistantText(session.state);
109
111
  } catch (error) {
112
+ failed = true;
113
+ rc.progress.agentFailed(label, error, rowId);
110
114
  rc.progress.log(`${label} failed: ${error instanceof Error ? error.message : String(error)}`);
111
115
  throw error;
112
116
  } finally {
113
117
  unsubscribe();
114
118
  session.dispose();
115
- rc.progress.agentDone(label);
119
+ if (!failed) rc.progress.agentDone(label, rowId);
116
120
  }
117
121
  });
118
122
  }
package/src/engine.ts CHANGED
@@ -3,7 +3,8 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { parallel, pipeline, Semaphore } from "./concurrency.ts";
4
4
  import { runAgent, type RunContext } from "./agent-runner.ts";
5
5
  import { ProgressTracker } from "./progress.ts";
6
- import type { AgentOptions, WorkflowApi, WorkflowModule } from "./types.ts";
6
+ import type { AgentOptions, WorkflowApi, WorkflowModule, WorkflowRunOptions } from "./types.ts";
7
+ import { WorkflowInspector } from "./ui/workflow-inspector.ts";
7
8
 
8
9
  /** Default global cap on concurrent agents per run. */
9
10
  const DEFAULT_CONCURRENCY = Math.min(8, Math.max(2, cpus().length));
@@ -12,8 +13,22 @@ const DEFAULT_CONCURRENCY = Math.min(8, Math.max(2, cpus().length));
12
13
  * Run a workflow module: build the per-run primitives (binding agent/parallel/pipeline
13
14
  * to a shared semaphore + progress tracker), invoke the workflow, return its result.
14
15
  */
15
- export async function runWorkflow(ctx: ExtensionContext, mod: WorkflowModule, args: string): Promise<unknown> {
16
+ export async function runWorkflow(
17
+ ctx: ExtensionContext,
18
+ mod: WorkflowModule,
19
+ args: string,
20
+ options: WorkflowRunOptions = {},
21
+ ): Promise<unknown> {
16
22
  const progress = new ProgressTracker(ctx, mod.meta.name);
23
+ if (options.inspect && ctx.hasUI) {
24
+ void ctx.ui
25
+ .custom<void>(
26
+ (tui, theme, _keybindings, done) => new WorkflowInspector(() => progress.snapshot(), tui, theme, () => done(undefined)),
27
+ { overlay: true, overlayOptions: { anchor: "right-center", width: "60%", maxHeight: "80%", margin: 1 } },
28
+ )
29
+ .catch((error: unknown) => progress.log(`inspector failed: ${error instanceof Error ? error.message : String(error)}`));
30
+ }
31
+
17
32
  const rc: RunContext = {
18
33
  cwd: ctx.cwd,
19
34
  hostModel: ctx.model,
@@ -31,6 +46,7 @@ export async function runWorkflow(ctx: ExtensionContext, mod: WorkflowModule, ar
31
46
  pipeline,
32
47
  phase: (title) => progress.phase(title),
33
48
  log: (message) => progress.log(message),
49
+ progress: (event) => progress.event(event),
34
50
  args,
35
51
  cwd: ctx.cwd,
36
52
  signal: ctx.signal,
package/src/index.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import { fileURLToPath } from "node:url";
2
2
  import { Type } from "typebox";
3
3
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
+ import { Text } from "@earendil-works/pi-tui";
4
5
  import { discoverWorkflows } from "./discovery.ts";
5
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";
6
9
 
7
10
  /** Repo root (this file lives in <repo>/src/index.ts). */
8
11
  const REPO_DIR = fileURLToPath(new URL("..", import.meta.url));
@@ -18,34 +21,84 @@ function formatReport(name: string, result: unknown): string {
18
21
  return [`## Workflow: ${name}`, "", "```json", JSON.stringify(result, null, 2), "```"].join("\n");
19
22
  }
20
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
+
21
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
+
22
81
  // /workflow <name> [args] — user-invoked.
23
82
  pi.registerCommand("workflow", {
24
83
  description: "Run a multi-agent workflow: /workflow <name> [args]",
25
84
  handler: async (args: string, ctx: ExtensionCommandContext) => {
26
- const trimmed = args.trim();
27
- const space = trimmed.indexOf(" ");
28
- const name = space === -1 ? trimmed : trimmed.slice(0, space);
29
- const rest = space === -1 ? "" : trimmed.slice(space + 1).trim();
30
-
31
85
  const workflows = await discoverWorkflows(REPO_DIR);
32
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;
33
89
 
34
- if (!name) {
90
+ if (!invocation) {
35
91
  ctx.ui.notify(`Usage: /workflow <name> [args]. Available: ${available}`, "warning");
36
92
  return;
37
93
  }
38
- const mod = workflows.get(name);
94
+
95
+ const mod = workflows.get(invocation.name);
39
96
  if (!mod) {
40
- ctx.ui.notify(`Unknown workflow "${name}". Available: ${available}`, "error");
97
+ ctx.ui.notify(`Unknown workflow "${invocation.name}". Available: ${available}`, "error");
41
98
  return;
42
99
  }
43
100
 
44
- const result = await runWorkflow(ctx, mod, rest);
45
- pi.sendMessage(
46
- { customType: "workflow-result", content: formatReport(name, result), display: true, details: result },
47
- { triggerTurn: false },
48
- );
101
+ await sendWorkflowResult(pi, ctx, invocation.name, mod, invocation.args, invocation.options);
49
102
  },
50
103
  });
51
104
 
@@ -59,6 +112,18 @@ export default function workflowEngine(pi: ExtensionAPI): void {
59
112
  name: Type.String({ description: "Workflow name, e.g. code-review" }),
60
113
  args: Type.Optional(Type.String({ description: "Arguments for the workflow (e.g. target or focus)" })),
61
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
+ },
62
127
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
63
128
  const workflows = await discoverWorkflows(REPO_DIR);
64
129
  const mod = workflows.get(params.name);
@@ -70,7 +135,7 @@ export default function workflowEngine(pi: ExtensionAPI): void {
70
135
  };
71
136
  }
72
137
  const result = await runWorkflow(ctx, mod, params.args ?? "");
73
- return { content: [{ type: "text", text: summarize(result) }], details: result };
138
+ return { content: [{ type: "text", text: summarize(result) }], details: workflowEnvelope(params.name, result) };
74
139
  },
75
140
  });
76
141
  }