@henryqw/pi-pr 3.1.10 → 4.0.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.
@@ -15,46 +15,121 @@ import {
15
15
  import {
16
16
  deriveNextStep,
17
17
  type NextStep,
18
+ type PullRequestTarget,
18
19
  } from "./pr-routing.ts";
19
20
 
20
- type WorkflowNextStep = Extract<NextStep, "create" | "update-branch" | "sweep" | "fix-ci">;
21
+ export type WorkflowNextStep = Extract<NextStep, "create" | "update-branch" | "sweep" | "fix-ci">;
21
22
 
22
- const WORKFLOWS: Record<WorkflowNextStep, string> = {
23
- create: "skill:pi-pr-create",
24
- "update-branch": "skill:pi-pr-update-branch",
25
- sweep: "skill:pi-pr-comment-sweep",
26
- "fix-ci": "skill:pi-pr-fix-ci",
23
+ const WORKFLOWS: Record<WorkflowNextStep, { command: string; action: string }> = {
24
+ create: { command: "skill:pi-pr-create", action: "prepare" },
25
+ "update-branch": { command: "skill:pi-pr-update-branch", action: "merge" },
26
+ sweep: { command: "skill:pi-pr-comment-sweep", action: "start" },
27
+ "fix-ci": { command: "skill:pi-pr-fix-ci", action: "collect" },
27
28
  };
29
+ export type WorkflowReservation =
30
+ | { route: "create"; target: PullRequestTarget; base?: string }
31
+ | { route: Exclude<WorkflowNextStep, "create">; pullRequest: CurrentPullRequest };
28
32
 
29
33
  type PrCommandPi = Pick<ExtensionAPI, "exec" | "getCommands" | "sendUserMessage">;
34
+ export type PrCommandInvocation = ((nextStep: NextStep) => void) & {
35
+ sessionGeneration: number;
36
+ assertCurrent(): void;
37
+ };
30
38
  export type PrCommandHandler = (
31
39
  args: string,
32
40
  ctx: ExtensionCommandContext,
33
- onRouteResolved?: (nextStep: NextStep) => void,
41
+ onRouteResolved?: PrCommandInvocation | ((nextStep: NextStep) => void),
34
42
  ) => Promise<NextStep>;
35
43
 
36
- type PrCommandDependencies = {
44
+ export type WorkflowPromptIdentity = Readonly<{
45
+ route: WorkflowNextStep;
46
+ skill: string;
47
+ runId: string;
48
+ action: string;
49
+ }>;
50
+
51
+ export type PrCommandDependencies = {
37
52
  loadCurrentPullRequest?: typeof loadCurrentPullRequest;
38
53
  linkInferredPullRequest?: typeof linkInferredPullRequest;
54
+ reserveWorkflow?: (
55
+ reservation: WorkflowReservation,
56
+ ctx: ExtensionCommandContext,
57
+ invocation?: PrCommandInvocation,
58
+ ) => Promise<string>;
59
+ markWorkflowPromptQueued?: (identity: WorkflowPromptIdentity, queued: boolean) => void;
60
+ releaseWorkflow?: (runId: string, invocation?: PrCommandInvocation) => void;
39
61
  };
40
62
 
41
- function dispatchWorkflow(
42
- pi: PrCommandPi,
43
- ctx: ExtensionCommandContext,
44
- commandName: string,
63
+ type ParsedPrArguments = {
64
+ base?: string;
65
+ instructions: string;
66
+ };
67
+
68
+ function parsePrArguments(args: string): ParsedPrArguments {
69
+ const leading = args.trimStart();
70
+ if (leading.startsWith("--base=")) throw new Error("/pr base syntax is --base <branch>");
71
+ if (!leading.startsWith("--base") || !/^--base(?:\s|$)/.test(leading)) {
72
+ return { instructions: args.trim() };
73
+ }
74
+ const value = /^--base\s+(\S+)/.exec(leading);
75
+ if (!value) throw new Error("/pr --base requires a branch");
76
+ return { base: value[1]!, instructions: leading.slice(value[0].length).trim() };
77
+ }
78
+
79
+ function workflowReservation(
80
+ nextStep: WorkflowNextStep,
81
+ discovery: Awaited<ReturnType<typeof loadCurrentPullRequest>>,
82
+ base: string | undefined,
45
83
  instructions: string,
46
- ): void {
84
+ ): WorkflowReservation {
85
+ if (nextStep === "create") {
86
+ if (discovery.kind !== "none") throw new Error("/pr create failed: creation target is unavailable");
87
+ return { route: "create", target: discovery.creationTarget, ...(base === undefined ? {} : { base }) };
88
+ }
89
+ if (instructions) throw new Error("The current /pr helper route does not accept instructions");
90
+ if (discovery.kind !== "current") throw new Error(`/pr ${nextStep} failed: pull request is unavailable`);
91
+ return { route: nextStep, pullRequest: discovery.pullRequest };
92
+ }
93
+
94
+ function packageWorkflowCommand(pi: PrCommandPi, route: WorkflowNextStep) {
95
+ const workflow = WORKFLOWS[route];
47
96
  const command = pi.getCommands().find((candidate) =>
48
- candidate.name === commandName &&
97
+ candidate.name === workflow.command &&
49
98
  candidate.source === "skill" &&
50
99
  candidate.sourceInfo.origin === "package"
51
100
  );
52
- if (!command) throw new Error(`${commandName} failed: bundled workflow is unavailable`);
101
+ if (!command) throw new Error(`${workflow.command} failed: bundled workflow is unavailable`);
102
+ return { command, action: workflow.action };
103
+ }
53
104
 
54
- const options = ctx.isIdle()
55
- ? { expandPromptTemplates: true }
56
- : { deliverAs: "followUp" as const, expandPromptTemplates: true };
57
- pi.sendUserMessage(`/${command.name}${instructions ? ` ${instructions}` : ""}`, options);
105
+ async function dispatchWorkflow(
106
+ pi: PrCommandPi,
107
+ ctx: ExtensionCommandContext,
108
+ route: WorkflowNextStep,
109
+ reservation: WorkflowReservation,
110
+ invocation: PrCommandInvocation | undefined,
111
+ reserve: NonNullable<PrCommandDependencies["reserveWorkflow"]>,
112
+ markPromptQueued: NonNullable<PrCommandDependencies["markWorkflowPromptQueued"]>,
113
+ release: NonNullable<PrCommandDependencies["releaseWorkflow"]>,
114
+ instructions: string,
115
+ ): Promise<void> {
116
+ const workflow = packageWorkflowCommand(pi, route);
117
+ let runId: string | undefined;
118
+ try {
119
+ runId = await reserve(reservation, ctx, invocation);
120
+ invocation?.assertCurrent();
121
+ const queued = !ctx.isIdle();
122
+ const identity = { route, skill: workflow.command.name, runId, action: workflow.action };
123
+ markPromptQueued(identity, queued);
124
+ const options = queued
125
+ ? { deliverAs: "followUp" as const, expandPromptTemplates: true }
126
+ : { expandPromptTemplates: true };
127
+ invocation?.assertCurrent();
128
+ pi.sendUserMessage(`/${identity.skill} runId=${identity.runId} action=${identity.action}${instructions ? ` ${instructions}` : ""}`, options);
129
+ } catch (error) {
130
+ if (runId !== undefined) release(runId, invocation);
131
+ throw error;
132
+ }
58
133
  }
59
134
 
60
135
  function noActionNotification(pullRequest: CurrentPullRequest): { message: string; type: "info" | "warning" } {
@@ -64,6 +139,9 @@ function noActionNotification(pullRequest: CurrentPullRequest): { message: strin
64
139
  if (pullRequest.conditions.draft) {
65
140
  return { message: `PR #${pullRequest.number} is draft; no action available`, type: "warning" };
66
141
  }
142
+ if (pullRequest.conditions.ci === "failure-blocked") {
143
+ return { message: `PR #${pullRequest.number} has a failed CI check that cannot run the CI fix workflow`, type: "warning" };
144
+ }
67
145
  const mutatingWorkflowSelected = pullRequest.conditions.baseUpdateRequired || pullRequest.conditions.conflict ||
68
146
  pullRequest.conditions.changesRequested || pullRequest.conditions.unresolvedThreads > 0 ||
69
147
  pullRequest.conditions.ci === "failure";
@@ -179,11 +257,23 @@ export function createPrCommandHandler(
179
257
  ): PrCommandHandler {
180
258
  const load = dependencies.loadCurrentPullRequest ?? loadCurrentPullRequest;
181
259
  const link = dependencies.linkInferredPullRequest ?? linkInferredPullRequest;
260
+ const reserve = dependencies.reserveWorkflow ?? (async () => {
261
+ throw new Error("/pr workflow tools are unavailable");
262
+ });
263
+ const markPromptQueued = dependencies.markWorkflowPromptQueued ?? (() => {});
264
+ const release = dependencies.releaseWorkflow ?? (() => {});
182
265
  return async (args, ctx, onRouteResolved) => {
183
- const instructions = args.trim();
184
- const discovery = await load(pi, ctx);
266
+ const commandInvocation = onRouteResolved && "assertCurrent" in onRouteResolved
267
+ ? onRouteResolved as PrCommandInvocation
268
+ : undefined;
269
+ const { base, instructions } = parsePrArguments(args);
270
+ const discovery = await load(pi, ctx, undefined, undefined, base);
271
+ commandInvocation?.assertCurrent();
185
272
  const nextStep = deriveNextStep(discovery);
186
273
  onRouteResolved?.(nextStep);
274
+ if (base !== undefined && nextStep !== "create") {
275
+ throw new Error("/pr --base is accepted only for pull request creation");
276
+ }
187
277
  if (instructions && !(nextStep in WORKFLOWS)) {
188
278
  throw new Error("The current /pr route does not accept instructions");
189
279
  }
@@ -209,7 +299,19 @@ export function createPrCommandHandler(
209
299
  }
210
300
 
211
301
  if (!(nextStep in WORKFLOWS)) throw new Error(`/pr cannot dispatch route ${nextStep}`);
212
- dispatchWorkflow(pi, ctx, WORKFLOWS[nextStep as WorkflowNextStep], instructions);
302
+ const route = nextStep as WorkflowNextStep;
303
+ const reservation = workflowReservation(route, discovery, base, instructions);
304
+ await dispatchWorkflow(
305
+ pi,
306
+ ctx,
307
+ route,
308
+ reservation,
309
+ commandInvocation,
310
+ reserve,
311
+ markPromptQueued,
312
+ release,
313
+ route === "create" ? instructions : "",
314
+ );
213
315
  return nextStep;
214
316
  };
215
317
  }