pi-workflow-engine 0.11.0 → 0.13.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 +255 -97
- 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 +13 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +26 -4
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +3 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +82 -10
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +13 -6
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +88 -1
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +127 -41
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +24 -0
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +120 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +43 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
- package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/types.ts +52 -15
- package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -3
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +151 -178
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +47 -3
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +65 -162
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +48 -149
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +28 -77
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +28 -77
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +72 -141
- package/USAGE.md +270 -17
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AdvisoryCandidatesSchema, AdvisoryVerdictSchema, type AdvisoryCandidate, type
|
|
2
|
-
import {
|
|
3
|
-
import type { AgentOptions, WorkflowApi, WorkflowProgressEvent } from "./types.ts";
|
|
1
|
+
import { AdvisoryCandidatesSchema, AdvisoryVerdictSchema, type AdvisoryCandidate, type IdentifiedAdvisoryCandidate, type AdvisoryLocation, type AdvisoryReport, type AdvisoryVerdict } from "./advisory-schema.ts";
|
|
2
|
+
import { AdvisorySynthesisSchema, SYNTHESIS_ID_INSTRUCTIONS, withAdvisoryCoverage, collectAdvisoryStage, dedupeCandidates, identifyCandidates, uniqueLocations, type AdvisoryStageCoverage, type AdvisorySynthesis } from "./advisory-evidence.ts";
|
|
3
|
+
import type { AgentOptions, WorkflowApi, WorkflowProgressEvent, WorkflowRunStats } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
export interface AdvisoryLens {
|
|
6
6
|
label: string;
|
|
@@ -8,17 +8,19 @@ export interface AdvisoryLens {
|
|
|
8
8
|
text: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export type AdvisoryVerified
|
|
11
|
+
export type AdvisoryVerified = IdentifiedAdvisoryCandidate & {
|
|
12
12
|
verdict: AdvisoryVerdict["verdict"];
|
|
13
13
|
evidence: string[];
|
|
14
14
|
confidence?: AdvisoryVerdict["confidence"];
|
|
15
|
+
challenge?: import("./advisory-challenge.ts").ChallengeRecord;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
export interface LensVerificationPipelineResult
|
|
18
|
-
verified:
|
|
18
|
+
export interface LensVerificationPipelineResult {
|
|
19
|
+
verified: AdvisoryVerified[];
|
|
19
20
|
rawCandidates: number;
|
|
20
21
|
dropped: number;
|
|
21
22
|
refuted: number;
|
|
23
|
+
coverage: AdvisoryStageCoverage[];
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/** Default concrete read/inspect tools for advisory workflows. */
|
|
@@ -27,20 +29,34 @@ export const DEFAULT_ADVISORY_TOOLS: NonNullable<AgentOptions["tools"]> = ["read
|
|
|
27
29
|
/** Dynamically include installed grep/find/search-like extension tools. */
|
|
28
30
|
export const DEFAULT_ADVISORY_TOOL_HINTS: NonNullable<AgentOptions["toolHints"]> = ["search"];
|
|
29
31
|
|
|
30
|
-
export
|
|
32
|
+
export function emptyAdvisoryReport<Stats extends WorkflowRunStats>(
|
|
33
|
+
summary: string,
|
|
34
|
+
nextSteps: string[],
|
|
35
|
+
stats: Stats,
|
|
36
|
+
): AdvisoryReport & { stats: Stats } {
|
|
37
|
+
return { summary, findings: [], nextSteps, stats };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function publishVerifiedKeptProgress(
|
|
41
|
+
api: Pick<WorkflowApi, "progress" | "log">,
|
|
42
|
+
verified: number,
|
|
43
|
+
kept: number,
|
|
44
|
+
): void {
|
|
45
|
+
api.progress({ type: "counter", key: "verified", label: "verified", value: verified });
|
|
46
|
+
api.progress({ type: "counter", key: "kept", label: "kept", value: kept });
|
|
47
|
+
api.progress({ type: "summary", key: "verified", value: verified });
|
|
48
|
+
api.progress({ type: "summary", key: "kept", value: kept });
|
|
49
|
+
api.log(`${verified} verified → ${kept} kept`);
|
|
50
|
+
}
|
|
31
51
|
|
|
32
|
-
export interface LensVerificationPipelineOptions
|
|
33
|
-
api: Pick<WorkflowApi, "agent" | "parallel" | "
|
|
34
|
-
lenses: readonly
|
|
52
|
+
export interface LensVerificationPipelineOptions {
|
|
53
|
+
api: Pick<WorkflowApi, "agent" | "parallel" | "phase" | "progress" | "log">;
|
|
54
|
+
lenses: readonly AdvisoryLens[];
|
|
35
55
|
perLens: number;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
schedulingMode?: AdvisorySchedulingMode;
|
|
41
|
-
finderPrompt(lens: Lens): string;
|
|
42
|
-
verifierPrompt(candidate: AdvisoryCandidate): string;
|
|
43
|
-
makeVerified(candidate: AdvisoryCandidate, lens: Lens, verdict: AdvisoryVerdict): Verified;
|
|
56
|
+
finderPhase?: "Find" | "Hypothesize";
|
|
57
|
+
finderPrompt(lens: AdvisoryLens): string;
|
|
58
|
+
verifierPrompt(candidate: IdentifiedAdvisoryCandidate): string;
|
|
59
|
+
boundCandidate?(candidate: IdentifiedAdvisoryCandidate, lens: AdvisoryLens): IdentifiedAdvisoryCandidate | undefined;
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
export interface AdvisoryBackfillDefaults {
|
|
@@ -48,117 +64,77 @@ export interface AdvisoryBackfillDefaults {
|
|
|
48
64
|
recommendation?: string;
|
|
49
65
|
}
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
lens: Lens;
|
|
58
|
-
candidate: AdvisoryCandidate;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function runLensVerificationPipeline<Lens extends AdvisoryLens, Verified extends AdvisoryVerified>(
|
|
62
|
-
options: LensVerificationPipelineOptions<Lens, Verified>,
|
|
63
|
-
): Promise<LensVerificationPipelineResult<Verified>> {
|
|
64
|
-
const {
|
|
65
|
-
api,
|
|
66
|
-
lenses,
|
|
67
|
-
perLens,
|
|
68
|
-
tools = DEFAULT_ADVISORY_TOOLS,
|
|
69
|
-
toolHints = DEFAULT_ADVISORY_TOOL_HINTS,
|
|
70
|
-
finderPhase = "Find",
|
|
71
|
-
verifierPhase = "Verify",
|
|
72
|
-
schedulingMode = "pipeline",
|
|
73
|
-
finderPrompt,
|
|
74
|
-
verifierPrompt,
|
|
75
|
-
makeVerified,
|
|
76
|
-
} = options;
|
|
77
|
-
const seen = new Set<string>();
|
|
67
|
+
/** Finish all discovery before verification competes for the shared agent limit. */
|
|
68
|
+
export async function runLensVerificationPipeline(
|
|
69
|
+
options: LensVerificationPipelineOptions,
|
|
70
|
+
): Promise<LensVerificationPipelineResult> {
|
|
71
|
+
const { api, lenses, perLens, finderPhase = "Find", finderPrompt, verifierPrompt, boundCandidate } = options;
|
|
72
|
+
const coverage: AdvisoryStageCoverage[] = [];
|
|
78
73
|
let rawCandidates = 0;
|
|
79
|
-
let dropped = 0;
|
|
80
74
|
let refuted = 0;
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
phase: finderPhase,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
toolHints,
|
|
88
|
-
thinkingLevel: "low",
|
|
89
|
-
schema: AdvisoryCandidatesSchema,
|
|
75
|
+
api.phase(finderPhase);
|
|
76
|
+
const found = await collectAdvisoryStage(api, "Find", lenses.map((lens) => ({ id: lens.label, run: async () => {
|
|
77
|
+
const result = await api.agent(finderPrompt(lens), {
|
|
78
|
+
phase: finderPhase, label: `${finderPhase.toLowerCase()}:${lens.label}`,
|
|
79
|
+
tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
|
|
80
|
+
profile: "small", schema: AdvisoryCandidatesSchema,
|
|
90
81
|
});
|
|
91
|
-
const
|
|
92
|
-
rawCandidates +=
|
|
93
|
-
api.progress({ type: "counter_delta", key: "candidates", label: "candidates", delta:
|
|
82
|
+
const raw = identifyCandidates(result.candidates.slice(0, perLens), lens.label);
|
|
83
|
+
rawCandidates += raw.length;
|
|
84
|
+
api.progress({ type: "counter_delta", key: "candidates", label: "candidates", delta: raw.length });
|
|
85
|
+
const candidates = boundCandidate ? raw.flatMap((candidate) => {
|
|
86
|
+
const bounded = boundCandidate(candidate, lens);
|
|
87
|
+
return bounded ? [bounded] : [];
|
|
88
|
+
}) : raw;
|
|
94
89
|
for (const candidate of candidates) {
|
|
95
|
-
api.progress({
|
|
96
|
-
|
|
97
|
-
lane: "Candidates",
|
|
98
|
-
title: candidate.summary,
|
|
99
|
-
subtitle: formatLocation(candidate),
|
|
100
|
-
status: "pending",
|
|
101
|
-
details: candidate.impact,
|
|
102
|
-
});
|
|
90
|
+
api.progress({ type: "lane_item", lane: finderPhase === "Hypothesize" ? "Hypotheses" : "Candidates",
|
|
91
|
+
title: candidate.summary, subtitle: formatLocation(candidate), status: "pending", details: candidate.impact });
|
|
103
92
|
}
|
|
104
|
-
return
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
dropped += droppedForLens;
|
|
112
|
-
api.progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: droppedForLens });
|
|
113
|
-
api.log(`find:${found.lens.label}: dropped ${droppedForLens} duplicate candidate(s)`);
|
|
114
|
-
}
|
|
115
|
-
return novel;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const verifyCandidate = async ({ lens, candidate }: NovelCandidate<Lens>): Promise<Verified | null> => {
|
|
119
|
-
const location = primaryLocation(candidate);
|
|
120
|
-
const judged = await api.agent(verifierPrompt(candidate), {
|
|
121
|
-
phase: verifierPhase,
|
|
122
|
-
label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
123
|
-
tools,
|
|
124
|
-
toolHints,
|
|
125
|
-
thinkingLevel: "low",
|
|
126
|
-
schema: AdvisoryVerdictSchema,
|
|
127
|
-
});
|
|
128
|
-
if (!judged) return null;
|
|
129
|
-
recordVerdictProgress(api.progress, candidate, judged, () => {
|
|
130
|
-
refuted += 1;
|
|
131
|
-
});
|
|
132
|
-
return makeVerified(candidate, lens, judged);
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
if (schedulingMode === "finder-barrier") {
|
|
136
|
-
const found = await api.parallel(lenses.map((lens) => async () => findForLens(lens)));
|
|
137
|
-
const novel = compactResults(found).flatMap(dedupeFound);
|
|
138
|
-
const verdicts = await api.parallel(novel.map((entry) => async () => verifyCandidate(entry)));
|
|
139
|
-
return { verified: compactResults(verdicts), rawCandidates, dropped, refuted };
|
|
93
|
+
return candidates;
|
|
94
|
+
} })), coverage);
|
|
95
|
+
const candidates = dedupeCandidates(found.flat());
|
|
96
|
+
const dropped = rawCandidates - candidates.length;
|
|
97
|
+
if (dropped > 0) {
|
|
98
|
+
api.progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
|
|
99
|
+
api.log(`Dropped ${dropped} duplicate or out-of-scope candidate(s)`);
|
|
140
100
|
}
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
101
|
+
api.phase("Verify");
|
|
102
|
+
const verified = await collectAdvisoryStage(api, "Verify", candidates.map((candidate) => ({
|
|
103
|
+
id: candidate.candidateId, candidate, run: async (): Promise<AdvisoryVerified> => {
|
|
104
|
+
const location = primaryLocation(candidate);
|
|
105
|
+
const judged = await api.agent(`${verifierPrompt(candidate)}\nCandidate record: ${JSON.stringify(candidate)}`, {
|
|
106
|
+
phase: "Verify", label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
107
|
+
tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
|
|
108
|
+
profile: "small", schema: AdvisoryVerdictSchema,
|
|
109
|
+
});
|
|
110
|
+
recordVerdictProgress(api.progress, candidate, judged, () => { refuted += 1; });
|
|
111
|
+
return { ...candidate, verdict: judged.verdict, evidence: judged.evidence, confidence: judged.confidence };
|
|
149
112
|
},
|
|
150
|
-
);
|
|
113
|
+
})), coverage);
|
|
114
|
+
return { verified, rawCandidates, dropped, refuted, coverage };
|
|
115
|
+
}
|
|
151
116
|
|
|
152
|
-
|
|
117
|
+
export async function synthesizeAdvisoryReport(
|
|
118
|
+
api: Pick<WorkflowApi, "agent" | "parallel" | "phase">,
|
|
119
|
+
prompt: string,
|
|
120
|
+
ranked: readonly AdvisoryVerified[],
|
|
121
|
+
coverage: AdvisoryStageCoverage[],
|
|
122
|
+
): Promise<AdvisoryReport> {
|
|
123
|
+
api.phase("Synthesize");
|
|
124
|
+
const [report] = await collectAdvisoryStage(api, "Synthesize", [{ id: "synthesize", run: () => api.agent(
|
|
125
|
+
SYNTHESIS_ID_INSTRUCTIONS + prompt,
|
|
126
|
+
{ phase: "Synthesize", label: "synthesize", tools: [], profile: "medium", resume: "read-only", schema: AdvisorySynthesisSchema },
|
|
127
|
+
) }], coverage);
|
|
128
|
+
return resolveAdvisorySynthesis(report, ranked, {
|
|
129
|
+
impact: "Impact not restated by verification.",
|
|
130
|
+
recommendation: "Inspect the cited evidence and validate the smallest repair.",
|
|
131
|
+
}, coverage);
|
|
153
132
|
}
|
|
154
133
|
|
|
155
|
-
function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
seen.add(key);
|
|
160
|
-
return true;
|
|
161
|
-
});
|
|
134
|
+
export function finishAdvisoryReport<T extends AdvisoryReport>(
|
|
135
|
+
report: T, coverage: AdvisoryStageCoverage[], verification: AdvisoryVerified[] = [],
|
|
136
|
+
) {
|
|
137
|
+
return { ...withAdvisoryCoverage(report, coverage), verification };
|
|
162
138
|
}
|
|
163
139
|
|
|
164
140
|
export function primaryLocation(candidate: Pick<AdvisoryCandidate, "locations">): AdvisoryLocation {
|
|
@@ -180,52 +156,17 @@ export function normalizePath(path: string): string {
|
|
|
180
156
|
return path.replace(/^\.\//, "").replace(/^[ab]\//, "");
|
|
181
157
|
}
|
|
182
158
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
case "CONFIRMED":
|
|
192
|
-
return "Confirmed";
|
|
193
|
-
case "PLAUSIBLE":
|
|
194
|
-
return "Plausible";
|
|
195
|
-
case "REFUTED":
|
|
196
|
-
return "Refuted";
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function verdictStatus(verdict: AdvisoryVerdict["verdict"]): "success" | "warning" | "error" {
|
|
201
|
-
switch (verdict) {
|
|
202
|
-
case "CONFIRMED":
|
|
203
|
-
return "success";
|
|
204
|
-
case "PLAUSIBLE":
|
|
205
|
-
return "warning";
|
|
206
|
-
case "REFUTED":
|
|
207
|
-
return "error";
|
|
208
|
-
}
|
|
209
|
-
}
|
|
159
|
+
const VERDICT_PRESENTATION = {
|
|
160
|
+
CONFIRMED: { lane: "Confirmed", status: "success", confidence: "high" },
|
|
161
|
+
PLAUSIBLE: { lane: "Plausible", status: "warning", confidence: "medium" },
|
|
162
|
+
NOT_SUBSTANTIATED: { lane: "Unresolved", status: "warning", confidence: "medium" },
|
|
163
|
+
REFUTED: { lane: "Refuted", status: "error", confidence: "low" },
|
|
164
|
+
} satisfies Record<AdvisoryVerdict["verdict"], {
|
|
165
|
+
lane: string; status: "success" | "warning" | "error"; confidence: NonNullable<AdvisoryVerdict["confidence"]>;
|
|
166
|
+
}>;
|
|
210
167
|
|
|
211
168
|
export function verdictConfidence(verdict: AdvisoryVerdict["verdict"]): "high" | "medium" | "low" {
|
|
212
|
-
|
|
213
|
-
case "CONFIRMED":
|
|
214
|
-
return "high";
|
|
215
|
-
case "PLAUSIBLE":
|
|
216
|
-
return "medium";
|
|
217
|
-
case "REFUTED":
|
|
218
|
-
return "low";
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function sameFinding(candidate: Pick<AdvisoryCandidate, "locations">, finding: Pick<AdvisoryFinding, "locations" | "summary">): boolean {
|
|
223
|
-
return findingLocationKey(candidate) === findingLocationKey(finding);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export function findingLocationKey(value: Pick<AdvisoryCandidate, "locations"> | Pick<AdvisoryFinding, "locations">): string {
|
|
227
|
-
const location = primaryLocation(value);
|
|
228
|
-
return `${normalizePath(location.file)}:${location.line ?? "file"}`;
|
|
169
|
+
return VERDICT_PRESENTATION[verdict].confidence;
|
|
229
170
|
}
|
|
230
171
|
|
|
231
172
|
export function recordVerdictProgress(
|
|
@@ -241,32 +182,64 @@ export function recordVerdictProgress(
|
|
|
241
182
|
}
|
|
242
183
|
progress({
|
|
243
184
|
type: "lane_item",
|
|
244
|
-
lane:
|
|
185
|
+
lane: VERDICT_PRESENTATION[verdict.verdict].lane,
|
|
245
186
|
title: candidate.summary,
|
|
246
187
|
subtitle: formatLocation(candidate),
|
|
247
|
-
status:
|
|
188
|
+
status: VERDICT_PRESENTATION[verdict.verdict].status,
|
|
248
189
|
details: formatEvidence(verdict.evidence),
|
|
249
190
|
});
|
|
250
191
|
}
|
|
251
192
|
|
|
252
193
|
export function backfillAdvisoryFindings<Source extends AdvisoryVerified>(
|
|
253
|
-
findings:
|
|
194
|
+
findings: AdvisorySynthesis["findings"],
|
|
254
195
|
ranked: readonly Source[],
|
|
255
196
|
defaults: AdvisoryBackfillDefaults,
|
|
256
197
|
): AdvisoryReport["findings"] {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
198
|
+
const sources = new Map(ranked.flatMap((source) => source.sourceCandidateIds.map((id) => [id, source] as const)));
|
|
199
|
+
const used = new Set<string>();
|
|
200
|
+
return findings.flatMap((selection) => {
|
|
201
|
+
const ids = [...new Set(selection.sourceCandidateIds)];
|
|
202
|
+
if (ids.length === 0 || ids.some((id) => !sources.has(id) || used.has(id))) return [];
|
|
203
|
+
const records = [...new Set(ids.map((id) => sources.get(id)!))];
|
|
204
|
+
if (records.some((record) => record.verdict === "REFUTED")) return [];
|
|
205
|
+
const sourceCandidateIds = [...new Set(records.flatMap((record) => record.sourceCandidateIds))];
|
|
206
|
+
if (sourceCandidateIds.some((id) => used.has(id))) return [];
|
|
207
|
+
sourceCandidateIds.forEach((id) => used.add(id));
|
|
208
|
+
const first = records[0]!;
|
|
209
|
+
return [{
|
|
210
|
+
sourceCandidateIds,
|
|
211
|
+
summary: [...new Set(records.map((record) => record.summary))].join("; "),
|
|
212
|
+
category: first.category,
|
|
213
|
+
severity: selection.severity,
|
|
214
|
+
verdict: records.some((record) => record.verdict === "NOT_SUBSTANTIATED") ? "NOT_SUBSTANTIATED" as const : records.some((record) => record.verdict === "PLAUSIBLE") ? "PLAUSIBLE" as const : "CONFIRMED" as const,
|
|
215
|
+
confidence: records.every((record) => record.verdict === "CONFIRMED") ? "high" as const : "medium" as const,
|
|
216
|
+
locations: uniqueLocations(records.flatMap((record) => record.locations)),
|
|
217
|
+
...(first.reviewAnchor ? { reviewAnchor: first.reviewAnchor } : {}),
|
|
218
|
+
evidence: [...new Set(records.flatMap((record) => [...(record.discoveryEvidence ?? []), ...record.evidence]))],
|
|
219
|
+
impact: [...new Set(records.map((record) => record.impact || defaults.impact))].join("\n"),
|
|
220
|
+
recommendation: selection.recommendation || first.recommendation || defaults.recommendation || "",
|
|
221
|
+
}];
|
|
222
|
+
});
|
|
223
|
+
}
|
|
262
224
|
|
|
263
|
-
|
|
264
|
-
|
|
225
|
+
/** Retain verified records if synthesis fails, and expose invalid ID selections as a gap. */
|
|
226
|
+
export function resolveAdvisorySynthesis<Source extends AdvisoryVerified>(
|
|
227
|
+
report: AdvisorySynthesis | undefined,
|
|
228
|
+
ranked: readonly Source[],
|
|
229
|
+
defaults: AdvisoryBackfillDefaults,
|
|
230
|
+
coverage: AdvisoryStageCoverage[],
|
|
231
|
+
): AdvisoryReport {
|
|
232
|
+
if (!report) {
|
|
265
233
|
return {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
recommendation: finding.recommendation || source?.recommendation || defaults.recommendation || "",
|
|
234
|
+
summary: "Synthesis unavailable; verified records retained.",
|
|
235
|
+
findings: backfillAdvisoryFindings(ranked.map((finding) => ({ sourceCandidateIds: finding.sourceCandidateIds, severity: "medium", recommendation: finding.recommendation ?? defaults.recommendation ?? "Inspect verifier evidence." })), ranked, defaults),
|
|
236
|
+
nextSteps: ["Inspect verifier evidence or rerun synthesis."],
|
|
270
237
|
};
|
|
271
|
-
}
|
|
238
|
+
}
|
|
239
|
+
const findings = backfillAdvisoryFindings(report.findings, ranked, defaults);
|
|
240
|
+
if (findings.length !== report.findings.length) {
|
|
241
|
+
coverage.push({ stage: "Synthesis provenance", expected: report.findings.length, completed: findings.length,
|
|
242
|
+
failed: report.findings.length - findings.length, failures: [{ branch: "synthesize", reason: "Discarded invalid, repeated, or unverified candidate IDs." }] });
|
|
243
|
+
}
|
|
244
|
+
return { ...report, findings };
|
|
272
245
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { ResolvedWorkflowRunOptions } from "./options.ts";
|
|
3
3
|
import type { PerfAggregate, PerfSink, PerfSnapshot } from "./perf.ts";
|
|
4
|
-
import type { WorkflowProgressSnapshot } from "./progress.ts";
|
|
4
|
+
import type { WorkflowProgressSnapshot } from "./progress-types.ts";
|
|
5
5
|
import type {
|
|
6
6
|
LoadedWorkflow,
|
|
7
7
|
WorkflowProgressSource,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { WorkflowBackgroundOrigin } from "./types.ts";
|
|
2
|
+
import type { WorkflowRunRecord } from "./workflow-run-record.ts";
|
|
3
|
+
|
|
4
|
+
const MAX_BACKGROUND_TEXT = 2_048;
|
|
5
|
+
|
|
6
|
+
export type WorkflowRunDelivery =
|
|
7
|
+
| { readonly state: "pending" }
|
|
8
|
+
| { readonly state: "delivered"; readonly deliveredAt: number }
|
|
9
|
+
| { readonly state: "unavailable"; readonly attemptedAt: number; readonly message: string };
|
|
10
|
+
|
|
11
|
+
export interface PersistedWorkflowBackground {
|
|
12
|
+
readonly origin: WorkflowBackgroundOrigin;
|
|
13
|
+
readonly delivery: WorkflowRunDelivery;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createPersistedWorkflowBackground(
|
|
17
|
+
origin: WorkflowBackgroundOrigin | undefined,
|
|
18
|
+
): PersistedWorkflowBackground | undefined {
|
|
19
|
+
if (!origin) return undefined;
|
|
20
|
+
return {
|
|
21
|
+
origin: {
|
|
22
|
+
sessionId: boundedText(origin.sessionId),
|
|
23
|
+
requestedAt: origin.requestedAt,
|
|
24
|
+
},
|
|
25
|
+
delivery: { state: "pending" },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function updateWorkflowRunDelivery(
|
|
30
|
+
record: WorkflowRunRecord,
|
|
31
|
+
delivery: WorkflowRunDelivery,
|
|
32
|
+
at = Date.now(),
|
|
33
|
+
): WorkflowRunRecord {
|
|
34
|
+
if (!record.background) throw new Error(`Workflow run ${record.runId} is not a background run.`);
|
|
35
|
+
return {
|
|
36
|
+
...record,
|
|
37
|
+
updatedAt: at,
|
|
38
|
+
background: {
|
|
39
|
+
origin: record.background.origin,
|
|
40
|
+
delivery: delivery.state === "unavailable"
|
|
41
|
+
? { ...delivery, message: boundedText(delivery.message) }
|
|
42
|
+
: delivery,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isPersistedWorkflowBackground(value: unknown): value is PersistedWorkflowBackground {
|
|
48
|
+
if (!isRecord(value) || !isRecord(value.origin) || !isRecord(value.delivery)) return false;
|
|
49
|
+
if (typeof value.origin.sessionId !== "string" || !isFiniteNumber(value.origin.requestedAt)) return false;
|
|
50
|
+
switch (value.delivery.state) {
|
|
51
|
+
case "pending":
|
|
52
|
+
return true;
|
|
53
|
+
case "delivered":
|
|
54
|
+
return isFiniteNumber(value.delivery.deliveredAt);
|
|
55
|
+
case "unavailable":
|
|
56
|
+
return isFiniteNumber(value.delivery.attemptedAt) && typeof value.delivery.message === "string";
|
|
57
|
+
default:
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function boundedText(value: string): string {
|
|
63
|
+
return value.length <= MAX_BACKGROUND_TEXT ? value : `${value.slice(0, MAX_BACKGROUND_TEXT - 1)}…`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
67
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
71
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
72
|
+
}
|