@gethmy/agent 1.19.0 → 1.20.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.
Files changed (3) hide show
  1. package/dist/cli.js +144 -43
  2. package/dist/index.js +144 -43
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -731,9 +731,29 @@ var init_config_validation = __esm(() => {
731
731
  };
732
732
  });
733
733
  // ../harmony-shared/dist/branchRef.js
734
- var BRANCH_REF_PATTERN, SAFE_GIT_REF_PATTERN;
734
+ function extractBranchRef(description) {
735
+ if (!description)
736
+ return null;
737
+ for (const match of description.matchAll(BRANCH_REF_PATTERN)) {
738
+ const branch = match[1];
739
+ if (SAFE_GIT_REF_PATTERN.test(branch))
740
+ return branch;
741
+ }
742
+ return null;
743
+ }
744
+ function hasUnsafeDaemonBranchLine(description) {
745
+ if (!description)
746
+ return false;
747
+ for (const match of description.matchAll(DAEMON_BRANCH_LINE_PATTERN)) {
748
+ if (!SAFE_GIT_REF_PATTERN.test(match[1]))
749
+ return true;
750
+ }
751
+ return false;
752
+ }
753
+ var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN;
735
754
  var init_branchRef = __esm(() => {
736
- BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/;
755
+ BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/g;
756
+ DAEMON_BRANCH_LINE_PATTERN = /^[ \t]*Branch:\s*`([^`]+)`/gm;
737
757
  SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9/_.+-]+$/;
738
758
  });
739
759
 
@@ -1540,6 +1560,7 @@ __export(exports_git_pr, {
1540
1560
  findExistingPr: () => findExistingPr,
1541
1561
  extractReviewedSha: () => extractReviewedSha,
1542
1562
  extractPrUrl: () => extractPrUrl,
1563
+ extractAzurePrId: () => extractAzurePrId,
1543
1564
  detectGitProvider: () => detectGitProvider,
1544
1565
  deriveCiStatus: () => deriveCiStatus,
1545
1566
  decidePrBranch: () => decidePrBranch,
@@ -1734,13 +1755,18 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
1734
1755
  return "unknown";
1735
1756
  }
1736
1757
  }
1737
- function decidePrBranch(provider, rawJson) {
1738
- if (provider !== "github") {
1739
- return {
1740
- kind: "skip",
1741
- reason: `PR-link review not yet supported for provider "${provider}" (GitHub only)`
1742
- };
1758
+ function stripHeadsPrefix(ref) {
1759
+ return ref.replace(/^refs\/heads\//, "");
1760
+ }
1761
+ function branchOrSkip(branch) {
1762
+ if (!branch)
1763
+ return { kind: "skip", reason: "PR has no head branch name" };
1764
+ if (!SAFE_GIT_REF_PATTERN.test(branch)) {
1765
+ return { kind: "skip", reason: `unsafe git ref: ${branch}` };
1743
1766
  }
1767
+ return { kind: "branch", branch };
1768
+ }
1769
+ function decideGithubPrBranch(rawJson) {
1744
1770
  if (rawJson === null) {
1745
1771
  return { kind: "skip", reason: "gh pr view failed" };
1746
1772
  }
@@ -1759,25 +1785,74 @@ function decidePrBranch(provider, rawJson) {
1759
1785
  reason: "fork PR (cross-repo head branch not on origin)"
1760
1786
  };
1761
1787
  }
1762
- const branch = typeof parsed.headRefName === "string" ? parsed.headRefName : null;
1763
- if (!branch) {
1764
- return { kind: "skip", reason: "PR has no head branch name" };
1788
+ return branchOrSkip(typeof parsed.headRefName === "string" ? parsed.headRefName : null);
1789
+ }
1790
+ function decideAzurePrBranch(rawJson) {
1791
+ if (rawJson === null) {
1792
+ return { kind: "skip", reason: "az repos pr show failed" };
1765
1793
  }
1766
- if (!SAFE_GIT_REF_PATTERN.test(branch)) {
1767
- return { kind: "skip", reason: `unsafe git ref: ${branch}` };
1794
+ let parsed;
1795
+ try {
1796
+ parsed = JSON.parse(rawJson.trim());
1797
+ } catch {
1798
+ return { kind: "skip", reason: "unparseable az repos pr show output" };
1768
1799
  }
1769
- return { kind: "branch", branch };
1800
+ if (typeof parsed !== "object" || parsed === null) {
1801
+ return { kind: "skip", reason: "unparseable az repos pr show output" };
1802
+ }
1803
+ if (parsed.forkSource != null) {
1804
+ return {
1805
+ kind: "skip",
1806
+ reason: "fork PR (cross-repo head branch not on origin)"
1807
+ };
1808
+ }
1809
+ const ref = typeof parsed.sourceRefName === "string" ? parsed.sourceRefName : null;
1810
+ return branchOrSkip(ref ? stripHeadsPrefix(ref) : null);
1811
+ }
1812
+ function decidePrBranch(provider, rawJson) {
1813
+ switch (provider) {
1814
+ case "github":
1815
+ return decideGithubPrBranch(rawJson);
1816
+ case "azure":
1817
+ return decideAzurePrBranch(rawJson);
1818
+ default:
1819
+ return {
1820
+ kind: "skip",
1821
+ reason: `PR-link review not yet supported for provider "${provider}"`
1822
+ };
1823
+ }
1824
+ }
1825
+ function extractAzurePrId(prUrl) {
1826
+ const m = prUrl.match(/pullrequest\/(\d+)/i);
1827
+ return m ? m[1] : null;
1770
1828
  }
1771
1829
  async function resolvePrHeadBranch(prUrl, cwd, provider) {
1772
- if (provider !== "github")
1773
- return decidePrBranch(provider, null);
1774
- try {
1775
- const { stdout } = await execFileAsync("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
1776
- return decidePrBranch("github", stdout);
1777
- } catch (err) {
1778
- log.warn(TAG2, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1779
- return decidePrBranch("github", null);
1830
+ if (provider === "github") {
1831
+ try {
1832
+ const { stdout } = await execFileAsync("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
1833
+ return decidePrBranch("github", stdout);
1834
+ } catch (err) {
1835
+ log.warn(TAG2, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1836
+ return decidePrBranch("github", null);
1837
+ }
1838
+ }
1839
+ if (provider === "azure") {
1840
+ const prId = extractAzurePrId(prUrl);
1841
+ if (!prId) {
1842
+ return {
1843
+ kind: "skip",
1844
+ reason: `could not parse Azure PR id from ${prUrl}`
1845
+ };
1846
+ }
1847
+ try {
1848
+ const { stdout } = await execFileAsync("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
1849
+ return decidePrBranch("azure", stdout);
1850
+ } catch (err) {
1851
+ log.warn(TAG2, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1852
+ return decidePrBranch("azure", null);
1853
+ }
1780
1854
  }
1855
+ return decidePrBranch(provider, null);
1781
1856
  }
1782
1857
  function extractPrUrl(description) {
1783
1858
  if (!description)
@@ -1909,7 +1984,11 @@ function buildPrBody(card, commitLog) {
1909
1984
  ].join(`
1910
1985
  `);
1911
1986
  }
1912
- function createPullRequest(card, branchName, worktreePath, config, provider) {
1987
+ function createPullRequest(card, branchName, worktreePath, config, provider, existingPrUrl) {
1988
+ if (existingPrUrl) {
1989
+ log.info(TAG2, `Reusing existing PR from card description: ${existingPrUrl}`);
1990
+ return existingPrUrl;
1991
+ }
1913
1992
  let commitLog = "";
1914
1993
  try {
1915
1994
  commitLog = execFileSync("git", ["log", "--oneline", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
@@ -2380,17 +2459,26 @@ function cleanupWorktree(worktreePath, branchName) {
2380
2459
  const repoRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
2381
2460
  encoding: "utf-8"
2382
2461
  }).trim();
2383
- try {
2384
- execFileSync3("git", ["worktree", "remove", worktreePath, "--force"], {
2385
- cwd: repoRoot,
2386
- stdio: "pipe"
2387
- });
2388
- log.info(TAG6, `Removed worktree: ${worktreePath}`);
2389
- } catch (err) {
2390
- log.warn(TAG6, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
2391
- if (existsSync2(worktreePath)) {
2392
- rmSync(worktreePath, { recursive: true, force: true });
2462
+ if (existsSync2(worktreePath)) {
2463
+ try {
2464
+ execFileSync3("git", ["worktree", "remove", worktreePath, "--force"], {
2465
+ cwd: repoRoot,
2466
+ stdio: "pipe"
2467
+ });
2468
+ log.info(TAG6, `Removed worktree: ${worktreePath}`);
2469
+ } catch (err) {
2470
+ log.warn(TAG6, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
2471
+ if (existsSync2(worktreePath)) {
2472
+ rmSync(worktreePath, { recursive: true, force: true });
2473
+ }
2474
+ try {
2475
+ execFileSync3("git", ["worktree", "prune", "--expire=now"], {
2476
+ cwd: repoRoot,
2477
+ stdio: "pipe"
2478
+ });
2479
+ } catch {}
2393
2480
  }
2481
+ } else {
2394
2482
  try {
2395
2483
  execFileSync3("git", ["worktree", "prune", "--expire=now"], {
2396
2484
  cwd: repoRoot,
@@ -2412,9 +2500,19 @@ function resolveRepoRoot() {
2412
2500
  encoding: "utf-8"
2413
2501
  }).trim();
2414
2502
  }
2503
+ function localBranchExists(branchName, repoRoot) {
2504
+ try {
2505
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot, stdio: "ignore" });
2506
+ return true;
2507
+ } catch {
2508
+ return false;
2509
+ }
2510
+ }
2415
2511
  function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
2512
+ if (!localBranchExists(branchName, repoRoot))
2513
+ return false;
2416
2514
  try {
2417
- const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8" }).trim();
2515
+ const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
2418
2516
  return out.length > 0;
2419
2517
  } catch {
2420
2518
  return false;
@@ -2545,12 +2643,9 @@ function checkoutExistingBranch(basePath, branchName) {
2545
2643
  return worktreeDir;
2546
2644
  }
2547
2645
  function extractBranchFromDescription(description) {
2548
- if (!description)
2549
- return null;
2550
- const branch = description.match(BRANCH_REF_PATTERN)?.[1] ?? null;
2551
- if (branch && !SAFE_GIT_REF_PATTERN.test(branch)) {
2552
- log.warn(TAG7, `Extracted branch name contains unsafe characters: ${branch}`);
2553
- return null;
2646
+ const branch = extractBranchRef(description);
2647
+ if (!branch && hasUnsafeDaemonBranchLine(description)) {
2648
+ log.warn(TAG7, "Daemon Branch: line contains unsafe characters; ignoring it");
2554
2649
  }
2555
2650
  return branch;
2556
2651
  }
@@ -2568,6 +2663,11 @@ async function resolveReviewBranch(description, cwd) {
2568
2663
  const resolved = await resolvePrHeadBranch(prUrl, cwd, provider);
2569
2664
  return resolved;
2570
2665
  }
2666
+ function reviewedFromPrUrl(description) {
2667
+ if (extractBranchFromDescription(description))
2668
+ return null;
2669
+ return extractPrUrl(description ?? null);
2670
+ }
2571
2671
  var TAG7 = "review-worktree";
2572
2672
  var init_review_worktree = __esm(() => {
2573
2673
  init_dist();
@@ -5459,7 +5559,7 @@ async function postReviewComment(client, card, commentType, body) {
5459
5559
  log.error(TAG20, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
5460
5560
  }
5461
5561
  }
5462
- async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore) {
5562
+ async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore, resolvedFromPrUrl) {
5463
5563
  let freshDesc;
5464
5564
  try {
5465
5565
  const { card: fresh } = await client.getCard(card.id);
@@ -5522,7 +5622,7 @@ ${runLogTail}
5522
5622
  }
5523
5623
  if (config.review.createPR && approvedBranch) {
5524
5624
  const provider = detectGitProvider(worktreePath);
5525
- prUrl = createPullRequest(card, approvedBranch, worktreePath, config, provider);
5625
+ prUrl = createPullRequest(card, approvedBranch, worktreePath, config, provider, resolvedFromPrUrl ?? null);
5526
5626
  }
5527
5627
  }
5528
5628
  await addLabelByName(client, card, config.review.approvedLabel, config.review.approvedLabelColor);
@@ -6602,7 +6702,7 @@ class ReviewWorker {
6602
6702
  currentTask: `Processing ${result.verdict} verdict`,
6603
6703
  progressPercent: 80
6604
6704
  });
6605
- await runReviewCompletion(this.client, card, result, this.config, cwd, this.branchName, sessionStats, this.lastRunLogPath, this.workspaceId, this.sessionId, this.stateStore);
6705
+ await runReviewCompletion(this.client, card, result, this.config, cwd, this.branchName, sessionStats, this.lastRunLogPath, this.workspaceId, this.sessionId, this.stateStore, reviewedFromPrUrl(card.description));
6606
6706
  await this.collectReviewGate(card, result);
6607
6707
  } catch (err) {
6608
6708
  this.state = "error";
@@ -7922,6 +8022,7 @@ class Worker {
7922
8022
  stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
7923
8023
  } : undefined;
7924
8024
  const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup);
8025
+ this.worktreePath = null;
7925
8026
  this.verificationFailed = !completed;
7926
8027
  if (completed && stageRun) {
7927
8028
  const outcome = await this.advanceFromGateEvaluation(card, stageRun.stage, stageRun.index, stageRun.def, stageGateEvaluation);
package/dist/index.js CHANGED
@@ -730,9 +730,29 @@ var init_config_validation = __esm(() => {
730
730
  };
731
731
  });
732
732
  // ../harmony-shared/dist/branchRef.js
733
- var BRANCH_REF_PATTERN, SAFE_GIT_REF_PATTERN;
733
+ function extractBranchRef(description) {
734
+ if (!description)
735
+ return null;
736
+ for (const match of description.matchAll(BRANCH_REF_PATTERN)) {
737
+ const branch = match[1];
738
+ if (SAFE_GIT_REF_PATTERN.test(branch))
739
+ return branch;
740
+ }
741
+ return null;
742
+ }
743
+ function hasUnsafeDaemonBranchLine(description) {
744
+ if (!description)
745
+ return false;
746
+ for (const match of description.matchAll(DAEMON_BRANCH_LINE_PATTERN)) {
747
+ if (!SAFE_GIT_REF_PATTERN.test(match[1]))
748
+ return true;
749
+ }
750
+ return false;
751
+ }
752
+ var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN;
734
753
  var init_branchRef = __esm(() => {
735
- BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/;
754
+ BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/g;
755
+ DAEMON_BRANCH_LINE_PATTERN = /^[ \t]*Branch:\s*`([^`]+)`/gm;
736
756
  SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9/_.+-]+$/;
737
757
  });
738
758
 
@@ -1539,6 +1559,7 @@ __export(exports_git_pr, {
1539
1559
  findExistingPr: () => findExistingPr,
1540
1560
  extractReviewedSha: () => extractReviewedSha,
1541
1561
  extractPrUrl: () => extractPrUrl,
1562
+ extractAzurePrId: () => extractAzurePrId,
1542
1563
  detectGitProvider: () => detectGitProvider,
1543
1564
  deriveCiStatus: () => deriveCiStatus,
1544
1565
  decidePrBranch: () => decidePrBranch,
@@ -1733,13 +1754,18 @@ async function checkPrMergeStatus(prUrl, cwd, provider) {
1733
1754
  return "unknown";
1734
1755
  }
1735
1756
  }
1736
- function decidePrBranch(provider, rawJson) {
1737
- if (provider !== "github") {
1738
- return {
1739
- kind: "skip",
1740
- reason: `PR-link review not yet supported for provider "${provider}" (GitHub only)`
1741
- };
1757
+ function stripHeadsPrefix(ref) {
1758
+ return ref.replace(/^refs\/heads\//, "");
1759
+ }
1760
+ function branchOrSkip(branch) {
1761
+ if (!branch)
1762
+ return { kind: "skip", reason: "PR has no head branch name" };
1763
+ if (!SAFE_GIT_REF_PATTERN.test(branch)) {
1764
+ return { kind: "skip", reason: `unsafe git ref: ${branch}` };
1742
1765
  }
1766
+ return { kind: "branch", branch };
1767
+ }
1768
+ function decideGithubPrBranch(rawJson) {
1743
1769
  if (rawJson === null) {
1744
1770
  return { kind: "skip", reason: "gh pr view failed" };
1745
1771
  }
@@ -1758,25 +1784,74 @@ function decidePrBranch(provider, rawJson) {
1758
1784
  reason: "fork PR (cross-repo head branch not on origin)"
1759
1785
  };
1760
1786
  }
1761
- const branch = typeof parsed.headRefName === "string" ? parsed.headRefName : null;
1762
- if (!branch) {
1763
- return { kind: "skip", reason: "PR has no head branch name" };
1787
+ return branchOrSkip(typeof parsed.headRefName === "string" ? parsed.headRefName : null);
1788
+ }
1789
+ function decideAzurePrBranch(rawJson) {
1790
+ if (rawJson === null) {
1791
+ return { kind: "skip", reason: "az repos pr show failed" };
1764
1792
  }
1765
- if (!SAFE_GIT_REF_PATTERN.test(branch)) {
1766
- return { kind: "skip", reason: `unsafe git ref: ${branch}` };
1793
+ let parsed;
1794
+ try {
1795
+ parsed = JSON.parse(rawJson.trim());
1796
+ } catch {
1797
+ return { kind: "skip", reason: "unparseable az repos pr show output" };
1767
1798
  }
1768
- return { kind: "branch", branch };
1799
+ if (typeof parsed !== "object" || parsed === null) {
1800
+ return { kind: "skip", reason: "unparseable az repos pr show output" };
1801
+ }
1802
+ if (parsed.forkSource != null) {
1803
+ return {
1804
+ kind: "skip",
1805
+ reason: "fork PR (cross-repo head branch not on origin)"
1806
+ };
1807
+ }
1808
+ const ref = typeof parsed.sourceRefName === "string" ? parsed.sourceRefName : null;
1809
+ return branchOrSkip(ref ? stripHeadsPrefix(ref) : null);
1810
+ }
1811
+ function decidePrBranch(provider, rawJson) {
1812
+ switch (provider) {
1813
+ case "github":
1814
+ return decideGithubPrBranch(rawJson);
1815
+ case "azure":
1816
+ return decideAzurePrBranch(rawJson);
1817
+ default:
1818
+ return {
1819
+ kind: "skip",
1820
+ reason: `PR-link review not yet supported for provider "${provider}"`
1821
+ };
1822
+ }
1823
+ }
1824
+ function extractAzurePrId(prUrl) {
1825
+ const m = prUrl.match(/pullrequest\/(\d+)/i);
1826
+ return m ? m[1] : null;
1769
1827
  }
1770
1828
  async function resolvePrHeadBranch(prUrl, cwd, provider) {
1771
- if (provider !== "github")
1772
- return decidePrBranch(provider, null);
1773
- try {
1774
- const { stdout } = await execFileAsync("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
1775
- return decidePrBranch("github", stdout);
1776
- } catch (err) {
1777
- log.warn(TAG2, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1778
- return decidePrBranch("github", null);
1829
+ if (provider === "github") {
1830
+ try {
1831
+ const { stdout } = await execFileAsync("gh", ["pr", "view", prUrl, "--json", "headRefName,isCrossRepository"], { cwd, encoding: "utf-8", timeout: 1e4 });
1832
+ return decidePrBranch("github", stdout);
1833
+ } catch (err) {
1834
+ log.warn(TAG2, `gh pr view failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1835
+ return decidePrBranch("github", null);
1836
+ }
1837
+ }
1838
+ if (provider === "azure") {
1839
+ const prId = extractAzurePrId(prUrl);
1840
+ if (!prId) {
1841
+ return {
1842
+ kind: "skip",
1843
+ reason: `could not parse Azure PR id from ${prUrl}`
1844
+ };
1845
+ }
1846
+ try {
1847
+ const { stdout } = await execFileAsync("az", ["repos", "pr", "show", "--id", prId, "--output", "json"], { cwd, encoding: "utf-8", timeout: 1e4 });
1848
+ return decidePrBranch("azure", stdout);
1849
+ } catch (err) {
1850
+ log.warn(TAG2, `az repos pr show failed for ${prUrl}: ${err instanceof Error ? err.message : String(err)}`);
1851
+ return decidePrBranch("azure", null);
1852
+ }
1779
1853
  }
1854
+ return decidePrBranch(provider, null);
1780
1855
  }
1781
1856
  function extractPrUrl(description) {
1782
1857
  if (!description)
@@ -1908,7 +1983,11 @@ function buildPrBody(card, commitLog) {
1908
1983
  ].join(`
1909
1984
  `);
1910
1985
  }
1911
- function createPullRequest(card, branchName, worktreePath, config, provider) {
1986
+ function createPullRequest(card, branchName, worktreePath, config, provider, existingPrUrl) {
1987
+ if (existingPrUrl) {
1988
+ log.info(TAG2, `Reusing existing PR from card description: ${existingPrUrl}`);
1989
+ return existingPrUrl;
1990
+ }
1912
1991
  let commitLog = "";
1913
1992
  try {
1914
1993
  commitLog = execFileSync("git", ["log", "--oneline", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
@@ -2379,17 +2458,26 @@ function cleanupWorktree(worktreePath, branchName) {
2379
2458
  const repoRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
2380
2459
  encoding: "utf-8"
2381
2460
  }).trim();
2382
- try {
2383
- execFileSync3("git", ["worktree", "remove", worktreePath, "--force"], {
2384
- cwd: repoRoot,
2385
- stdio: "pipe"
2386
- });
2387
- log.info(TAG6, `Removed worktree: ${worktreePath}`);
2388
- } catch (err) {
2389
- log.warn(TAG6, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
2390
- if (existsSync2(worktreePath)) {
2391
- rmSync(worktreePath, { recursive: true, force: true });
2461
+ if (existsSync2(worktreePath)) {
2462
+ try {
2463
+ execFileSync3("git", ["worktree", "remove", worktreePath, "--force"], {
2464
+ cwd: repoRoot,
2465
+ stdio: "pipe"
2466
+ });
2467
+ log.info(TAG6, `Removed worktree: ${worktreePath}`);
2468
+ } catch (err) {
2469
+ log.warn(TAG6, `Failed to remove worktree cleanly: ${err instanceof Error ? err.message : err}`);
2470
+ if (existsSync2(worktreePath)) {
2471
+ rmSync(worktreePath, { recursive: true, force: true });
2472
+ }
2473
+ try {
2474
+ execFileSync3("git", ["worktree", "prune", "--expire=now"], {
2475
+ cwd: repoRoot,
2476
+ stdio: "pipe"
2477
+ });
2478
+ } catch {}
2392
2479
  }
2480
+ } else {
2393
2481
  try {
2394
2482
  execFileSync3("git", ["worktree", "prune", "--expire=now"], {
2395
2483
  cwd: repoRoot,
@@ -2411,9 +2499,19 @@ function resolveRepoRoot() {
2411
2499
  encoding: "utf-8"
2412
2500
  }).trim();
2413
2501
  }
2502
+ function localBranchExists(branchName, repoRoot) {
2503
+ try {
2504
+ execFileSync3("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot, stdio: "ignore" });
2505
+ return true;
2506
+ } catch {
2507
+ return false;
2508
+ }
2509
+ }
2414
2510
  function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
2511
+ if (!localBranchExists(branchName, repoRoot))
2512
+ return false;
2415
2513
  try {
2416
- const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8" }).trim();
2514
+ const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
2417
2515
  return out.length > 0;
2418
2516
  } catch {
2419
2517
  return false;
@@ -2544,12 +2642,9 @@ function checkoutExistingBranch(basePath, branchName) {
2544
2642
  return worktreeDir;
2545
2643
  }
2546
2644
  function extractBranchFromDescription(description) {
2547
- if (!description)
2548
- return null;
2549
- const branch = description.match(BRANCH_REF_PATTERN)?.[1] ?? null;
2550
- if (branch && !SAFE_GIT_REF_PATTERN.test(branch)) {
2551
- log.warn(TAG7, `Extracted branch name contains unsafe characters: ${branch}`);
2552
- return null;
2645
+ const branch = extractBranchRef(description);
2646
+ if (!branch && hasUnsafeDaemonBranchLine(description)) {
2647
+ log.warn(TAG7, "Daemon Branch: line contains unsafe characters; ignoring it");
2553
2648
  }
2554
2649
  return branch;
2555
2650
  }
@@ -2567,6 +2662,11 @@ async function resolveReviewBranch(description, cwd) {
2567
2662
  const resolved = await resolvePrHeadBranch(prUrl, cwd, provider);
2568
2663
  return resolved;
2569
2664
  }
2665
+ function reviewedFromPrUrl(description) {
2666
+ if (extractBranchFromDescription(description))
2667
+ return null;
2668
+ return extractPrUrl(description ?? null);
2669
+ }
2570
2670
  var TAG7 = "review-worktree";
2571
2671
  var init_review_worktree = __esm(() => {
2572
2672
  init_dist();
@@ -5458,7 +5558,7 @@ async function postReviewComment(client, card, commentType, body) {
5458
5558
  log.error(TAG20, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
5459
5559
  }
5460
5560
  }
5461
- async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore) {
5561
+ async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore, resolvedFromPrUrl) {
5462
5562
  let freshDesc;
5463
5563
  try {
5464
5564
  const { card: fresh } = await client.getCard(card.id);
@@ -5521,7 +5621,7 @@ ${runLogTail}
5521
5621
  }
5522
5622
  if (config.review.createPR && approvedBranch) {
5523
5623
  const provider = detectGitProvider(worktreePath);
5524
- prUrl = createPullRequest(card, approvedBranch, worktreePath, config, provider);
5624
+ prUrl = createPullRequest(card, approvedBranch, worktreePath, config, provider, resolvedFromPrUrl ?? null);
5525
5625
  }
5526
5626
  }
5527
5627
  await addLabelByName(client, card, config.review.approvedLabel, config.review.approvedLabelColor);
@@ -6601,7 +6701,7 @@ class ReviewWorker {
6601
6701
  currentTask: `Processing ${result.verdict} verdict`,
6602
6702
  progressPercent: 80
6603
6703
  });
6604
- await runReviewCompletion(this.client, card, result, this.config, cwd, this.branchName, sessionStats, this.lastRunLogPath, this.workspaceId, this.sessionId, this.stateStore);
6704
+ await runReviewCompletion(this.client, card, result, this.config, cwd, this.branchName, sessionStats, this.lastRunLogPath, this.workspaceId, this.sessionId, this.stateStore, reviewedFromPrUrl(card.description));
6605
6705
  await this.collectReviewGate(card, result);
6606
6706
  } catch (err) {
6607
6707
  this.state = "error";
@@ -7921,6 +8021,7 @@ class Worker {
7921
8021
  stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
7922
8022
  } : undefined;
7923
8023
  const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup);
8024
+ this.worktreePath = null;
7924
8025
  this.verificationFailed = !completed;
7925
8026
  if (completed && stageRun) {
7926
8027
  const outcome = await this.advanceFromGateEvaluation(card, stageRun.stage, stageRun.index, stageRun.def, stageGateEvaluation);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.19.0",
3
+ "version": "1.20.1",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",