@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.
- package/dist/cli.js +144 -43
- package/dist/index.js +144 -43
- 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
|
-
|
|
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
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
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
|
-
|
|
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
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
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
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
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
|
-
|
|
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
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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
|
-
|
|
1766
|
-
|
|
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
|
-
|
|
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
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
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
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
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);
|