automata-cli 0.7.0-develop.316 → 0.7.0-develop.321
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/index.js +1281 -1191
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1534,7 +1534,7 @@ function getCurrentBranchPr(branch) {
|
|
|
1534
1534
|
if (branch) {
|
|
1535
1535
|
args.push(branch);
|
|
1536
1536
|
}
|
|
1537
|
-
args.push("--json", "number,url,body");
|
|
1537
|
+
args.push("--json", "number,url,body,assignees");
|
|
1538
1538
|
const { stdout, stderr, status } = run3("gh", args);
|
|
1539
1539
|
if (status !== 0) {
|
|
1540
1540
|
if (stderr.includes("no pull requests found") || stderr.includes("Could not resolve")) {
|
|
@@ -1542,7 +1542,15 @@ function getCurrentBranchPr(branch) {
|
|
|
1542
1542
|
}
|
|
1543
1543
|
throw new Error(stderr.trim() || "Failed to query PR for current branch.");
|
|
1544
1544
|
}
|
|
1545
|
-
|
|
1545
|
+
const raw = JSON.parse(stdout);
|
|
1546
|
+
return {
|
|
1547
|
+
number: raw.number,
|
|
1548
|
+
url: raw.url,
|
|
1549
|
+
body: raw.body,
|
|
1550
|
+
// Absent when an older `gh` ignores the field, and `[]` is the right reading
|
|
1551
|
+
// of "nobody is assigned" either way.
|
|
1552
|
+
assignees: (raw.assignees ?? []).map((a) => a.login ?? "").filter((name) => name.length > 0)
|
|
1553
|
+
};
|
|
1546
1554
|
}
|
|
1547
1555
|
function addClosesRefToPr(prNumber, issueNumber) {
|
|
1548
1556
|
const { stdout, status: viewStatus } = run3("gh", [
|
|
@@ -1577,1335 +1585,1350 @@ function addCopilotReviewer(prNumber) {
|
|
|
1577
1585
|
}
|
|
1578
1586
|
}
|
|
1579
1587
|
|
|
1580
|
-
// src/
|
|
1581
|
-
import {
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
if (SHELL_SAFE.test(arg)) return arg;
|
|
1591
|
-
return "'" + arg.replaceAll("'", ESCAPED_QUOTE) + "'";
|
|
1592
|
-
}
|
|
1593
|
-
function truncate(str, max) {
|
|
1594
|
-
return str.length > max ? str.slice(0, max) + "..." : str;
|
|
1595
|
-
}
|
|
1596
|
-
function handleSpawnError(error, toolName) {
|
|
1597
|
-
if (!error) return;
|
|
1598
|
-
const err = error;
|
|
1599
|
-
if (err.code === "ENOENT") {
|
|
1600
|
-
process.stderr.write(`Error: \`${toolName}\` CLI is not installed or not on PATH.
|
|
1601
|
-
`);
|
|
1602
|
-
process.exit(1);
|
|
1588
|
+
// src/github/ghWorkService.ts
|
|
1589
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
1590
|
+
function run4(cmd, args) {
|
|
1591
|
+
const result = spawnSync4(cmd, args, { encoding: "utf8" });
|
|
1592
|
+
if (result.error) {
|
|
1593
|
+
const err = result.error;
|
|
1594
|
+
if (err.code === "ENOENT") {
|
|
1595
|
+
throw new Error(`\`${cmd}\` CLI is not installed or not on PATH.`);
|
|
1596
|
+
}
|
|
1597
|
+
throw new Error(err.message);
|
|
1603
1598
|
}
|
|
1604
|
-
|
|
1605
|
-
`);
|
|
1606
|
-
process.exit(1);
|
|
1599
|
+
return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", status: result.status ?? 1 };
|
|
1607
1600
|
}
|
|
1608
|
-
function
|
|
1609
|
-
|
|
1610
|
-
process.stderr.write(`Error: ${toolName} terminated abnormally (exit code is null, likely due to a signal).
|
|
1611
|
-
`);
|
|
1612
|
-
process.exit(1);
|
|
1613
|
-
}
|
|
1601
|
+
function ghJson(args, what) {
|
|
1602
|
+
const { stdout, stderr, status } = run4("gh", args);
|
|
1614
1603
|
if (status !== 0) {
|
|
1615
|
-
|
|
1616
|
-
`);
|
|
1617
|
-
process.exit(status);
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
function resolveEffortOption(value) {
|
|
1621
|
-
if (value === void 0) return void 0;
|
|
1622
|
-
const trimmed2 = value.trim();
|
|
1623
|
-
if (trimmed2.length === 0) {
|
|
1624
|
-
process.stderr.write("Error: --effort must be a non-empty level.\n");
|
|
1625
|
-
process.exit(1);
|
|
1604
|
+
throw new Error(stderr.trim() || `Failed to ${what}. Is \`gh\` installed and authenticated?`);
|
|
1626
1605
|
}
|
|
1627
|
-
return
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
// src/cli/childRegistry.ts
|
|
1631
|
-
var active = /* @__PURE__ */ new Set();
|
|
1632
|
-
function trackChild(child) {
|
|
1633
|
-
active.add(child);
|
|
1606
|
+
return JSON.parse(stdout);
|
|
1634
1607
|
}
|
|
1635
|
-
function
|
|
1636
|
-
|
|
1608
|
+
function login(author) {
|
|
1609
|
+
return author?.login ?? "";
|
|
1637
1610
|
}
|
|
1638
|
-
function
|
|
1639
|
-
return
|
|
1640
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1641
|
-
resolve2();
|
|
1642
|
-
return;
|
|
1643
|
-
}
|
|
1644
|
-
child.once("exit", () => resolve2());
|
|
1645
|
-
});
|
|
1611
|
+
function byCreatedAt(a, b) {
|
|
1612
|
+
return a.createdAt.localeCompare(b.createdAt);
|
|
1646
1613
|
}
|
|
1647
|
-
function
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
timer.unref();
|
|
1651
|
-
});
|
|
1614
|
+
function visibleAt(createdAt, submittedAt) {
|
|
1615
|
+
if (submittedAt === null || submittedAt === void 0) return createdAt;
|
|
1616
|
+
return submittedAt > createdAt ? submittedAt : createdAt;
|
|
1652
1617
|
}
|
|
1653
|
-
|
|
1654
|
-
const
|
|
1655
|
-
if (
|
|
1656
|
-
|
|
1657
|
-
for (const child of children) {
|
|
1658
|
-
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
1618
|
+
function getRepoSlug() {
|
|
1619
|
+
const { stdout, status } = run4("git", ["remote", "get-url", "origin"]);
|
|
1620
|
+
if (status !== 0) {
|
|
1621
|
+
throw new Error("Could not read the `origin` remote. Is this a git repository with a remote?");
|
|
1659
1622
|
}
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1623
|
+
const url = stdout.trim();
|
|
1624
|
+
const match = /github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/.exec(url) ?? /github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/.exec(url);
|
|
1625
|
+
if (!match) {
|
|
1626
|
+
throw new Error(`Could not determine the GitHub owner/repo from the origin remote: ${url}`);
|
|
1664
1627
|
}
|
|
1665
|
-
|
|
1666
|
-
Promise.all(exits).then(() => "exited"),
|
|
1667
|
-
afterDelay(killGraceMs)
|
|
1668
|
-
]);
|
|
1669
|
-
return escalated === "exited";
|
|
1628
|
+
return { owner: match[1], repo: match[2] };
|
|
1670
1629
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1630
|
+
function getAuthenticatedLogin() {
|
|
1631
|
+
const { stdout, status } = run4("gh", ["api", "user", "--jq", ".login"]);
|
|
1632
|
+
if (status !== 0) return null;
|
|
1633
|
+
const login2 = stdout.trim();
|
|
1634
|
+
return login2.length > 0 ? login2 : null;
|
|
1635
|
+
}
|
|
1636
|
+
function listCandidateIssues(technique, value, limit) {
|
|
1637
|
+
const args = [
|
|
1638
|
+
"issue",
|
|
1639
|
+
"list",
|
|
1640
|
+
"--state",
|
|
1641
|
+
"open",
|
|
1642
|
+
"--limit",
|
|
1643
|
+
String(limit),
|
|
1644
|
+
"--json",
|
|
1645
|
+
"number,title,body,url"
|
|
1646
|
+
];
|
|
1647
|
+
switch (technique) {
|
|
1648
|
+
case "label":
|
|
1649
|
+
args.push("--label", value);
|
|
1650
|
+
break;
|
|
1651
|
+
case "assignee":
|
|
1652
|
+
args.push("--assignee", value);
|
|
1653
|
+
break;
|
|
1654
|
+
case "title-contains":
|
|
1655
|
+
args.push("--search", `${value} in:title`);
|
|
1656
|
+
break;
|
|
1678
1657
|
}
|
|
1679
|
-
return
|
|
1658
|
+
return ghJson(args, "query GitHub issues");
|
|
1680
1659
|
}
|
|
1681
|
-
function
|
|
1682
|
-
const
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1660
|
+
function getIssueSurface(issueNumber) {
|
|
1661
|
+
const raw = ghJson(
|
|
1662
|
+
[
|
|
1663
|
+
"issue",
|
|
1664
|
+
"view",
|
|
1665
|
+
String(issueNumber),
|
|
1666
|
+
"--json",
|
|
1667
|
+
"number,title,body,url,state,author,createdAt,assignees,labels,comments"
|
|
1668
|
+
],
|
|
1669
|
+
`read issue #${String(issueNumber)}`
|
|
1670
|
+
);
|
|
1671
|
+
const messages = [
|
|
1672
|
+
{
|
|
1673
|
+
kind: "issue-body",
|
|
1674
|
+
author: login(raw.author),
|
|
1675
|
+
body: raw.body,
|
|
1676
|
+
createdAt: raw.createdAt
|
|
1677
|
+
},
|
|
1678
|
+
...(raw.comments ?? []).map((comment) => ({
|
|
1679
|
+
kind: "issue-comment",
|
|
1680
|
+
author: login(comment.author),
|
|
1681
|
+
body: comment.body,
|
|
1682
|
+
createdAt: comment.createdAt
|
|
1683
|
+
}))
|
|
1684
|
+
].sort(byCreatedAt);
|
|
1685
|
+
return {
|
|
1686
|
+
issue: { number: raw.number, title: raw.title, body: raw.body, url: raw.url },
|
|
1687
|
+
state: raw.state === "CLOSED" ? "CLOSED" : "OPEN",
|
|
1688
|
+
assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
|
|
1689
|
+
labels: (raw.labels ?? []).map((label) => label.name ?? "").filter((name) => name.length > 0),
|
|
1690
|
+
messages
|
|
1691
|
+
};
|
|
1689
1692
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
if (options.printSteps !== true) return;
|
|
1705
|
-
try {
|
|
1706
|
-
const event = JSON.parse(line);
|
|
1707
|
-
formatEvent(event, turnCount);
|
|
1708
|
-
if (event["type"] === "assistant") turnCount++;
|
|
1709
|
-
} catch {
|
|
1710
|
-
}
|
|
1711
|
-
});
|
|
1712
|
-
child.on("error", (err) => {
|
|
1713
|
-
untrackChild(child);
|
|
1714
|
-
const nodeErr = err;
|
|
1715
|
-
reject(
|
|
1716
|
-
nodeErr.code === "ENOENT" ? new Error("`claude` CLI is not installed or not on PATH.") : new Error(nodeErr.message)
|
|
1717
|
-
);
|
|
1718
|
-
});
|
|
1719
|
-
child.on("close", (code, signal) => {
|
|
1720
|
-
untrackChild(child);
|
|
1721
|
-
if (code === 0) {
|
|
1722
|
-
resolve2();
|
|
1723
|
-
return;
|
|
1693
|
+
var LINK_MAP_QUERY = `
|
|
1694
|
+
query($owner:String!,$repo:String!,$cursor:String){
|
|
1695
|
+
repository(owner:$owner,name:$repo){
|
|
1696
|
+
defaultBranchRef{ name }
|
|
1697
|
+
pullRequests(states:OPEN, first:100, after:$cursor, orderBy:{field:UPDATED_AT, direction:DESC}){
|
|
1698
|
+
pageInfo{ hasNextPage endCursor }
|
|
1699
|
+
nodes{
|
|
1700
|
+
number url title headRefName baseRefName isCrossRepository isDraft updatedAt
|
|
1701
|
+
labels(first:50){ nodes{ name } }
|
|
1702
|
+
assignees(first:50){ nodes{ login } }
|
|
1703
|
+
closingIssuesReferences(first:50){
|
|
1704
|
+
pageInfo{ hasNextPage }
|
|
1705
|
+
nodes{ number repository{ nameWithOwner } }
|
|
1706
|
+
}
|
|
1724
1707
|
}
|
|
1725
|
-
|
|
1726
|
-
new Error(
|
|
1727
|
-
signal === null ? `Claude Code exited with code ${String(code)}.` : `Claude Code terminated on ${signal}.`
|
|
1728
|
-
)
|
|
1729
|
-
);
|
|
1730
|
-
});
|
|
1731
|
-
});
|
|
1732
|
-
}
|
|
1733
|
-
function invokeClaudeCode(prompt, options = {}) {
|
|
1734
|
-
if (options.verbose) {
|
|
1735
|
-
return invokeClaudeCodeVerbose(prompt, options.yolo ?? false, options.model, options.effort);
|
|
1708
|
+
}
|
|
1736
1709
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
function
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
});
|
|
1756
|
-
rl.on("line", (line) => {
|
|
1757
|
-
try {
|
|
1758
|
-
const event = JSON.parse(line);
|
|
1759
|
-
formatEvent(event, turnCount);
|
|
1760
|
-
if (event["type"] === "assistant") turnCount++;
|
|
1761
|
-
} catch {
|
|
1762
|
-
}
|
|
1763
|
-
});
|
|
1764
|
-
child.on("close", (code) => {
|
|
1765
|
-
handleExitCode(code, "Claude Code");
|
|
1766
|
-
resolve2();
|
|
1767
|
-
});
|
|
1768
|
-
});
|
|
1769
|
-
}
|
|
1770
|
-
function formatAssistantEvent(event, turnCount) {
|
|
1771
|
-
const message = event["message"];
|
|
1772
|
-
const content = message?.["content"];
|
|
1773
|
-
if (!content) return;
|
|
1774
|
-
for (const block of content) {
|
|
1775
|
-
const line = formatContentBlock(block);
|
|
1776
|
-
if (line !== null) {
|
|
1777
|
-
process.stderr.write(` [step ${turnCount + 1}] ${line}
|
|
1778
|
-
`);
|
|
1710
|
+
}`.trim();
|
|
1711
|
+
var MAX_LINK_MAP_PAGES = 50;
|
|
1712
|
+
function indexPullRequest(map, node, nameWithOwner) {
|
|
1713
|
+
const ref = toPullRequestRef(node);
|
|
1714
|
+
if (node.closingIssuesReferences.pageInfo?.hasNextPage) {
|
|
1715
|
+
throw new Error(
|
|
1716
|
+
`Pull request #${String(node.number)} closes more than 50 issues, so the issue-to-pull-request map cannot be read completely. Refusing the tick rather than risk starting work on an issue that already has a pull request.`
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
let closedAnyHere = false;
|
|
1720
|
+
for (const issue of node.closingIssuesReferences.nodes) {
|
|
1721
|
+
if (issue.repository.nameWithOwner.toLowerCase() !== nameWithOwner.toLowerCase()) continue;
|
|
1722
|
+
closedAnyHere = true;
|
|
1723
|
+
const existing = map.get(issue.number);
|
|
1724
|
+
if (existing) {
|
|
1725
|
+
existing.push(ref);
|
|
1726
|
+
} else {
|
|
1727
|
+
map.set(issue.number, [ref]);
|
|
1779
1728
|
}
|
|
1780
1729
|
}
|
|
1730
|
+
return closedAnyHere;
|
|
1781
1731
|
}
|
|
1782
|
-
function
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
}
|
|
1794
|
-
return null;
|
|
1732
|
+
function toPullRequestRef(node) {
|
|
1733
|
+
return {
|
|
1734
|
+
number: node.number,
|
|
1735
|
+
url: node.url,
|
|
1736
|
+
title: node.title,
|
|
1737
|
+
headRefName: node.headRefName,
|
|
1738
|
+
baseRefName: node.baseRefName,
|
|
1739
|
+
isCrossRepository: node.isCrossRepository,
|
|
1740
|
+
state: "OPEN",
|
|
1741
|
+
isDraft: node.isDraft,
|
|
1742
|
+
updatedAt: node.updatedAt
|
|
1743
|
+
};
|
|
1795
1744
|
}
|
|
1796
|
-
function
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
const parts = [];
|
|
1803
|
-
if (turns !== void 0) parts.push(`${turns} turns`);
|
|
1804
|
-
if (duration !== void 0) parts.push(`${(duration / 1e3).toFixed(1)}s`);
|
|
1805
|
-
if (cost !== void 0) parts.push(`$${cost.toFixed(4)}`);
|
|
1806
|
-
if (parts.length > 0) {
|
|
1807
|
-
process.stderr.write(` [info] ${parts.join(" | ")}
|
|
1808
|
-
`);
|
|
1809
|
-
}
|
|
1810
|
-
if (result) {
|
|
1811
|
-
process.stdout.write(result + "\n");
|
|
1812
|
-
}
|
|
1745
|
+
function toOrphanPr(node) {
|
|
1746
|
+
return {
|
|
1747
|
+
pr: toPullRequestRef(node),
|
|
1748
|
+
labels: (node.labels?.nodes ?? []).map((label) => label.name ?? "").filter((name) => name.length > 0),
|
|
1749
|
+
assignees: (node.assignees?.nodes ?? []).map((assignee) => assignee.login ?? "").filter((login2) => login2.length > 0)
|
|
1750
|
+
};
|
|
1813
1751
|
}
|
|
1814
|
-
function
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1752
|
+
function getOpenPrLinkMap() {
|
|
1753
|
+
const { owner, repo } = getRepoSlug();
|
|
1754
|
+
const map = /* @__PURE__ */ new Map();
|
|
1755
|
+
const orphans = [];
|
|
1756
|
+
let defaultBranch = null;
|
|
1757
|
+
let cursor = null;
|
|
1758
|
+
for (let page = 0; page < MAX_LINK_MAP_PAGES; page++) {
|
|
1759
|
+
const args = ["api", "graphql", "-f", `query=${LINK_MAP_QUERY}`, "-f", `owner=${owner}`, "-f", `repo=${repo}`];
|
|
1760
|
+
if (cursor !== null) args.push("-f", `cursor=${cursor}`);
|
|
1761
|
+
const response = ghJson(args, "query open pull requests");
|
|
1762
|
+
defaultBranch = response.data.repository.defaultBranchRef?.name ?? defaultBranch;
|
|
1763
|
+
const connection = response.data.repository.pullRequests;
|
|
1764
|
+
for (const node of connection.nodes) {
|
|
1765
|
+
if (!indexPullRequest(map, node, `${owner}/${repo}`)) {
|
|
1766
|
+
orphans.push(toOrphanPr(node));
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
1770
|
+
return { byIssue: map, defaultBranch, orphans };
|
|
1771
|
+
}
|
|
1772
|
+
cursor = connection.pageInfo.endCursor;
|
|
1820
1773
|
}
|
|
1774
|
+
throw new Error(
|
|
1775
|
+
`Stopped paginating open pull requests after ${String(MAX_LINK_MAP_PAGES)} pages, so the issue-to-pull-request map cannot be trusted. Refusing the tick rather than risk starting work on an issue that already has a pull request.`
|
|
1776
|
+
);
|
|
1821
1777
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
return `searching files: ${asText(input["pattern"], "")}`;
|
|
1838
|
-
case "Grep":
|
|
1839
|
-
return `searching content: ${truncate(asText(input["pattern"], ""), 60)}`;
|
|
1840
|
-
case "Agent":
|
|
1841
|
-
return `spawning agent: ${asText(input["description"], name)}`;
|
|
1842
|
-
default:
|
|
1843
|
-
return `tool: ${name}`;
|
|
1778
|
+
var REVIEW_THREADS_QUERY2 = `
|
|
1779
|
+
query($owner:String!,$repo:String!,$prNumber:Int!,$cursor:String){
|
|
1780
|
+
repository(owner:$owner,name:$repo){
|
|
1781
|
+
pullRequest(number:$prNumber){
|
|
1782
|
+
reviewThreads(first:100, after:$cursor){
|
|
1783
|
+
pageInfo{ hasNextPage endCursor }
|
|
1784
|
+
nodes{
|
|
1785
|
+
isResolved isOutdated path line
|
|
1786
|
+
comments(last:100){
|
|
1787
|
+
pageInfo{ hasPreviousPage }
|
|
1788
|
+
nodes{ author{login} body createdAt url pullRequestReview{ submittedAt } }
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1844
1793
|
}
|
|
1794
|
+
}`.trim();
|
|
1795
|
+
var MAX_THREAD_PAGES = 50;
|
|
1796
|
+
function normalizePrState(state) {
|
|
1797
|
+
if (state === "MERGED") return "MERGED";
|
|
1798
|
+
if (state === "CLOSED") return "CLOSED";
|
|
1799
|
+
return "OPEN";
|
|
1845
1800
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1801
|
+
function getReviewThreads(prNumber) {
|
|
1802
|
+
const { owner, repo } = getRepoSlug();
|
|
1803
|
+
const threads = [];
|
|
1804
|
+
let cursor = null;
|
|
1805
|
+
for (let page = 0; page < MAX_THREAD_PAGES; page++) {
|
|
1806
|
+
const args = [
|
|
1807
|
+
"api",
|
|
1808
|
+
"graphql",
|
|
1809
|
+
"-f",
|
|
1810
|
+
`query=${REVIEW_THREADS_QUERY2}`,
|
|
1811
|
+
"-f",
|
|
1812
|
+
`owner=${owner}`,
|
|
1813
|
+
"-f",
|
|
1814
|
+
`repo=${repo}`,
|
|
1815
|
+
"-F",
|
|
1816
|
+
`prNumber=${String(prNumber)}`
|
|
1817
|
+
];
|
|
1818
|
+
if (cursor !== null) args.push("-f", `cursor=${cursor}`);
|
|
1819
|
+
const response = ghJson(
|
|
1820
|
+
args,
|
|
1821
|
+
`query review threads for pull request #${String(prNumber)}`
|
|
1822
|
+
);
|
|
1823
|
+
const connection = response.data.repository.pullRequest.reviewThreads;
|
|
1824
|
+
for (const node of connection.nodes) {
|
|
1825
|
+
if (node.comments.pageInfo?.hasPreviousPage) {
|
|
1826
|
+
throw new Error(
|
|
1827
|
+
`Review thread on ${node.path} in pull request #${String(prNumber)} has more than 100 comments, so the earliest ones were not read. Refusing rather than risk suppressing a maintainer's request.`
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
threads.push({
|
|
1831
|
+
path: node.path,
|
|
1832
|
+
line: node.line ?? null,
|
|
1833
|
+
isResolved: node.isResolved,
|
|
1834
|
+
url: node.comments.nodes.at(-1)?.url ?? null,
|
|
1835
|
+
comments: node.comments.nodes.map((comment) => ({
|
|
1836
|
+
kind: "thread-comment",
|
|
1837
|
+
author: login(comment.author),
|
|
1838
|
+
body: comment.body,
|
|
1839
|
+
// When the comment became *visible*, not when it was drafted.
|
|
1840
|
+
// GitHub stamps `createdAt` the moment a comment is added to a
|
|
1841
|
+
// pending review, and it only becomes visible when the review is
|
|
1842
|
+
// submitted — minutes later for a human working through a diff.
|
|
1843
|
+
// Using `createdAt` let an agent answer posted in between look newer
|
|
1844
|
+
// than the review, which marked every one of its threads answered
|
|
1845
|
+
// and discarded the whole review silently.
|
|
1846
|
+
createdAt: visibleAt(comment.createdAt, comment.pullRequestReview?.submittedAt)
|
|
1847
|
+
})).sort(byCreatedAt)
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
1851
|
+
return threads;
|
|
1852
|
+
}
|
|
1853
|
+
cursor = connection.pageInfo.endCursor;
|
|
1863
1854
|
}
|
|
1864
|
-
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
const codexBin = resolveCommand("codex");
|
|
1868
|
-
const args = buildCodexArgs(prompt, { yolo, model, effort });
|
|
1869
|
-
const result = spawnSync5(codexBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
1870
|
-
handleSpawnError(result.error, "codex");
|
|
1871
|
-
handleExitCode(result.status, "Codex");
|
|
1855
|
+
throw new Error(
|
|
1856
|
+
`Stopped paginating review threads for pull request #${String(prNumber)} after ${String(MAX_THREAD_PAGES)} pages; refusing rather than answering only part of the feedback.`
|
|
1857
|
+
);
|
|
1872
1858
|
}
|
|
1873
|
-
function
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1859
|
+
function getPrSurface(prNumber) {
|
|
1860
|
+
const raw = ghJson(
|
|
1861
|
+
[
|
|
1862
|
+
"pr",
|
|
1863
|
+
"view",
|
|
1864
|
+
String(prNumber),
|
|
1865
|
+
"--json",
|
|
1866
|
+
"number,title,url,headRefName,baseRefName,isCrossRepository,state,isDraft,body,author,createdAt,updatedAt,assignees,comments,reviews"
|
|
1867
|
+
],
|
|
1868
|
+
`read pull request #${String(prNumber)}`
|
|
1869
|
+
);
|
|
1870
|
+
const messages = [
|
|
1871
|
+
...(raw.comments ?? []).map((comment) => ({
|
|
1872
|
+
kind: "pr-comment",
|
|
1873
|
+
author: login(comment.author),
|
|
1874
|
+
body: comment.body,
|
|
1875
|
+
createdAt: comment.createdAt
|
|
1876
|
+
})),
|
|
1877
|
+
// A review with no body carries no message — only its inline comments do,
|
|
1878
|
+
// and those arrive through the review-thread query.
|
|
1879
|
+
...(raw.reviews ?? []).filter((review) => review.body.trim().length > 0).map((review) => ({
|
|
1880
|
+
kind: "pr-review",
|
|
1881
|
+
author: login(review.author),
|
|
1882
|
+
body: review.body,
|
|
1883
|
+
createdAt: review.submittedAt ?? review.createdAt ?? ""
|
|
1884
|
+
}))
|
|
1885
|
+
].sort(byCreatedAt);
|
|
1886
|
+
const threads = getReviewThreads(prNumber);
|
|
1887
|
+
const state = normalizePrState(raw.state);
|
|
1888
|
+
return {
|
|
1889
|
+
pr: {
|
|
1890
|
+
number: raw.number,
|
|
1891
|
+
url: raw.url,
|
|
1892
|
+
title: raw.title,
|
|
1893
|
+
headRefName: raw.headRefName,
|
|
1894
|
+
baseRefName: raw.baseRefName ?? "",
|
|
1895
|
+
isCrossRepository: raw.isCrossRepository ?? false,
|
|
1896
|
+
state,
|
|
1897
|
+
isDraft: raw.isDraft ?? false,
|
|
1898
|
+
updatedAt: raw.updatedAt ?? raw.createdAt
|
|
1899
|
+
},
|
|
1900
|
+
assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
|
|
1901
|
+
messages,
|
|
1902
|
+
threads
|
|
1903
|
+
};
|
|
1899
1904
|
}
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1905
|
+
function assignIssueToAgent(issueNumber, agentUser) {
|
|
1906
|
+
const { stderr, status } = run4("gh", [
|
|
1907
|
+
"issue",
|
|
1908
|
+
"edit",
|
|
1909
|
+
String(issueNumber),
|
|
1910
|
+
"--add-assignee",
|
|
1911
|
+
agentUser
|
|
1912
|
+
]);
|
|
1913
|
+
if (status !== 0) {
|
|
1914
|
+
throw new Error(stderr.trim() || `Failed to assign issue #${String(issueNumber)} to ${agentUser}.`);
|
|
1906
1915
|
}
|
|
1907
1916
|
}
|
|
1908
|
-
function
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
`)
|
|
1917
|
+
function assignPrToAgent(prNumber, agentUser) {
|
|
1918
|
+
const { stderr, status } = run4("gh", ["pr", "edit", String(prNumber), "--add-assignee", agentUser]);
|
|
1919
|
+
if (status !== 0) {
|
|
1920
|
+
throw new Error(
|
|
1921
|
+
stderr.trim() || `Failed to assign pull request #${String(prNumber)} to ${agentUser}.`
|
|
1922
|
+
);
|
|
1913
1923
|
}
|
|
1914
|
-
writeOverflowHint(output, issues, limit);
|
|
1915
1924
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
const
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1925
|
+
function postMarker(_surface, number, body) {
|
|
1926
|
+
const { owner, repo } = getRepoSlug();
|
|
1927
|
+
const raw = ghJson(
|
|
1928
|
+
[
|
|
1929
|
+
"api",
|
|
1930
|
+
"--method",
|
|
1931
|
+
"POST",
|
|
1932
|
+
`repos/${owner}/${repo}/issues/${String(number)}/comments`,
|
|
1933
|
+
"-f",
|
|
1934
|
+
`body=${body}`
|
|
1935
|
+
],
|
|
1936
|
+
`post a comment on #${String(number)}`
|
|
1922
1937
|
);
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1938
|
+
return { commentId: String(raw.id), createdAt: raw.created_at };
|
|
1939
|
+
}
|
|
1940
|
+
function updateMarker(marker, body) {
|
|
1941
|
+
const { owner, repo } = getRepoSlug();
|
|
1942
|
+
const { stderr, status } = run4("gh", [
|
|
1943
|
+
"api",
|
|
1944
|
+
"--method",
|
|
1945
|
+
"PATCH",
|
|
1946
|
+
`repos/${owner}/${repo}/issues/comments/${marker.commentId}`,
|
|
1947
|
+
"-f",
|
|
1948
|
+
`body=${body}`
|
|
1949
|
+
]);
|
|
1950
|
+
if (status !== 0) {
|
|
1951
|
+
throw new Error(stderr.trim() || `Failed to update comment ${marker.commentId}.`);
|
|
1929
1952
|
}
|
|
1930
|
-
return issues[n - 1];
|
|
1931
1953
|
}
|
|
1932
|
-
function
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1954
|
+
function deleteMarker(marker) {
|
|
1955
|
+
const { owner, repo } = getRepoSlug();
|
|
1956
|
+
const { stderr, status } = run4("gh", [
|
|
1957
|
+
"api",
|
|
1958
|
+
"--method",
|
|
1959
|
+
"DELETE",
|
|
1960
|
+
`repos/${owner}/${repo}/issues/comments/${marker.commentId}`
|
|
1961
|
+
]);
|
|
1962
|
+
if (status !== 0) {
|
|
1963
|
+
if (/not found/i.test(stderr) || /HTTP 404/i.test(stderr)) return;
|
|
1964
|
+
throw new Error(stderr.trim() || `Failed to delete comment ${marker.commentId}.`);
|
|
1938
1965
|
}
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1966
|
+
}
|
|
1967
|
+
function toHeadState(state) {
|
|
1968
|
+
return state === "OPEN" || state === "MERGED" ? state : "CLOSED";
|
|
1969
|
+
}
|
|
1970
|
+
function listPullRequestsForHead(branch) {
|
|
1971
|
+
const raw = ghJson(
|
|
1972
|
+
["pr", "list", "--head", branch, "--state", "all", "--json", "number,state,url,updatedAt,author"],
|
|
1973
|
+
`list pull requests for branch ${branch}`
|
|
1974
|
+
);
|
|
1975
|
+
return raw.map((pr) => ({
|
|
1976
|
+
number: pr.number,
|
|
1977
|
+
url: pr.url,
|
|
1978
|
+
state: toHeadState(pr.state),
|
|
1979
|
+
updatedAt: pr.updatedAt,
|
|
1980
|
+
author: pr.author?.login ?? ""
|
|
1981
|
+
})).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
1982
|
+
}
|
|
1983
|
+
var MISSING_LABEL_PATTERNS = [
|
|
1984
|
+
// gh's own message: `could not add label: 'rescue' not found`.
|
|
1985
|
+
/could not add label/i,
|
|
1986
|
+
// The GraphQL error it wraps, seen directly on some gh versions.
|
|
1987
|
+
/could not resolve to a label/i,
|
|
1988
|
+
/\blabels?\b[^\n]*\b(?:not found|does not exist)\b/i
|
|
1989
|
+
];
|
|
1990
|
+
function isMissingLabelError(stderr) {
|
|
1991
|
+
return MISSING_LABEL_PATTERNS.some((pattern) => pattern.test(stderr));
|
|
1992
|
+
}
|
|
1993
|
+
function createDraftPullRequest(input) {
|
|
1994
|
+
const base = [
|
|
1995
|
+
"pr",
|
|
1996
|
+
"create",
|
|
1997
|
+
"--draft",
|
|
1998
|
+
"--head",
|
|
1999
|
+
input.head,
|
|
2000
|
+
"--base",
|
|
2001
|
+
input.base,
|
|
2002
|
+
"--title",
|
|
2003
|
+
input.title,
|
|
2004
|
+
"--body",
|
|
2005
|
+
input.body
|
|
2006
|
+
];
|
|
2007
|
+
if (input.label !== void 0 && input.label.length > 0) {
|
|
2008
|
+
const labelled = run4("gh", [...base, "--label", input.label]);
|
|
2009
|
+
if (labelled.status === 0) return parseCreatedPrUrl(labelled.stdout, input.head);
|
|
2010
|
+
if (!isMissingLabelError(labelled.stderr)) {
|
|
2011
|
+
throw new Error(
|
|
2012
|
+
labelled.stderr.trim() || `Failed to open a draft pull request for ${input.head}.`
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
1944
2015
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
);
|
|
1949
|
-
process.exit(1);
|
|
2016
|
+
const { stdout, stderr, status } = run4("gh", base);
|
|
2017
|
+
if (status !== 0) {
|
|
2018
|
+
throw new Error(stderr.trim() || `Failed to open a draft pull request for ${input.head}.`);
|
|
1950
2019
|
}
|
|
2020
|
+
return parseCreatedPrUrl(stdout, input.head);
|
|
1951
2021
|
}
|
|
1952
|
-
|
|
1953
|
-
const
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2022
|
+
function parseCreatedPrUrl(stdout, head) {
|
|
2023
|
+
const url = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
2024
|
+
const match = /\/pull\/(\d+)\s*$/.exec(url);
|
|
2025
|
+
if (!match) {
|
|
2026
|
+
throw new Error(`Opened a pull request for ${head} but could not read its number from: ${url}`);
|
|
1957
2027
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2028
|
+
return { number: Number(match[1]), url };
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// src/claude/claudeService.ts
|
|
2032
|
+
import { spawn, spawnSync as spawnSync5 } from "child_process";
|
|
2033
|
+
import { createInterface } from "readline";
|
|
2034
|
+
import { existsSync } from "fs";
|
|
2035
|
+
import { delimiter, join } from "path";
|
|
2036
|
+
|
|
2037
|
+
// src/cli/spawnUtils.ts
|
|
2038
|
+
var SHELL_SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
2039
|
+
var ESCAPED_QUOTE = String.raw`'\''`;
|
|
2040
|
+
function shellQuote(arg) {
|
|
2041
|
+
if (SHELL_SAFE.test(arg)) return arg;
|
|
2042
|
+
return "'" + arg.replaceAll("'", ESCAPED_QUOTE) + "'";
|
|
2043
|
+
}
|
|
2044
|
+
function truncate(str, max) {
|
|
2045
|
+
return str.length > max ? str.slice(0, max) + "..." : str;
|
|
2046
|
+
}
|
|
2047
|
+
function handleSpawnError(error, toolName) {
|
|
2048
|
+
if (!error) return;
|
|
2049
|
+
const err = error;
|
|
2050
|
+
if (err.code === "ENOENT") {
|
|
2051
|
+
process.stderr.write(`Error: \`${toolName}\` CLI is not installed or not on PATH.
|
|
2052
|
+
`);
|
|
2053
|
+
process.exit(1);
|
|
1961
2054
|
}
|
|
1962
|
-
|
|
1963
|
-
if (issues.length === 1) {
|
|
1964
|
-
issue = issues[0];
|
|
1965
|
-
selectionOutput.write(`Issue: #${issue.number}
|
|
1966
|
-
Title: ${issue.title}
|
|
2055
|
+
process.stderr.write(`Error: ${err.message}
|
|
1967
2056
|
`);
|
|
1968
|
-
|
|
1969
|
-
issue = issues[0];
|
|
1970
|
-
selectionOutput.write(`Selecting issue #${issue.number}: ${issue.title}
|
|
1971
|
-
`);
|
|
1972
|
-
} else {
|
|
1973
|
-
issue = await promptSelection(issues, limit, selectionOutput);
|
|
1974
|
-
selectionOutput.write(`
|
|
1975
|
-
Issue: #${issue.number}
|
|
1976
|
-
Title: ${issue.title}
|
|
1977
|
-
`);
|
|
1978
|
-
}
|
|
1979
|
-
return issue;
|
|
2057
|
+
process.exit(1);
|
|
1980
2058
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
const limit = Number.parseInt(options.limit, 10);
|
|
1985
|
-
if (Number.isNaN(limit) || limit <= 0) {
|
|
1986
|
-
process.stderr.write(`Error: --limit must be a positive integer (got "${options.limit}").
|
|
1987
|
-
`);
|
|
1988
|
-
process.exit(1);
|
|
1989
|
-
}
|
|
1990
|
-
let issues;
|
|
1991
|
-
try {
|
|
1992
|
-
issues = listIssues(config.issueDiscoveryTechnique, config.issueDiscoveryValue, limit);
|
|
1993
|
-
} catch (err) {
|
|
1994
|
-
process.stderr.write(`Error: ${err.message}
|
|
2059
|
+
function handleExitCode(status, toolName) {
|
|
2060
|
+
if (status === null) {
|
|
2061
|
+
process.stderr.write(`Error: ${toolName} terminated abnormally (exit code is null, likely due to a signal).
|
|
1995
2062
|
`);
|
|
1996
2063
|
process.exit(1);
|
|
1997
2064
|
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
process.stdout.write(JSON.stringify({ number: issue.number, title: issue.title, body: issue.body, url: issue.url }, null, 2) + "\n");
|
|
2001
|
-
} else {
|
|
2002
|
-
process.stdout.write(`URL: ${issue.url}
|
|
2003
|
-
|
|
2004
|
-
${issue.body}
|
|
2065
|
+
if (status !== 0) {
|
|
2066
|
+
process.stderr.write(`Error: ${toolName} exited with code ${status}.
|
|
2005
2067
|
`);
|
|
2068
|
+
process.exit(status);
|
|
2006
2069
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
const
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
commentUrl = postComment(issue.number, "working");
|
|
2014
|
-
} catch (err) {
|
|
2015
|
-
process.stderr.write(`Error: ${err.message}
|
|
2016
|
-
`);
|
|
2070
|
+
}
|
|
2071
|
+
function resolveEffortOption(value) {
|
|
2072
|
+
if (value === void 0) return void 0;
|
|
2073
|
+
const trimmed2 = value.trim();
|
|
2074
|
+
if (trimmed2.length === 0) {
|
|
2075
|
+
process.stderr.write("Error: --effort must be a non-empty level.\n");
|
|
2017
2076
|
process.exit(1);
|
|
2018
2077
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
const systemPrompt = config.claudeSystemPrompt ?? DEFAULT_CLAUDE_SYSTEM_PROMPT;
|
|
2022
|
-
const prompt = `Resolving issue #${issue.number}:
|
|
2023
|
-
|
|
2024
|
-
${systemPrompt}
|
|
2078
|
+
return trimmed2;
|
|
2079
|
+
}
|
|
2025
2080
|
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
}
|
|
2031
|
-
invokeCodexCode(prompt, { yolo: options.yolo, model: options.model, effort });
|
|
2032
|
-
} else {
|
|
2033
|
-
await invokeClaudeCode(prompt, {
|
|
2034
|
-
yolo: options.yolo,
|
|
2035
|
-
verbose: !options.silent,
|
|
2036
|
-
model: options.model,
|
|
2037
|
-
effort
|
|
2038
|
-
});
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
linkPrToIssue(issue.number, commentUrl, options.askCopilotReview === true);
|
|
2042
|
-
});
|
|
2043
|
-
function resolveExecutor(requested) {
|
|
2044
|
-
const executor = requested.toLowerCase();
|
|
2045
|
-
if (executor !== "claude" && executor !== "codex") {
|
|
2046
|
-
process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${requested}'.
|
|
2047
|
-
`);
|
|
2048
|
-
process.exit(1);
|
|
2049
|
-
}
|
|
2050
|
-
return executor;
|
|
2081
|
+
// src/cli/childRegistry.ts
|
|
2082
|
+
var active = /* @__PURE__ */ new Set();
|
|
2083
|
+
function trackChild(child) {
|
|
2084
|
+
active.add(child);
|
|
2051
2085
|
}
|
|
2052
|
-
function
|
|
2053
|
-
|
|
2054
|
-
action();
|
|
2055
|
-
} catch (err) {
|
|
2056
|
-
process.stderr.write(`Warning: could not ${what}: ${err.message}
|
|
2057
|
-
`);
|
|
2058
|
-
}
|
|
2086
|
+
function untrackChild(child) {
|
|
2087
|
+
active.delete(child);
|
|
2059
2088
|
}
|
|
2060
|
-
function
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
return;
|
|
2068
|
-
}
|
|
2069
|
-
if (!pr) return;
|
|
2070
|
-
if (commentUrl) {
|
|
2071
|
-
warnOnFailure("update issue comment", () => {
|
|
2072
|
-
editComment(commentUrl, `Working on this in PR #${pr.number} \u2014 ${pr.url}`);
|
|
2073
|
-
});
|
|
2074
|
-
}
|
|
2075
|
-
warnOnFailure(`add Closes #${String(issueNumber)} to PR`, () => {
|
|
2076
|
-
addClosesRefToPr(pr.number, issueNumber);
|
|
2089
|
+
function waitForExit(child) {
|
|
2090
|
+
return new Promise((resolve2) => {
|
|
2091
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
2092
|
+
resolve2();
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
child.once("exit", () => resolve2());
|
|
2077
2096
|
});
|
|
2078
|
-
if (askCopilotReview) {
|
|
2079
|
-
warnOnFailure("request Copilot review", () => {
|
|
2080
|
-
addCopilotReviewer(pr.number);
|
|
2081
|
-
});
|
|
2082
|
-
}
|
|
2083
2097
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
function readStdin() {
|
|
2089
|
-
return new Promise((resolve2, reject) => {
|
|
2090
|
-
const chunks = [];
|
|
2091
|
-
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
2092
|
-
process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
2093
|
-
process.stdin.on("error", reject);
|
|
2098
|
+
function afterDelay(ms) {
|
|
2099
|
+
return new Promise((resolve2) => {
|
|
2100
|
+
const timer = setTimeout(() => resolve2("timeout"), ms);
|
|
2101
|
+
timer.unref();
|
|
2094
2102
|
});
|
|
2095
2103
|
}
|
|
2096
|
-
async function
|
|
2097
|
-
const
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
}
|
|
2103
|
-
if (hasCli) {
|
|
2104
|
-
return options.prompt;
|
|
2105
|
-
}
|
|
2106
|
-
if (hasFile) {
|
|
2107
|
-
const path = options.filePrompt;
|
|
2108
|
-
try {
|
|
2109
|
-
return readFileSync2(path, "utf8");
|
|
2110
|
-
} catch {
|
|
2111
|
-
process.stderr.write(`Error: Cannot read file: ${path}
|
|
2112
|
-
`);
|
|
2113
|
-
process.exit(1);
|
|
2114
|
-
}
|
|
2104
|
+
async function terminateTrackedChildren(timeoutMs = 1e4, killGraceMs = 5e3) {
|
|
2105
|
+
const children = [...active];
|
|
2106
|
+
if (children.length === 0) return true;
|
|
2107
|
+
const exits = children.map((child) => waitForExit(child));
|
|
2108
|
+
for (const child of children) {
|
|
2109
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
2115
2110
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
process.exit(1);
|
|
2121
|
-
}
|
|
2122
|
-
return content;
|
|
2111
|
+
const settled = await Promise.race([Promise.all(exits).then(() => "exited"), afterDelay(timeoutMs)]);
|
|
2112
|
+
if (settled === "exited") return true;
|
|
2113
|
+
for (const child of children) {
|
|
2114
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
2123
2115
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2116
|
+
const escalated = await Promise.race([
|
|
2117
|
+
Promise.all(exits).then(() => "exited"),
|
|
2118
|
+
afterDelay(killGraceMs)
|
|
2119
|
+
]);
|
|
2120
|
+
return escalated === "exited";
|
|
2126
2121
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2122
|
+
|
|
2123
|
+
// src/claude/claudeService.ts
|
|
2124
|
+
function resolveCommand(name) {
|
|
2125
|
+
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
2126
|
+
for (const dir of pathDirs) {
|
|
2127
|
+
const candidate = join(dir, name);
|
|
2128
|
+
if (existsSync(candidate)) return candidate;
|
|
2133
2129
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2130
|
+
return name;
|
|
2131
|
+
}
|
|
2132
|
+
function buildClaudeArgs(prompt, options = {}) {
|
|
2133
|
+
const args = [];
|
|
2134
|
+
if (options.yolo) args.push("--dangerously-skip-permissions");
|
|
2135
|
+
if (options.model) args.push("--model", options.model);
|
|
2136
|
+
if (options.effort) args.push("--effort", options.effort);
|
|
2137
|
+
if (options.verbose) args.push("--verbose", "--output-format", "stream-json");
|
|
2138
|
+
args.push("-p", prompt);
|
|
2139
|
+
return args;
|
|
2140
|
+
}
|
|
2141
|
+
function runClaude(prompt, options = {}) {
|
|
2142
|
+
return new Promise((resolve2, reject) => {
|
|
2143
|
+
const claudeBin = resolveCommand("claude");
|
|
2144
|
+
const args = buildClaudeArgs(prompt, {
|
|
2140
2145
|
yolo: true,
|
|
2141
|
-
verbose: !options.silent,
|
|
2142
2146
|
model: options.model,
|
|
2143
|
-
effort
|
|
2147
|
+
effort: options.effort,
|
|
2148
|
+
verbose: true
|
|
2144
2149
|
});
|
|
2150
|
+
const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
|
|
2151
|
+
trackChild(child);
|
|
2152
|
+
const rl = createInterface({ input: child.stdout });
|
|
2153
|
+
let turnCount = 0;
|
|
2154
|
+
rl.on("line", (line) => {
|
|
2155
|
+
if (options.printSteps !== true) return;
|
|
2156
|
+
try {
|
|
2157
|
+
const event = JSON.parse(line);
|
|
2158
|
+
formatEvent(event, turnCount);
|
|
2159
|
+
if (event["type"] === "assistant") turnCount++;
|
|
2160
|
+
} catch {
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
child.on("error", (err) => {
|
|
2164
|
+
untrackChild(child);
|
|
2165
|
+
const nodeErr = err;
|
|
2166
|
+
reject(
|
|
2167
|
+
nodeErr.code === "ENOENT" ? new Error("`claude` CLI is not installed or not on PATH.") : new Error(nodeErr.message)
|
|
2168
|
+
);
|
|
2169
|
+
});
|
|
2170
|
+
child.on("close", (code, signal) => {
|
|
2171
|
+
untrackChild(child);
|
|
2172
|
+
if (code === 0) {
|
|
2173
|
+
resolve2();
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
reject(
|
|
2177
|
+
new Error(
|
|
2178
|
+
signal === null ? `Claude Code exited with code ${String(code)}.` : `Claude Code terminated on ${signal}.`
|
|
2179
|
+
)
|
|
2180
|
+
);
|
|
2181
|
+
});
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
function invokeClaudeCode(prompt, options = {}) {
|
|
2185
|
+
if (options.verbose) {
|
|
2186
|
+
return invokeClaudeCodeVerbose(prompt, options.yolo ?? false, options.model, options.effort);
|
|
2145
2187
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
// src/commands/executePrompt.ts
|
|
2149
|
-
import { Command as Command5 } from "commander";
|
|
2150
|
-
|
|
2151
|
-
// src/github/conversation.ts
|
|
2152
|
-
var KIND_LABELS = {
|
|
2153
|
-
"issue-body": "issue description",
|
|
2154
|
-
"issue-comment": "comment",
|
|
2155
|
-
"pr-comment": "pull request comment",
|
|
2156
|
-
"pr-review": "pull request review",
|
|
2157
|
-
"thread-comment": "review thread comment"
|
|
2158
|
-
};
|
|
2159
|
-
function byCreatedAt(a, b) {
|
|
2160
|
-
return a.createdAt.localeCompare(b.createdAt);
|
|
2161
|
-
}
|
|
2162
|
-
function classify(author, p) {
|
|
2163
|
-
const login2 = author.toLowerCase();
|
|
2164
|
-
if (login2 === p.agentUser.toLowerCase()) return "agent";
|
|
2165
|
-
return p.allowedUsers.some((user) => user.toLowerCase() === login2) ? "authorized" : "other";
|
|
2166
|
-
}
|
|
2167
|
-
function analyzeSurface(messages, p) {
|
|
2168
|
-
const ordered = [...messages].sort(byCreatedAt);
|
|
2169
|
-
let lastAgentAt = null;
|
|
2170
|
-
for (const message of ordered) {
|
|
2171
|
-
if (message.kind === "issue-body") continue;
|
|
2172
|
-
if (classify(message.author, p) !== "agent") continue;
|
|
2173
|
-
if (lastAgentAt === null || message.createdAt > lastAgentAt) {
|
|
2174
|
-
lastAgentAt = message.createdAt;
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
const kept = [];
|
|
2178
|
-
for (const message of ordered) {
|
|
2179
|
-
const authorClass = classify(message.author, p);
|
|
2180
|
-
if (authorClass === "other") continue;
|
|
2181
|
-
const isNew = authorClass === "authorized" && (lastAgentAt === null || message.createdAt > lastAgentAt);
|
|
2182
|
-
kept.push({ ...message, isNew });
|
|
2183
|
-
}
|
|
2184
|
-
const newMessages = kept.filter((message) => message.isNew);
|
|
2185
|
-
return {
|
|
2186
|
-
messages: kept,
|
|
2187
|
-
newMessages,
|
|
2188
|
-
newMessageCount: newMessages.length,
|
|
2189
|
-
hasNewMessage: newMessages.length > 0,
|
|
2190
|
-
lastAgentAt
|
|
2191
|
-
};
|
|
2188
|
+
invokeClaudeCodeSync(prompt, options.yolo ?? false, options.model, options.effort);
|
|
2192
2189
|
}
|
|
2193
|
-
function
|
|
2194
|
-
|
|
2195
|
-
const
|
|
2196
|
-
|
|
2190
|
+
function invokeClaudeCodeSync(prompt, yolo, model, effort) {
|
|
2191
|
+
const claudeBin = resolveCommand("claude");
|
|
2192
|
+
const args = buildClaudeArgs(prompt, { yolo, model, effort, verbose: false });
|
|
2193
|
+
const result = spawnSync5(claudeBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
2194
|
+
handleSpawnError(result.error, "claude");
|
|
2195
|
+
handleExitCode(result.status, "Claude Code");
|
|
2197
2196
|
}
|
|
2198
|
-
function
|
|
2199
|
-
return
|
|
2200
|
-
const
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2197
|
+
function invokeClaudeCodeVerbose(prompt, yolo, model, effort) {
|
|
2198
|
+
return new Promise((resolve2) => {
|
|
2199
|
+
const claudeBin = resolveCommand("claude");
|
|
2200
|
+
const args = buildClaudeArgs(prompt, { yolo, model, effort, verbose: true });
|
|
2201
|
+
const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
|
|
2202
|
+
const rl = createInterface({ input: child.stdout });
|
|
2203
|
+
let turnCount = 0;
|
|
2204
|
+
child.on("error", (err) => {
|
|
2205
|
+
handleSpawnError(err, "claude");
|
|
2206
|
+
});
|
|
2207
|
+
rl.on("line", (line) => {
|
|
2208
|
+
try {
|
|
2209
|
+
const event = JSON.parse(line);
|
|
2210
|
+
formatEvent(event, turnCount);
|
|
2211
|
+
if (event["type"] === "assistant") turnCount++;
|
|
2212
|
+
} catch {
|
|
2213
|
+
}
|
|
2214
|
+
});
|
|
2215
|
+
child.on("close", (code) => {
|
|
2216
|
+
handleExitCode(code, "Claude Code");
|
|
2217
|
+
resolve2();
|
|
2218
|
+
});
|
|
2219
|
+
});
|
|
2204
2220
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
const
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
...conversation.comments.map((comment) => ({
|
|
2217
|
-
kind: "issue-comment",
|
|
2218
|
-
author: comment.author,
|
|
2219
|
-
body: comment.body,
|
|
2220
|
-
createdAt: comment.createdAt
|
|
2221
|
-
}))
|
|
2222
|
-
];
|
|
2223
|
-
const analysis = analyzeSurface(messages, participants);
|
|
2224
|
-
return {
|
|
2225
|
-
messages: analysis.messages.map(toConversationMessage),
|
|
2226
|
-
newMessageCount: analysis.newMessageCount,
|
|
2227
|
-
hasNewMessage: analysis.hasNewMessage,
|
|
2228
|
-
lastAgentAt: analysis.lastAgentAt
|
|
2229
|
-
};
|
|
2221
|
+
function formatAssistantEvent(event, turnCount) {
|
|
2222
|
+
const message = event["message"];
|
|
2223
|
+
const content = message?.["content"];
|
|
2224
|
+
if (!content) return;
|
|
2225
|
+
for (const block of content) {
|
|
2226
|
+
const line = formatContentBlock(block);
|
|
2227
|
+
if (line !== null) {
|
|
2228
|
+
process.stderr.write(` [step ${turnCount + 1}] ${line}
|
|
2229
|
+
`);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2230
2232
|
}
|
|
2231
|
-
function
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2233
|
+
function formatContentBlock(block) {
|
|
2234
|
+
if (block["type"] === "tool_use") {
|
|
2235
|
+
const toolName = block["name"];
|
|
2236
|
+
const input = block["input"];
|
|
2237
|
+
return summarizeTool(toolName, input);
|
|
2238
|
+
}
|
|
2239
|
+
if (block["type"] === "text") {
|
|
2240
|
+
const text = block["text"] ?? "";
|
|
2241
|
+
if (text.length === 0) return null;
|
|
2242
|
+
const preview = text.length > 120 ? text.slice(0, 120) + "..." : text;
|
|
2243
|
+
return preview.split("\n")[0] ?? null;
|
|
2244
|
+
}
|
|
2245
|
+
return null;
|
|
2239
2246
|
}
|
|
2240
|
-
function
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
);
|
|
2247
|
+
function formatResultEvent(event) {
|
|
2248
|
+
const result = event["result"];
|
|
2249
|
+
const cost = event["cost_usd"];
|
|
2250
|
+
const duration = event["duration_ms"];
|
|
2251
|
+
const turns = event["num_turns"];
|
|
2252
|
+
process.stderr.write("\n--- Result ---\n");
|
|
2253
|
+
const parts = [];
|
|
2254
|
+
if (turns !== void 0) parts.push(`${turns} turns`);
|
|
2255
|
+
if (duration !== void 0) parts.push(`${(duration / 1e3).toFixed(1)}s`);
|
|
2256
|
+
if (cost !== void 0) parts.push(`$${cost.toFixed(4)}`);
|
|
2257
|
+
if (parts.length > 0) {
|
|
2258
|
+
process.stderr.write(` [info] ${parts.join(" | ")}
|
|
2259
|
+
`);
|
|
2260
|
+
}
|
|
2261
|
+
if (result) {
|
|
2262
|
+
process.stdout.write(result + "\n");
|
|
2263
|
+
}
|
|
2250
2264
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2265
|
+
function formatEvent(event, turnCount) {
|
|
2266
|
+
const type = event["type"];
|
|
2267
|
+
if (type === "assistant") {
|
|
2268
|
+
formatAssistantEvent(event, turnCount);
|
|
2269
|
+
} else if (type === "result") {
|
|
2270
|
+
formatResultEvent(event);
|
|
2271
|
+
}
|
|
2258
2272
|
}
|
|
2259
|
-
function
|
|
2260
|
-
return
|
|
2273
|
+
function asText(value, fallback) {
|
|
2274
|
+
return typeof value === "string" ? value : fallback;
|
|
2261
2275
|
}
|
|
2262
|
-
function
|
|
2263
|
-
|
|
2276
|
+
function summarizeTool(name, input) {
|
|
2277
|
+
if (!input) return `tool: ${name}`;
|
|
2278
|
+
switch (name) {
|
|
2279
|
+
case "Read":
|
|
2280
|
+
return `reading ${asText(input["file_path"], "file")}`;
|
|
2281
|
+
case "Write":
|
|
2282
|
+
return `writing ${asText(input["file_path"], "file")}`;
|
|
2283
|
+
case "Edit":
|
|
2284
|
+
return `editing ${asText(input["file_path"], "file")}`;
|
|
2285
|
+
case "Bash":
|
|
2286
|
+
return `running: ${truncate(asText(input["command"], ""), 80)}`;
|
|
2287
|
+
case "Glob":
|
|
2288
|
+
return `searching files: ${asText(input["pattern"], "")}`;
|
|
2289
|
+
case "Grep":
|
|
2290
|
+
return `searching content: ${truncate(asText(input["pattern"], ""), 60)}`;
|
|
2291
|
+
case "Agent":
|
|
2292
|
+
return `spawning agent: ${asText(input["description"], name)}`;
|
|
2293
|
+
default:
|
|
2294
|
+
return `tool: ${name}`;
|
|
2295
|
+
}
|
|
2264
2296
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2297
|
+
|
|
2298
|
+
// src/codex/codexService.ts
|
|
2299
|
+
import { spawn as spawn2, spawnSync as spawnSync6 } from "child_process";
|
|
2300
|
+
function toTomlBasicString(value) {
|
|
2301
|
+
return JSON.stringify(value);
|
|
2267
2302
|
}
|
|
2268
|
-
function
|
|
2269
|
-
const
|
|
2270
|
-
if (
|
|
2271
|
-
|
|
2272
|
-
`);
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
return executor;
|
|
2303
|
+
function buildCodexArgs(prompt, options = {}) {
|
|
2304
|
+
const args = ["exec"];
|
|
2305
|
+
if (options.yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
2306
|
+
if (options.model) args.push("--model", options.model);
|
|
2307
|
+
if (options.effort) args.push("-c", `model_reasoning_effort=${toTomlBasicString(options.effort)}`);
|
|
2308
|
+
args.push(prompt);
|
|
2309
|
+
return args;
|
|
2276
2310
|
}
|
|
2277
|
-
function
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
invokeCodexCode(prompt, { yolo: true, model: options.model, effort });
|
|
2281
|
-
return;
|
|
2311
|
+
function invokeCodexCode(prompt, options = {}) {
|
|
2312
|
+
if (options.verbose) {
|
|
2313
|
+
process.stderr.write("Warning: --verbose is not supported for Codex and will be ignored.\n");
|
|
2282
2314
|
}
|
|
2283
|
-
|
|
2284
|
-
yolo: true,
|
|
2285
|
-
verbose: !options.silent,
|
|
2286
|
-
model: options.model,
|
|
2287
|
-
effort
|
|
2288
|
-
});
|
|
2315
|
+
invokeCodexCodeSync(prompt, options.yolo ?? false, options.model, options.effort);
|
|
2289
2316
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
)
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
let branch;
|
|
2297
|
-
try {
|
|
2298
|
-
branch = getCurrentBranch();
|
|
2299
|
-
} catch (err) {
|
|
2300
|
-
process.stderr.write(`Error: ${err.message}
|
|
2301
|
-
`);
|
|
2302
|
-
process.exit(1);
|
|
2303
|
-
}
|
|
2304
|
-
let pr;
|
|
2305
|
-
try {
|
|
2306
|
-
pr = await getPrInfo2(branch);
|
|
2307
|
-
} catch (err) {
|
|
2308
|
-
process.stderr.write(`Error: ${err.message}
|
|
2309
|
-
`);
|
|
2310
|
-
process.exit(1);
|
|
2311
|
-
}
|
|
2312
|
-
if (pr === null) {
|
|
2313
|
-
process.stderr.write(`Error: No pull request found for branch: ${branch}
|
|
2314
|
-
`);
|
|
2315
|
-
process.exit(1);
|
|
2316
|
-
}
|
|
2317
|
-
if (!pr.sonarcloudUrl) {
|
|
2318
|
-
process.stderr.write(
|
|
2319
|
-
`Error: No SonarCloud analysis found for PR #${pr.number}. Ensure a SonarCloud check is configured on this repository.
|
|
2320
|
-
`
|
|
2321
|
-
);
|
|
2322
|
-
process.exit(1);
|
|
2323
|
-
}
|
|
2324
|
-
const config = readConfig();
|
|
2325
|
-
const sonarPromptText = config.prompts?.sonar ?? DEFAULT_SONAR_PROMPT;
|
|
2326
|
-
const fullPrompt = withPush(
|
|
2327
|
-
`${sonarPromptText}
|
|
2328
|
-
|
|
2329
|
-
SonarCloud analysis URL: ${pr.sonarcloudUrl}
|
|
2330
|
-
|
|
2331
|
-
Current PR context from automata git get-pr-info --json:
|
|
2332
|
-
${formatPrInfoContext(pr)}`,
|
|
2333
|
-
options.push
|
|
2334
|
-
);
|
|
2335
|
-
await invokeSelectedExecutor(fullPrompt, executor, options);
|
|
2336
|
-
});
|
|
2337
|
-
function formatComments(comments) {
|
|
2338
|
-
return comments.map((c) => {
|
|
2339
|
-
const loc = c.line === null ? `${c.path}:(file)` : `${c.path}:${String(c.line)}`;
|
|
2340
|
-
return `[${c.author}] on ${loc}
|
|
2341
|
-
${c.body}`;
|
|
2342
|
-
}).join("\n\n");
|
|
2317
|
+
function invokeCodexCodeSync(prompt, yolo, model, effort) {
|
|
2318
|
+
const codexBin = resolveCommand("codex");
|
|
2319
|
+
const args = buildCodexArgs(prompt, { yolo, model, effort });
|
|
2320
|
+
const result = spawnSync6(codexBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
2321
|
+
handleSpawnError(result.error, "codex");
|
|
2322
|
+
handleExitCode(result.status, "Codex");
|
|
2343
2323
|
}
|
|
2344
|
-
|
|
2345
|
-
new
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
`)
|
|
2355
|
-
process.exit(1);
|
|
2356
|
-
}
|
|
2357
|
-
if (result.kind === "unsupported") {
|
|
2358
|
-
process.stderr.write(
|
|
2359
|
-
`Error: fix-comments is not supported for Azure DevOps. See docs/azdo-gap.md for details.
|
|
2360
|
-
`
|
|
2324
|
+
function runCodex(prompt, options = {}) {
|
|
2325
|
+
return new Promise((resolve2, reject) => {
|
|
2326
|
+
const codexBin = resolveCommand("codex");
|
|
2327
|
+
const args = buildCodexArgs(prompt, { yolo: true, model: options.model, effort: options.effort });
|
|
2328
|
+
const child = spawn2(codexBin, args, { stdio: "inherit" });
|
|
2329
|
+
trackChild(child);
|
|
2330
|
+
child.on("error", (err) => {
|
|
2331
|
+
untrackChild(child);
|
|
2332
|
+
const nodeErr = err;
|
|
2333
|
+
reject(
|
|
2334
|
+
nodeErr.code === "ENOENT" ? new Error("`codex` CLI is not installed or not on PATH.") : new Error(nodeErr.message)
|
|
2361
2335
|
);
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2336
|
+
});
|
|
2337
|
+
child.on("close", (code, signal) => {
|
|
2338
|
+
untrackChild(child);
|
|
2339
|
+
if (code === 0) {
|
|
2340
|
+
resolve2();
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
reject(
|
|
2344
|
+
new Error(
|
|
2345
|
+
signal === null ? `Codex exited with code ${String(code)}.` : `Codex terminated on ${signal}.`
|
|
2346
|
+
)
|
|
2347
|
+
);
|
|
2348
|
+
});
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// src/commands/getReady.ts
|
|
2353
|
+
function writeOverflowHint(output, issues, limit) {
|
|
2354
|
+
if (issues.length === limit) {
|
|
2355
|
+
output.write(`(Showing first ${limit} matching issues \u2014 there may be more. Use --limit to fetch more.)
|
|
2365
2356
|
`);
|
|
2366
|
-
process.exit(1);
|
|
2367
2357
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2358
|
+
}
|
|
2359
|
+
function writeIssueList(output, issues, limit) {
|
|
2360
|
+
output.write("\nAvailable issues:\n");
|
|
2361
|
+
for (let i = 0; i < issues.length; i++) {
|
|
2362
|
+
output.write(` [${i + 1}] #${issues[i].number} - ${issues[i].title}
|
|
2371
2363
|
`);
|
|
2372
|
-
process.exit(1);
|
|
2373
2364
|
}
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
${formatComments(comments)}`,
|
|
2384
|
-
options.push
|
|
2365
|
+
writeOverflowHint(output, issues, limit);
|
|
2366
|
+
}
|
|
2367
|
+
async function promptSelection(issues, limit, output) {
|
|
2368
|
+
writeIssueList(output, issues, limit);
|
|
2369
|
+
const rl = createInterface2({ input: process.stdin, output });
|
|
2370
|
+
const answer = await new Promise(
|
|
2371
|
+
(resolve2) => rl.question(`
|
|
2372
|
+
Select issue (1-${issues.length}): `, resolve2)
|
|
2385
2373
|
);
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
"Check a GitHub issue for a new message from an allowed user since the last agent run and invoke the AI with the issue conversation"
|
|
2391
|
-
).argument("<issue-number>", "GitHub issue number to check")
|
|
2392
|
-
).option("--force", "Skip the new-message check and invoke the AI directly").action(async (issueNumberArg, options) => {
|
|
2393
|
-
const executor = resolveExecutor2(options.with);
|
|
2394
|
-
const issueNumber = Number.parseInt(issueNumberArg, 10);
|
|
2395
|
-
if (Number.isNaN(issueNumber) || issueNumber <= 0) {
|
|
2396
|
-
process.stderr.write(`Error: <issue-number> must be a positive integer (got '${issueNumberArg}').
|
|
2374
|
+
rl.close();
|
|
2375
|
+
const n = Number.parseInt(answer.trim(), 10);
|
|
2376
|
+
if (Number.isNaN(n) || n < 1 || n > issues.length) {
|
|
2377
|
+
process.stderr.write(`Error: Invalid selection "${answer.trim()}". Enter a number between 1 and ${issues.length}.
|
|
2397
2378
|
`);
|
|
2398
2379
|
process.exit(1);
|
|
2399
2380
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2381
|
+
return issues[n - 1];
|
|
2382
|
+
}
|
|
2383
|
+
function validateConfig(config) {
|
|
2384
|
+
if (config.remoteType !== "gh") {
|
|
2402
2385
|
process.stderr.write(
|
|
2403
|
-
"Error:
|
|
2386
|
+
"Error: implement-next is not supported in Azure DevOps mode. Work item discovery is not available in azdo-cli. See docs/azdo-gap.md for details.\n"
|
|
2404
2387
|
);
|
|
2405
2388
|
process.exit(1);
|
|
2406
2389
|
}
|
|
2407
|
-
|
|
2408
|
-
if (allowedUsers.length === 0) {
|
|
2390
|
+
if (!config.issueDiscoveryTechnique) {
|
|
2409
2391
|
process.stderr.write(
|
|
2410
|
-
"Error: No
|
|
2392
|
+
"Error: No issue discovery technique configured. Run `automata config` to set one.\n"
|
|
2411
2393
|
);
|
|
2412
2394
|
process.exit(1);
|
|
2413
2395
|
}
|
|
2414
|
-
|
|
2415
|
-
if (agentUser.length === 0) {
|
|
2396
|
+
if (!config.issueDiscoveryValue) {
|
|
2416
2397
|
process.stderr.write(
|
|
2417
|
-
"Error: No
|
|
2398
|
+
"Error: No issue discovery value configured. Run `automata config` to set one.\n"
|
|
2418
2399
|
);
|
|
2419
2400
|
process.exit(1);
|
|
2420
2401
|
}
|
|
2421
|
-
|
|
2402
|
+
}
|
|
2403
|
+
async function resolveIssue(issues, options, limit) {
|
|
2404
|
+
const selectionOutput = options.json ? process.stderr : process.stdout;
|
|
2405
|
+
if (issues.length === 0) {
|
|
2406
|
+
process.stdout.write("No issues found matching the configured filter.\n");
|
|
2407
|
+
process.exit(0);
|
|
2408
|
+
}
|
|
2409
|
+
if (issues.length > 1 && options.queryOnly) {
|
|
2410
|
+
writeIssueList(selectionOutput, issues, limit);
|
|
2411
|
+
process.exit(0);
|
|
2412
|
+
}
|
|
2413
|
+
let issue;
|
|
2414
|
+
if (issues.length === 1) {
|
|
2415
|
+
issue = issues[0];
|
|
2416
|
+
selectionOutput.write(`Issue: #${issue.number}
|
|
2417
|
+
Title: ${issue.title}
|
|
2418
|
+
`);
|
|
2419
|
+
} else if (options.takeFirst) {
|
|
2420
|
+
issue = issues[0];
|
|
2421
|
+
selectionOutput.write(`Selecting issue #${issue.number}: ${issue.title}
|
|
2422
|
+
`);
|
|
2423
|
+
} else {
|
|
2424
|
+
issue = await promptSelection(issues, limit, selectionOutput);
|
|
2425
|
+
selectionOutput.write(`
|
|
2426
|
+
Issue: #${issue.number}
|
|
2427
|
+
Title: ${issue.title}
|
|
2428
|
+
`);
|
|
2429
|
+
}
|
|
2430
|
+
return issue;
|
|
2431
|
+
}
|
|
2432
|
+
var implementNextCommand = new Command3("implement-next").description("Find the next open GitHub issue matching the configured filter, claim it, and invoke the AI code assistant (Claude or Codex)").option("--json", "Output issue details as JSON").option("--no-claude", "Skip all AI invocation (Claude or Codex) after claiming the issue").option("--with <executor>", "Executor to use: claude or codex", "claude").option("--query-only", "Print issue content and exit without claiming or invoking any AI tools").option("--yolo", "Launch with --dangerously-skip-permissions (Claude) or --dangerously-bypass-approvals-and-sandbox (Codex)").option("--silent", "Suppress step-by-step Claude output; show only the final summary").option("--model <string>", "Model identifier to pass to the executor").option("--effort <level>", "Reasoning effort to pass to the executor").option("--take-first", "When multiple issues match, pick the first without prompting").option("--limit <n>", "Max issues to fetch and display (default: 10)", "10").option("--ask-copilot-review", "Request a Copilot code review on the PR after AI invocation finishes").action(async (options) => {
|
|
2433
|
+
const config = readConfig();
|
|
2434
|
+
validateConfig(config);
|
|
2435
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
2436
|
+
if (Number.isNaN(limit) || limit <= 0) {
|
|
2437
|
+
process.stderr.write(`Error: --limit must be a positive integer (got "${options.limit}").
|
|
2438
|
+
`);
|
|
2439
|
+
process.exit(1);
|
|
2440
|
+
}
|
|
2441
|
+
let issues;
|
|
2422
2442
|
try {
|
|
2423
|
-
|
|
2443
|
+
issues = listIssues(config.issueDiscoveryTechnique, config.issueDiscoveryValue, limit);
|
|
2424
2444
|
} catch (err) {
|
|
2425
2445
|
process.stderr.write(`Error: ${err.message}
|
|
2426
2446
|
`);
|
|
2427
2447
|
process.exit(1);
|
|
2428
2448
|
}
|
|
2429
|
-
const
|
|
2430
|
-
if (
|
|
2431
|
-
|
|
2432
|
-
process.stdout.write(
|
|
2433
|
-
`No new messages from allowed users on issue #${String(issueNumber)}${since}. Use --force to invoke the AI anyway.
|
|
2434
|
-
`
|
|
2435
|
-
);
|
|
2436
|
-
return;
|
|
2437
|
-
}
|
|
2438
|
-
if (analysis.hasNewMessage) {
|
|
2439
|
-
process.stdout.write(
|
|
2440
|
-
`Found ${String(analysis.newMessageCount)} new message${pluralSuffix(analysis.newMessageCount)} on issue #${String(issueNumber)}. Invoking AI\u2026
|
|
2441
|
-
`
|
|
2442
|
-
);
|
|
2449
|
+
const issue = await resolveIssue(issues, options, limit);
|
|
2450
|
+
if (options.json) {
|
|
2451
|
+
process.stdout.write(JSON.stringify({ number: issue.number, title: issue.title, body: issue.body, url: issue.url }, null, 2) + "\n");
|
|
2443
2452
|
} else {
|
|
2444
|
-
process.stdout.write(`
|
|
2453
|
+
process.stdout.write(`URL: ${issue.url}
|
|
2454
|
+
|
|
2455
|
+
${issue.body}
|
|
2445
2456
|
`);
|
|
2446
2457
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
URL: ${conversation.url}
|
|
2453
|
-
|
|
2454
|
-
Conversation (only messages from allowed users and the agent, oldest first):
|
|
2455
|
-
|
|
2456
|
-
${formatConversation(analysis.messages)}`,
|
|
2457
|
-
options.push
|
|
2458
|
-
);
|
|
2459
|
-
const marker = analysis.hasNewMessage ? `automata check-issue: picked up ${String(analysis.newMessageCount)} new message${pluralSuffix(analysis.newMessageCount)}, starting an agent run.` : "automata check-issue: forced run, starting an agent run.";
|
|
2458
|
+
if (options.queryOnly) {
|
|
2459
|
+
process.exit(0);
|
|
2460
|
+
}
|
|
2461
|
+
const executor = options.claude === false ? void 0 : resolveExecutor(options.with);
|
|
2462
|
+
let commentUrl;
|
|
2460
2463
|
try {
|
|
2461
|
-
postComment(
|
|
2464
|
+
commentUrl = postComment(issue.number, "working");
|
|
2462
2465
|
} catch (err) {
|
|
2463
|
-
process.stderr.write(
|
|
2464
|
-
|
|
2465
|
-
`
|
|
2466
|
-
);
|
|
2466
|
+
process.stderr.write(`Error: ${err.message}
|
|
2467
|
+
`);
|
|
2467
2468
|
process.exit(1);
|
|
2468
2469
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
// src/commands/doWork.ts
|
|
2474
|
-
import { Command as Command6 } from "commander";
|
|
2470
|
+
const effort = resolveEffortOption(options.effort);
|
|
2471
|
+
if (options.claude !== false) {
|
|
2472
|
+
const systemPrompt = config.claudeSystemPrompt ?? DEFAULT_CLAUDE_SYSTEM_PROMPT;
|
|
2473
|
+
const prompt = `Resolving issue #${issue.number}:
|
|
2475
2474
|
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2475
|
+
${systemPrompt}
|
|
2476
|
+
|
|
2477
|
+
${issue.body}`;
|
|
2478
|
+
if (executor === "codex") {
|
|
2479
|
+
if (options.silent) {
|
|
2480
|
+
process.stderr.write("Warning: --silent is only supported with Claude and has no effect when used with Codex.\n");
|
|
2481
|
+
}
|
|
2482
|
+
invokeCodexCode(prompt, { yolo: options.yolo, model: options.model, effort });
|
|
2483
|
+
} else {
|
|
2484
|
+
await invokeClaudeCode(prompt, {
|
|
2485
|
+
yolo: options.yolo,
|
|
2486
|
+
verbose: !options.silent,
|
|
2487
|
+
model: options.model,
|
|
2488
|
+
effort
|
|
2489
|
+
});
|
|
2484
2490
|
}
|
|
2485
|
-
throw new Error(err.message);
|
|
2486
2491
|
}
|
|
2487
|
-
|
|
2488
|
-
}
|
|
2489
|
-
function
|
|
2490
|
-
const
|
|
2491
|
-
if (
|
|
2492
|
-
|
|
2492
|
+
linkPrToIssue(issue.number, commentUrl, options.askCopilotReview === true, config.agentUser);
|
|
2493
|
+
});
|
|
2494
|
+
function resolveExecutor(requested) {
|
|
2495
|
+
const executor = requested.toLowerCase();
|
|
2496
|
+
if (executor !== "claude" && executor !== "codex") {
|
|
2497
|
+
process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${requested}'.
|
|
2498
|
+
`);
|
|
2499
|
+
process.exit(1);
|
|
2493
2500
|
}
|
|
2494
|
-
return
|
|
2495
|
-
}
|
|
2496
|
-
function login(author) {
|
|
2497
|
-
return author?.login ?? "";
|
|
2498
|
-
}
|
|
2499
|
-
function byCreatedAt2(a, b) {
|
|
2500
|
-
return a.createdAt.localeCompare(b.createdAt);
|
|
2501
|
-
}
|
|
2502
|
-
function visibleAt(createdAt, submittedAt) {
|
|
2503
|
-
if (submittedAt === null || submittedAt === void 0) return createdAt;
|
|
2504
|
-
return submittedAt > createdAt ? submittedAt : createdAt;
|
|
2501
|
+
return executor;
|
|
2505
2502
|
}
|
|
2506
|
-
function
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
const match = /github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/.exec(url) ?? /github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/.exec(url);
|
|
2513
|
-
if (!match) {
|
|
2514
|
-
throw new Error(`Could not determine the GitHub owner/repo from the origin remote: ${url}`);
|
|
2503
|
+
function warnOnFailure(what, action) {
|
|
2504
|
+
try {
|
|
2505
|
+
action();
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
process.stderr.write(`Warning: could not ${what}: ${err.message}
|
|
2508
|
+
`);
|
|
2515
2509
|
}
|
|
2516
|
-
return { owner: match[1], repo: match[2] };
|
|
2517
2510
|
}
|
|
2518
|
-
function
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
"issue",
|
|
2527
|
-
"list",
|
|
2528
|
-
"--state",
|
|
2529
|
-
"open",
|
|
2530
|
-
"--limit",
|
|
2531
|
-
String(limit),
|
|
2532
|
-
"--json",
|
|
2533
|
-
"number,title,body,url"
|
|
2534
|
-
];
|
|
2535
|
-
switch (technique) {
|
|
2536
|
-
case "label":
|
|
2537
|
-
args.push("--label", value);
|
|
2538
|
-
break;
|
|
2539
|
-
case "assignee":
|
|
2540
|
-
args.push("--assignee", value);
|
|
2541
|
-
break;
|
|
2542
|
-
case "title-contains":
|
|
2543
|
-
args.push("--search", `${value} in:title`);
|
|
2544
|
-
break;
|
|
2511
|
+
function linkPrToIssue(issueNumber, commentUrl, askCopilotReview, agentUser) {
|
|
2512
|
+
let pr;
|
|
2513
|
+
try {
|
|
2514
|
+
pr = getCurrentBranchPr();
|
|
2515
|
+
} catch (err) {
|
|
2516
|
+
process.stderr.write(`Warning: could not detect current branch PR: ${err.message}
|
|
2517
|
+
`);
|
|
2518
|
+
return;
|
|
2545
2519
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
"issue",
|
|
2552
|
-
"view",
|
|
2553
|
-
String(issueNumber),
|
|
2554
|
-
"--json",
|
|
2555
|
-
"number,title,body,url,state,author,createdAt,assignees,labels,comments"
|
|
2556
|
-
],
|
|
2557
|
-
`read issue #${String(issueNumber)}`
|
|
2558
|
-
);
|
|
2559
|
-
const messages = [
|
|
2560
|
-
{
|
|
2561
|
-
kind: "issue-body",
|
|
2562
|
-
author: login(raw.author),
|
|
2563
|
-
body: raw.body,
|
|
2564
|
-
createdAt: raw.createdAt
|
|
2565
|
-
},
|
|
2566
|
-
...(raw.comments ?? []).map((comment) => ({
|
|
2567
|
-
kind: "issue-comment",
|
|
2568
|
-
author: login(comment.author),
|
|
2569
|
-
body: comment.body,
|
|
2570
|
-
createdAt: comment.createdAt
|
|
2571
|
-
}))
|
|
2572
|
-
].sort(byCreatedAt2);
|
|
2573
|
-
return {
|
|
2574
|
-
issue: { number: raw.number, title: raw.title, body: raw.body, url: raw.url },
|
|
2575
|
-
state: raw.state === "CLOSED" ? "CLOSED" : "OPEN",
|
|
2576
|
-
assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
|
|
2577
|
-
labels: (raw.labels ?? []).map((label) => label.name ?? "").filter((name) => name.length > 0),
|
|
2578
|
-
messages
|
|
2579
|
-
};
|
|
2580
|
-
}
|
|
2581
|
-
var LINK_MAP_QUERY = `
|
|
2582
|
-
query($owner:String!,$repo:String!,$cursor:String){
|
|
2583
|
-
repository(owner:$owner,name:$repo){
|
|
2584
|
-
defaultBranchRef{ name }
|
|
2585
|
-
pullRequests(states:OPEN, first:100, after:$cursor, orderBy:{field:UPDATED_AT, direction:DESC}){
|
|
2586
|
-
pageInfo{ hasNextPage endCursor }
|
|
2587
|
-
nodes{
|
|
2588
|
-
number url title headRefName baseRefName isCrossRepository isDraft updatedAt
|
|
2589
|
-
labels(first:50){ nodes{ name } }
|
|
2590
|
-
assignees(first:50){ nodes{ login } }
|
|
2591
|
-
closingIssuesReferences(first:50){
|
|
2592
|
-
pageInfo{ hasNextPage }
|
|
2593
|
-
nodes{ number repository{ nameWithOwner } }
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2520
|
+
if (!pr) return;
|
|
2521
|
+
if (commentUrl) {
|
|
2522
|
+
warnOnFailure("update issue comment", () => {
|
|
2523
|
+
editComment(commentUrl, `Working on this in PR #${pr.number} \u2014 ${pr.url}`);
|
|
2524
|
+
});
|
|
2597
2525
|
}
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
);
|
|
2526
|
+
warnOnFailure(`add Closes #${String(issueNumber)} to PR`, () => {
|
|
2527
|
+
addClosesRefToPr(pr.number, issueNumber);
|
|
2528
|
+
});
|
|
2529
|
+
if (pr.assignees.length === 0) {
|
|
2530
|
+
const claimant = (agentUser ?? "").trim() || "@me";
|
|
2531
|
+
warnOnFailure(`assign PR #${String(pr.number)} to ${claimant}`, () => {
|
|
2532
|
+
assignPrToAgent(pr.number, claimant);
|
|
2533
|
+
});
|
|
2606
2534
|
}
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
const existing = map.get(issue.number);
|
|
2612
|
-
if (existing) {
|
|
2613
|
-
existing.push(ref);
|
|
2614
|
-
} else {
|
|
2615
|
-
map.set(issue.number, [ref]);
|
|
2616
|
-
}
|
|
2535
|
+
if (askCopilotReview) {
|
|
2536
|
+
warnOnFailure("request Copilot review", () => {
|
|
2537
|
+
addCopilotReviewer(pr.number);
|
|
2538
|
+
});
|
|
2617
2539
|
}
|
|
2618
|
-
return closedAnyHere;
|
|
2619
|
-
}
|
|
2620
|
-
function toPullRequestRef(node) {
|
|
2621
|
-
return {
|
|
2622
|
-
number: node.number,
|
|
2623
|
-
url: node.url,
|
|
2624
|
-
title: node.title,
|
|
2625
|
-
headRefName: node.headRefName,
|
|
2626
|
-
baseRefName: node.baseRefName,
|
|
2627
|
-
isCrossRepository: node.isCrossRepository,
|
|
2628
|
-
state: "OPEN",
|
|
2629
|
-
isDraft: node.isDraft,
|
|
2630
|
-
updatedAt: node.updatedAt
|
|
2631
|
-
};
|
|
2632
2540
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
let defaultBranch = null;
|
|
2645
|
-
let cursor = null;
|
|
2646
|
-
for (let page = 0; page < MAX_LINK_MAP_PAGES; page++) {
|
|
2647
|
-
const args = ["api", "graphql", "-f", `query=${LINK_MAP_QUERY}`, "-f", `owner=${owner}`, "-f", `repo=${repo}`];
|
|
2648
|
-
if (cursor !== null) args.push("-f", `cursor=${cursor}`);
|
|
2649
|
-
const response = ghJson(args, "query open pull requests");
|
|
2650
|
-
defaultBranch = response.data.repository.defaultBranchRef?.name ?? defaultBranch;
|
|
2651
|
-
const connection = response.data.repository.pullRequests;
|
|
2652
|
-
for (const node of connection.nodes) {
|
|
2653
|
-
if (!indexPullRequest(map, node, `${owner}/${repo}`)) {
|
|
2654
|
-
orphans.push(toOrphanPr(node));
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
2658
|
-
return { byIssue: map, defaultBranch, orphans };
|
|
2659
|
-
}
|
|
2660
|
-
cursor = connection.pageInfo.endCursor;
|
|
2661
|
-
}
|
|
2662
|
-
throw new Error(
|
|
2663
|
-
`Stopped paginating open pull requests after ${String(MAX_LINK_MAP_PAGES)} pages, so the issue-to-pull-request map cannot be trusted. Refusing the tick rather than risk starting work on an issue that already has a pull request.`
|
|
2664
|
-
);
|
|
2541
|
+
|
|
2542
|
+
// src/commands/execute.ts
|
|
2543
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2544
|
+
import { Command as Command4 } from "commander";
|
|
2545
|
+
function readStdin() {
|
|
2546
|
+
return new Promise((resolve2, reject) => {
|
|
2547
|
+
const chunks = [];
|
|
2548
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
2549
|
+
process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
2550
|
+
process.stdin.on("error", reject);
|
|
2551
|
+
});
|
|
2665
2552
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
nodes{
|
|
2673
|
-
isResolved isOutdated path line
|
|
2674
|
-
comments(last:100){
|
|
2675
|
-
pageInfo{ hasPreviousPage }
|
|
2676
|
-
nodes{ author{login} body createdAt url pullRequestReview{ submittedAt } }
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2553
|
+
async function resolvePrompt(options) {
|
|
2554
|
+
const hasCli = options.prompt !== void 0;
|
|
2555
|
+
const hasFile = options.filePrompt !== void 0;
|
|
2556
|
+
if (hasCli && hasFile) {
|
|
2557
|
+
process.stderr.write("Error: --prompt is mutually exclusive with --file-prompt.\n");
|
|
2558
|
+
process.exit(1);
|
|
2681
2559
|
}
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
if (
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
for (let page = 0; page < MAX_THREAD_PAGES; page++) {
|
|
2694
|
-
const args = [
|
|
2695
|
-
"api",
|
|
2696
|
-
"graphql",
|
|
2697
|
-
"-f",
|
|
2698
|
-
`query=${REVIEW_THREADS_QUERY2}`,
|
|
2699
|
-
"-f",
|
|
2700
|
-
`owner=${owner}`,
|
|
2701
|
-
"-f",
|
|
2702
|
-
`repo=${repo}`,
|
|
2703
|
-
"-F",
|
|
2704
|
-
`prNumber=${String(prNumber)}`
|
|
2705
|
-
];
|
|
2706
|
-
if (cursor !== null) args.push("-f", `cursor=${cursor}`);
|
|
2707
|
-
const response = ghJson(
|
|
2708
|
-
args,
|
|
2709
|
-
`query review threads for pull request #${String(prNumber)}`
|
|
2710
|
-
);
|
|
2711
|
-
const connection = response.data.repository.pullRequest.reviewThreads;
|
|
2712
|
-
for (const node of connection.nodes) {
|
|
2713
|
-
if (node.comments.pageInfo?.hasPreviousPage) {
|
|
2714
|
-
throw new Error(
|
|
2715
|
-
`Review thread on ${node.path} in pull request #${String(prNumber)} has more than 100 comments, so the earliest ones were not read. Refusing rather than risk suppressing a maintainer's request.`
|
|
2716
|
-
);
|
|
2717
|
-
}
|
|
2718
|
-
threads.push({
|
|
2719
|
-
path: node.path,
|
|
2720
|
-
line: node.line ?? null,
|
|
2721
|
-
isResolved: node.isResolved,
|
|
2722
|
-
url: node.comments.nodes.at(-1)?.url ?? null,
|
|
2723
|
-
comments: node.comments.nodes.map((comment) => ({
|
|
2724
|
-
kind: "thread-comment",
|
|
2725
|
-
author: login(comment.author),
|
|
2726
|
-
body: comment.body,
|
|
2727
|
-
// When the comment became *visible*, not when it was drafted.
|
|
2728
|
-
// GitHub stamps `createdAt` the moment a comment is added to a
|
|
2729
|
-
// pending review, and it only becomes visible when the review is
|
|
2730
|
-
// submitted — minutes later for a human working through a diff.
|
|
2731
|
-
// Using `createdAt` let an agent answer posted in between look newer
|
|
2732
|
-
// than the review, which marked every one of its threads answered
|
|
2733
|
-
// and discarded the whole review silently.
|
|
2734
|
-
createdAt: visibleAt(comment.createdAt, comment.pullRequestReview?.submittedAt)
|
|
2735
|
-
})).sort(byCreatedAt2)
|
|
2736
|
-
});
|
|
2560
|
+
if (hasCli) {
|
|
2561
|
+
return options.prompt;
|
|
2562
|
+
}
|
|
2563
|
+
if (hasFile) {
|
|
2564
|
+
const path = options.filePrompt;
|
|
2565
|
+
try {
|
|
2566
|
+
return readFileSync2(path, "utf8");
|
|
2567
|
+
} catch {
|
|
2568
|
+
process.stderr.write(`Error: Cannot read file: ${path}
|
|
2569
|
+
`);
|
|
2570
|
+
process.exit(1);
|
|
2737
2571
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2572
|
+
}
|
|
2573
|
+
if (!process.stdin.isTTY) {
|
|
2574
|
+
const content = await readStdin();
|
|
2575
|
+
if (content.trim().length === 0) {
|
|
2576
|
+
process.stderr.write("Error: No prompt provided. Use --prompt, --file-prompt, or pipe via stdin.\n");
|
|
2577
|
+
process.exit(1);
|
|
2740
2578
|
}
|
|
2741
|
-
|
|
2579
|
+
return content;
|
|
2742
2580
|
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
);
|
|
2581
|
+
process.stderr.write("Error: No prompt provided. Use --prompt, --file-prompt, or pipe via stdin.\n");
|
|
2582
|
+
process.exit(1);
|
|
2746
2583
|
}
|
|
2747
|
-
|
|
2748
|
-
const
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2584
|
+
var executeCommand = new Command4("execute").description("Delegate work to an AI executor").requiredOption("--with <executor>", "Executor to use: claude or codex").option("--prompt <string>", "Prompt to send to the executor").option("--file-prompt <path>", "Path to a file whose content is used as the prompt").option("--silent", "Suppress step-by-step Claude output; show only the final summary").option("--model <string>", "Model identifier to pass to the executor").option("--effort <level>", "Reasoning effort to pass to the executor").action(async (options) => {
|
|
2585
|
+
const executor = options.with.toLowerCase();
|
|
2586
|
+
if (executor !== "claude" && executor !== "codex") {
|
|
2587
|
+
process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${options.with}'.
|
|
2588
|
+
`);
|
|
2589
|
+
process.exit(1);
|
|
2590
|
+
}
|
|
2591
|
+
const prompt = await resolvePrompt(options);
|
|
2592
|
+
const effort = resolveEffortOption(options.effort);
|
|
2593
|
+
if (executor === "codex") {
|
|
2594
|
+
invokeCodexCode(prompt, { yolo: true, model: options.model, effort });
|
|
2595
|
+
} else {
|
|
2596
|
+
await invokeClaudeCode(prompt, {
|
|
2597
|
+
yolo: true,
|
|
2598
|
+
verbose: !options.silent,
|
|
2599
|
+
model: options.model,
|
|
2600
|
+
effort
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
});
|
|
2604
|
+
|
|
2605
|
+
// src/commands/executePrompt.ts
|
|
2606
|
+
import { Command as Command5 } from "commander";
|
|
2607
|
+
|
|
2608
|
+
// src/github/conversation.ts
|
|
2609
|
+
var KIND_LABELS = {
|
|
2610
|
+
"issue-body": "issue description",
|
|
2611
|
+
"issue-comment": "comment",
|
|
2612
|
+
"pr-comment": "pull request comment",
|
|
2613
|
+
"pr-review": "pull request review",
|
|
2614
|
+
"thread-comment": "review thread comment"
|
|
2615
|
+
};
|
|
2616
|
+
function byCreatedAt2(a, b) {
|
|
2617
|
+
return a.createdAt.localeCompare(b.createdAt);
|
|
2618
|
+
}
|
|
2619
|
+
function classify(author, p) {
|
|
2620
|
+
const login2 = author.toLowerCase();
|
|
2621
|
+
if (login2 === p.agentUser.toLowerCase()) return "agent";
|
|
2622
|
+
return p.allowedUsers.some((user) => user.toLowerCase() === login2) ? "authorized" : "other";
|
|
2623
|
+
}
|
|
2624
|
+
function analyzeSurface(messages, p) {
|
|
2625
|
+
const ordered = [...messages].sort(byCreatedAt2);
|
|
2626
|
+
let lastAgentAt = null;
|
|
2627
|
+
for (const message of ordered) {
|
|
2628
|
+
if (message.kind === "issue-body") continue;
|
|
2629
|
+
if (classify(message.author, p) !== "agent") continue;
|
|
2630
|
+
if (lastAgentAt === null || message.createdAt > lastAgentAt) {
|
|
2631
|
+
lastAgentAt = message.createdAt;
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
const kept = [];
|
|
2635
|
+
for (const message of ordered) {
|
|
2636
|
+
const authorClass = classify(message.author, p);
|
|
2637
|
+
if (authorClass === "other") continue;
|
|
2638
|
+
const isNew = authorClass === "authorized" && (lastAgentAt === null || message.createdAt > lastAgentAt);
|
|
2639
|
+
kept.push({ ...message, isNew });
|
|
2640
|
+
}
|
|
2641
|
+
const newMessages = kept.filter((message) => message.isNew);
|
|
2642
|
+
return {
|
|
2643
|
+
messages: kept,
|
|
2644
|
+
newMessages,
|
|
2645
|
+
newMessageCount: newMessages.length,
|
|
2646
|
+
hasNewMessage: newMessages.length > 0,
|
|
2647
|
+
lastAgentAt
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
function lastAuthorClass(messages, p) {
|
|
2651
|
+
if (messages.length === 0) return "none";
|
|
2652
|
+
const newest2 = [...messages].sort(byCreatedAt2).at(-1);
|
|
2653
|
+
return newest2 === void 0 ? "none" : classify(newest2.author, p);
|
|
2654
|
+
}
|
|
2655
|
+
function formatMessages(messages) {
|
|
2656
|
+
return messages.map((message) => {
|
|
2657
|
+
const marker = message.isNew ? " \xB7 NEW since last agent run" : "";
|
|
2658
|
+
return `[${message.author}] ${KIND_LABELS[message.kind]} \xB7 ${message.createdAt}${marker}
|
|
2659
|
+
${message.body}`;
|
|
2660
|
+
}).join("\n\n");
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
// src/github/issueConversation.ts
|
|
2664
|
+
function analyzeConversation(conversation, allowedUsers, agentUser) {
|
|
2665
|
+
const participants = { allowedUsers, agentUser };
|
|
2758
2666
|
const messages = [
|
|
2759
|
-
|
|
2760
|
-
kind: "
|
|
2761
|
-
author:
|
|
2667
|
+
{
|
|
2668
|
+
kind: "issue-body",
|
|
2669
|
+
author: conversation.author,
|
|
2670
|
+
body: conversation.body,
|
|
2671
|
+
createdAt: conversation.createdAt
|
|
2672
|
+
},
|
|
2673
|
+
...conversation.comments.map((comment) => ({
|
|
2674
|
+
kind: "issue-comment",
|
|
2675
|
+
author: comment.author,
|
|
2762
2676
|
body: comment.body,
|
|
2763
2677
|
createdAt: comment.createdAt
|
|
2764
|
-
})),
|
|
2765
|
-
// A review with no body carries no message — only its inline comments do,
|
|
2766
|
-
// and those arrive through the review-thread query.
|
|
2767
|
-
...(raw.reviews ?? []).filter((review) => review.body.trim().length > 0).map((review) => ({
|
|
2768
|
-
kind: "pr-review",
|
|
2769
|
-
author: login(review.author),
|
|
2770
|
-
body: review.body,
|
|
2771
|
-
createdAt: review.submittedAt ?? review.createdAt ?? ""
|
|
2772
2678
|
}))
|
|
2773
|
-
]
|
|
2774
|
-
const
|
|
2775
|
-
const state = normalizePrState(raw.state);
|
|
2679
|
+
];
|
|
2680
|
+
const analysis = analyzeSurface(messages, participants);
|
|
2776
2681
|
return {
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
headRefName: raw.headRefName,
|
|
2782
|
-
baseRefName: raw.baseRefName ?? "",
|
|
2783
|
-
isCrossRepository: raw.isCrossRepository ?? false,
|
|
2784
|
-
state,
|
|
2785
|
-
isDraft: raw.isDraft ?? false,
|
|
2786
|
-
updatedAt: raw.updatedAt ?? raw.createdAt
|
|
2787
|
-
},
|
|
2788
|
-
messages,
|
|
2789
|
-
threads
|
|
2682
|
+
messages: analysis.messages.map(toConversationMessage),
|
|
2683
|
+
newMessageCount: analysis.newMessageCount,
|
|
2684
|
+
hasNewMessage: analysis.hasNewMessage,
|
|
2685
|
+
lastAgentAt: analysis.lastAgentAt
|
|
2790
2686
|
};
|
|
2791
2687
|
}
|
|
2792
|
-
function
|
|
2793
|
-
|
|
2794
|
-
"issue",
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
if (status !== 0) {
|
|
2801
|
-
throw new Error(stderr.trim() || `Failed to assign issue #${String(issueNumber)} to ${agentUser}.`);
|
|
2802
|
-
}
|
|
2688
|
+
function toConversationMessage(message) {
|
|
2689
|
+
return {
|
|
2690
|
+
kind: message.kind === "issue-body" ? "issue" : "comment",
|
|
2691
|
+
author: message.author,
|
|
2692
|
+
body: message.body,
|
|
2693
|
+
createdAt: message.createdAt,
|
|
2694
|
+
isNew: message.isNew
|
|
2695
|
+
};
|
|
2803
2696
|
}
|
|
2804
|
-
function
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
`body=${body}`
|
|
2814
|
-
],
|
|
2815
|
-
`post a comment on #${String(number)}`
|
|
2697
|
+
function formatConversation(messages) {
|
|
2698
|
+
return formatMessages(
|
|
2699
|
+
messages.map((message) => ({
|
|
2700
|
+
kind: message.kind === "issue" ? "issue-body" : "issue-comment",
|
|
2701
|
+
author: message.author,
|
|
2702
|
+
body: message.body,
|
|
2703
|
+
createdAt: message.createdAt,
|
|
2704
|
+
isNew: message.isNew
|
|
2705
|
+
}))
|
|
2816
2706
|
);
|
|
2817
|
-
return { commentId: String(raw.id), createdAt: raw.created_at };
|
|
2818
2707
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2708
|
+
|
|
2709
|
+
// src/commands/executePrompt.ts
|
|
2710
|
+
var PUSH_INSTRUCTION = "Once all changes are complete, stage every modified file, create a single commit with a clear and descriptive commit message that summarises what was fixed, and push the branch to the remote.";
|
|
2711
|
+
function withPush(prompt, push) {
|
|
2712
|
+
return push ? `${prompt}
|
|
2713
|
+
|
|
2714
|
+
${PUSH_INSTRUCTION}` : prompt;
|
|
2715
|
+
}
|
|
2716
|
+
function formatPrInfoContext(pr) {
|
|
2717
|
+
return JSON.stringify(pr, null, 2);
|
|
2718
|
+
}
|
|
2719
|
+
function pluralSuffix(count) {
|
|
2720
|
+
return count === 1 ? "" : "s";
|
|
2721
|
+
}
|
|
2722
|
+
function addAiOptions(cmd) {
|
|
2723
|
+
return cmd.requiredOption("--with <executor>", "Executor to use: claude or codex").option("--model <string>", "Model identifier to pass to the executor").option("--effort <level>", "Reasoning effort to pass to the executor").option("--silent", "Suppress step-by-step Claude output; show only the final summary").option("--push", "Append instruction to commit and push changes after the AI finishes");
|
|
2724
|
+
}
|
|
2725
|
+
function resolveExecutor2(withOption) {
|
|
2726
|
+
const executor = withOption.toLowerCase();
|
|
2727
|
+
if (executor !== "claude" && executor !== "codex") {
|
|
2728
|
+
process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${withOption}'.
|
|
2729
|
+
`);
|
|
2730
|
+
process.exit(1);
|
|
2731
|
+
}
|
|
2732
|
+
return executor;
|
|
2733
|
+
}
|
|
2734
|
+
function invokeSelectedExecutor(prompt, executor, options) {
|
|
2735
|
+
const effort = resolveEffortOption(options.effort);
|
|
2736
|
+
if (executor === "codex") {
|
|
2737
|
+
invokeCodexCode(prompt, { yolo: true, model: options.model, effort });
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
return invokeClaudeCode(prompt, {
|
|
2741
|
+
yolo: true,
|
|
2742
|
+
verbose: !options.silent,
|
|
2743
|
+
model: options.model,
|
|
2744
|
+
effort
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
var executeSonarCmd = addAiOptions(
|
|
2748
|
+
new Command5("sonar").description(
|
|
2749
|
+
"Check the current branch for a SonarCloud analysis and invoke the AI with the Sonar prompt and analysis URL"
|
|
2750
|
+
)
|
|
2751
|
+
).action(async (options) => {
|
|
2752
|
+
const executor = resolveExecutor2(options.with);
|
|
2753
|
+
let branch;
|
|
2754
|
+
try {
|
|
2755
|
+
branch = getCurrentBranch();
|
|
2756
|
+
} catch (err) {
|
|
2757
|
+
process.stderr.write(`Error: ${err.message}
|
|
2758
|
+
`);
|
|
2759
|
+
process.exit(1);
|
|
2831
2760
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
`repos/${owner}/${repo}/issues/comments/${marker.commentId}`
|
|
2840
|
-
]);
|
|
2841
|
-
if (status !== 0) {
|
|
2842
|
-
if (/not found/i.test(stderr) || /HTTP 404/i.test(stderr)) return;
|
|
2843
|
-
throw new Error(stderr.trim() || `Failed to delete comment ${marker.commentId}.`);
|
|
2761
|
+
let pr;
|
|
2762
|
+
try {
|
|
2763
|
+
pr = await getPrInfo2(branch);
|
|
2764
|
+
} catch (err) {
|
|
2765
|
+
process.stderr.write(`Error: ${err.message}
|
|
2766
|
+
`);
|
|
2767
|
+
process.exit(1);
|
|
2844
2768
|
}
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2769
|
+
if (pr === null) {
|
|
2770
|
+
process.stderr.write(`Error: No pull request found for branch: ${branch}
|
|
2771
|
+
`);
|
|
2772
|
+
process.exit(1);
|
|
2773
|
+
}
|
|
2774
|
+
if (!pr.sonarcloudUrl) {
|
|
2775
|
+
process.stderr.write(
|
|
2776
|
+
`Error: No SonarCloud analysis found for PR #${pr.number}. Ensure a SonarCloud check is configured on this repository.
|
|
2777
|
+
`
|
|
2778
|
+
);
|
|
2779
|
+
process.exit(1);
|
|
2780
|
+
}
|
|
2781
|
+
const config = readConfig();
|
|
2782
|
+
const sonarPromptText = config.prompts?.sonar ?? DEFAULT_SONAR_PROMPT;
|
|
2783
|
+
const fullPrompt = withPush(
|
|
2784
|
+
`${sonarPromptText}
|
|
2785
|
+
|
|
2786
|
+
SonarCloud analysis URL: ${pr.sonarcloudUrl}
|
|
2787
|
+
|
|
2788
|
+
Current PR context from automata git get-pr-info --json:
|
|
2789
|
+
${formatPrInfoContext(pr)}`,
|
|
2790
|
+
options.push
|
|
2853
2791
|
);
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
}
|
|
2862
|
-
var MISSING_LABEL_PATTERNS = [
|
|
2863
|
-
// gh's own message: `could not add label: 'rescue' not found`.
|
|
2864
|
-
/could not add label/i,
|
|
2865
|
-
// The GraphQL error it wraps, seen directly on some gh versions.
|
|
2866
|
-
/could not resolve to a label/i,
|
|
2867
|
-
/\blabels?\b[^\n]*\b(?:not found|does not exist)\b/i
|
|
2868
|
-
];
|
|
2869
|
-
function isMissingLabelError(stderr) {
|
|
2870
|
-
return MISSING_LABEL_PATTERNS.some((pattern) => pattern.test(stderr));
|
|
2792
|
+
await invokeSelectedExecutor(fullPrompt, executor, options);
|
|
2793
|
+
});
|
|
2794
|
+
function formatComments(comments) {
|
|
2795
|
+
return comments.map((c) => {
|
|
2796
|
+
const loc = c.line === null ? `${c.path}:(file)` : `${c.path}:${String(c.line)}`;
|
|
2797
|
+
return `[${c.author}] on ${loc}
|
|
2798
|
+
${c.body}`;
|
|
2799
|
+
}).join("\n\n");
|
|
2871
2800
|
}
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
"
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
if (!isMissingLabelError(labelled.stderr)) {
|
|
2890
|
-
throw new Error(
|
|
2891
|
-
labelled.stderr.trim() || `Failed to open a draft pull request for ${input.head}.`
|
|
2801
|
+
var executeFixCommentsCmd = addAiOptions(
|
|
2802
|
+
new Command5("fix-comments").description(
|
|
2803
|
+
"Fetch open review comments on the current PR and invoke the AI with the Fix-Comments prompt"
|
|
2804
|
+
)
|
|
2805
|
+
).action(async (options) => {
|
|
2806
|
+
const executor = resolveExecutor2(options.with);
|
|
2807
|
+
const result = resolveCurrentBranchComments();
|
|
2808
|
+
if (!result.ok) {
|
|
2809
|
+
if (result.kind === "error") {
|
|
2810
|
+
process.stderr.write(`Error: ${result.message}
|
|
2811
|
+
`);
|
|
2812
|
+
process.exit(1);
|
|
2813
|
+
}
|
|
2814
|
+
if (result.kind === "unsupported") {
|
|
2815
|
+
process.stderr.write(
|
|
2816
|
+
`Error: fix-comments is not supported for Azure DevOps. See docs/azdo-gap.md for details.
|
|
2817
|
+
`
|
|
2892
2818
|
);
|
|
2819
|
+
process.exit(1);
|
|
2893
2820
|
}
|
|
2821
|
+
process.stderr.write(`Error: No pull request found for branch: ${result.branch}
|
|
2822
|
+
`);
|
|
2823
|
+
process.exit(1);
|
|
2894
2824
|
}
|
|
2895
|
-
const {
|
|
2896
|
-
if (
|
|
2897
|
-
|
|
2825
|
+
const { comments } = result;
|
|
2826
|
+
if (comments.length === 0) {
|
|
2827
|
+
process.stderr.write(`Error: No open review comments found on the pull request.
|
|
2828
|
+
`);
|
|
2829
|
+
process.exit(1);
|
|
2898
2830
|
}
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
const
|
|
2903
|
-
const
|
|
2904
|
-
|
|
2905
|
-
|
|
2831
|
+
process.stdout.write(`Found ${String(comments.length)} open review comment${comments.length === 1 ? "" : "s"} on PR. Invoking AI\u2026
|
|
2832
|
+
`);
|
|
2833
|
+
const config = readConfig();
|
|
2834
|
+
const promptText = config.prompts?.fixComments ?? DEFAULT_FIX_COMMENTS_PROMPT;
|
|
2835
|
+
const fullPrompt = withPush(
|
|
2836
|
+
`${promptText}
|
|
2837
|
+
|
|
2838
|
+
Open review comments:
|
|
2839
|
+
|
|
2840
|
+
${formatComments(comments)}`,
|
|
2841
|
+
options.push
|
|
2842
|
+
);
|
|
2843
|
+
await invokeSelectedExecutor(fullPrompt, executor, options);
|
|
2844
|
+
});
|
|
2845
|
+
var executeCheckIssueCmd = addAiOptions(
|
|
2846
|
+
new Command5("check-issue").description(
|
|
2847
|
+
"Check a GitHub issue for a new message from an allowed user since the last agent run and invoke the AI with the issue conversation"
|
|
2848
|
+
).argument("<issue-number>", "GitHub issue number to check")
|
|
2849
|
+
).option("--force", "Skip the new-message check and invoke the AI directly").action(async (issueNumberArg, options) => {
|
|
2850
|
+
const executor = resolveExecutor2(options.with);
|
|
2851
|
+
const issueNumber = Number.parseInt(issueNumberArg, 10);
|
|
2852
|
+
if (Number.isNaN(issueNumber) || issueNumber <= 0) {
|
|
2853
|
+
process.stderr.write(`Error: <issue-number> must be a positive integer (got '${issueNumberArg}').
|
|
2854
|
+
`);
|
|
2855
|
+
process.exit(1);
|
|
2906
2856
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2857
|
+
const config = readConfig();
|
|
2858
|
+
if (config.remoteType === "azdo") {
|
|
2859
|
+
process.stderr.write(
|
|
2860
|
+
"Error: check-issue is not supported for Azure DevOps. See docs/azdo-gap.md for details.\n"
|
|
2861
|
+
);
|
|
2862
|
+
process.exit(1);
|
|
2863
|
+
}
|
|
2864
|
+
const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
|
|
2865
|
+
if (allowedUsers.length === 0) {
|
|
2866
|
+
process.stderr.write(
|
|
2867
|
+
"Error: No allowed users configured. Run `automata config` or `automata config set allowed-users <user1,user2>` to set them.\n"
|
|
2868
|
+
);
|
|
2869
|
+
process.exit(1);
|
|
2870
|
+
}
|
|
2871
|
+
const agentUser = (config.agentUser ?? "").trim();
|
|
2872
|
+
if (agentUser.length === 0) {
|
|
2873
|
+
process.stderr.write(
|
|
2874
|
+
"Error: No agent user configured. Run `automata config` or `automata config set agent-user <login>` to set it.\n"
|
|
2875
|
+
);
|
|
2876
|
+
process.exit(1);
|
|
2877
|
+
}
|
|
2878
|
+
let conversation;
|
|
2879
|
+
try {
|
|
2880
|
+
conversation = getIssueConversation(issueNumber);
|
|
2881
|
+
} catch (err) {
|
|
2882
|
+
process.stderr.write(`Error: ${err.message}
|
|
2883
|
+
`);
|
|
2884
|
+
process.exit(1);
|
|
2885
|
+
}
|
|
2886
|
+
const analysis = analyzeConversation(conversation, allowedUsers, agentUser);
|
|
2887
|
+
if (!analysis.hasNewMessage && !options.force) {
|
|
2888
|
+
const since = analysis.lastAgentAt === null ? "" : ` (last agent message: ${analysis.lastAgentAt})`;
|
|
2889
|
+
process.stdout.write(
|
|
2890
|
+
`No new messages from allowed users on issue #${String(issueNumber)}${since}. Use --force to invoke the AI anyway.
|
|
2891
|
+
`
|
|
2892
|
+
);
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
if (analysis.hasNewMessage) {
|
|
2896
|
+
process.stdout.write(
|
|
2897
|
+
`Found ${String(analysis.newMessageCount)} new message${pluralSuffix(analysis.newMessageCount)} on issue #${String(issueNumber)}. Invoking AI\u2026
|
|
2898
|
+
`
|
|
2899
|
+
);
|
|
2900
|
+
} else {
|
|
2901
|
+
process.stdout.write(`No new messages on issue #${String(issueNumber)} \u2014 forced run. Invoking AI\u2026
|
|
2902
|
+
`);
|
|
2903
|
+
}
|
|
2904
|
+
const promptText = config.prompts?.checkIssue ?? DEFAULT_CHECK_ISSUE_PROMPT;
|
|
2905
|
+
const fullPrompt = withPush(
|
|
2906
|
+
`${promptText}
|
|
2907
|
+
|
|
2908
|
+
Issue #${String(issueNumber)}: ${conversation.title}
|
|
2909
|
+
URL: ${conversation.url}
|
|
2910
|
+
|
|
2911
|
+
Conversation (only messages from allowed users and the agent, oldest first):
|
|
2912
|
+
|
|
2913
|
+
${formatConversation(analysis.messages)}`,
|
|
2914
|
+
options.push
|
|
2915
|
+
);
|
|
2916
|
+
const marker = analysis.hasNewMessage ? `automata check-issue: picked up ${String(analysis.newMessageCount)} new message${pluralSuffix(analysis.newMessageCount)}, starting an agent run.` : "automata check-issue: forced run, starting an agent run.";
|
|
2917
|
+
try {
|
|
2918
|
+
postComment(issueNumber, marker);
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
process.stderr.write(
|
|
2921
|
+
`Error: could not post the execution marker comment on issue #${String(issueNumber)}: ${err.message}
|
|
2922
|
+
`
|
|
2923
|
+
);
|
|
2924
|
+
process.exit(1);
|
|
2925
|
+
}
|
|
2926
|
+
await invokeSelectedExecutor(fullPrompt, executor, options);
|
|
2927
|
+
});
|
|
2928
|
+
var executePromptCommand = new Command5("execute-prompt").description("Execute a configured custom prompt using an AI assistant").addCommand(executeSonarCmd).addCommand(executeFixCommentsCmd).addCommand(executeCheckIssueCmd);
|
|
2929
|
+
|
|
2930
|
+
// src/commands/doWork.ts
|
|
2931
|
+
import { Command as Command6 } from "commander";
|
|
2909
2932
|
|
|
2910
2933
|
// src/github/workDetection.ts
|
|
2911
2934
|
var NO_ISSUE_MESSAGES = {
|
|
@@ -2915,10 +2938,6 @@ var NO_ISSUE_MESSAGES = {
|
|
|
2915
2938
|
hasNewMessage: false,
|
|
2916
2939
|
lastAgentAt: null
|
|
2917
2940
|
};
|
|
2918
|
-
function isAssignedToAgent(assignees, agentUser) {
|
|
2919
|
-
const agent = agentUser.toLowerCase();
|
|
2920
|
-
return assignees.some((name) => name.toLowerCase() === agent);
|
|
2921
|
-
}
|
|
2922
2941
|
function findActionableThreads(threads, p, prLastAgentAt) {
|
|
2923
2942
|
const actionable = [];
|
|
2924
2943
|
for (const thread of threads) {
|
|
@@ -2983,7 +3002,7 @@ function decideWork(state, p, policy) {
|
|
|
2983
3002
|
return { kind: "skip", issue, pr: null, reason: "issue-closed", detail: "the issue is closed" };
|
|
2984
3003
|
}
|
|
2985
3004
|
const issueAnalysis = analyzeSurface(issueSurface.messages, p);
|
|
2986
|
-
const needsAssignment =
|
|
3005
|
+
const needsAssignment = issueSurface.assignees.length === 0;
|
|
2987
3006
|
const openPrs = state.linkedPrs.filter((pr) => pr.state === "OPEN");
|
|
2988
3007
|
const hasOpenPr = openPrs.length > 0 && prSurface !== null && prSurface.pr.state === "OPEN";
|
|
2989
3008
|
if (!hasOpenPr) {
|
|
@@ -3004,6 +3023,7 @@ function decideWork(state, p, policy) {
|
|
|
3004
3023
|
pr: null,
|
|
3005
3024
|
branch: baseBranch,
|
|
3006
3025
|
needsAssignment,
|
|
3026
|
+
prNeedsAssignment: false,
|
|
3007
3027
|
issueAnalysis,
|
|
3008
3028
|
prAnalysis: null,
|
|
3009
3029
|
actionableThreads: [],
|
|
@@ -3044,6 +3064,7 @@ function decideWork(state, p, policy) {
|
|
|
3044
3064
|
pr: surface.pr,
|
|
3045
3065
|
branch: surface.pr.headRefName,
|
|
3046
3066
|
needsAssignment,
|
|
3067
|
+
prNeedsAssignment: surface.assignees.length === 0,
|
|
3047
3068
|
issueAnalysis,
|
|
3048
3069
|
prAnalysis,
|
|
3049
3070
|
actionableThreads,
|
|
@@ -3097,6 +3118,13 @@ function decideOrphanPrWork(state, p, policy) {
|
|
|
3097
3118
|
// agent here would change what the discovery filter matches next tick.
|
|
3098
3119
|
// The `working…` marker on the pull request is the claim.
|
|
3099
3120
|
needsAssignment: false,
|
|
3121
|
+
// For the same reason, and more directly: the orphan pass discovers by
|
|
3122
|
+
// the *pull request's own* assignees, so claiming an unassigned orphan
|
|
3123
|
+
// would make it match the filter on the next tick — the agent would
|
|
3124
|
+
// permanently own a pull request the operator never opted in. A build
|
|
3125
|
+
// turn is safe because it reaches its pull request through an issue that
|
|
3126
|
+
// matched the filter, not through the pull request's assignees.
|
|
3127
|
+
prNeedsAssignment: false,
|
|
3100
3128
|
issueAnalysis: NO_ISSUE_MESSAGES,
|
|
3101
3129
|
prAnalysis,
|
|
3102
3130
|
actionableThreads,
|
|
@@ -3129,6 +3157,30 @@ function selectLinkedPr(linkedPrs) {
|
|
|
3129
3157
|
const open = linkedPrs.filter((pr) => pr.state === "OPEN");
|
|
3130
3158
|
return open.length === 0 ? null : newestPr(open);
|
|
3131
3159
|
}
|
|
3160
|
+
function claimStates(item) {
|
|
3161
|
+
const states = [];
|
|
3162
|
+
if (item.issue !== null) {
|
|
3163
|
+
states.push({
|
|
3164
|
+
surface: "issue",
|
|
3165
|
+
number: item.issue.number,
|
|
3166
|
+
state: item.needsAssignment ? "would-claim" : "already-assigned"
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
if (item.pr !== null) {
|
|
3170
|
+
states.push({
|
|
3171
|
+
surface: "pull request",
|
|
3172
|
+
number: item.pr.number,
|
|
3173
|
+
// An exemption is a rule, not a state, so it outranks the assignee list:
|
|
3174
|
+
// an orphan pull request with nobody on it is still not claimed.
|
|
3175
|
+
state: prClaimState(item)
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
return states;
|
|
3179
|
+
}
|
|
3180
|
+
function prClaimState(item) {
|
|
3181
|
+
if (item.turn === "pr-orphan") return "rule-exempt";
|
|
3182
|
+
return item.prNeedsAssignment ? "would-claim" : "already-assigned";
|
|
3183
|
+
}
|
|
3132
3184
|
|
|
3133
3185
|
// src/github/workPrompt.ts
|
|
3134
3186
|
function promptBaseBranch(item, configuredBaseBranch) {
|
|
@@ -4271,10 +4323,32 @@ function planRun(item, settings, execution) {
|
|
|
4271
4323
|
});
|
|
4272
4324
|
return { prompt, bin, args, command: [bin, ...args].map(shellQuote).join(" ") };
|
|
4273
4325
|
}
|
|
4326
|
+
function describeAssignment(item, agentUser) {
|
|
4327
|
+
return claimStates(item).map((claim) => {
|
|
4328
|
+
const label = claim.surface === "issue" ? "issue" : `pull request #${String(claim.number)}`;
|
|
4329
|
+
switch (claim.state) {
|
|
4330
|
+
case "would-claim":
|
|
4331
|
+
return `would assign ${label} to ${agentUser}`;
|
|
4332
|
+
case "already-assigned":
|
|
4333
|
+
return `${label} already assigned`;
|
|
4334
|
+
case "rule-exempt":
|
|
4335
|
+
return `${label} not claimed (orphan pass)`;
|
|
4336
|
+
}
|
|
4337
|
+
}).join(" \xB7 ");
|
|
4338
|
+
}
|
|
4339
|
+
function describePlanClaim(item) {
|
|
4340
|
+
const claims = claimStates(item);
|
|
4341
|
+
const would = claims.filter((claim) => claim.state === "would-claim").map((claim) => `the ${claim.surface}`);
|
|
4342
|
+
const exempt = claims.filter((claim) => claim.state === "rule-exempt").map((claim) => claim.surface);
|
|
4343
|
+
const parts = [];
|
|
4344
|
+
if (would.length > 0) parts.push(`will assign ${would.join(" and ")} to the agent`);
|
|
4345
|
+
if (exempt.length > 0) parts.push(`${exempt.join(" and ")} not claimed (orphan pass)`);
|
|
4346
|
+
return parts.length === 0 ? "" : `, ${parts.join(", ")}`;
|
|
4347
|
+
}
|
|
4274
4348
|
function describePlannedRun(item, settings, run5, execution) {
|
|
4275
4349
|
const rule = "\u2500".repeat(72);
|
|
4276
4350
|
const branchAction = item.turn === "issue-discuss" ? " and pull" : " and fast-forward";
|
|
4277
|
-
const assignment = item
|
|
4351
|
+
const assignment = describeAssignment(item, settings.participants.agentUser);
|
|
4278
4352
|
const markerTarget = markerSurfaceLabel(item);
|
|
4279
4353
|
const lines = [
|
|
4280
4354
|
rule,
|
|
@@ -4469,8 +4543,7 @@ function describePlan(decisions) {
|
|
|
4469
4543
|
return ` ${subjectLabel(decision.issue?.number ?? null, decision.pr?.number ?? null)} nothing to do \u2014 ${decision.detail}`;
|
|
4470
4544
|
}
|
|
4471
4545
|
const item = decision.item;
|
|
4472
|
-
|
|
4473
|
-
return ` ${itemLabel(item)} ${item.turn} on ${item.branch} \u2014 ${item.reason}${claim}`;
|
|
4546
|
+
return ` ${itemLabel(item)} ${item.turn} on ${item.branch} \u2014 ${item.reason}${describePlanClaim(item)}`;
|
|
4474
4547
|
});
|
|
4475
4548
|
return lines.length === 0 ? " (nothing matched the discovery filter)\n" : lines.join("\n") + "\n";
|
|
4476
4549
|
}
|
|
@@ -4485,6 +4558,16 @@ function claimIssue(item, settings) {
|
|
|
4485
4558
|
`);
|
|
4486
4559
|
}
|
|
4487
4560
|
}
|
|
4561
|
+
function claimPr(prNumber, agentUser) {
|
|
4562
|
+
try {
|
|
4563
|
+
assignPrToAgent(prNumber, agentUser);
|
|
4564
|
+
progress(` assigned pull request #${String(prNumber)} to ${agentUser}.
|
|
4565
|
+
`);
|
|
4566
|
+
} catch (err) {
|
|
4567
|
+
progress(` warning: could not assign pull request #${String(prNumber)}: ${err.message}
|
|
4568
|
+
`);
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4488
4571
|
function notePickupOnIssue(item, marker) {
|
|
4489
4572
|
if (item.turn !== "pr-work" || item.pr === null || item.issue === null) return null;
|
|
4490
4573
|
if (!item.issueAnalysis.hasNewMessage) return null;
|
|
@@ -4657,7 +4740,7 @@ async function invokeExecutor(prompt, execution, silent) {
|
|
|
4657
4740
|
}
|
|
4658
4741
|
await runClaude(prompt, { model: execution.model, effort: execution.effort, printSteps: !silent });
|
|
4659
4742
|
}
|
|
4660
|
-
function repairIssueLink(item, baseBranch) {
|
|
4743
|
+
function repairIssueLink(item, baseBranch, agentUser) {
|
|
4661
4744
|
const issue = item.issue;
|
|
4662
4745
|
if (issue === null) return false;
|
|
4663
4746
|
try {
|
|
@@ -4673,6 +4756,9 @@ function repairIssueLink(item, baseBranch) {
|
|
|
4673
4756
|
`);
|
|
4674
4757
|
return false;
|
|
4675
4758
|
}
|
|
4759
|
+
if (pr.assignees.length === 0) {
|
|
4760
|
+
claimPr(pr.number, agentUser);
|
|
4761
|
+
}
|
|
4676
4762
|
const closesRef = new RegExp(String.raw`\bcloses\s+#` + String(issue.number) + String.raw`\b`, "i");
|
|
4677
4763
|
if (closesRef.test(pr.body)) {
|
|
4678
4764
|
progress(` pull request #${String(pr.number)} already closes issue #${String(issue.number)}.
|
|
@@ -4736,6 +4822,9 @@ ${itemLabel(planned)} ${planned.turn}: ${planned.reason}
|
|
|
4736
4822
|
return { ...base, outcome: "skipped", detail: `${prepared.reason}: ${prepared.detail}` };
|
|
4737
4823
|
}
|
|
4738
4824
|
claimIssue(item, settings);
|
|
4825
|
+
if (item.turn === "pr-work" && item.pr !== null && item.prNeedsAssignment) {
|
|
4826
|
+
claimPr(item.pr.number, settings.participants.agentUser);
|
|
4827
|
+
}
|
|
4739
4828
|
let marker;
|
|
4740
4829
|
const markerTarget = markerSurfaceTarget(item);
|
|
4741
4830
|
try {
|
|
@@ -4819,7 +4908,7 @@ function adjustOutcome(reconciled, item, settings, buriedByNote) {
|
|
|
4819
4908
|
};
|
|
4820
4909
|
}
|
|
4821
4910
|
if (item.turn !== "issue-discuss") return outcome;
|
|
4822
|
-
const linked = repairIssueLink(item, settings.baseBranch);
|
|
4911
|
+
const linked = repairIssueLink(item, settings.baseBranch, settings.participants.agentUser);
|
|
4823
4912
|
if (linked && outcome.reason === "no-answer") {
|
|
4824
4913
|
progress(" a pull request was opened, so the turn is counted as answered.\n");
|
|
4825
4914
|
return {
|
|
@@ -5227,6 +5316,7 @@ function toPlanJson(decision) {
|
|
|
5227
5316
|
turn: item.turn,
|
|
5228
5317
|
branch: item.branch,
|
|
5229
5318
|
needsAssignment: item.needsAssignment,
|
|
5319
|
+
prNeedsAssignment: item.prNeedsAssignment,
|
|
5230
5320
|
reason: item.reason
|
|
5231
5321
|
};
|
|
5232
5322
|
}
|