@henryqw/pi-pr 2.1.0 → 2.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 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
@@ -46,6 +48,16 @@ Each footer entry is one linked `PR #number` plus one plain-language status: `N
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.
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.0",
3
+ "version": "2.1.1",
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",
@@ -1024,6 +1024,7 @@ if [ "$1" = pr ] && [ "$2" = view ]; then
1024
1024
  exit 1
1025
1025
  fi
1026
1026
  if [ "$1" = api ] && [ "$2" = graphql ] && [ "$3" = --hostname ] && [ "$4" = ${enterpriseHost} ]; then
1027
+ cat >/dev/null
1027
1028
  echo 'host GraphQL failed' >&2
1028
1029
  exit 1
1029
1030
  fi