@henryqw/pi-pr 2.1.0 → 2.1.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.
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ The comment sweep resolves its bundled helper and references from the installed
|
|
|
14
14
|
|
|
15
15
|
## Works with
|
|
16
16
|
|
|
17
|
+
**Requires.** [`@henryqw/pi-herdr`](https://pi.henry.wang/extensions/pi-herdr) is the shared Herdr CLI client. It installs with this package.
|
|
18
|
+
|
|
17
19
|
**Improves.** [`@henryqw/pi-footer`](https://pi.henry.wang/extensions/pi-footer) shows current-branch pull-request status in the footer.
|
|
18
20
|
|
|
19
21
|
## Use
|
|
@@ -39,13 +41,23 @@ Each footer entry is one linked `PR #number` plus one plain-language status: `N
|
|
|
39
41
|
| --- | --- |
|
|
40
42
|
| No current-branch pull request, including no upstream push target | Start pull-request creation. |
|
|
41
43
|
| 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. |
|
|
42
|
-
| Changes requested or unresolved review threads | Run the package comment sweep when the same local prerequisite holds. |
|
|
43
44
|
| CI failed | Run the CI fix workflow when the same local prerequisite holds. |
|
|
45
|
+
| Changes requested or unresolved review threads | Run the package comment sweep when the same local prerequisite holds. |
|
|
44
46
|
| No-action state | Report the state without taking action. |
|
|
45
47
|
| Merge-ready pull request | Ask for final confirmation, recheck fresh state, and merge directly if confirmed. |
|
|
46
48
|
|
|
47
49
|
`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.
|
|
48
50
|
|
|
51
|
+
After a `/pr` create workflow settles, the extension waits for a refresh that finds an open current PR. It then ends the Herdr workspace label with one ` · PR #<number>` suffix.
|
|
52
|
+
|
|
53
|
+
Failed or empty discovery leaves one rename pending for a later refresh. Closed or merged historical matches do not trigger it.
|
|
54
|
+
|
|
55
|
+
It removes every stale trailing PR suffix before adding the current one. This requires `HERDR_ENV=1` and a non-empty, trimmed `HERDR_WORKSPACE_ID`.
|
|
56
|
+
|
|
57
|
+
It renames only the workspace. Outside Herdr, it does nothing.
|
|
58
|
+
|
|
59
|
+
If Herdr lookup, JSON validation, or rename fails, the PR and normal UI refresh remain available. Each Herdr command has a 10-second timeout. The extension warns with `Herdr workspace rename failed: <error>`.
|
|
60
|
+
|
|
49
61
|
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.
|
|
50
62
|
|
|
51
63
|
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.
|
|
@@ -56,8 +68,8 @@ A missing pull request uses creation. For an existing pull request, the first ma
|
|
|
56
68
|
|
|
57
69
|
1. Merged, closed, or draft: no action.
|
|
58
70
|
2. Base update required or merge conflict. Run only with a clean tree and equal local and PR heads.
|
|
59
|
-
3.
|
|
60
|
-
4.
|
|
71
|
+
3. CI failure. Apply the same local prerequisite.
|
|
72
|
+
4. Changes requested or unresolved review threads. Apply the same local prerequisite.
|
|
61
73
|
5. Waiting or local safety block: no action.
|
|
62
74
|
6. Merge-ready: allow clean local HEAD equal to or behind the PR head. Confirm, then merge directly.
|
|
63
75
|
|
package/docs/pr-routing.svg
CHANGED
|
@@ -78,9 +78,9 @@
|
|
|
78
78
|
<line x1="704" y1="360" x2="704" y2="392" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
79
79
|
<text x="148" y="384" fill="#4c5665" font-size="12" font-family="'Geist Mono', monospace" text-anchor="middle">4</text>
|
|
80
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">
|
|
81
|
+
<text x="200" y="388" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">CI failed</text>
|
|
82
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">
|
|
83
|
+
<text x="728" y="388" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Fix CI if clean + equal</text>
|
|
84
84
|
<text x="1136" y="388" fill="#4c5665" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end">otherwise no action</text>
|
|
85
85
|
|
|
86
86
|
<!-- 5 -->
|
|
@@ -89,9 +89,9 @@
|
|
|
89
89
|
<line x1="704" y1="424" x2="704" y2="456" stroke="rgba(16,24,40,0.141)" stroke-width="1"/>
|
|
90
90
|
<text x="148" y="448" fill="#4c5665" font-size="12" font-family="'Geist Mono', monospace" text-anchor="middle">5</text>
|
|
91
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">
|
|
92
|
+
<text x="200" y="452" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Changes requested or unresolved threads</text>
|
|
93
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">
|
|
94
|
+
<text x="728" y="452" fill="#101828" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Sweep if clean + equal</text>
|
|
95
95
|
<text x="1136" y="452" fill="#4c5665" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end">otherwise no action</text>
|
|
96
96
|
|
|
97
97
|
<!-- 6 -->
|
package/extensions/pr-routing.ts
CHANGED
|
@@ -45,10 +45,10 @@ export function deriveNextStep(pullRequest: PullRequest | null): NextStep {
|
|
|
45
45
|
if (conditions.baseUpdateRequired || conditions.conflict) {
|
|
46
46
|
return localMutationSafe(local) ? "update-branch" : "none";
|
|
47
47
|
}
|
|
48
|
+
if (conditions.ci === "failure") return localMutationSafe(local) ? "fix-ci" : "none";
|
|
48
49
|
if (conditions.changesRequested || conditions.unresolvedThreads > 0) {
|
|
49
50
|
return localMutationSafe(local) ? "sweep" : "none";
|
|
50
51
|
}
|
|
51
|
-
if (conditions.ci === "failure") return localMutationSafe(local) ? "fix-ci" : "none";
|
|
52
52
|
if (
|
|
53
53
|
conditions.ci === "running" ||
|
|
54
54
|
conditions.review === "pending" ||
|
package/extensions/pr-ui.ts
CHANGED
|
@@ -38,9 +38,9 @@ function footerStatus(input: PrDisplayInput, nextStep: NextStep): Pick<PrFooter,
|
|
|
38
38
|
if (conditions.draft) return { text: "draft", color: "warning" };
|
|
39
39
|
if (conditions.conflict) return { text: "merge conflict", color: "error" };
|
|
40
40
|
if (conditions.baseUpdateRequired) return { text: "base update required", color: "warning" };
|
|
41
|
+
if (conditions.ci === "failure") return { text: "CI failed", color: "error" };
|
|
41
42
|
if (conditions.unresolvedThreads > 0) return { text: `${conditions.unresolvedThreads} unresolved`, color: "warning" };
|
|
42
43
|
if (conditions.changesRequested) return { text: "changes requested", color: "error" };
|
|
43
|
-
if (conditions.ci === "failure") return { text: "CI failed", color: "error" };
|
|
44
44
|
if (conditions.ci === "running") return { text: "CI running", color: "warning" };
|
|
45
45
|
if (nextStep === "merge") return { text: "merge-ready", color: "success" };
|
|
46
46
|
if (input.approved) return { text: "approved", color: "success" };
|
package/extensions/pr.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type ExtensionAPI,
|
|
4
4
|
type ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { createHerdrClient } from "@henryqw/pi-herdr";
|
|
6
7
|
import { createPrCommandHandler } from "./pr-command.ts";
|
|
7
8
|
import {
|
|
8
9
|
hasLocalCommit,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
} from "./pr-ui.ts";
|
|
17
18
|
|
|
18
19
|
const POLL_INTERVAL_MS = 30_000;
|
|
20
|
+
const HERDR_TIMEOUT_MS = 10_000;
|
|
19
21
|
const UI_KEY = "pi-pr";
|
|
20
22
|
const GH_PR_CREATE = /(?:^|[;&|]\s*|\n\s*)gh\s+pr\s+create(?=\s|$|[;&|])/;
|
|
21
23
|
const GIT_COMMIT = /(?:^|[;&|]\s*|\n\s*)git\s+commit(?=\s|$|[;&|])/;
|
|
@@ -27,10 +29,54 @@ type PullRequestExtensionDependencies = {
|
|
|
27
29
|
createPrCommandHandler?: typeof createPrCommandHandler;
|
|
28
30
|
};
|
|
29
31
|
|
|
32
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
33
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseWorkspaceLabel(response: Record<string, unknown>, workspaceId: string): string {
|
|
37
|
+
const result = response.result;
|
|
38
|
+
const workspace = isRecord(result) && isRecord(result.workspace) ? result.workspace : undefined;
|
|
39
|
+
if (!workspace || workspace.workspace_id !== workspaceId) {
|
|
40
|
+
throw new Error("workspace get returned a different workspace_id");
|
|
41
|
+
}
|
|
42
|
+
const label = workspace.label;
|
|
43
|
+
if (typeof label !== "string" || !label.trim()) {
|
|
44
|
+
throw new Error("workspace get returned an empty label");
|
|
45
|
+
}
|
|
46
|
+
return label;
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
export default function pullRequestExtension(
|
|
31
50
|
pi: ExtensionAPI,
|
|
32
51
|
dependencies: PullRequestExtensionDependencies = {},
|
|
33
52
|
): void {
|
|
53
|
+
const herdr = createHerdrClient(pi.exec.bind(pi));
|
|
54
|
+
const renameHerdrWorkspace = async (
|
|
55
|
+
cwd: string,
|
|
56
|
+
signal: AbortSignal,
|
|
57
|
+
workspaceId: string,
|
|
58
|
+
pullRequestNumber: number,
|
|
59
|
+
): Promise<void> => {
|
|
60
|
+
signal.throwIfAborted();
|
|
61
|
+
const current = await herdr.json(["workspace", "get", workspaceId], {
|
|
62
|
+
cwd,
|
|
63
|
+
signal,
|
|
64
|
+
timeout: HERDR_TIMEOUT_MS,
|
|
65
|
+
});
|
|
66
|
+
signal.throwIfAborted();
|
|
67
|
+
const label = parseWorkspaceLabel(current, workspaceId);
|
|
68
|
+
const normalized = `${label.replace(/(?: · PR #[1-9][0-9]*)+$/, "")} · PR #${pullRequestNumber}`;
|
|
69
|
+
if (normalized === label) return;
|
|
70
|
+
|
|
71
|
+
signal.throwIfAborted();
|
|
72
|
+
await herdr.run(["workspace", "rename", workspaceId, normalized], {
|
|
73
|
+
cwd,
|
|
74
|
+
signal,
|
|
75
|
+
timeout: HERDR_TIMEOUT_MS,
|
|
76
|
+
});
|
|
77
|
+
signal.throwIfAborted();
|
|
78
|
+
};
|
|
79
|
+
|
|
34
80
|
const load = dependencies.loadCurrentPullRequest ?? loadCurrentPullRequest;
|
|
35
81
|
const detectLocalCommit = dependencies.hasLocalCommit ?? hasLocalCommit;
|
|
36
82
|
const createCommandHandler = dependencies.createPrCommandHandler ?? createPrCommandHandler;
|
|
@@ -40,9 +86,10 @@ export default function pullRequestExtension(
|
|
|
40
86
|
let active: AbortController | undefined;
|
|
41
87
|
let queued = false;
|
|
42
88
|
let refreshFailureReported = false;
|
|
89
|
+
let pendingWorkspaceRename = false;
|
|
43
90
|
let displayedWidget: PrDisplay | undefined;
|
|
44
91
|
let commandGeneration = 0;
|
|
45
|
-
const activeInvocations = new Map<number, "routing" | "workflow">();
|
|
92
|
+
const activeInvocations = new Map<number, "routing" | "create-workflow" | "workflow">();
|
|
46
93
|
|
|
47
94
|
const setWidget = (ctx: ExtensionContext, display: PrDisplay | undefined): void => {
|
|
48
95
|
if (display?.widget === undefined) {
|
|
@@ -80,6 +127,7 @@ export default function pullRequestExtension(
|
|
|
80
127
|
context = undefined;
|
|
81
128
|
queued = false;
|
|
82
129
|
refreshFailureReported = false;
|
|
130
|
+
pendingWorkspaceRename = false;
|
|
83
131
|
displayedWidget = undefined;
|
|
84
132
|
commandGeneration = 0;
|
|
85
133
|
activeInvocations.clear();
|
|
@@ -101,6 +149,15 @@ export default function pullRequestExtension(
|
|
|
101
149
|
}
|
|
102
150
|
};
|
|
103
151
|
|
|
152
|
+
const reportHerdrRenameFailure = (ctx: ExtensionContext, error: unknown): void => {
|
|
153
|
+
try {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
ctx.ui.notify(`Herdr workspace rename failed: ${message.slice(0, 500)}`, "warning");
|
|
156
|
+
} catch (reportError) {
|
|
157
|
+
console.error("Herdr workspace rename failed and could not be reported", error, reportError);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
104
161
|
const refresh = async (): Promise<void> => {
|
|
105
162
|
const ctx = context;
|
|
106
163
|
if (!ctx) return;
|
|
@@ -128,6 +185,20 @@ export default function pullRequestExtension(
|
|
|
128
185
|
if (controller.signal.aborted || sessionGeneration !== generation) return;
|
|
129
186
|
render(ctx, pullRequest, localCommit);
|
|
130
187
|
refreshFailureReported = false;
|
|
188
|
+
|
|
189
|
+
if (pendingWorkspaceRename && pullRequest?.lifecycle === "open") {
|
|
190
|
+
pendingWorkspaceRename = false;
|
|
191
|
+
const workspaceId = process.env.HERDR_WORKSPACE_ID?.trim();
|
|
192
|
+
if (process.env.HERDR_ENV === "1" && workspaceId) {
|
|
193
|
+
try {
|
|
194
|
+
await renameHerdrWorkspace(ctx.cwd, controller.signal, workspaceId, pullRequest.number);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (!controller.signal.aborted && sessionGeneration === generation) {
|
|
197
|
+
reportHerdrRenameFailure(ctx, error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
131
202
|
} finally {
|
|
132
203
|
if (active !== controller) return;
|
|
133
204
|
active = undefined;
|
|
@@ -162,13 +233,16 @@ export default function pullRequestExtension(
|
|
|
162
233
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
163
234
|
if (!ctx.hasUI || !ctx.isIdle() || !context) return;
|
|
164
235
|
let workflowSettled = false;
|
|
236
|
+
let createWorkflowSettled = false;
|
|
165
237
|
for (const [invocation, phase] of activeInvocations) {
|
|
166
|
-
if (phase !== "workflow") continue;
|
|
238
|
+
if (phase !== "workflow" && phase !== "create-workflow") continue;
|
|
167
239
|
activeInvocations.delete(invocation);
|
|
168
240
|
workflowSettled = true;
|
|
241
|
+
if (phase === "create-workflow") createWorkflowSettled = true;
|
|
169
242
|
}
|
|
170
243
|
if (!workflowSettled) return;
|
|
171
244
|
cancelRefresh();
|
|
245
|
+
if (createWorkflowSettled) pendingWorkspaceRename = true;
|
|
172
246
|
await refresh().catch(reportRefreshFailure);
|
|
173
247
|
});
|
|
174
248
|
|
|
@@ -206,7 +280,7 @@ export default function pullRequestExtension(
|
|
|
206
280
|
if (sessionGeneration !== generation) return;
|
|
207
281
|
cancelRefresh();
|
|
208
282
|
if (nextStep !== "none" && nextStep !== "merge") {
|
|
209
|
-
activeInvocations.set(invocation, "workflow");
|
|
283
|
+
activeInvocations.set(invocation, nextStep === "create" ? "create-workflow" : "workflow");
|
|
210
284
|
if (nextStep === "create") ctx.ui.setStatus(UI_KEY, undefined);
|
|
211
285
|
setWidget(ctx, undefined);
|
|
212
286
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-pr",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"description": "Run /pr to automatically choose the current pull request's next safe lifecycle step: create, update, address feedback, fix CI, or merge when ready.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
31
31
|
"@earendil-works/pi-tui": "^0.85.1"
|
|
32
32
|
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@henryqw/pi-herdr": "^0.4.5"
|
|
35
|
+
},
|
|
33
36
|
"repository": {
|
|
34
37
|
"type": "git",
|
|
35
38
|
"url": "git+https://github.com/HenryQW/pi-harness.git",
|
|
@@ -36,4 +36,23 @@ Create current branch GitHub pull request.
|
|
|
36
36
|
Reuse one result only when its base matches. Refresh its title and body.
|
|
37
37
|
Stop on a different base or multiple results. Otherwise create with explicit
|
|
38
38
|
`--head <OWNER>:<ref>`, `--base <base>`, title, and body file.
|
|
39
|
-
6.
|
|
39
|
+
6. After successful create or reuse, retain the already validated PR URL. Parse
|
|
40
|
+
its PR path segment with Node's standard `URL` and derive `<number>`. Require
|
|
41
|
+
the positive decimal form `^[1-9][0-9]*$`.
|
|
42
|
+
|
|
43
|
+
Only when `HERDR_ENV=1` and trimmed `HERDR_WORKSPACE_ID` is non-empty, use
|
|
44
|
+
that trimmed value as `<workspace-id>`. Run `herdr workspace get
|
|
45
|
+
"<workspace-id>"`. Parse its default JSON with Node's standard library
|
|
46
|
+
(`node:fs` and `JSON.parse`), not `jq`. Require
|
|
47
|
+
`result.workspace.workspace_id` to exactly equal `<workspace-id>` and
|
|
48
|
+
`result.workspace.label` to be a non-empty string.
|
|
49
|
+
Remove every trailing suffix with `/(?: · PR #[1-9][0-9]*)+$/` from the
|
|
50
|
+
label, then append exactly one ` · PR #<number>` suffix. Run `herdr workspace
|
|
51
|
+
rename "<workspace-id>" "<normalized-label>"` only when the normalized label
|
|
52
|
+
differs from the current label. Rename only the workspace, never a pane, tab,
|
|
53
|
+
Pi session, or Git branch.
|
|
54
|
+
|
|
55
|
+
Otherwise, including outside Herdr, skip labeling silently. If any
|
|
56
|
+
workspace-label step fails after PR success, do not undo the PR; reply with
|
|
57
|
+
its URL and `Herdr workspace rename failed: <error>`. Otherwise reply only
|
|
58
|
+
with the PR URL.
|