pi-workflow-engine 0.10.1 → 0.10.3
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.
|
@@ -12,6 +12,7 @@ import { handleReviewViewerAction } from "./src/review/review-actions.ts";
|
|
|
12
12
|
import { decideReviewResultsPresentation, extensionContextMode, maybeShowReviewResultsViewer } from "./src/review/review-results-flow.ts";
|
|
13
13
|
import {
|
|
14
14
|
formatWorkflowDetailLines,
|
|
15
|
+
isAdvisoryReport,
|
|
15
16
|
isWorkflowResult,
|
|
16
17
|
renderWorkflowResult,
|
|
17
18
|
type WorkflowPerfDetails,
|
|
@@ -37,7 +38,39 @@ function formatMessageContent(
|
|
|
37
38
|
metadata?: WorkflowRunMetadata,
|
|
38
39
|
): string {
|
|
39
40
|
const details = formatWorkflowDetailLines({ usage, perf, metadata });
|
|
40
|
-
return `## Workflow: ${name}\n\n${
|
|
41
|
+
return `## Workflow: ${name}\n\n${formatResultForContext(result)}${details.length > 0 ? `\n\n${details.join("\n")}` : ""}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatResultForContext(result: unknown): string {
|
|
45
|
+
if (!isAdvisoryReport(result)) return summarize(result);
|
|
46
|
+
|
|
47
|
+
const lines = [result.summary];
|
|
48
|
+
if (result.findings.length > 0) {
|
|
49
|
+
lines.push("", "Findings:");
|
|
50
|
+
result.findings.forEach((finding, index) => {
|
|
51
|
+
const id = `R${String(index + 1).padStart(3, "0")}`;
|
|
52
|
+
lines.push(
|
|
53
|
+
`\n### ${id}: ${finding.summary}`,
|
|
54
|
+
`- Severity: ${finding.severity}`,
|
|
55
|
+
`- Confidence: ${finding.confidence}`,
|
|
56
|
+
`- Category: ${finding.category}`,
|
|
57
|
+
`- Location: ${finding.locations.map(formatFindingLocation).join(", ")}`,
|
|
58
|
+
`- Impact: ${finding.impact}`,
|
|
59
|
+
`- Evidence: ${finding.evidence.length > 0 ? finding.evidence.join("; ") : "(none cited)"}`,
|
|
60
|
+
`- Recommendation: ${finding.recommendation}`,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (result.nextSteps.length > 0) {
|
|
65
|
+
lines.push("", "Next steps:", ...result.nextSteps.map((step) => `- ${step}`));
|
|
66
|
+
}
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatFindingLocation(location: { readonly file: string; readonly line?: number; readonly symbol?: string }): string {
|
|
71
|
+
const line = location.line === undefined ? "" : `:${location.line}`;
|
|
72
|
+
const symbol = location.symbol === undefined ? "" : ` (${location.symbol})`;
|
|
73
|
+
return `${location.file}${line}${symbol}`;
|
|
41
74
|
}
|
|
42
75
|
|
|
43
76
|
function workflowEnvelope(
|
|
@@ -696,9 +729,8 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
696
729
|
},
|
|
697
730
|
});
|
|
698
731
|
const perf = compactPerfSnapshot(perfSnapshot);
|
|
699
|
-
const detailLines = formatWorkflowDetailLines({ usage: usageSnapshot, perf, metadata: runMetadata });
|
|
700
732
|
return {
|
|
701
|
-
content: [{ type: "text", text:
|
|
733
|
+
content: [{ type: "text", text: formatMessageContent(resultName, result, usageSnapshot, perf, runMetadata) }],
|
|
702
734
|
details: workflowEnvelope(resultName, result, usageSnapshot, perf, runMetadata),
|
|
703
735
|
};
|
|
704
736
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import { cp, mkdir, rm } from "node:fs/promises";
|
|
4
|
+
import { join, resolve, sep } from "node:path";
|
|
4
5
|
import { runBoundedProcess } from "./process-runner.ts";
|
|
5
6
|
|
|
6
7
|
export interface WorktreeGitCommandOptions {
|
|
@@ -24,11 +25,13 @@ export interface WorktreeGitRunner {
|
|
|
24
25
|
|
|
25
26
|
export interface WorktreeRef {
|
|
26
27
|
readonly path: string;
|
|
28
|
+
readonly snapshot?: boolean;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface WorktreeAddFailure {
|
|
30
32
|
readonly path: string;
|
|
31
33
|
readonly error: string;
|
|
34
|
+
readonly snapshot?: boolean;
|
|
32
35
|
readonly cleanup?: WorktreeGitCommandResult;
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -67,6 +70,7 @@ let pathCounter = 0;
|
|
|
67
70
|
|
|
68
71
|
export class WorktreeRegistry {
|
|
69
72
|
private readonly paths = new Set<string>();
|
|
73
|
+
private readonly snapshots = new Set<string>();
|
|
70
74
|
private readonly runner: WorktreeGitRunner;
|
|
71
75
|
private readonly patchCapture: WorktreePatchCapture;
|
|
72
76
|
private readonly timeoutMs: number;
|
|
@@ -92,6 +96,7 @@ export class WorktreeRegistry {
|
|
|
92
96
|
async add(signal?: AbortSignal): Promise<WorktreeRef | WorktreeAddFailure> {
|
|
93
97
|
const added = await addWorktree({ repoCwd: this.repoCwd, runner: this.runner, signal, timeoutMs: this.timeoutMs });
|
|
94
98
|
this.register(added.path);
|
|
99
|
+
if (added.snapshot === true) this.snapshots.add(added.path);
|
|
95
100
|
if ("error" in added) {
|
|
96
101
|
const cleanup = await this.remove(added.path);
|
|
97
102
|
return { ...added, cleanup };
|
|
@@ -104,13 +109,16 @@ export class WorktreeRegistry {
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
async remove(path: string): Promise<WorktreeGitCommandResult> {
|
|
107
|
-
const result = await removeWorktree({ repoCwd: this.repoCwd, path, runner: this.runner, timeoutMs: this.timeoutMs }).catch((error: unknown) => ({
|
|
112
|
+
const result = await removeWorktree({ repoCwd: this.repoCwd, path, runner: this.runner, timeoutMs: this.timeoutMs, snapshot: this.snapshots.has(path) }).catch((error: unknown) => ({
|
|
108
113
|
ok: false,
|
|
109
114
|
stdout: "",
|
|
110
115
|
stderr: "",
|
|
111
116
|
error: formatError(error),
|
|
112
117
|
}));
|
|
113
|
-
if (result.ok)
|
|
118
|
+
if (result.ok) {
|
|
119
|
+
this.paths.delete(path);
|
|
120
|
+
this.snapshots.delete(path);
|
|
121
|
+
}
|
|
114
122
|
return result;
|
|
115
123
|
}
|
|
116
124
|
|
|
@@ -163,8 +171,16 @@ export async function addWorktree(options: {
|
|
|
163
171
|
timeoutMs: options.timeoutMs ?? DEFAULT_WORKTREE_TIMEOUT_MS,
|
|
164
172
|
})
|
|
165
173
|
.catch((error: unknown) => ({ ok: false, stdout: "", stderr: "", error: formatError(error) }));
|
|
166
|
-
if (
|
|
167
|
-
|
|
174
|
+
if (result.ok) return { path };
|
|
175
|
+
|
|
176
|
+
const message = result.error ?? (result.stderr.trim() || "git worktree add failed");
|
|
177
|
+
if (!isInvalidHeadError(message)) return { path, error: message };
|
|
178
|
+
|
|
179
|
+
const snapshotError = await createUnbornRepoSnapshot({ repoCwd: options.repoCwd, path, timeoutMs: options.timeoutMs, signal: options.signal })
|
|
180
|
+
.then(() => undefined)
|
|
181
|
+
.catch((error: unknown) => formatError(error));
|
|
182
|
+
if (snapshotError !== undefined) return { path, snapshot: true, error: `git worktree add failed (${message}); unborn-repo snapshot fallback failed: ${snapshotError}` };
|
|
183
|
+
return { path, snapshot: true };
|
|
168
184
|
}
|
|
169
185
|
|
|
170
186
|
export async function removeWorktree(options: {
|
|
@@ -172,7 +188,12 @@ export async function removeWorktree(options: {
|
|
|
172
188
|
readonly path: string;
|
|
173
189
|
readonly runner?: WorktreeGitRunner;
|
|
174
190
|
readonly timeoutMs?: number;
|
|
191
|
+
readonly snapshot?: boolean;
|
|
175
192
|
}): Promise<WorktreeGitCommandResult> {
|
|
193
|
+
if (options.snapshot === true) {
|
|
194
|
+
await rm(options.path, { recursive: true, force: true });
|
|
195
|
+
return { ok: true, stdout: "", stderr: "" };
|
|
196
|
+
}
|
|
176
197
|
return await (options.runner ?? spawnGitRunner).runGit({
|
|
177
198
|
cwd: options.repoCwd,
|
|
178
199
|
args: ["worktree", "remove", "--force", options.path],
|
|
@@ -211,6 +232,38 @@ export async function captureWorktreePatch(options: {
|
|
|
211
232
|
return { patch: diff.stdout, changed: diff.stdout.trim().length > 0 };
|
|
212
233
|
}
|
|
213
234
|
|
|
235
|
+
async function createUnbornRepoSnapshot(options: {
|
|
236
|
+
readonly repoCwd: string;
|
|
237
|
+
readonly path: string;
|
|
238
|
+
readonly signal?: AbortSignal;
|
|
239
|
+
readonly timeoutMs?: number;
|
|
240
|
+
}): Promise<void> {
|
|
241
|
+
const targetRoot = resolve(options.path);
|
|
242
|
+
await rm(options.path, { recursive: true, force: true });
|
|
243
|
+
await mkdir(options.path, { recursive: true });
|
|
244
|
+
await cp(options.repoCwd, options.path, {
|
|
245
|
+
recursive: true,
|
|
246
|
+
force: true,
|
|
247
|
+
filter: (source) => {
|
|
248
|
+
const resolvedSource = resolve(source);
|
|
249
|
+
if (resolvedSource === targetRoot || resolvedSource.startsWith(`${targetRoot}${sep}`)) return false;
|
|
250
|
+
return !resolvedSource.split(/[\\/]/).includes(".git");
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
await runGitCommand({ cwd: options.path, args: ["init"], signal: options.signal, timeoutMs: options.timeoutMs ?? DEFAULT_WORKTREE_TIMEOUT_MS });
|
|
254
|
+
await runGitCommand({ cwd: options.path, args: ["add", "-A"], signal: options.signal, timeoutMs: options.timeoutMs ?? DEFAULT_WORKTREE_TIMEOUT_MS });
|
|
255
|
+
await runGitCommand({
|
|
256
|
+
cwd: options.path,
|
|
257
|
+
args: ["-c", "user.name=pi-workflow", "-c", "user.email=pi-workflow@example.invalid", "commit", "--allow-empty", "-m", "pi workflow baseline"],
|
|
258
|
+
signal: options.signal,
|
|
259
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_WORKTREE_TIMEOUT_MS,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isInvalidHeadError(message: string): boolean {
|
|
264
|
+
return /invalid reference:\s*HEAD/i.test(message) || /ambiguous argument ['"]?HEAD/i.test(message) || /unknown revision or path.*HEAD/i.test(message);
|
|
265
|
+
}
|
|
266
|
+
|
|
214
267
|
export const spawnGitRunner: WorktreeGitRunner = {
|
|
215
268
|
async runGit(options) {
|
|
216
269
|
return await runGitCommand(options);
|
|
@@ -218,11 +271,14 @@ export const spawnGitRunner: WorktreeGitRunner = {
|
|
|
218
271
|
};
|
|
219
272
|
|
|
220
273
|
async function runGitCommand(options: WorktreeGitCommandOptions): Promise<WorktreeGitCommandResult> {
|
|
274
|
+
const env = { ...process.env };
|
|
275
|
+
delete env.GIT_EXTERNAL_DIFF;
|
|
276
|
+
delete env.GIT_DIFF_OPTS;
|
|
221
277
|
return await runBoundedProcess({
|
|
222
278
|
file: "git",
|
|
223
279
|
args: options.args,
|
|
224
280
|
cwd: options.cwd,
|
|
225
|
-
env
|
|
281
|
+
env,
|
|
226
282
|
signal: options.signal,
|
|
227
283
|
timeoutMs: options.timeoutMs,
|
|
228
284
|
maxBufferBytes: options.maxBufferBytes,
|
package/package.json
CHANGED