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${summarize(result)}${details.length > 0 ? `\n\n${details.join("\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: `${summarize(result)}${detailLines.length > 0 ? `\n\n${detailLines.join("\n")}` : ""}` }],
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 { join } from "node:path";
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) this.paths.delete(path);
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 (!result.ok) return { path, error: result.error ?? (result.stderr.trim() || "git worktree add failed") };
167
- return { path };
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: { ...process.env, GIT_EXTERNAL_DIFF: "", GIT_DIFF_OPTS: "" },
281
+ env,
226
282
  signal: options.signal,
227
283
  timeoutMs: options.timeoutMs,
228
284
  maxBufferBytes: options.maxBufferBytes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-workflow-engine",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency Dynamax workflows for pi: live parallel subagents, typed handoffs, synthesized results.",
6
6
  "keywords": [