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
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
transitionWorkflowRun,
|
|
9
9
|
updateWorkflowRunProgress,
|
|
10
10
|
type WorkflowRunRecord,
|
|
11
|
+
type WorkflowRunState,
|
|
11
12
|
type WorkflowRunTransition,
|
|
12
13
|
} from "./workflow-run-record.ts";
|
|
13
14
|
|
|
@@ -37,8 +38,9 @@ export class ProjectWorkflowRunStore implements WorkflowRunStore {
|
|
|
37
38
|
try {
|
|
38
39
|
await writeFile(temporaryPath, content, { encoding: "utf8", mode: 0o600 });
|
|
39
40
|
await rename(temporaryPath, path);
|
|
40
|
-
}
|
|
41
|
+
} catch (error) {
|
|
41
42
|
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
43
|
+
throw error;
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -47,43 +49,22 @@ export class ProjectWorkflowRunStore implements WorkflowRunStore {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
async list(): Promise<WorkflowRunRecord[]> {
|
|
50
|
-
const
|
|
51
|
-
let files: string[];
|
|
52
|
-
try {
|
|
53
|
-
files = await readdir(dir);
|
|
54
|
-
} catch {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
const records = await Promise.all(
|
|
58
|
-
files
|
|
59
|
-
.filter((file) => file.endsWith(WORKFLOW_RUN_RECORD_SUFFIX))
|
|
60
|
-
.map((file) => loadWorkflowRunRecord(join(dir, file))),
|
|
61
|
-
);
|
|
52
|
+
const records = await Promise.all((await this.recordPaths()).map(loadWorkflowRunRecord));
|
|
62
53
|
return records
|
|
63
54
|
.filter((record): record is WorkflowRunRecord => record !== undefined)
|
|
64
55
|
.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
65
56
|
}
|
|
66
57
|
|
|
67
58
|
async prune(keep = WORKFLOW_RUN_RECORD_KEEP): Promise<void> {
|
|
68
|
-
const dir = join(this.cwd, WORKFLOW_RUNS_DIR);
|
|
69
|
-
let files: string[];
|
|
70
|
-
try {
|
|
71
|
-
files = await readdir(dir);
|
|
72
|
-
} catch {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
59
|
const candidates = await Promise.all(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
}),
|
|
60
|
+
(await this.recordPaths()).map(async (path) => {
|
|
61
|
+
try {
|
|
62
|
+
const info = await stat(path);
|
|
63
|
+
return { path, mtimeMs: info.mtimeMs };
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}),
|
|
87
68
|
);
|
|
88
69
|
const stale = candidates
|
|
89
70
|
.filter((entry): entry is { readonly path: string; readonly mtimeMs: number } => entry !== undefined)
|
|
@@ -91,6 +72,17 @@ export class ProjectWorkflowRunStore implements WorkflowRunStore {
|
|
|
91
72
|
.slice(Math.max(0, keep));
|
|
92
73
|
await Promise.all(stale.map((entry) => rm(entry.path, { force: true }).catch(() => undefined)));
|
|
93
74
|
}
|
|
75
|
+
|
|
76
|
+
/** Record file paths in the runs directory; empty when the directory cannot be read. */
|
|
77
|
+
private async recordPaths(): Promise<string[]> {
|
|
78
|
+
const dir = join(this.cwd, WORKFLOW_RUNS_DIR);
|
|
79
|
+
try {
|
|
80
|
+
const files = await readdir(dir);
|
|
81
|
+
return files.filter((file) => file.endsWith(WORKFLOW_RUN_RECORD_SUFFIX)).map((file) => join(dir, file));
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
94
86
|
}
|
|
95
87
|
|
|
96
88
|
/** Owns one run record and coalesces progress writes while a prior atomic save is in flight. */
|
|
@@ -110,6 +102,10 @@ export class DurableWorkflowRun {
|
|
|
110
102
|
this.queue(initial);
|
|
111
103
|
}
|
|
112
104
|
|
|
105
|
+
get state(): WorkflowRunState {
|
|
106
|
+
return this.record.state;
|
|
107
|
+
}
|
|
108
|
+
|
|
113
109
|
updateProgress(progress: WorkflowProgressSnapshot, at = Date.now()): void {
|
|
114
110
|
this.record = updateWorkflowRunProgress(this.record, progress, at);
|
|
115
111
|
this.queue(this.record);
|
|
@@ -13,7 +13,7 @@ interface ScheduledUsageLimitResume {
|
|
|
13
13
|
readonly handle: unknown;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
const defaultWorkflowUsageLimitSchedulerClock: WorkflowUsageLimitSchedulerClock = {
|
|
17
17
|
now: () => Date.now(),
|
|
18
18
|
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
19
19
|
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
@@ -73,8 +73,4 @@ export class WorkflowUsageLimitScheduler {
|
|
|
73
73
|
activateSession(ctx: Pick<ExtensionContext, "sessionManager">): void {
|
|
74
74
|
this.inactiveSessions.delete(ctx.sessionManager.getSessionId());
|
|
75
75
|
}
|
|
76
|
-
|
|
77
|
-
has(runId: string): boolean {
|
|
78
|
-
return this.scheduled.has(runId);
|
|
79
|
-
}
|
|
80
76
|
}
|
|
@@ -12,8 +12,9 @@ import type { WorkflowModule } from "./types.ts";
|
|
|
12
12
|
* pi's bundled `typebox` / SDK — guaranteeing schema validation works.
|
|
13
13
|
*
|
|
14
14
|
* To add a guaranteed workflow: drop a `.ts` in `workflows/`, import it here, and add
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* a `defineBuiltinWorkflow(mod, "<name>.ts")` entry to `BUILTIN_WORKFLOW_DEFINITIONS`.
|
|
16
|
+
* (Drop-in workflows discovered at runtime are also supported — see discovery.ts — but
|
|
17
|
+
* the static registry is the always-correct path.)
|
|
17
18
|
*
|
|
18
19
|
* Do not replace these built-in imports with dynamic imports unless a dedicated test
|
|
19
20
|
* proves pi's jiti virtual modules preserve the same bundled `typebox` identity for
|
|
@@ -26,7 +27,6 @@ export interface BuiltinWorkflowDefinition {
|
|
|
26
27
|
readonly module: WorkflowModule;
|
|
27
28
|
readonly filename: string;
|
|
28
29
|
readonly path: string;
|
|
29
|
-
readonly root: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export const BUILTIN_WORKFLOW_DEFINITIONS: readonly BuiltinWorkflowDefinition[] = [
|
|
@@ -36,15 +36,12 @@ export const BUILTIN_WORKFLOW_DEFINITIONS: readonly BuiltinWorkflowDefinition[]
|
|
|
36
36
|
defineBuiltinWorkflow(perfReview, "perf-review.ts"),
|
|
37
37
|
defineBuiltinWorkflow(research, "research.ts"),
|
|
38
38
|
];
|
|
39
|
-
export const BUILTIN_WORKFLOWS = BUILTIN_WORKFLOW_DEFINITIONS.map(({ module }) => module);
|
|
40
39
|
export const BUILTIN_WORKFLOW_FILES = new Set(BUILTIN_WORKFLOW_DEFINITIONS.map(({ filename }) => filename));
|
|
41
|
-
export const BUILTIN_WORKFLOW_NAMES = BUILTIN_WORKFLOWS.map((mod) => mod.meta.name);
|
|
42
40
|
|
|
43
41
|
function defineBuiltinWorkflow(module: WorkflowModule, filename: string): BuiltinWorkflowDefinition {
|
|
44
42
|
return {
|
|
45
43
|
module: { meta: module.meta, default: module.default },
|
|
46
44
|
filename,
|
|
47
45
|
path: fileURLToPath(new URL(`../workflows/${filename}`, import.meta.url)),
|
|
48
|
-
root: BUILTIN_SOURCE_ROOT,
|
|
49
46
|
};
|
|
50
47
|
}
|
|
@@ -1,26 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { cp, mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { join, resolve, sep } from "node:path";
|
|
6
|
+
import { isGitObjectId, parseGitTopLevel, runGit, type GitCommandOptions } from "./git.ts";
|
|
7
|
+
import { GIT_DIFF_MACHINE_FORMAT } from "./review-diff-target.ts";
|
|
8
|
+
import { FINGERPRINT_EXCLUDED_RELATIVE_PATHS, isExcludedDeclaredInput, portableRelativePath } from "./tree-fingerprint.ts";
|
|
6
9
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
7
10
|
|
|
8
|
-
export
|
|
9
|
-
readonly cwd: string;
|
|
10
|
-
readonly args: readonly string[];
|
|
11
|
-
readonly env?: NodeJS.ProcessEnv;
|
|
12
|
-
readonly stdin?: string;
|
|
13
|
-
readonly signal?: AbortSignal;
|
|
14
|
-
readonly timeoutMs: number;
|
|
15
|
-
readonly maxBufferBytes?: number;
|
|
16
|
-
}
|
|
11
|
+
export type WorktreeGitCommandOptions = Omit<GitCommandOptions, "file" | "label">;
|
|
17
12
|
|
|
18
|
-
export
|
|
19
|
-
readonly ok:
|
|
20
|
-
readonly stdout: string;
|
|
21
|
-
readonly stderr: string;
|
|
22
|
-
readonly error?: string;
|
|
23
|
-
}
|
|
13
|
+
export type WorktreeGitCommandResult =
|
|
14
|
+
| { readonly ok: true; readonly stdout: string; readonly stderr: string }
|
|
15
|
+
| { readonly ok: false; readonly stdout: string; readonly stderr: string; readonly error: string };
|
|
24
16
|
|
|
25
17
|
export interface WorktreeGitRunner {
|
|
26
18
|
runGit(options: WorktreeGitCommandOptions): Promise<WorktreeGitCommandResult>;
|
|
@@ -46,24 +38,18 @@ export interface WorktreeAddFailure {
|
|
|
46
38
|
readonly cleanup?: WorktreeGitCommandResult;
|
|
47
39
|
}
|
|
48
40
|
|
|
49
|
-
export
|
|
50
|
-
readonly ok: boolean;
|
|
51
|
-
readonly inside: boolean;
|
|
52
|
-
readonly error?: string;
|
|
53
|
-
}
|
|
41
|
+
export type GitWorktreeProbe = { readonly ok: true; readonly inside: boolean } | { readonly ok: false; readonly error: string };
|
|
54
42
|
|
|
55
|
-
export
|
|
56
|
-
readonly path: string;
|
|
57
|
-
}
|
|
43
|
+
export type WorktreeRemovalOutcome = WorktreeGitCommandResult & { readonly path: string };
|
|
58
44
|
|
|
59
45
|
export class WorktreeCleanupError extends AggregateError {
|
|
60
46
|
readonly outcomes: readonly WorktreeRemovalOutcome[];
|
|
61
47
|
|
|
62
48
|
constructor(outcomes: readonly WorktreeRemovalOutcome[]) {
|
|
63
49
|
const failures = outcomes.filter((outcome) => !outcome.ok);
|
|
64
|
-
const details = failures.map((failure) => `${failure.path} (${
|
|
50
|
+
const details = failures.map((failure) => `${failure.path} (${failure.error})`);
|
|
65
51
|
super(
|
|
66
|
-
failures.map((failure) => new Error(`Failed to remove isolated worktree ${failure.path}: ${
|
|
52
|
+
failures.map((failure) => new Error(`Failed to remove isolated worktree ${failure.path}: ${failure.error}`)),
|
|
67
53
|
`Failed to remove ${failures.length} isolated worktree${failures.length === 1 ? "" : "s"}: ${details.join(", ")}`,
|
|
68
54
|
);
|
|
69
55
|
this.name = "WorktreeCleanupError";
|
|
@@ -77,7 +63,6 @@ export interface WorktreePatch {
|
|
|
77
63
|
}
|
|
78
64
|
|
|
79
65
|
export interface WorktreeRegistryOptions {
|
|
80
|
-
readonly repoCwd: string;
|
|
81
66
|
readonly runner?: WorktreeGitRunner;
|
|
82
67
|
readonly patchCapture?: WorktreePatchCapture;
|
|
83
68
|
readonly timeoutMs?: number;
|
|
@@ -111,7 +96,7 @@ export class WorktreeRegistry {
|
|
|
111
96
|
private readonly patchCapture: WorktreePatchCapture;
|
|
112
97
|
private readonly timeoutMs: number;
|
|
113
98
|
|
|
114
|
-
constructor(private readonly repoCwd: string, options:
|
|
99
|
+
constructor(private readonly repoCwd: string, options: WorktreeRegistryOptions = {}) {
|
|
115
100
|
this.runner = options.runner ?? spawnGitRunner;
|
|
116
101
|
this.patchCapture = options.patchCapture ?? captureWorktreePatch;
|
|
117
102
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_WORKTREE_TIMEOUT_MS;
|
|
@@ -131,6 +116,8 @@ export class WorktreeRegistry {
|
|
|
131
116
|
|
|
132
117
|
async add(signal?: AbortSignal, baseline?: WorktreeBaseline): Promise<WorktreeRef | WorktreeAddFailure> {
|
|
133
118
|
const added = await addWorktree({ repoCwd: this.repoCwd, runner: this.runner, signal, timeoutMs: this.timeoutMs, baseline });
|
|
119
|
+
// A rejected `git worktree add` creates nothing; tracking it would make cleanup fail on a path git never knew.
|
|
120
|
+
if ("error" in added && !existsSync(added.path)) return added;
|
|
134
121
|
this.register(added.path);
|
|
135
122
|
if (added.snapshot === true) this.snapshots.add(added.path);
|
|
136
123
|
if ("error" in added) {
|
|
@@ -154,6 +141,13 @@ export class WorktreeRegistry {
|
|
|
154
141
|
});
|
|
155
142
|
}
|
|
156
143
|
|
|
144
|
+
async applyPatch(path: string, patch: string, signal?: AbortSignal): Promise<WorktreeGitCommandResult> {
|
|
145
|
+
return this.runner.runGit({
|
|
146
|
+
cwd: path, args: ["apply", "--index", "--binary", "-"],
|
|
147
|
+
stdin: patch, signal, timeoutMs: this.timeoutMs,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
157
151
|
async validatePatch(path: string, candidate: WorktreePatch, signal?: AbortSignal): Promise<WorktreeGitCommandResult> {
|
|
158
152
|
return await validateWorktreePatch({
|
|
159
153
|
worktreePath: path,
|
|
@@ -191,12 +185,7 @@ export class WorktreeRegistry {
|
|
|
191
185
|
runner: this.runner,
|
|
192
186
|
timeoutMs: this.timeoutMs,
|
|
193
187
|
snapshot: this.snapshots.has(path),
|
|
194
|
-
}).catch(
|
|
195
|
-
ok: false,
|
|
196
|
-
stdout: "",
|
|
197
|
-
stderr: "",
|
|
198
|
-
error: unknownErrorMessage(error),
|
|
199
|
-
}));
|
|
188
|
+
}).catch(toGitFailure);
|
|
200
189
|
if (result.ok) {
|
|
201
190
|
this.paths.delete(path);
|
|
202
191
|
this.snapshots.delete(path);
|
|
@@ -223,8 +212,8 @@ export async function isGitWorktree(options: {
|
|
|
223
212
|
signal: options.signal,
|
|
224
213
|
timeoutMs: options.timeoutMs ?? DEFAULT_WORKTREE_TIMEOUT_MS,
|
|
225
214
|
})
|
|
226
|
-
.catch(
|
|
227
|
-
if (!result.ok) return { ok: false,
|
|
215
|
+
.catch(toGitFailure);
|
|
216
|
+
if (!result.ok) return { ok: false, error: result.error };
|
|
228
217
|
return { ok: true, inside: result.stdout.trim() === "true" };
|
|
229
218
|
}
|
|
230
219
|
|
|
@@ -247,7 +236,7 @@ export async function addWorktree(options: {
|
|
|
247
236
|
signal: options.signal,
|
|
248
237
|
timeoutMs,
|
|
249
238
|
})
|
|
250
|
-
.catch(
|
|
239
|
+
.catch(toGitFailure);
|
|
251
240
|
if (result.ok) {
|
|
252
241
|
const patch = options.baseline?.patch;
|
|
253
242
|
if (patch !== undefined && patch.trim().length > 0) {
|
|
@@ -257,22 +246,20 @@ export async function addWorktree(options: {
|
|
|
257
246
|
runner,
|
|
258
247
|
signal: options.signal,
|
|
259
248
|
timeoutMs,
|
|
260
|
-
}).catch(
|
|
261
|
-
if (!prepared.ok) return { path, error: prepared.error
|
|
249
|
+
}).catch(toGitFailure);
|
|
250
|
+
if (!prepared.ok) return { path, error: prepared.error };
|
|
262
251
|
}
|
|
263
252
|
return await finalizeWorktreeRef({ path, runner, signal: options.signal, timeoutMs });
|
|
264
253
|
}
|
|
265
254
|
|
|
266
|
-
|
|
267
|
-
if (!isInvalidHeadError(message)) return { path, error: message };
|
|
255
|
+
if (!isInvalidHeadError(result.error)) return { path, error: result.error };
|
|
268
256
|
|
|
269
257
|
return await addUnbornRepositoryWorktree({
|
|
270
258
|
repoCwd: options.repoCwd,
|
|
271
259
|
path,
|
|
272
|
-
addError:
|
|
260
|
+
addError: result.error,
|
|
273
261
|
runner,
|
|
274
262
|
signal: options.signal,
|
|
275
|
-
snapshotTimeoutMs: options.timeoutMs,
|
|
276
263
|
timeoutMs,
|
|
277
264
|
});
|
|
278
265
|
}
|
|
@@ -283,13 +270,12 @@ async function addUnbornRepositoryWorktree(options: {
|
|
|
283
270
|
readonly addError: string;
|
|
284
271
|
readonly runner: WorktreeGitRunner;
|
|
285
272
|
readonly signal?: AbortSignal;
|
|
286
|
-
readonly snapshotTimeoutMs?: number;
|
|
287
273
|
readonly timeoutMs: number;
|
|
288
274
|
}): Promise<WorktreeRef | WorktreeAddFailure> {
|
|
289
275
|
const snapshotError = await createUnbornRepoSnapshot({
|
|
290
276
|
repoCwd: options.repoCwd,
|
|
291
277
|
path: options.path,
|
|
292
|
-
timeoutMs: options.
|
|
278
|
+
timeoutMs: options.timeoutMs,
|
|
293
279
|
signal: options.signal,
|
|
294
280
|
})
|
|
295
281
|
.then(() => undefined)
|
|
@@ -324,12 +310,12 @@ async function finalizeWorktreeRef(options: {
|
|
|
324
310
|
signal: options.signal,
|
|
325
311
|
timeoutMs: options.timeoutMs,
|
|
326
312
|
})
|
|
327
|
-
.catch(
|
|
313
|
+
.catch(toGitFailure);
|
|
328
314
|
if (!resolved.ok) {
|
|
329
315
|
return {
|
|
330
316
|
path: options.path,
|
|
331
317
|
snapshot: options.snapshot,
|
|
332
|
-
error: resolved.error
|
|
318
|
+
error: resolved.error,
|
|
333
319
|
};
|
|
334
320
|
}
|
|
335
321
|
const baselineOid = resolved.stdout.trim();
|
|
@@ -403,19 +389,19 @@ export async function captureWorktreePatch(options: {
|
|
|
403
389
|
signal: options.signal,
|
|
404
390
|
timeoutMs,
|
|
405
391
|
})
|
|
406
|
-
.catch(
|
|
407
|
-
if (!intentToAdd.ok) return { error: intentToAdd.error
|
|
392
|
+
.catch(toGitFailure);
|
|
393
|
+
if (!intentToAdd.ok) return { error: intentToAdd.error };
|
|
408
394
|
|
|
409
395
|
const diff = await runner
|
|
410
396
|
.runGit({
|
|
411
397
|
cwd: options.worktreePath,
|
|
412
|
-
args: ["diff", "--binary", "--full-index", "--no-ext-diff",
|
|
398
|
+
args: ["diff", "--binary", "--full-index", "--no-ext-diff", ...GIT_DIFF_MACHINE_FORMAT, options.baselineOid, "--"],
|
|
413
399
|
signal: options.signal,
|
|
414
400
|
timeoutMs,
|
|
415
401
|
maxBufferBytes: WORKTREE_DIFF_MAX_BYTES,
|
|
416
402
|
})
|
|
417
|
-
.catch(
|
|
418
|
-
if (!diff.ok) return { error: diff.error
|
|
403
|
+
.catch(toGitFailure);
|
|
404
|
+
if (!diff.ok) return { error: diff.error };
|
|
419
405
|
return { patch: diff.stdout, changed: diff.stdout.trim().length > 0 };
|
|
420
406
|
}
|
|
421
407
|
|
|
@@ -428,7 +414,7 @@ export async function validateWorktreePatch(options: {
|
|
|
428
414
|
}): Promise<WorktreeGitCommandResult> {
|
|
429
415
|
const hasPatch = options.candidate.patch.trim().length > 0;
|
|
430
416
|
if (options.candidate.changed !== hasPatch) {
|
|
431
|
-
return
|
|
417
|
+
return gitFailure("cached worktree patch changed flag does not match patch content");
|
|
432
418
|
}
|
|
433
419
|
if (!hasPatch) return { ok: true, stdout: "", stderr: "" };
|
|
434
420
|
|
|
@@ -441,29 +427,31 @@ export async function validateWorktreePatch(options: {
|
|
|
441
427
|
signal: options.signal,
|
|
442
428
|
timeoutMs,
|
|
443
429
|
})
|
|
444
|
-
.catch(
|
|
430
|
+
.catch(toGitFailure);
|
|
445
431
|
}
|
|
446
432
|
|
|
447
|
-
function
|
|
433
|
+
function gitFailure(error: string): WorktreeGitCommandResult {
|
|
448
434
|
return { ok: false, stdout: "", stderr: "", error };
|
|
449
435
|
}
|
|
450
436
|
|
|
437
|
+
function toGitFailure(error: unknown): WorktreeGitCommandResult {
|
|
438
|
+
return gitFailure(unknownErrorMessage(error));
|
|
439
|
+
}
|
|
440
|
+
|
|
451
441
|
async function createUnbornRepoSnapshot(options: {
|
|
452
442
|
readonly repoCwd: string;
|
|
453
443
|
readonly path: string;
|
|
454
444
|
readonly signal?: AbortSignal;
|
|
455
|
-
readonly timeoutMs
|
|
445
|
+
readonly timeoutMs: number;
|
|
456
446
|
}): Promise<void> {
|
|
457
|
-
const timeoutMs = options
|
|
447
|
+
const { timeoutMs } = options;
|
|
458
448
|
const rootProbe = await runGitCommand({
|
|
459
449
|
cwd: options.repoCwd,
|
|
460
450
|
args: ["rev-parse", "--show-toplevel"],
|
|
461
451
|
signal: options.signal,
|
|
462
452
|
timeoutMs,
|
|
463
453
|
});
|
|
464
|
-
if (!rootProbe.ok)
|
|
465
|
-
throw new Error(rootProbe.error ?? (rootProbe.stderr.trim() || "failed to resolve unborn repository root"));
|
|
466
|
-
}
|
|
454
|
+
if (!rootProbe.ok) throw new Error(rootProbe.error);
|
|
467
455
|
const sourceRoot = parseGitTopLevel(rootProbe.stdout, options.repoCwd);
|
|
468
456
|
if (!sourceRoot) throw new Error("unborn repository root probe returned an invalid path");
|
|
469
457
|
|
|
@@ -473,10 +461,12 @@ async function createUnbornRepoSnapshot(options: {
|
|
|
473
461
|
await cp(sourceRoot, options.path, {
|
|
474
462
|
recursive: true,
|
|
475
463
|
force: true,
|
|
464
|
+
// Keep link targets as stored, like a git checkout; resolved targets would point into the source repo.
|
|
465
|
+
verbatimSymlinks: true,
|
|
476
466
|
filter: (source) => {
|
|
477
467
|
const resolvedSource = resolve(source);
|
|
478
468
|
if (resolvedSource === targetRoot || resolvedSource.startsWith(`${targetRoot}${sep}`)) return false;
|
|
479
|
-
return !
|
|
469
|
+
return !isExcludedDeclaredInput(portableRelativePath(sourceRoot, resolvedSource), FINGERPRINT_EXCLUDED_RELATIVE_PATHS);
|
|
480
470
|
},
|
|
481
471
|
});
|
|
482
472
|
await requireGitCommand({ cwd: options.path, args: ["init"], signal: options.signal, timeoutMs });
|
|
@@ -513,60 +503,15 @@ function syntheticCommitArgs(hooksPath: string, message: string): readonly strin
|
|
|
513
503
|
|
|
514
504
|
async function requireGitCommand(options: WorktreeGitCommandOptions): Promise<void> {
|
|
515
505
|
const result = await runGitCommand(options);
|
|
516
|
-
if (!result.ok) throw new Error(result.error
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function parseGitTopLevel(output: string, cwd: string): string | undefined {
|
|
520
|
-
const withoutLf = output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
521
|
-
const value = withoutLf.endsWith("\r") ? withoutLf.slice(0, -1) : withoutLf;
|
|
522
|
-
if (value.length === 0 || value.includes("\n") || value.includes("\0")) return undefined;
|
|
523
|
-
const root = resolve(cwd, value);
|
|
524
|
-
const cwdFromRoot = relative(root, resolve(cwd));
|
|
525
|
-
return isAbsolute(cwdFromRoot) || cwdFromRoot === ".." || cwdFromRoot.startsWith(`..${sep}`) ? undefined : root;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function isExcludedSnapshotPath(sourceRoot: string, source: string): boolean {
|
|
529
|
-
const path = relative(sourceRoot, source);
|
|
530
|
-
if (path.length === 0) return false;
|
|
531
|
-
const segments = path.split(sep);
|
|
532
|
-
if (segments.includes(".git")) return true;
|
|
533
|
-
return segments.some((segment, index) => segment === ".pi" && segments[index + 1] === ".workflow-runs");
|
|
506
|
+
if (!result.ok) throw new Error(result.error);
|
|
534
507
|
}
|
|
535
508
|
|
|
536
509
|
function isInvalidHeadError(message: string): boolean {
|
|
537
510
|
return /invalid reference:\s*HEAD/i.test(message) || /ambiguous argument ['"]?HEAD/i.test(message) || /unknown revision or path.*HEAD/i.test(message);
|
|
538
511
|
}
|
|
539
512
|
|
|
540
|
-
|
|
541
|
-
return /^[0-9a-f]{40,64}$/i.test(value);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
export const spawnGitRunner: WorktreeGitRunner = {
|
|
545
|
-
async runGit(options) {
|
|
546
|
-
return await runGitCommand(options);
|
|
547
|
-
},
|
|
548
|
-
};
|
|
513
|
+
export const spawnGitRunner: WorktreeGitRunner = { runGit: runGitCommand };
|
|
549
514
|
|
|
550
515
|
async function runGitCommand(options: WorktreeGitCommandOptions): Promise<WorktreeGitCommandResult> {
|
|
551
|
-
|
|
552
|
-
delete env.GIT_EXTERNAL_DIFF;
|
|
553
|
-
delete env.GIT_DIFF_OPTS;
|
|
554
|
-
return await runBoundedProcess({
|
|
555
|
-
file: "git",
|
|
556
|
-
args: options.args,
|
|
557
|
-
cwd: options.cwd,
|
|
558
|
-
env,
|
|
559
|
-
stdin: options.stdin,
|
|
560
|
-
signal: options.signal,
|
|
561
|
-
timeoutMs: options.timeoutMs,
|
|
562
|
-
maxBufferBytes: options.maxBufferBytes,
|
|
563
|
-
abortError: "git worktree command aborted",
|
|
564
|
-
timeoutError: `git worktree command timed out after ${options.timeoutMs}ms`,
|
|
565
|
-
maxBufferError: options.maxBufferBytes === undefined ? undefined : `git worktree command exceeded ${options.maxBufferBytes} bytes`,
|
|
566
|
-
exitError: (stderr, code, signal) => stderr.trim() || `git exited with code ${code ?? `signal ${signal ?? "unknown"}`}`,
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function worktreeRemovalError(outcome: WorktreeRemovalOutcome): string {
|
|
571
|
-
return outcome.error ?? (outcome.stderr.trim() || "unknown error");
|
|
516
|
+
return await runGit({ ...options, label: "git worktree command" });
|
|
572
517
|
}
|