@henryqw/pi-pr 6.0.8 → 6.2.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/README.md +151 -247
- package/docs/pr-routing.svg +13 -13
- package/extensions/pr-command.ts +68 -94
- package/extensions/pr-comment-sweep.ts +245 -53
- package/extensions/pr-create.ts +50 -80
- package/extensions/pr-feedback-attention.ts +69 -0
- package/extensions/pr-feedback.ts +23 -1
- package/extensions/pr-publish-work.ts +143 -0
- package/extensions/pr-routing.ts +4 -44
- package/extensions/pr-ui.ts +2 -0
- package/extensions/pr-update-branch.ts +187 -63
- package/extensions/pr.ts +154 -50
- package/package.json +1 -1
- package/skills/pi-pr-comment-sweep/SKILL.md +9 -46
- package/skills/pi-pr-comment-sweep/references/recovery.md +5 -24
- package/skills/pi-pr-comment-sweep/references/thread-triage.md +3 -5
- package/skills/pi-pr-create/SKILL.md +4 -6
- package/skills/pi-pr-publish-work/SKILL.md +12 -0
- package/skills/pi-pr-update-branch/SKILL.md +5 -7
package/extensions/pr-command.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ExtensionAPI,
|
|
3
|
-
|
|
3
|
+
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { executeGitHubMerge } from "./pr-merge.ts";
|
|
6
|
+
import { needsFeedbackAttention } from "./pr-feedback-attention.ts";
|
|
6
7
|
import {
|
|
7
8
|
linkInferredPullRequest,
|
|
8
9
|
loadCurrentPullRequest,
|
|
@@ -11,35 +12,34 @@ import {
|
|
|
11
12
|
} from "./pr-github.ts";
|
|
12
13
|
import {
|
|
13
14
|
deriveNextStep,
|
|
14
|
-
deriveRouteDecision,
|
|
15
|
-
type FeedbackRouteBlocker,
|
|
16
15
|
type NextStep,
|
|
17
16
|
type PullRequestTarget,
|
|
18
|
-
type RouteIntent,
|
|
19
17
|
} from "./pr-routing.ts";
|
|
20
18
|
|
|
21
|
-
type WorkflowNextStep = Extract<NextStep, "create" | "update-branch" | "sweep" | "fix-ci">;
|
|
19
|
+
type WorkflowNextStep = Extract<NextStep, "create" | "publish-work" | "update-branch" | "sweep" | "fix-ci">;
|
|
22
20
|
|
|
23
21
|
const WORKFLOWS: Record<WorkflowNextStep, string> = {
|
|
24
22
|
create: "skill:pi-pr-create",
|
|
23
|
+
"publish-work": "skill:pi-pr-publish-work",
|
|
25
24
|
"update-branch": "skill:pi-pr-update-branch",
|
|
26
25
|
sweep: "skill:pi-pr-comment-sweep",
|
|
27
26
|
"fix-ci": "skill:pi-pr-fix-ci",
|
|
28
27
|
};
|
|
29
28
|
type WorkflowReservation =
|
|
30
|
-
| { route: "create"; target: PullRequestTarget
|
|
29
|
+
| { route: "create"; target: PullRequestTarget }
|
|
31
30
|
| { route: Exclude<WorkflowNextStep, "create">; pullRequest: CurrentPullRequest };
|
|
32
|
-
type WorkflowLaunchAction = "prepare" | "
|
|
31
|
+
type WorkflowLaunchAction = "prepare" | "inspect" | "rebase" | "start" | "resume" | "collect";
|
|
33
32
|
type WorkflowReservationResult = { runId: string; action: WorkflowLaunchAction };
|
|
34
33
|
|
|
35
34
|
type PrCommandPi = Pick<ExtensionAPI, "exec" | "getCommands" | "sendUserMessage">;
|
|
36
35
|
export type PrCommandInvocation = ((nextStep: NextStep) => void) & {
|
|
37
36
|
sessionGeneration: number;
|
|
38
37
|
assertCurrent(): void;
|
|
38
|
+
completedRoutes?: ReadonlySet<string>;
|
|
39
39
|
};
|
|
40
40
|
export type PrCommandHandler = (
|
|
41
41
|
args: string,
|
|
42
|
-
ctx:
|
|
42
|
+
ctx: ExtensionContext,
|
|
43
43
|
onRouteResolved?: PrCommandInvocation | ((nextStep: NextStep) => void),
|
|
44
44
|
) => Promise<NextStep>;
|
|
45
45
|
|
|
@@ -52,67 +52,27 @@ export type WorkflowPromptIdentity = Readonly<{
|
|
|
52
52
|
|
|
53
53
|
export type PrCommandDependencies = {
|
|
54
54
|
loadCurrentPullRequest?: typeof loadCurrentPullRequest;
|
|
55
|
+
needsFeedbackAttention?: typeof needsFeedbackAttention;
|
|
55
56
|
linkInferredPullRequest?: typeof linkInferredPullRequest;
|
|
57
|
+
inspectSweepRecovery?: (pullRequest: CurrentPullRequest, ctx: ExtensionContext) => Promise<boolean>;
|
|
58
|
+
inspectBranchRecovery?: (pullRequest: CurrentPullRequest, ctx: ExtensionContext) => Promise<boolean>;
|
|
56
59
|
reserveWorkflow?: (
|
|
57
60
|
reservation: WorkflowReservation,
|
|
58
|
-
ctx:
|
|
61
|
+
ctx: ExtensionContext,
|
|
59
62
|
invocation?: PrCommandInvocation,
|
|
60
63
|
) => Promise<WorkflowReservationResult>;
|
|
61
64
|
markWorkflowPromptQueued?: (identity: WorkflowPromptIdentity, queued: boolean) => void;
|
|
62
65
|
releaseWorkflow?: (runId: string, invocation?: PrCommandInvocation) => void;
|
|
63
66
|
};
|
|
64
67
|
|
|
65
|
-
type ParsedPrArguments = {
|
|
66
|
-
base?: string;
|
|
67
|
-
intent: RouteIntent;
|
|
68
|
-
instructions: string;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
function parsePrArguments(args: string): ParsedPrArguments {
|
|
72
|
-
const leading = args.trimStart();
|
|
73
|
-
const trimmed = leading.trim();
|
|
74
|
-
if (leading.startsWith("--feedback=")) throw new Error("/pr feedback syntax is --feedback");
|
|
75
|
-
if (/^--feedback(?:\s|$)/.test(leading)) {
|
|
76
|
-
if (trimmed !== "--feedback") throw new Error("/pr --feedback cannot be combined with other options or instructions");
|
|
77
|
-
return { intent: "feedback", instructions: "" };
|
|
78
|
-
}
|
|
79
|
-
if (/(?:^|\s)--feedback(?=\s|=|$)/.test(leading)) {
|
|
80
|
-
throw new Error("/pr --feedback cannot be combined with other options or instructions");
|
|
81
|
-
}
|
|
82
|
-
if (leading.startsWith("--base=")) throw new Error("/pr base syntax is --base <branch>");
|
|
83
|
-
if (/^--base(?:\s|$)/.test(leading)) {
|
|
84
|
-
const value = /^--base\s+(\S+)/.exec(leading);
|
|
85
|
-
if (!value) throw new Error("/pr --base requires a branch");
|
|
86
|
-
const instructions = leading.slice(value[0].length).trim();
|
|
87
|
-
if (instructions.startsWith("--")) throw new Error(`Unknown /pr option: ${instructions.split(/\s+/, 1)[0]}`);
|
|
88
|
-
return { base: value[1]!, intent: "automatic", instructions };
|
|
89
|
-
}
|
|
90
|
-
if (leading.startsWith("--")) throw new Error(`Unknown /pr option: ${leading.split(/\s+/, 1)[0]}`);
|
|
91
|
-
return { intent: "automatic", instructions: args.trim() };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function feedbackBlockerMessage(blocker: FeedbackRouteBlocker): string {
|
|
95
|
-
switch (blocker.kind) {
|
|
96
|
-
case "discovery-blocked": return "/pr --feedback is blocked because pull request discovery is unsafe";
|
|
97
|
-
case "pull-request-unavailable": return "/pr --feedback requires a current pull request";
|
|
98
|
-
case "pull-request-not-open": return "/pr --feedback requires an open pull request";
|
|
99
|
-
case "target-not-configured": return "/pr --feedback requires a configured pull request target; run /pr to link the branch first";
|
|
100
|
-
case "worktree-dirty": return "/pr --feedback is blocked by a dirty worktree";
|
|
101
|
-
case "head-not-equal": return `/pr --feedback is blocked by local HEAD ${blocker.relation}`;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
68
|
function workflowReservation(
|
|
106
69
|
nextStep: WorkflowNextStep,
|
|
107
70
|
discovery: Awaited<ReturnType<typeof loadCurrentPullRequest>>,
|
|
108
|
-
base: string | undefined,
|
|
109
|
-
instructions: string,
|
|
110
71
|
): WorkflowReservation {
|
|
111
72
|
if (nextStep === "create") {
|
|
112
73
|
if (discovery.kind !== "none") throw new Error("/pr create failed: creation target is unavailable");
|
|
113
|
-
return { route: "create", target: discovery.creationTarget
|
|
74
|
+
return { route: "create", target: discovery.creationTarget };
|
|
114
75
|
}
|
|
115
|
-
if (instructions) throw new Error("The current /pr helper route does not accept instructions");
|
|
116
76
|
if (discovery.kind !== "current") throw new Error(`/pr ${nextStep} failed: pull request is unavailable`);
|
|
117
77
|
return { route: nextStep, pullRequest: discovery.pullRequest };
|
|
118
78
|
}
|
|
@@ -130,14 +90,13 @@ function packageWorkflowCommand(pi: PrCommandPi, route: WorkflowNextStep) {
|
|
|
130
90
|
|
|
131
91
|
async function dispatchWorkflow(
|
|
132
92
|
pi: PrCommandPi,
|
|
133
|
-
ctx:
|
|
93
|
+
ctx: ExtensionContext,
|
|
134
94
|
route: WorkflowNextStep,
|
|
135
95
|
reservation: WorkflowReservation,
|
|
136
96
|
invocation: PrCommandInvocation | undefined,
|
|
137
97
|
reserve: NonNullable<PrCommandDependencies["reserveWorkflow"]>,
|
|
138
98
|
markPromptQueued: NonNullable<PrCommandDependencies["markWorkflowPromptQueued"]>,
|
|
139
99
|
release: NonNullable<PrCommandDependencies["releaseWorkflow"]>,
|
|
140
|
-
instructions: string,
|
|
141
100
|
): Promise<void> {
|
|
142
101
|
const workflow = packageWorkflowCommand(pi, route);
|
|
143
102
|
let runId: string | undefined;
|
|
@@ -152,7 +111,7 @@ async function dispatchWorkflow(
|
|
|
152
111
|
? { deliverAs: "followUp" as const, expandPromptTemplates: true }
|
|
153
112
|
: { expandPromptTemplates: true };
|
|
154
113
|
invocation?.assertCurrent();
|
|
155
|
-
pi.sendUserMessage(`/${identity.skill} runId=${identity.runId} action=${identity.action}
|
|
114
|
+
pi.sendUserMessage(`/${identity.skill} runId=${identity.runId} action=${identity.action}`, options);
|
|
156
115
|
} catch (error) {
|
|
157
116
|
if (runId !== undefined) release(runId, invocation);
|
|
158
117
|
throw error;
|
|
@@ -169,7 +128,7 @@ function noActionNotification(pullRequest: CurrentPullRequest): { message: strin
|
|
|
169
128
|
if (pullRequest.conditions.ci === "failure-blocked") {
|
|
170
129
|
return { message: `PR #${pullRequest.number} has a failed CI check that cannot run the CI fix workflow`, type: "warning" };
|
|
171
130
|
}
|
|
172
|
-
const mutatingWorkflowSelected = pullRequest.conditions.
|
|
131
|
+
const mutatingWorkflowSelected = pullRequest.conditions.conflict ||
|
|
173
132
|
pullRequest.conditions.changesRequested || pullRequest.conditions.unresolvedThreads > 0 ||
|
|
174
133
|
pullRequest.conditions.ci === "failure";
|
|
175
134
|
if (mutatingWorkflowSelected && pullRequest.local.worktree === "dirty") {
|
|
@@ -207,16 +166,11 @@ function isSameConfirmedMerge(current: CurrentPullRequest, fresh: CurrentPullReq
|
|
|
207
166
|
|
|
208
167
|
async function mergePullRequest(
|
|
209
168
|
pi: PrCommandPi,
|
|
210
|
-
ctx:
|
|
169
|
+
ctx: ExtensionContext,
|
|
211
170
|
current: CurrentPullRequest,
|
|
212
171
|
load: typeof loadCurrentPullRequest,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
`Merge PR #${current.number}?`,
|
|
216
|
-
"Method: squash.",
|
|
217
|
-
);
|
|
218
|
-
if (!confirmed) return false;
|
|
219
|
-
|
|
172
|
+
needsFeedback: typeof needsFeedbackAttention,
|
|
173
|
+
): Promise<void> {
|
|
220
174
|
await executeGitHubMerge({
|
|
221
175
|
exec: (command, args, options) => pi.exec(command, args, {
|
|
222
176
|
...options,
|
|
@@ -241,32 +195,27 @@ async function mergePullRequest(
|
|
|
241
195
|
if (deriveNextStep(discovery) !== "merge") {
|
|
242
196
|
throw new Error(`PR #${fresh.number} merge cancelled: pull request is no longer merge-ready`);
|
|
243
197
|
}
|
|
198
|
+
if (await needsFeedback(fresh, { cwd: ctx.cwd, signal: ctx.signal, load })) {
|
|
199
|
+
throw new Error(`PR #${fresh.number} merge cancelled: new feedback needs review`);
|
|
200
|
+
}
|
|
244
201
|
},
|
|
245
202
|
});
|
|
246
|
-
return true;
|
|
247
203
|
}
|
|
248
204
|
|
|
249
205
|
async function linkPullRequest(
|
|
250
206
|
pi: PrCommandPi,
|
|
251
|
-
ctx:
|
|
207
|
+
ctx: ExtensionContext,
|
|
252
208
|
current: CurrentPullRequest,
|
|
253
209
|
load: typeof loadCurrentPullRequest,
|
|
254
210
|
link: typeof linkInferredPullRequest,
|
|
255
|
-
): Promise<
|
|
256
|
-
const targetName = `${current.target.remote}/${current.target.ref}`;
|
|
257
|
-
const confirmed = await ctx.ui.confirm(
|
|
258
|
-
`Link pull request branch to ${targetName}?`,
|
|
259
|
-
`Set ${targetName} as the push target for this branch.`,
|
|
260
|
-
);
|
|
261
|
-
if (!confirmed) return;
|
|
262
|
-
|
|
211
|
+
): Promise<CurrentPullRequest> {
|
|
263
212
|
const discovery = await load(pi, ctx);
|
|
264
213
|
if (
|
|
265
214
|
discovery.kind !== "current" ||
|
|
266
215
|
discovery.pullRequest.target.provenance !== "inferred" ||
|
|
267
216
|
!samePullRequestSnapshot(current, discovery.pullRequest)
|
|
268
217
|
) throw new Error("Link branch cancelled: inferred pull request context changed");
|
|
269
|
-
await link(pi, ctx, discovery.pullRequest);
|
|
218
|
+
return await link(pi, ctx, discovery.pullRequest);
|
|
270
219
|
}
|
|
271
220
|
|
|
272
221
|
export function createPrCommandHandler(
|
|
@@ -274,32 +223,51 @@ export function createPrCommandHandler(
|
|
|
274
223
|
dependencies: PrCommandDependencies = {},
|
|
275
224
|
): PrCommandHandler {
|
|
276
225
|
const load = dependencies.loadCurrentPullRequest ?? loadCurrentPullRequest;
|
|
226
|
+
const needsFeedback = dependencies.needsFeedbackAttention ?? needsFeedbackAttention;
|
|
277
227
|
const link = dependencies.linkInferredPullRequest ?? linkInferredPullRequest;
|
|
228
|
+
const inspectSweepRecovery = dependencies.inspectSweepRecovery ?? (async () => false);
|
|
229
|
+
const inspectBranchRecovery = dependencies.inspectBranchRecovery ?? (async () => false);
|
|
278
230
|
const reserve = dependencies.reserveWorkflow ?? (async () => {
|
|
279
231
|
throw new Error("/pr workflow tools are unavailable");
|
|
280
232
|
});
|
|
281
233
|
const markPromptQueued = dependencies.markWorkflowPromptQueued ?? (() => {});
|
|
282
234
|
const release = dependencies.releaseWorkflow ?? (() => {});
|
|
283
|
-
|
|
235
|
+
const handle = async (
|
|
236
|
+
args: string, ctx: ExtensionContext,
|
|
237
|
+
onRouteResolved?: PrCommandInvocation | ((nextStep: NextStep) => void),
|
|
238
|
+
linkedAuthority?: CurrentPullRequest,
|
|
239
|
+
): Promise<NextStep> => {
|
|
284
240
|
const commandInvocation = onRouteResolved && "assertCurrent" in onRouteResolved
|
|
285
241
|
? onRouteResolved as PrCommandInvocation
|
|
286
242
|
: undefined;
|
|
287
|
-
|
|
288
|
-
const discovery = await load(pi, ctx
|
|
243
|
+
if (args.trim()) throw new Error("/pr does not accept arguments");
|
|
244
|
+
const discovery = await load(pi, ctx);
|
|
289
245
|
commandInvocation?.assertCurrent();
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
throw new Error(feedbackBlockerMessage(decision.blocker));
|
|
246
|
+
if (linkedAuthority && (discovery.kind !== "current" || discovery.pullRequest.target.provenance !== "configured" ||
|
|
247
|
+
!isSameConfirmedMerge(linkedAuthority, discovery.pullRequest))) {
|
|
248
|
+
throw new Error("Link branch continuation cancelled: configured pull request context changed");
|
|
294
249
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
250
|
+
let nextStep = deriveNextStep(discovery);
|
|
251
|
+
if (discovery.kind === "current" && discovery.pullRequest.lifecycle === "open" &&
|
|
252
|
+
!discovery.pullRequest.conditions.draft && discovery.pullRequest.target.provenance === "configured" &&
|
|
253
|
+
await inspectBranchRecovery(discovery.pullRequest, ctx)) {
|
|
254
|
+
nextStep = "update-branch";
|
|
255
|
+
commandInvocation?.assertCurrent();
|
|
256
|
+
}
|
|
257
|
+
if (nextStep !== "update-branch" && discovery.kind === "current" && discovery.pullRequest.lifecycle === "open" &&
|
|
258
|
+
!discovery.pullRequest.conditions.draft && discovery.pullRequest.target.provenance === "configured" &&
|
|
259
|
+
await inspectSweepRecovery(discovery.pullRequest, ctx)) {
|
|
260
|
+
nextStep = "sweep";
|
|
261
|
+
commandInvocation?.assertCurrent();
|
|
299
262
|
}
|
|
300
|
-
if (
|
|
301
|
-
|
|
263
|
+
if (discovery.kind === "current" && (nextStep === "merge" || nextStep === "none") &&
|
|
264
|
+
discovery.pullRequest.lifecycle === "open" && !discovery.pullRequest.conditions.draft &&
|
|
265
|
+
discovery.pullRequest.target.provenance === "configured" &&
|
|
266
|
+
discovery.pullRequest.local.worktree === "clean" && discovery.pullRequest.local.head === "equal") {
|
|
267
|
+
if (await needsFeedback(discovery.pullRequest, { cwd: ctx.cwd, signal: ctx.signal, load })) nextStep = "sweep";
|
|
268
|
+
commandInvocation?.assertCurrent();
|
|
302
269
|
}
|
|
270
|
+
onRouteResolved?.(nextStep);
|
|
303
271
|
if (discovery.kind === "inactive") return nextStep;
|
|
304
272
|
if (discovery.kind === "blocked") {
|
|
305
273
|
return nextStep;
|
|
@@ -313,17 +281,23 @@ export function createPrCommandHandler(
|
|
|
313
281
|
}
|
|
314
282
|
if (nextStep === "link-branch") {
|
|
315
283
|
if (discovery.kind !== "current") throw new Error("/pr link failed: pull request is unavailable");
|
|
316
|
-
await linkPullRequest(pi, ctx, discovery.pullRequest, load, link);
|
|
317
|
-
|
|
284
|
+
const linked = await linkPullRequest(pi, ctx, discovery.pullRequest, load, link);
|
|
285
|
+
if (linked.target.provenance !== "configured") throw new Error("Link branch failed: target was not configured");
|
|
286
|
+
return await handle("", ctx, onRouteResolved, linked);
|
|
318
287
|
}
|
|
319
288
|
if (nextStep === "merge") {
|
|
320
289
|
if (discovery.kind !== "current") throw new Error("/pr merge failed: pull request is unavailable");
|
|
321
|
-
|
|
290
|
+
await mergePullRequest(pi, ctx, discovery.pullRequest, load, needsFeedback);
|
|
291
|
+
return "merge";
|
|
322
292
|
}
|
|
323
293
|
|
|
324
294
|
if (!(nextStep in WORKFLOWS)) throw new Error(`/pr cannot dispatch route ${nextStep}`);
|
|
325
295
|
const route = nextStep as WorkflowNextStep;
|
|
326
|
-
|
|
296
|
+
if (commandInvocation?.completedRoutes?.has(route)) {
|
|
297
|
+
ctx.ui.notify(`PR ${route} already ran; inspect fresh state before retrying`, "warning");
|
|
298
|
+
return "none";
|
|
299
|
+
}
|
|
300
|
+
const reservation = workflowReservation(route, discovery);
|
|
327
301
|
await dispatchWorkflow(
|
|
328
302
|
pi,
|
|
329
303
|
ctx,
|
|
@@ -333,8 +307,8 @@ export function createPrCommandHandler(
|
|
|
333
307
|
reserve,
|
|
334
308
|
markPromptQueued,
|
|
335
309
|
release,
|
|
336
|
-
route === "create" ? instructions : "",
|
|
337
310
|
);
|
|
338
311
|
return nextStep;
|
|
339
312
|
};
|
|
313
|
+
return handle;
|
|
340
314
|
}
|