@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.
@@ -15,46 +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">;
36
+ export type PrCommandInvocation = ((nextStep: NextStep) => void) & {
37
+ sessionGeneration: number;
38
+ assertCurrent(): void;
39
+ };
30
40
  export type PrCommandHandler = (
31
41
  args: string,
32
42
  ctx: ExtensionCommandContext,
33
- onRouteResolved?: (nextStep: NextStep) => void,
43
+ onRouteResolved?: PrCommandInvocation | ((nextStep: NextStep) => void),
34
44
  ) => Promise<NextStep>;
35
45
 
36
- 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 = {
37
54
  loadCurrentPullRequest?: typeof loadCurrentPullRequest;
38
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;
39
63
  };
40
64
 
41
- function dispatchWorkflow(
42
- pi: PrCommandPi,
43
- ctx: ExtensionCommandContext,
44
- 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>>,
45
75
  instructions: string,
46
- ): 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];
47
89
  const command = pi.getCommands().find((candidate) =>
48
- candidate.name === commandName &&
90
+ candidate.name === workflow.command &&
49
91
  candidate.source === "skill" &&
50
92
  candidate.sourceInfo.origin === "package"
51
93
  );
52
- 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
+ }
53
97
 
54
- const options = ctx.isIdle()
55
- ? { expandPromptTemplates: true }
56
- : { deliverAs: "followUp" as const, expandPromptTemplates: true };
57
- 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
+ }
58
125
  }
59
126
 
60
127
  function noActionNotification(pullRequest: CurrentPullRequest): { message: string; type: "info" | "warning" } {
@@ -64,6 +131,9 @@ function noActionNotification(pullRequest: CurrentPullRequest): { message: strin
64
131
  if (pullRequest.conditions.draft) {
65
132
  return { message: `PR #${pullRequest.number} is draft; no action available`, type: "warning" };
66
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
+ }
67
137
  const mutatingWorkflowSelected = pullRequest.conditions.baseUpdateRequired || pullRequest.conditions.conflict ||
68
138
  pullRequest.conditions.changesRequested || pullRequest.conditions.unresolvedThreads > 0 ||
69
139
  pullRequest.conditions.ci === "failure";
@@ -179,9 +249,18 @@ export function createPrCommandHandler(
179
249
  ): PrCommandHandler {
180
250
  const load = dependencies.loadCurrentPullRequest ?? loadCurrentPullRequest;
181
251
  const link = dependencies.linkInferredPullRequest ?? linkInferredPullRequest;
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 ?? (() => {});
182
257
  return async (args, ctx, onRouteResolved) => {
258
+ const commandInvocation = onRouteResolved && "assertCurrent" in onRouteResolved
259
+ ? onRouteResolved as PrCommandInvocation
260
+ : undefined;
183
261
  const instructions = args.trim();
184
262
  const discovery = await load(pi, ctx);
263
+ commandInvocation?.assertCurrent();
185
264
  const nextStep = deriveNextStep(discovery);
186
265
  onRouteResolved?.(nextStep);
187
266
  if (instructions && !(nextStep in WORKFLOWS)) {
@@ -209,7 +288,9 @@ export function createPrCommandHandler(
209
288
  }
210
289
 
211
290
  if (!(nextStep in WORKFLOWS)) throw new Error(`/pr cannot dispatch route ${nextStep}`);
212
- 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);
213
294
  return nextStep;
214
295
  };
215
296
  }