pi-workflow-engine 0.2.0 → 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 +109 -56
- package/package.json +1 -1
- package/src/advisory-schema.ts +64 -0
- package/src/ui/workflow-result-renderer.ts +80 -34
- package/src/workflows.ts +4 -1
- package/workflows/code-review.ts +75 -63
- package/workflows/diagnose.ts +305 -0
- package/workflows/perf-review.ts +295 -0
- package/workflows/refactor-scout.ts +289 -0
package/README.md
CHANGED
|
@@ -1,10 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
<img src="assets/preview.png" width="200" alt="pi-workflow-engine — a central agent orchestrating a ring of subagents" />
|
|
3
|
-
</p>
|
|
1
|
+

|
|
4
2
|
|
|
5
3
|
# pi-workflow-engine
|
|
6
4
|
|
|
7
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
42
|
-
/workflow
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
| `parallel(thunks)` | Runs every thunk concurrently and waits for all
|
|
72
|
-
| `pipeline(items, ...stages)` | Runs each item through all stages independently
|
|
73
|
-
| `phase(title)` / `log(msg)` |
|
|
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 = {
|
|
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,
|
|
125
|
+
export default async function run({ agent, parallel, phase }: WorkflowApi) {
|
|
82
126
|
phase("Find");
|
|
83
|
-
|
|
84
|
-
|
|
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`**
|
|
91
|
-
- **Structured output is a terminating tool
|
|
92
|
-
- **A single global semaphore caps concurrent agents**, so `parallel
|
|
93
|
-
- **
|
|
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
|
|
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
|
|
103
|
-
bun run typecheck
|
|
104
|
-
bun scripts/smoke.ts
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
171
|
+
index.ts extension entry; registers the command and tool
|
|
122
172
|
types.ts WorkflowApi / WorkflowModule contracts
|
|
123
|
-
agent-runner.ts
|
|
173
|
+
agent-runner.ts createAgentSession + terminating-tool schema bridge
|
|
124
174
|
concurrency.ts Semaphore, parallel(), pipeline()
|
|
125
|
-
engine.ts runWorkflow()
|
|
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
|
|
130
|
-
|
|
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
|
|
188
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -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>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Box, type Component, Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import type { AdvisoryFinding, AdvisoryLocation, AdvisoryReport } from "../advisory-schema.ts";
|
|
4
|
+
import { badge, formatCount, type WorkflowThemeColor } from "./workflow-format.ts";
|
|
4
5
|
|
|
5
6
|
export interface WorkflowResultEnvelope {
|
|
6
7
|
name: string;
|
|
@@ -8,19 +9,7 @@ export interface WorkflowResultEnvelope {
|
|
|
8
9
|
completedAt: number;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export interface
|
|
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[];
|
|
12
|
+
export interface AdvisoryWorkflowResult extends AdvisoryReport {
|
|
24
13
|
stats?: Record<string, string | number>;
|
|
25
14
|
}
|
|
26
15
|
|
|
@@ -29,21 +18,32 @@ export function isWorkflowResult(value: unknown): value is WorkflowResultEnvelop
|
|
|
29
18
|
return typeof value.name === "string" && "result" in value && typeof value.completedAt === "number";
|
|
30
19
|
}
|
|
31
20
|
|
|
32
|
-
export function
|
|
21
|
+
export function isAdvisoryLocation(value: unknown): value is AdvisoryLocation {
|
|
33
22
|
if (!isRecord(value)) return false;
|
|
34
|
-
if (typeof value.file !== "string"
|
|
23
|
+
if (typeof value.file !== "string") return false;
|
|
35
24
|
if (value.line !== undefined && typeof value.line !== "number") return false;
|
|
36
|
-
if (value.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
if (value.symbol !== undefined && typeof value.symbol !== "string") return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isAdvisoryFinding(value: unknown): value is AdvisoryFinding {
|
|
30
|
+
if (!isRecord(value)) return false;
|
|
31
|
+
if (typeof value.summary !== "string") return false;
|
|
32
|
+
if (typeof value.category !== "string") return false;
|
|
33
|
+
if (!isSeverity(value.severity)) return false;
|
|
34
|
+
if (!isConfidence(value.confidence)) return false;
|
|
35
|
+
if (!Array.isArray(value.locations) || !value.locations.every(isAdvisoryLocation)) return false;
|
|
36
|
+
if (!Array.isArray(value.evidence) || !value.evidence.every((entry) => typeof entry === "string")) return false;
|
|
37
|
+
if (typeof value.impact !== "string") return false;
|
|
38
|
+
if (typeof value.recommendation !== "string") return false;
|
|
40
39
|
return true;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
export function
|
|
42
|
+
export function isAdvisoryReport(value: unknown): value is AdvisoryWorkflowResult {
|
|
44
43
|
if (!isRecord(value)) return false;
|
|
45
44
|
if (typeof value.summary !== "string" || !Array.isArray(value.findings)) return false;
|
|
46
|
-
if (!value.findings.every(
|
|
45
|
+
if (!value.findings.every(isAdvisoryFinding)) return false;
|
|
46
|
+
if (!Array.isArray(value.nextSteps) || !value.nextSteps.every((entry) => typeof entry === "string")) return false;
|
|
47
47
|
if (value.stats !== undefined && !isStats(value.stats)) return false;
|
|
48
48
|
return true;
|
|
49
49
|
}
|
|
@@ -55,13 +55,13 @@ export function renderWorkflowResult(name: string, result: unknown, expanded: bo
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function renderWorkflowResultText(name: string, result: unknown, expanded: boolean, theme: Theme): string {
|
|
58
|
-
if (
|
|
59
|
-
return
|
|
58
|
+
if (isAdvisoryReport(result)) {
|
|
59
|
+
return renderAdvisoryResult(name, result, expanded, theme);
|
|
60
60
|
}
|
|
61
61
|
return renderGenericWorkflowResult(name, result, expanded, theme);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function
|
|
64
|
+
function renderAdvisoryResult(name: string, result: AdvisoryWorkflowResult, expanded: boolean, theme: Theme): string {
|
|
65
65
|
const icon = theme.fg("success", "✓");
|
|
66
66
|
const title = theme.fg("accent", theme.bold(`Workflow: ${name}`));
|
|
67
67
|
const lines = [`${icon} ${title}`, theme.fg("muted", result.summary)];
|
|
@@ -70,35 +70,73 @@ function renderCodeReviewResult(name: string, result: CodeReviewResult, expanded
|
|
|
70
70
|
|
|
71
71
|
if (result.findings.length === 0) {
|
|
72
72
|
lines.push(theme.fg("success", "No findings."));
|
|
73
|
+
if (expanded && result.nextSteps.length > 0) renderNextSteps(result.nextSteps, lines, theme);
|
|
73
74
|
return lines.join("\n");
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
const findings = expanded ? result.findings : result.findings.slice(0, 3);
|
|
77
78
|
lines.push(theme.fg("dim", expanded ? "Findings:" : "Top findings:"));
|
|
78
79
|
for (const finding of findings) {
|
|
79
|
-
lines.push(
|
|
80
|
+
lines.push(renderAdvisoryFinding(finding, expanded, theme));
|
|
80
81
|
}
|
|
81
82
|
if (!expanded && result.findings.length > findings.length) {
|
|
82
83
|
lines.push(theme.fg("dim", `… ${result.findings.length - findings.length} more finding(s)`));
|
|
83
84
|
}
|
|
85
|
+
if (expanded && result.nextSteps.length > 0) renderNextSteps(result.nextSteps, lines, theme);
|
|
84
86
|
return lines.join("\n");
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
function
|
|
88
|
-
const location =
|
|
89
|
-
const severityColor = finding.severity === "bug" ? "error" : "warning";
|
|
90
|
-
const verdictColor = finding.verdict === "CONFIRMED" ? "success" : finding.verdict === "PLAUSIBLE" ? "warning" : "muted";
|
|
89
|
+
function renderAdvisoryFinding(finding: AdvisoryFinding, expanded: boolean, theme: Theme): string {
|
|
90
|
+
const location = formatLocation(finding.locations[0]);
|
|
91
91
|
let text =
|
|
92
|
-
` ${badge(finding.
|
|
93
|
-
`${badge(finding.
|
|
92
|
+
` ${badge(finding.category, "accent", theme)} ` +
|
|
93
|
+
`${badge(finding.severity, severityColor(finding.severity), theme)} ` +
|
|
94
|
+
`${badge(`${finding.confidence} confidence`, confidenceColor(finding.confidence), theme)} ` +
|
|
94
95
|
`${theme.fg("accent", location)} ${theme.fg("muted", finding.summary)}`;
|
|
95
96
|
if (expanded) {
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
text += `\n ${theme.fg("dim", `Impact: ${finding.impact}`)}`;
|
|
98
|
+
text += `\n ${theme.fg("dim", `Evidence: ${finding.evidence.join("; ") || "(none cited)"}`)}`;
|
|
99
|
+
text += `\n ${theme.fg("dim", `Recommendation: ${finding.recommendation}`)}`;
|
|
98
100
|
}
|
|
99
101
|
return text;
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
function renderNextSteps(nextSteps: string[], lines: string[], theme: Theme): void {
|
|
105
|
+
lines.push(theme.fg("dim", "Next steps:"));
|
|
106
|
+
for (const step of nextSteps) {
|
|
107
|
+
lines.push(` - ${theme.fg("muted", step)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatLocation(location: AdvisoryLocation | undefined): string {
|
|
112
|
+
if (!location) return "(no location)";
|
|
113
|
+
const line = location.line != null ? `:${location.line}` : "";
|
|
114
|
+
const symbol = location.symbol ? ` (${location.symbol})` : "";
|
|
115
|
+
return `${location.file}${line}${symbol}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function severityColor(severity: AdvisoryFinding["severity"]): WorkflowThemeColor {
|
|
119
|
+
switch (severity) {
|
|
120
|
+
case "high":
|
|
121
|
+
return "error";
|
|
122
|
+
case "medium":
|
|
123
|
+
return "warning";
|
|
124
|
+
case "low":
|
|
125
|
+
return "muted";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function confidenceColor(confidence: AdvisoryFinding["confidence"]): WorkflowThemeColor {
|
|
130
|
+
switch (confidence) {
|
|
131
|
+
case "high":
|
|
132
|
+
return "success";
|
|
133
|
+
case "medium":
|
|
134
|
+
return "warning";
|
|
135
|
+
case "low":
|
|
136
|
+
return "muted";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
102
140
|
function renderGenericWorkflowResult(name: string, result: unknown, expanded: boolean, theme: Theme): string {
|
|
103
141
|
const lines = [`${theme.fg("success", "✓")} ${theme.fg("accent", theme.bold(`Workflow: ${name}`))}`];
|
|
104
142
|
const summary = extractSummary(result);
|
|
@@ -139,6 +177,14 @@ function safeJson(value: unknown): string {
|
|
|
139
177
|
}
|
|
140
178
|
}
|
|
141
179
|
|
|
180
|
+
function isSeverity(value: unknown): value is AdvisoryFinding["severity"] {
|
|
181
|
+
return value === "low" || value === "medium" || value === "high";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isConfidence(value: unknown): value is AdvisoryFinding["confidence"] {
|
|
185
|
+
return value === "low" || value === "medium" || value === "high";
|
|
186
|
+
}
|
|
187
|
+
|
|
142
188
|
function isStats(value: unknown): value is Record<string, string | number> {
|
|
143
189
|
if (!isRecord(value)) return false;
|
|
144
190
|
return Object.values(value).every((entry) => typeof entry === "string" || typeof entry === "number");
|
package/src/workflows.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { WorkflowModule } from "./types.ts";
|
|
2
2
|
import * as codeReview from "../workflows/code-review.ts";
|
|
3
3
|
import * as ping from "../workflows/ping.ts";
|
|
4
|
+
import * as refactorScout from "../workflows/refactor-scout.ts";
|
|
5
|
+
import * as diagnose from "../workflows/diagnose.ts";
|
|
6
|
+
import * as perfReview from "../workflows/perf-review.ts";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Workflows statically imported by the extension. These are loaded through pi's own
|
|
@@ -11,4 +14,4 @@ import * as ping from "../workflows/ping.ts";
|
|
|
11
14
|
* it to this array. (Drop-in workflows discovered at runtime are also supported — see
|
|
12
15
|
* discovery.ts — but the static registry is the always-correct path.)
|
|
13
16
|
*/
|
|
14
|
-
export const BUILTIN_WORKFLOWS: WorkflowModule[] = [codeReview, ping];
|
|
17
|
+
export const BUILTIN_WORKFLOWS: WorkflowModule[] = [codeReview, ping, refactorScout, diagnose, perfReview];
|