automata-cli 0.1.0-feature-030-do-work.198 → 0.1.0-feature-030-do-work.202
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.
|
@@ -252,7 +252,8 @@ function ConfigWizard() {
|
|
|
252
252
|
"do-work-max-runs": {
|
|
253
253
|
setValue: setDoWorkMaxRuns,
|
|
254
254
|
onSubmit: () => {
|
|
255
|
-
const
|
|
255
|
+
const trimmedMaxRuns = doWorkMaxRuns.trim();
|
|
256
|
+
const parsedMaxRuns = /^\d+$/.test(trimmedMaxRuns) ? Number(trimmedMaxRuns) : Number.NaN;
|
|
256
257
|
const current = readRawConfig();
|
|
257
258
|
writeConfig({
|
|
258
259
|
...current,
|
|
@@ -264,7 +265,7 @@ function ConfigWizard() {
|
|
|
264
265
|
claude: doWorkClaudeModel.trim() || void 0,
|
|
265
266
|
codex: doWorkCodexModel.trim() || void 0
|
|
266
267
|
},
|
|
267
|
-
maxRunsPerTick: Number.
|
|
268
|
+
maxRunsPerTick: Number.isSafeInteger(parsedMaxRuns) && parsedMaxRuns >= 0 ? parsedMaxRuns : void 0
|
|
268
269
|
}
|
|
269
270
|
});
|
|
270
271
|
exit();
|
package/dist/index.js
CHANGED
|
@@ -183,7 +183,7 @@ var configCommand = new Command("config").description("Configure automata settin
|
|
|
183
183
|
const [{ render }, React, { ConfigWizard }] = await Promise.all([
|
|
184
184
|
import("ink"),
|
|
185
185
|
import("react"),
|
|
186
|
-
import("./ConfigWizard-
|
|
186
|
+
import("./ConfigWizard-26Y5YBF7.js")
|
|
187
187
|
]);
|
|
188
188
|
const { waitUntilExit } = render(React.createElement(ConfigWizard));
|
|
189
189
|
await waitUntilExit();
|
|
@@ -693,8 +693,12 @@ function isUpstreamGone(branch) {
|
|
|
693
693
|
const { status } = run2("git", ["ls-remote", "--exit-code", "--heads", "origin", branch]);
|
|
694
694
|
return status !== 0;
|
|
695
695
|
}
|
|
696
|
-
function hasUncommittedChanges() {
|
|
697
|
-
const
|
|
696
|
+
function hasUncommittedChanges(excludePaths = []) {
|
|
697
|
+
const args = ["status", "--porcelain"];
|
|
698
|
+
if (excludePaths.length > 0) {
|
|
699
|
+
args.push("--", ".", ...excludePaths.map((path) => `:(exclude)${path}`));
|
|
700
|
+
}
|
|
701
|
+
const { stdout } = run2("git", args);
|
|
698
702
|
return stdout.trim().length > 0;
|
|
699
703
|
}
|
|
700
704
|
function checkoutAndPull(targetBranch) {
|
|
@@ -1517,26 +1521,16 @@ function handleExitCode(status, toolName) {
|
|
|
1517
1521
|
}
|
|
1518
1522
|
}
|
|
1519
1523
|
|
|
1520
|
-
// src/
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
const candidate = join(dir, name);
|
|
1525
|
-
if (existsSync(candidate)) return candidate;
|
|
1526
|
-
}
|
|
1527
|
-
return name;
|
|
1524
|
+
// src/cli/childRegistry.ts
|
|
1525
|
+
var active = /* @__PURE__ */ new Set();
|
|
1526
|
+
function trackChild(child) {
|
|
1527
|
+
active.add(child);
|
|
1528
1528
|
}
|
|
1529
|
-
function
|
|
1530
|
-
|
|
1531
|
-
if (options.yolo) args.push("--dangerously-skip-permissions");
|
|
1532
|
-
if (options.model) args.push("--model", options.model);
|
|
1533
|
-
if (options.verbose) args.push("--verbose", "--output-format", "stream-json");
|
|
1534
|
-
args.push("-p", prompt);
|
|
1535
|
-
return args;
|
|
1529
|
+
function untrackChild(child) {
|
|
1530
|
+
active.delete(child);
|
|
1536
1531
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const children = [...activeChildren];
|
|
1532
|
+
function terminateTrackedChildren(timeoutMs = 1e4) {
|
|
1533
|
+
const children = [...active];
|
|
1540
1534
|
if (children.length === 0) return Promise.resolve();
|
|
1541
1535
|
const exits = children.map(
|
|
1542
1536
|
(child) => new Promise((resolve2) => {
|
|
@@ -1559,6 +1553,62 @@ function terminateActiveClaudeProcesses(timeoutMs = 1e4) {
|
|
|
1559
1553
|
})
|
|
1560
1554
|
]);
|
|
1561
1555
|
}
|
|
1556
|
+
|
|
1557
|
+
// src/claude/claudeService.ts
|
|
1558
|
+
function resolveCommand(name) {
|
|
1559
|
+
const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
|
|
1560
|
+
for (const dir of pathDirs) {
|
|
1561
|
+
const candidate = join(dir, name);
|
|
1562
|
+
if (existsSync(candidate)) return candidate;
|
|
1563
|
+
}
|
|
1564
|
+
return name;
|
|
1565
|
+
}
|
|
1566
|
+
function buildClaudeArgs(prompt, options = {}) {
|
|
1567
|
+
const args = [];
|
|
1568
|
+
if (options.yolo) args.push("--dangerously-skip-permissions");
|
|
1569
|
+
if (options.model) args.push("--model", options.model);
|
|
1570
|
+
if (options.verbose) args.push("--verbose", "--output-format", "stream-json");
|
|
1571
|
+
args.push("-p", prompt);
|
|
1572
|
+
return args;
|
|
1573
|
+
}
|
|
1574
|
+
function runClaude(prompt, options = {}) {
|
|
1575
|
+
return new Promise((resolve2, reject) => {
|
|
1576
|
+
const claudeBin = resolveCommand("claude");
|
|
1577
|
+
const args = buildClaudeArgs(prompt, { yolo: true, model: options.model, verbose: true });
|
|
1578
|
+
const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
|
|
1579
|
+
trackChild(child);
|
|
1580
|
+
const rl = createInterface({ input: child.stdout });
|
|
1581
|
+
let turnCount = 0;
|
|
1582
|
+
rl.on("line", (line) => {
|
|
1583
|
+
if (options.printSteps !== true) return;
|
|
1584
|
+
try {
|
|
1585
|
+
const event = JSON.parse(line);
|
|
1586
|
+
formatEvent(event, turnCount);
|
|
1587
|
+
if (event["type"] === "assistant") turnCount++;
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
child.on("error", (err) => {
|
|
1592
|
+
untrackChild(child);
|
|
1593
|
+
const nodeErr = err;
|
|
1594
|
+
reject(
|
|
1595
|
+
nodeErr.code === "ENOENT" ? new Error("`claude` CLI is not installed or not on PATH.") : new Error(nodeErr.message)
|
|
1596
|
+
);
|
|
1597
|
+
});
|
|
1598
|
+
child.on("close", (code, signal) => {
|
|
1599
|
+
untrackChild(child);
|
|
1600
|
+
if (code === 0) {
|
|
1601
|
+
resolve2();
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
reject(
|
|
1605
|
+
new Error(
|
|
1606
|
+
signal === null ? `Claude Code exited with code ${String(code)}.` : `Claude Code terminated on ${signal}.`
|
|
1607
|
+
)
|
|
1608
|
+
);
|
|
1609
|
+
});
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1562
1612
|
function invokeClaudeCode(prompt, options = {}) {
|
|
1563
1613
|
if (options.verbose) {
|
|
1564
1614
|
return invokeClaudeCodeVerbose(prompt, options.yolo ?? false, options.model);
|
|
@@ -1577,7 +1627,6 @@ function invokeClaudeCodeVerbose(prompt, yolo, model) {
|
|
|
1577
1627
|
const claudeBin = resolveCommand("claude");
|
|
1578
1628
|
const args = buildClaudeArgs(prompt, { yolo, model, verbose: true });
|
|
1579
1629
|
const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
|
|
1580
|
-
activeChildren.add(child);
|
|
1581
1630
|
const rl = createInterface({ input: child.stdout });
|
|
1582
1631
|
let turnCount = 0;
|
|
1583
1632
|
child.on("error", (err) => {
|
|
@@ -1592,7 +1641,6 @@ function invokeClaudeCodeVerbose(prompt, yolo, model) {
|
|
|
1592
1641
|
}
|
|
1593
1642
|
});
|
|
1594
1643
|
child.on("close", (code) => {
|
|
1595
|
-
activeChildren.delete(child);
|
|
1596
1644
|
handleExitCode(code, "Claude Code");
|
|
1597
1645
|
resolve2();
|
|
1598
1646
|
});
|
|
@@ -1663,7 +1711,7 @@ function summarizeTool(name, input) {
|
|
|
1663
1711
|
}
|
|
1664
1712
|
|
|
1665
1713
|
// src/codex/codexService.ts
|
|
1666
|
-
import { spawnSync as spawnSync5 } from "child_process";
|
|
1714
|
+
import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
|
|
1667
1715
|
function buildCodexArgs(prompt, options = {}) {
|
|
1668
1716
|
const args = ["exec"];
|
|
1669
1717
|
if (options.yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
@@ -1684,6 +1732,33 @@ function invokeCodexCodeSync(prompt, yolo, model) {
|
|
|
1684
1732
|
handleSpawnError(result.error, "codex");
|
|
1685
1733
|
handleExitCode(result.status, "Codex");
|
|
1686
1734
|
}
|
|
1735
|
+
function runCodex(prompt, options = {}) {
|
|
1736
|
+
return new Promise((resolve2, reject) => {
|
|
1737
|
+
const codexBin = resolveCommand("codex");
|
|
1738
|
+
const args = buildCodexArgs(prompt, { yolo: true, model: options.model });
|
|
1739
|
+
const child = spawn2(codexBin, args, { stdio: "inherit" });
|
|
1740
|
+
trackChild(child);
|
|
1741
|
+
child.on("error", (err) => {
|
|
1742
|
+
untrackChild(child);
|
|
1743
|
+
const nodeErr = err;
|
|
1744
|
+
reject(
|
|
1745
|
+
nodeErr.code === "ENOENT" ? new Error("`codex` CLI is not installed or not on PATH.") : new Error(nodeErr.message)
|
|
1746
|
+
);
|
|
1747
|
+
});
|
|
1748
|
+
child.on("close", (code, signal) => {
|
|
1749
|
+
untrackChild(child);
|
|
1750
|
+
if (code === 0) {
|
|
1751
|
+
resolve2();
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
reject(
|
|
1755
|
+
new Error(
|
|
1756
|
+
signal === null ? `Codex exited with code ${String(code)}.` : `Codex terminated on ${signal}.`
|
|
1757
|
+
)
|
|
1758
|
+
);
|
|
1759
|
+
});
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1687
1762
|
|
|
1688
1763
|
// src/commands/getReady.ts
|
|
1689
1764
|
function writeOverflowHint(output, issues, limit) {
|
|
@@ -2314,7 +2389,7 @@ function getIssueSurface(issueNumber) {
|
|
|
2314
2389
|
"view",
|
|
2315
2390
|
String(issueNumber),
|
|
2316
2391
|
"--json",
|
|
2317
|
-
"number,title,body,url,state,author,createdAt,assignees,comments"
|
|
2392
|
+
"number,title,body,url,state,author,createdAt,assignees,labels,comments"
|
|
2318
2393
|
],
|
|
2319
2394
|
`read issue #${String(issueNumber)}`
|
|
2320
2395
|
);
|
|
@@ -2336,6 +2411,7 @@ function getIssueSurface(issueNumber) {
|
|
|
2336
2411
|
issue: { number: raw.number, title: raw.title, body: raw.body, url: raw.url },
|
|
2337
2412
|
state: raw.state === "CLOSED" ? "CLOSED" : "OPEN",
|
|
2338
2413
|
assignees: (raw.assignees ?? []).map(login).filter((name) => name.length > 0),
|
|
2414
|
+
labels: (raw.labels ?? []).map((label) => label.name ?? "").filter((name) => name.length > 0),
|
|
2339
2415
|
messages
|
|
2340
2416
|
};
|
|
2341
2417
|
}
|
|
@@ -2366,9 +2442,8 @@ function indexPullRequest(map, node) {
|
|
|
2366
2442
|
updatedAt: node.updatedAt
|
|
2367
2443
|
};
|
|
2368
2444
|
if (node.closingIssuesReferences.pageInfo?.hasNextPage) {
|
|
2369
|
-
|
|
2370
|
-
`
|
|
2371
|
-
`
|
|
2445
|
+
throw new Error(
|
|
2446
|
+
`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.`
|
|
2372
2447
|
);
|
|
2373
2448
|
}
|
|
2374
2449
|
for (const issue of node.closingIssuesReferences.nodes) {
|
|
@@ -2397,17 +2472,16 @@ function getOpenPrLinkMap() {
|
|
|
2397
2472
|
}
|
|
2398
2473
|
cursor = connection.pageInfo.endCursor;
|
|
2399
2474
|
}
|
|
2400
|
-
|
|
2401
|
-
`
|
|
2402
|
-
`
|
|
2475
|
+
throw new Error(
|
|
2476
|
+
`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.`
|
|
2403
2477
|
);
|
|
2404
|
-
return map;
|
|
2405
2478
|
}
|
|
2406
2479
|
var REVIEW_THREADS_QUERY2 = `
|
|
2407
|
-
query($owner:String!,$repo:String!,$prNumber:Int
|
|
2480
|
+
query($owner:String!,$repo:String!,$prNumber:Int!,$cursor:String){
|
|
2408
2481
|
repository(owner:$owner,name:$repo){
|
|
2409
2482
|
pullRequest(number:$prNumber){
|
|
2410
|
-
reviewThreads(first:100){
|
|
2483
|
+
reviewThreads(first:100, after:$cursor){
|
|
2484
|
+
pageInfo{ hasNextPage endCursor }
|
|
2411
2485
|
nodes{
|
|
2412
2486
|
isResolved isOutdated path line
|
|
2413
2487
|
comments(last:100){
|
|
@@ -2418,11 +2492,57 @@ query($owner:String!,$repo:String!,$prNumber:Int!){
|
|
|
2418
2492
|
}
|
|
2419
2493
|
}
|
|
2420
2494
|
}`.trim();
|
|
2495
|
+
var MAX_THREAD_PAGES = 50;
|
|
2421
2496
|
function normalizePrState(state) {
|
|
2422
2497
|
if (state === "MERGED") return "MERGED";
|
|
2423
2498
|
if (state === "CLOSED") return "CLOSED";
|
|
2424
2499
|
return "OPEN";
|
|
2425
2500
|
}
|
|
2501
|
+
function getReviewThreads(prNumber) {
|
|
2502
|
+
const { owner, repo } = getRepoSlug();
|
|
2503
|
+
const threads = [];
|
|
2504
|
+
let cursor = null;
|
|
2505
|
+
for (let page = 0; page < MAX_THREAD_PAGES; page++) {
|
|
2506
|
+
const args = [
|
|
2507
|
+
"api",
|
|
2508
|
+
"graphql",
|
|
2509
|
+
"-f",
|
|
2510
|
+
`query=${REVIEW_THREADS_QUERY2}`,
|
|
2511
|
+
"-f",
|
|
2512
|
+
`owner=${owner}`,
|
|
2513
|
+
"-f",
|
|
2514
|
+
`repo=${repo}`,
|
|
2515
|
+
"-F",
|
|
2516
|
+
`prNumber=${String(prNumber)}`
|
|
2517
|
+
];
|
|
2518
|
+
if (cursor !== null) args.push("-f", `cursor=${cursor}`);
|
|
2519
|
+
const response = ghJson(
|
|
2520
|
+
args,
|
|
2521
|
+
`query review threads for pull request #${String(prNumber)}`
|
|
2522
|
+
);
|
|
2523
|
+
const connection = response.data.repository.pullRequest.reviewThreads;
|
|
2524
|
+
for (const node of connection.nodes) {
|
|
2525
|
+
threads.push({
|
|
2526
|
+
path: node.path,
|
|
2527
|
+
line: node.line ?? null,
|
|
2528
|
+
isResolved: node.isResolved,
|
|
2529
|
+
comments: node.comments.nodes.map((comment) => ({
|
|
2530
|
+
kind: "thread-comment",
|
|
2531
|
+
author: login(comment.author),
|
|
2532
|
+
body: comment.body,
|
|
2533
|
+
createdAt: comment.createdAt
|
|
2534
|
+
})).sort(byCreatedAt2)
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
2538
|
+
return threads;
|
|
2539
|
+
}
|
|
2540
|
+
cursor = connection.pageInfo.endCursor;
|
|
2541
|
+
}
|
|
2542
|
+
throw new Error(
|
|
2543
|
+
`Stopped paginating review threads for pull request #${String(prNumber)} after ${String(MAX_THREAD_PAGES)} pages; refusing rather than answering only part of the feedback.`
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2426
2546
|
function getPrSurface(prNumber) {
|
|
2427
2547
|
const raw = ghJson(
|
|
2428
2548
|
[
|
|
@@ -2450,35 +2570,7 @@ function getPrSurface(prNumber) {
|
|
|
2450
2570
|
createdAt: review.submittedAt ?? review.createdAt ?? ""
|
|
2451
2571
|
}))
|
|
2452
2572
|
].sort(byCreatedAt2);
|
|
2453
|
-
const
|
|
2454
|
-
const threadsResponse = ghJson(
|
|
2455
|
-
[
|
|
2456
|
-
"api",
|
|
2457
|
-
"graphql",
|
|
2458
|
-
"-f",
|
|
2459
|
-
`query=${REVIEW_THREADS_QUERY2}`,
|
|
2460
|
-
"-f",
|
|
2461
|
-
`owner=${owner}`,
|
|
2462
|
-
"-f",
|
|
2463
|
-
`repo=${repo}`,
|
|
2464
|
-
"-F",
|
|
2465
|
-
`prNumber=${String(prNumber)}`
|
|
2466
|
-
],
|
|
2467
|
-
`query review threads for pull request #${String(prNumber)}`
|
|
2468
|
-
);
|
|
2469
|
-
const threads = threadsResponse.data.repository.pullRequest.reviewThreads.nodes.map(
|
|
2470
|
-
(node) => ({
|
|
2471
|
-
path: node.path,
|
|
2472
|
-
line: node.line ?? null,
|
|
2473
|
-
isResolved: node.isResolved,
|
|
2474
|
-
comments: node.comments.nodes.map((comment) => ({
|
|
2475
|
-
kind: "thread-comment",
|
|
2476
|
-
author: login(comment.author),
|
|
2477
|
-
body: comment.body,
|
|
2478
|
-
createdAt: comment.createdAt
|
|
2479
|
-
})).sort(byCreatedAt2)
|
|
2480
|
-
})
|
|
2481
|
-
);
|
|
2573
|
+
const threads = getReviewThreads(prNumber);
|
|
2482
2574
|
const state = normalizePrState(raw.state);
|
|
2483
2575
|
return {
|
|
2484
2576
|
pr: {
|
|
@@ -2731,54 +2823,14 @@ function composePrompt(input) {
|
|
|
2731
2823
|
return lines.join("\n");
|
|
2732
2824
|
}
|
|
2733
2825
|
|
|
2734
|
-
// src/git/workspaceService.ts
|
|
2735
|
-
function dirtyTree() {
|
|
2736
|
-
return {
|
|
2737
|
-
ok: false,
|
|
2738
|
-
reason: "dirty-tree",
|
|
2739
|
-
detail: "the working tree has uncommitted changes; commit or stash them yourself and re-run"
|
|
2740
|
-
};
|
|
2741
|
-
}
|
|
2742
|
-
function prepareBaseBranch(baseBranch) {
|
|
2743
|
-
if (hasUncommittedChanges()) return dirtyTree();
|
|
2744
|
-
const checkout = checkoutBranch(baseBranch);
|
|
2745
|
-
if (!checkout.ok) {
|
|
2746
|
-
return { ok: false, reason: "checkout-failed", detail: checkout.stderr };
|
|
2747
|
-
}
|
|
2748
|
-
const pull = pullFastForwardOnly();
|
|
2749
|
-
if (!pull.ok) {
|
|
2750
|
-
return { ok: false, reason: "pull-failed", detail: pull.stderr };
|
|
2751
|
-
}
|
|
2752
|
-
return { ok: true, branch: baseBranch };
|
|
2753
|
-
}
|
|
2754
|
-
function preparePrBranch(headRefName) {
|
|
2755
|
-
if (hasUncommittedChanges()) return dirtyTree();
|
|
2756
|
-
const fetched = fetchBranch(headRefName);
|
|
2757
|
-
if (!fetched.ok) {
|
|
2758
|
-
return { ok: false, reason: "checkout-failed", detail: fetched.stderr };
|
|
2759
|
-
}
|
|
2760
|
-
const checkout = checkoutBranch(headRefName);
|
|
2761
|
-
if (!checkout.ok) {
|
|
2762
|
-
const created = createTrackingBranch(headRefName);
|
|
2763
|
-
if (!created.ok) {
|
|
2764
|
-
return { ok: false, reason: "checkout-failed", detail: created.stderr };
|
|
2765
|
-
}
|
|
2766
|
-
return { ok: true, branch: headRefName };
|
|
2767
|
-
}
|
|
2768
|
-
const pull = pullFastForwardOnly(headRefName);
|
|
2769
|
-
if (!pull.ok) {
|
|
2770
|
-
return { ok: false, reason: "pull-failed", detail: pull.stderr };
|
|
2771
|
-
}
|
|
2772
|
-
return { ok: true, branch: headRefName };
|
|
2773
|
-
}
|
|
2774
|
-
|
|
2775
2826
|
// src/run/runLock.ts
|
|
2776
|
-
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync } from "fs";
|
|
2827
|
+
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync, renameSync } from "fs";
|
|
2777
2828
|
import { randomUUID } from "crypto";
|
|
2778
2829
|
import { hostname } from "os";
|
|
2779
2830
|
import { join as join2 } from "path";
|
|
2780
2831
|
var LOCK_DIR = ".automata";
|
|
2781
2832
|
var LOCK_FILE = "automata.lock";
|
|
2833
|
+
var RUN_LOCK_RELATIVE_PATH = `${LOCK_DIR}/${LOCK_FILE}`;
|
|
2782
2834
|
function lockPath() {
|
|
2783
2835
|
return join2(process.cwd(), LOCK_DIR, LOCK_FILE);
|
|
2784
2836
|
}
|
|
@@ -2831,7 +2883,7 @@ function makeHandle(path, token) {
|
|
|
2831
2883
|
}
|
|
2832
2884
|
};
|
|
2833
2885
|
}
|
|
2834
|
-
function write(path, command, token) {
|
|
2886
|
+
function write(path, command, token, flag = "wx") {
|
|
2835
2887
|
const owner = {
|
|
2836
2888
|
pid: process.pid,
|
|
2837
2889
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2839,7 +2891,7 @@ function write(path, command, token) {
|
|
|
2839
2891
|
command,
|
|
2840
2892
|
token
|
|
2841
2893
|
};
|
|
2842
|
-
writeFileSync(path, JSON.stringify(owner, null, 2) + "\n", { encoding: "utf8", flag
|
|
2894
|
+
writeFileSync(path, JSON.stringify(owner, null, 2) + "\n", { encoding: "utf8", flag });
|
|
2843
2895
|
}
|
|
2844
2896
|
function acquireRunLock(command, staleMinutes) {
|
|
2845
2897
|
const path = lockPath();
|
|
@@ -2855,27 +2907,73 @@ function acquireRunLock(command, staleMinutes) {
|
|
|
2855
2907
|
if (!isStale(owner, staleMinutes)) {
|
|
2856
2908
|
return { ok: false, heldBy: owner };
|
|
2857
2909
|
}
|
|
2910
|
+
return reclaim(path, command, token);
|
|
2911
|
+
}
|
|
2912
|
+
var UNKNOWN_OWNER = {
|
|
2913
|
+
pid: 0,
|
|
2914
|
+
startedAt: "unknown",
|
|
2915
|
+
host: "unknown",
|
|
2916
|
+
command: "unknown",
|
|
2917
|
+
token: ""
|
|
2918
|
+
};
|
|
2919
|
+
function reclaim(path, command, token) {
|
|
2920
|
+
const candidate = `${path}.${token}`;
|
|
2858
2921
|
try {
|
|
2859
|
-
|
|
2860
|
-
|
|
2922
|
+
write(candidate, command, token, "w");
|
|
2923
|
+
renameSync(candidate, path);
|
|
2924
|
+
} catch (err) {
|
|
2925
|
+
try {
|
|
2926
|
+
unlinkSync(candidate);
|
|
2927
|
+
} catch {
|
|
2928
|
+
}
|
|
2929
|
+
throw err;
|
|
2861
2930
|
}
|
|
2862
|
-
|
|
2863
|
-
|
|
2931
|
+
const winner = readOwner(path);
|
|
2932
|
+
if (winner !== null && winner.token === token) {
|
|
2864
2933
|
return { ok: true, handle: makeHandle(path, token) };
|
|
2865
|
-
} catch (err) {
|
|
2866
|
-
if (err.code !== "EEXIST") throw err;
|
|
2867
|
-
const winner = readOwner(path);
|
|
2868
|
-
return {
|
|
2869
|
-
ok: false,
|
|
2870
|
-
heldBy: winner ?? {
|
|
2871
|
-
pid: 0,
|
|
2872
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2873
|
-
host: "unknown",
|
|
2874
|
-
command: "unknown",
|
|
2875
|
-
token: ""
|
|
2876
|
-
}
|
|
2877
|
-
};
|
|
2878
2934
|
}
|
|
2935
|
+
return { ok: false, heldBy: winner ?? UNKNOWN_OWNER };
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
// src/git/workspaceService.ts
|
|
2939
|
+
function dirtyTree() {
|
|
2940
|
+
return {
|
|
2941
|
+
ok: false,
|
|
2942
|
+
reason: "dirty-tree",
|
|
2943
|
+
detail: "the working tree has uncommitted changes; commit or stash them yourself and re-run"
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
function prepareBaseBranch(baseBranch) {
|
|
2947
|
+
if (hasUncommittedChanges([RUN_LOCK_RELATIVE_PATH])) return dirtyTree();
|
|
2948
|
+
const checkout = checkoutBranch(baseBranch);
|
|
2949
|
+
if (!checkout.ok) {
|
|
2950
|
+
return { ok: false, reason: "checkout-failed", detail: checkout.stderr };
|
|
2951
|
+
}
|
|
2952
|
+
const pull = pullFastForwardOnly();
|
|
2953
|
+
if (!pull.ok) {
|
|
2954
|
+
return { ok: false, reason: "pull-failed", detail: pull.stderr };
|
|
2955
|
+
}
|
|
2956
|
+
return { ok: true, branch: baseBranch };
|
|
2957
|
+
}
|
|
2958
|
+
function preparePrBranch(headRefName) {
|
|
2959
|
+
if (hasUncommittedChanges([RUN_LOCK_RELATIVE_PATH])) return dirtyTree();
|
|
2960
|
+
const fetched = fetchBranch(headRefName);
|
|
2961
|
+
if (!fetched.ok) {
|
|
2962
|
+
return { ok: false, reason: "checkout-failed", detail: fetched.stderr };
|
|
2963
|
+
}
|
|
2964
|
+
const checkout = checkoutBranch(headRefName);
|
|
2965
|
+
if (!checkout.ok) {
|
|
2966
|
+
const created = createTrackingBranch(headRefName);
|
|
2967
|
+
if (!created.ok) {
|
|
2968
|
+
return { ok: false, reason: "checkout-failed", detail: created.stderr };
|
|
2969
|
+
}
|
|
2970
|
+
return { ok: true, branch: headRefName };
|
|
2971
|
+
}
|
|
2972
|
+
const pull = pullFastForwardOnly(headRefName);
|
|
2973
|
+
if (!pull.ok) {
|
|
2974
|
+
return { ok: false, reason: "pull-failed", detail: pull.stderr };
|
|
2975
|
+
}
|
|
2976
|
+
return { ok: true, branch: headRefName };
|
|
2879
2977
|
}
|
|
2880
2978
|
|
|
2881
2979
|
// src/commands/doWork.ts
|
|
@@ -2891,10 +2989,14 @@ function fail(message) {
|
|
|
2891
2989
|
process.exit(1);
|
|
2892
2990
|
}
|
|
2893
2991
|
function parsePositiveInt(value, label) {
|
|
2894
|
-
const
|
|
2895
|
-
if (
|
|
2992
|
+
const trimmed = value.trim();
|
|
2993
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
2896
2994
|
fail(`${label} must be a positive integer (got "${value}").`);
|
|
2897
2995
|
}
|
|
2996
|
+
const parsed = Number(trimmed);
|
|
2997
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
2998
|
+
fail(`${label} must be a positive integer within the safe range (got "${value}").`);
|
|
2999
|
+
}
|
|
2898
3000
|
return parsed;
|
|
2899
3001
|
}
|
|
2900
3002
|
function resolveSettings(options) {
|
|
@@ -2924,6 +3026,7 @@ function resolveSettings(options) {
|
|
|
2924
3026
|
fail("No agent user configured. Run `automata config set agent-user <login>`.");
|
|
2925
3027
|
}
|
|
2926
3028
|
const doWork = config.doWork ?? {};
|
|
3029
|
+
validateDoWorkConfig(doWork);
|
|
2927
3030
|
let executor = doWork.executor ?? DEFAULT_DO_WORK.executor;
|
|
2928
3031
|
if (options.with !== void 0) {
|
|
2929
3032
|
const requested = options.with.toLowerCase();
|
|
@@ -2932,7 +3035,9 @@ function resolveSettings(options) {
|
|
|
2932
3035
|
}
|
|
2933
3036
|
executor = requested;
|
|
2934
3037
|
}
|
|
2935
|
-
|
|
3038
|
+
if (options.dryRun !== true) {
|
|
3039
|
+
checkAuthenticatedIdentity(agentUser, allowedUsers);
|
|
3040
|
+
}
|
|
2936
3041
|
return {
|
|
2937
3042
|
baseBranch: doWork.baseBranch ?? DEFAULT_DO_WORK.baseBranch,
|
|
2938
3043
|
executor,
|
|
@@ -2970,7 +3075,7 @@ function checkAuthenticatedIdentity(agentUser, allowedUsers) {
|
|
|
2970
3075
|
`\`gh\` is authenticated as "${login2}" but agentUser is "${agentUser}". Comments posted under that identity are neither the agent's nor an authorized user's, so they are filtered out of the conversation: the answer boundary would never advance and the same message would start a run on every tick. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`
|
|
2971
3076
|
);
|
|
2972
3077
|
}
|
|
2973
|
-
function planRun(item, settings
|
|
3078
|
+
function planRun(item, settings) {
|
|
2974
3079
|
const prompt = composePrompt({
|
|
2975
3080
|
item,
|
|
2976
3081
|
repo: getRepoSlug(),
|
|
@@ -2979,7 +3084,7 @@ function planRun(item, settings, silent) {
|
|
|
2979
3084
|
frame: settings.prompts[item.turn]
|
|
2980
3085
|
});
|
|
2981
3086
|
const bin = resolveCommand(settings.executor === "codex" ? "codex" : "claude");
|
|
2982
|
-
const args = settings.executor === "codex" ? buildCodexArgs(prompt, { yolo: true, model: settings.model }) : buildClaudeArgs(prompt, { yolo: true, verbose:
|
|
3087
|
+
const args = settings.executor === "codex" ? buildCodexArgs(prompt, { yolo: true, model: settings.model }) : buildClaudeArgs(prompt, { yolo: true, verbose: true, model: settings.model });
|
|
2983
3088
|
return { prompt, bin, args, command: [bin, ...args].map(shellQuote).join(" ") };
|
|
2984
3089
|
}
|
|
2985
3090
|
function describePlannedRun(item, settings, run5) {
|
|
@@ -3013,6 +3118,36 @@ function describePlannedRun(item, settings, run5) {
|
|
|
3013
3118
|
];
|
|
3014
3119
|
return lines.join("\n") + "\n";
|
|
3015
3120
|
}
|
|
3121
|
+
function validateDoWorkConfig(doWork) {
|
|
3122
|
+
if (doWork.executor !== void 0 && doWork.executor !== "claude" && doWork.executor !== "codex") {
|
|
3123
|
+
fail(`doWork.executor must be 'claude' or 'codex', got '${String(doWork.executor)}'.`);
|
|
3124
|
+
}
|
|
3125
|
+
if (doWork.baseBranch !== void 0 && doWork.baseBranch.trim().length === 0) {
|
|
3126
|
+
fail("doWork.baseBranch must not be empty.");
|
|
3127
|
+
}
|
|
3128
|
+
if (doWork.maxRunsPerTick !== void 0) {
|
|
3129
|
+
if (!Number.isSafeInteger(doWork.maxRunsPerTick) || doWork.maxRunsPerTick < 0) {
|
|
3130
|
+
fail(
|
|
3131
|
+
`doWork.maxRunsPerTick must be a non-negative integer (0 = unlimited), got ${String(doWork.maxRunsPerTick)}.`
|
|
3132
|
+
);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
if (doWork.lockStaleMinutes !== void 0) {
|
|
3136
|
+
if (!Number.isSafeInteger(doWork.lockStaleMinutes) || doWork.lockStaleMinutes <= 0) {
|
|
3137
|
+
fail(`doWork.lockStaleMinutes must be a positive integer, got ${String(doWork.lockStaleMinutes)}.`);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
for (const [key, value] of [
|
|
3141
|
+
["doWork.models.claude", doWork.models?.claude],
|
|
3142
|
+
["doWork.models.codex", doWork.models?.codex],
|
|
3143
|
+
["doWork.prompts.issueDiscuss", doWork.prompts?.issueDiscuss],
|
|
3144
|
+
["doWork.prompts.prWork", doWork.prompts?.prWork]
|
|
3145
|
+
]) {
|
|
3146
|
+
if (value !== void 0 && (typeof value !== "string" || value.trim().length === 0)) {
|
|
3147
|
+
fail(`${key} must be a non-empty string.`);
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3016
3151
|
function discoverIssues(settings) {
|
|
3017
3152
|
const candidates = listCandidateIssues(settings.technique, settings.discoveryValue, settings.limit);
|
|
3018
3153
|
if (settings.onlyIssue === void 0) {
|
|
@@ -3026,11 +3161,25 @@ function discoverIssues(settings) {
|
|
|
3026
3161
|
}
|
|
3027
3162
|
const match = candidates.find((issue) => issue.number === settings.onlyIssue);
|
|
3028
3163
|
if (match) return [match];
|
|
3029
|
-
|
|
3030
|
-
|
|
3164
|
+
const surface = getIssueSurface(settings.onlyIssue);
|
|
3165
|
+
if (!issueMatchesFilter(surface, settings)) {
|
|
3166
|
+
progress(
|
|
3167
|
+
`Note: issue #${String(settings.onlyIssue)} does not match the configured discovery filter (${settings.technique} = ${settings.discoveryValue}); processing it anyway because --issue was given.
|
|
3031
3168
|
`
|
|
3032
|
-
|
|
3033
|
-
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
return [surface.issue];
|
|
3172
|
+
}
|
|
3173
|
+
function issueMatchesFilter(surface, settings) {
|
|
3174
|
+
const value = settings.discoveryValue.toLowerCase();
|
|
3175
|
+
switch (settings.technique) {
|
|
3176
|
+
case "label":
|
|
3177
|
+
return surface.labels.some((label) => label.toLowerCase() === value);
|
|
3178
|
+
case "assignee":
|
|
3179
|
+
return surface.assignees.some((assignee) => assignee.toLowerCase() === value);
|
|
3180
|
+
case "title-contains":
|
|
3181
|
+
return surface.issue.title.toLowerCase().includes(value);
|
|
3182
|
+
}
|
|
3034
3183
|
}
|
|
3035
3184
|
function buildIssueState(issue, linkMap) {
|
|
3036
3185
|
const issueSurface = getIssueSurface(issue.number);
|
|
@@ -3053,6 +3202,13 @@ function describePlan(decisions) {
|
|
|
3053
3202
|
});
|
|
3054
3203
|
return lines.length === 0 ? " (no issues matched the discovery filter)\n" : lines.join("\n") + "\n";
|
|
3055
3204
|
}
|
|
3205
|
+
function refreshItem(item, settings, linkMap) {
|
|
3206
|
+
return decideWork(
|
|
3207
|
+
buildIssueState(item.issue, linkMap),
|
|
3208
|
+
settings.participants,
|
|
3209
|
+
settings.baseBranch
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3056
3212
|
function readAnsweringSurface(item) {
|
|
3057
3213
|
if (item.turn === "issue-discuss" || item.pr === null) {
|
|
3058
3214
|
return getIssueSurface(item.issue.number).messages;
|
|
@@ -3060,6 +3216,9 @@ function readAnsweringSurface(item) {
|
|
|
3060
3216
|
const surface = getPrSurface(item.pr.number);
|
|
3061
3217
|
return [...surface.messages, ...surface.threads.flatMap((thread) => thread.comments)];
|
|
3062
3218
|
}
|
|
3219
|
+
function markerSurfaceLabel(item) {
|
|
3220
|
+
return item.turn === "pr-work" && item.pr ? `pull request #${String(item.pr.number)}` : `issue #${String(item.issue.number)}`;
|
|
3221
|
+
}
|
|
3063
3222
|
function reconcileMarker(item, marker, agentUser, runError) {
|
|
3064
3223
|
let answered;
|
|
3065
3224
|
try {
|
|
@@ -3069,7 +3228,20 @@ function reconcileMarker(item, marker, agentUser, runError) {
|
|
|
3069
3228
|
` warning: could not re-read issue #${String(item.issue.number)} to check for an answer: ${err.message}
|
|
3070
3229
|
`
|
|
3071
3230
|
);
|
|
3072
|
-
answered =
|
|
3231
|
+
answered = "unknown";
|
|
3232
|
+
}
|
|
3233
|
+
if (answered === "unknown") {
|
|
3234
|
+
const surface2 = markerSurfaceLabel(item);
|
|
3235
|
+
try {
|
|
3236
|
+
updateMarker(
|
|
3237
|
+
marker,
|
|
3238
|
+
`automata do-work: the agent run finished, but automata could not read ${surface2} afterwards to confirm whether an answer was posted. Check this thread and the branch before assuming either. Reply here to have another attempt made.`
|
|
3239
|
+
);
|
|
3240
|
+
} catch (updateErr) {
|
|
3241
|
+
progress(` warning: could not update the marker comment: ${updateErr.message}
|
|
3242
|
+
`);
|
|
3243
|
+
}
|
|
3244
|
+
return { outcome: "answered-no-reply", detail: "could not verify whether an answer was posted" };
|
|
3073
3245
|
}
|
|
3074
3246
|
if (answered) {
|
|
3075
3247
|
try {
|
|
@@ -3080,7 +3252,8 @@ function reconcileMarker(item, marker, agentUser, runError) {
|
|
|
3080
3252
|
}
|
|
3081
3253
|
return runError === null ? { outcome: "answered", detail: "answered" } : { outcome: "answered", detail: `answered, but the run reported: ${runError.message}` };
|
|
3082
3254
|
}
|
|
3083
|
-
const
|
|
3255
|
+
const surface = markerSurfaceLabel(item);
|
|
3256
|
+
const explanation = runError === null ? `automata do-work: the agent run finished without posting an answer on ${surface}. It may still have changed the branch \`${item.branch}\` \u2014 check it before assuming otherwise. Reply on ${surface} to have another attempt made.` : `automata do-work: the agent run failed before posting an answer (${runError.message}). It may have left partial changes on the branch \`${item.branch}\`. Reply on ${surface} to have another attempt made.`;
|
|
3084
3257
|
try {
|
|
3085
3258
|
updateMarker(marker, explanation);
|
|
3086
3259
|
} catch (err) {
|
|
@@ -3094,10 +3267,10 @@ function reconcileMarker(item, marker, agentUser, runError) {
|
|
|
3094
3267
|
}
|
|
3095
3268
|
async function invokeExecutor(prompt, settings, silent) {
|
|
3096
3269
|
if (settings.executor === "codex") {
|
|
3097
|
-
|
|
3270
|
+
await runCodex(prompt, { model: settings.model });
|
|
3098
3271
|
return;
|
|
3099
3272
|
}
|
|
3100
|
-
await
|
|
3273
|
+
await runClaude(prompt, { model: settings.model, printSteps: !silent });
|
|
3101
3274
|
}
|
|
3102
3275
|
function repairIssueLink(item, baseBranch) {
|
|
3103
3276
|
try {
|
|
@@ -3126,15 +3299,26 @@ function repairIssueLink(item, baseBranch) {
|
|
|
3126
3299
|
`);
|
|
3127
3300
|
}
|
|
3128
3301
|
}
|
|
3129
|
-
async function processItem(
|
|
3302
|
+
async function processItem(planned, settings, linkMap, silent) {
|
|
3130
3303
|
const base = {
|
|
3131
|
-
issue:
|
|
3132
|
-
title:
|
|
3133
|
-
turn:
|
|
3304
|
+
issue: planned.issue.number,
|
|
3305
|
+
title: planned.issue.title,
|
|
3306
|
+
turn: planned.turn
|
|
3134
3307
|
};
|
|
3135
3308
|
progress(`
|
|
3136
|
-
#${String(
|
|
3309
|
+
#${String(planned.issue.number)} ${planned.turn}: ${planned.reason}
|
|
3310
|
+
`);
|
|
3311
|
+
const refreshed = refreshItem(planned, settings, linkMap);
|
|
3312
|
+
if (refreshed.kind === "skip") {
|
|
3313
|
+
progress(` skipped: ${refreshed.detail}
|
|
3137
3314
|
`);
|
|
3315
|
+
return { ...base, outcome: "skipped", detail: `no longer actionable: ${refreshed.detail}` };
|
|
3316
|
+
}
|
|
3317
|
+
const item = refreshed.item;
|
|
3318
|
+
if (item.turn !== planned.turn) {
|
|
3319
|
+
progress(` turn changed to ${item.turn} since the plan was built; using the current state.
|
|
3320
|
+
`);
|
|
3321
|
+
}
|
|
3138
3322
|
const prepared = item.turn === "issue-discuss" ? prepareBaseBranch(item.branch) : preparePrBranch(item.branch);
|
|
3139
3323
|
if (!prepared.ok) {
|
|
3140
3324
|
progress(` skipped: ${prepared.reason} \u2014 ${prepared.detail}
|
|
@@ -3213,7 +3397,7 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
3213
3397
|
if (shuttingDown) return;
|
|
3214
3398
|
shuttingDown = true;
|
|
3215
3399
|
progress("\nInterrupted: stopping the executor before releasing the run lock\u2026\n");
|
|
3216
|
-
void
|
|
3400
|
+
void terminateTrackedChildren().then(() => {
|
|
3217
3401
|
handle.release();
|
|
3218
3402
|
process.exit(130);
|
|
3219
3403
|
});
|
|
@@ -3254,7 +3438,7 @@ ${describePlan(decisions)}`;
|
|
|
3254
3438
|
const deferred = items.slice(runnable.length);
|
|
3255
3439
|
const reports = [];
|
|
3256
3440
|
for (const item of runnable) {
|
|
3257
|
-
reports.push(await processItem(item, settings, options.silent === true));
|
|
3441
|
+
reports.push(await processItem(item, settings, linkMap, options.silent === true));
|
|
3258
3442
|
}
|
|
3259
3443
|
for (const item of deferred) {
|
|
3260
3444
|
progress(`
|
|
@@ -3279,7 +3463,7 @@ ${describePlan(decisions)}`;
|
|
|
3279
3463
|
}
|
|
3280
3464
|
function reportDryRun(items, decisions, settings, options) {
|
|
3281
3465
|
const describable = settings.maxRuns > 0 ? items.slice(0, settings.maxRuns) : items;
|
|
3282
|
-
const planned = describable.map((item) => planRun(item, settings
|
|
3466
|
+
const planned = describable.map((item) => planRun(item, settings));
|
|
3283
3467
|
if (options.json) {
|
|
3284
3468
|
out(
|
|
3285
3469
|
JSON.stringify(
|