automata-cli 0.7.0-develop.316 → 0.7.0-develop.324
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 +1364 -1219
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -815,10 +815,14 @@ function listRemoteBranches() {
|
|
|
815
815
|
function createBranchAtHead(branch) {
|
|
816
816
|
return gitCommand(["checkout", "-b", branch]);
|
|
817
817
|
}
|
|
818
|
+
function pathIsIgnored(path) {
|
|
819
|
+
return run2("git", ["check-ignore", "-q", "--", path]).status === 0;
|
|
820
|
+
}
|
|
818
821
|
function stageAllExcept(excludePaths) {
|
|
822
|
+
const needed = excludePaths.filter((path) => !pathIsIgnored(path));
|
|
819
823
|
const args = ["add", "-A"];
|
|
820
|
-
if (
|
|
821
|
-
args.push("--", ".", ...
|
|
824
|
+
if (needed.length > 0) {
|
|
825
|
+
args.push("--", ".", ...needed.map((path) => `:(exclude)${path}`));
|
|
822
826
|
}
|
|
823
827
|
return gitCommand(args);
|
|
824
828
|
}
|
|
@@ -1534,7 +1538,7 @@ function getCurrentBranchPr(branch) {
|
|
|
1534
1538
|
if (branch) {
|
|
1535
1539
|
args.push(branch);
|
|
1536
1540
|
}
|
|
1537
|
-
args.push("--json", "number,url,body");
|
|
1541
|
+
args.push("--json", "number,url,body,assignees");
|
|
1538
1542
|
const { stdout, stderr, status } = run3("gh", args);
|
|
1539
1543
|
if (status !== 0) {
|
|
1540
1544
|
if (stderr.includes("no pull requests found") || stderr.includes("Could not resolve")) {
|
|
@@ -1542,7 +1546,15 @@ function getCurrentBranchPr(branch) {
|
|
|
1542
1546
|
}
|
|
1543
1547
|
throw new Error(stderr.trim() || "Failed to query PR for current branch.");
|
|
1544
1548
|
}
|
|
1545
|
-
|
|
1549
|
+
const raw = JSON.parse(stdout);
|
|
1550
|
+
return {
|
|
1551
|
+
number: raw.number,
|
|
1552
|
+
url: raw.url,
|
|
1553
|
+
body: raw.body,
|
|
1554
|
+
// Absent when an older `gh` ignores the field, and `[]` is the right reading
|
|
1555
|
+
// of "nobody is assigned" either way.
|
|
1556
|
+
assignees: (raw.assignees ?? []).map((a) => a.login ?? "").filter((name) => name.length > 0)
|
|
1557
|
+
};
|
|
1546
1558
|
}
|
|
1547
1559
|
function addClosesRefToPr(prNumber, issueNumber) {
|
|
1548
1560
|
const { stdout, status: viewStatus } = run3("gh", [
|
|
@@ -1577,1335 +1589,1350 @@ function addCopilotReviewer(prNumber) {
|
|
|
1577
1589
|
}
|
|
1578
1590
|
}
|
|
1579
1591
|
|
|
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);
|
|
1592
|
+
// src/github/ghWorkService.ts
|
|
1593
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
1594
|
+
function run4(cmd, args) {
|
|
1595
|
+
const result = spawnSync4(cmd, args, { encoding: "utf8" });
|
|
1596
|
+
if (result.error) {
|
|
1597
|
+
const err = result.error;
|
|
1598
|
+
if (err.code === "ENOENT") {
|
|
1599
|
+
throw new Error(`\`${cmd}\` CLI is not installed or not on PATH.`);
|
|
1600
|
+
}
|
|
1601
|
+
throw new Error(err.message);
|
|
1603
1602
|
}
|
|
1604
|
-
|
|
1605
|
-
`);
|
|
1606
|
-
process.exit(1);
|
|
1603
|
+
return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", status: result.status ?? 1 };
|
|
1607
1604
|
}
|
|
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
|
-
}
|
|
1605
|
+
function ghJson(args, what) {
|
|
1606
|
+
const { stdout, stderr, status } = run4("gh", args);
|
|
1614
1607
|
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);
|
|
1608
|
+
throw new Error(stderr.trim() || `Failed to ${what}. Is \`gh\` installed and authenticated?`);
|
|
1626
1609
|
}
|
|
1627
|
-
return
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
// src/cli/childRegistry.ts
|
|
1631
|
-
var active = /* @__PURE__ */ new Set();
|
|
1632
|
-
function trackChild(child) {
|
|
1633
|
-
active.add(child);
|
|
1610
|
+
return JSON.parse(stdout);
|
|
1634
1611
|
}
|
|
1635
|
-
function
|
|
1636
|
-
|
|
1612
|
+
function login(author) {
|
|
1613
|
+
return author?.login ?? "";
|
|
1637
1614
|
}
|
|
1638
|
-
function
|
|
1639
|
-
return
|
|
1640
|
-
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1641
|
-
resolve2();
|
|
1642
|
-
return;
|
|
1643
|
-
}
|
|
1644
|
-
child.once("exit", () => resolve2());
|
|
1645
|
-
});
|
|
1615
|
+
function byCreatedAt(a, b) {
|
|
1616
|
+
return a.createdAt.localeCompare(b.createdAt);
|
|
1646
1617
|
}
|
|
1647
|
-
function
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
timer.unref();
|
|
1651
|
-
});
|
|
1618
|
+
function visibleAt(createdAt, submittedAt) {
|
|
1619
|
+
if (submittedAt === null || submittedAt === void 0) return createdAt;
|
|
1620
|
+
return submittedAt > createdAt ? submittedAt : createdAt;
|
|
1652
1621
|
}
|
|
1653
|
-
|
|
1654
|
-
const
|
|
1655
|
-
if (
|
|
1656
|
-
|
|
1657
|
-
for (const child of children) {
|
|
1658
|
-
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
1622
|
+
function getRepoSlug() {
|
|
1623
|
+
const { stdout, status } = run4("git", ["remote", "get-url", "origin"]);
|
|
1624
|
+
if (status !== 0) {
|
|
1625
|
+
throw new Error("Could not read the `origin` remote. Is this a git repository with a remote?");
|
|
1659
1626
|
}
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1627
|
+
const url = stdout.trim();
|
|
1628
|
+
const match = /github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/.exec(url) ?? /github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/.exec(url);
|
|
1629
|
+
if (!match) {
|
|
1630
|
+
throw new Error(`Could not determine the GitHub owner/repo from the origin remote: ${url}`);
|
|
1664
1631
|
}
|
|
1665
|
-
|
|
1666
|
-
Promise.all(exits).then(() => "exited"),
|
|
1667
|
-
afterDelay(killGraceMs)
|
|
1668
|
-
]);
|
|
1669
|
-
return escalated === "exited";
|
|
1632
|
+
return { owner: match[1], repo: match[2] };
|
|
1670
1633
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1634
|
+
function getAuthenticatedLogin() {
|
|
1635
|
+
const { stdout, status } = run4("gh", ["api", "user", "--jq", ".login"]);
|
|
1636
|
+
if (status !== 0) return null;
|
|
1637
|
+
const login2 = stdout.trim();
|
|
1638
|
+
return login2.length > 0 ? login2 : null;
|
|
1639
|
+
}
|
|
1640
|
+
function listCandidateIssues(technique, value, limit) {
|
|
1641
|
+
const args = [
|
|
1642
|
+
"issue",
|
|
1643
|
+
"list",
|
|
1644
|
+
"--state",
|
|
1645
|
+
"open",
|
|
1646
|
+
"--limit",
|
|
1647
|
+
String(limit),
|
|
1648
|
+
"--json",
|
|
1649
|
+
"number,title,body,url"
|
|
1650
|
+
];
|
|
1651
|
+
switch (technique) {
|
|
1652
|
+
case "label":
|
|
1653
|
+
args.push("--label", value);
|
|
1654
|
+
break;
|
|
1655
|
+
case "assignee":
|
|
1656
|
+
args.push("--assignee", value);
|
|
1657
|
+
break;
|
|
1658
|
+
case "title-contains":
|
|
1659
|
+
args.push("--search", `${value} in:title`);
|
|
1660
|
+
break;
|
|
1678
1661
|
}
|
|
1679
|
-
return
|
|
1662
|
+
return ghJson(args, "query GitHub issues");
|
|
1680
1663
|
}
|
|
1681
|
-
function
|
|
1682
|
-
const
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1664
|
+
function getIssueSurface(issueNumber) {
|
|
1665
|
+
const raw = ghJson(
|
|
1666
|
+
[
|
|
1667
|
+
"issue",
|
|
1668
|
+
"view",
|
|
1669
|
+
String(issueNumber),
|
|
1670
|
+
"--json",
|
|
1671
|
+
"number,title,body,url,state,author,createdAt,assignees,labels,comments"
|
|
1672
|
+
],
|
|
1673
|
+
`read issue #${String(issueNumber)}`
|
|
1674
|
+
);
|
|
1675
|
+
const messages = [
|
|
1676
|
+
{
|
|
1677
|
+
kind: "issue-body",
|
|
1678
|
+
author: login(raw.author),
|
|
1679
|
+
body: raw.body,
|
|
1680
|
+
createdAt: raw.createdAt
|
|
1681
|
+
},
|
|
1682
|
+
...(raw.comments ?? []).map((comment) => ({
|
|
1683
|
+
kind: "issue-comment",
|
|
1684
|
+
author: login(comment.author),
|
|
1685
|
+
body: comment.body,
|
|
1686
|
+
createdAt: comment.createdAt
|
|
1687
|
+
}))
|
|
1688
|
+
].sort(byCreatedAt);
|
|
1689
|
+
return {
|
|
1690
|
+
issue: { number: raw.number, title: raw.title, body: raw.body, url: raw.url },
|
|
1691
|
+
state: raw.state === "CLOSED" ? "CLOSED" : "OPEN",
|
|
1692
|
+
assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
|
|
1693
|
+
labels: (raw.labels ?? []).map((label) => label.name ?? "").filter((name) => name.length > 0),
|
|
1694
|
+
messages
|
|
1695
|
+
};
|
|
1689
1696
|
}
|
|
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;
|
|
1697
|
+
var LINK_MAP_QUERY = `
|
|
1698
|
+
query($owner:String!,$repo:String!,$cursor:String){
|
|
1699
|
+
repository(owner:$owner,name:$repo){
|
|
1700
|
+
defaultBranchRef{ name }
|
|
1701
|
+
pullRequests(states:OPEN, first:100, after:$cursor, orderBy:{field:UPDATED_AT, direction:DESC}){
|
|
1702
|
+
pageInfo{ hasNextPage endCursor }
|
|
1703
|
+
nodes{
|
|
1704
|
+
number url title headRefName baseRefName isCrossRepository isDraft updatedAt
|
|
1705
|
+
labels(first:50){ nodes{ name } }
|
|
1706
|
+
assignees(first:50){ nodes{ login } }
|
|
1707
|
+
closingIssuesReferences(first:50){
|
|
1708
|
+
pageInfo{ hasNextPage }
|
|
1709
|
+
nodes{ number repository{ nameWithOwner } }
|
|
1710
|
+
}
|
|
1724
1711
|
}
|
|
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);
|
|
1712
|
+
}
|
|
1736
1713
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
function
|
|
1740
|
-
const
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
}
|
|
1746
|
-
function invokeClaudeCodeVerbose(prompt, yolo, model, effort) {
|
|
1747
|
-
return new Promise((resolve2) => {
|
|
1748
|
-
const claudeBin = resolveCommand("claude");
|
|
1749
|
-
const args = buildClaudeArgs(prompt, { yolo, model, effort, verbose: true });
|
|
1750
|
-
const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
|
|
1751
|
-
const rl = createInterface({ input: child.stdout });
|
|
1752
|
-
let turnCount = 0;
|
|
1753
|
-
child.on("error", (err) => {
|
|
1754
|
-
handleSpawnError(err, "claude");
|
|
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
|
-
`);
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
function formatContentBlock(block) {
|
|
1783
|
-
if (block["type"] === "tool_use") {
|
|
1784
|
-
const toolName = block["name"];
|
|
1785
|
-
const input = block["input"];
|
|
1786
|
-
return summarizeTool(toolName, input);
|
|
1787
|
-
}
|
|
1788
|
-
if (block["type"] === "text") {
|
|
1789
|
-
const text = block["text"] ?? "";
|
|
1790
|
-
if (text.length === 0) return null;
|
|
1791
|
-
const preview = text.length > 120 ? text.slice(0, 120) + "..." : text;
|
|
1792
|
-
return preview.split("\n")[0] ?? null;
|
|
1793
|
-
}
|
|
1794
|
-
return null;
|
|
1795
|
-
}
|
|
1796
|
-
function formatResultEvent(event) {
|
|
1797
|
-
const result = event["result"];
|
|
1798
|
-
const cost = event["cost_usd"];
|
|
1799
|
-
const duration = event["duration_ms"];
|
|
1800
|
-
const turns = event["num_turns"];
|
|
1801
|
-
process.stderr.write("\n--- Result ---\n");
|
|
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
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
function formatEvent(event, turnCount) {
|
|
1815
|
-
const type = event["type"];
|
|
1816
|
-
if (type === "assistant") {
|
|
1817
|
-
formatAssistantEvent(event, turnCount);
|
|
1818
|
-
} else if (type === "result") {
|
|
1819
|
-
formatResultEvent(event);
|
|
1714
|
+
}`.trim();
|
|
1715
|
+
var MAX_LINK_MAP_PAGES = 50;
|
|
1716
|
+
function indexPullRequest(map, node, nameWithOwner) {
|
|
1717
|
+
const ref = toPullRequestRef(node);
|
|
1718
|
+
if (node.closingIssuesReferences.pageInfo?.hasNextPage) {
|
|
1719
|
+
throw new Error(
|
|
1720
|
+
`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.`
|
|
1721
|
+
);
|
|
1820
1722
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
return `writing ${asText(input["file_path"], "file")}`;
|
|
1832
|
-
case "Edit":
|
|
1833
|
-
return `editing ${asText(input["file_path"], "file")}`;
|
|
1834
|
-
case "Bash":
|
|
1835
|
-
return `running: ${truncate(asText(input["command"], ""), 80)}`;
|
|
1836
|
-
case "Glob":
|
|
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}`;
|
|
1723
|
+
let closedAnyHere = false;
|
|
1724
|
+
for (const issue of node.closingIssuesReferences.nodes) {
|
|
1725
|
+
if (issue.repository.nameWithOwner.toLowerCase() !== nameWithOwner.toLowerCase()) continue;
|
|
1726
|
+
closedAnyHere = true;
|
|
1727
|
+
const existing = map.get(issue.number);
|
|
1728
|
+
if (existing) {
|
|
1729
|
+
existing.push(ref);
|
|
1730
|
+
} else {
|
|
1731
|
+
map.set(issue.number, [ref]);
|
|
1732
|
+
}
|
|
1844
1733
|
}
|
|
1734
|
+
return closedAnyHere;
|
|
1845
1735
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1736
|
+
function toPullRequestRef(node) {
|
|
1737
|
+
return {
|
|
1738
|
+
number: node.number,
|
|
1739
|
+
url: node.url,
|
|
1740
|
+
title: node.title,
|
|
1741
|
+
headRefName: node.headRefName,
|
|
1742
|
+
baseRefName: node.baseRefName,
|
|
1743
|
+
isCrossRepository: node.isCrossRepository,
|
|
1744
|
+
state: "OPEN",
|
|
1745
|
+
isDraft: node.isDraft,
|
|
1746
|
+
updatedAt: node.updatedAt
|
|
1747
|
+
};
|
|
1851
1748
|
}
|
|
1852
|
-
function
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
return args;
|
|
1749
|
+
function toOrphanPr(node) {
|
|
1750
|
+
return {
|
|
1751
|
+
pr: toPullRequestRef(node),
|
|
1752
|
+
labels: (node.labels?.nodes ?? []).map((label) => label.name ?? "").filter((name) => name.length > 0),
|
|
1753
|
+
assignees: (node.assignees?.nodes ?? []).map((assignee) => assignee.login ?? "").filter((login2) => login2.length > 0)
|
|
1754
|
+
};
|
|
1859
1755
|
}
|
|
1860
|
-
function
|
|
1861
|
-
|
|
1862
|
-
|
|
1756
|
+
function getOpenPrLinkMap() {
|
|
1757
|
+
const { owner, repo } = getRepoSlug();
|
|
1758
|
+
const map = /* @__PURE__ */ new Map();
|
|
1759
|
+
const orphans = [];
|
|
1760
|
+
let defaultBranch = null;
|
|
1761
|
+
let cursor = null;
|
|
1762
|
+
for (let page = 0; page < MAX_LINK_MAP_PAGES; page++) {
|
|
1763
|
+
const args = ["api", "graphql", "-f", `query=${LINK_MAP_QUERY}`, "-f", `owner=${owner}`, "-f", `repo=${repo}`];
|
|
1764
|
+
if (cursor !== null) args.push("-f", `cursor=${cursor}`);
|
|
1765
|
+
const response = ghJson(args, "query open pull requests");
|
|
1766
|
+
defaultBranch = response.data.repository.defaultBranchRef?.name ?? defaultBranch;
|
|
1767
|
+
const connection = response.data.repository.pullRequests;
|
|
1768
|
+
for (const node of connection.nodes) {
|
|
1769
|
+
if (!indexPullRequest(map, node, `${owner}/${repo}`)) {
|
|
1770
|
+
orphans.push(toOrphanPr(node));
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
1774
|
+
return { byIssue: map, defaultBranch, orphans };
|
|
1775
|
+
}
|
|
1776
|
+
cursor = connection.pageInfo.endCursor;
|
|
1863
1777
|
}
|
|
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");
|
|
1778
|
+
throw new Error(
|
|
1779
|
+
`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.`
|
|
1780
|
+
);
|
|
1872
1781
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
child.on("close", (code, signal) => {
|
|
1887
|
-
untrackChild(child);
|
|
1888
|
-
if (code === 0) {
|
|
1889
|
-
resolve2();
|
|
1890
|
-
return;
|
|
1782
|
+
var REVIEW_THREADS_QUERY2 = `
|
|
1783
|
+
query($owner:String!,$repo:String!,$prNumber:Int!,$cursor:String){
|
|
1784
|
+
repository(owner:$owner,name:$repo){
|
|
1785
|
+
pullRequest(number:$prNumber){
|
|
1786
|
+
reviewThreads(first:100, after:$cursor){
|
|
1787
|
+
pageInfo{ hasNextPage endCursor }
|
|
1788
|
+
nodes{
|
|
1789
|
+
isResolved isOutdated path line
|
|
1790
|
+
comments(last:100){
|
|
1791
|
+
pageInfo{ hasPreviousPage }
|
|
1792
|
+
nodes{ author{login} body createdAt url pullRequestReview{ submittedAt } }
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1891
1795
|
}
|
|
1892
|
-
|
|
1893
|
-
new Error(
|
|
1894
|
-
signal === null ? `Codex exited with code ${String(code)}.` : `Codex terminated on ${signal}.`
|
|
1895
|
-
)
|
|
1896
|
-
);
|
|
1897
|
-
});
|
|
1898
|
-
});
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
// src/commands/getReady.ts
|
|
1902
|
-
function writeOverflowHint(output, issues, limit) {
|
|
1903
|
-
if (issues.length === limit) {
|
|
1904
|
-
output.write(`(Showing first ${limit} matching issues \u2014 there may be more. Use --limit to fetch more.)
|
|
1905
|
-
`);
|
|
1796
|
+
}
|
|
1906
1797
|
}
|
|
1798
|
+
}`.trim();
|
|
1799
|
+
var MAX_THREAD_PAGES = 50;
|
|
1800
|
+
function normalizePrState(state) {
|
|
1801
|
+
if (state === "MERGED") return "MERGED";
|
|
1802
|
+
if (state === "CLOSED") return "CLOSED";
|
|
1803
|
+
return "OPEN";
|
|
1907
1804
|
}
|
|
1908
|
-
function
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1805
|
+
function getReviewThreads(prNumber) {
|
|
1806
|
+
const { owner, repo } = getRepoSlug();
|
|
1807
|
+
const threads = [];
|
|
1808
|
+
let cursor = null;
|
|
1809
|
+
for (let page = 0; page < MAX_THREAD_PAGES; page++) {
|
|
1810
|
+
const args = [
|
|
1811
|
+
"api",
|
|
1812
|
+
"graphql",
|
|
1813
|
+
"-f",
|
|
1814
|
+
`query=${REVIEW_THREADS_QUERY2}`,
|
|
1815
|
+
"-f",
|
|
1816
|
+
`owner=${owner}`,
|
|
1817
|
+
"-f",
|
|
1818
|
+
`repo=${repo}`,
|
|
1819
|
+
"-F",
|
|
1820
|
+
`prNumber=${String(prNumber)}`
|
|
1821
|
+
];
|
|
1822
|
+
if (cursor !== null) args.push("-f", `cursor=${cursor}`);
|
|
1823
|
+
const response = ghJson(
|
|
1824
|
+
args,
|
|
1825
|
+
`query review threads for pull request #${String(prNumber)}`
|
|
1826
|
+
);
|
|
1827
|
+
const connection = response.data.repository.pullRequest.reviewThreads;
|
|
1828
|
+
for (const node of connection.nodes) {
|
|
1829
|
+
if (node.comments.pageInfo?.hasPreviousPage) {
|
|
1830
|
+
throw new Error(
|
|
1831
|
+
`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.`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
threads.push({
|
|
1835
|
+
path: node.path,
|
|
1836
|
+
line: node.line ?? null,
|
|
1837
|
+
isResolved: node.isResolved,
|
|
1838
|
+
url: node.comments.nodes.at(-1)?.url ?? null,
|
|
1839
|
+
comments: node.comments.nodes.map((comment) => ({
|
|
1840
|
+
kind: "thread-comment",
|
|
1841
|
+
author: login(comment.author),
|
|
1842
|
+
body: comment.body,
|
|
1843
|
+
// When the comment became *visible*, not when it was drafted.
|
|
1844
|
+
// GitHub stamps `createdAt` the moment a comment is added to a
|
|
1845
|
+
// pending review, and it only becomes visible when the review is
|
|
1846
|
+
// submitted — minutes later for a human working through a diff.
|
|
1847
|
+
// Using `createdAt` let an agent answer posted in between look newer
|
|
1848
|
+
// than the review, which marked every one of its threads answered
|
|
1849
|
+
// and discarded the whole review silently.
|
|
1850
|
+
createdAt: visibleAt(comment.createdAt, comment.pullRequestReview?.submittedAt)
|
|
1851
|
+
})).sort(byCreatedAt)
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
1855
|
+
return threads;
|
|
1856
|
+
}
|
|
1857
|
+
cursor = connection.pageInfo.endCursor;
|
|
1913
1858
|
}
|
|
1914
|
-
|
|
1859
|
+
throw new Error(
|
|
1860
|
+
`Stopped paginating review threads for pull request #${String(prNumber)} after ${String(MAX_THREAD_PAGES)} pages; refusing rather than answering only part of the feedback.`
|
|
1861
|
+
);
|
|
1915
1862
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1863
|
+
function getPrSurface(prNumber) {
|
|
1864
|
+
const raw = ghJson(
|
|
1865
|
+
[
|
|
1866
|
+
"pr",
|
|
1867
|
+
"view",
|
|
1868
|
+
String(prNumber),
|
|
1869
|
+
"--json",
|
|
1870
|
+
"number,title,url,headRefName,baseRefName,isCrossRepository,state,isDraft,body,author,createdAt,updatedAt,assignees,comments,reviews"
|
|
1871
|
+
],
|
|
1872
|
+
`read pull request #${String(prNumber)}`
|
|
1922
1873
|
);
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1874
|
+
const messages = [
|
|
1875
|
+
...(raw.comments ?? []).map((comment) => ({
|
|
1876
|
+
kind: "pr-comment",
|
|
1877
|
+
author: login(comment.author),
|
|
1878
|
+
body: comment.body,
|
|
1879
|
+
createdAt: comment.createdAt
|
|
1880
|
+
})),
|
|
1881
|
+
// A review with no body carries no message — only its inline comments do,
|
|
1882
|
+
// and those arrive through the review-thread query.
|
|
1883
|
+
...(raw.reviews ?? []).filter((review) => review.body.trim().length > 0).map((review) => ({
|
|
1884
|
+
kind: "pr-review",
|
|
1885
|
+
author: login(review.author),
|
|
1886
|
+
body: review.body,
|
|
1887
|
+
createdAt: review.submittedAt ?? review.createdAt ?? ""
|
|
1888
|
+
}))
|
|
1889
|
+
].sort(byCreatedAt);
|
|
1890
|
+
const threads = getReviewThreads(prNumber);
|
|
1891
|
+
const state = normalizePrState(raw.state);
|
|
1892
|
+
return {
|
|
1893
|
+
pr: {
|
|
1894
|
+
number: raw.number,
|
|
1895
|
+
url: raw.url,
|
|
1896
|
+
title: raw.title,
|
|
1897
|
+
headRefName: raw.headRefName,
|
|
1898
|
+
baseRefName: raw.baseRefName ?? "",
|
|
1899
|
+
isCrossRepository: raw.isCrossRepository ?? false,
|
|
1900
|
+
state,
|
|
1901
|
+
isDraft: raw.isDraft ?? false,
|
|
1902
|
+
updatedAt: raw.updatedAt ?? raw.createdAt
|
|
1903
|
+
},
|
|
1904
|
+
assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
|
|
1905
|
+
messages,
|
|
1906
|
+
threads
|
|
1907
|
+
};
|
|
1931
1908
|
}
|
|
1932
|
-
function
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
)
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
);
|
|
1943
|
-
process.exit(1);
|
|
1909
|
+
function assignIssueToAgent(issueNumber, agentUser) {
|
|
1910
|
+
const { stderr, status } = run4("gh", [
|
|
1911
|
+
"issue",
|
|
1912
|
+
"edit",
|
|
1913
|
+
String(issueNumber),
|
|
1914
|
+
"--add-assignee",
|
|
1915
|
+
agentUser
|
|
1916
|
+
]);
|
|
1917
|
+
if (status !== 0) {
|
|
1918
|
+
throw new Error(stderr.trim() || `Failed to assign issue #${String(issueNumber)} to ${agentUser}.`);
|
|
1944
1919
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1920
|
+
}
|
|
1921
|
+
function assignPrToAgent(prNumber, agentUser) {
|
|
1922
|
+
const { stderr, status } = run4("gh", ["pr", "edit", String(prNumber), "--add-assignee", agentUser]);
|
|
1923
|
+
if (status !== 0) {
|
|
1924
|
+
throw new Error(
|
|
1925
|
+
stderr.trim() || `Failed to assign pull request #${String(prNumber)} to ${agentUser}.`
|
|
1948
1926
|
);
|
|
1949
|
-
process.exit(1);
|
|
1950
1927
|
}
|
|
1951
1928
|
}
|
|
1952
|
-
|
|
1953
|
-
const
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
Title: ${issue.title}
|
|
1967
|
-
`);
|
|
1968
|
-
} else if (options.takeFirst) {
|
|
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;
|
|
1929
|
+
function postMarker(_surface, number, body) {
|
|
1930
|
+
const { owner, repo } = getRepoSlug();
|
|
1931
|
+
const raw = ghJson(
|
|
1932
|
+
[
|
|
1933
|
+
"api",
|
|
1934
|
+
"--method",
|
|
1935
|
+
"POST",
|
|
1936
|
+
`repos/${owner}/${repo}/issues/${String(number)}/comments`,
|
|
1937
|
+
"-f",
|
|
1938
|
+
`body=${body}`
|
|
1939
|
+
],
|
|
1940
|
+
`post a comment on #${String(number)}`
|
|
1941
|
+
);
|
|
1942
|
+
return { commentId: String(raw.id), createdAt: raw.created_at };
|
|
1980
1943
|
}
|
|
1981
|
-
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
`
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
} catch (err) {
|
|
1994
|
-
process.stderr.write(`Error: ${err.message}
|
|
1995
|
-
`);
|
|
1996
|
-
process.exit(1);
|
|
1997
|
-
}
|
|
1998
|
-
const issue = await resolveIssue(issues, options, limit);
|
|
1999
|
-
if (options.json) {
|
|
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}
|
|
2005
|
-
`);
|
|
2006
|
-
}
|
|
2007
|
-
if (options.queryOnly) {
|
|
2008
|
-
process.exit(0);
|
|
2009
|
-
}
|
|
2010
|
-
const executor = options.claude === false ? void 0 : resolveExecutor(options.with);
|
|
2011
|
-
let commentUrl;
|
|
2012
|
-
try {
|
|
2013
|
-
commentUrl = postComment(issue.number, "working");
|
|
2014
|
-
} catch (err) {
|
|
2015
|
-
process.stderr.write(`Error: ${err.message}
|
|
2016
|
-
`);
|
|
2017
|
-
process.exit(1);
|
|
2018
|
-
}
|
|
2019
|
-
const effort = resolveEffortOption(options.effort);
|
|
2020
|
-
if (options.claude !== false) {
|
|
2021
|
-
const systemPrompt = config.claudeSystemPrompt ?? DEFAULT_CLAUDE_SYSTEM_PROMPT;
|
|
2022
|
-
const prompt = `Resolving issue #${issue.number}:
|
|
2023
|
-
|
|
2024
|
-
${systemPrompt}
|
|
2025
|
-
|
|
2026
|
-
${issue.body}`;
|
|
2027
|
-
if (executor === "codex") {
|
|
2028
|
-
if (options.silent) {
|
|
2029
|
-
process.stderr.write("Warning: --silent is only supported with Claude and has no effect when used with Codex.\n");
|
|
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);
|
|
1944
|
+
function updateMarker(marker, body) {
|
|
1945
|
+
const { owner, repo } = getRepoSlug();
|
|
1946
|
+
const { stderr, status } = run4("gh", [
|
|
1947
|
+
"api",
|
|
1948
|
+
"--method",
|
|
1949
|
+
"PATCH",
|
|
1950
|
+
`repos/${owner}/${repo}/issues/comments/${marker.commentId}`,
|
|
1951
|
+
"-f",
|
|
1952
|
+
`body=${body}`
|
|
1953
|
+
]);
|
|
1954
|
+
if (status !== 0) {
|
|
1955
|
+
throw new Error(stderr.trim() || `Failed to update comment ${marker.commentId}.`);
|
|
2049
1956
|
}
|
|
2050
|
-
return executor;
|
|
2051
1957
|
}
|
|
2052
|
-
function
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
1958
|
+
function deleteMarker(marker) {
|
|
1959
|
+
const { owner, repo } = getRepoSlug();
|
|
1960
|
+
const { stderr, status } = run4("gh", [
|
|
1961
|
+
"api",
|
|
1962
|
+
"--method",
|
|
1963
|
+
"DELETE",
|
|
1964
|
+
`repos/${owner}/${repo}/issues/comments/${marker.commentId}`
|
|
1965
|
+
]);
|
|
1966
|
+
if (status !== 0) {
|
|
1967
|
+
if (/not found/i.test(stderr) || /HTTP 404/i.test(stderr)) return;
|
|
1968
|
+
throw new Error(stderr.trim() || `Failed to delete comment ${marker.commentId}.`);
|
|
2058
1969
|
}
|
|
2059
1970
|
}
|
|
2060
|
-
function
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
`
|
|
2067
|
-
|
|
1971
|
+
function toHeadState(state) {
|
|
1972
|
+
return state === "OPEN" || state === "MERGED" ? state : "CLOSED";
|
|
1973
|
+
}
|
|
1974
|
+
function listPullRequestsForHead(branch) {
|
|
1975
|
+
const raw = ghJson(
|
|
1976
|
+
["pr", "list", "--head", branch, "--state", "all", "--json", "number,state,url,updatedAt,author"],
|
|
1977
|
+
`list pull requests for branch ${branch}`
|
|
1978
|
+
);
|
|
1979
|
+
return raw.map((pr) => ({
|
|
1980
|
+
number: pr.number,
|
|
1981
|
+
url: pr.url,
|
|
1982
|
+
state: toHeadState(pr.state),
|
|
1983
|
+
updatedAt: pr.updatedAt,
|
|
1984
|
+
author: pr.author?.login ?? ""
|
|
1985
|
+
})).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
1986
|
+
}
|
|
1987
|
+
var MISSING_LABEL_PATTERNS = [
|
|
1988
|
+
// gh's own message: `could not add label: 'rescue' not found`.
|
|
1989
|
+
/could not add label/i,
|
|
1990
|
+
// The GraphQL error it wraps, seen directly on some gh versions.
|
|
1991
|
+
/could not resolve to a label/i,
|
|
1992
|
+
/\blabels?\b[^\n]*\b(?:not found|does not exist)\b/i
|
|
1993
|
+
];
|
|
1994
|
+
function isMissingLabelError(stderr) {
|
|
1995
|
+
return MISSING_LABEL_PATTERNS.some((pattern) => pattern.test(stderr));
|
|
1996
|
+
}
|
|
1997
|
+
function createDraftPullRequest(input) {
|
|
1998
|
+
const base = [
|
|
1999
|
+
"pr",
|
|
2000
|
+
"create",
|
|
2001
|
+
"--draft",
|
|
2002
|
+
"--head",
|
|
2003
|
+
input.head,
|
|
2004
|
+
"--base",
|
|
2005
|
+
input.base,
|
|
2006
|
+
"--title",
|
|
2007
|
+
input.title,
|
|
2008
|
+
"--body",
|
|
2009
|
+
input.body
|
|
2010
|
+
];
|
|
2011
|
+
if (input.label !== void 0 && input.label.length > 0) {
|
|
2012
|
+
const labelled = run4("gh", [...base, "--label", input.label]);
|
|
2013
|
+
if (labelled.status === 0) return parseCreatedPrUrl(labelled.stdout, input.head);
|
|
2014
|
+
if (!isMissingLabelError(labelled.stderr)) {
|
|
2015
|
+
throw new Error(
|
|
2016
|
+
labelled.stderr.trim() || `Failed to open a draft pull request for ${input.head}.`
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2068
2019
|
}
|
|
2069
|
-
|
|
2070
|
-
if (
|
|
2071
|
-
|
|
2072
|
-
editComment(commentUrl, `Working on this in PR #${pr.number} \u2014 ${pr.url}`);
|
|
2073
|
-
});
|
|
2020
|
+
const { stdout, stderr, status } = run4("gh", base);
|
|
2021
|
+
if (status !== 0) {
|
|
2022
|
+
throw new Error(stderr.trim() || `Failed to open a draft pull request for ${input.head}.`);
|
|
2074
2023
|
}
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
});
|
|
2024
|
+
return parseCreatedPrUrl(stdout, input.head);
|
|
2025
|
+
}
|
|
2026
|
+
function parseCreatedPrUrl(stdout, head) {
|
|
2027
|
+
const url = stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
2028
|
+
const match = /\/pull\/(\d+)\s*$/.exec(url);
|
|
2029
|
+
if (!match) {
|
|
2030
|
+
throw new Error(`Opened a pull request for ${head} but could not read its number from: ${url}`);
|
|
2082
2031
|
}
|
|
2032
|
+
return { number: Number(match[1]), url };
|
|
2083
2033
|
}
|
|
2084
2034
|
|
|
2085
|
-
// src/
|
|
2086
|
-
import {
|
|
2087
|
-
import {
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2035
|
+
// src/claude/claudeService.ts
|
|
2036
|
+
import { spawn, spawnSync as spawnSync5 } from "child_process";
|
|
2037
|
+
import { createInterface } from "readline";
|
|
2038
|
+
import { existsSync } from "fs";
|
|
2039
|
+
import { delimiter, join } from "path";
|
|
2040
|
+
|
|
2041
|
+
// src/cli/spawnUtils.ts
|
|
2042
|
+
var SHELL_SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
2043
|
+
var ESCAPED_QUOTE = String.raw`'\''`;
|
|
2044
|
+
function shellQuote(arg) {
|
|
2045
|
+
if (SHELL_SAFE.test(arg)) return arg;
|
|
2046
|
+
return "'" + arg.replaceAll("'", ESCAPED_QUOTE) + "'";
|
|
2047
|
+
}
|
|
2048
|
+
function truncate(str, max) {
|
|
2049
|
+
return str.length > max ? str.slice(0, max) + "..." : str;
|
|
2095
2050
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
const
|
|
2099
|
-
if (
|
|
2100
|
-
process.stderr.write(
|
|
2051
|
+
function handleSpawnError(error, toolName) {
|
|
2052
|
+
if (!error) return;
|
|
2053
|
+
const err = error;
|
|
2054
|
+
if (err.code === "ENOENT") {
|
|
2055
|
+
process.stderr.write(`Error: \`${toolName}\` CLI is not installed or not on PATH.
|
|
2056
|
+
`);
|
|
2101
2057
|
process.exit(1);
|
|
2102
2058
|
}
|
|
2103
|
-
|
|
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}
|
|
2059
|
+
process.stderr.write(`Error: ${err.message}
|
|
2112
2060
|
`);
|
|
2113
|
-
process.exit(1);
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
if (!process.stdin.isTTY) {
|
|
2117
|
-
const content = await readStdin();
|
|
2118
|
-
if (content.trim().length === 0) {
|
|
2119
|
-
process.stderr.write("Error: No prompt provided. Use --prompt, --file-prompt, or pipe via stdin.\n");
|
|
2120
|
-
process.exit(1);
|
|
2121
|
-
}
|
|
2122
|
-
return content;
|
|
2123
|
-
}
|
|
2124
|
-
process.stderr.write("Error: No prompt provided. Use --prompt, --file-prompt, or pipe via stdin.\n");
|
|
2125
2061
|
process.exit(1);
|
|
2126
2062
|
}
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${options.with}'.
|
|
2063
|
+
function handleExitCode(status, toolName) {
|
|
2064
|
+
if (status === null) {
|
|
2065
|
+
process.stderr.write(`Error: ${toolName} terminated abnormally (exit code is null, likely due to a signal).
|
|
2131
2066
|
`);
|
|
2132
2067
|
process.exit(1);
|
|
2133
2068
|
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
} else {
|
|
2139
|
-
await invokeClaudeCode(prompt, {
|
|
2140
|
-
yolo: true,
|
|
2141
|
-
verbose: !options.silent,
|
|
2142
|
-
model: options.model,
|
|
2143
|
-
effort
|
|
2144
|
-
});
|
|
2069
|
+
if (status !== 0) {
|
|
2070
|
+
process.stderr.write(`Error: ${toolName} exited with code ${status}.
|
|
2071
|
+
`);
|
|
2072
|
+
process.exit(status);
|
|
2145
2073
|
}
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2074
|
+
}
|
|
2075
|
+
function resolveEffortOption(value) {
|
|
2076
|
+
if (value === void 0) return void 0;
|
|
2077
|
+
const trimmed2 = value.trim();
|
|
2078
|
+
if (trimmed2.length === 0) {
|
|
2079
|
+
process.stderr.write("Error: --effort must be a non-empty level.\n");
|
|
2080
|
+
process.exit(1);
|
|
2081
|
+
}
|
|
2082
|
+
return trimmed2;
|
|
2083
|
+
}
|
|
2150
2084
|
|
|
2151
|
-
// src/
|
|
2152
|
-
var
|
|
2153
|
-
|
|
2154
|
-
|
|
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);
|
|
2085
|
+
// src/cli/childRegistry.ts
|
|
2086
|
+
var active = /* @__PURE__ */ new Set();
|
|
2087
|
+
function trackChild(child) {
|
|
2088
|
+
active.add(child);
|
|
2161
2089
|
}
|
|
2162
|
-
function
|
|
2163
|
-
|
|
2164
|
-
if (login2 === p.agentUser.toLowerCase()) return "agent";
|
|
2165
|
-
return p.allowedUsers.some((user) => user.toLowerCase() === login2) ? "authorized" : "other";
|
|
2090
|
+
function untrackChild(child) {
|
|
2091
|
+
active.delete(child);
|
|
2166
2092
|
}
|
|
2167
|
-
function
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
if (classify(message.author, p) !== "agent") continue;
|
|
2173
|
-
if (lastAgentAt === null || message.createdAt > lastAgentAt) {
|
|
2174
|
-
lastAgentAt = message.createdAt;
|
|
2093
|
+
function waitForExit(child) {
|
|
2094
|
+
return new Promise((resolve2) => {
|
|
2095
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
2096
|
+
resolve2();
|
|
2097
|
+
return;
|
|
2175
2098
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
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
|
-
};
|
|
2099
|
+
child.once("exit", () => resolve2());
|
|
2100
|
+
});
|
|
2192
2101
|
}
|
|
2193
|
-
function
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2102
|
+
function afterDelay(ms) {
|
|
2103
|
+
return new Promise((resolve2) => {
|
|
2104
|
+
const timer = setTimeout(() => resolve2("timeout"), ms);
|
|
2105
|
+
timer.unref();
|
|
2106
|
+
});
|
|
2197
2107
|
}
|
|
2198
|
-
function
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2108
|
+
async function terminateTrackedChildren(timeoutMs = 1e4, killGraceMs = 5e3) {
|
|
2109
|
+
const children = [...active];
|
|
2110
|
+
if (children.length === 0) return true;
|
|
2111
|
+
const exits = children.map((child) => waitForExit(child));
|
|
2112
|
+
for (const child of children) {
|
|
2113
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
2114
|
+
}
|
|
2115
|
+
const settled = await Promise.race([Promise.all(exits).then(() => "exited"), afterDelay(timeoutMs)]);
|
|
2116
|
+
if (settled === "exited") return true;
|
|
2117
|
+
for (const child of children) {
|
|
2118
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
2119
|
+
}
|
|
2120
|
+
const escalated = await Promise.race([
|
|
2121
|
+
Promise.all(exits).then(() => "exited"),
|
|
2122
|
+
afterDelay(killGraceMs)
|
|
2123
|
+
]);
|
|
2124
|
+
return escalated === "exited";
|
|
2204
2125
|
}
|
|
2205
2126
|
|
|
2206
|
-
// src/
|
|
2207
|
-
function
|
|
2208
|
-
const
|
|
2209
|
-
const
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
createdAt: conversation.createdAt
|
|
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
|
-
};
|
|
2230
|
-
}
|
|
2231
|
-
function toConversationMessage(message) {
|
|
2232
|
-
return {
|
|
2233
|
-
kind: message.kind === "issue-body" ? "issue" : "comment",
|
|
2234
|
-
author: message.author,
|
|
2235
|
-
body: message.body,
|
|
2236
|
-
createdAt: message.createdAt,
|
|
2237
|
-
isNew: message.isNew
|
|
2238
|
-
};
|
|
2127
|
+
// src/claude/claudeService.ts
|
|
2128
|
+
function resolveCommand(name) {
|
|
2129
|
+
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
2130
|
+
for (const dir of pathDirs) {
|
|
2131
|
+
const candidate = join(dir, name);
|
|
2132
|
+
if (existsSync(candidate)) return candidate;
|
|
2133
|
+
}
|
|
2134
|
+
return name;
|
|
2239
2135
|
}
|
|
2240
|
-
function
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
}))
|
|
2249
|
-
);
|
|
2136
|
+
function buildClaudeArgs(prompt, options = {}) {
|
|
2137
|
+
const args = [];
|
|
2138
|
+
if (options.yolo) args.push("--dangerously-skip-permissions");
|
|
2139
|
+
if (options.model) args.push("--model", options.model);
|
|
2140
|
+
if (options.effort) args.push("--effort", options.effort);
|
|
2141
|
+
if (options.verbose) args.push("--verbose", "--output-format", "stream-json");
|
|
2142
|
+
args.push("-p", prompt);
|
|
2143
|
+
return args;
|
|
2250
2144
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2145
|
+
function runClaude(prompt, options = {}) {
|
|
2146
|
+
return new Promise((resolve2, reject) => {
|
|
2147
|
+
const claudeBin = resolveCommand("claude");
|
|
2148
|
+
const args = buildClaudeArgs(prompt, {
|
|
2149
|
+
yolo: true,
|
|
2150
|
+
model: options.model,
|
|
2151
|
+
effort: options.effort,
|
|
2152
|
+
verbose: true
|
|
2153
|
+
});
|
|
2154
|
+
const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
|
|
2155
|
+
trackChild(child);
|
|
2156
|
+
const rl = createInterface({ input: child.stdout });
|
|
2157
|
+
let turnCount = 0;
|
|
2158
|
+
rl.on("line", (line) => {
|
|
2159
|
+
if (options.printSteps !== true) return;
|
|
2160
|
+
try {
|
|
2161
|
+
const event = JSON.parse(line);
|
|
2162
|
+
formatEvent(event, turnCount);
|
|
2163
|
+
if (event["type"] === "assistant") turnCount++;
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
child.on("error", (err) => {
|
|
2168
|
+
untrackChild(child);
|
|
2169
|
+
const nodeErr = err;
|
|
2170
|
+
reject(
|
|
2171
|
+
nodeErr.code === "ENOENT" ? new Error("`claude` CLI is not installed or not on PATH.") : new Error(nodeErr.message)
|
|
2172
|
+
);
|
|
2173
|
+
});
|
|
2174
|
+
child.on("close", (code, signal) => {
|
|
2175
|
+
untrackChild(child);
|
|
2176
|
+
if (code === 0) {
|
|
2177
|
+
resolve2();
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
reject(
|
|
2181
|
+
new Error(
|
|
2182
|
+
signal === null ? `Claude Code exited with code ${String(code)}.` : `Claude Code terminated on ${signal}.`
|
|
2183
|
+
)
|
|
2184
|
+
);
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2258
2187
|
}
|
|
2259
|
-
function
|
|
2260
|
-
|
|
2188
|
+
function invokeClaudeCode(prompt, options = {}) {
|
|
2189
|
+
if (options.verbose) {
|
|
2190
|
+
return invokeClaudeCodeVerbose(prompt, options.yolo ?? false, options.model, options.effort);
|
|
2191
|
+
}
|
|
2192
|
+
invokeClaudeCodeSync(prompt, options.yolo ?? false, options.model, options.effort);
|
|
2261
2193
|
}
|
|
2262
|
-
function
|
|
2263
|
-
|
|
2194
|
+
function invokeClaudeCodeSync(prompt, yolo, model, effort) {
|
|
2195
|
+
const claudeBin = resolveCommand("claude");
|
|
2196
|
+
const args = buildClaudeArgs(prompt, { yolo, model, effort, verbose: false });
|
|
2197
|
+
const result = spawnSync5(claudeBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
2198
|
+
handleSpawnError(result.error, "claude");
|
|
2199
|
+
handleExitCode(result.status, "Claude Code");
|
|
2264
2200
|
}
|
|
2265
|
-
function
|
|
2266
|
-
return
|
|
2201
|
+
function invokeClaudeCodeVerbose(prompt, yolo, model, effort) {
|
|
2202
|
+
return new Promise((resolve2) => {
|
|
2203
|
+
const claudeBin = resolveCommand("claude");
|
|
2204
|
+
const args = buildClaudeArgs(prompt, { yolo, model, effort, verbose: true });
|
|
2205
|
+
const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
|
|
2206
|
+
const rl = createInterface({ input: child.stdout });
|
|
2207
|
+
let turnCount = 0;
|
|
2208
|
+
child.on("error", (err) => {
|
|
2209
|
+
handleSpawnError(err, "claude");
|
|
2210
|
+
});
|
|
2211
|
+
rl.on("line", (line) => {
|
|
2212
|
+
try {
|
|
2213
|
+
const event = JSON.parse(line);
|
|
2214
|
+
formatEvent(event, turnCount);
|
|
2215
|
+
if (event["type"] === "assistant") turnCount++;
|
|
2216
|
+
} catch {
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2219
|
+
child.on("close", (code) => {
|
|
2220
|
+
handleExitCode(code, "Claude Code");
|
|
2221
|
+
resolve2();
|
|
2222
|
+
});
|
|
2223
|
+
});
|
|
2267
2224
|
}
|
|
2268
|
-
function
|
|
2269
|
-
const
|
|
2270
|
-
|
|
2271
|
-
|
|
2225
|
+
function formatAssistantEvent(event, turnCount) {
|
|
2226
|
+
const message = event["message"];
|
|
2227
|
+
const content = message?.["content"];
|
|
2228
|
+
if (!content) return;
|
|
2229
|
+
for (const block of content) {
|
|
2230
|
+
const line = formatContentBlock(block);
|
|
2231
|
+
if (line !== null) {
|
|
2232
|
+
process.stderr.write(` [step ${turnCount + 1}] ${line}
|
|
2272
2233
|
`);
|
|
2273
|
-
|
|
2234
|
+
}
|
|
2274
2235
|
}
|
|
2275
|
-
return executor;
|
|
2276
2236
|
}
|
|
2277
|
-
function
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
return;
|
|
2237
|
+
function formatContentBlock(block) {
|
|
2238
|
+
if (block["type"] === "tool_use") {
|
|
2239
|
+
const toolName = block["name"];
|
|
2240
|
+
const input = block["input"];
|
|
2241
|
+
return summarizeTool(toolName, input);
|
|
2282
2242
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
}
|
|
2243
|
+
if (block["type"] === "text") {
|
|
2244
|
+
const text = block["text"] ?? "";
|
|
2245
|
+
if (text.length === 0) return null;
|
|
2246
|
+
const preview = text.length > 120 ? text.slice(0, 120) + "..." : text;
|
|
2247
|
+
return preview.split("\n")[0] ?? null;
|
|
2248
|
+
}
|
|
2249
|
+
return null;
|
|
2289
2250
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2251
|
+
function formatResultEvent(event) {
|
|
2252
|
+
const result = event["result"];
|
|
2253
|
+
const cost = event["cost_usd"];
|
|
2254
|
+
const duration = event["duration_ms"];
|
|
2255
|
+
const turns = event["num_turns"];
|
|
2256
|
+
process.stderr.write("\n--- Result ---\n");
|
|
2257
|
+
const parts = [];
|
|
2258
|
+
if (turns !== void 0) parts.push(`${turns} turns`);
|
|
2259
|
+
if (duration !== void 0) parts.push(`${(duration / 1e3).toFixed(1)}s`);
|
|
2260
|
+
if (cost !== void 0) parts.push(`$${cost.toFixed(4)}`);
|
|
2261
|
+
if (parts.length > 0) {
|
|
2262
|
+
process.stderr.write(` [info] ${parts.join(" | ")}
|
|
2301
2263
|
`);
|
|
2302
|
-
process.exit(1);
|
|
2303
2264
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
pr = await getPrInfo2(branch);
|
|
2307
|
-
} catch (err) {
|
|
2308
|
-
process.stderr.write(`Error: ${err.message}
|
|
2309
|
-
`);
|
|
2310
|
-
process.exit(1);
|
|
2265
|
+
if (result) {
|
|
2266
|
+
process.stdout.write(result + "\n");
|
|
2311
2267
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2268
|
+
}
|
|
2269
|
+
function formatEvent(event, turnCount) {
|
|
2270
|
+
const type = event["type"];
|
|
2271
|
+
if (type === "assistant") {
|
|
2272
|
+
formatAssistantEvent(event, turnCount);
|
|
2273
|
+
} else if (type === "result") {
|
|
2274
|
+
formatResultEvent(event);
|
|
2316
2275
|
}
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2276
|
+
}
|
|
2277
|
+
function asText(value, fallback) {
|
|
2278
|
+
return typeof value === "string" ? value : fallback;
|
|
2279
|
+
}
|
|
2280
|
+
function summarizeTool(name, input) {
|
|
2281
|
+
if (!input) return `tool: ${name}`;
|
|
2282
|
+
switch (name) {
|
|
2283
|
+
case "Read":
|
|
2284
|
+
return `reading ${asText(input["file_path"], "file")}`;
|
|
2285
|
+
case "Write":
|
|
2286
|
+
return `writing ${asText(input["file_path"], "file")}`;
|
|
2287
|
+
case "Edit":
|
|
2288
|
+
return `editing ${asText(input["file_path"], "file")}`;
|
|
2289
|
+
case "Bash":
|
|
2290
|
+
return `running: ${truncate(asText(input["command"], ""), 80)}`;
|
|
2291
|
+
case "Glob":
|
|
2292
|
+
return `searching files: ${asText(input["pattern"], "")}`;
|
|
2293
|
+
case "Grep":
|
|
2294
|
+
return `searching content: ${truncate(asText(input["pattern"], ""), 60)}`;
|
|
2295
|
+
case "Agent":
|
|
2296
|
+
return `spawning agent: ${asText(input["description"], name)}`;
|
|
2297
|
+
default:
|
|
2298
|
+
return `tool: ${name}`;
|
|
2323
2299
|
}
|
|
2324
|
-
|
|
2325
|
-
const sonarPromptText = config.prompts?.sonar ?? DEFAULT_SONAR_PROMPT;
|
|
2326
|
-
const fullPrompt = withPush(
|
|
2327
|
-
`${sonarPromptText}
|
|
2328
|
-
|
|
2329
|
-
SonarCloud analysis URL: ${pr.sonarcloudUrl}
|
|
2300
|
+
}
|
|
2330
2301
|
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
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");
|
|
2302
|
+
// src/codex/codexService.ts
|
|
2303
|
+
import { spawn as spawn2, spawnSync as spawnSync6 } from "child_process";
|
|
2304
|
+
function toTomlBasicString(value) {
|
|
2305
|
+
return JSON.stringify(value);
|
|
2343
2306
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
)
|
|
2348
|
-
).
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2307
|
+
function buildCodexArgs(prompt, options = {}) {
|
|
2308
|
+
const args = ["exec"];
|
|
2309
|
+
if (options.yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
2310
|
+
if (options.model) args.push("--model", options.model);
|
|
2311
|
+
if (options.effort) args.push("-c", `model_reasoning_effort=${toTomlBasicString(options.effort)}`);
|
|
2312
|
+
args.push(prompt);
|
|
2313
|
+
return args;
|
|
2314
|
+
}
|
|
2315
|
+
function invokeCodexCode(prompt, options = {}) {
|
|
2316
|
+
if (options.verbose) {
|
|
2317
|
+
process.stderr.write("Warning: --verbose is not supported for Codex and will be ignored.\n");
|
|
2318
|
+
}
|
|
2319
|
+
invokeCodexCodeSync(prompt, options.yolo ?? false, options.model, options.effort);
|
|
2320
|
+
}
|
|
2321
|
+
function invokeCodexCodeSync(prompt, yolo, model, effort) {
|
|
2322
|
+
const codexBin = resolveCommand("codex");
|
|
2323
|
+
const args = buildCodexArgs(prompt, { yolo, model, effort });
|
|
2324
|
+
const result = spawnSync6(codexBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
2325
|
+
handleSpawnError(result.error, "codex");
|
|
2326
|
+
handleExitCode(result.status, "Codex");
|
|
2327
|
+
}
|
|
2328
|
+
function runCodex(prompt, options = {}) {
|
|
2329
|
+
return new Promise((resolve2, reject) => {
|
|
2330
|
+
const codexBin = resolveCommand("codex");
|
|
2331
|
+
const args = buildCodexArgs(prompt, { yolo: true, model: options.model, effort: options.effort });
|
|
2332
|
+
const child = spawn2(codexBin, args, { stdio: "inherit" });
|
|
2333
|
+
trackChild(child);
|
|
2334
|
+
child.on("error", (err) => {
|
|
2335
|
+
untrackChild(child);
|
|
2336
|
+
const nodeErr = err;
|
|
2337
|
+
reject(
|
|
2338
|
+
nodeErr.code === "ENOENT" ? new Error("`codex` CLI is not installed or not on PATH.") : new Error(nodeErr.message)
|
|
2361
2339
|
);
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2340
|
+
});
|
|
2341
|
+
child.on("close", (code, signal) => {
|
|
2342
|
+
untrackChild(child);
|
|
2343
|
+
if (code === 0) {
|
|
2344
|
+
resolve2();
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
reject(
|
|
2348
|
+
new Error(
|
|
2349
|
+
signal === null ? `Codex exited with code ${String(code)}.` : `Codex terminated on ${signal}.`
|
|
2350
|
+
)
|
|
2351
|
+
);
|
|
2352
|
+
});
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// src/commands/getReady.ts
|
|
2357
|
+
function writeOverflowHint(output, issues, limit) {
|
|
2358
|
+
if (issues.length === limit) {
|
|
2359
|
+
output.write(`(Showing first ${limit} matching issues \u2014 there may be more. Use --limit to fetch more.)
|
|
2365
2360
|
`);
|
|
2366
|
-
process.exit(1);
|
|
2367
2361
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2362
|
+
}
|
|
2363
|
+
function writeIssueList(output, issues, limit) {
|
|
2364
|
+
output.write("\nAvailable issues:\n");
|
|
2365
|
+
for (let i = 0; i < issues.length; i++) {
|
|
2366
|
+
output.write(` [${i + 1}] #${issues[i].number} - ${issues[i].title}
|
|
2371
2367
|
`);
|
|
2372
|
-
process.exit(1);
|
|
2373
2368
|
}
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
${formatComments(comments)}`,
|
|
2384
|
-
options.push
|
|
2369
|
+
writeOverflowHint(output, issues, limit);
|
|
2370
|
+
}
|
|
2371
|
+
async function promptSelection(issues, limit, output) {
|
|
2372
|
+
writeIssueList(output, issues, limit);
|
|
2373
|
+
const rl = createInterface2({ input: process.stdin, output });
|
|
2374
|
+
const answer = await new Promise(
|
|
2375
|
+
(resolve2) => rl.question(`
|
|
2376
|
+
Select issue (1-${issues.length}): `, resolve2)
|
|
2385
2377
|
);
|
|
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}').
|
|
2378
|
+
rl.close();
|
|
2379
|
+
const n = Number.parseInt(answer.trim(), 10);
|
|
2380
|
+
if (Number.isNaN(n) || n < 1 || n > issues.length) {
|
|
2381
|
+
process.stderr.write(`Error: Invalid selection "${answer.trim()}". Enter a number between 1 and ${issues.length}.
|
|
2397
2382
|
`);
|
|
2398
2383
|
process.exit(1);
|
|
2399
2384
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2385
|
+
return issues[n - 1];
|
|
2386
|
+
}
|
|
2387
|
+
function validateConfig(config) {
|
|
2388
|
+
if (config.remoteType !== "gh") {
|
|
2402
2389
|
process.stderr.write(
|
|
2403
|
-
"Error:
|
|
2390
|
+
"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
2391
|
);
|
|
2405
2392
|
process.exit(1);
|
|
2406
2393
|
}
|
|
2407
|
-
|
|
2408
|
-
if (allowedUsers.length === 0) {
|
|
2394
|
+
if (!config.issueDiscoveryTechnique) {
|
|
2409
2395
|
process.stderr.write(
|
|
2410
|
-
"Error: No
|
|
2396
|
+
"Error: No issue discovery technique configured. Run `automata config` to set one.\n"
|
|
2411
2397
|
);
|
|
2412
2398
|
process.exit(1);
|
|
2413
2399
|
}
|
|
2414
|
-
|
|
2415
|
-
if (agentUser.length === 0) {
|
|
2400
|
+
if (!config.issueDiscoveryValue) {
|
|
2416
2401
|
process.stderr.write(
|
|
2417
|
-
"Error: No
|
|
2402
|
+
"Error: No issue discovery value configured. Run `automata config` to set one.\n"
|
|
2418
2403
|
);
|
|
2419
2404
|
process.exit(1);
|
|
2420
2405
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
process.
|
|
2426
|
-
|
|
2427
|
-
process.exit(1);
|
|
2406
|
+
}
|
|
2407
|
+
async function resolveIssue(issues, options, limit) {
|
|
2408
|
+
const selectionOutput = options.json ? process.stderr : process.stdout;
|
|
2409
|
+
if (issues.length === 0) {
|
|
2410
|
+
process.stdout.write("No issues found matching the configured filter.\n");
|
|
2411
|
+
process.exit(0);
|
|
2428
2412
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
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;
|
|
2413
|
+
if (issues.length > 1 && options.queryOnly) {
|
|
2414
|
+
writeIssueList(selectionOutput, issues, limit);
|
|
2415
|
+
process.exit(0);
|
|
2437
2416
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
`
|
|
2442
|
-
|
|
2417
|
+
let issue;
|
|
2418
|
+
if (issues.length === 1) {
|
|
2419
|
+
issue = issues[0];
|
|
2420
|
+
selectionOutput.write(`Issue: #${issue.number}
|
|
2421
|
+
Title: ${issue.title}
|
|
2422
|
+
`);
|
|
2423
|
+
} else if (options.takeFirst) {
|
|
2424
|
+
issue = issues[0];
|
|
2425
|
+
selectionOutput.write(`Selecting issue #${issue.number}: ${issue.title}
|
|
2426
|
+
`);
|
|
2443
2427
|
} else {
|
|
2444
|
-
|
|
2428
|
+
issue = await promptSelection(issues, limit, selectionOutput);
|
|
2429
|
+
selectionOutput.write(`
|
|
2430
|
+
Issue: #${issue.number}
|
|
2431
|
+
Title: ${issue.title}
|
|
2445
2432
|
`);
|
|
2446
2433
|
}
|
|
2447
|
-
|
|
2448
|
-
const fullPrompt = withPush(
|
|
2449
|
-
`${promptText}
|
|
2450
|
-
|
|
2451
|
-
Issue #${String(issueNumber)}: ${conversation.title}
|
|
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.";
|
|
2460
|
-
try {
|
|
2461
|
-
postComment(issueNumber, marker);
|
|
2462
|
-
} catch (err) {
|
|
2463
|
-
process.stderr.write(
|
|
2464
|
-
`Error: could not post the execution marker comment on issue #${String(issueNumber)}: ${err.message}
|
|
2465
|
-
`
|
|
2466
|
-
);
|
|
2467
|
-
process.exit(1);
|
|
2468
|
-
}
|
|
2469
|
-
await invokeSelectedExecutor(fullPrompt, executor, options);
|
|
2470
|
-
});
|
|
2471
|
-
var executePromptCommand = new Command5("execute-prompt").description("Execute a configured custom prompt using an AI assistant").addCommand(executeSonarCmd).addCommand(executeFixCommentsCmd).addCommand(executeCheckIssueCmd);
|
|
2472
|
-
|
|
2473
|
-
// src/commands/doWork.ts
|
|
2474
|
-
import { Command as Command6 } from "commander";
|
|
2475
|
-
|
|
2476
|
-
// src/github/ghWorkService.ts
|
|
2477
|
-
import { spawnSync as spawnSync6 } from "child_process";
|
|
2478
|
-
function run4(cmd, args) {
|
|
2479
|
-
const result = spawnSync6(cmd, args, { encoding: "utf8" });
|
|
2480
|
-
if (result.error) {
|
|
2481
|
-
const err = result.error;
|
|
2482
|
-
if (err.code === "ENOENT") {
|
|
2483
|
-
throw new Error(`\`${cmd}\` CLI is not installed or not on PATH.`);
|
|
2484
|
-
}
|
|
2485
|
-
throw new Error(err.message);
|
|
2486
|
-
}
|
|
2487
|
-
return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", status: result.status ?? 1 };
|
|
2488
|
-
}
|
|
2489
|
-
function ghJson(args, what) {
|
|
2490
|
-
const { stdout, stderr, status } = run4("gh", args);
|
|
2491
|
-
if (status !== 0) {
|
|
2492
|
-
throw new Error(stderr.trim() || `Failed to ${what}. Is \`gh\` installed and authenticated?`);
|
|
2493
|
-
}
|
|
2494
|
-
return JSON.parse(stdout);
|
|
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;
|
|
2505
|
-
}
|
|
2506
|
-
function getRepoSlug() {
|
|
2507
|
-
const { stdout, status } = run4("git", ["remote", "get-url", "origin"]);
|
|
2508
|
-
if (status !== 0) {
|
|
2509
|
-
throw new Error("Could not read the `origin` remote. Is this a git repository with a remote?");
|
|
2510
|
-
}
|
|
2511
|
-
const url = stdout.trim();
|
|
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}`);
|
|
2515
|
-
}
|
|
2516
|
-
return { owner: match[1], repo: match[2] };
|
|
2517
|
-
}
|
|
2518
|
-
function getAuthenticatedLogin() {
|
|
2519
|
-
const { stdout, status } = run4("gh", ["api", "user", "--jq", ".login"]);
|
|
2520
|
-
if (status !== 0) return null;
|
|
2521
|
-
const login2 = stdout.trim();
|
|
2522
|
-
return login2.length > 0 ? login2 : null;
|
|
2434
|
+
return issue;
|
|
2523
2435
|
}
|
|
2524
|
-
|
|
2525
|
-
const
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
"
|
|
2530
|
-
|
|
2531
|
-
|
|
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;
|
|
2436
|
+
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) => {
|
|
2437
|
+
const config = readConfig();
|
|
2438
|
+
validateConfig(config);
|
|
2439
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
2440
|
+
if (Number.isNaN(limit) || limit <= 0) {
|
|
2441
|
+
process.stderr.write(`Error: --limit must be a positive integer (got "${options.limit}").
|
|
2442
|
+
`);
|
|
2443
|
+
process.exit(1);
|
|
2545
2444
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
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
|
-
}
|
|
2445
|
+
let issues;
|
|
2446
|
+
try {
|
|
2447
|
+
issues = listIssues(config.issueDiscoveryTechnique, config.issueDiscoveryValue, limit);
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
process.stderr.write(`Error: ${err.message}
|
|
2450
|
+
`);
|
|
2451
|
+
process.exit(1);
|
|
2597
2452
|
}
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2453
|
+
const issue = await resolveIssue(issues, options, limit);
|
|
2454
|
+
if (options.json) {
|
|
2455
|
+
process.stdout.write(JSON.stringify({ number: issue.number, title: issue.title, body: issue.body, url: issue.url }, null, 2) + "\n");
|
|
2456
|
+
} else {
|
|
2457
|
+
process.stdout.write(`URL: ${issue.url}
|
|
2458
|
+
|
|
2459
|
+
${issue.body}
|
|
2460
|
+
`);
|
|
2606
2461
|
}
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2462
|
+
if (options.queryOnly) {
|
|
2463
|
+
process.exit(0);
|
|
2464
|
+
}
|
|
2465
|
+
const executor = options.claude === false ? void 0 : resolveExecutor(options.with);
|
|
2466
|
+
let commentUrl;
|
|
2467
|
+
try {
|
|
2468
|
+
commentUrl = postComment(issue.number, "working");
|
|
2469
|
+
} catch (err) {
|
|
2470
|
+
process.stderr.write(`Error: ${err.message}
|
|
2471
|
+
`);
|
|
2472
|
+
process.exit(1);
|
|
2473
|
+
}
|
|
2474
|
+
const effort = resolveEffortOption(options.effort);
|
|
2475
|
+
if (options.claude !== false) {
|
|
2476
|
+
const systemPrompt = config.claudeSystemPrompt ?? DEFAULT_CLAUDE_SYSTEM_PROMPT;
|
|
2477
|
+
const prompt = `Resolving issue #${issue.number}:
|
|
2478
|
+
|
|
2479
|
+
${systemPrompt}
|
|
2480
|
+
|
|
2481
|
+
${issue.body}`;
|
|
2482
|
+
if (executor === "codex") {
|
|
2483
|
+
if (options.silent) {
|
|
2484
|
+
process.stderr.write("Warning: --silent is only supported with Claude and has no effect when used with Codex.\n");
|
|
2485
|
+
}
|
|
2486
|
+
invokeCodexCode(prompt, { yolo: options.yolo, model: options.model, effort });
|
|
2614
2487
|
} else {
|
|
2615
|
-
|
|
2488
|
+
await invokeClaudeCode(prompt, {
|
|
2489
|
+
yolo: options.yolo,
|
|
2490
|
+
verbose: !options.silent,
|
|
2491
|
+
model: options.model,
|
|
2492
|
+
effort
|
|
2493
|
+
});
|
|
2616
2494
|
}
|
|
2617
2495
|
}
|
|
2618
|
-
|
|
2496
|
+
linkPrToIssue(issue.number, commentUrl, options.askCopilotReview === true, config.agentUser);
|
|
2497
|
+
});
|
|
2498
|
+
function resolveExecutor(requested) {
|
|
2499
|
+
const executor = requested.toLowerCase();
|
|
2500
|
+
if (executor !== "claude" && executor !== "codex") {
|
|
2501
|
+
process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${requested}'.
|
|
2502
|
+
`);
|
|
2503
|
+
process.exit(1);
|
|
2504
|
+
}
|
|
2505
|
+
return executor;
|
|
2619
2506
|
}
|
|
2620
|
-
function
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
isCrossRepository: node.isCrossRepository,
|
|
2628
|
-
state: "OPEN",
|
|
2629
|
-
isDraft: node.isDraft,
|
|
2630
|
-
updatedAt: node.updatedAt
|
|
2631
|
-
};
|
|
2507
|
+
function warnOnFailure(what, action) {
|
|
2508
|
+
try {
|
|
2509
|
+
action();
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
process.stderr.write(`Warning: could not ${what}: ${err.message}
|
|
2512
|
+
`);
|
|
2513
|
+
}
|
|
2632
2514
|
}
|
|
2633
|
-
function
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2515
|
+
function linkPrToIssue(issueNumber, commentUrl, askCopilotReview, agentUser) {
|
|
2516
|
+
let pr;
|
|
2517
|
+
try {
|
|
2518
|
+
pr = getCurrentBranchPr();
|
|
2519
|
+
} catch (err) {
|
|
2520
|
+
process.stderr.write(`Warning: could not detect current branch PR: ${err.message}
|
|
2521
|
+
`);
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
if (!pr) return;
|
|
2525
|
+
if (commentUrl) {
|
|
2526
|
+
warnOnFailure("update issue comment", () => {
|
|
2527
|
+
editComment(commentUrl, `Working on this in PR #${pr.number} \u2014 ${pr.url}`);
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
warnOnFailure(`add Closes #${String(issueNumber)} to PR`, () => {
|
|
2531
|
+
addClosesRefToPr(pr.number, issueNumber);
|
|
2532
|
+
});
|
|
2533
|
+
if (pr.assignees.length === 0) {
|
|
2534
|
+
const claimant = (agentUser ?? "").trim() || "@me";
|
|
2535
|
+
warnOnFailure(`assign PR #${String(pr.number)} to ${claimant}`, () => {
|
|
2536
|
+
assignPrToAgent(pr.number, claimant);
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
if (askCopilotReview) {
|
|
2540
|
+
warnOnFailure("request Copilot review", () => {
|
|
2541
|
+
addCopilotReviewer(pr.number);
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2639
2544
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2545
|
+
|
|
2546
|
+
// src/commands/execute.ts
|
|
2547
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2548
|
+
import { Command as Command4 } from "commander";
|
|
2549
|
+
function readStdin() {
|
|
2550
|
+
return new Promise((resolve2, reject) => {
|
|
2551
|
+
const chunks = [];
|
|
2552
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
2553
|
+
process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
2554
|
+
process.stdin.on("error", reject);
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
async function resolvePrompt(options) {
|
|
2558
|
+
const hasCli = options.prompt !== void 0;
|
|
2559
|
+
const hasFile = options.filePrompt !== void 0;
|
|
2560
|
+
if (hasCli && hasFile) {
|
|
2561
|
+
process.stderr.write("Error: --prompt is mutually exclusive with --file-prompt.\n");
|
|
2562
|
+
process.exit(1);
|
|
2563
|
+
}
|
|
2564
|
+
if (hasCli) {
|
|
2565
|
+
return options.prompt;
|
|
2566
|
+
}
|
|
2567
|
+
if (hasFile) {
|
|
2568
|
+
const path = options.filePrompt;
|
|
2569
|
+
try {
|
|
2570
|
+
return readFileSync2(path, "utf8");
|
|
2571
|
+
} catch {
|
|
2572
|
+
process.stderr.write(`Error: Cannot read file: ${path}
|
|
2573
|
+
`);
|
|
2574
|
+
process.exit(1);
|
|
2659
2575
|
}
|
|
2660
|
-
cursor = connection.pageInfo.endCursor;
|
|
2661
2576
|
}
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
query($owner:String!,$repo:String!,$prNumber:Int!,$cursor:String){
|
|
2668
|
-
repository(owner:$owner,name:$repo){
|
|
2669
|
-
pullRequest(number:$prNumber){
|
|
2670
|
-
reviewThreads(first:100, after:$cursor){
|
|
2671
|
-
pageInfo{ hasNextPage endCursor }
|
|
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
|
-
}
|
|
2577
|
+
if (!process.stdin.isTTY) {
|
|
2578
|
+
const content = await readStdin();
|
|
2579
|
+
if (content.trim().length === 0) {
|
|
2580
|
+
process.stderr.write("Error: No prompt provided. Use --prompt, --file-prompt, or pipe via stdin.\n");
|
|
2581
|
+
process.exit(1);
|
|
2680
2582
|
}
|
|
2583
|
+
return content;
|
|
2681
2584
|
}
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
function normalizePrState(state) {
|
|
2685
|
-
if (state === "MERGED") return "MERGED";
|
|
2686
|
-
if (state === "CLOSED") return "CLOSED";
|
|
2687
|
-
return "OPEN";
|
|
2585
|
+
process.stderr.write("Error: No prompt provided. Use --prompt, --file-prompt, or pipe via stdin.\n");
|
|
2586
|
+
process.exit(1);
|
|
2688
2587
|
}
|
|
2689
|
-
|
|
2690
|
-
const
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
}
|
|
2738
|
-
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
2739
|
-
return threads;
|
|
2588
|
+
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) => {
|
|
2589
|
+
const executor = options.with.toLowerCase();
|
|
2590
|
+
if (executor !== "claude" && executor !== "codex") {
|
|
2591
|
+
process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${options.with}'.
|
|
2592
|
+
`);
|
|
2593
|
+
process.exit(1);
|
|
2594
|
+
}
|
|
2595
|
+
const prompt = await resolvePrompt(options);
|
|
2596
|
+
const effort = resolveEffortOption(options.effort);
|
|
2597
|
+
if (executor === "codex") {
|
|
2598
|
+
invokeCodexCode(prompt, { yolo: true, model: options.model, effort });
|
|
2599
|
+
} else {
|
|
2600
|
+
await invokeClaudeCode(prompt, {
|
|
2601
|
+
yolo: true,
|
|
2602
|
+
verbose: !options.silent,
|
|
2603
|
+
model: options.model,
|
|
2604
|
+
effort
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
|
|
2609
|
+
// src/commands/executePrompt.ts
|
|
2610
|
+
import { Command as Command5 } from "commander";
|
|
2611
|
+
|
|
2612
|
+
// src/github/conversation.ts
|
|
2613
|
+
var KIND_LABELS = {
|
|
2614
|
+
"issue-body": "issue description",
|
|
2615
|
+
"issue-comment": "comment",
|
|
2616
|
+
"pr-comment": "pull request comment",
|
|
2617
|
+
"pr-review": "pull request review",
|
|
2618
|
+
"thread-comment": "review thread comment"
|
|
2619
|
+
};
|
|
2620
|
+
function byCreatedAt2(a, b) {
|
|
2621
|
+
return a.createdAt.localeCompare(b.createdAt);
|
|
2622
|
+
}
|
|
2623
|
+
function classify(author, p) {
|
|
2624
|
+
const login2 = author.toLowerCase();
|
|
2625
|
+
if (login2 === p.agentUser.toLowerCase()) return "agent";
|
|
2626
|
+
return p.allowedUsers.some((user) => user.toLowerCase() === login2) ? "authorized" : "other";
|
|
2627
|
+
}
|
|
2628
|
+
function analyzeSurface(messages, p) {
|
|
2629
|
+
const ordered = [...messages].sort(byCreatedAt2);
|
|
2630
|
+
let lastAgentAt = null;
|
|
2631
|
+
for (const message of ordered) {
|
|
2632
|
+
if (message.kind === "issue-body") continue;
|
|
2633
|
+
if (classify(message.author, p) !== "agent") continue;
|
|
2634
|
+
if (lastAgentAt === null || message.createdAt > lastAgentAt) {
|
|
2635
|
+
lastAgentAt = message.createdAt;
|
|
2740
2636
|
}
|
|
2741
|
-
cursor = connection.pageInfo.endCursor;
|
|
2742
2637
|
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2638
|
+
const kept = [];
|
|
2639
|
+
for (const message of ordered) {
|
|
2640
|
+
const authorClass = classify(message.author, p);
|
|
2641
|
+
if (authorClass === "other") continue;
|
|
2642
|
+
const isNew = authorClass === "authorized" && (lastAgentAt === null || message.createdAt > lastAgentAt);
|
|
2643
|
+
kept.push({ ...message, isNew });
|
|
2644
|
+
}
|
|
2645
|
+
const newMessages = kept.filter((message) => message.isNew);
|
|
2646
|
+
return {
|
|
2647
|
+
messages: kept,
|
|
2648
|
+
newMessages,
|
|
2649
|
+
newMessageCount: newMessages.length,
|
|
2650
|
+
hasNewMessage: newMessages.length > 0,
|
|
2651
|
+
lastAgentAt
|
|
2652
|
+
};
|
|
2746
2653
|
}
|
|
2747
|
-
function
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
]
|
|
2756
|
-
|
|
2757
|
-
);
|
|
2654
|
+
function lastAuthorClass(messages, p) {
|
|
2655
|
+
if (messages.length === 0) return "none";
|
|
2656
|
+
const newest2 = [...messages].sort(byCreatedAt2).at(-1);
|
|
2657
|
+
return newest2 === void 0 ? "none" : classify(newest2.author, p);
|
|
2658
|
+
}
|
|
2659
|
+
function formatMessages(messages) {
|
|
2660
|
+
return messages.map((message) => {
|
|
2661
|
+
const marker = message.isNew ? " \xB7 NEW since last agent run" : "";
|
|
2662
|
+
return `[${message.author}] ${KIND_LABELS[message.kind]} \xB7 ${message.createdAt}${marker}
|
|
2663
|
+
${message.body}`;
|
|
2664
|
+
}).join("\n\n");
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// src/github/issueConversation.ts
|
|
2668
|
+
function analyzeConversation(conversation, allowedUsers, agentUser) {
|
|
2669
|
+
const participants = { allowedUsers, agentUser };
|
|
2758
2670
|
const messages = [
|
|
2759
|
-
|
|
2760
|
-
kind: "
|
|
2761
|
-
author:
|
|
2671
|
+
{
|
|
2672
|
+
kind: "issue-body",
|
|
2673
|
+
author: conversation.author,
|
|
2674
|
+
body: conversation.body,
|
|
2675
|
+
createdAt: conversation.createdAt
|
|
2676
|
+
},
|
|
2677
|
+
...conversation.comments.map((comment) => ({
|
|
2678
|
+
kind: "issue-comment",
|
|
2679
|
+
author: comment.author,
|
|
2762
2680
|
body: comment.body,
|
|
2763
2681
|
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
2682
|
}))
|
|
2773
|
-
]
|
|
2774
|
-
const
|
|
2775
|
-
const state = normalizePrState(raw.state);
|
|
2683
|
+
];
|
|
2684
|
+
const analysis = analyzeSurface(messages, participants);
|
|
2776
2685
|
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
|
|
2686
|
+
messages: analysis.messages.map(toConversationMessage),
|
|
2687
|
+
newMessageCount: analysis.newMessageCount,
|
|
2688
|
+
hasNewMessage: analysis.hasNewMessage,
|
|
2689
|
+
lastAgentAt: analysis.lastAgentAt
|
|
2790
2690
|
};
|
|
2791
2691
|
}
|
|
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
|
-
}
|
|
2692
|
+
function toConversationMessage(message) {
|
|
2693
|
+
return {
|
|
2694
|
+
kind: message.kind === "issue-body" ? "issue" : "comment",
|
|
2695
|
+
author: message.author,
|
|
2696
|
+
body: message.body,
|
|
2697
|
+
createdAt: message.createdAt,
|
|
2698
|
+
isNew: message.isNew
|
|
2699
|
+
};
|
|
2803
2700
|
}
|
|
2804
|
-
function
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
`body=${body}`
|
|
2814
|
-
],
|
|
2815
|
-
`post a comment on #${String(number)}`
|
|
2701
|
+
function formatConversation(messages) {
|
|
2702
|
+
return formatMessages(
|
|
2703
|
+
messages.map((message) => ({
|
|
2704
|
+
kind: message.kind === "issue" ? "issue-body" : "issue-comment",
|
|
2705
|
+
author: message.author,
|
|
2706
|
+
body: message.body,
|
|
2707
|
+
createdAt: message.createdAt,
|
|
2708
|
+
isNew: message.isNew
|
|
2709
|
+
}))
|
|
2816
2710
|
);
|
|
2817
|
-
return { commentId: String(raw.id), createdAt: raw.created_at };
|
|
2818
2711
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2712
|
+
|
|
2713
|
+
// src/commands/executePrompt.ts
|
|
2714
|
+
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.";
|
|
2715
|
+
function withPush(prompt, push) {
|
|
2716
|
+
return push ? `${prompt}
|
|
2717
|
+
|
|
2718
|
+
${PUSH_INSTRUCTION}` : prompt;
|
|
2719
|
+
}
|
|
2720
|
+
function formatPrInfoContext(pr) {
|
|
2721
|
+
return JSON.stringify(pr, null, 2);
|
|
2722
|
+
}
|
|
2723
|
+
function pluralSuffix(count) {
|
|
2724
|
+
return count === 1 ? "" : "s";
|
|
2725
|
+
}
|
|
2726
|
+
function addAiOptions(cmd) {
|
|
2727
|
+
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");
|
|
2728
|
+
}
|
|
2729
|
+
function resolveExecutor2(withOption) {
|
|
2730
|
+
const executor = withOption.toLowerCase();
|
|
2731
|
+
if (executor !== "claude" && executor !== "codex") {
|
|
2732
|
+
process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${withOption}'.
|
|
2733
|
+
`);
|
|
2734
|
+
process.exit(1);
|
|
2831
2735
|
}
|
|
2736
|
+
return executor;
|
|
2832
2737
|
}
|
|
2833
|
-
function
|
|
2834
|
-
const
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2738
|
+
function invokeSelectedExecutor(prompt, executor, options) {
|
|
2739
|
+
const effort = resolveEffortOption(options.effort);
|
|
2740
|
+
if (executor === "codex") {
|
|
2741
|
+
invokeCodexCode(prompt, { yolo: true, model: options.model, effort });
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
return invokeClaudeCode(prompt, {
|
|
2745
|
+
yolo: true,
|
|
2746
|
+
verbose: !options.silent,
|
|
2747
|
+
model: options.model,
|
|
2748
|
+
effort
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
var executeSonarCmd = addAiOptions(
|
|
2752
|
+
new Command5("sonar").description(
|
|
2753
|
+
"Check the current branch for a SonarCloud analysis and invoke the AI with the Sonar prompt and analysis URL"
|
|
2754
|
+
)
|
|
2755
|
+
).action(async (options) => {
|
|
2756
|
+
const executor = resolveExecutor2(options.with);
|
|
2757
|
+
let branch;
|
|
2758
|
+
try {
|
|
2759
|
+
branch = getCurrentBranch();
|
|
2760
|
+
} catch (err) {
|
|
2761
|
+
process.stderr.write(`Error: ${err.message}
|
|
2762
|
+
`);
|
|
2763
|
+
process.exit(1);
|
|
2764
|
+
}
|
|
2765
|
+
let pr;
|
|
2766
|
+
try {
|
|
2767
|
+
pr = await getPrInfo2(branch);
|
|
2768
|
+
} catch (err) {
|
|
2769
|
+
process.stderr.write(`Error: ${err.message}
|
|
2770
|
+
`);
|
|
2771
|
+
process.exit(1);
|
|
2772
|
+
}
|
|
2773
|
+
if (pr === null) {
|
|
2774
|
+
process.stderr.write(`Error: No pull request found for branch: ${branch}
|
|
2775
|
+
`);
|
|
2776
|
+
process.exit(1);
|
|
2777
|
+
}
|
|
2778
|
+
if (!pr.sonarcloudUrl) {
|
|
2779
|
+
process.stderr.write(
|
|
2780
|
+
`Error: No SonarCloud analysis found for PR #${pr.number}. Ensure a SonarCloud check is configured on this repository.
|
|
2781
|
+
`
|
|
2782
|
+
);
|
|
2783
|
+
process.exit(1);
|
|
2844
2784
|
}
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2785
|
+
const config = readConfig();
|
|
2786
|
+
const sonarPromptText = config.prompts?.sonar ?? DEFAULT_SONAR_PROMPT;
|
|
2787
|
+
const fullPrompt = withPush(
|
|
2788
|
+
`${sonarPromptText}
|
|
2789
|
+
|
|
2790
|
+
SonarCloud analysis URL: ${pr.sonarcloudUrl}
|
|
2791
|
+
|
|
2792
|
+
Current PR context from automata git get-pr-info --json:
|
|
2793
|
+
${formatPrInfoContext(pr)}`,
|
|
2794
|
+
options.push
|
|
2853
2795
|
);
|
|
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));
|
|
2796
|
+
await invokeSelectedExecutor(fullPrompt, executor, options);
|
|
2797
|
+
});
|
|
2798
|
+
function formatComments(comments) {
|
|
2799
|
+
return comments.map((c) => {
|
|
2800
|
+
const loc = c.line === null ? `${c.path}:(file)` : `${c.path}:${String(c.line)}`;
|
|
2801
|
+
return `[${c.author}] on ${loc}
|
|
2802
|
+
${c.body}`;
|
|
2803
|
+
}).join("\n\n");
|
|
2871
2804
|
}
|
|
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}.`
|
|
2805
|
+
var executeFixCommentsCmd = addAiOptions(
|
|
2806
|
+
new Command5("fix-comments").description(
|
|
2807
|
+
"Fetch open review comments on the current PR and invoke the AI with the Fix-Comments prompt"
|
|
2808
|
+
)
|
|
2809
|
+
).action(async (options) => {
|
|
2810
|
+
const executor = resolveExecutor2(options.with);
|
|
2811
|
+
const result = resolveCurrentBranchComments();
|
|
2812
|
+
if (!result.ok) {
|
|
2813
|
+
if (result.kind === "error") {
|
|
2814
|
+
process.stderr.write(`Error: ${result.message}
|
|
2815
|
+
`);
|
|
2816
|
+
process.exit(1);
|
|
2817
|
+
}
|
|
2818
|
+
if (result.kind === "unsupported") {
|
|
2819
|
+
process.stderr.write(
|
|
2820
|
+
`Error: fix-comments is not supported for Azure DevOps. See docs/azdo-gap.md for details.
|
|
2821
|
+
`
|
|
2892
2822
|
);
|
|
2823
|
+
process.exit(1);
|
|
2893
2824
|
}
|
|
2825
|
+
process.stderr.write(`Error: No pull request found for branch: ${result.branch}
|
|
2826
|
+
`);
|
|
2827
|
+
process.exit(1);
|
|
2894
2828
|
}
|
|
2895
|
-
const {
|
|
2896
|
-
if (
|
|
2897
|
-
|
|
2829
|
+
const { comments } = result;
|
|
2830
|
+
if (comments.length === 0) {
|
|
2831
|
+
process.stderr.write(`Error: No open review comments found on the pull request.
|
|
2832
|
+
`);
|
|
2833
|
+
process.exit(1);
|
|
2898
2834
|
}
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
const
|
|
2903
|
-
const
|
|
2904
|
-
|
|
2905
|
-
|
|
2835
|
+
process.stdout.write(`Found ${String(comments.length)} open review comment${comments.length === 1 ? "" : "s"} on PR. Invoking AI\u2026
|
|
2836
|
+
`);
|
|
2837
|
+
const config = readConfig();
|
|
2838
|
+
const promptText = config.prompts?.fixComments ?? DEFAULT_FIX_COMMENTS_PROMPT;
|
|
2839
|
+
const fullPrompt = withPush(
|
|
2840
|
+
`${promptText}
|
|
2841
|
+
|
|
2842
|
+
Open review comments:
|
|
2843
|
+
|
|
2844
|
+
${formatComments(comments)}`,
|
|
2845
|
+
options.push
|
|
2846
|
+
);
|
|
2847
|
+
await invokeSelectedExecutor(fullPrompt, executor, options);
|
|
2848
|
+
});
|
|
2849
|
+
var executeCheckIssueCmd = addAiOptions(
|
|
2850
|
+
new Command5("check-issue").description(
|
|
2851
|
+
"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"
|
|
2852
|
+
).argument("<issue-number>", "GitHub issue number to check")
|
|
2853
|
+
).option("--force", "Skip the new-message check and invoke the AI directly").action(async (issueNumberArg, options) => {
|
|
2854
|
+
const executor = resolveExecutor2(options.with);
|
|
2855
|
+
const issueNumber = Number.parseInt(issueNumberArg, 10);
|
|
2856
|
+
if (Number.isNaN(issueNumber) || issueNumber <= 0) {
|
|
2857
|
+
process.stderr.write(`Error: <issue-number> must be a positive integer (got '${issueNumberArg}').
|
|
2858
|
+
`);
|
|
2859
|
+
process.exit(1);
|
|
2906
2860
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2861
|
+
const config = readConfig();
|
|
2862
|
+
if (config.remoteType === "azdo") {
|
|
2863
|
+
process.stderr.write(
|
|
2864
|
+
"Error: check-issue is not supported for Azure DevOps. See docs/azdo-gap.md for details.\n"
|
|
2865
|
+
);
|
|
2866
|
+
process.exit(1);
|
|
2867
|
+
}
|
|
2868
|
+
const allowedUsers = (config.allowedUsers ?? []).filter((user) => user.trim().length > 0);
|
|
2869
|
+
if (allowedUsers.length === 0) {
|
|
2870
|
+
process.stderr.write(
|
|
2871
|
+
"Error: No allowed users configured. Run `automata config` or `automata config set allowed-users <user1,user2>` to set them.\n"
|
|
2872
|
+
);
|
|
2873
|
+
process.exit(1);
|
|
2874
|
+
}
|
|
2875
|
+
const agentUser = (config.agentUser ?? "").trim();
|
|
2876
|
+
if (agentUser.length === 0) {
|
|
2877
|
+
process.stderr.write(
|
|
2878
|
+
"Error: No agent user configured. Run `automata config` or `automata config set agent-user <login>` to set it.\n"
|
|
2879
|
+
);
|
|
2880
|
+
process.exit(1);
|
|
2881
|
+
}
|
|
2882
|
+
let conversation;
|
|
2883
|
+
try {
|
|
2884
|
+
conversation = getIssueConversation(issueNumber);
|
|
2885
|
+
} catch (err) {
|
|
2886
|
+
process.stderr.write(`Error: ${err.message}
|
|
2887
|
+
`);
|
|
2888
|
+
process.exit(1);
|
|
2889
|
+
}
|
|
2890
|
+
const analysis = analyzeConversation(conversation, allowedUsers, agentUser);
|
|
2891
|
+
if (!analysis.hasNewMessage && !options.force) {
|
|
2892
|
+
const since = analysis.lastAgentAt === null ? "" : ` (last agent message: ${analysis.lastAgentAt})`;
|
|
2893
|
+
process.stdout.write(
|
|
2894
|
+
`No new messages from allowed users on issue #${String(issueNumber)}${since}. Use --force to invoke the AI anyway.
|
|
2895
|
+
`
|
|
2896
|
+
);
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
if (analysis.hasNewMessage) {
|
|
2900
|
+
process.stdout.write(
|
|
2901
|
+
`Found ${String(analysis.newMessageCount)} new message${pluralSuffix(analysis.newMessageCount)} on issue #${String(issueNumber)}. Invoking AI\u2026
|
|
2902
|
+
`
|
|
2903
|
+
);
|
|
2904
|
+
} else {
|
|
2905
|
+
process.stdout.write(`No new messages on issue #${String(issueNumber)} \u2014 forced run. Invoking AI\u2026
|
|
2906
|
+
`);
|
|
2907
|
+
}
|
|
2908
|
+
const promptText = config.prompts?.checkIssue ?? DEFAULT_CHECK_ISSUE_PROMPT;
|
|
2909
|
+
const fullPrompt = withPush(
|
|
2910
|
+
`${promptText}
|
|
2911
|
+
|
|
2912
|
+
Issue #${String(issueNumber)}: ${conversation.title}
|
|
2913
|
+
URL: ${conversation.url}
|
|
2914
|
+
|
|
2915
|
+
Conversation (only messages from allowed users and the agent, oldest first):
|
|
2916
|
+
|
|
2917
|
+
${formatConversation(analysis.messages)}`,
|
|
2918
|
+
options.push
|
|
2919
|
+
);
|
|
2920
|
+
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.";
|
|
2921
|
+
try {
|
|
2922
|
+
postComment(issueNumber, marker);
|
|
2923
|
+
} catch (err) {
|
|
2924
|
+
process.stderr.write(
|
|
2925
|
+
`Error: could not post the execution marker comment on issue #${String(issueNumber)}: ${err.message}
|
|
2926
|
+
`
|
|
2927
|
+
);
|
|
2928
|
+
process.exit(1);
|
|
2929
|
+
}
|
|
2930
|
+
await invokeSelectedExecutor(fullPrompt, executor, options);
|
|
2931
|
+
});
|
|
2932
|
+
var executePromptCommand = new Command5("execute-prompt").description("Execute a configured custom prompt using an AI assistant").addCommand(executeSonarCmd).addCommand(executeFixCommentsCmd).addCommand(executeCheckIssueCmd);
|
|
2933
|
+
|
|
2934
|
+
// src/commands/doWork.ts
|
|
2935
|
+
import { Command as Command6 } from "commander";
|
|
2909
2936
|
|
|
2910
2937
|
// src/github/workDetection.ts
|
|
2911
2938
|
var NO_ISSUE_MESSAGES = {
|
|
@@ -2915,10 +2942,6 @@ var NO_ISSUE_MESSAGES = {
|
|
|
2915
2942
|
hasNewMessage: false,
|
|
2916
2943
|
lastAgentAt: null
|
|
2917
2944
|
};
|
|
2918
|
-
function isAssignedToAgent(assignees, agentUser) {
|
|
2919
|
-
const agent = agentUser.toLowerCase();
|
|
2920
|
-
return assignees.some((name) => name.toLowerCase() === agent);
|
|
2921
|
-
}
|
|
2922
2945
|
function findActionableThreads(threads, p, prLastAgentAt) {
|
|
2923
2946
|
const actionable = [];
|
|
2924
2947
|
for (const thread of threads) {
|
|
@@ -2983,7 +3006,7 @@ function decideWork(state, p, policy) {
|
|
|
2983
3006
|
return { kind: "skip", issue, pr: null, reason: "issue-closed", detail: "the issue is closed" };
|
|
2984
3007
|
}
|
|
2985
3008
|
const issueAnalysis = analyzeSurface(issueSurface.messages, p);
|
|
2986
|
-
const needsAssignment =
|
|
3009
|
+
const needsAssignment = issueSurface.assignees.length === 0;
|
|
2987
3010
|
const openPrs = state.linkedPrs.filter((pr) => pr.state === "OPEN");
|
|
2988
3011
|
const hasOpenPr = openPrs.length > 0 && prSurface !== null && prSurface.pr.state === "OPEN";
|
|
2989
3012
|
if (!hasOpenPr) {
|
|
@@ -3004,6 +3027,7 @@ function decideWork(state, p, policy) {
|
|
|
3004
3027
|
pr: null,
|
|
3005
3028
|
branch: baseBranch,
|
|
3006
3029
|
needsAssignment,
|
|
3030
|
+
prNeedsAssignment: false,
|
|
3007
3031
|
issueAnalysis,
|
|
3008
3032
|
prAnalysis: null,
|
|
3009
3033
|
actionableThreads: [],
|
|
@@ -3044,6 +3068,7 @@ function decideWork(state, p, policy) {
|
|
|
3044
3068
|
pr: surface.pr,
|
|
3045
3069
|
branch: surface.pr.headRefName,
|
|
3046
3070
|
needsAssignment,
|
|
3071
|
+
prNeedsAssignment: surface.assignees.length === 0,
|
|
3047
3072
|
issueAnalysis,
|
|
3048
3073
|
prAnalysis,
|
|
3049
3074
|
actionableThreads,
|
|
@@ -3097,6 +3122,13 @@ function decideOrphanPrWork(state, p, policy) {
|
|
|
3097
3122
|
// agent here would change what the discovery filter matches next tick.
|
|
3098
3123
|
// The `working…` marker on the pull request is the claim.
|
|
3099
3124
|
needsAssignment: false,
|
|
3125
|
+
// For the same reason, and more directly: the orphan pass discovers by
|
|
3126
|
+
// the *pull request's own* assignees, so claiming an unassigned orphan
|
|
3127
|
+
// would make it match the filter on the next tick — the agent would
|
|
3128
|
+
// permanently own a pull request the operator never opted in. A build
|
|
3129
|
+
// turn is safe because it reaches its pull request through an issue that
|
|
3130
|
+
// matched the filter, not through the pull request's assignees.
|
|
3131
|
+
prNeedsAssignment: false,
|
|
3100
3132
|
issueAnalysis: NO_ISSUE_MESSAGES,
|
|
3101
3133
|
prAnalysis,
|
|
3102
3134
|
actionableThreads,
|
|
@@ -3129,6 +3161,30 @@ function selectLinkedPr(linkedPrs) {
|
|
|
3129
3161
|
const open = linkedPrs.filter((pr) => pr.state === "OPEN");
|
|
3130
3162
|
return open.length === 0 ? null : newestPr(open);
|
|
3131
3163
|
}
|
|
3164
|
+
function claimStates(item) {
|
|
3165
|
+
const states = [];
|
|
3166
|
+
if (item.issue !== null) {
|
|
3167
|
+
states.push({
|
|
3168
|
+
surface: "issue",
|
|
3169
|
+
number: item.issue.number,
|
|
3170
|
+
state: item.needsAssignment ? "would-claim" : "already-assigned"
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
if (item.pr !== null) {
|
|
3174
|
+
states.push({
|
|
3175
|
+
surface: "pull request",
|
|
3176
|
+
number: item.pr.number,
|
|
3177
|
+
// An exemption is a rule, not a state, so it outranks the assignee list:
|
|
3178
|
+
// an orphan pull request with nobody on it is still not claimed.
|
|
3179
|
+
state: prClaimState(item)
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
return states;
|
|
3183
|
+
}
|
|
3184
|
+
function prClaimState(item) {
|
|
3185
|
+
if (item.turn === "pr-orphan") return "rule-exempt";
|
|
3186
|
+
return item.prNeedsAssignment ? "would-claim" : "already-assigned";
|
|
3187
|
+
}
|
|
3132
3188
|
|
|
3133
3189
|
// src/github/workPrompt.ts
|
|
3134
3190
|
function promptBaseBranch(item, configuredBaseBranch) {
|
|
@@ -3711,6 +3767,12 @@ function rescueUncommittedChanges(options, now) {
|
|
|
3711
3767
|
);
|
|
3712
3768
|
return { kind: "would-rescue", branch: target.branch, createdBranch: target.createdBranch };
|
|
3713
3769
|
}
|
|
3770
|
+
const staged = stageAllExcept([RUN_LOCK_RELATIVE_PATH]);
|
|
3771
|
+
if (!staged.ok) {
|
|
3772
|
+
options.log(` rescue FAILED to stage the changes: ${staged.stderr}
|
|
3773
|
+
`);
|
|
3774
|
+
return { kind: "failed", step: "stage", detail: staged.stderr };
|
|
3775
|
+
}
|
|
3714
3776
|
if (target.createdBranch) {
|
|
3715
3777
|
const created = createBranchAtHead(target.branch);
|
|
3716
3778
|
if (!created.ok) {
|
|
@@ -3719,17 +3781,18 @@ function rescueUncommittedChanges(options, now) {
|
|
|
3719
3781
|
return { kind: "failed", step: "branch", detail: created.stderr };
|
|
3720
3782
|
}
|
|
3721
3783
|
}
|
|
3722
|
-
const
|
|
3723
|
-
if (!staged.ok) {
|
|
3724
|
-
options.log(` rescue FAILED to stage the changes: ${staged.stderr}
|
|
3725
|
-
`);
|
|
3726
|
-
return { kind: "failed", step: "stage", detail: staged.stderr };
|
|
3727
|
-
}
|
|
3784
|
+
const left = target.createdBranch ? { branch: target.branch, createdBranch: target.branch } : { branch: target.branch };
|
|
3728
3785
|
const committed = commitStaged(`chore(automata): rescue uncommitted work from ${target.source}`);
|
|
3729
3786
|
if (!committed.ok) {
|
|
3730
|
-
options.log(` rescue FAILED to commit: ${committed.stderr}
|
|
3787
|
+
options.log(` rescue FAILED to commit on ${target.branch}: ${committed.stderr}
|
|
3731
3788
|
`);
|
|
3732
|
-
|
|
3789
|
+
if (target.createdBranch) {
|
|
3790
|
+
options.log(
|
|
3791
|
+
` rescue ${target.branch} was created by this rescue and carries none of the work; it is left in the checkout
|
|
3792
|
+
`
|
|
3793
|
+
);
|
|
3794
|
+
}
|
|
3795
|
+
return { kind: "failed", step: "commit", detail: committed.stderr, ...left };
|
|
3733
3796
|
}
|
|
3734
3797
|
const pushed = pushSetUpstream(target.branch);
|
|
3735
3798
|
if (!pushed.ok) {
|
|
@@ -3738,7 +3801,7 @@ function rescueUncommittedChanges(options, now) {
|
|
|
3738
3801
|
rescue the work is committed locally on ${target.branch}; push it by hand before it is lost
|
|
3739
3802
|
`
|
|
3740
3803
|
);
|
|
3741
|
-
return { kind: "failed", step: "push", detail: pushed.stderr };
|
|
3804
|
+
return { kind: "failed", step: "push", detail: pushed.stderr, ...left };
|
|
3742
3805
|
}
|
|
3743
3806
|
let existing;
|
|
3744
3807
|
try {
|
|
@@ -3748,7 +3811,7 @@ function rescueUncommittedChanges(options, now) {
|
|
|
3748
3811
|
` rescue committed and pushed ${target.branch}, but could not check for an open PR: ${err.message}
|
|
3749
3812
|
`
|
|
3750
3813
|
);
|
|
3751
|
-
return { kind: "failed", step: "pr", detail: err.message };
|
|
3814
|
+
return { kind: "failed", step: "pr", detail: err.message, ...left };
|
|
3752
3815
|
}
|
|
3753
3816
|
if (existing !== null) {
|
|
3754
3817
|
options.log(
|
|
@@ -3789,7 +3852,7 @@ function rescueUncommittedChanges(options, now) {
|
|
|
3789
3852
|
` rescue committed and pushed ${target.branch}, but could not open a draft PR: ${err.message}
|
|
3790
3853
|
`
|
|
3791
3854
|
);
|
|
3792
|
-
return { kind: "failed", step: "pr", detail: err.message };
|
|
3855
|
+
return { kind: "failed", step: "pr", detail: err.message, ...left };
|
|
3793
3856
|
}
|
|
3794
3857
|
}
|
|
3795
3858
|
function prepareBase(options) {
|
|
@@ -3814,12 +3877,13 @@ function prepareBase(options) {
|
|
|
3814
3877
|
`);
|
|
3815
3878
|
return { ok: true };
|
|
3816
3879
|
}
|
|
3817
|
-
function collectCandidates(options) {
|
|
3880
|
+
function collectCandidates(options, rescueBranch) {
|
|
3818
3881
|
const remote = listRemoteBranches();
|
|
3819
3882
|
if (remote === null) return null;
|
|
3820
3883
|
const remoteNames = new Set(remote);
|
|
3821
3884
|
const current = getCurrentBranch();
|
|
3822
3885
|
const untouchable = /* @__PURE__ */ new Set([options.baseBranch, current, ...options.protectedBranches]);
|
|
3886
|
+
if (rescueBranch !== null) untouchable.add(rescueBranch);
|
|
3823
3887
|
return listLocalBranches().filter(
|
|
3824
3888
|
(branch) => !untouchable.has(branch) && !remoteNames.has(branch)
|
|
3825
3889
|
);
|
|
@@ -3915,8 +3979,8 @@ function pruneCandidate(branch, options) {
|
|
|
3915
3979
|
return { kind: "rescued", branch, pr: null, prUrl: null };
|
|
3916
3980
|
}
|
|
3917
3981
|
}
|
|
3918
|
-
function prune(options) {
|
|
3919
|
-
const candidates = collectCandidates(options);
|
|
3982
|
+
function prune(options, rescueBranch) {
|
|
3983
|
+
const candidates = collectCandidates(options, rescueBranch);
|
|
3920
3984
|
if (candidates === null) {
|
|
3921
3985
|
options.log(
|
|
3922
3986
|
" prune skipped: could not list origin's branches, so no branch can be shown to have no remote\n"
|
|
@@ -3936,12 +4000,46 @@ function runRepoHygiene(options, now = /* @__PURE__ */ new Date()) {
|
|
|
3936
4000
|
options.log(options.dryRun ? "\nPre-flight (dry run):\n" : "\nPre-flight:\n");
|
|
3937
4001
|
const rescue = rescueUncommittedChanges(options, now);
|
|
3938
4002
|
const base = prepareBase(options);
|
|
3939
|
-
const { outcomes, remoteUnreadable } = prune(options);
|
|
4003
|
+
const { outcomes, remoteUnreadable } = prune(options, rescueCreatedBranch(rescue));
|
|
3940
4004
|
const degraded = rescue.kind === "failed" || !base.ok || remoteUnreadable || outcomes.some(
|
|
3941
4005
|
(outcome) => outcome.kind === "kept" && outcome.reason !== "open-pr" || outcome.kind === "rescued" && outcome.pr === null
|
|
3942
4006
|
);
|
|
3943
4007
|
return { rescue, base, prunes: outcomes, degraded };
|
|
3944
4008
|
}
|
|
4009
|
+
function rescueCreatedBranch(rescue) {
|
|
4010
|
+
switch (rescue.kind) {
|
|
4011
|
+
case "rescued":
|
|
4012
|
+
case "would-rescue":
|
|
4013
|
+
return rescue.createdBranch ? rescue.branch : null;
|
|
4014
|
+
case "failed":
|
|
4015
|
+
return rescue.createdBranch ?? null;
|
|
4016
|
+
default:
|
|
4017
|
+
return null;
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
function describePreflightFailures(report) {
|
|
4021
|
+
const causes = [];
|
|
4022
|
+
if (report.rescue.kind === "failed") {
|
|
4023
|
+
causes.push(
|
|
4024
|
+
`the rescue failed at the ${report.rescue.step} step (${describeRescueRemains(report.rescue)}): ${report.rescue.detail}`
|
|
4025
|
+
);
|
|
4026
|
+
}
|
|
4027
|
+
if (!report.base.ok) {
|
|
4028
|
+
causes.push(`the base branch ${report.base.step} failed: ${report.base.detail}`);
|
|
4029
|
+
}
|
|
4030
|
+
return causes;
|
|
4031
|
+
}
|
|
4032
|
+
function describeRescueRemains(rescue) {
|
|
4033
|
+
if (rescue.branch === void 0) return "the tree is still dirty";
|
|
4034
|
+
switch (rescue.step) {
|
|
4035
|
+
case "push":
|
|
4036
|
+
return `the work is committed on ${rescue.branch} but not pushed`;
|
|
4037
|
+
case "pr":
|
|
4038
|
+
return `the work is committed and pushed on ${rescue.branch}, without a draft pull request`;
|
|
4039
|
+
default:
|
|
4040
|
+
return `the tree is still dirty and ${rescue.branch} carries none of it`;
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
3945
4043
|
|
|
3946
4044
|
// src/run/operationLog.ts
|
|
3947
4045
|
import {
|
|
@@ -4271,10 +4369,32 @@ function planRun(item, settings, execution) {
|
|
|
4271
4369
|
});
|
|
4272
4370
|
return { prompt, bin, args, command: [bin, ...args].map(shellQuote).join(" ") };
|
|
4273
4371
|
}
|
|
4372
|
+
function describeAssignment(item, agentUser) {
|
|
4373
|
+
return claimStates(item).map((claim) => {
|
|
4374
|
+
const label = claim.surface === "issue" ? "issue" : `pull request #${String(claim.number)}`;
|
|
4375
|
+
switch (claim.state) {
|
|
4376
|
+
case "would-claim":
|
|
4377
|
+
return `would assign ${label} to ${agentUser}`;
|
|
4378
|
+
case "already-assigned":
|
|
4379
|
+
return `${label} already assigned`;
|
|
4380
|
+
case "rule-exempt":
|
|
4381
|
+
return `${label} not claimed (orphan pass)`;
|
|
4382
|
+
}
|
|
4383
|
+
}).join(" \xB7 ");
|
|
4384
|
+
}
|
|
4385
|
+
function describePlanClaim(item) {
|
|
4386
|
+
const claims = claimStates(item);
|
|
4387
|
+
const would = claims.filter((claim) => claim.state === "would-claim").map((claim) => `the ${claim.surface}`);
|
|
4388
|
+
const exempt = claims.filter((claim) => claim.state === "rule-exempt").map((claim) => claim.surface);
|
|
4389
|
+
const parts = [];
|
|
4390
|
+
if (would.length > 0) parts.push(`will assign ${would.join(" and ")} to the agent`);
|
|
4391
|
+
if (exempt.length > 0) parts.push(`${exempt.join(" and ")} not claimed (orphan pass)`);
|
|
4392
|
+
return parts.length === 0 ? "" : `, ${parts.join(", ")}`;
|
|
4393
|
+
}
|
|
4274
4394
|
function describePlannedRun(item, settings, run5, execution) {
|
|
4275
4395
|
const rule = "\u2500".repeat(72);
|
|
4276
4396
|
const branchAction = item.turn === "issue-discuss" ? " and pull" : " and fast-forward";
|
|
4277
|
-
const assignment = item
|
|
4397
|
+
const assignment = describeAssignment(item, settings.participants.agentUser);
|
|
4278
4398
|
const markerTarget = markerSurfaceLabel(item);
|
|
4279
4399
|
const lines = [
|
|
4280
4400
|
rule,
|
|
@@ -4469,8 +4589,7 @@ function describePlan(decisions) {
|
|
|
4469
4589
|
return ` ${subjectLabel(decision.issue?.number ?? null, decision.pr?.number ?? null)} nothing to do \u2014 ${decision.detail}`;
|
|
4470
4590
|
}
|
|
4471
4591
|
const item = decision.item;
|
|
4472
|
-
|
|
4473
|
-
return ` ${itemLabel(item)} ${item.turn} on ${item.branch} \u2014 ${item.reason}${claim}`;
|
|
4592
|
+
return ` ${itemLabel(item)} ${item.turn} on ${item.branch} \u2014 ${item.reason}${describePlanClaim(item)}`;
|
|
4474
4593
|
});
|
|
4475
4594
|
return lines.length === 0 ? " (nothing matched the discovery filter)\n" : lines.join("\n") + "\n";
|
|
4476
4595
|
}
|
|
@@ -4485,6 +4604,16 @@ function claimIssue(item, settings) {
|
|
|
4485
4604
|
`);
|
|
4486
4605
|
}
|
|
4487
4606
|
}
|
|
4607
|
+
function claimPr(prNumber, agentUser) {
|
|
4608
|
+
try {
|
|
4609
|
+
assignPrToAgent(prNumber, agentUser);
|
|
4610
|
+
progress(` assigned pull request #${String(prNumber)} to ${agentUser}.
|
|
4611
|
+
`);
|
|
4612
|
+
} catch (err) {
|
|
4613
|
+
progress(` warning: could not assign pull request #${String(prNumber)}: ${err.message}
|
|
4614
|
+
`);
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4488
4617
|
function notePickupOnIssue(item, marker) {
|
|
4489
4618
|
if (item.turn !== "pr-work" || item.pr === null || item.issue === null) return null;
|
|
4490
4619
|
if (!item.issueAnalysis.hasNewMessage) return null;
|
|
@@ -4657,7 +4786,7 @@ async function invokeExecutor(prompt, execution, silent) {
|
|
|
4657
4786
|
}
|
|
4658
4787
|
await runClaude(prompt, { model: execution.model, effort: execution.effort, printSteps: !silent });
|
|
4659
4788
|
}
|
|
4660
|
-
function repairIssueLink(item, baseBranch) {
|
|
4789
|
+
function repairIssueLink(item, baseBranch, agentUser) {
|
|
4661
4790
|
const issue = item.issue;
|
|
4662
4791
|
if (issue === null) return false;
|
|
4663
4792
|
try {
|
|
@@ -4673,6 +4802,9 @@ function repairIssueLink(item, baseBranch) {
|
|
|
4673
4802
|
`);
|
|
4674
4803
|
return false;
|
|
4675
4804
|
}
|
|
4805
|
+
if (pr.assignees.length === 0) {
|
|
4806
|
+
claimPr(pr.number, agentUser);
|
|
4807
|
+
}
|
|
4676
4808
|
const closesRef = new RegExp(String.raw`\bcloses\s+#` + String(issue.number) + String.raw`\b`, "i");
|
|
4677
4809
|
if (closesRef.test(pr.body)) {
|
|
4678
4810
|
progress(` pull request #${String(pr.number)} already closes issue #${String(issue.number)}.
|
|
@@ -4701,7 +4833,10 @@ function refuseBeforeRun(base, marker, detail, markerText) {
|
|
|
4701
4833
|
}
|
|
4702
4834
|
return { ...base, outcome: "failed", detail };
|
|
4703
4835
|
}
|
|
4704
|
-
|
|
4836
|
+
function preflightSuffix(causes) {
|
|
4837
|
+
return causes.length === 0 ? "" : ` [pre-flight: ${causes.join("; ")}]`;
|
|
4838
|
+
}
|
|
4839
|
+
async function processItem(planned, settings, silent, preflightCauses) {
|
|
4705
4840
|
progress(`
|
|
4706
4841
|
${itemLabel(planned)} ${planned.turn}: ${planned.reason}
|
|
4707
4842
|
`);
|
|
@@ -4731,11 +4866,19 @@ ${itemLabel(planned)} ${planned.turn}: ${planned.reason}
|
|
|
4731
4866
|
};
|
|
4732
4867
|
const prepared = item.turn === "issue-discuss" ? prepareBaseBranch(item.branch) : preparePrBranch(item.branch);
|
|
4733
4868
|
if (!prepared.ok) {
|
|
4734
|
-
|
|
4869
|
+
const why = preflightSuffix(preflightCauses);
|
|
4870
|
+
progress(` skipped: ${prepared.reason} \u2014 ${prepared.detail}${why}
|
|
4735
4871
|
`);
|
|
4736
|
-
return {
|
|
4872
|
+
return {
|
|
4873
|
+
...base,
|
|
4874
|
+
outcome: "skipped",
|
|
4875
|
+
detail: `${prepared.reason}: ${prepared.detail}${why}`
|
|
4876
|
+
};
|
|
4737
4877
|
}
|
|
4738
4878
|
claimIssue(item, settings);
|
|
4879
|
+
if (item.turn === "pr-work" && item.pr !== null && item.prNeedsAssignment) {
|
|
4880
|
+
claimPr(item.pr.number, settings.participants.agentUser);
|
|
4881
|
+
}
|
|
4739
4882
|
let marker;
|
|
4740
4883
|
const markerTarget = markerSurfaceTarget(item);
|
|
4741
4884
|
try {
|
|
@@ -4819,7 +4962,7 @@ function adjustOutcome(reconciled, item, settings, buriedByNote) {
|
|
|
4819
4962
|
};
|
|
4820
4963
|
}
|
|
4821
4964
|
if (item.turn !== "issue-discuss") return outcome;
|
|
4822
|
-
const linked = repairIssueLink(item, settings.baseBranch);
|
|
4965
|
+
const linked = repairIssueLink(item, settings.baseBranch, settings.participants.agentUser);
|
|
4823
4966
|
if (linked && outcome.reason === "no-answer") {
|
|
4824
4967
|
progress(" a pull request was opened, so the turn is counted as answered.\n");
|
|
4825
4968
|
return {
|
|
@@ -5021,6 +5164,7 @@ ${describePlan(decisions)}`;
|
|
|
5021
5164
|
}
|
|
5022
5165
|
const reports = [];
|
|
5023
5166
|
const deferred = [];
|
|
5167
|
+
const preflightCauses = describePreflightFailures(hygiene);
|
|
5024
5168
|
let runsUsed = 0;
|
|
5025
5169
|
for (const item of items) {
|
|
5026
5170
|
if (settings.maxRuns > 0 && runsUsed >= settings.maxRuns) {
|
|
@@ -5029,7 +5173,7 @@ ${describePlan(decisions)}`;
|
|
|
5029
5173
|
}
|
|
5030
5174
|
let report;
|
|
5031
5175
|
try {
|
|
5032
|
-
report = await processItem(item, settings, options.silent === true);
|
|
5176
|
+
report = await processItem(item, settings, options.silent === true, preflightCauses);
|
|
5033
5177
|
} catch (err) {
|
|
5034
5178
|
progress(` failed: ${err.message}
|
|
5035
5179
|
`);
|
|
@@ -5183,7 +5327,7 @@ function describeRescue(rescue) {
|
|
|
5183
5327
|
case "would-rescue":
|
|
5184
5328
|
return `would rescue onto ${rescue.branch}`;
|
|
5185
5329
|
case "failed":
|
|
5186
|
-
return `${rescue.step} failed \u2014 ${rescue.detail};
|
|
5330
|
+
return `${rescue.step} failed \u2014 ${rescue.detail}; ${describeRescueRemains(rescue)}; nothing was discarded`;
|
|
5187
5331
|
}
|
|
5188
5332
|
}
|
|
5189
5333
|
function describePrune(outcome) {
|
|
@@ -5227,6 +5371,7 @@ function toPlanJson(decision) {
|
|
|
5227
5371
|
turn: item.turn,
|
|
5228
5372
|
branch: item.branch,
|
|
5229
5373
|
needsAssignment: item.needsAssignment,
|
|
5374
|
+
prNeedsAssignment: item.prNeedsAssignment,
|
|
5230
5375
|
reason: item.reason
|
|
5231
5376
|
};
|
|
5232
5377
|
}
|