@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 +12 -10
- package/extensions/pr-ci.ts +0 -1
- package/extensions/pr-command.ts +36 -27
- package/extensions/pr-comment-sweep.ts +0 -27
- package/extensions/pr-create.ts +76 -230
- package/extensions/pr-execution.ts +28 -163
- package/extensions/pr-github.ts +339 -193
- package/extensions/pr-merge.ts +24 -111
- package/extensions/pr-routing.ts +6 -2
- package/extensions/pr-ui.ts +1 -2
- package/extensions/pr-update-branch.ts +28 -90
- package/extensions/pr.ts +36 -58
- package/package.json +6 -2
- package/skills/pi-pr-create/SKILL.md +5 -5
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
75
|
+
| Merge-ready pull request | Ask for final confirmation, recheck fresh state, and squash-merge if confirmed. |
|
|
76
76
|
|
|
77
|
-
`pi-pr-create`
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
156
|
-
-
|
|
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.
|
package/extensions/pr-ci.ts
CHANGED
|
@@ -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
|
|
package/extensions/pr-command.ts
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
}
|