@henryqw/pi-pr 4.0.2 → 4.0.4

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 CHANGED
@@ -42,11 +42,11 @@ GitHub, use the network, or write files.
42
42
 
43
43
  Run `/pr` in a GitHub checkout. It reads fresh pull request and local state, then runs one route. The PR hostname selects its GitHub API host, and the extension works outside Herdr.
44
44
 
45
- Creation accepts one optional base anchor. Use `/pr --base=<host>/<owner>/<repository>:<ref>`. All other route instructions fail instead of being ignored.
45
+ For creation, put an optional base branch first. For example, run `/pr --base release/2026 Keep the title concise.` The base is a branch name, not a host or repository. Only creation accepts the base and remaining guidance. Other routes reject them instead of ignoring them.
46
46
 
47
47
  | Surface | Type | Purpose |
48
48
  | --- | --- | --- |
49
- | `/pr [--base=<host>/<owner>/<repository>:<ref>]` | command | Run the current pull request's next safe route. |
49
+ | `/pr [--base BRANCH] [creation instructions]` | command | Run the current pull request's next safe route. |
50
50
  | Footer | ui | Show a linked `PR #number` and one plain-language status. |
51
51
  | Widget | ui | Show one action hint or transient routing status. |
52
52
 
@@ -64,7 +64,7 @@ Each footer entry is one linked `PR #number` plus one plain-language status: `N
64
64
 
65
65
  | Current condition | `/pr` route |
66
66
  | --- | --- |
67
- | No current-branch pull request, no published matching ref, and safe Git push configuration | Start pull-request creation. |
67
+ | No current-branch pull request, no published matching ref, safe Git push configuration, and a commit ahead of the selected base | Start pull-request creation. |
68
68
  | One open pull request inferred from a published matching ref | Confirm the exact `remote/ref`, then link the local branch. |
69
69
  | Ambiguous or unsafe discovery | Show the blocked reason and do not mutate Git or GitHub. |
70
70
  | Base update required or merge conflict | Update from the base branch's current target when the tree is clean and local HEAD equals the PR head. |
@@ -72,13 +72,15 @@ Each footer entry is one linked `PR #number` plus one plain-language status: `N
72
72
  | External check or commit status failed | Show `CI failed` as a no-action blocker. |
73
73
  | Changes requested or unresolved review threads | Run the package comment sweep when the same local prerequisite holds. |
74
74
  | No-action state | Report the state without taking action. |
75
- | Merge-ready pull request | Ask for final confirmation, recheck fresh state, and merge directly if confirmed. |
75
+ | Merge-ready pull request | Ask for final confirmation, recheck fresh state, and squash-merge if confirmed. |
76
76
 
77
- `pi-pr-create` accepts an anchored base from `/pr --base=...`. Otherwise, it finds one unique parent from validated `origin` refs and commit history. The parent can be a feature branch.
77
+ `pi-pr-create` selects its base in this order: the leading `/pr --base BRANCH`, one `branch.<branch>.gh-merge-base` value, then the default branch of validated `origin`. It captures the selected base OID and merge-base. Creation requires at least one committed change ahead. Dirty work alone does not enable creation.
78
78
 
79
- It merges the parent's captured commit before validation and push. It resolves clear conflicts and stops when the base or conflict intent is ambiguous.
79
+ The base always comes from validated `origin`. The head may use that repository or a fork with the same GitHub source. Base and head must use the same GitHub host. Other fork relationships stop before mutation.
80
80
 
81
- It honors an existing configured push target. Without one, it pushes a captured OID to the local branch ref on `origin` and sets upstream.
81
+ It merges the captured base commit before validation and push. It resolves clear conflicts and stops when the base or conflict intent is ambiguous.
82
+
83
+ A configured target never changes branch upstream settings. Without a target, the helper pushes the captured OID to the local branch ref on validated `origin` and fetches its tracking ref. It leaves upstream unset. It creates or updates and validates the exact PR before it sets and verifies upstream. A failed setup rolls back only unchanged helper-owned settings. If configuration changed concurrently, it stops without overwriting it. Retrying `publish` resumes setup without another push or PR mutation.
82
84
 
83
85
  Without a configured push target, discovery checks validated remotes for the same branch ref. One exact open PR becomes an inferred target. `/pr` names the exact `remote/ref` and asks before linking it. The extension revalidates the branch, PR, remote OID, and Git configuration before mutation. It rolls back its upstream and remote-tracking changes if final verification fails.
84
86
 
@@ -145,15 +147,15 @@ The GitHub response must match the observed URL, host, repository, head ref, hea
145
147
 
146
148
  ## Limits and recovery
147
149
 
148
- - `/pr` accepts only the optional anchored creation base described above. It does not open a browser.
150
+ - `/pr` accepts creation syntax only as a leading `--base BRANCH`, followed by optional creation guidance. It does not open a browser.
149
151
  - It does not run `/done` or `/sweep`.
150
152
  - Polling does not auto-triage comments or start a workflow. The package comment sweep runs only when an explicit `/pr` selects it.
151
153
  - It does not enable auto-merge or add a merge queue.
152
154
  - It does not rebase the local branch, overwrite concurrent remote updates, delete branches, or clean up worktrees. Creation uses exact leases plus ancestry checks; an empty lease is only an atomic absence check.
153
155
  - Creation, discovery, and comment-sweep pushes require one unambiguous push URL for the configured destination.
154
156
  - Presentation fetches use that exact push URL and exact advertised OID. They do not use shared fetch state.
155
- - Strict status checks in legacy branch protection or applicable repository rulesets require a base update.
156
- - Applicable ruleset restrictions intersect repository-wide merge methods. An empty intersection stops the workflow.
157
+ - A pull request that GitHub reports as behind requires a base update.
158
+ - Direct merges always use squash. GitHub rejects the mutation if repository policy does not allow it.
157
159
  - Before merge, `/pr` fetches the exact head OID from the validated push URL without shared fetch state.
158
160
  - A merge, rebase, cherry-pick, revert, or sequencer state blocks direct merge, even when `git status` is empty.
159
161
  - A branch update resolves the base repository ref directly. It stops if that ref moves before merge or push.
@@ -149,7 +149,6 @@ function cloneAuthority(value: CurrentPullRequest): CurrentPullRequest {
149
149
  base: { ...value.base },
150
150
  head: { ...value.head },
151
151
  target: { ...value.target },
152
- merge: value.merge ? { ...value.merge, allowedMergeMethods: [...value.merge.allowedMergeMethods] } : null,
153
152
  };
154
153
  }
155
154
 
@@ -2,10 +2,7 @@ import type {
2
2
  ExtensionAPI,
3
3
  ExtensionCommandContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
- import {
6
- executeGitHubMerge,
7
- selectMergeMethod,
8
- } from "./pr-merge.ts";
5
+ import { executeGitHubMerge } from "./pr-merge.ts";
9
6
  import {
10
7
  linkInferredPullRequest,
11
8
  loadCurrentPullRequest,
@@ -26,8 +23,6 @@ const WORKFLOWS: Record<WorkflowNextStep, { command: string; action: string }> =
26
23
  sweep: { command: "skill:pi-pr-comment-sweep", action: "start" },
27
24
  "fix-ci": { command: "skill:pi-pr-fix-ci", action: "collect" },
28
25
  };
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
26
  export type WorkflowReservation =
32
27
  | { route: "create"; target: PullRequestTarget; base?: string }
33
28
  | { route: Exclude<WorkflowNextStep, "create">; pullRequest: CurrentPullRequest };
@@ -62,21 +57,30 @@ export type PrCommandDependencies = {
62
57
  releaseWorkflow?: (runId: string, invocation?: PrCommandInvocation) => void;
63
58
  };
64
59
 
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]!;
60
+ type ParsedPrArguments = {
61
+ base?: string;
62
+ instructions: string;
63
+ };
64
+
65
+ function parsePrArguments(args: string): ParsedPrArguments {
66
+ const leading = args.trimStart();
67
+ if (leading.startsWith("--base=")) throw new Error("/pr base syntax is --base <branch>");
68
+ if (!leading.startsWith("--base") || !/^--base(?:\s|$)/.test(leading)) {
69
+ return { instructions: args.trim() };
70
+ }
71
+ const value = /^--base\s+(\S+)/.exec(leading);
72
+ if (!value) throw new Error("/pr --base requires a branch");
73
+ return { base: value[1]!, instructions: leading.slice(value[0].length).trim() };
70
74
  }
71
75
 
72
76
  function workflowReservation(
73
77
  nextStep: WorkflowNextStep,
74
78
  discovery: Awaited<ReturnType<typeof loadCurrentPullRequest>>,
79
+ base: string | undefined,
75
80
  instructions: string,
76
81
  ): WorkflowReservation {
77
82
  if (nextStep === "create") {
78
83
  if (discovery.kind !== "none") throw new Error("/pr create failed: creation target is unavailable");
79
- const base = createBase(instructions);
80
84
  return { route: "create", target: discovery.creationTarget, ...(base === undefined ? {} : { base }) };
81
85
  }
82
86
  if (instructions) throw new Error("The current /pr helper route does not accept instructions");
@@ -104,6 +108,7 @@ async function dispatchWorkflow(
104
108
  reserve: NonNullable<PrCommandDependencies["reserveWorkflow"]>,
105
109
  markPromptQueued: NonNullable<PrCommandDependencies["markWorkflowPromptQueued"]>,
106
110
  release: NonNullable<PrCommandDependencies["releaseWorkflow"]>,
111
+ instructions: string,
107
112
  ): Promise<void> {
108
113
  const workflow = packageWorkflowCommand(pi, route);
109
114
  let runId: string | undefined;
@@ -117,7 +122,7 @@ async function dispatchWorkflow(
117
122
  ? { deliverAs: "followUp" as const, expandPromptTemplates: true }
118
123
  : { expandPromptTemplates: true };
119
124
  invocation?.assertCurrent();
120
- pi.sendUserMessage(`/${identity.skill} runId=${identity.runId} action=${identity.action}`, options);
125
+ pi.sendUserMessage(`/${identity.skill} runId=${identity.runId} action=${identity.action}${instructions ? ` ${instructions}` : ""}`, options);
121
126
  } catch (error) {
122
127
  if (runId !== undefined) release(runId, invocation);
123
128
  throw error;
@@ -176,11 +181,9 @@ async function mergePullRequest(
176
181
  current: CurrentPullRequest,
177
182
  load: typeof loadCurrentPullRequest,
178
183
  ): Promise<boolean> {
179
- if (!current.merge) throw new Error(`PR #${current.number} merge failed: merge capabilities are unavailable`);
180
- const method = selectMergeMethod(current.merge);
181
184
  const confirmed = await ctx.ui.confirm(
182
185
  `Merge PR #${current.number}?`,
183
- `Method: ${method}.`,
186
+ "Method: squash.",
184
187
  );
185
188
  if (!confirmed) return false;
186
189
 
@@ -196,8 +199,6 @@ async function mergePullRequest(
196
199
  expectedHead: current.head.oid,
197
200
  expectedBase: current.base,
198
201
  headFetchSource: current.headFetchSource,
199
- allowedMergeMethods: current.merge.allowedMergeMethods,
200
- viewerDefaultMergeMethod: current.merge.viewerDefaultMergeMethod,
201
202
  revalidateReadiness: async (local) => {
202
203
  const discovery = await load(pi, ctx, local);
203
204
  if (discovery.kind !== "current") {
@@ -210,11 +211,6 @@ async function mergePullRequest(
210
211
  if (deriveNextStep(discovery) !== "merge") {
211
212
  throw new Error(`PR #${fresh.number} merge cancelled: pull request is no longer merge-ready`);
212
213
  }
213
- if (!fresh.merge) throw new Error(`PR #${fresh.number} merge failed: merge capabilities are unavailable`);
214
- const freshMethod = selectMergeMethod(fresh.merge);
215
- if (freshMethod !== method) {
216
- throw new Error(`PR #${fresh.number} merge cancelled: merge method changed from ${method} to ${freshMethod}`);
217
- }
218
214
  },
219
215
  });
220
216
  return true;
@@ -258,11 +254,14 @@ export function createPrCommandHandler(
258
254
  const commandInvocation = onRouteResolved && "assertCurrent" in onRouteResolved
259
255
  ? onRouteResolved as PrCommandInvocation
260
256
  : undefined;
261
- const instructions = args.trim();
262
- const discovery = await load(pi, ctx);
257
+ const { base, instructions } = parsePrArguments(args);
258
+ const discovery = await load(pi, ctx, undefined, undefined, base);
263
259
  commandInvocation?.assertCurrent();
264
260
  const nextStep = deriveNextStep(discovery);
265
261
  onRouteResolved?.(nextStep);
262
+ if (base !== undefined && nextStep !== "create") {
263
+ throw new Error("/pr --base is accepted only for pull request creation");
264
+ }
266
265
  if (instructions && !(nextStep in WORKFLOWS)) {
267
266
  throw new Error("The current /pr route does not accept instructions");
268
267
  }
@@ -289,8 +288,18 @@ export function createPrCommandHandler(
289
288
 
290
289
  if (!(nextStep in WORKFLOWS)) throw new Error(`/pr cannot dispatch route ${nextStep}`);
291
290
  const route = nextStep as WorkflowNextStep;
292
- const reservation = workflowReservation(route, discovery, instructions);
293
- await dispatchWorkflow(pi, ctx, route, reservation, commandInvocation, reserve, markPromptQueued, release);
291
+ const reservation = workflowReservation(route, discovery, base, instructions);
292
+ await dispatchWorkflow(
293
+ pi,
294
+ ctx,
295
+ route,
296
+ reservation,
297
+ commandInvocation,
298
+ reserve,
299
+ markPromptQueued,
300
+ release,
301
+ route === "create" ? instructions : "",
302
+ );
294
303
  return nextStep;
295
304
  };
296
305
  }
@@ -42,10 +42,6 @@ import {
42
42
  type ExecOptions,
43
43
  } from "./pr-execution.ts";
44
44
 
45
- export const PR_COMMENT_SWEEP_ACTIONS = [
46
- "start", "resume", "show", "record", "publish", "refresh", "resolve", "finalize",
47
- ] as const;
48
- export type PrCommentSweepAction = (typeof PR_COMMENT_SWEEP_ACTIONS)[number];
49
45
  export const SWEEP_RECOVERY_MAX_BYTES = 1024 * 1024;
50
46
 
51
47
  const STATE_VERSION = 1;
@@ -140,16 +136,6 @@ export type PullRequestCommentSweepOptions = {
140
136
  newRunId?: () => string;
141
137
  pause?: (milliseconds: number) => Promise<void>;
142
138
  };
143
- export type PrCommentSweepRequest =
144
- | { action: "start" }
145
- | { action: "resume" }
146
- | { action: "show"; guard: SweepRunGuard; id: string }
147
- | { action: "record"; guard: SweepRunGuard; ledger: SweepLedgerEntry[]; ownedPaths?: string[] }
148
- | { action: "publish"; guard: SweepRunGuard }
149
- | { action: "refresh"; guard: SweepRunGuard }
150
- | { action: "resolve"; guard: SweepRunGuard; threadIds: string[] }
151
- | { action: "finalize"; guard: SweepRunGuard; projection: SweepFinalProjection; checks: SweepCheck[] };
152
-
153
139
  function isRecord(value: unknown): value is Record<string, unknown> {
154
140
  return value !== null && typeof value === "object" && !Array.isArray(value);
155
141
  }
@@ -1086,17 +1072,4 @@ export class PullRequestCommentSweep {
1086
1072
  return { kind: "finalized", pullRequestUrl: state.authority.url, head: state.publicationHead, checks: checks.length };
1087
1073
  }, { agentDir: this.agentDir, signal: this.signal });
1088
1074
  }
1089
-
1090
- async action(request: PrCommentSweepRequest): Promise<SweepStatus | FeedbackItem | { kind: "finalized"; pullRequestUrl: string; head: string; checks: number }> {
1091
- switch (request.action) {
1092
- case "start": return await this.start();
1093
- case "resume": return await this.resume();
1094
- case "show": return await this.show(request.guard, request.id);
1095
- case "record": return await this.record(request.guard, request.ledger, request.ownedPaths);
1096
- case "publish": return await this.publish(request.guard);
1097
- case "refresh": return await this.refresh(request.guard);
1098
- case "resolve": return await this.resolve(request.guard, request.threadIds);
1099
- case "finalize": return await this.finalize(request.guard, request.projection, request.checks);
1100
- }
1101
- }
1102
1075
  }