@henryqw/pi-pr 3.1.10 → 4.0.2
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/README.md +24 -9
- package/docs/pr-routing.svg +1 -1
- package/extensions/pr-ci.ts +908 -0
- package/extensions/pr-command.ts +101 -20
- package/extensions/pr-comment-sweep.ts +1102 -0
- package/extensions/pr-create.ts +684 -0
- package/extensions/pr-execution.ts +558 -0
- package/extensions/pr-feedback.ts +519 -0
- package/extensions/pr-github.ts +363 -145
- package/extensions/pr-merge.ts +4 -44
- package/extensions/pr-routing.ts +2 -1
- package/extensions/pr-ui.ts +1 -1
- package/extensions/pr-update-branch.ts +327 -0
- package/extensions/pr.ts +396 -7
- package/package.json +4 -2
- package/skills/pi-pr-comment-sweep/SKILL.md +45 -89
- package/skills/pi-pr-comment-sweep/references/recovery.md +24 -46
- package/skills/pi-pr-comment-sweep/references/thread-triage.md +2 -2
- package/skills/pi-pr-comment-sweep/scripts/pr-feedback.mjs +223 -1799
- package/skills/pi-pr-create/SKILL.md +6 -77
- package/skills/pi-pr-fix-ci/SKILL.md +39 -39
- package/skills/pi-pr-update-branch/SKILL.md +5 -108
package/extensions/pr.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { realpath } from "node:fs/promises";
|
|
1
3
|
import {
|
|
2
4
|
isBashToolResult,
|
|
3
5
|
type ExtensionAPI,
|
|
@@ -5,7 +7,17 @@ import {
|
|
|
5
7
|
} from "@earendil-works/pi-coding-agent";
|
|
6
8
|
import { truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
7
9
|
import { createHerdrClient } from "@henryqw/pi-herdr";
|
|
8
|
-
import {
|
|
10
|
+
import { Type } from "typebox";
|
|
11
|
+
import { PullRequestCiFixer, type PullRequestCiFixOptions } from "./pr-ci.ts";
|
|
12
|
+
import { PullRequestCommentSweep, type PullRequestCommentSweepOptions } from "./pr-comment-sweep.ts";
|
|
13
|
+
import {
|
|
14
|
+
createPrCommandHandler,
|
|
15
|
+
type PrCommandDependencies,
|
|
16
|
+
type PrCommandInvocation,
|
|
17
|
+
type WorkflowPromptIdentity,
|
|
18
|
+
type WorkflowReservation,
|
|
19
|
+
} from "./pr-command.ts";
|
|
20
|
+
import { PullRequestCreator, type CreatePullRequestOptions } from "./pr-create.ts";
|
|
9
21
|
import {
|
|
10
22
|
hasLocalCommit,
|
|
11
23
|
loadCurrentPullRequest,
|
|
@@ -14,6 +26,7 @@ import {
|
|
|
14
26
|
samePullRequestObservation,
|
|
15
27
|
type PullRequestObservation,
|
|
16
28
|
} from "./pr-github.ts";
|
|
29
|
+
import { runChecked, spawnBounded } from "./pr-execution.ts";
|
|
17
30
|
import {
|
|
18
31
|
discoveryIssueKey,
|
|
19
32
|
discoveryIssueMessage,
|
|
@@ -23,6 +36,7 @@ import {
|
|
|
23
36
|
unavailablePrDisplay,
|
|
24
37
|
type PrDisplay,
|
|
25
38
|
} from "./pr-ui.ts";
|
|
39
|
+
import { PullRequestBranchUpdater, type UpdateBranchOptions } from "./pr-update-branch.ts";
|
|
26
40
|
|
|
27
41
|
const POLL_INTERVAL_MS = 30_000;
|
|
28
42
|
const ROUTING_SPINNER_INTERVAL_MS = 80;
|
|
@@ -36,13 +50,150 @@ const GIT_COMMIT = /(?:^|[;&|]\s*|\n\s*)git\s+commit(?=\s|$|[;&|])/;
|
|
|
36
50
|
const GIT_PUSH = /(?:^|[;&|]\s*|\n\s*)git\s+push(?=\s|$|[;&|])/;
|
|
37
51
|
const WORKFLOW_ROUTES = new Set(["create", "update-branch", "sweep", "fix-ci"]);
|
|
38
52
|
const DELEGATED_TOOLS = new Set(["delegate_task", "delegate_flow", "delegate_flow_continue"]);
|
|
53
|
+
const CLOSED = { additionalProperties: false } as const;
|
|
54
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
55
|
+
|
|
56
|
+
const RouteRunId = Type.String({ minLength: 36, maxLength: 36, pattern: UUID.source });
|
|
57
|
+
const ResolvedPaths = Type.Array(Type.String({ minLength: 1, maxLength: 4_096 }), { minItems: 1, maxItems: 128 });
|
|
58
|
+
const OwnedPaths = Type.Array(Type.String({ minLength: 1, maxLength: 4_096 }), { maxItems: 128 });
|
|
59
|
+
const SweepGuard = Type.Object({
|
|
60
|
+
epoch: Type.Integer({ minimum: 1 }),
|
|
61
|
+
runId: Type.String({ minLength: 1, maxLength: 128 }),
|
|
62
|
+
generation: Type.Integer({ minimum: 1 }),
|
|
63
|
+
fingerprint: Type.String({ minLength: 64, maxLength: 64 }),
|
|
64
|
+
}, CLOSED);
|
|
65
|
+
const SweepLedgerEntry = Type.Object({
|
|
66
|
+
id: Type.String({ minLength: 1, maxLength: 1_024 }),
|
|
67
|
+
kind: Type.Union([
|
|
68
|
+
Type.Literal("conversation_comment"), Type.Literal("review"), Type.Literal("thread"), Type.Literal("thread_comment"),
|
|
69
|
+
]),
|
|
70
|
+
disposition: Type.Union([Type.Literal("addressed"), Type.Literal("non-actionable"), Type.Literal("blocked")]),
|
|
71
|
+
note: Type.String({ maxLength: 2_048 }),
|
|
72
|
+
}, CLOSED);
|
|
73
|
+
const SweepLedger = Type.Array(SweepLedgerEntry, { maxItems: 1_000 });
|
|
74
|
+
const SweepProjection = Type.Object({
|
|
75
|
+
generation: Type.Integer({ minimum: 1 }),
|
|
76
|
+
contentFingerprint: Type.String({ minLength: 64, maxLength: 64 }),
|
|
77
|
+
items: Type.Array(Type.Object({
|
|
78
|
+
id: Type.String({ minLength: 1, maxLength: 1_024 }),
|
|
79
|
+
kind: Type.Union([
|
|
80
|
+
Type.Literal("conversation_comment"), Type.Literal("review"), Type.Literal("thread"), Type.Literal("thread_comment"),
|
|
81
|
+
]),
|
|
82
|
+
}, CLOSED), { maxItems: 1_000 }),
|
|
83
|
+
threads: Type.Array(Type.Object({
|
|
84
|
+
id: Type.String({ minLength: 1, maxLength: 1_024 }),
|
|
85
|
+
isResolved: Type.Boolean(),
|
|
86
|
+
}, CLOSED), { maxItems: 1_000 }),
|
|
87
|
+
}, CLOSED);
|
|
88
|
+
const SweepChecks = Type.Array(Type.Object({
|
|
89
|
+
command: Type.String({ minLength: 1, maxLength: 1_024 }),
|
|
90
|
+
args: Type.Array(Type.String({ maxLength: 4_096 }), { maxItems: 256 }),
|
|
91
|
+
}, CLOSED), { maxItems: 32 });
|
|
92
|
+
|
|
93
|
+
const UpdateBranchParameters = Type.Union([
|
|
94
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("merge") }, CLOSED),
|
|
95
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("continue"), resolvedPaths: ResolvedPaths }, CLOSED),
|
|
96
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("publish") }, CLOSED),
|
|
97
|
+
]);
|
|
98
|
+
const CreateParameters = Type.Union([
|
|
99
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("prepare") }, CLOSED),
|
|
100
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("merge") }, CLOSED),
|
|
101
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("continue"), resolvedPaths: ResolvedPaths }, CLOSED),
|
|
102
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("push") }, CLOSED),
|
|
103
|
+
Type.Object({
|
|
104
|
+
runId: RouteRunId,
|
|
105
|
+
action: Type.Literal("publish"),
|
|
106
|
+
title: Type.String({ minLength: 1, maxLength: 256 }),
|
|
107
|
+
body: Type.String({ maxLength: 65_536 }),
|
|
108
|
+
}, CLOSED),
|
|
109
|
+
]);
|
|
110
|
+
const SweepParameters = Type.Union([
|
|
111
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("start") }, CLOSED),
|
|
112
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("resume") }, CLOSED),
|
|
113
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("show"), guard: SweepGuard, id: Type.String({ minLength: 1, maxLength: 1_024 }) }, CLOSED),
|
|
114
|
+
Type.Object({
|
|
115
|
+
runId: RouteRunId,
|
|
116
|
+
action: Type.Literal("record"),
|
|
117
|
+
guard: SweepGuard,
|
|
118
|
+
ledger: SweepLedger,
|
|
119
|
+
ownedPaths: Type.Optional(OwnedPaths),
|
|
120
|
+
}, CLOSED),
|
|
121
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("publish"), guard: SweepGuard }, CLOSED),
|
|
122
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("refresh"), guard: SweepGuard }, CLOSED),
|
|
123
|
+
Type.Object({
|
|
124
|
+
runId: RouteRunId,
|
|
125
|
+
action: Type.Literal("resolve"),
|
|
126
|
+
guard: SweepGuard,
|
|
127
|
+
threadIds: Type.Array(Type.String({ minLength: 1, maxLength: 1_024 }), { maxItems: 1_000 }),
|
|
128
|
+
}, CLOSED),
|
|
129
|
+
Type.Object({
|
|
130
|
+
runId: RouteRunId,
|
|
131
|
+
action: Type.Literal("finalize"),
|
|
132
|
+
guard: SweepGuard,
|
|
133
|
+
projection: SweepProjection,
|
|
134
|
+
checks: SweepChecks,
|
|
135
|
+
}, CLOSED),
|
|
136
|
+
]);
|
|
137
|
+
const FixCiParameters = Type.Union([
|
|
138
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("collect") }, CLOSED),
|
|
139
|
+
Type.Object({ runId: RouteRunId, action: Type.Literal("publish") }, CLOSED),
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
type UpdateBranchWorkflow = Pick<PullRequestBranchUpdater, "state" | "merge" | "continue" | "publish">;
|
|
143
|
+
type CreateWorkflow = Pick<PullRequestCreator, "state" | "prepare" | "merge" | "continue" | "push" | "publish">;
|
|
144
|
+
type SweepWorkflow = Pick<PullRequestCommentSweep, "start" | "resume" | "show" | "record" | "publish" | "refresh" | "resolve" | "finalize">;
|
|
145
|
+
type FixCiWorkflow = Pick<PullRequestCiFixer, "collect" | "publish">;
|
|
146
|
+
|
|
147
|
+
type WorkflowContextBase = {
|
|
148
|
+
runId: string;
|
|
149
|
+
sessionGeneration: number;
|
|
150
|
+
worktree: string;
|
|
151
|
+
controller: AbortController;
|
|
152
|
+
usedSinceSettlement: boolean;
|
|
153
|
+
queuedPrompt?: WorkflowPromptIdentity;
|
|
154
|
+
conflictRetained: boolean;
|
|
155
|
+
};
|
|
156
|
+
type WorkflowContext =
|
|
157
|
+
| (WorkflowContextBase & { route: "update-branch"; workflow: UpdateBranchWorkflow })
|
|
158
|
+
| (WorkflowContextBase & { route: "create"; base?: string; workflow: CreateWorkflow })
|
|
159
|
+
| (WorkflowContextBase & { route: "sweep"; workflow: SweepWorkflow })
|
|
160
|
+
| (WorkflowContextBase & { route: "fix-ci"; workflow: FixCiWorkflow });
|
|
39
161
|
|
|
40
162
|
type PullRequestExtensionDependencies = {
|
|
41
163
|
loadCurrentPullRequest?: typeof loadCurrentPullRequest;
|
|
42
164
|
hasLocalCommit?: typeof hasLocalCommit;
|
|
43
165
|
createPrCommandHandler?: typeof createPrCommandHandler;
|
|
166
|
+
createBranchUpdater?: (options: UpdateBranchOptions) => UpdateBranchWorkflow;
|
|
167
|
+
createPullRequestCreator?: (options: CreatePullRequestOptions) => CreateWorkflow;
|
|
168
|
+
createCommentSweep?: (options: PullRequestCommentSweepOptions) => SweepWorkflow;
|
|
169
|
+
createCiFixer?: (options: PullRequestCiFixOptions) => FixCiWorkflow;
|
|
170
|
+
canonicalWorktree?: (cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
171
|
+
newRunId?: () => string;
|
|
44
172
|
};
|
|
45
173
|
|
|
174
|
+
async function canonicalWorktree(cwd: string, signal?: AbortSignal): Promise<string> {
|
|
175
|
+
const result = await runChecked(spawnBounded, "git", ["rev-parse", "--show-toplevel"], { cwd, signal });
|
|
176
|
+
const normalized = result.stdout.replace(/\r\n/g, "\n");
|
|
177
|
+
const lines = (normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized).split("\n");
|
|
178
|
+
if (lines.length !== 1 || !lines[0]) throw new Error("Git worktree root resolution returned invalid output");
|
|
179
|
+
return await realpath(lines[0]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function toolResult(value: unknown) {
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text" as const, text: JSON.stringify(value) }],
|
|
185
|
+
details: value,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function matchesWorkflowPrompt(prompt: string, identity: WorkflowPromptIdentity): boolean {
|
|
190
|
+
const tokens = new Set(prompt.split(/\s+/).filter(Boolean));
|
|
191
|
+
const skillName = identity.skill.startsWith("skill:") ? identity.skill.slice("skill:".length) : "";
|
|
192
|
+
const matchesSkill = tokens.has(`/${identity.skill}`) ||
|
|
193
|
+
(skillName !== "" && tokens.has("<skill") && tokens.has(`name="${skillName}"`));
|
|
194
|
+
return matchesSkill && tokens.has(`runId=${identity.runId}`) && tokens.has(`action=${identity.action}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
46
197
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
198
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
199
|
}
|
|
@@ -106,6 +257,12 @@ export default function pullRequestExtension(
|
|
|
106
257
|
const discover = dependencies.loadCurrentPullRequest ?? loadCurrentPullRequest;
|
|
107
258
|
const detectLocalCommit = dependencies.hasLocalCommit ?? hasLocalCommit;
|
|
108
259
|
const createCommandHandler = dependencies.createPrCommandHandler ?? createPrCommandHandler;
|
|
260
|
+
const createBranchUpdater = dependencies.createBranchUpdater ?? ((options) => new PullRequestBranchUpdater(options));
|
|
261
|
+
const createPullRequestCreator = dependencies.createPullRequestCreator ?? ((options) => new PullRequestCreator(options));
|
|
262
|
+
const createCommentSweep = dependencies.createCommentSweep ?? ((options) => new PullRequestCommentSweep(options));
|
|
263
|
+
const createCiFixer = dependencies.createCiFixer ?? ((options) => new PullRequestCiFixer(options));
|
|
264
|
+
const resolveCanonicalWorktree = dependencies.canonicalWorktree ?? canonicalWorktree;
|
|
265
|
+
const newRunId = dependencies.newRunId ?? randomUUID;
|
|
109
266
|
let context: ExtensionContext | undefined;
|
|
110
267
|
let observation: PullRequestObservation | undefined;
|
|
111
268
|
const load: typeof loadCurrentPullRequest = async (api, loadContext, inspectedLocal) => {
|
|
@@ -134,6 +291,7 @@ export default function pullRequestExtension(
|
|
|
134
291
|
let mergeCompleted = false;
|
|
135
292
|
let displayedWidget: PrDisplay | undefined;
|
|
136
293
|
let commandGeneration = 0;
|
|
294
|
+
let workflowContext: WorkflowContext | undefined;
|
|
137
295
|
const activeInvocations = new Map<number, "routing" | "resolved" | "create-workflow" | "workflow">();
|
|
138
296
|
let widgetKind: "presentation" | "routing" = "presentation";
|
|
139
297
|
let routingSpinnerFrame = 0;
|
|
@@ -148,6 +306,202 @@ export default function pullRequestExtension(
|
|
|
148
306
|
routingSpinnerComponent = undefined;
|
|
149
307
|
};
|
|
150
308
|
|
|
309
|
+
const clearWorkflow = (selected: WorkflowContext | undefined): void => {
|
|
310
|
+
if (!selected || workflowContext !== selected) return;
|
|
311
|
+
workflowContext = undefined;
|
|
312
|
+
selected.controller.abort();
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const reserveWorkflow: NonNullable<PrCommandDependencies["reserveWorkflow"]> = async (reservation, ctx, invocation) => {
|
|
316
|
+
if (!invocation) throw new Error("PR workflow command generation is unavailable");
|
|
317
|
+
invocation.assertCurrent();
|
|
318
|
+
const worktree = await resolveCanonicalWorktree(ctx.cwd, ctx.signal);
|
|
319
|
+
invocation.assertCurrent();
|
|
320
|
+
if (workflowContext) throw new Error(`PR workflow ${workflowContext.runId} is still active`);
|
|
321
|
+
const runId = newRunId();
|
|
322
|
+
if (!UUID.test(runId)) throw new Error("PR workflow runId generator returned an invalid UUID");
|
|
323
|
+
const common: WorkflowContextBase = {
|
|
324
|
+
runId,
|
|
325
|
+
sessionGeneration: invocation.sessionGeneration,
|
|
326
|
+
worktree,
|
|
327
|
+
controller: new AbortController(),
|
|
328
|
+
usedSinceSettlement: false,
|
|
329
|
+
conflictRetained: false,
|
|
330
|
+
};
|
|
331
|
+
switch (reservation.route) {
|
|
332
|
+
case "update-branch":
|
|
333
|
+
workflowContext = {
|
|
334
|
+
...common,
|
|
335
|
+
route: "update-branch",
|
|
336
|
+
workflow: createBranchUpdater({
|
|
337
|
+
cwd: worktree,
|
|
338
|
+
authority: reservation.pullRequest,
|
|
339
|
+
signal: common.controller.signal,
|
|
340
|
+
loadCurrentPullRequest: load,
|
|
341
|
+
}),
|
|
342
|
+
};
|
|
343
|
+
break;
|
|
344
|
+
case "create":
|
|
345
|
+
workflowContext = {
|
|
346
|
+
...common,
|
|
347
|
+
route: "create",
|
|
348
|
+
...(reservation.base === undefined ? {} : { base: reservation.base }),
|
|
349
|
+
workflow: createPullRequestCreator({
|
|
350
|
+
cwd: worktree,
|
|
351
|
+
target: reservation.target,
|
|
352
|
+
signal: common.controller.signal,
|
|
353
|
+
loadCurrentPullRequest: load,
|
|
354
|
+
}),
|
|
355
|
+
};
|
|
356
|
+
break;
|
|
357
|
+
case "sweep":
|
|
358
|
+
workflowContext = {
|
|
359
|
+
...common,
|
|
360
|
+
route: "sweep",
|
|
361
|
+
workflow: createCommentSweep({
|
|
362
|
+
cwd: worktree,
|
|
363
|
+
authority: reservation.pullRequest,
|
|
364
|
+
signal: common.controller.signal,
|
|
365
|
+
loadCurrentPullRequest: load,
|
|
366
|
+
}),
|
|
367
|
+
};
|
|
368
|
+
break;
|
|
369
|
+
case "fix-ci":
|
|
370
|
+
workflowContext = {
|
|
371
|
+
...common,
|
|
372
|
+
route: "fix-ci",
|
|
373
|
+
workflow: createCiFixer({
|
|
374
|
+
cwd: worktree,
|
|
375
|
+
authority: reservation.pullRequest,
|
|
376
|
+
signal: common.controller.signal,
|
|
377
|
+
loadCurrentPullRequest: load,
|
|
378
|
+
}),
|
|
379
|
+
};
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
return common.runId;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const markWorkflowPromptQueued: NonNullable<PrCommandDependencies["markWorkflowPromptQueued"]> = (identity, queued) => {
|
|
386
|
+
if (workflowContext?.runId !== identity.runId || workflowContext.route !== identity.route) {
|
|
387
|
+
throw new Error("PR workflow reservation is wrong or stale");
|
|
388
|
+
}
|
|
389
|
+
workflowContext.queuedPrompt = queued ? identity : undefined;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const releaseWorkflow: NonNullable<PrCommandDependencies["releaseWorkflow"]> = (runId, invocation) => {
|
|
393
|
+
const selected = workflowContext;
|
|
394
|
+
if (
|
|
395
|
+
invocation && selected?.runId === runId &&
|
|
396
|
+
selected.sessionGeneration === invocation.sessionGeneration
|
|
397
|
+
) clearWorkflow(selected);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const executeWorkflowAction = async <Route extends WorkflowContext["route"]>(
|
|
401
|
+
runId: string,
|
|
402
|
+
route: Route,
|
|
403
|
+
ctx: ExtensionContext,
|
|
404
|
+
signal: AbortSignal | undefined,
|
|
405
|
+
action: (selected: Extract<WorkflowContext, { route: Route }>) => Promise<unknown>,
|
|
406
|
+
) => {
|
|
407
|
+
signal?.throwIfAborted();
|
|
408
|
+
const selected = workflowContext;
|
|
409
|
+
if (!selected) throw new Error("No PR workflow is active");
|
|
410
|
+
if (selected.runId !== runId) throw new Error("PR workflow runId is wrong or stale");
|
|
411
|
+
if (selected.sessionGeneration !== sessionGeneration) throw new Error("PR workflow session is stale");
|
|
412
|
+
if (selected.route !== route) throw new Error(`PR workflow route is ${selected.route}, not ${route}`);
|
|
413
|
+
const abortRun = () => selected.controller.abort(signal?.reason);
|
|
414
|
+
if (signal?.aborted) abortRun();
|
|
415
|
+
else signal?.addEventListener("abort", abortRun, { once: true });
|
|
416
|
+
try {
|
|
417
|
+
selected.controller.signal.throwIfAborted();
|
|
418
|
+
const worktree = await resolveCanonicalWorktree(ctx.cwd, selected.controller.signal);
|
|
419
|
+
if (workflowContext !== selected || selected.sessionGeneration !== sessionGeneration) {
|
|
420
|
+
throw new Error("PR workflow session changed during validation");
|
|
421
|
+
}
|
|
422
|
+
selected.controller.signal.throwIfAborted();
|
|
423
|
+
if (worktree !== selected.worktree) throw new Error("PR workflow worktree is wrong or stale");
|
|
424
|
+
selected.usedSinceSettlement = true;
|
|
425
|
+
return toolResult(await action(selected as Extract<WorkflowContext, { route: Route }>));
|
|
426
|
+
} finally {
|
|
427
|
+
signal?.removeEventListener("abort", abortRun);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
pi.registerTool({
|
|
432
|
+
name: "pi_pr_update_branch",
|
|
433
|
+
label: "Update PR Branch",
|
|
434
|
+
description: "Run one guarded action for the /pr branch-update route.",
|
|
435
|
+
parameters: UpdateBranchParameters,
|
|
436
|
+
executionMode: "sequential",
|
|
437
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
438
|
+
return executeWorkflowAction(params.runId, "update-branch", ctx, signal, async (selected) => {
|
|
439
|
+
switch (params.action) {
|
|
440
|
+
case "merge": return await selected.workflow.merge();
|
|
441
|
+
case "continue": return await selected.workflow.continue(params.resolvedPaths);
|
|
442
|
+
case "publish": return await selected.workflow.publish();
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
pi.registerTool({
|
|
449
|
+
name: "pi_pr_create",
|
|
450
|
+
label: "Create Pull Request",
|
|
451
|
+
description: "Run one guarded action for the /pr creation route.",
|
|
452
|
+
parameters: CreateParameters,
|
|
453
|
+
executionMode: "sequential",
|
|
454
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
455
|
+
return executeWorkflowAction(params.runId, "create", ctx, signal, async (selected) => {
|
|
456
|
+
switch (params.action) {
|
|
457
|
+
case "prepare": return await selected.workflow.prepare(selected.base);
|
|
458
|
+
case "merge": return await selected.workflow.merge();
|
|
459
|
+
case "continue": return await selected.workflow.continue(params.resolvedPaths);
|
|
460
|
+
case "push": return await selected.workflow.push();
|
|
461
|
+
case "publish": return await selected.workflow.publish(params.title, params.body);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
pi.registerTool({
|
|
468
|
+
name: "pi_pr_sweep",
|
|
469
|
+
label: "Sweep PR Feedback",
|
|
470
|
+
description: "Run one guarded action for the /pr feedback route.",
|
|
471
|
+
parameters: SweepParameters,
|
|
472
|
+
executionMode: "sequential",
|
|
473
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
474
|
+
return executeWorkflowAction(params.runId, "sweep", ctx, signal, async (selected) => {
|
|
475
|
+
switch (params.action) {
|
|
476
|
+
case "start": return await selected.workflow.start();
|
|
477
|
+
case "resume": return await selected.workflow.resume();
|
|
478
|
+
case "show": return await selected.workflow.show(params.guard, params.id);
|
|
479
|
+
case "record": return await selected.workflow.record(params.guard, params.ledger, params.ownedPaths);
|
|
480
|
+
case "publish": return await selected.workflow.publish(params.guard);
|
|
481
|
+
case "refresh": return await selected.workflow.refresh(params.guard);
|
|
482
|
+
case "resolve": return await selected.workflow.resolve(params.guard, params.threadIds);
|
|
483
|
+
case "finalize": return await selected.workflow.finalize(params.guard, params.projection, params.checks);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
pi.registerTool({
|
|
490
|
+
name: "pi_pr_fix_ci",
|
|
491
|
+
label: "Fix PR CI",
|
|
492
|
+
description: "Run one guarded action for the /pr failed-CI route.",
|
|
493
|
+
parameters: FixCiParameters,
|
|
494
|
+
executionMode: "sequential",
|
|
495
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
496
|
+
return executeWorkflowAction(params.runId, "fix-ci", ctx, signal, async (selected) => {
|
|
497
|
+
switch (params.action) {
|
|
498
|
+
case "collect": return await selected.workflow.collect();
|
|
499
|
+
case "publish": return await selected.workflow.publish();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
151
505
|
const setWidget = (ctx: ExtensionContext, display: PrDisplay | undefined): void => {
|
|
152
506
|
stopRoutingSpinner();
|
|
153
507
|
widgetKind = "presentation";
|
|
@@ -270,6 +624,7 @@ export default function pullRequestExtension(
|
|
|
270
624
|
mergeCompleted = false;
|
|
271
625
|
displayedWidget = undefined;
|
|
272
626
|
commandGeneration = 0;
|
|
627
|
+
clearWorkflow(workflowContext);
|
|
273
628
|
activeInvocations.clear();
|
|
274
629
|
stopRoutingSpinner();
|
|
275
630
|
widgetKind = "presentation";
|
|
@@ -372,6 +727,13 @@ export default function pullRequestExtension(
|
|
|
372
727
|
queued = false;
|
|
373
728
|
};
|
|
374
729
|
|
|
730
|
+
pi.on("before_agent_start", (event) => {
|
|
731
|
+
const selected = workflowContext;
|
|
732
|
+
if (selected?.queuedPrompt && matchesWorkflowPrompt(event.prompt, selected.queuedPrompt)) {
|
|
733
|
+
selected.queuedPrompt = undefined;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
375
737
|
pi.on("session_start", async (_event, ctx) => {
|
|
376
738
|
stop();
|
|
377
739
|
const generation = sessionGeneration;
|
|
@@ -388,19 +750,33 @@ export default function pullRequestExtension(
|
|
|
388
750
|
|
|
389
751
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
390
752
|
if (!ctx.hasUI || !ctx.isIdle() || !context) return;
|
|
753
|
+
const selected = workflowContext;
|
|
754
|
+
const helperSettled = selected?.usedSinceSettlement ?? false;
|
|
755
|
+
const queuedHelperPending = selected?.queuedPrompt !== undefined && !helperSettled;
|
|
391
756
|
let workflowSettled = false;
|
|
392
757
|
let createWorkflowSettled = false;
|
|
393
758
|
for (const [invocation, phase] of activeInvocations) {
|
|
394
759
|
if (phase !== "workflow" && phase !== "create-workflow") continue;
|
|
760
|
+
if (queuedHelperPending) continue;
|
|
395
761
|
activeInvocations.delete(invocation);
|
|
396
762
|
workflowSettled = true;
|
|
397
763
|
if (phase === "create-workflow") createWorkflowSettled = true;
|
|
398
764
|
}
|
|
765
|
+
if (selected) {
|
|
766
|
+
const conflictPending = (selected.route === "create" || selected.route === "update-branch") &&
|
|
767
|
+
selected.workflow.state.phase === "conflict-awaiting-user";
|
|
768
|
+
if (!queuedHelperPending) {
|
|
769
|
+
if (helperSettled && conflictPending && !selected.conflictRetained) {
|
|
770
|
+
selected.usedSinceSettlement = false;
|
|
771
|
+
selected.conflictRetained = true;
|
|
772
|
+
} else clearWorkflow(selected);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
399
775
|
const delegatedRefresh = delegatedWorkPending && lastDiscovery !== "inactive";
|
|
400
776
|
delegatedWorkPending = false;
|
|
401
|
-
if (!workflowSettled && !delegatedRefresh) return;
|
|
777
|
+
if (!workflowSettled && !helperSettled && !delegatedRefresh) return;
|
|
402
778
|
cancelRefresh();
|
|
403
|
-
if (createWorkflowSettled) pendingWorkspaceRename = true;
|
|
779
|
+
if (createWorkflowSettled || helperSettled && selected?.route === "create") pendingWorkspaceRename = true;
|
|
404
780
|
await refresh().catch(reportRefreshFailure);
|
|
405
781
|
});
|
|
406
782
|
|
|
@@ -415,23 +791,36 @@ export default function pullRequestExtension(
|
|
|
415
791
|
}
|
|
416
792
|
});
|
|
417
793
|
|
|
418
|
-
const commandHandler = createCommandHandler(pi, {
|
|
794
|
+
const commandHandler = createCommandHandler(pi, {
|
|
795
|
+
loadCurrentPullRequest: load,
|
|
796
|
+
reserveWorkflow,
|
|
797
|
+
markWorkflowPromptQueued,
|
|
798
|
+
releaseWorkflow,
|
|
799
|
+
});
|
|
419
800
|
pi.registerCommand("pr", {
|
|
420
|
-
description: "[
|
|
801
|
+
description: "[--base=<host>/<owner>/<repository>:<ref>] — Run the current branch pull request next step",
|
|
421
802
|
handler: async (args, ctx) => {
|
|
422
803
|
if (!ctx.hasUI || !context) return;
|
|
423
804
|
const generation = sessionGeneration;
|
|
424
805
|
const invocation = ++commandGeneration;
|
|
425
806
|
activeInvocations.set(invocation, "routing");
|
|
426
807
|
reconcileWidget(ctx);
|
|
427
|
-
const routeResolved = (): void => {
|
|
808
|
+
const routeResolved = (_nextStep?: unknown): void => {
|
|
428
809
|
if (sessionGeneration !== generation || activeInvocations.get(invocation) !== "routing") return;
|
|
429
810
|
activeInvocations.set(invocation, "resolved");
|
|
430
811
|
reconcileWidget(ctx);
|
|
431
812
|
};
|
|
813
|
+
const commandInvocation: PrCommandInvocation = Object.assign(routeResolved, {
|
|
814
|
+
sessionGeneration: generation,
|
|
815
|
+
assertCurrent() {
|
|
816
|
+
if (sessionGeneration !== generation) {
|
|
817
|
+
throw new Error("PR command session changed during dispatch");
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
});
|
|
432
821
|
let nextStep: Awaited<ReturnType<typeof commandHandler>>;
|
|
433
822
|
try {
|
|
434
|
-
nextStep = await commandHandler(args, ctx,
|
|
823
|
+
nextStep = await commandHandler(args, ctx, commandInvocation);
|
|
435
824
|
routeResolved();
|
|
436
825
|
} catch (error) {
|
|
437
826
|
if (sessionGeneration === generation) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-pr",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "Run /pr to safely discover or link the current pull request, then create, update, address feedback, fix CI, or merge when ready.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -28,9 +28,11 @@
|
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
31
|
-
"@earendil-works/pi-tui": "^0.85.1"
|
|
31
|
+
"@earendil-works/pi-tui": "^0.85.1",
|
|
32
|
+
"typebox": "^1.3.15"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
35
|
+
"@henryqw/pi-config-store": "^1.1.0",
|
|
34
36
|
"@henryqw/pi-herdr": "^0.4.5"
|
|
35
37
|
},
|
|
36
38
|
"repository": {
|