pi-workflow-engine 0.12.0 → 1.0.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/.pi/extensions/pi-workflow-engine/index.ts +106 -391
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +20 -2
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +5 -11
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +93 -100
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +18 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +37 -86
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +39 -98
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -80
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
- package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
- package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +1 -12
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
- package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +128 -142
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +119 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +41 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -116
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +9 -14
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
- package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
- package/.pi/extensions/pi-workflow-engine/src/types.ts +9 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -126
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +21 -32
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +178 -188
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +55 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +101 -203
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +51 -187
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +33 -109
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +36 -106
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/USAGE.md +90 -17
- package/package.json +1 -2
- package/skills/workflow-code-review-actions/SKILL.md +12 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -49
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { SelectList, visibleWidth, type Component, type SelectItem, type SelectListTheme } from "@earendil-works/pi-tui";
|
|
3
|
+
import { dot, fit, keyHints } from "./kit.ts";
|
|
4
|
+
|
|
5
|
+
const MAX_VISIBLE = 10;
|
|
6
|
+
const NAME_GAP = 2;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The `/workflow` picker: pi's own `SelectList` (so names and descriptions align like the slash
|
|
10
|
+
* autocomplete) under a title, with key hints below, between two rules like pi's native selectors.
|
|
11
|
+
* Items without a description (the author option) render as a full-width label.
|
|
12
|
+
*/
|
|
13
|
+
export class WorkflowPicker implements Component {
|
|
14
|
+
private readonly list: SelectList;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly items: readonly SelectItem[],
|
|
18
|
+
private readonly theme: Theme,
|
|
19
|
+
listTheme: SelectListTheme,
|
|
20
|
+
done: (value: string | undefined) => void,
|
|
21
|
+
) {
|
|
22
|
+
const nameWidth = Math.max(0, ...items.filter((item) => item.description).map((item) => visibleWidth(item.label))) + NAME_GAP;
|
|
23
|
+
this.list = new SelectList([...items], Math.min(MAX_VISIBLE, items.length), listTheme, {
|
|
24
|
+
minPrimaryColumnWidth: nameWidth,
|
|
25
|
+
maxPrimaryColumnWidth: nameWidth,
|
|
26
|
+
});
|
|
27
|
+
this.list.onSelect = (item) => done(item.value);
|
|
28
|
+
this.list.onCancel = () => done(undefined);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
invalidate(): void {
|
|
32
|
+
this.list.invalidate();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
render(width: number): string[] {
|
|
36
|
+
const inner = Math.max(1, width - 2);
|
|
37
|
+
const rule = this.theme.fg("border", "─".repeat(Math.max(1, width)));
|
|
38
|
+
const count = this.items.filter((item) => item.description).length;
|
|
39
|
+
const title = `${this.theme.fg("accent", this.theme.bold("Run a workflow"))}${dot(this.theme)}${this.theme.fg("muted", `${count} available`)}`;
|
|
40
|
+
const hints = keyHints([["↑↓", "move"], ["enter", "run"], ["esc", "cancel"]], this.theme);
|
|
41
|
+
const pad = (line: string) => ` ${fit(line, inner)} `;
|
|
42
|
+
return [rule, pad(title), "", ...this.list.render(inner).map(pad), "", pad(hints), rule];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
handleInput(data: string): void {
|
|
46
|
+
this.list.handleInput(data);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -1,168 +1,281 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Box, type Component
|
|
2
|
+
import { Box, truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
3
3
|
import { isAdvisoryReport, type AdvisoryReportWithStats } from "../advisory-schema.ts";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
import { isRecord } from "../guards.ts";
|
|
5
|
+
import { findingPathColumn, renderFindingDetail, renderFindingRow, sortIssuesForDisplay } from "../review/review-format.ts";
|
|
6
|
+
import { toReviewIssues, type ReviewIssue } from "../review/review-issues.ts";
|
|
7
|
+
import { formatCount, prettyJson } from "../text.ts";
|
|
8
|
+
import { workflowResultSummary } from "../workflow-execution.ts";
|
|
9
|
+
import { GLYPH, joinParts, severityColor, type Severity } from "./kit.ts";
|
|
10
|
+
import { isResearchReport, renderResearchResult } from "./research-result-renderer.ts";
|
|
11
|
+
import {
|
|
12
|
+
agentCount,
|
|
13
|
+
bullets,
|
|
14
|
+
clips,
|
|
15
|
+
footerLine,
|
|
16
|
+
headerLine,
|
|
17
|
+
indent,
|
|
18
|
+
innerWidth,
|
|
19
|
+
labelled,
|
|
20
|
+
LinesComponent,
|
|
21
|
+
paragraph,
|
|
22
|
+
plural,
|
|
23
|
+
runDetailRows,
|
|
24
|
+
type HintPair,
|
|
25
|
+
type ResultFrame,
|
|
26
|
+
type WorkflowResultHints,
|
|
27
|
+
type WorkflowResultView,
|
|
28
|
+
} from "./result-layout.ts";
|
|
29
|
+
|
|
30
|
+
export type { WorkflowResultHints, WorkflowResultView } from "./result-layout.ts";
|
|
31
|
+
|
|
32
|
+
const COLLAPSED_SUMMARY_LINES = 2;
|
|
33
|
+
const COLLAPSED_GENERIC_LINES = 3;
|
|
34
|
+
const COLLAPSED_FINDINGS = 6;
|
|
35
|
+
|
|
36
|
+
/** Recognises persisted result-envelope details by the fields every envelope carries. */
|
|
37
|
+
export function isWorkflowResult(value: unknown): value is WorkflowResultView {
|
|
12
38
|
if (!isRecord(value)) return false;
|
|
13
39
|
return typeof value.name === "string" && "result" in value && typeof value.completedAt === "number";
|
|
14
40
|
}
|
|
15
41
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
42
|
+
/** The `workflow-result` message: the result on pi's custom-message background. */
|
|
43
|
+
export function renderWorkflowResult(view: WorkflowResultView, expanded: boolean, theme: Theme, hints: WorkflowResultHints = {}): Component {
|
|
44
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
45
|
+
box.addChild(workflowResultComponent(view, expanded, theme, hints));
|
|
46
|
+
return box;
|
|
19
47
|
}
|
|
20
48
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
readonly metadata?: WorkflowRunDisplayMetadata;
|
|
49
|
+
/** The bare result, for surfaces that already provide a shell (the workflow tool row). */
|
|
50
|
+
export function workflowResultComponent(view: WorkflowResultView, expanded: boolean, theme: Theme, hints: WorkflowResultHints = {}): Component {
|
|
51
|
+
return new LinesComponent((width) => renderWorkflowResultLines(view, expanded, width, theme, hints));
|
|
25
52
|
}
|
|
26
53
|
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
result: unknown,
|
|
54
|
+
export function renderWorkflowResultLines(
|
|
55
|
+
view: WorkflowResultView,
|
|
30
56
|
expanded: boolean,
|
|
57
|
+
width: number,
|
|
31
58
|
theme: Theme,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
59
|
+
hints: WorkflowResultHints = {},
|
|
60
|
+
): string[] {
|
|
61
|
+
const frame: ResultFrame = { view, expanded, width, theme, hints };
|
|
62
|
+
const lines = view.background === true && isInterruptedState(view.status) ? renderInterruptedRun(frame, view.status)
|
|
63
|
+
: isAdvisoryReport(view.result) ? renderAdvisoryResult(frame, view.result)
|
|
64
|
+
: isResearchReport(view.result) ? renderResearchResult(frame, view.result)
|
|
65
|
+
: renderGenericResult(frame);
|
|
66
|
+
return lines.map((line) => truncateToWidth(line, width, "…"));
|
|
39
67
|
}
|
|
40
68
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
):
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
function renderAdvisoryResult(frame: ResultFrame, report: AdvisoryReportWithStats): string[] {
|
|
70
|
+
const { view, expanded, theme, hints } = frame;
|
|
71
|
+
const width = innerWidth(frame);
|
|
72
|
+
const issues = sortIssuesForDisplay(toReviewIssues(report));
|
|
73
|
+
const incomplete = report.status === "incomplete";
|
|
74
|
+
const failures = incompleteStages(report.coverage ?? []);
|
|
75
|
+
const [findings, ...severities] = findingFacts(issues, incomplete, theme);
|
|
76
|
+
const facts = [
|
|
77
|
+
incomplete ? theme.fg("warning", "incomplete") : undefined,
|
|
78
|
+
...failures.map((failure) => theme.fg("warning", failure)),
|
|
79
|
+
findings,
|
|
80
|
+
];
|
|
81
|
+
const glyph = incomplete ? theme.fg("warning", GLYPH.warning) : theme.fg("success", GLYPH.done);
|
|
82
|
+
const lines = [headerLine(glyph, frame, facts, severities)];
|
|
83
|
+
// An incomplete report's summary restates the header ("Incomplete review: n branch(es) failed…"); show it expanded only.
|
|
84
|
+
if (expanded || !incomplete) {
|
|
85
|
+
lines.push(...indent(paragraph(report.summary, width, (line) => theme.fg("text", line), expanded ? undefined : COLLAPSED_SUMMARY_LINES)));
|
|
52
86
|
}
|
|
53
|
-
|
|
54
|
-
|
|
87
|
+
const gaps = report.gaps ?? [];
|
|
88
|
+
if (!expanded && gaps.length > 0) lines.push(...indent([gapLine(gaps, theme)]));
|
|
55
89
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
pushWorkflowDetailLines(lines, theme, { usage, metadata, perf });
|
|
71
|
-
|
|
72
|
-
if (result.findings.length === 0) {
|
|
73
|
-
lines.push(theme.fg("success", "No findings."));
|
|
74
|
-
if (expanded && result.nextSteps.length > 0) renderNextSteps(result.nextSteps, lines, theme);
|
|
75
|
-
return lines.join("\n");
|
|
90
|
+
if (issues.length > 0) {
|
|
91
|
+
lines.push("");
|
|
92
|
+
if (expanded) {
|
|
93
|
+
issues.forEach((issue, index) => {
|
|
94
|
+
if (index > 0) lines.push("");
|
|
95
|
+
lines.push(...indent(renderFindingDetail(issue, width, theme)));
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
const shown = issues.slice(0, COLLAPSED_FINDINGS);
|
|
99
|
+
const column = findingPathColumn(shown);
|
|
100
|
+
lines.push(...indent(shown.map((issue) => renderFindingRow(issue, width, theme, column))));
|
|
101
|
+
const hidden = issues.length - COLLAPSED_FINDINGS;
|
|
102
|
+
if (hidden > 0) lines.push(...indent([theme.fg("dim", `+${plural(hidden, "more finding", "more findings")}`)]));
|
|
103
|
+
}
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
const issues = toReviewIssues(name, result);
|
|
79
|
-
lines.push(theme.fg("dim", "Findings:"));
|
|
80
|
-
lines.push(renderIssuesTable(issues, theme, { maxRows: expanded ? issues.length : 12 }));
|
|
81
106
|
if (expanded) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
const rows = [
|
|
108
|
+
...labelled(frame, "Coverage", coverageItems(report, theme)),
|
|
109
|
+
...labelled(frame, "Gaps", gaps.map((gap) => theme.fg("warning", `${GLYPH.failed} ${gap}`)), " "),
|
|
110
|
+
...labelled(frame, "Funnel", funnelItems(report.stats, theme)),
|
|
111
|
+
...labelled(frame, "Follow-up", bullets(report.nextSteps), " "),
|
|
112
|
+
...runDetailRows(frame),
|
|
113
|
+
];
|
|
114
|
+
if (rows.length > 0) lines.push("", ...rows);
|
|
85
115
|
}
|
|
86
|
-
if (expanded && result.nextSteps.length > 0) renderNextSteps(result.nextSteps, lines, theme);
|
|
87
|
-
return lines.join("\n");
|
|
88
|
-
}
|
|
89
116
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
117
|
+
const pairs: HintPair[] = hints.triage && view.name === "code-review" && issues.length > 0 ? [[hints.triage, "triage"]] : [];
|
|
118
|
+
const footer = footerLine(frame, pairs, { more: true });
|
|
119
|
+
if (footer) lines.push(footer);
|
|
120
|
+
return lines;
|
|
95
121
|
}
|
|
96
122
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (summary) lines.push(theme.fg("muted", summary));
|
|
109
|
-
pushWorkflowDetailLines(lines, theme, { usage, metadata, perf });
|
|
110
|
-
if (expanded) lines.push(theme.fg("dim", safeJson(result)));
|
|
111
|
-
else if (!summary) lines.push(theme.fg("dim", "Result available in expanded view."));
|
|
112
|
-
return lines.join("\n");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function pushWorkflowDetailLines(lines: string[], theme: Theme, input: WorkflowDetailLineInput): void {
|
|
116
|
-
for (const line of formatWorkflowDetailLines(input)) {
|
|
117
|
-
lines.push(theme.fg("dim", line));
|
|
118
|
-
}
|
|
123
|
+
/** `3 findings · 1 high · 1 medium`, zero severities hidden; `no findings` when the report is clean. */
|
|
124
|
+
function findingFacts(issues: readonly ReviewIssue[], incomplete: boolean, theme: Theme): string[] {
|
|
125
|
+
if (issues.length === 0) return [theme.fg(incomplete ? "warning" : "success", incomplete ? "no verified findings" : "no findings")];
|
|
126
|
+
const severities: Severity[] = ["high", "medium", "low"];
|
|
127
|
+
return [
|
|
128
|
+
theme.fg("text", plural(issues.length, "finding")),
|
|
129
|
+
...severities.flatMap((severity) => {
|
|
130
|
+
const count = issues.filter((issue) => issue.finding.severity === severity).length;
|
|
131
|
+
return count > 0 ? [theme.fg(severityColor(severity), `${count} ${severity}`)] : [];
|
|
132
|
+
}),
|
|
133
|
+
];
|
|
119
134
|
}
|
|
120
135
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
type AdvisoryCoverage = NonNullable<AdvisoryReportWithStats["coverage"]>[number];
|
|
137
|
+
|
|
138
|
+
const STAGE_AGENTS: Readonly<Record<string, string>> = { Find: "finders", Verify: "verifiers", Challenge: "challengers" };
|
|
139
|
+
|
|
140
|
+
/** `1 of 5 finders failed`, `Synthesize failed`: one phrase per stage that lost work. */
|
|
141
|
+
function incompleteStages(coverage: readonly AdvisoryCoverage[]): string[] {
|
|
142
|
+
return coverage.flatMap((stage) => {
|
|
143
|
+
const missing = stage.failed > 0 ? stage.failed : stage.expected - stage.completed;
|
|
144
|
+
if (missing <= 0) return [];
|
|
145
|
+
const outcome = stage.failed > 0 ? "failed" : "missing";
|
|
146
|
+
if (stage.expected <= 1) return [`${stage.stage} ${outcome}`];
|
|
147
|
+
return [`${missing} of ${stage.expected} ${STAGE_AGENTS[stage.stage] ?? `${stage.stage} branches`} ${outcome}`];
|
|
148
|
+
});
|
|
127
149
|
}
|
|
128
150
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
151
|
+
/** The first coverage gap, with a count of the rest: `✗ Find/error-paths: provider failed · +1 more`. */
|
|
152
|
+
function gapLine(gaps: readonly string[], theme: Theme): string {
|
|
153
|
+
const more = gaps.length > 1 ? theme.fg("dim", ` · +${gaps.length - 1} more`) : "";
|
|
154
|
+
return `${theme.fg("error", GLYPH.failed)} ${theme.fg("warning", gaps[0] ?? "")}${more}`;
|
|
132
155
|
}
|
|
133
156
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
157
|
+
/** `9 of 10 branches complete · Find 4 of 5`: totals, then only the stages that lost work. */
|
|
158
|
+
function coverageItems(report: AdvisoryReportWithStats, theme: Theme): string[] {
|
|
159
|
+
const coverage = report.coverage ?? [];
|
|
160
|
+
if (coverage.length === 0) return [];
|
|
161
|
+
const expected = coverage.reduce((sum, stage) => sum + stage.expected, 0);
|
|
162
|
+
const completed = coverage.reduce((sum, stage) => sum + stage.completed, 0);
|
|
163
|
+
const lossy = coverage
|
|
164
|
+
.filter((stage) => stage.completed < stage.expected)
|
|
165
|
+
.map((stage) => theme.fg("warning", `${stage.stage} ${stage.completed} of ${stage.expected}${stage.failed > 0 ? ` (${stage.failed} failed)` : ""}`));
|
|
166
|
+
return [joinParts([theme.fg(lossy.length > 0 ? "warning" : "muted", `${completed} of ${plural(expected, "branch", "branches")} complete`), ...lossy], theme)];
|
|
138
167
|
}
|
|
139
168
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
169
|
+
const FUNNEL_STAGES: readonly (readonly [key: string, singular: string, pluralLabel: string])[] = [
|
|
170
|
+
["files", "file", "files"],
|
|
171
|
+
["candidates", "candidate", "candidates"],
|
|
172
|
+
["verified", "verified", "verified"],
|
|
173
|
+
["kept", "kept", "kept"],
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
/** `3 files → 3 candidates → 3 verified → 3 kept · 1 refuted`, zero side counts hidden. */
|
|
177
|
+
function funnelItems(stats: AdvisoryReportWithStats["stats"], theme: Theme): string[] {
|
|
178
|
+
if (!stats) return [];
|
|
179
|
+
const known = new Set(FUNNEL_STAGES.map(([key]) => key));
|
|
180
|
+
const stages = FUNNEL_STAGES.flatMap(([key, singular, pluralLabel]) => {
|
|
144
181
|
const value = stats[key];
|
|
145
182
|
if (value === undefined) return [];
|
|
146
|
-
return [
|
|
183
|
+
return [typeof value === "number" ? `${formatCount(value)} ${value === 1 ? singular : pluralLabel}` : `${value} ${pluralLabel}`];
|
|
147
184
|
});
|
|
148
|
-
|
|
149
|
-
|
|
185
|
+
const extras = Object.entries(stats).flatMap(([key, value]) => {
|
|
186
|
+
if (known.has(key) || value === 0 || value === "") return [];
|
|
187
|
+
return [`${typeof value === "number" ? formatCount(value) : value} ${key}`];
|
|
188
|
+
});
|
|
189
|
+
const funnel = stages.join(theme.fg("dim", ` ${GLYPH.arrow} `));
|
|
190
|
+
const line = joinParts([funnel ? theme.fg("muted", funnel) : undefined, ...extras.map((extra) => theme.fg("muted", extra))], theme);
|
|
191
|
+
return line ? [line] : [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function renderGenericResult(frame: ResultFrame): string[] {
|
|
195
|
+
const { view, expanded, theme } = frame;
|
|
196
|
+
const width = innerWidth(frame);
|
|
197
|
+
const summary = workflowResultSummary(view.result);
|
|
198
|
+
const legacyBackground = view.background === true;
|
|
199
|
+
const agents = agentCount(view);
|
|
200
|
+
const facts = [theme.fg("success", "done"), agents > 0 ? theme.fg("muted", plural(agents, "agent")) : undefined];
|
|
201
|
+
const lines = [headerLine(theme.fg("success", GLYPH.done), frame, facts)];
|
|
202
|
+
|
|
203
|
+
const json = resultJson(view.result, summary !== undefined);
|
|
204
|
+
let more = json !== undefined;
|
|
205
|
+
if (summary) {
|
|
206
|
+
const style = (line: string) => theme.fg("text", line);
|
|
207
|
+
lines.push(...indent(paragraph(summary, width, style, expanded ? undefined : COLLAPSED_GENERIC_LINES)));
|
|
208
|
+
more ||= clips(summary, width, COLLAPSED_GENERIC_LINES);
|
|
209
|
+
} else if (!expanded) {
|
|
210
|
+
lines.push(...indent([theme.fg("dim", resultShape(view.result))]));
|
|
211
|
+
}
|
|
212
|
+
if (expanded && json !== undefined) {
|
|
213
|
+
if (summary) lines.push("");
|
|
214
|
+
lines.push(...indent(jsonLines(json, width, theme)));
|
|
215
|
+
}
|
|
216
|
+
if (expanded) {
|
|
217
|
+
const rows = runDetailRows(frame, { agents: false });
|
|
218
|
+
if (rows.length > 0) lines.push("", ...rows);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const pairs: HintPair[] = legacyBackground ? [["/workflow:runs", "run history"]] : [];
|
|
222
|
+
const footer = footerLine(frame, pairs, { more: more || Boolean(view.runId) });
|
|
223
|
+
if (footer) lines.push(footer);
|
|
224
|
+
return lines;
|
|
150
225
|
}
|
|
151
226
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
return
|
|
227
|
+
/** The result as pretty JSON, minus the summary already shown above it; undefined when nothing is left. */
|
|
228
|
+
function resultJson(result: unknown, summaryShown: boolean): string | undefined {
|
|
229
|
+
if (typeof result === "string") return undefined;
|
|
230
|
+
if (!summaryShown || !isRecord(result)) return prettyJson(result);
|
|
231
|
+
const rest = Object.entries(result).filter(([key]) => key !== "summary");
|
|
232
|
+
return rest.length > 0 ? prettyJson(Object.fromEntries(rest)) : undefined;
|
|
156
233
|
}
|
|
157
234
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
235
|
+
/** What a summary-less result holds: `result · areas, risks, notes` or `result · 12 items`. */
|
|
236
|
+
function resultShape(result: unknown): string {
|
|
237
|
+
if (result === undefined || result === null) return "no result";
|
|
238
|
+
if (Array.isArray(result)) return `result · ${plural(result.length, "item")}`;
|
|
239
|
+
if (isRecord(result)) {
|
|
240
|
+
const keys = Object.keys(result);
|
|
241
|
+
return keys.length === 0 ? "empty result" : `result · ${keys.join(", ")}`;
|
|
163
242
|
}
|
|
243
|
+
return `result · ${String(result)}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Pretty JSON, each long line wrapped under its own indentation. */
|
|
247
|
+
function jsonLines(json: string, width: number, theme: Theme): string[] {
|
|
248
|
+
return json.split("\n").flatMap((line) => {
|
|
249
|
+
const depth = line.length - line.trimStart().length;
|
|
250
|
+
const lead = " ".repeat(depth);
|
|
251
|
+
return paragraph(line.trimStart(), Math.max(8, width - depth - 2), (part) => part).map((part, index) =>
|
|
252
|
+
theme.fg("dim", `${index === 0 ? lead : `${lead} `}${part}`));
|
|
253
|
+
});
|
|
164
254
|
}
|
|
165
255
|
|
|
166
|
-
|
|
167
|
-
|
|
256
|
+
type InterruptedState = "failed" | "stopped" | "paused";
|
|
257
|
+
|
|
258
|
+
function isInterruptedState(status: unknown): status is InterruptedState {
|
|
259
|
+
return status === "failed" || status === "stopped" || status === "paused";
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** A background run that ended without a result: what happened, and where to resume it. */
|
|
263
|
+
function renderInterruptedRun(frame: ResultFrame, status: InterruptedState): string[] {
|
|
264
|
+
const { view, expanded, theme } = frame;
|
|
265
|
+
const color = status === "failed" ? "error" : "warning";
|
|
266
|
+
const glyph = theme.fg(color, status === "failed" ? GLYPH.failed : GLYPH.warning);
|
|
267
|
+
const lines = [headerLine(glyph, frame, [theme.fg(color, status)])];
|
|
268
|
+
const summary = workflowResultSummary(view.result);
|
|
269
|
+
if (summary) {
|
|
270
|
+
lines.push(...indent(paragraph(summary, innerWidth(frame), (line) => theme.fg("muted", line), expanded ? undefined : COLLAPSED_GENERIC_LINES)));
|
|
271
|
+
}
|
|
272
|
+
if (expanded) {
|
|
273
|
+
const rows = runDetailRows(frame);
|
|
274
|
+
if (rows.length > 0) lines.push("", ...rows);
|
|
275
|
+
}
|
|
276
|
+
const footer = footerLine(frame, [["/workflow:runs", status === "failed" ? "resume or restart" : "resume"]], {
|
|
277
|
+
more: Boolean(view.runId) || (summary !== undefined && clips(summary, innerWidth(frame), COLLAPSED_GENERIC_LINES)),
|
|
278
|
+
});
|
|
279
|
+
if (footer) lines.push(footer);
|
|
280
|
+
return lines;
|
|
168
281
|
}
|