@henryqw/pi-pr 3.0.1 → 3.1.1
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 +17 -5
- package/docs/pr-routing.svg +1 -1
- package/extensions/pr-command.ts +65 -16
- package/extensions/pr-github.ts +860 -134
- package/extensions/pr-routing.ts +39 -4
- package/extensions/pr-ui.ts +82 -11
- package/extensions/pr.ts +88 -23
- package/package.json +2 -2
- package/skills/pi-pr-create/SKILL.md +50 -37
- package/skills/pi-pr-fix-ci/SKILL.md +4 -4
package/README.md
CHANGED
|
@@ -54,7 +54,9 @@ Each footer entry is one linked `PR #number` plus one plain-language status: `N
|
|
|
54
54
|
|
|
55
55
|
| Current condition | `/pr` route |
|
|
56
56
|
| --- | --- |
|
|
57
|
-
| No current-branch pull request,
|
|
57
|
+
| No current-branch pull request, no published matching ref, and safe Git push configuration | Start pull-request creation. |
|
|
58
|
+
| One open pull request inferred from a published matching ref | Confirm the exact `remote/ref`, then link the local branch. |
|
|
59
|
+
| Ambiguous or unsafe discovery | Show the blocked reason and do not mutate Git or GitHub. |
|
|
58
60
|
| 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. |
|
|
59
61
|
| CI failed | Run the CI fix workflow when the same local prerequisite holds. |
|
|
60
62
|
| Changes requested or unresolved review threads | Run the package comment sweep when the same local prerequisite holds. |
|
|
@@ -63,11 +65,17 @@ Each footer entry is one linked `PR #number` plus one plain-language status: `N
|
|
|
63
65
|
|
|
64
66
|
`pi-pr-create` honors an existing configured push target. Without one, it pushes a captured OID to the local branch ref on `origin` and sets upstream.
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
Multiple candidate remotes, multiple matching PRs, OID mismatches, and unsafe Git push configuration block routing. A published ref with no PR also blocks creation. If no candidate ref exists, creation uses only a validated `origin` destination.
|
|
71
|
+
|
|
72
|
+
The creation workflow repeats destination, remote OID, PR, and configuration checks immediately before pushing. It pushes to the saved validated URL, not a mutable remote name. Every push uses the saved remote OID as an exact lease. Existing refs must also be ancestors of the captured local OID. A missing ref uses an empty lease as a create-only compare-and-swap.
|
|
73
|
+
|
|
74
|
+
After a `/pr` create workflow settles, the extension waits for a refresh that finds an open current PR. It then prefixes the Herdr workspace label with `#<number> • `.
|
|
67
75
|
|
|
68
76
|
Failed or empty discovery leaves one rename pending for a later refresh. Closed or merged historical matches do not trigger it.
|
|
69
77
|
|
|
70
|
-
It removes
|
|
78
|
+
It removes repeated leading `#<number> • ` prefixes and legacy trailing ` · PR #<number>` suffixes before adding one current prefix. The remaining workspace name must be non-empty. This requires `HERDR_ENV=1` and a non-empty, trimmed `HERDR_WORKSPACE_ID`.
|
|
71
79
|
|
|
72
80
|
It renames only the workspace. Outside Herdr, it does nothing.
|
|
73
81
|
|
|
@@ -94,7 +102,9 @@ The comment sweep resolves its bundled helper and references from the installed
|
|
|
94
102
|
|
|
95
103
|
### Refresh
|
|
96
104
|
|
|
97
|
-
The footer and widget load at session start.
|
|
105
|
+
The footer and widget load at session start. A directory outside a Git worktree stays silent and does not start polling. The UI shows `PR · status unavailable` for other discovery failures and reports only a generic error.
|
|
106
|
+
|
|
107
|
+
They refresh after local commits, PR creation, pushes, and each dispatched workflow settles. They also refresh after any successful delegated task settles. Active Git worktrees poll every 30 seconds. Polling updates presentation only and may be stale.
|
|
98
108
|
|
|
99
109
|
The create widget stays hidden until the local branch has a commit beyond its creation point. Any displayed widget clears as soon as `/pr` starts. A dispatched workflow keeps it hidden until the agent settles. A direct merge, no-action route, or failed command refreshes the widget when the handler finishes.
|
|
100
110
|
|
|
@@ -106,7 +116,7 @@ Presentation uses route priority, so draft appears before running CI. `/pr` read
|
|
|
106
116
|
- It does not run `/done` or `/sweep`.
|
|
107
117
|
- Polling does not auto-triage comments or start a workflow. The package comment sweep runs only when an explicit `/pr` selects it.
|
|
108
118
|
- It does not enable auto-merge or add a merge queue.
|
|
109
|
-
- It does not rebase the local branch,
|
|
119
|
+
- 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.
|
|
110
120
|
- Creation, discovery, and comment-sweep pushes require one unambiguous push URL for the configured destination.
|
|
111
121
|
- Presentation fetches use that exact push URL and exact advertised OID. They do not use shared fetch state.
|
|
112
122
|
- Strict status checks in legacy branch protection or applicable repository rulesets require a base update.
|
|
@@ -115,6 +125,8 @@ Presentation uses route priority, so draft appears before running CI. `/pr` read
|
|
|
115
125
|
- A merge, rebase, cherry-pick, revert, or sequencer state blocks direct merge, even when `git status` is empty.
|
|
116
126
|
- A branch update resolves the base repository ref directly. It stops if that ref moves before merge or push.
|
|
117
127
|
- Before a comment-sweep push, it revalidates the configured destination, full PR identity, and local HEAD. It pushes the captured OID.
|
|
128
|
+
- CI repair captures the failed-step log tail and runs one narrow local reproducer before editing.
|
|
118
129
|
- An already-published local HEAD needs no second push.
|
|
119
130
|
- Direct merge requires final confirmation and a fresh readiness check.
|
|
131
|
+
- After a successful merge, the create widget stays hidden until a new local commit.
|
|
120
132
|
- Only authenticated GitHub.com and GitHub Enterprise repositories are supported.
|
package/docs/pr-routing.svg
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
<line x1="704" y1="168" x2="704" y2="200" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
46
46
|
<text x="148" y="192" fill="#4c5665" font-size="12" font-family="'Geist Mono', monospace" text-anchor="middle">1</text>
|
|
47
47
|
<text x="200" y="176" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">CONDITION</text>
|
|
48
|
-
<text x="200" y="196" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">
|
|
48
|
+
<text x="200" y="196" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Safe PR absence</text>
|
|
49
49
|
<text x="728" y="176" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">ROUTE</text>
|
|
50
50
|
<text x="728" y="196" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Create pull request</text>
|
|
51
51
|
<text x="1136" y="196" fill="#4c5665" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end">pi-pr-create</text>
|
package/extensions/pr-command.ts
CHANGED
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
selectMergeMethod,
|
|
8
8
|
} from "./pr-merge.ts";
|
|
9
9
|
import {
|
|
10
|
+
linkInferredPullRequest,
|
|
10
11
|
loadCurrentPullRequest,
|
|
12
|
+
samePullRequestSnapshot,
|
|
11
13
|
type CurrentPullRequest,
|
|
12
14
|
} from "./pr-github.ts";
|
|
13
15
|
import {
|
|
@@ -15,7 +17,7 @@ import {
|
|
|
15
17
|
type NextStep,
|
|
16
18
|
} from "./pr-routing.ts";
|
|
17
19
|
|
|
18
|
-
type WorkflowNextStep =
|
|
20
|
+
type WorkflowNextStep = Extract<NextStep, "create" | "update-branch" | "sweep" | "fix-ci">;
|
|
19
21
|
|
|
20
22
|
const WORKFLOWS: Record<WorkflowNextStep, string> = {
|
|
21
23
|
create: "skill:pi-pr-create",
|
|
@@ -27,11 +29,16 @@ const WORKFLOWS: Record<WorkflowNextStep, string> = {
|
|
|
27
29
|
type PrCommandPi = Pick<ExtensionAPI, "exec" | "getCommands" | "sendUserMessage">;
|
|
28
30
|
export type PrCommandHandler = (args: string, ctx: ExtensionCommandContext) => Promise<NextStep>;
|
|
29
31
|
|
|
32
|
+
type PrCommandDependencies = {
|
|
33
|
+
loadCurrentPullRequest?: typeof loadCurrentPullRequest;
|
|
34
|
+
linkInferredPullRequest?: typeof linkInferredPullRequest;
|
|
35
|
+
};
|
|
36
|
+
|
|
30
37
|
function dispatchWorkflow(pi: PrCommandPi, ctx: ExtensionCommandContext, commandName: string): void {
|
|
31
38
|
const command = pi.getCommands().find((candidate) =>
|
|
32
39
|
candidate.name === commandName &&
|
|
33
40
|
candidate.source === "skill" &&
|
|
34
|
-
candidate.sourceInfo.origin === "package"
|
|
41
|
+
candidate.sourceInfo.origin === "package"
|
|
35
42
|
);
|
|
36
43
|
if (!command) throw new Error(`${commandName} failed: bundled workflow is unavailable`);
|
|
37
44
|
|
|
@@ -88,14 +95,15 @@ async function mergePullRequest(
|
|
|
88
95
|
pi: PrCommandPi,
|
|
89
96
|
ctx: ExtensionCommandContext,
|
|
90
97
|
current: CurrentPullRequest,
|
|
91
|
-
|
|
98
|
+
load: typeof loadCurrentPullRequest,
|
|
99
|
+
): Promise<boolean> {
|
|
92
100
|
if (!current.merge) throw new Error(`PR #${current.number} merge failed: merge capabilities are unavailable`);
|
|
93
101
|
const method = selectMergeMethod(current.merge);
|
|
94
102
|
const confirmed = await ctx.ui.confirm(
|
|
95
103
|
`Merge PR #${current.number} with ${method}?`,
|
|
96
104
|
`Merge PR #${current.number} using ${method}.`,
|
|
97
105
|
);
|
|
98
|
-
if (!confirmed) return;
|
|
106
|
+
if (!confirmed) return false;
|
|
99
107
|
|
|
100
108
|
await executeGitHubMerge({
|
|
101
109
|
exec: (command, args, options) => pi.exec(command, args, {
|
|
@@ -112,12 +120,15 @@ async function mergePullRequest(
|
|
|
112
120
|
allowedMergeMethods: current.merge.allowedMergeMethods,
|
|
113
121
|
viewerDefaultMergeMethod: current.merge.viewerDefaultMergeMethod,
|
|
114
122
|
revalidateReadiness: async (local) => {
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
123
|
+
const discovery = await load(pi, ctx, local);
|
|
124
|
+
if (discovery.kind !== "current") {
|
|
125
|
+
throw new Error(`PR #${current.number} merge cancelled: pull request is no longer current`);
|
|
126
|
+
}
|
|
127
|
+
const fresh = discovery.pullRequest;
|
|
117
128
|
if (!isSameConfirmedMerge(current, fresh)) {
|
|
118
129
|
throw new Error(`PR #${current.number} merge cancelled: confirmed pull request context changed`);
|
|
119
130
|
}
|
|
120
|
-
if (deriveNextStep(
|
|
131
|
+
if (deriveNextStep(discovery) !== "merge") {
|
|
121
132
|
throw new Error(`PR #${fresh.number} merge cancelled: pull request is no longer merge-ready`);
|
|
122
133
|
}
|
|
123
134
|
if (!fresh.merge) throw new Error(`PR #${fresh.number} merge failed: merge capabilities are unavailable`);
|
|
@@ -127,28 +138,66 @@ async function mergePullRequest(
|
|
|
127
138
|
}
|
|
128
139
|
},
|
|
129
140
|
});
|
|
141
|
+
return true;
|
|
130
142
|
}
|
|
131
143
|
|
|
132
|
-
|
|
144
|
+
async function linkPullRequest(
|
|
145
|
+
pi: PrCommandPi,
|
|
146
|
+
ctx: ExtensionCommandContext,
|
|
147
|
+
current: CurrentPullRequest,
|
|
148
|
+
load: typeof loadCurrentPullRequest,
|
|
149
|
+
link: typeof linkInferredPullRequest,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
const targetName = `${current.target.remote}/${current.target.ref}`;
|
|
152
|
+
const confirmed = await ctx.ui.confirm(
|
|
153
|
+
`Link pull request branch to ${targetName}?`,
|
|
154
|
+
`Set ${targetName} as the push target for this branch.`,
|
|
155
|
+
);
|
|
156
|
+
if (!confirmed) return;
|
|
157
|
+
|
|
158
|
+
const discovery = await load(pi, ctx);
|
|
159
|
+
if (
|
|
160
|
+
discovery.kind !== "current" ||
|
|
161
|
+
discovery.pullRequest.target.provenance !== "inferred" ||
|
|
162
|
+
!samePullRequestSnapshot(current, discovery.pullRequest)
|
|
163
|
+
) throw new Error("Link branch cancelled: inferred pull request context changed");
|
|
164
|
+
await link(pi, ctx, discovery.pullRequest);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createPrCommandHandler(
|
|
168
|
+
pi: PrCommandPi,
|
|
169
|
+
dependencies: PrCommandDependencies = {},
|
|
170
|
+
): PrCommandHandler {
|
|
171
|
+
const load = dependencies.loadCurrentPullRequest ?? loadCurrentPullRequest;
|
|
172
|
+
const link = dependencies.linkInferredPullRequest ?? linkInferredPullRequest;
|
|
133
173
|
return async (args, ctx) => {
|
|
134
174
|
if (args.trim()) throw new Error("/pr does not accept arguments");
|
|
135
175
|
|
|
136
|
-
const
|
|
137
|
-
const nextStep = deriveNextStep(
|
|
176
|
+
const discovery = await load(pi, ctx);
|
|
177
|
+
const nextStep = deriveNextStep(discovery);
|
|
178
|
+
if (discovery.kind === "inactive") return nextStep;
|
|
179
|
+
if (discovery.kind === "blocked") {
|
|
180
|
+
return nextStep;
|
|
181
|
+
}
|
|
138
182
|
if (nextStep === "none") {
|
|
139
|
-
if (current) {
|
|
140
|
-
const notification = noActionNotification(
|
|
183
|
+
if (discovery.kind === "current") {
|
|
184
|
+
const notification = noActionNotification(discovery.pullRequest);
|
|
141
185
|
ctx.ui.notify(notification.message, notification.type);
|
|
142
186
|
}
|
|
143
187
|
return nextStep;
|
|
144
188
|
}
|
|
145
|
-
if (nextStep === "
|
|
146
|
-
if (
|
|
147
|
-
await
|
|
189
|
+
if (nextStep === "link-branch") {
|
|
190
|
+
if (discovery.kind !== "current") throw new Error("/pr link failed: pull request is unavailable");
|
|
191
|
+
await linkPullRequest(pi, ctx, discovery.pullRequest, load, link);
|
|
148
192
|
return nextStep;
|
|
149
193
|
}
|
|
194
|
+
if (nextStep === "merge") {
|
|
195
|
+
if (discovery.kind !== "current") throw new Error("/pr merge failed: pull request is unavailable");
|
|
196
|
+
return await mergePullRequest(pi, ctx, discovery.pullRequest, load) ? "merge" : "none";
|
|
197
|
+
}
|
|
150
198
|
|
|
151
|
-
|
|
199
|
+
if (!(nextStep in WORKFLOWS)) throw new Error(`/pr cannot dispatch route ${nextStep}`);
|
|
200
|
+
dispatchWorkflow(pi, ctx, WORKFLOWS[nextStep as WorkflowNextStep]);
|
|
152
201
|
return nextStep;
|
|
153
202
|
};
|
|
154
203
|
}
|