@autoclawd/autoclawd 1.1.23 → 1.1.25
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/README.md +3 -3
- package/dist/index.js +297 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ autoclawd init
|
|
|
37
37
|
autoclawd watch
|
|
38
38
|
|
|
39
39
|
# Or run a single ticket
|
|
40
|
-
autoclawd run
|
|
40
|
+
autoclawd run TEAM-123
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## Commands
|
|
@@ -145,7 +145,7 @@ If the repo doesn't exist, autoclawd creates it automatically and pushes an init
|
|
|
145
145
|
For dependent tasks where ticket B needs code from ticket A:
|
|
146
146
|
|
|
147
147
|
1. **Ticket A** is processed normally (branches from main)
|
|
148
|
-
2. After A completes, autoclawd adds a `base:autoclawd/
|
|
148
|
+
2. After A completes, autoclawd adds a `base:autoclawd/TEAM-100-...` label to it
|
|
149
149
|
3. **Ticket B** gets that label → it branches from A's PR branch instead of main
|
|
150
150
|
4. When A's PR merges, autoclawd auto-rebases B onto main and retargets the PR
|
|
151
151
|
|
|
@@ -157,7 +157,7 @@ autoclawd rebase owner/repo
|
|
|
157
157
|
### Setting up stacked diffs from the Claude app
|
|
158
158
|
|
|
159
159
|
When using the Claude app with a Linear connector to create tickets, tell Claude:
|
|
160
|
-
- "Task B depends on task A" → Claude adds the `base:autoclawd/
|
|
160
|
+
- "Task B depends on task A" → Claude adds the `base:autoclawd/TEAM-{A}-...` label to B
|
|
161
161
|
|
|
162
162
|
## Validation hooks
|
|
163
163
|
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,10 @@ var SafetyConfigSchema = z.object({
|
|
|
56
56
|
branchPrefix: z.string().default("autoclawd/"),
|
|
57
57
|
maxFileChanges: z.number().min(1).optional()
|
|
58
58
|
});
|
|
59
|
+
var IntegrationConfigSchema = z.object({
|
|
60
|
+
crossBranchValidate: z.boolean().default(true),
|
|
61
|
+
maxCrossBranchIterations: z.number().min(1).max(10).default(3)
|
|
62
|
+
});
|
|
59
63
|
var ConfigSchema = z.object({
|
|
60
64
|
linear: LinearConfigSchema,
|
|
61
65
|
github: GitHubConfigSchema,
|
|
@@ -63,6 +67,7 @@ var ConfigSchema = z.object({
|
|
|
63
67
|
agent: AgentConfigSchema.default({}),
|
|
64
68
|
webhook: WebhookConfigSchema.default({}),
|
|
65
69
|
safety: SafetyConfigSchema.default({}),
|
|
70
|
+
integration: IntegrationConfigSchema.default({}),
|
|
66
71
|
maxConcurrent: z.number().default(1),
|
|
67
72
|
validate: z.array(z.string()).optional()
|
|
68
73
|
// Global default validation commands
|
|
@@ -72,8 +77,9 @@ var RepoLocalConfigSchema = z.object({
|
|
|
72
77
|
base: z.string().default("main"),
|
|
73
78
|
agent: AgentConfigSchema.partial().optional(),
|
|
74
79
|
docker: DockerConfigSchema.partial().optional(),
|
|
75
|
-
validate: z.array(z.string()).optional()
|
|
80
|
+
validate: z.array(z.string()).optional(),
|
|
76
81
|
// Validation commands to run before PR
|
|
82
|
+
integration: IntegrationConfigSchema.partial().optional()
|
|
77
83
|
});
|
|
78
84
|
function resolveEnvVars(obj) {
|
|
79
85
|
if (typeof obj === "string") {
|
|
@@ -135,8 +141,12 @@ function mergeConfigs(host, local) {
|
|
|
135
141
|
...host.docker,
|
|
136
142
|
...local?.docker
|
|
137
143
|
});
|
|
144
|
+
const integration = IntegrationConfigSchema.parse({
|
|
145
|
+
...host.integration,
|
|
146
|
+
...local?.integration
|
|
147
|
+
});
|
|
138
148
|
const validate = local?.validate ?? host.validate;
|
|
139
|
-
return { agent, docker: docker2, prompt: local?.prompt, validate };
|
|
149
|
+
return { agent, docker: docker2, prompt: local?.prompt, validate, integration };
|
|
140
150
|
}
|
|
141
151
|
function parseRepoFromLabels(labels) {
|
|
142
152
|
for (const label of labels) {
|
|
@@ -411,6 +421,31 @@ async function addBranchLabel(client, ticketId, branchName) {
|
|
|
411
421
|
log.debug(`Could not add branch label: ${err instanceof Error ? err.message : err}`);
|
|
412
422
|
}
|
|
413
423
|
}
|
|
424
|
+
async function findTicketsWithLabel(client, teamId, labelName) {
|
|
425
|
+
const team = await client.team(teamId);
|
|
426
|
+
const teamLabels = await team.labels();
|
|
427
|
+
const label = teamLabels.nodes.find((l) => l.name === labelName);
|
|
428
|
+
if (!label) return [];
|
|
429
|
+
const issues = await label.issues({ first: 50 });
|
|
430
|
+
const tickets = [];
|
|
431
|
+
for (const issue of issues.nodes) {
|
|
432
|
+
const labels = await issue.labels();
|
|
433
|
+
const labelNames = labels.nodes.map((l) => l.name);
|
|
434
|
+
const repoUrl = parseRepoFromLabels(labelNames);
|
|
435
|
+
const baseBranch = parseBaseFromLabels(labelNames);
|
|
436
|
+
tickets.push({
|
|
437
|
+
id: issue.id,
|
|
438
|
+
identifier: issue.identifier,
|
|
439
|
+
title: issue.title,
|
|
440
|
+
description: issue.description ?? "",
|
|
441
|
+
labels: labelNames,
|
|
442
|
+
repoUrl,
|
|
443
|
+
baseBranch,
|
|
444
|
+
url: issue.url
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return tickets;
|
|
448
|
+
}
|
|
414
449
|
async function failTicket(client, ticketId, reason) {
|
|
415
450
|
await client.createComment({
|
|
416
451
|
issueId: ticketId,
|
|
@@ -630,6 +665,21 @@ async function enableAutoMerge(octokit, opts) {
|
|
|
630
665
|
}
|
|
631
666
|
}, { label: "GitHub auto-merge", retryIf: isTransientError });
|
|
632
667
|
}
|
|
668
|
+
async function findSiblingPRs(octokit, opts) {
|
|
669
|
+
const { owner, repo } = parseRepoUrl(opts.repoUrl);
|
|
670
|
+
const { data: prs } = await octokit.rest.pulls.list({
|
|
671
|
+
owner,
|
|
672
|
+
repo,
|
|
673
|
+
base: opts.baseBranch,
|
|
674
|
+
state: "open"
|
|
675
|
+
});
|
|
676
|
+
return prs.filter((pr) => pr.head.ref !== opts.excludeHead && pr.head.ref.startsWith(opts.branchPrefix)).map((pr) => ({
|
|
677
|
+
number: pr.number,
|
|
678
|
+
head: pr.head.ref,
|
|
679
|
+
base: pr.base.ref,
|
|
680
|
+
title: pr.title
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
633
683
|
async function retargetPR(octokit, opts) {
|
|
634
684
|
return retry(async () => {
|
|
635
685
|
const { owner, repo } = parseRepoUrl(opts.repoUrl);
|
|
@@ -1516,7 +1566,7 @@ function countRuns() {
|
|
|
1516
1566
|
}
|
|
1517
1567
|
|
|
1518
1568
|
// src/worker.ts
|
|
1519
|
-
function buildPrompt(ticket, repoPrompt) {
|
|
1569
|
+
function buildPrompt(ticket, repoPrompt, siblings) {
|
|
1520
1570
|
const parts = [];
|
|
1521
1571
|
if (repoPrompt) {
|
|
1522
1572
|
parts.push(repoPrompt);
|
|
@@ -1527,6 +1577,17 @@ function buildPrompt(ticket, repoPrompt) {
|
|
|
1527
1577
|
if (ticket.description) {
|
|
1528
1578
|
parts.push(ticket.description);
|
|
1529
1579
|
}
|
|
1580
|
+
if (siblings && siblings.length > 0) {
|
|
1581
|
+
parts.push("");
|
|
1582
|
+
parts.push("## Related Tickets (being worked on in parallel branches)");
|
|
1583
|
+
parts.push("The following tickets target the same codebase and may be worked on concurrently.");
|
|
1584
|
+
parts.push("Ensure your implementation is compatible \u2014 use consistent type definitions, interfaces, and patterns.");
|
|
1585
|
+
parts.push("");
|
|
1586
|
+
for (const sib of siblings) {
|
|
1587
|
+
const desc = sib.description ? sib.description.length > 300 ? sib.description.slice(0, 300) + "..." : sib.description : "(no description)";
|
|
1588
|
+
parts.push(`- **${sib.identifier}: ${sib.title}** \u2014 ${desc}`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1530
1591
|
parts.push("");
|
|
1531
1592
|
parts.push("## Instructions");
|
|
1532
1593
|
parts.push("- Start by reading CLAUDE.md if it exists \u2014 it has project-specific guidance");
|
|
@@ -1665,6 +1726,97 @@ function buildValidationFixPrompt(failures) {
|
|
|
1665
1726
|
parts.push("- When done, include [autoclawd:done] in your output");
|
|
1666
1727
|
return parts.join("\n");
|
|
1667
1728
|
}
|
|
1729
|
+
async function runCrossBranchValidation(container, validate, siblingBranches, ticketId) {
|
|
1730
|
+
const results = [];
|
|
1731
|
+
for (const siblingBranch of siblingBranches) {
|
|
1732
|
+
log.ticket(ticketId, `Cross-branch check: trial merge with ${siblingBranch}`);
|
|
1733
|
+
const fetchResult = await exec(container, ["git", "fetch", "origin", siblingBranch], {
|
|
1734
|
+
user: "autoclawd",
|
|
1735
|
+
env: ["HOME=/home/autoclawd"],
|
|
1736
|
+
timeout: 6e4
|
|
1737
|
+
});
|
|
1738
|
+
if (fetchResult.exitCode !== 0) {
|
|
1739
|
+
log.ticket(ticketId, ` Could not fetch ${siblingBranch}, skipping`);
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
const mergeResult = await exec(container, [
|
|
1743
|
+
"git",
|
|
1744
|
+
"merge",
|
|
1745
|
+
"--no-commit",
|
|
1746
|
+
"--no-ff",
|
|
1747
|
+
`origin/${siblingBranch}`
|
|
1748
|
+
], {
|
|
1749
|
+
user: "autoclawd",
|
|
1750
|
+
env: ["HOME=/home/autoclawd"],
|
|
1751
|
+
timeout: 6e4
|
|
1752
|
+
});
|
|
1753
|
+
if (mergeResult.exitCode !== 0) {
|
|
1754
|
+
const mergeOutput = (mergeResult.stdout + mergeResult.stderr).trim();
|
|
1755
|
+
const diffResult = await exec(container, ["git", "diff", "--diff-filter=U"], {
|
|
1756
|
+
user: "autoclawd",
|
|
1757
|
+
env: ["HOME=/home/autoclawd"],
|
|
1758
|
+
timeout: 1e4
|
|
1759
|
+
});
|
|
1760
|
+
const conflictDetail = diffResult.stdout.trim();
|
|
1761
|
+
const output = [
|
|
1762
|
+
mergeOutput,
|
|
1763
|
+
"",
|
|
1764
|
+
"Conflict details (files with conflict markers):",
|
|
1765
|
+
conflictDetail || "(no diff available)"
|
|
1766
|
+
].join("\n").slice(-3e3);
|
|
1767
|
+
results.push({
|
|
1768
|
+
branch: siblingBranch,
|
|
1769
|
+
failures: [{
|
|
1770
|
+
command: `git merge origin/${siblingBranch}`,
|
|
1771
|
+
exitCode: mergeResult.exitCode,
|
|
1772
|
+
output
|
|
1773
|
+
}]
|
|
1774
|
+
});
|
|
1775
|
+
await exec(container, ["git", "merge", "--abort"], {
|
|
1776
|
+
user: "autoclawd",
|
|
1777
|
+
env: ["HOME=/home/autoclawd"],
|
|
1778
|
+
timeout: 1e4
|
|
1779
|
+
});
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
const failures = await runValidation(container, validate, ticketId);
|
|
1783
|
+
results.push({ branch: siblingBranch, failures });
|
|
1784
|
+
await exec(container, ["git", "merge", "--abort"], {
|
|
1785
|
+
user: "autoclawd",
|
|
1786
|
+
env: ["HOME=/home/autoclawd"],
|
|
1787
|
+
timeout: 1e4
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
return results;
|
|
1791
|
+
}
|
|
1792
|
+
function buildCrossBranchFixPrompt(crossResults) {
|
|
1793
|
+
const parts = [];
|
|
1794
|
+
parts.push("## Cross-Branch Compatibility Issues");
|
|
1795
|
+
parts.push("");
|
|
1796
|
+
parts.push("Your branch was trial-merged with sibling branches that target the same codebase.");
|
|
1797
|
+
parts.push("The following issues were found. You must fix YOUR code to be compatible \u2014 do not assume the sibling is wrong.");
|
|
1798
|
+
parts.push("");
|
|
1799
|
+
for (const { branch, failures } of crossResults) {
|
|
1800
|
+
if (failures.length === 0) continue;
|
|
1801
|
+
parts.push(`### Conflicts with \`${branch}\``);
|
|
1802
|
+
for (const f of failures) {
|
|
1803
|
+
parts.push(`#### \`${f.command}\` (exit code ${f.exitCode})`);
|
|
1804
|
+
parts.push("```");
|
|
1805
|
+
parts.push(f.output);
|
|
1806
|
+
parts.push("```");
|
|
1807
|
+
parts.push("");
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
parts.push("## Instructions");
|
|
1811
|
+
parts.push("- Fix YOUR code to be compatible with the sibling branches");
|
|
1812
|
+
parts.push("- Focus on: consistent type definitions, matching interfaces, compatible exports");
|
|
1813
|
+
parts.push("- If there are merge conflicts in a file (e.g. src/index.ts), you MUST include BOTH your changes AND the sibling's changes in that file \u2014 fetch the sibling branch, look at their version of the file, and make yours include both sets of changes");
|
|
1814
|
+
parts.push("- To see what the sibling added: run `git diff main..origin/<sibling-branch> -- <file>`");
|
|
1815
|
+
parts.push("- Run validation commands to verify your fix");
|
|
1816
|
+
parts.push('- Commit your fixes with: git add -A && git commit -m "fix: resolve cross-branch compatibility"');
|
|
1817
|
+
parts.push("- When done, include [autoclawd:done] in your output");
|
|
1818
|
+
return parts.join("\n");
|
|
1819
|
+
}
|
|
1668
1820
|
async function executeTicket(opts) {
|
|
1669
1821
|
const { ticket, config, linearClient, octokit, force } = opts;
|
|
1670
1822
|
const startedAt = /* @__PURE__ */ new Date();
|
|
@@ -1792,7 +1944,7 @@ async function executeTicket(opts) {
|
|
|
1792
1944
|
log.ticket(ticket.identifier, "Cloned repo");
|
|
1793
1945
|
const repoLocal = loadRepoLocalConfig(workDir);
|
|
1794
1946
|
const actualBase = ticket.baseBranch ?? repoLocal?.base ?? detectedBase;
|
|
1795
|
-
const { agent, docker: docker2, prompt, validate } = mergeConfigs(config, repoLocal);
|
|
1947
|
+
const { agent, docker: docker2, prompt, validate, integration } = mergeConfigs(config, repoLocal);
|
|
1796
1948
|
if (repoLocal) {
|
|
1797
1949
|
log.ticket(ticket.identifier, "Loaded .autoclawd.yaml from repo");
|
|
1798
1950
|
}
|
|
@@ -1826,7 +1978,46 @@ async function executeTicket(opts) {
|
|
|
1826
1978
|
name: containerName
|
|
1827
1979
|
});
|
|
1828
1980
|
await setupRepo(container, { branchName });
|
|
1829
|
-
|
|
1981
|
+
let siblings = [];
|
|
1982
|
+
try {
|
|
1983
|
+
const siblingPRs = await findSiblingPRs(octokit, {
|
|
1984
|
+
repoUrl: ticket.repoUrl,
|
|
1985
|
+
baseBranch: actualBase,
|
|
1986
|
+
excludeHead: branchName,
|
|
1987
|
+
branchPrefix: config.safety.branchPrefix
|
|
1988
|
+
});
|
|
1989
|
+
for (const spr of siblingPRs) {
|
|
1990
|
+
const match = spr.head.match(/^(?:autoclawd\/)([A-Za-z]+-\d+)/);
|
|
1991
|
+
if (!match) continue;
|
|
1992
|
+
const sibTicket = await fetchTicket(linearClient, match[1]);
|
|
1993
|
+
if (sibTicket && sibTicket.identifier !== ticket.identifier) {
|
|
1994
|
+
siblings.push({
|
|
1995
|
+
identifier: sibTicket.identifier,
|
|
1996
|
+
title: sibTicket.title,
|
|
1997
|
+
description: sibTicket.description
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
if (ticket.baseBranch && siblings.length === 0) {
|
|
2002
|
+
const baseLabelName = `base:${ticket.baseBranch}`;
|
|
2003
|
+
const siblingTickets = await findTicketsWithLabel(linearClient, config.linear.teamId, baseLabelName);
|
|
2004
|
+
for (const sib of siblingTickets) {
|
|
2005
|
+
if (sib.identifier !== ticket.identifier) {
|
|
2006
|
+
siblings.push({
|
|
2007
|
+
identifier: sib.identifier,
|
|
2008
|
+
title: sib.title,
|
|
2009
|
+
description: sib.description
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (siblings.length > 0) {
|
|
2015
|
+
log.ticket(ticket.identifier, `Found ${siblings.length} sibling ticket(s): ${siblings.map((s) => s.identifier).join(", ")}`);
|
|
2016
|
+
}
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
log.debug(`Could not discover siblings: ${err instanceof Error ? err.message : err}`);
|
|
2019
|
+
}
|
|
2020
|
+
const agentPrompt = buildPrompt(ticket, prompt, siblings.length > 0 ? siblings : void 0);
|
|
1830
2021
|
let agentResult = await runAgentLoop({
|
|
1831
2022
|
container,
|
|
1832
2023
|
agentConfig: agent,
|
|
@@ -1889,7 +2080,7 @@ async function executeTicket(opts) {
|
|
|
1889
2080
|
`| Model | ${agent.model} |`,
|
|
1890
2081
|
"",
|
|
1891
2082
|
"---",
|
|
1892
|
-
"*Generated by [autoclawd](https://github.com/
|
|
2083
|
+
"*Generated by [autoclawd](https://github.com/autoclawd/autoclawd)*"
|
|
1893
2084
|
);
|
|
1894
2085
|
return prBody.join("\n");
|
|
1895
2086
|
};
|
|
@@ -1967,6 +2158,78 @@ async function executeTicket(opts) {
|
|
|
1967
2158
|
}
|
|
1968
2159
|
}
|
|
1969
2160
|
}
|
|
2161
|
+
if (integration.crossBranchValidate && validate && validate.length > 0) {
|
|
2162
|
+
try {
|
|
2163
|
+
const siblingPRs = await findSiblingPRs(octokit, {
|
|
2164
|
+
repoUrl: ticket.repoUrl,
|
|
2165
|
+
baseBranch: actualBase,
|
|
2166
|
+
excludeHead: branchName,
|
|
2167
|
+
branchPrefix: config.safety.branchPrefix
|
|
2168
|
+
});
|
|
2169
|
+
const siblingBranches = siblingPRs.map((s) => s.head);
|
|
2170
|
+
if (siblingBranches.length > 0) {
|
|
2171
|
+
log.ticket(ticket.identifier, `Running cross-branch validation against ${siblingBranches.length} sibling(s)`);
|
|
2172
|
+
let crossIterations = 0;
|
|
2173
|
+
const maxCrossIterations = integration.maxCrossBranchIterations;
|
|
2174
|
+
while (crossIterations < maxCrossIterations) {
|
|
2175
|
+
const crossResults = await runCrossBranchValidation(
|
|
2176
|
+
container,
|
|
2177
|
+
validate,
|
|
2178
|
+
siblingBranches,
|
|
2179
|
+
ticket.identifier
|
|
2180
|
+
);
|
|
2181
|
+
const failing = crossResults.filter((r) => r.failures.length > 0);
|
|
2182
|
+
if (failing.length === 0) {
|
|
2183
|
+
log.ticket(ticket.identifier, "Cross-branch validation passed");
|
|
2184
|
+
break;
|
|
2185
|
+
}
|
|
2186
|
+
crossIterations++;
|
|
2187
|
+
log.ticket(ticket.identifier, `Cross-branch issues with ${failing.map((f) => f.branch).join(", ")} (attempt ${crossIterations}/${maxCrossIterations})`);
|
|
2188
|
+
const fixPrompt = buildCrossBranchFixPrompt(failing);
|
|
2189
|
+
const fixResult = await runAgentLoop({
|
|
2190
|
+
container,
|
|
2191
|
+
agentConfig: { ...agent, maxIterations: 1 },
|
|
2192
|
+
prompt: fixPrompt,
|
|
2193
|
+
ticketId: ticket.identifier
|
|
2194
|
+
});
|
|
2195
|
+
agentResult = {
|
|
2196
|
+
iterations: agentResult.iterations + fixResult.iterations,
|
|
2197
|
+
success: fixResult.success,
|
|
2198
|
+
lastOutput: fixResult.lastOutput
|
|
2199
|
+
};
|
|
2200
|
+
await commitAndPush(container, {
|
|
2201
|
+
branchName,
|
|
2202
|
+
ticketId: ticket.identifier,
|
|
2203
|
+
title: ticket.title,
|
|
2204
|
+
repoUrl: ticket.repoUrl,
|
|
2205
|
+
githubToken: config.github.token,
|
|
2206
|
+
branchPrefix: config.safety.branchPrefix,
|
|
2207
|
+
maxFileChanges: config.safety.maxFileChanges
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
const finalCross = await runCrossBranchValidation(
|
|
2211
|
+
container,
|
|
2212
|
+
validate,
|
|
2213
|
+
siblingBranches,
|
|
2214
|
+
ticket.identifier
|
|
2215
|
+
);
|
|
2216
|
+
const stillFailing = finalCross.filter((r) => r.failures.length > 0);
|
|
2217
|
+
if (stillFailing.length > 0) {
|
|
2218
|
+
const allFailures = stillFailing.flatMap(
|
|
2219
|
+
(r) => r.failures.map((f) => ({ ...f, command: `[cross: ${r.branch}] ${f.command}` }))
|
|
2220
|
+
);
|
|
2221
|
+
log.ticket(ticket.identifier, "Cross-branch validation still failing \u2014 adding warnings to PR");
|
|
2222
|
+
await updatePRBody(octokit, {
|
|
2223
|
+
repoUrl: ticket.repoUrl,
|
|
2224
|
+
prNumber: pr.number,
|
|
2225
|
+
body: buildPRBody({ validationWarnings: allFailures, commitCount: gitResult.commitCount })
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
} catch (err) {
|
|
2230
|
+
log.debug(`Cross-branch validation error: ${err instanceof Error ? err.message : err}`);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
1970
2233
|
await reviewTicket(linearClient, config, ticket.id, `PR opened: ${pr.url}`);
|
|
1971
2234
|
await addBranchLabel(linearClient, ticket.id, branchName);
|
|
1972
2235
|
log.success(`${ticket.identifier} done \u2014 ${pr.url}`);
|
|
@@ -2223,6 +2486,7 @@ var WebhookServer = class {
|
|
|
2223
2486
|
} else {
|
|
2224
2487
|
return;
|
|
2225
2488
|
}
|
|
2489
|
+
this.recentlyRequeued.clear();
|
|
2226
2490
|
if (this.activeInMemory.has(data.id) || isTicketActive(data.id)) {
|
|
2227
2491
|
log.debug(`${data.identifier}: already active, ignoring`);
|
|
2228
2492
|
return;
|
|
@@ -2288,6 +2552,7 @@ var WebhookServer = class {
|
|
|
2288
2552
|
log.ticket(ticket.identifier, "Re-queued (waiting for dependency branch)");
|
|
2289
2553
|
finishRun(ticket.id, "failed", result.error);
|
|
2290
2554
|
removeFromProcessed(ticket.id);
|
|
2555
|
+
this.recentlyRequeued.add(ticket.id);
|
|
2291
2556
|
enqueue(ticket);
|
|
2292
2557
|
} else if (result.success) {
|
|
2293
2558
|
finishRun(ticket.id, "success");
|
|
@@ -2303,14 +2568,33 @@ var WebhookServer = class {
|
|
|
2303
2568
|
this.drainQueue();
|
|
2304
2569
|
});
|
|
2305
2570
|
}
|
|
2571
|
+
// Track recently requeued tickets to prevent cycling in the same drain
|
|
2572
|
+
recentlyRequeued = /* @__PURE__ */ new Set();
|
|
2306
2573
|
drainQueue() {
|
|
2307
2574
|
if (this.pausedUntil > Date.now()) return;
|
|
2308
|
-
|
|
2575
|
+
const tickets = [];
|
|
2576
|
+
while (true) {
|
|
2309
2577
|
const next = dequeue();
|
|
2310
2578
|
if (!next) break;
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2579
|
+
tickets.push(next);
|
|
2580
|
+
}
|
|
2581
|
+
tickets.sort((a, b) => {
|
|
2582
|
+
const aStacked = a.baseBranch ? 1 : 0;
|
|
2583
|
+
const bStacked = b.baseBranch ? 1 : 0;
|
|
2584
|
+
return aStacked - bStacked;
|
|
2585
|
+
});
|
|
2586
|
+
for (const ticket of tickets) {
|
|
2587
|
+
if (this.activeInMemory.has(ticket.id)) continue;
|
|
2588
|
+
if (ticket.baseBranch && this.recentlyRequeued.has(ticket.id)) {
|
|
2589
|
+
enqueue(ticket);
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
if (this.activeInMemory.size >= this.config.maxConcurrent) {
|
|
2593
|
+
enqueue(ticket);
|
|
2594
|
+
continue;
|
|
2595
|
+
}
|
|
2596
|
+
log.ticket(ticket.identifier, `Dequeued (${getQueueLength()} remaining)`);
|
|
2597
|
+
this.dispatch(ticket);
|
|
2314
2598
|
}
|
|
2315
2599
|
}
|
|
2316
2600
|
};
|
|
@@ -2912,7 +3196,7 @@ program.command("stop").description("Stop the background watcher").action(() =>
|
|
|
2912
3196
|
process.exit(1);
|
|
2913
3197
|
}
|
|
2914
3198
|
});
|
|
2915
|
-
program.command("run <ticket>").description("Run a single ticket (e.g. autoclawd run
|
|
3199
|
+
program.command("run <ticket>").description("Run a single ticket (e.g. autoclawd run TEAM-123)").option("-c, --config <path>", "Config file path").option("-v, --verbose", "Verbose logging").option("--dry-run", "Show what would happen without executing").option("--force", "Bypass completed-ticket check and re-run").action(async (ticketId, opts) => {
|
|
2916
3200
|
if (opts.verbose) setLogLevel("debug");
|
|
2917
3201
|
enableFileLogging();
|
|
2918
3202
|
const config = loadConfig(opts.config);
|
|
@@ -2984,12 +3268,12 @@ Expected: https://github.com/owner/repo/pull/123`);
|
|
|
2984
3268
|
throw new Error(result.error ?? "Fix failed");
|
|
2985
3269
|
}
|
|
2986
3270
|
});
|
|
2987
|
-
program.command("retry [ticket]").description("Retry a failed ticket (e.g. autoclawd retry
|
|
3271
|
+
program.command("retry [ticket]").description("Retry a failed ticket (e.g. autoclawd retry TEAM-123)").option("-c, --config <path>", "Config file path").option("-v, --verbose", "Verbose logging").option("--last-failed", "Retry the most recent failed run").option("--all-failed", "Retry all failed runs (sequentially)").action(async (ticketArg, opts) => {
|
|
2988
3272
|
if (opts.verbose) setLogLevel("debug");
|
|
2989
3273
|
enableFileLogging();
|
|
2990
3274
|
if (!ticketArg && !opts.lastFailed && !opts.allFailed) {
|
|
2991
3275
|
throw new Error(
|
|
2992
|
-
"Provide a ticket ID, --last-failed, or --all-failed.\nUsage: autoclawd retry
|
|
3276
|
+
"Provide a ticket ID, --last-failed, or --all-failed.\nUsage: autoclawd retry TEAM-123 | --last-failed | --all-failed"
|
|
2993
3277
|
);
|
|
2994
3278
|
}
|
|
2995
3279
|
const config = loadConfig(opts.config);
|