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
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
import { AdvisoryCandidatesSchema, AdvisoryVerdictSchema, type AdvisoryCandidate, type AdvisoryFinding, type AdvisoryLocation, type AdvisoryReport, type AdvisoryVerdict } from "./advisory-schema.ts";
|
|
2
|
-
import {
|
|
1
|
+
import { AdvisoryCandidatesSchema, AdvisoryVerdictSchema, formatAdvisoryLocation, type AdvisoryCandidate, type AdvisoryFinding, 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
3
|
import type { AgentOptions, WorkflowApi, WorkflowProgressEvent, WorkflowRunStats } from "./types.ts";
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
export interface AdvisoryLens {
|
|
6
7
|
label: string;
|
|
7
8
|
category: string;
|
|
8
9
|
text: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export type AdvisoryVerified
|
|
12
|
+
export type AdvisoryVerified = IdentifiedAdvisoryCandidate & {
|
|
12
13
|
verdict: AdvisoryVerdict["verdict"];
|
|
13
14
|
evidence: string[];
|
|
14
|
-
|
|
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
|
+
coverage: AdvisoryStageCoverage[];
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/** Default concrete read/inspect tools for advisory workflows. */
|
|
@@ -47,20 +48,14 @@ export function publishVerifiedKeptProgress(
|
|
|
47
48
|
api.log(`${verified} verified → ${kept} kept`);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
export
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
api: Pick<WorkflowApi, "agent" | "parallel" | "pipeline" | "progress" | "log">;
|
|
54
|
-
lenses: readonly Lens[];
|
|
51
|
+
export interface LensVerificationPipelineOptions {
|
|
52
|
+
api: Pick<WorkflowApi, "agent" | "parallel" | "phase" | "progress" | "log">;
|
|
53
|
+
lenses: readonly AdvisoryLens[];
|
|
55
54
|
perLens: number;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
schedulingMode?: AdvisorySchedulingMode;
|
|
61
|
-
finderPrompt(lens: Lens): string;
|
|
62
|
-
verifierPrompt(candidate: AdvisoryCandidate): string;
|
|
63
|
-
makeVerified(candidate: AdvisoryCandidate, lens: Lens, verdict: AdvisoryVerdict): Verified;
|
|
55
|
+
finderPhase?: "Find" | "Hypothesize";
|
|
56
|
+
finderPrompt(lens: AdvisoryLens): string;
|
|
57
|
+
verifierPrompt(candidate: IdentifiedAdvisoryCandidate): string;
|
|
58
|
+
boundCandidate?(candidate: IdentifiedAdvisoryCandidate, lens: AdvisoryLens): IdentifiedAdvisoryCandidate | undefined;
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
export interface AdvisoryBackfillDefaults {
|
|
@@ -68,117 +63,124 @@ export interface AdvisoryBackfillDefaults {
|
|
|
68
63
|
recommendation?: string;
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
lens: Lens;
|
|
78
|
-
candidate: AdvisoryCandidate;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function runLensVerificationPipeline<Lens extends AdvisoryLens, Verified extends AdvisoryVerified>(
|
|
82
|
-
options: LensVerificationPipelineOptions<Lens, Verified>,
|
|
83
|
-
): Promise<LensVerificationPipelineResult<Verified>> {
|
|
84
|
-
const {
|
|
85
|
-
api,
|
|
86
|
-
lenses,
|
|
87
|
-
perLens,
|
|
88
|
-
tools = DEFAULT_ADVISORY_TOOLS,
|
|
89
|
-
toolHints = DEFAULT_ADVISORY_TOOL_HINTS,
|
|
90
|
-
finderPhase = "Find",
|
|
91
|
-
verifierPhase = "Verify",
|
|
92
|
-
schedulingMode = "pipeline",
|
|
93
|
-
finderPrompt,
|
|
94
|
-
verifierPrompt,
|
|
95
|
-
makeVerified,
|
|
96
|
-
} = options;
|
|
97
|
-
const seen = new Set<string>();
|
|
66
|
+
/** Finish all discovery before verification competes for the shared agent limit. */
|
|
67
|
+
export async function runLensVerificationPipeline(
|
|
68
|
+
options: LensVerificationPipelineOptions,
|
|
69
|
+
): Promise<LensVerificationPipelineResult> {
|
|
70
|
+
const { api, lenses, perLens, finderPhase = "Find", finderPrompt, verifierPrompt, boundCandidate } = options;
|
|
71
|
+
const coverage: AdvisoryStageCoverage[] = [];
|
|
98
72
|
let rawCandidates = 0;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
label: `find:${lens.label}`,
|
|
106
|
-
tools,
|
|
107
|
-
toolHints,
|
|
108
|
-
profile: "small",
|
|
109
|
-
schema: AdvisoryCandidatesSchema,
|
|
73
|
+
api.phase(finderPhase);
|
|
74
|
+
const found = await collectAdvisoryStage(api, "Find", lenses.map((lens) => ({ id: lens.label, run: async () => {
|
|
75
|
+
const result = await api.agent(finderPrompt(lens), {
|
|
76
|
+
phase: finderPhase, label: `${finderPhase.toLowerCase()}:${lens.label}`,
|
|
77
|
+
tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
|
|
78
|
+
profile: "small", schema: AdvisoryCandidatesSchema,
|
|
110
79
|
});
|
|
111
|
-
const
|
|
112
|
-
rawCandidates +=
|
|
113
|
-
api.progress({ type: "counter_delta", key: "candidates", label: "candidates", delta:
|
|
80
|
+
const raw = identifyCandidates(result.candidates.slice(0, perLens), lens.label);
|
|
81
|
+
rawCandidates += raw.length;
|
|
82
|
+
api.progress({ type: "counter_delta", key: "candidates", label: "candidates", delta: raw.length });
|
|
83
|
+
const candidates = boundCandidate ? raw.flatMap((candidate) => {
|
|
84
|
+
const bounded = boundCandidate(candidate, lens);
|
|
85
|
+
return bounded ? [bounded] : [];
|
|
86
|
+
}) : raw;
|
|
114
87
|
for (const candidate of candidates) {
|
|
115
|
-
api.progress({
|
|
116
|
-
|
|
117
|
-
lane: "Candidates",
|
|
118
|
-
title: candidate.summary,
|
|
119
|
-
subtitle: formatLocation(candidate),
|
|
120
|
-
status: "pending",
|
|
121
|
-
details: candidate.impact,
|
|
122
|
-
});
|
|
88
|
+
api.progress({ type: "lane_item", lane: finderPhase === "Hypothesize" ? "Hypotheses" : "Candidates",
|
|
89
|
+
title: candidate.summary, subtitle: formatLocation(candidate), status: "pending", details: candidate.impact });
|
|
123
90
|
}
|
|
124
|
-
return
|
|
125
|
-
};
|
|
91
|
+
return candidates;
|
|
92
|
+
} })), coverage);
|
|
93
|
+
const candidates = dedupeCandidates(found.flat());
|
|
94
|
+
const dropped = rawCandidates - candidates.length;
|
|
95
|
+
if (dropped > 0) {
|
|
96
|
+
api.progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
|
|
97
|
+
api.log(`Dropped ${dropped} duplicate or out-of-scope candidate(s)`);
|
|
98
|
+
}
|
|
99
|
+
api.phase("Verify");
|
|
100
|
+
const verified = await collectAdvisoryStage(api, "Verify", candidates.map((candidate) => ({
|
|
101
|
+
id: candidate.candidateId, candidate, run: async (): Promise<AdvisoryVerified> => {
|
|
102
|
+
const location = primaryLocation(candidate);
|
|
103
|
+
const judged = await api.agent(`${verifierPrompt(candidate)}\nCandidate record: ${JSON.stringify(candidate)}`, {
|
|
104
|
+
phase: "Verify", label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
105
|
+
tools: DEFAULT_ADVISORY_TOOLS, toolHints: DEFAULT_ADVISORY_TOOL_HINTS,
|
|
106
|
+
profile: "small", schema: AdvisoryVerdictSchema,
|
|
107
|
+
});
|
|
108
|
+
recordVerdictProgress(api.progress, candidate, judged);
|
|
109
|
+
return { ...candidate, verdict: judged.verdict, evidence: judged.evidence };
|
|
110
|
+
},
|
|
111
|
+
})), coverage);
|
|
112
|
+
return { verified, rawCandidates, dropped, coverage };
|
|
113
|
+
}
|
|
126
114
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
115
|
+
/** Stats for a lens review that stopped before discovery. */
|
|
116
|
+
export const EMPTY_LENS_REVIEW_STATS: Readonly<WorkflowRunStats> = Object.freeze({ files: 0, candidates: 0, verified: 0, kept: 0, dropped: 0, refuted: 0 });
|
|
117
|
+
|
|
118
|
+
export interface LensReviewConclusion {
|
|
119
|
+
/** Scoped file count reported in stats. */
|
|
120
|
+
files: number;
|
|
121
|
+
rank(finding: AdvisoryVerified): number;
|
|
122
|
+
/** One ranked finding's synthesis-prompt block, headed by the IDs synthesis selects; defaults to {@link formatRankedFinding}. */
|
|
123
|
+
formatFinding?(finding: AdvisoryVerified, index: number): string;
|
|
124
|
+
/** Report used when no finding survives verification and challenge. */
|
|
125
|
+
empty: { summary: string; nextSteps: string[] };
|
|
126
|
+
synthesisPrompt(block: string, ranked: readonly AdvisoryVerified[], refuted: readonly AdvisoryVerified[]): string;
|
|
127
|
+
}
|
|
137
128
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
label: `verify:${location.file.split("/").pop() ?? location.file}`,
|
|
143
|
-
tools,
|
|
144
|
-
toolHints,
|
|
145
|
-
profile: "small",
|
|
146
|
-
schema: AdvisoryVerdictSchema,
|
|
147
|
-
});
|
|
148
|
-
if (!judged) return null;
|
|
149
|
-
recordVerdictProgress(api.progress, candidate, judged, () => {
|
|
150
|
-
refuted += 1;
|
|
151
|
-
});
|
|
152
|
-
return makeVerified(candidate, lens, judged);
|
|
153
|
-
};
|
|
129
|
+
export function formatRankedFinding(finding: AdvisoryVerified, index: number, recommendationLabel = "Recommendation"): string {
|
|
130
|
+
return `### [${index}] IDs: ${finding.sourceCandidateIds.join(", ")} ${formatLocation(finding)} (${finding.verdict}, ${finding.category})\n` +
|
|
131
|
+
`${finding.summary}\nImpact: ${finding.impact}\nEvidence: ${formatEvidence(finding.evidence)}\n${recommendationLabel}: ${finding.recommendation ?? "(none supplied)"}`;
|
|
132
|
+
}
|
|
154
133
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
134
|
+
/** Rank and synthesize the findings that survived verification and challenge into the final report. */
|
|
135
|
+
export async function concludeLensReview(
|
|
136
|
+
api: Pick<WorkflowApi, "agent" | "parallel" | "phase" | "progress" | "log">,
|
|
137
|
+
pipeline: LensVerificationPipelineResult,
|
|
138
|
+
verified: AdvisoryVerified[],
|
|
139
|
+
conclusion: LensReviewConclusion,
|
|
140
|
+
) {
|
|
141
|
+
const { coverage } = pipeline;
|
|
142
|
+
const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
|
|
143
|
+
const stats: WorkflowRunStats = {
|
|
144
|
+
files: conclusion.files,
|
|
145
|
+
candidates: pipeline.rawCandidates,
|
|
146
|
+
verified: verified.length,
|
|
147
|
+
kept: surviving.length,
|
|
148
|
+
dropped: pipeline.dropped,
|
|
149
|
+
refuted: verified.length - surviving.length,
|
|
150
|
+
};
|
|
151
|
+
publishVerifiedKeptProgress(api, verified.length, surviving.length);
|
|
152
|
+
if (surviving.length === 0) {
|
|
153
|
+
return finishAdvisoryReport(emptyAdvisoryReport(conclusion.empty.summary, conclusion.empty.nextSteps, stats), coverage, verified);
|
|
160
154
|
}
|
|
161
155
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return compactResults(verdicts);
|
|
169
|
-
},
|
|
170
|
-
);
|
|
156
|
+
const ranked = [...surviving].sort((a, b) => conclusion.rank(a) - conclusion.rank(b));
|
|
157
|
+
const block = ranked.map((finding, index) => (conclusion.formatFinding ?? formatRankedFinding)(finding, index)).join("\n\n");
|
|
158
|
+
const refuted = verified.filter((finding) => finding.verdict === "REFUTED");
|
|
159
|
+
const resolved = await synthesizeAdvisoryReport(api, conclusion.synthesisPrompt(block, ranked, refuted), ranked, coverage);
|
|
160
|
+
return finishAdvisoryReport({ ...resolved, stats: { ...stats, kept: resolved.findings.length } }, coverage, verified);
|
|
161
|
+
}
|
|
171
162
|
|
|
172
|
-
|
|
163
|
+
export async function synthesizeAdvisoryReport(
|
|
164
|
+
api: Pick<WorkflowApi, "agent" | "parallel" | "phase">,
|
|
165
|
+
prompt: string,
|
|
166
|
+
ranked: readonly AdvisoryVerified[],
|
|
167
|
+
coverage: AdvisoryStageCoverage[],
|
|
168
|
+
): Promise<AdvisoryReport> {
|
|
169
|
+
api.phase("Synthesize");
|
|
170
|
+
const [report] = await collectAdvisoryStage(api, "Synthesize", [{ id: "synthesize", run: () => api.agent(
|
|
171
|
+
SYNTHESIS_ID_INSTRUCTIONS + prompt,
|
|
172
|
+
{ phase: "Synthesize", label: "synthesize", tools: [], skills: [], profile: "medium", resume: "read-only", schema: AdvisorySynthesisSchema },
|
|
173
|
+
) }], coverage);
|
|
174
|
+
return resolveAdvisorySynthesis(report, ranked, {
|
|
175
|
+
impact: "Impact not restated by verification.",
|
|
176
|
+
recommendation: "Inspect the cited evidence and validate the smallest repair.",
|
|
177
|
+
}, coverage);
|
|
173
178
|
}
|
|
174
179
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
seen.add(key);
|
|
180
|
-
return true;
|
|
181
|
-
});
|
|
180
|
+
export function finishAdvisoryReport<T extends AdvisoryReport>(
|
|
181
|
+
report: T, coverage: AdvisoryStageCoverage[], verification: AdvisoryVerified[] = [],
|
|
182
|
+
) {
|
|
183
|
+
return { ...withAdvisoryCoverage(report, coverage), verification };
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
export function primaryLocation(candidate: Pick<AdvisoryCandidate, "locations">): AdvisoryLocation {
|
|
@@ -186,107 +188,95 @@ export function primaryLocation(candidate: Pick<AdvisoryCandidate, "locations">)
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
export function formatLocation(candidate: Pick<AdvisoryCandidate, "locations">): string {
|
|
189
|
-
|
|
190
|
-
const line = location.line != null ? `:${location.line}` : "";
|
|
191
|
-
const symbol = location.symbol ? ` (${location.symbol})` : "";
|
|
192
|
-
return `${location.file}${line}${symbol}`;
|
|
191
|
+
return formatAdvisoryLocation(primaryLocation(candidate));
|
|
193
192
|
}
|
|
194
193
|
|
|
195
194
|
export function formatEvidence(evidence: readonly string[]): string {
|
|
196
195
|
return evidence.join("; ");
|
|
197
196
|
}
|
|
198
197
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function verdictLane(verdict: AdvisoryVerdict["verdict"]): string {
|
|
210
|
-
switch (verdict) {
|
|
211
|
-
case "CONFIRMED":
|
|
212
|
-
return "Confirmed";
|
|
213
|
-
case "PLAUSIBLE":
|
|
214
|
-
return "Plausible";
|
|
215
|
-
case "REFUTED":
|
|
216
|
-
return "Refuted";
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function verdictStatus(verdict: AdvisoryVerdict["verdict"]): "success" | "warning" | "error" {
|
|
221
|
-
switch (verdict) {
|
|
222
|
-
case "CONFIRMED":
|
|
223
|
-
return "success";
|
|
224
|
-
case "PLAUSIBLE":
|
|
225
|
-
return "warning";
|
|
226
|
-
case "REFUTED":
|
|
227
|
-
return "error";
|
|
228
|
-
}
|
|
229
|
-
}
|
|
198
|
+
const VERDICT_PRESENTATION = {
|
|
199
|
+
CONFIRMED: { lane: "Confirmed", status: "success", confidence: "high" },
|
|
200
|
+
PLAUSIBLE: { lane: "Plausible", status: "warning", confidence: "medium" },
|
|
201
|
+
NOT_SUBSTANTIATED: { lane: "Unresolved", status: "warning", confidence: "medium" },
|
|
202
|
+
REFUTED: { lane: "Refuted", status: "error", confidence: "low" },
|
|
203
|
+
} satisfies Record<AdvisoryVerdict["verdict"], {
|
|
204
|
+
lane: string; status: "success" | "warning" | "error"; confidence: AdvisoryFinding["confidence"];
|
|
205
|
+
}>;
|
|
230
206
|
|
|
231
207
|
export function verdictConfidence(verdict: AdvisoryVerdict["verdict"]): "high" | "medium" | "low" {
|
|
232
|
-
|
|
233
|
-
case "CONFIRMED":
|
|
234
|
-
return "high";
|
|
235
|
-
case "PLAUSIBLE":
|
|
236
|
-
return "medium";
|
|
237
|
-
case "REFUTED":
|
|
238
|
-
return "low";
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function sameFinding(candidate: Pick<AdvisoryCandidate, "locations">, finding: Pick<AdvisoryFinding, "locations" | "summary">): boolean {
|
|
243
|
-
return findingLocationKey(candidate) === findingLocationKey(finding);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export function findingLocationKey(value: Pick<AdvisoryCandidate, "locations"> | Pick<AdvisoryFinding, "locations">): string {
|
|
247
|
-
const location = primaryLocation(value);
|
|
248
|
-
return `${normalizePath(location.file)}:${location.line ?? "file"}`;
|
|
208
|
+
return VERDICT_PRESENTATION[verdict].confidence;
|
|
249
209
|
}
|
|
250
210
|
|
|
251
211
|
export function recordVerdictProgress(
|
|
252
212
|
progress: (event: WorkflowProgressEvent) => void,
|
|
253
213
|
candidate: Pick<AdvisoryCandidate, "locations" | "summary">,
|
|
254
214
|
verdict: Pick<AdvisoryVerdict, "verdict" | "evidence">,
|
|
255
|
-
onRefuted?: () => void,
|
|
256
215
|
): void {
|
|
257
216
|
progress({ type: "counter_delta", key: `verdict.${verdict.verdict.toLowerCase()}`, label: verdict.verdict, delta: 1 });
|
|
258
217
|
if (verdict.verdict === "REFUTED") {
|
|
259
|
-
onRefuted?.();
|
|
260
218
|
progress({ type: "counter_delta", key: "refuted", label: "refuted", delta: 1 });
|
|
261
219
|
}
|
|
262
220
|
progress({
|
|
263
221
|
type: "lane_item",
|
|
264
|
-
lane:
|
|
222
|
+
lane: VERDICT_PRESENTATION[verdict.verdict].lane,
|
|
265
223
|
title: candidate.summary,
|
|
266
224
|
subtitle: formatLocation(candidate),
|
|
267
|
-
status:
|
|
225
|
+
status: VERDICT_PRESENTATION[verdict.verdict].status,
|
|
268
226
|
details: formatEvidence(verdict.evidence),
|
|
269
227
|
});
|
|
270
228
|
}
|
|
271
229
|
|
|
272
230
|
export function backfillAdvisoryFindings<Source extends AdvisoryVerified>(
|
|
273
|
-
findings:
|
|
231
|
+
findings: AdvisorySynthesis["findings"],
|
|
274
232
|
ranked: readonly Source[],
|
|
275
233
|
defaults: AdvisoryBackfillDefaults,
|
|
276
234
|
): AdvisoryReport["findings"] {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
235
|
+
const sources = new Map(ranked.flatMap((source) => source.sourceCandidateIds.map((id) => [id, source] as const)));
|
|
236
|
+
const used = new Set<string>();
|
|
237
|
+
return findings.flatMap((selection) => {
|
|
238
|
+
const ids = [...new Set(selection.sourceCandidateIds)];
|
|
239
|
+
if (ids.length === 0 || ids.some((id) => !sources.has(id) || used.has(id))) return [];
|
|
240
|
+
const records = [...new Set(ids.map((id) => sources.get(id)!))];
|
|
241
|
+
if (records.some((record) => record.verdict === "REFUTED")) return [];
|
|
242
|
+
const sourceCandidateIds = [...new Set(records.flatMap((record) => record.sourceCandidateIds))];
|
|
243
|
+
if (sourceCandidateIds.some((id) => used.has(id))) return [];
|
|
244
|
+
sourceCandidateIds.forEach((id) => used.add(id));
|
|
245
|
+
const first = records[0]!;
|
|
246
|
+
return [{
|
|
247
|
+
sourceCandidateIds,
|
|
248
|
+
summary: [...new Set(records.map((record) => record.summary))].join("; "),
|
|
249
|
+
category: first.category,
|
|
250
|
+
severity: selection.severity,
|
|
251
|
+
verdict: records.some((record) => record.verdict === "NOT_SUBSTANTIATED") ? "NOT_SUBSTANTIATED" as const : records.some((record) => record.verdict === "PLAUSIBLE") ? "PLAUSIBLE" as const : "CONFIRMED" as const,
|
|
252
|
+
confidence: records.every((record) => record.verdict === "CONFIRMED") ? "high" as const : "medium" as const,
|
|
253
|
+
locations: uniqueLocations(records.flatMap((record) => record.locations)),
|
|
254
|
+
...(first.reviewAnchor ? { reviewAnchor: first.reviewAnchor } : {}),
|
|
255
|
+
evidence: [...new Set(records.flatMap((record) => [...(record.discoveryEvidence ?? []), ...record.evidence]))],
|
|
256
|
+
impact: [...new Set(records.map((record) => record.impact || defaults.impact))].join("\n"),
|
|
257
|
+
recommendation: selection.recommendation || first.recommendation || defaults.recommendation || "",
|
|
258
|
+
}];
|
|
259
|
+
});
|
|
260
|
+
}
|
|
282
261
|
|
|
283
|
-
|
|
284
|
-
|
|
262
|
+
/** Retain verified records if synthesis fails, and expose invalid ID selections as a gap. */
|
|
263
|
+
export function resolveAdvisorySynthesis<Source extends AdvisoryVerified>(
|
|
264
|
+
report: AdvisorySynthesis | undefined,
|
|
265
|
+
ranked: readonly Source[],
|
|
266
|
+
defaults: AdvisoryBackfillDefaults,
|
|
267
|
+
coverage: AdvisoryStageCoverage[],
|
|
268
|
+
): AdvisoryReport {
|
|
269
|
+
if (!report) {
|
|
285
270
|
return {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
recommendation: finding.recommendation || source?.recommendation || defaults.recommendation || "",
|
|
271
|
+
summary: "Synthesis unavailable; verified records retained.",
|
|
272
|
+
findings: backfillAdvisoryFindings(ranked.map((finding) => ({ sourceCandidateIds: finding.sourceCandidateIds, severity: "medium", recommendation: finding.recommendation ?? defaults.recommendation ?? "Inspect verifier evidence." })), ranked, defaults),
|
|
273
|
+
nextSteps: ["Inspect verifier evidence or rerun synthesis."],
|
|
290
274
|
};
|
|
291
|
-
}
|
|
275
|
+
}
|
|
276
|
+
const findings = backfillAdvisoryFindings(report.findings, ranked, defaults);
|
|
277
|
+
if (findings.length !== report.findings.length) {
|
|
278
|
+
coverage.push({ stage: "Synthesis provenance", expected: report.findings.length, completed: findings.length,
|
|
279
|
+
failed: report.findings.length - findings.length, failures: [{ branch: "synthesize", reason: "Discarded invalid, repeated, or unverified candidate IDs." }] });
|
|
280
|
+
}
|
|
281
|
+
return { ...report, findings };
|
|
292
282
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { isRecord } from "./guards.ts";
|
|
2
3
|
import type { ResolvedWorkflowRunOptions } from "./options.ts";
|
|
3
4
|
import type { PerfAggregate, PerfSink, PerfSnapshot } from "./perf.ts";
|
|
4
5
|
import type { WorkflowProgressSnapshot } from "./progress-types.ts";
|
|
@@ -11,14 +12,14 @@ import type {
|
|
|
11
12
|
import type { WorkflowUsageSnapshot } from "./usage.ts";
|
|
12
13
|
|
|
13
14
|
export interface WorkflowPerfDetails {
|
|
14
|
-
readonly enabled: boolean;
|
|
15
|
-
readonly startedAt: number;
|
|
16
15
|
readonly aggregates: readonly PerfAggregate[];
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export interface WorkflowResultEnvelope {
|
|
20
19
|
readonly name: string;
|
|
21
20
|
readonly result: unknown;
|
|
21
|
+
/** Optional so envelopes persisted before it existed still render. */
|
|
22
|
+
readonly startedAt?: number;
|
|
22
23
|
readonly completedAt: number;
|
|
23
24
|
readonly usage?: WorkflowUsageSnapshot;
|
|
24
25
|
readonly perf?: WorkflowPerfDetails;
|
|
@@ -26,6 +27,12 @@ export interface WorkflowResultEnvelope {
|
|
|
26
27
|
readonly resumedFromRunId?: string;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/** A workflow result's own summary: the result itself when it is a string, else its string `summary` field. */
|
|
31
|
+
export function workflowResultSummary(result: unknown): string | undefined {
|
|
32
|
+
if (typeof result === "string") return result;
|
|
33
|
+
return isRecord(result) && typeof result.summary === "string" ? result.summary : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
export type ResolvedWorkflowRunner = (
|
|
30
37
|
ctx: ExtensionContext,
|
|
31
38
|
mod: LoadedWorkflow,
|
|
@@ -46,13 +53,8 @@ export interface WorkflowExecutionInput {
|
|
|
46
53
|
readonly onProgressSnapshot?: (snapshot: WorkflowProgressSnapshot) => void | Promise<void>;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
|
-
export interface WorkflowExecution {
|
|
50
|
-
readonly metadata?: WorkflowRunMetadata;
|
|
51
|
-
readonly envelope: WorkflowResultEnvelope;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
56
|
/** Shared command/tool execution path. The engine remains lazy-loaded by the caller. */
|
|
55
|
-
export async function executeWorkflowInvocation(input: WorkflowExecutionInput): Promise<
|
|
57
|
+
export async function executeWorkflowInvocation(input: WorkflowExecutionInput): Promise<WorkflowResultEnvelope> {
|
|
56
58
|
let perfSnapshot: PerfSnapshot | undefined;
|
|
57
59
|
let usageSnapshot: WorkflowUsageSnapshot | undefined;
|
|
58
60
|
let runMetadata: WorkflowRunMetadata | undefined;
|
|
@@ -86,27 +88,20 @@ export async function executeWorkflowInvocation(input: WorkflowExecutionInput):
|
|
|
86
88
|
);
|
|
87
89
|
},
|
|
88
90
|
};
|
|
91
|
+
const startedAt = Date.now();
|
|
89
92
|
const result = await input.runResolvedWorkflow(input.ctx, input.mod, input.args, runOptions);
|
|
90
|
-
const perf = compactPerfSnapshot(perfSnapshot);
|
|
91
93
|
return {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
resumedFromRunId: runMetadata?.resumedFromRunId,
|
|
101
|
-
},
|
|
94
|
+
name: input.name,
|
|
95
|
+
result,
|
|
96
|
+
startedAt,
|
|
97
|
+
completedAt: Date.now(),
|
|
98
|
+
usage: usageSnapshot,
|
|
99
|
+
perf: perfSnapshot?.enabled ? { aggregates: perfSnapshot.aggregates } : undefined,
|
|
100
|
+
runId: runMetadata?.runId,
|
|
101
|
+
resumedFromRunId: runMetadata?.resumedFromRunId,
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function compactPerfSnapshot(snapshot: PerfSnapshot | undefined): WorkflowPerfDetails | undefined {
|
|
106
|
-
if (!snapshot?.enabled) return undefined;
|
|
107
|
-
return { enabled: true, startedAt: snapshot.startedAt, aggregates: snapshot.aggregates };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
105
|
async function notifyLifecycleObservers(...observers: ReadonlyArray<() => void | Promise<void>>): Promise<void> {
|
|
111
106
|
const failures: unknown[] = [];
|
|
112
107
|
for (const observer of observers) {
|
|
@@ -5,35 +5,33 @@ import type {
|
|
|
5
5
|
WorkflowSourceIdentity,
|
|
6
6
|
} from "./types.ts";
|
|
7
7
|
import type { WorktreeBaseline } from "./worktree.ts";
|
|
8
|
+
import { isRecord } from "./guards.ts";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
10
|
+
/** Validate and normalize workflow metadata without executing workflow code. */
|
|
11
|
+
export function parseWorkflowMeta(meta: unknown): { meta: WorkflowMeta } | { reason: string } {
|
|
12
|
+
if (!isRecord(meta)) return { reason: "meta export must be an object" };
|
|
13
|
+
if (typeof meta.name !== "string") return { reason: "meta.name must be a string" };
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
readonly default?: unknown;
|
|
18
|
-
};
|
|
15
|
+
const description = meta.description;
|
|
16
|
+
if (description !== undefined && typeof description !== "string") return { reason: "meta.description must be a string when provided" };
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
const phases = meta.phases;
|
|
19
|
+
if (phases !== undefined && !isWorkflowPhases(phases)) return { reason: "meta.phases must be an array of { title: string }" };
|
|
20
|
+
|
|
21
|
+
const workflowMeta: WorkflowMeta = { name: meta.name, description: description ?? "" };
|
|
22
|
+
if (phases !== undefined) workflowMeta.phases = phases;
|
|
23
|
+
return { meta: workflowMeta };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function parseWorkflowModule(value: unknown): { module: WorkflowModule } | { reason: string } {
|
|
27
27
|
if (!isRecord(value)) return { reason: "module export is not an object" };
|
|
28
|
-
|
|
29
|
-
const meta = candidate.meta;
|
|
30
|
-
if (!isRecord(meta)) return { reason: "missing meta export" };
|
|
28
|
+
if (value.meta === undefined) return { reason: "missing meta export" };
|
|
31
29
|
|
|
32
|
-
const parsedMeta =
|
|
30
|
+
const parsedMeta = parseWorkflowMeta(value.meta);
|
|
33
31
|
if ("reason" in parsedMeta) return parsedMeta;
|
|
34
|
-
if (!isWorkflowRun(
|
|
32
|
+
if (!isWorkflowRun(value.default)) return { reason: "default export must be a function" };
|
|
35
33
|
|
|
36
|
-
return { module: { meta: parsedMeta.meta, default:
|
|
34
|
+
return { module: { meta: parsedMeta.meta, default: value.default } };
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
/** Attach engine-owned source provenance after an authored module has been validated. */
|
|
@@ -48,20 +46,6 @@ export function loadWorkflow(
|
|
|
48
46
|
: { ...loaded, isolatedWorktreeBaseline };
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
function parseWorkflowMetaObject(meta: WorkflowMetaCandidate): { meta: WorkflowMeta } | { reason: string } {
|
|
52
|
-
if (typeof meta.name !== "string") return { reason: "meta.name must be a string" };
|
|
53
|
-
|
|
54
|
-
const description = meta.description;
|
|
55
|
-
if (description !== undefined && typeof description !== "string") return { reason: "meta.description must be a string when provided" };
|
|
56
|
-
|
|
57
|
-
const phases = meta.phases;
|
|
58
|
-
if (phases !== undefined && !isWorkflowPhases(phases)) return { reason: "meta.phases must be an array of { title: string }" };
|
|
59
|
-
|
|
60
|
-
const workflowMeta: WorkflowMeta = { name: meta.name, description: description ?? "" };
|
|
61
|
-
if (phases !== undefined) workflowMeta.phases = phases;
|
|
62
|
-
return { meta: workflowMeta };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
49
|
function isWorkflowRun(value: unknown): value is WorkflowModule["default"] {
|
|
66
50
|
return typeof value === "function";
|
|
67
51
|
}
|
|
@@ -69,7 +53,3 @@ function isWorkflowRun(value: unknown): value is WorkflowModule["default"] {
|
|
|
69
53
|
function isWorkflowPhases(value: unknown): value is Array<{ title: string }> {
|
|
70
54
|
return Array.isArray(value) && value.every((phase) => isRecord(phase) && typeof phase.title === "string");
|
|
71
55
|
}
|
|
72
|
-
|
|
73
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
74
|
-
return typeof value === "object" && value !== null;
|
|
75
|
-
}
|