@henryqw/pi-pr 3.1.9 → 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.
@@ -15,42 +15,113 @@ 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
+ const CREATE_BASE = /^--base=([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+:[^\s\0]+)$/;
30
+
31
+ export type WorkflowReservation =
32
+ | { route: "create"; target: PullRequestTarget; base?: string }
33
+ | { route: Exclude<WorkflowNextStep, "create">; pullRequest: CurrentPullRequest };
28
34
 
29
35
  type PrCommandPi = Pick<ExtensionAPI, "exec" | "getCommands" | "sendUserMessage">;
30
- export type PrCommandHandler = (args: string, ctx: ExtensionCommandContext) => Promise<NextStep>;
36
+ export type PrCommandInvocation = ((nextStep: NextStep) => void) & {
37
+ sessionGeneration: number;
38
+ assertCurrent(): void;
39
+ };
40
+ export type PrCommandHandler = (
41
+ args: string,
42
+ ctx: ExtensionCommandContext,
43
+ onRouteResolved?: PrCommandInvocation | ((nextStep: NextStep) => void),
44
+ ) => Promise<NextStep>;
31
45
 
32
- type PrCommandDependencies = {
46
+ export type WorkflowPromptIdentity = Readonly<{
47
+ route: WorkflowNextStep;
48
+ skill: string;
49
+ runId: string;
50
+ action: string;
51
+ }>;
52
+
53
+ export type PrCommandDependencies = {
33
54
  loadCurrentPullRequest?: typeof loadCurrentPullRequest;
34
55
  linkInferredPullRequest?: typeof linkInferredPullRequest;
56
+ reserveWorkflow?: (
57
+ reservation: WorkflowReservation,
58
+ ctx: ExtensionCommandContext,
59
+ invocation?: PrCommandInvocation,
60
+ ) => Promise<string>;
61
+ markWorkflowPromptQueued?: (identity: WorkflowPromptIdentity, queued: boolean) => void;
62
+ releaseWorkflow?: (runId: string, invocation?: PrCommandInvocation) => void;
35
63
  };
36
64
 
37
- function dispatchWorkflow(
38
- pi: PrCommandPi,
39
- ctx: ExtensionCommandContext,
40
- commandName: string,
65
+ function createBase(instructions: string): string | undefined {
66
+ if (!instructions) return undefined;
67
+ const match = CREATE_BASE.exec(instructions);
68
+ if (!match) throw new Error("PR creation accepts only --base=<host>/<owner>/<repository>:<ref>");
69
+ return match[1]!;
70
+ }
71
+
72
+ function workflowReservation(
73
+ nextStep: WorkflowNextStep,
74
+ discovery: Awaited<ReturnType<typeof loadCurrentPullRequest>>,
41
75
  instructions: string,
42
- ): void {
76
+ ): WorkflowReservation {
77
+ if (nextStep === "create") {
78
+ if (discovery.kind !== "none") throw new Error("/pr create failed: creation target is unavailable");
79
+ const base = createBase(instructions);
80
+ return { route: "create", target: discovery.creationTarget, ...(base === undefined ? {} : { base }) };
81
+ }
82
+ if (instructions) throw new Error("The current /pr helper route does not accept instructions");
83
+ if (discovery.kind !== "current") throw new Error(`/pr ${nextStep} failed: pull request is unavailable`);
84
+ return { route: nextStep, pullRequest: discovery.pullRequest };
85
+ }
86
+
87
+ function packageWorkflowCommand(pi: PrCommandPi, route: WorkflowNextStep) {
88
+ const workflow = WORKFLOWS[route];
43
89
  const command = pi.getCommands().find((candidate) =>
44
- candidate.name === commandName &&
90
+ candidate.name === workflow.command &&
45
91
  candidate.source === "skill" &&
46
92
  candidate.sourceInfo.origin === "package"
47
93
  );
48
- if (!command) throw new Error(`${commandName} failed: bundled workflow is unavailable`);
94
+ if (!command) throw new Error(`${workflow.command} failed: bundled workflow is unavailable`);
95
+ return { command, action: workflow.action };
96
+ }
49
97
 
50
- const options = ctx.isIdle()
51
- ? { expandPromptTemplates: true }
52
- : { deliverAs: "followUp" as const, expandPromptTemplates: true };
53
- pi.sendUserMessage(`/${command.name}${instructions ? ` ${instructions}` : ""}`, options);
98
+ async function dispatchWorkflow(
99
+ pi: PrCommandPi,
100
+ ctx: ExtensionCommandContext,
101
+ route: WorkflowNextStep,
102
+ reservation: WorkflowReservation,
103
+ invocation: PrCommandInvocation | undefined,
104
+ reserve: NonNullable<PrCommandDependencies["reserveWorkflow"]>,
105
+ markPromptQueued: NonNullable<PrCommandDependencies["markWorkflowPromptQueued"]>,
106
+ release: NonNullable<PrCommandDependencies["releaseWorkflow"]>,
107
+ ): Promise<void> {
108
+ const workflow = packageWorkflowCommand(pi, route);
109
+ let runId: string | undefined;
110
+ try {
111
+ runId = await reserve(reservation, ctx, invocation);
112
+ invocation?.assertCurrent();
113
+ const queued = !ctx.isIdle();
114
+ const identity = { route, skill: workflow.command.name, runId, action: workflow.action };
115
+ markPromptQueued(identity, queued);
116
+ const options = queued
117
+ ? { deliverAs: "followUp" as const, expandPromptTemplates: true }
118
+ : { expandPromptTemplates: true };
119
+ invocation?.assertCurrent();
120
+ pi.sendUserMessage(`/${identity.skill} runId=${identity.runId} action=${identity.action}`, options);
121
+ } catch (error) {
122
+ if (runId !== undefined) release(runId, invocation);
123
+ throw error;
124
+ }
54
125
  }
55
126
 
56
127
  function noActionNotification(pullRequest: CurrentPullRequest): { message: string; type: "info" | "warning" } {
@@ -60,6 +131,9 @@ function noActionNotification(pullRequest: CurrentPullRequest): { message: strin
60
131
  if (pullRequest.conditions.draft) {
61
132
  return { message: `PR #${pullRequest.number} is draft; no action available`, type: "warning" };
62
133
  }
134
+ if (pullRequest.conditions.ci === "failure-blocked") {
135
+ return { message: `PR #${pullRequest.number} has a failed CI check that cannot run the CI fix workflow`, type: "warning" };
136
+ }
63
137
  const mutatingWorkflowSelected = pullRequest.conditions.baseUpdateRequired || pullRequest.conditions.conflict ||
64
138
  pullRequest.conditions.changesRequested || pullRequest.conditions.unresolvedThreads > 0 ||
65
139
  pullRequest.conditions.ci === "failure";
@@ -175,10 +249,20 @@ export function createPrCommandHandler(
175
249
  ): PrCommandHandler {
176
250
  const load = dependencies.loadCurrentPullRequest ?? loadCurrentPullRequest;
177
251
  const link = dependencies.linkInferredPullRequest ?? linkInferredPullRequest;
178
- return async (args, ctx) => {
252
+ const reserve = dependencies.reserveWorkflow ?? (async () => {
253
+ throw new Error("/pr workflow tools are unavailable");
254
+ });
255
+ const markPromptQueued = dependencies.markWorkflowPromptQueued ?? (() => {});
256
+ const release = dependencies.releaseWorkflow ?? (() => {});
257
+ return async (args, ctx, onRouteResolved) => {
258
+ const commandInvocation = onRouteResolved && "assertCurrent" in onRouteResolved
259
+ ? onRouteResolved as PrCommandInvocation
260
+ : undefined;
179
261
  const instructions = args.trim();
180
262
  const discovery = await load(pi, ctx);
263
+ commandInvocation?.assertCurrent();
181
264
  const nextStep = deriveNextStep(discovery);
265
+ onRouteResolved?.(nextStep);
182
266
  if (instructions && !(nextStep in WORKFLOWS)) {
183
267
  throw new Error("The current /pr route does not accept instructions");
184
268
  }
@@ -204,7 +288,9 @@ export function createPrCommandHandler(
204
288
  }
205
289
 
206
290
  if (!(nextStep in WORKFLOWS)) throw new Error(`/pr cannot dispatch route ${nextStep}`);
207
- dispatchWorkflow(pi, ctx, WORKFLOWS[nextStep as WorkflowNextStep], instructions);
291
+ const route = nextStep as WorkflowNextStep;
292
+ const reservation = workflowReservation(route, discovery, instructions);
293
+ await dispatchWorkflow(pi, ctx, route, reservation, commandInvocation, reserve, markPromptQueued, release);
208
294
  return nextStep;
209
295
  };
210
296
  }