@clipboard-health/groundcrew 4.44.0 → 4.44.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.
@@ -1 +1 @@
1
- {"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EACL,KAAK,UAAU,EAIhB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,UAAU,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,KAAK,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;AAE7C,UAAU,YAAY;IACpB,KAAK,EAAE,KAAK,CAAC;IACb,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,sEAAsE;AACtE,UAAU,eAAe;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AA+CD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CA8H3D"}
1
+ {"version":3,"file":"reviewer.d.ts","sourceRoot":"","sources":["../../src/commands/reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EACL,KAAK,UAAU,EAIhB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,UAAU,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,KAAK,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAAC;AAE7C,UAAU,YAAY;IACpB,KAAK,EAAE,KAAK,CAAC;IACb,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,sEAAsE;AACtE,UAAU,eAAe;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AA+CD,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CA+H3D"}
@@ -82,7 +82,8 @@ export function createReviewer(deps) {
82
82
  for (const entry of entries) {
83
83
  const branchName = reviewerConfig === undefined
84
84
  ? entry.branchName
85
- : effectiveBranchName({ config: reviewerConfig, entry });
85
+ : // oxlint-disable-next-line no-await-in-loop -- one git lookup per worktree; a task almost always has one entry.
86
+ await effectiveBranchName({ config: reviewerConfig, entry });
86
87
  // The injected lookup is contracted never to reject (failures resolve to
87
88
  // []), but we still guard it so one bad lookup can never abort the tick
88
89
  // and starve the other candidates. A failure means "can't tell yet" →
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAyrBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAIA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAenE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA4rBD,wBAAsB,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7D"}
@@ -43,7 +43,8 @@ async function writeTaskWorktrees(config, task) {
43
43
  }
44
44
  const runState = readRunState(config, task);
45
45
  for (const entry of entries) {
46
- const branchName = effectiveBranchNameFromRunState({ entry, runState });
46
+ // oxlint-disable-next-line no-await-in-loop -- status output is easier to read in worktree order.
47
+ const branchName = await effectiveBranchNameFromRunState({ entry, runState });
47
48
  // oxlint-disable-next-line no-await-in-loop -- status output is easier to read in worktree order.
48
49
  const dirtiness = await worktrees.probeWorkingTree({
49
50
  worktreeDir: entry.dir,
@@ -335,13 +336,13 @@ async function writeInventoryWorktrees(config, probe, statusByTask) {
335
336
  }
336
337
  }
337
338
  const accessHints = await collectAccessHints(config, entries);
338
- const pullRequests = await collectPullRequests(entries.map((entry) => ({
339
+ const pullRequests = await collectPullRequests(await Promise.all(entries.map(async (entry) => ({
339
340
  dir: entry.dir,
340
- branchName: effectiveBranchNameFromRunState({
341
+ branchName: await effectiveBranchNameFromRunState({
341
342
  entry,
342
343
  runState: runStates.get(entry.task),
343
344
  }),
344
- })));
345
+ }))));
345
346
  const now = new Date();
346
347
  for (const [index, entry] of entries.entries()) {
347
348
  const runState = runStates.get(entry.task);
@@ -1 +1 @@
1
- {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/commands/task.ts"],"names":[],"mappings":"AA+yBA,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB3D"}
1
+ {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/commands/task.ts"],"names":[],"mappings":"AAmzBA,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB3D"}
@@ -516,9 +516,13 @@ function describeDirtiness(dirtiness) {
516
516
  async function worktreeHasPullRequest(input) {
517
517
  const pullRequests = await findPullRequestsForBranch({
518
518
  cwd: input.entry.dir,
519
- branchName: effectiveBranchName({ config: input.config, entry: input.entry }),
519
+ branchName: await effectiveBranchName({ config: input.config, entry: input.entry }),
520
520
  });
521
- return pullRequests.length > 0;
521
+ // Only an open PR gates `crew task done`: a merged or closed PR for this
522
+ // branch is stale (work already landed, or abandoned) and shouldn't keep the
523
+ // worktree pinned. The lifecycle filter also defends against branch-name
524
+ // reuse — a long-dead merged PR with the same head name doesn't count.
525
+ return pullRequests.some((pr) => pr.state === "open");
522
526
  }
523
527
  async function assertCanMarkDone(arguments_) {
524
528
  if (arguments_.allowDirty) {
@@ -8,6 +8,14 @@
8
8
  * resolve the GitHub repo from that checkout's own `origin` remote. This
9
9
  * handles bare config names, full `owner/repo` slugs, forks, and SSH/HTTPS
10
10
  * remotes uniformly — we never reconstruct the slug ourselves.
11
+ *
12
+ * Whatever GitHub returns is returned verbatim. We do not filter by commit
13
+ * history: the branch name on the remote (which `gh pr list --head` matches)
14
+ * is already a strong identifier for "the PR(s) for this checkout". Stale
15
+ * PRs from a long-dead branch with a reused name surface as `(merged)` or
16
+ * `(closed)` in the status output, which is more informative than silently
17
+ * dropping them. Decision-making callers (e.g. `crew task done`) filter by
18
+ * lifecycle state instead.
11
19
  */
12
20
  export interface PullRequestSummary {
13
21
  url: string;
@@ -15,8 +23,6 @@ export interface PullRequestSummary {
15
23
  /** Lowercased lifecycle: "open" | "merged" | "closed". */
16
24
  state: string;
17
25
  title: string;
18
- /** PR head commit SHA used to ignore historical PRs from reused branch names. */
19
- headRefOid: string;
20
26
  }
21
27
  interface LookupArgs {
22
28
  /** Worktree directory; `gh` resolves the GitHub repo from its git remote. */
@@ -1 +1 @@
1
- {"version":3,"file":"pullRequests.d.ts","sourceRoot":"","sources":["../../src/lib/pullRequests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AASD,UAAU,UAAU;IAClB,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAmDD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,UAAU,sBAAsB;IAC9B,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA2BD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,sBAAsB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAmC9B;AAED,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CA6BxC"}
1
+ {"version":3,"file":"pullRequests.d.ts","sourceRoot":"","sources":["../../src/lib/pullRequests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AASD,UAAU,UAAU;IAClB,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAgDD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,UAAU,sBAAsB;IAC9B,yFAAyF;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA2BD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,sBAAsB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAmC9B;AAED,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CA0BxC"}
@@ -8,6 +8,14 @@
8
8
  * resolve the GitHub repo from that checkout's own `origin` remote. This
9
9
  * handles bare config names, full `owner/repo` slugs, forks, and SSH/HTTPS
10
10
  * remotes uniformly — we never reconstruct the slug ourselves.
11
+ *
12
+ * Whatever GitHub returns is returned verbatim. We do not filter by commit
13
+ * history: the branch name on the remote (which `gh pr list --head` matches)
14
+ * is already a strong identifier for "the PR(s) for this checkout". Stale
15
+ * PRs from a long-dead branch with a reused name surface as `(merged)` or
16
+ * `(closed)` in the status output, which is more informative than silently
17
+ * dropping them. Decision-making callers (e.g. `crew task done`) filter by
18
+ * lifecycle state instead.
11
19
  */
12
20
  import { runCommandAsync } from "./commandRunner.js";
13
21
  const GH_PR_LIST_LIMIT = 5;
@@ -37,7 +45,6 @@ function parsePullRequests(output) {
37
45
  number: entry.number,
38
46
  state: STATE_MAP[entry.state] ?? entry.state.toLowerCase(),
39
47
  title: entry.title,
40
- headRefOid: entry.headRefOid,
41
48
  });
42
49
  }
43
50
  return summaries;
@@ -51,8 +58,7 @@ function isRawPullRequest(value) {
51
58
  return (typeof record["url"] === "string" &&
52
59
  typeof record["number"] === "number" &&
53
60
  typeof record["state"] === "string" &&
54
- typeof record["title"] === "string" &&
55
- typeof record["headRefOid"] === "string");
61
+ typeof record["title"] === "string");
56
62
  }
57
63
  function isRawResolvedPullRequest(value) {
58
64
  if (typeof value !== "object" || value === null) {
@@ -109,25 +115,22 @@ export async function findPullRequestsForBranch(arguments_) {
109
115
  const { cwd, branchName, signal } = arguments_;
110
116
  const options = signal === undefined ? { cwd } : { cwd, signal };
111
117
  try {
112
- const [output, currentHeadOid] = await Promise.all([
113
- runCommandAsync("gh", [
114
- "pr",
115
- "list",
116
- "--head",
117
- branchName,
118
- "--state",
119
- "all",
120
- "--limit",
121
- String(GH_PR_LIST_LIMIT),
122
- "--json",
123
- "url,number,state,title,headRefOid",
124
- ], options),
125
- runCommandAsync("git", ["rev-parse", "HEAD"], options),
126
- ]);
127
- return parsePullRequests(output).filter((pr) => pr.headRefOid === currentHeadOid);
118
+ const output = await runCommandAsync("gh", [
119
+ "pr",
120
+ "list",
121
+ "--head",
122
+ branchName,
123
+ "--state",
124
+ "all",
125
+ "--limit",
126
+ String(GH_PR_LIST_LIMIT),
127
+ "--json",
128
+ "url,number,state,title",
129
+ ], options);
130
+ return parsePullRequests(output);
128
131
  }
129
132
  catch {
130
- // gh/git not installed / not authenticated / non-GitHub remote / network
133
+ // gh not installed / not authenticated / non-GitHub remote / network
131
134
  // error / etc. All resolve to "no PR info available" for display.
132
135
  return [];
133
136
  }
@@ -5,12 +5,21 @@ interface EffectiveBranchNameInput {
5
5
  config: ResolvedConfig;
6
6
  entry: Pick<WorktreeEntry, "repository" | "task" | "branchName" | "dir">;
7
7
  }
8
- export declare function effectiveBranchName(input: EffectiveBranchNameInput): string;
8
+ export declare function effectiveBranchName(input: EffectiveBranchNameInput): Promise<string>;
9
9
  interface EffectiveBranchNameFromRunStateInput {
10
10
  entry: Pick<WorktreeEntry, "repository" | "branchName" | "dir">;
11
11
  runState: RunState | undefined;
12
12
  }
13
- export declare function effectiveBranchNameFromRunState(input: EffectiveBranchNameFromRunStateInput): string;
13
+ /**
14
+ * Resolves the worktree's checked-out branch name. Git is the source of truth:
15
+ * run state records the branch we *requested* at creation, but a template hook
16
+ * or manual rename can drift the actual branch (e.g. flawless-inventory prefixes
17
+ * with the GitHub username). Downstream callers — `gh pr list --head <name>`,
18
+ * the displayed `branch:` row, `git push` — need what git has now, not what we
19
+ * once asked for. Falls back to run state / entry when git can't answer
20
+ * (detached HEAD, worktree not yet provisioned, git failure).
21
+ */
22
+ export declare function effectiveBranchNameFromRunState(input: EffectiveBranchNameFromRunStateInput): Promise<string>;
14
23
  interface HasAdoptedBranchInput {
15
24
  config: ResolvedConfig;
16
25
  entry: Pick<WorktreeEntry, "repository" | "task" | "dir" | "adoptedBranch">;
@@ -1 +1 @@
1
- {"version":3,"file":"worktreeRunState.d.ts","sourceRoot":"","sources":["../../src/lib/worktreeRunState.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAiCpD,UAAU,wBAAwB;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,GAAG,MAAM,GAAG,YAAY,GAAG,KAAK,CAAC,CAAC;CAC1E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM,CAK3E;AAED,UAAU,oCAAoC;IAC5C,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,GAAG,YAAY,GAAG,KAAK,CAAC,CAAC;IAChE,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;CAChC;AAED,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,oCAAoC,GAC1C,MAAM,CAKR;AAED,UAAU,qBAAqB;IAC7B,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,eAAe,CAAC,CAAC;CAC7E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAKtE"}
1
+ {"version":3,"file":"worktreeRunState.d.ts","sourceRoot":"","sources":["../../src/lib/worktreeRunState.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAiCpD,UAAU,wBAAwB;IAChC,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,GAAG,MAAM,GAAG,YAAY,GAAG,KAAK,CAAC,CAAC;CAC1E;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAK1F;AAED,UAAU,oCAAoC;IAC5C,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,GAAG,YAAY,GAAG,KAAK,CAAC,CAAC;IAChE,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAsB,+BAA+B,CACnD,KAAK,EAAE,oCAAoC,GAC1C,OAAO,CAAC,MAAM,CAAC,CASjB;AAWD,UAAU,qBAAqB;IAC7B,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,eAAe,CAAC,CAAC;CAC7E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAKtE"}
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { runCommandAsync } from "./commandRunner.js";
2
3
  import { readRunState } from "./runState.js";
3
4
  function readMatchingRunState(input) {
4
5
  const runState = readRunState(input.config, input.entry.task);
@@ -15,18 +16,40 @@ function runStateMatchesEntry(input) {
15
16
  return (runState.repository === entry.repository &&
16
17
  path.resolve(runState.worktreeDir) === path.resolve(entry.dir));
17
18
  }
18
- export function effectiveBranchName(input) {
19
- return effectiveBranchNameFromRunState({
19
+ export async function effectiveBranchName(input) {
20
+ return await effectiveBranchNameFromRunState({
20
21
  entry: input.entry,
21
22
  runState: readMatchingRunState(input),
22
23
  });
23
24
  }
24
- export function effectiveBranchNameFromRunState(input) {
25
+ /**
26
+ * Resolves the worktree's checked-out branch name. Git is the source of truth:
27
+ * run state records the branch we *requested* at creation, but a template hook
28
+ * or manual rename can drift the actual branch (e.g. flawless-inventory prefixes
29
+ * with the GitHub username). Downstream callers — `gh pr list --head <name>`,
30
+ * the displayed `branch:` row, `git push` — need what git has now, not what we
31
+ * once asked for. Falls back to run state / entry when git can't answer
32
+ * (detached HEAD, worktree not yet provisioned, git failure).
33
+ */
34
+ export async function effectiveBranchNameFromRunState(input) {
35
+ const checkedOut = await resolveCheckedOutBranch(input.entry.dir);
36
+ if (checkedOut !== undefined) {
37
+ return checkedOut;
38
+ }
25
39
  if (input.runState !== undefined && runStateMatchesEntry(input)) {
26
40
  return input.runState.branchName;
27
41
  }
28
42
  return input.entry.branchName;
29
43
  }
44
+ async function resolveCheckedOutBranch(dir) {
45
+ try {
46
+ const output = await runCommandAsync("git", ["branch", "--show-current"], { cwd: dir });
47
+ return output === "" ? undefined : output;
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
30
53
  export function hasAdoptedBranch(input) {
31
54
  if (input.entry.adoptedBranch === true) {
32
55
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/groundcrew",
3
- "version": "4.44.0",
3
+ "version": "4.44.1",
4
4
  "description": "Linear-driven orchestrator that launches AI coding agents in git worktrees, with workspace lifecycle and usage tracking.",
5
5
  "keywords": [
6
6
  "agent",