@henryqw/pi-pr 0.3.7 → 1.0.0
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 +56 -23
- package/docs/pr-routing.svg +127 -0
- package/extensions/pr-command.ts +154 -0
- package/extensions/pr-github.ts +994 -0
- package/extensions/pr-merge.ts +288 -0
- package/extensions/pr-routing.ts +59 -0
- package/extensions/pr-ui.ts +93 -0
- package/extensions/pr.ts +70 -259
- package/package.json +3 -2
- package/skills/pi-pr-comment-sweep/SKILL.md +79 -0
- package/skills/pi-pr-comment-sweep/references/recovery.md +49 -0
- package/skills/pi-pr-comment-sweep/references/thread-triage.md +8 -0
- package/skills/pi-pr-comment-sweep/scripts/pr-feedback.mjs +1672 -0
- package/skills/pi-pr-create/SKILL.md +25 -1
- package/skills/pi-pr-fix-ci/SKILL.md +59 -0
- package/skills/pi-pr-update-branch/SKILL.md +116 -0
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# `@henryqw/pi-pr`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Show the current branch pull request in the Pi footer and use `/pr` to run its next safe step.
|
|
4
4
|
|
|
5
5
|
## Why
|
|
6
6
|
|
|
7
|
-
- **Created for**:
|
|
8
|
-
- **Advantage**: See
|
|
7
|
+
- **Created for**: Check pull-request progress without repeated `gh` commands.
|
|
8
|
+
- **Advantage**: See the current pull request and its next step in one place.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -13,11 +13,13 @@ See the current branch's pull request state and open or create that pull request
|
|
|
13
13
|
pi install npm:@henryqw/pi-pr
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Requires authenticated GitHub CLI
|
|
16
|
+
Requires an authenticated GitHub CLI session (`gh auth login`) and a checkout on GitHub.com or GitHub Enterprise. The PR hostname selects its GitHub API host. It works in generic Pi sessions outside Herdr.
|
|
17
|
+
|
|
18
|
+
The comment sweep resolves its bundled helper and references from the installed package skill path. It does not require an external `jq` executable.
|
|
17
19
|
|
|
18
20
|
## With
|
|
19
21
|
|
|
20
|
-
[`@henryqw/pi-footer`](https://pi.henry.wang/extensions/pi-footer) improves this package by showing
|
|
22
|
+
[`@henryqw/pi-footer`](https://pi.henry.wang/extensions/pi-footer) improves this package by showing its pull-request status in the footer.
|
|
21
23
|
|
|
22
24
|
## Use
|
|
23
25
|
|
|
@@ -27,32 +29,63 @@ If none exists, Pi starts the bundled creation workflow instead.
|
|
|
27
29
|
|
|
28
30
|
| Surface | Type | Purpose |
|
|
29
31
|
| --- | --- | --- |
|
|
30
|
-
| Footer | ui | Show
|
|
31
|
-
|
|
|
32
|
+
| Footer | ui | Show a linked `PR #number` and one plain-language status. |
|
|
33
|
+
| Widget hint | ui | Show at most one hint for the next `/pr` step. |
|
|
34
|
+
|
|
35
|
+
Footer statuses include `N unresolved`, `draft`, `open`, `approved`, `CI running`, `CI failed`, `changes requested`, `base update required`, `merge conflict`, `merge-ready`, `merged`, and `closed`. Colors support the text; they do not carry meaning alone.
|
|
36
|
+
|
|
37
|
+
Use `/pr` without arguments. It reads the current branch pull request and local state, then runs one route.
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
## Routes
|
|
42
|
+
|
|
43
|
+
| Current condition | `/pr` route |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| No current-branch pull request, including no upstream push target | Start pull-request creation. |
|
|
46
|
+
| Base update required or merge conflict | Update from the exact base when the tree is clean and local HEAD equals the PR head. |
|
|
47
|
+
| Changes requested or unresolved review threads | Run the package comment sweep when the same local prerequisite holds. |
|
|
48
|
+
| CI failed | Run the CI fix workflow when the same local prerequisite holds. |
|
|
49
|
+
| No-action state | Report the state without taking action. |
|
|
50
|
+
| Merge-ready pull request | Ask for final confirmation, recheck fresh state, and merge directly if confirmed. |
|
|
32
51
|
|
|
33
|
-
|
|
52
|
+
`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.
|
|
34
53
|
|
|
35
|
-
-
|
|
36
|
-
- `CI failed`, `changes requested`, `merge conflict`, `merged`, and `closed`
|
|
54
|
+
Current-branch discovery matches the exact push repository and ref. It finds a fork-head PR whose base is an upstream repository. A unique historical match uses the exact remote push-ref OID, not local HEAD.
|
|
37
55
|
|
|
38
|
-
|
|
56
|
+
A no-action state includes a draft, merged or closed pull request, running CI, pending review, or blocked merge policy. It also includes a mutating workflow whose tree is dirty or whose local HEAD differs from the PR head.
|
|
39
57
|
|
|
40
|
-
|
|
58
|
+
## Route priority
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
2. `merge conflict`
|
|
44
|
-
3. `changes requested`
|
|
45
|
-
4. `CI failed`
|
|
46
|
-
5. `CI running` for CI progress
|
|
60
|
+
A missing pull request uses creation. For an existing pull request, the first matching condition wins:
|
|
47
61
|
|
|
48
|
-
|
|
62
|
+
1. Merged, closed, or draft: no action.
|
|
63
|
+
2. Base update required or merge conflict. Run only with a clean tree and equal local and PR heads.
|
|
64
|
+
3. Changes requested or unresolved review threads. Apply the same local prerequisite.
|
|
65
|
+
4. CI failure. Apply the same local prerequisite.
|
|
66
|
+
5. Waiting or local safety block: no action.
|
|
67
|
+
6. Merge-ready: allow clean local HEAD equal to or behind the PR head. Confirm, then merge directly.
|
|
49
68
|
|
|
50
|
-
|
|
69
|
+
Ordinary conversation comments do not trigger a route or block a merge. Changes requested and unresolved review threads can select the package comment sweep.
|
|
51
70
|
|
|
52
|
-
|
|
71
|
+
## Refresh
|
|
53
72
|
|
|
54
|
-
|
|
73
|
+
The footer and widget load at session start and poll every 30 seconds. Polling updates presentation only and may be stale. Presentation uses route priority, so draft appears before running CI. `/pr` reads fresh state before routing or merging. The command is authoritative for actions.
|
|
55
74
|
|
|
56
|
-
|
|
75
|
+
## Safety limits
|
|
57
76
|
|
|
58
|
-
|
|
77
|
+
- `/pr` takes no arguments and does not open a browser.
|
|
78
|
+
- It does not run `/done` or `/sweep`.
|
|
79
|
+
- Polling does not auto-triage comments or start a workflow. The package comment sweep runs only when an explicit `/pr` selects it.
|
|
80
|
+
- It does not enable auto-merge or add a merge queue.
|
|
81
|
+
- It does not rebase the local branch, force-push, delete branches, or clean up worktrees.
|
|
82
|
+
- Creation, discovery, and comment-sweep pushes require one unambiguous push URL for the configured destination.
|
|
83
|
+
- Presentation fetches use that exact push URL and exact advertised OID. They do not use shared fetch state.
|
|
84
|
+
- Strict status checks in legacy branch protection or applicable repository rulesets require a base update.
|
|
85
|
+
- Applicable ruleset restrictions intersect repository-wide merge methods. An empty intersection stops the workflow.
|
|
86
|
+
- Before merge, `/pr` fetches the exact head OID from the validated push URL without shared fetch state.
|
|
87
|
+
- A merge, rebase, cherry-pick, revert, or sequencer state blocks direct merge, even when `git status` is empty.
|
|
88
|
+
- Before a comment-sweep push, it revalidates the configured destination, full PR identity, and local HEAD. It pushes the captured OID.
|
|
89
|
+
- An already-published local HEAD needs no second push.
|
|
90
|
+
- Direct merge requires final confirmation and a fresh readiness check.
|
|
91
|
+
- Only authenticated GitHub.com and GitHub Enterprise repositories are supported.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" role="img" aria-labelledby="pr-routing-title pr-routing-desc">
|
|
3
|
+
<title id="pr-routing-title">How /pr picks one route</title>
|
|
4
|
+
<desc id="pr-routing-desc">The /pr command creates a pull request when needed, gates branch update, feedback sweep, and CI repair on a clean worktree with local HEAD equal to the PR head, and permits direct merge when clean local HEAD is equal to or behind the PR head.</desc>
|
|
5
|
+
<defs>
|
|
6
|
+
<style>@import url("https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap");</style>
|
|
7
|
+
<marker id="pr-routing-arrow" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
|
|
8
|
+
<polygon points="0 0, 8 4, 0 8" fill="#4c5665"/>
|
|
9
|
+
</marker>
|
|
10
|
+
<marker id="pr-routing-arrow-accent" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
|
|
11
|
+
<polygon points="0 0, 8 4, 0 8" fill="#1d4ed8"/>
|
|
12
|
+
</marker>
|
|
13
|
+
<marker id="pr-routing-arrow-link" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
|
|
14
|
+
<polygon points="0 0, 8 4, 0 8" fill="#1d4ed8"/>
|
|
15
|
+
</marker>
|
|
16
|
+
</defs>
|
|
17
|
+
|
|
18
|
+
<rect width="1280" height="720" fill="#f0eee9"/>
|
|
19
|
+
|
|
20
|
+
<!-- Header -->
|
|
21
|
+
<text x="80" y="68" fill="#101828" font-size="28" font-weight="400" font-family="'Instrument Serif', serif">How /pr picks one route</text>
|
|
22
|
+
<text x="80" y="88" fill="#4c5665" font-size="8" font-family="'Geist', sans-serif">Fresh state enters a strict, top-down priority ladder.</text>
|
|
23
|
+
<text x="1200" y="48" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.12em">FIRST MATCHING CONDITION WINS</text>
|
|
24
|
+
<text x="1200" y="68" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.12em">AT MOST ONE ROUTE</text>
|
|
25
|
+
|
|
26
|
+
<!-- Continuation arrows, drawn before nodes -->
|
|
27
|
+
<g fill="none" stroke="#4c5665" stroke-width="1.2" marker-end="url(#pr-routing-arrow)">
|
|
28
|
+
<line x1="640" y1="144" x2="640" y2="160"/>
|
|
29
|
+
<line x1="640" y1="208" x2="640" y2="224"/>
|
|
30
|
+
<line x1="640" y1="272" x2="640" y2="288"/>
|
|
31
|
+
<line x1="640" y1="336" x2="640" y2="352"/>
|
|
32
|
+
<line x1="640" y1="400" x2="640" y2="416"/>
|
|
33
|
+
<line x1="640" y1="464" x2="640" y2="480"/>
|
|
34
|
+
<line x1="640" y1="528" x2="640" y2="544"/>
|
|
35
|
+
</g>
|
|
36
|
+
|
|
37
|
+
<!-- Input node -->
|
|
38
|
+
<rect x="240" y="96" width="800" height="48" rx="8" fill="rgba(29,78,216,0.08)" stroke="#1d4ed8" stroke-width="1.2"/>
|
|
39
|
+
<text x="640" y="120" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">/pr reads fresh state</text>
|
|
40
|
+
<text x="640" y="140" fill="#1d4ed8" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.12em">GITHUB + LOCAL STATE</text>
|
|
41
|
+
|
|
42
|
+
<!-- 1 -->
|
|
43
|
+
<rect x="120" y="160" width="1040" height="48" rx="6" fill="#ffffff" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
44
|
+
<line x1="176" y1="168" x2="176" y2="200" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
45
|
+
<line x1="704" y1="168" x2="704" y2="200" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
46
|
+
<text x="148" y="192" fill="#4c5665" font-size="12" font-family="'Geist Mono', monospace" text-anchor="middle">1</text>
|
|
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">No current PR</text>
|
|
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
|
+
<text x="728" y="196" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Create pull request</text>
|
|
51
|
+
<text x="1136" y="196" fill="#4c5665" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end">pi-pr-create</text>
|
|
52
|
+
|
|
53
|
+
<!-- 2 -->
|
|
54
|
+
<rect x="120" y="224" width="1040" height="48" rx="6" fill="#ffffff" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
55
|
+
<line x1="176" y1="232" x2="176" y2="264" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
56
|
+
<line x1="704" y1="232" x2="704" y2="264" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
57
|
+
<text x="148" y="256" fill="#4c5665" font-size="12" font-family="'Geist Mono', monospace" text-anchor="middle">2</text>
|
|
58
|
+
<text x="200" y="240" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">CONDITION</text>
|
|
59
|
+
<text x="200" y="260" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Merged, closed, or draft</text>
|
|
60
|
+
<text x="728" y="240" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">ROUTE</text>
|
|
61
|
+
<text x="728" y="260" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">No action</text>
|
|
62
|
+
<text x="1136" y="260" fill="#4c5665" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end">report current state</text>
|
|
63
|
+
|
|
64
|
+
<!-- 3 -->
|
|
65
|
+
<rect x="120" y="288" width="1040" height="48" rx="6" fill="#ffffff" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
66
|
+
<line x1="176" y1="296" x2="176" y2="328" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
67
|
+
<line x1="704" y1="296" x2="704" y2="328" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
68
|
+
<text x="148" y="320" fill="#4c5665" font-size="12" font-family="'Geist Mono', monospace" text-anchor="middle">3</text>
|
|
69
|
+
<text x="200" y="304" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">CONDITION</text>
|
|
70
|
+
<text x="200" y="324" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Base behind or conflicted</text>
|
|
71
|
+
<text x="728" y="304" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">ROUTE</text>
|
|
72
|
+
<text x="728" y="324" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Update if clean + equal</text>
|
|
73
|
+
<text x="1136" y="324" fill="#4c5665" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end">otherwise no action</text>
|
|
74
|
+
|
|
75
|
+
<!-- 4 -->
|
|
76
|
+
<rect x="120" y="352" width="1040" height="48" rx="6" fill="#ffffff" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
77
|
+
<line x1="176" y1="360" x2="176" y2="392" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
78
|
+
<line x1="704" y1="360" x2="704" y2="392" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
79
|
+
<text x="148" y="384" fill="#4c5665" font-size="12" font-family="'Geist Mono', monospace" text-anchor="middle">4</text>
|
|
80
|
+
<text x="200" y="368" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">CONDITION</text>
|
|
81
|
+
<text x="200" y="388" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Changes requested or unresolved threads</text>
|
|
82
|
+
<text x="728" y="368" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">ROUTE</text>
|
|
83
|
+
<text x="728" y="388" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Sweep if clean + equal</text>
|
|
84
|
+
<text x="1136" y="388" fill="#4c5665" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end">otherwise no action</text>
|
|
85
|
+
|
|
86
|
+
<!-- 5 -->
|
|
87
|
+
<rect x="120" y="416" width="1040" height="48" rx="6" fill="#ffffff" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
88
|
+
<line x1="176" y1="424" x2="176" y2="456" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
89
|
+
<line x1="704" y1="424" x2="704" y2="456" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
90
|
+
<text x="148" y="448" fill="#4c5665" font-size="12" font-family="'Geist Mono', monospace" text-anchor="middle">5</text>
|
|
91
|
+
<text x="200" y="432" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">CONDITION</text>
|
|
92
|
+
<text x="200" y="452" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">CI failed</text>
|
|
93
|
+
<text x="728" y="432" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">ROUTE</text>
|
|
94
|
+
<text x="728" y="452" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Fix CI if clean + equal</text>
|
|
95
|
+
<text x="1136" y="452" fill="#4c5665" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end">otherwise no action</text>
|
|
96
|
+
|
|
97
|
+
<!-- 6 -->
|
|
98
|
+
<rect x="120" y="480" width="1040" height="48" rx="6" fill="#ffffff" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
99
|
+
<line x1="176" y1="488" x2="176" y2="520" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
100
|
+
<line x1="704" y1="488" x2="704" y2="520" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
101
|
+
<text x="148" y="512" fill="#4c5665" font-size="12" font-family="'Geist Mono', monospace" text-anchor="middle">6</text>
|
|
102
|
+
<text x="200" y="496" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">CONDITION</text>
|
|
103
|
+
<text x="200" y="516" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Other waiting or merge-local block</text>
|
|
104
|
+
<text x="728" y="496" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">ROUTE</text>
|
|
105
|
+
<text x="728" y="516" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">No action</text>
|
|
106
|
+
<text x="1136" y="516" fill="#4c5665" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end">CI running, review or policy pending, dirty, ahead, or diverged</text>
|
|
107
|
+
|
|
108
|
+
<!-- 7 -->
|
|
109
|
+
<rect x="120" y="544" width="1040" height="48" rx="6" fill="rgba(29,78,216,0.08)" stroke="#1d4ed8" stroke-width="1.2"/>
|
|
110
|
+
<line x1="176" y1="552" x2="176" y2="584" stroke="rgba(29,78,216,0.32)" stroke-width="1"/>
|
|
111
|
+
<line x1="704" y1="552" x2="704" y2="584" stroke="rgba(29,78,216,0.32)" stroke-width="1"/>
|
|
112
|
+
<text x="148" y="576" fill="#1d4ed8" font-size="12" font-family="'Geist Mono', monospace" text-anchor="middle">7</text>
|
|
113
|
+
<text x="200" y="560" fill="#1d4ed8" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">CONDITION</text>
|
|
114
|
+
<text x="200" y="580" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Merge-ready + clean + equal or behind</text>
|
|
115
|
+
<text x="728" y="560" fill="#1d4ed8" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">ROUTE</text>
|
|
116
|
+
<text x="728" y="580" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Confirm, recheck, merge</text>
|
|
117
|
+
<text x="1136" y="580" fill="#1d4ed8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end">expected-head GraphQL mutation</text>
|
|
118
|
+
|
|
119
|
+
<!-- Legend strip -->
|
|
120
|
+
<line x1="44" y1="624" x2="1236" y2="624" stroke="rgba(16,24,40,0.141)" stroke-width="0.8"/>
|
|
121
|
+
<line x1="80" y1="636" x2="80" y2="664" stroke="#4c5665" stroke-width="1.2" marker-end="url(#pr-routing-arrow)"/>
|
|
122
|
+
<text x="104" y="640" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">DOWNWARD CONNECTOR</text>
|
|
123
|
+
<text x="104" y="664" fill="#101828" font-size="8" font-family="'Geist', sans-serif">Otherwise, continue to the next condition.</text>
|
|
124
|
+
<text x="668" y="660" fill="#101828" font-size="28" font-weight="400" font-family="'Instrument Serif', serif">1×</text>
|
|
125
|
+
<text x="724" y="640" fill="#4c5665" font-size="8" font-weight="500" font-family="'Geist Mono', monospace" letter-spacing="0.12em">ONE-ROUTE RULE</text>
|
|
126
|
+
<text x="724" y="664" fill="#101828" font-size="8" font-family="'Geist', sans-serif">At most one route: first matching condition wins, then /pr stops.</text>
|
|
127
|
+
</svg>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
RegisteredCommand,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
executeGitHubMerge,
|
|
8
|
+
selectMergeMethod,
|
|
9
|
+
} from "./pr-merge.ts";
|
|
10
|
+
import {
|
|
11
|
+
loadCurrentPullRequest,
|
|
12
|
+
type CurrentPullRequest,
|
|
13
|
+
} from "./pr-github.ts";
|
|
14
|
+
import {
|
|
15
|
+
deriveNextStep,
|
|
16
|
+
type NextStep,
|
|
17
|
+
} from "./pr-routing.ts";
|
|
18
|
+
|
|
19
|
+
type WorkflowNextStep = Exclude<NextStep, "none" | "merge">;
|
|
20
|
+
|
|
21
|
+
const WORKFLOWS: Record<WorkflowNextStep, string> = {
|
|
22
|
+
create: "skill:pi-pr-create",
|
|
23
|
+
"update-branch": "skill:pi-pr-update-branch",
|
|
24
|
+
sweep: "skill:pi-pr-comment-sweep",
|
|
25
|
+
"fix-ci": "skill:pi-pr-fix-ci",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type PrCommandPi = Pick<ExtensionAPI, "exec" | "getCommands" | "sendUserMessage">;
|
|
29
|
+
export type PrCommandHandler = RegisteredCommand["handler"];
|
|
30
|
+
|
|
31
|
+
function dispatchWorkflow(pi: PrCommandPi, ctx: ExtensionCommandContext, commandName: string): void {
|
|
32
|
+
const command = pi.getCommands().find((candidate) =>
|
|
33
|
+
candidate.name === commandName &&
|
|
34
|
+
candidate.source === "skill" &&
|
|
35
|
+
candidate.sourceInfo.origin === "package",
|
|
36
|
+
);
|
|
37
|
+
if (!command) throw new Error(`${commandName} failed: bundled workflow is unavailable`);
|
|
38
|
+
|
|
39
|
+
const options = ctx.isIdle()
|
|
40
|
+
? { expandPromptTemplates: true }
|
|
41
|
+
: { deliverAs: "followUp" as const, expandPromptTemplates: true };
|
|
42
|
+
pi.sendUserMessage(`/${command.name}`, options);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function noActionNotification(pullRequest: CurrentPullRequest): { message: string; type: "info" | "warning" } {
|
|
46
|
+
if (pullRequest.lifecycle === "merged" || pullRequest.lifecycle === "closed") {
|
|
47
|
+
return { message: `PR #${pullRequest.number} is ${pullRequest.lifecycle}; no action needed`, type: "info" };
|
|
48
|
+
}
|
|
49
|
+
if (pullRequest.conditions.draft) {
|
|
50
|
+
return { message: `PR #${pullRequest.number} is draft; no action available`, type: "warning" };
|
|
51
|
+
}
|
|
52
|
+
const mutatingWorkflowSelected = pullRequest.conditions.baseUpdateRequired || pullRequest.conditions.conflict ||
|
|
53
|
+
pullRequest.conditions.changesRequested || pullRequest.conditions.unresolvedThreads > 0 ||
|
|
54
|
+
pullRequest.conditions.ci === "failure";
|
|
55
|
+
if (mutatingWorkflowSelected && pullRequest.local.worktree === "dirty") {
|
|
56
|
+
return { message: `PR #${pullRequest.number} is blocked by a dirty worktree`, type: "warning" };
|
|
57
|
+
}
|
|
58
|
+
if (mutatingWorkflowSelected && pullRequest.local.head !== "equal") {
|
|
59
|
+
return { message: `PR #${pullRequest.number} is blocked by local HEAD ${pullRequest.local.head}`, type: "warning" };
|
|
60
|
+
}
|
|
61
|
+
if (pullRequest.conditions.ci === "running") {
|
|
62
|
+
return { message: `PR #${pullRequest.number} is waiting for CI`, type: "warning" };
|
|
63
|
+
}
|
|
64
|
+
if (pullRequest.conditions.review === "pending") {
|
|
65
|
+
return { message: `PR #${pullRequest.number} is waiting for review`, type: "warning" };
|
|
66
|
+
}
|
|
67
|
+
if (pullRequest.conditions.policy === "pending") {
|
|
68
|
+
return { message: `PR #${pullRequest.number} is blocked by merge policy`, type: "warning" };
|
|
69
|
+
}
|
|
70
|
+
if (pullRequest.local.worktree === "dirty") {
|
|
71
|
+
return { message: `PR #${pullRequest.number} is blocked by a dirty worktree`, type: "warning" };
|
|
72
|
+
}
|
|
73
|
+
if (pullRequest.local.head === "ahead" || pullRequest.local.head === "diverged") {
|
|
74
|
+
return { message: `PR #${pullRequest.number} is blocked by local HEAD ${pullRequest.local.head}`, type: "warning" };
|
|
75
|
+
}
|
|
76
|
+
return { message: `PR #${pullRequest.number} has no available action`, type: "warning" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isSameConfirmedMerge(current: CurrentPullRequest, fresh: CurrentPullRequest): boolean {
|
|
80
|
+
return current.id === fresh.id && current.number === fresh.number &&
|
|
81
|
+
current.url.href === fresh.url.href && current.host === fresh.host &&
|
|
82
|
+
current.head.repository === fresh.head.repository &&
|
|
83
|
+
current.head.ref === fresh.head.ref && current.head.oid === fresh.head.oid &&
|
|
84
|
+
current.base.repository === fresh.base.repository &&
|
|
85
|
+
current.base.ref === fresh.base.ref && current.base.oid === fresh.base.oid;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function mergePullRequest(
|
|
89
|
+
pi: PrCommandPi,
|
|
90
|
+
ctx: ExtensionCommandContext,
|
|
91
|
+
current: CurrentPullRequest,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
if (!current.merge) throw new Error(`PR #${current.number} merge failed: merge capabilities are unavailable`);
|
|
94
|
+
const method = selectMergeMethod(current.merge);
|
|
95
|
+
const confirmed = await ctx.ui.confirm(
|
|
96
|
+
`Merge PR #${current.number} with ${method}?`,
|
|
97
|
+
`Merge PR #${current.number} using ${method}.`,
|
|
98
|
+
);
|
|
99
|
+
if (!confirmed) return;
|
|
100
|
+
|
|
101
|
+
await executeGitHubMerge({
|
|
102
|
+
exec: (command, args, options) => pi.exec(command, args, {
|
|
103
|
+
...options,
|
|
104
|
+
signal: ctx.signal,
|
|
105
|
+
timeout: 10_000,
|
|
106
|
+
}),
|
|
107
|
+
cwd: ctx.cwd,
|
|
108
|
+
pullRequestId: current.id,
|
|
109
|
+
hostname: current.host,
|
|
110
|
+
expectedHead: current.head.oid,
|
|
111
|
+
expectedBase: current.base,
|
|
112
|
+
headFetchSource: current.headFetchSource,
|
|
113
|
+
allowedMergeMethods: current.merge.allowedMergeMethods,
|
|
114
|
+
viewerDefaultMergeMethod: current.merge.viewerDefaultMergeMethod,
|
|
115
|
+
revalidateReadiness: async (local) => {
|
|
116
|
+
const fresh = await loadCurrentPullRequest(pi, ctx, local);
|
|
117
|
+
if (!fresh) throw new Error(`PR #${current.number} merge cancelled: pull request is no longer current`);
|
|
118
|
+
if (!isSameConfirmedMerge(current, fresh)) {
|
|
119
|
+
throw new Error(`PR #${current.number} merge cancelled: confirmed pull request context changed`);
|
|
120
|
+
}
|
|
121
|
+
if (deriveNextStep(fresh) !== "merge") {
|
|
122
|
+
throw new Error(`PR #${fresh.number} merge cancelled: pull request is no longer merge-ready`);
|
|
123
|
+
}
|
|
124
|
+
if (!fresh.merge) throw new Error(`PR #${fresh.number} merge failed: merge capabilities are unavailable`);
|
|
125
|
+
const freshMethod = selectMergeMethod(fresh.merge);
|
|
126
|
+
if (freshMethod !== method) {
|
|
127
|
+
throw new Error(`PR #${fresh.number} merge cancelled: merge method changed from ${method} to ${freshMethod}`);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createPrCommandHandler(pi: PrCommandPi): PrCommandHandler {
|
|
134
|
+
return async (args, ctx) => {
|
|
135
|
+
if (args.trim()) throw new Error("/pr does not accept arguments");
|
|
136
|
+
|
|
137
|
+
const current = await loadCurrentPullRequest(pi, ctx);
|
|
138
|
+
const nextStep = deriveNextStep(current);
|
|
139
|
+
if (nextStep === "none") {
|
|
140
|
+
if (current) {
|
|
141
|
+
const notification = noActionNotification(current);
|
|
142
|
+
ctx.ui.notify(notification.message, notification.type);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (nextStep === "merge") {
|
|
147
|
+
if (!current) throw new Error("/pr merge failed: pull request is unavailable");
|
|
148
|
+
await mergePullRequest(pi, ctx, current);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
dispatchWorkflow(pi, ctx, WORKFLOWS[nextStep]);
|
|
153
|
+
};
|
|
154
|
+
}
|