automata-cli 0.1.0-feature-030-do-work.190 → 0.1.0-feature-030-do-work.194
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
CHANGED
|
@@ -129,14 +129,7 @@ See [docs/execute-prompt.md](docs/execute-prompt.md) for full details.
|
|
|
129
129
|
|
|
130
130
|
## `automata do-work`
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
automata do-work # one tick
|
|
136
|
-
automata do-work --dry-run # show the work plan, change nothing
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
See [docs/do-work.md](docs/do-work.md) for the command reference, and the [wiki](docs/wiki/Home.md) for the process: the trust model, the issue lifecycle, setup and operation.
|
|
132
|
+
One tick of the autonomous loop: answer the open issues whose newest message from an authorized account the agent has not answered. Reference: [docs/do-work.md](docs/do-work.md). Process, trust model and setup: [the wiki](docs/wiki/Home.md).
|
|
140
133
|
|
|
141
134
|
---
|
|
142
135
|
|
|
@@ -164,7 +164,7 @@ function ConfigWizard() {
|
|
|
164
164
|
} else if (key.backspace || key.delete) {
|
|
165
165
|
setDoWorkMaxRuns((v) => v.slice(0, -1));
|
|
166
166
|
} else if (key.escape) {
|
|
167
|
-
setScreen("do-work-
|
|
167
|
+
setScreen("do-work-codex-model");
|
|
168
168
|
} else if (key.ctrl && input === "c") {
|
|
169
169
|
exit();
|
|
170
170
|
} else if (input && !key.ctrl && !key.meta) {
|
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-GN5JC4XP.js")
|
|
187
187
|
]);
|
|
188
188
|
const { waitUntilExit } = render(React.createElement(ConfigWizard));
|
|
189
189
|
await waitUntilExit();
|
|
@@ -1483,6 +1483,10 @@ import { existsSync } from "fs";
|
|
|
1483
1483
|
import { delimiter, join } from "path";
|
|
1484
1484
|
|
|
1485
1485
|
// src/cli/spawnUtils.ts
|
|
1486
|
+
function shellQuote(arg) {
|
|
1487
|
+
if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(arg)) return arg;
|
|
1488
|
+
return `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
1489
|
+
}
|
|
1486
1490
|
function truncate(str, max) {
|
|
1487
1491
|
return str.length > max ? str.slice(0, max) + "..." : str;
|
|
1488
1492
|
}
|
|
@@ -1520,6 +1524,39 @@ function resolveCommand(name) {
|
|
|
1520
1524
|
}
|
|
1521
1525
|
return name;
|
|
1522
1526
|
}
|
|
1527
|
+
function buildClaudeArgs(prompt, options = {}) {
|
|
1528
|
+
const args = [];
|
|
1529
|
+
if (options.yolo) args.push("--dangerously-skip-permissions");
|
|
1530
|
+
if (options.model) args.push("--model", options.model);
|
|
1531
|
+
if (options.verbose) args.push("--verbose", "--output-format", "stream-json");
|
|
1532
|
+
args.push("-p", prompt);
|
|
1533
|
+
return args;
|
|
1534
|
+
}
|
|
1535
|
+
var activeChildren = /* @__PURE__ */ new Set();
|
|
1536
|
+
function terminateActiveClaudeProcesses(timeoutMs = 1e4) {
|
|
1537
|
+
const children = [...activeChildren];
|
|
1538
|
+
if (children.length === 0) return Promise.resolve();
|
|
1539
|
+
const exits = children.map(
|
|
1540
|
+
(child) => new Promise((resolve2) => {
|
|
1541
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
1542
|
+
resolve2();
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
child.once("exit", () => resolve2());
|
|
1546
|
+
child.kill("SIGTERM");
|
|
1547
|
+
})
|
|
1548
|
+
);
|
|
1549
|
+
return Promise.race([
|
|
1550
|
+
Promise.all(exits).then(() => void 0),
|
|
1551
|
+
new Promise((resolve2) => {
|
|
1552
|
+
const timer = setTimeout(() => {
|
|
1553
|
+
for (const child of children) child.kill("SIGKILL");
|
|
1554
|
+
resolve2();
|
|
1555
|
+
}, timeoutMs);
|
|
1556
|
+
timer.unref();
|
|
1557
|
+
})
|
|
1558
|
+
]);
|
|
1559
|
+
}
|
|
1523
1560
|
function invokeClaudeCode(prompt, options = {}) {
|
|
1524
1561
|
if (options.verbose) {
|
|
1525
1562
|
return invokeClaudeCodeVerbose(prompt, options.yolo ?? false, options.model);
|
|
@@ -1528,10 +1565,7 @@ function invokeClaudeCode(prompt, options = {}) {
|
|
|
1528
1565
|
}
|
|
1529
1566
|
function invokeClaudeCodeSync(prompt, yolo, model) {
|
|
1530
1567
|
const claudeBin = resolveCommand("claude");
|
|
1531
|
-
const args =
|
|
1532
|
-
if (yolo) args.push("--dangerously-skip-permissions");
|
|
1533
|
-
if (model) args.push("--model", model);
|
|
1534
|
-
args.push("-p", prompt);
|
|
1568
|
+
const args = buildClaudeArgs(prompt, { yolo, model, verbose: false });
|
|
1535
1569
|
const result = spawnSync4(claudeBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
1536
1570
|
handleSpawnError(result.error, "claude");
|
|
1537
1571
|
handleExitCode(result.status, "Claude Code");
|
|
@@ -1539,11 +1573,9 @@ function invokeClaudeCodeSync(prompt, yolo, model) {
|
|
|
1539
1573
|
function invokeClaudeCodeVerbose(prompt, yolo, model) {
|
|
1540
1574
|
return new Promise((resolve2) => {
|
|
1541
1575
|
const claudeBin = resolveCommand("claude");
|
|
1542
|
-
const args =
|
|
1543
|
-
if (yolo) args.push("--dangerously-skip-permissions");
|
|
1544
|
-
if (model) args.push("--model", model);
|
|
1545
|
-
args.push("--verbose", "--output-format", "stream-json", "-p", prompt);
|
|
1576
|
+
const args = buildClaudeArgs(prompt, { yolo, model, verbose: true });
|
|
1546
1577
|
const child = spawn(claudeBin, args, { stdio: ["inherit", "pipe", "inherit"] });
|
|
1578
|
+
activeChildren.add(child);
|
|
1547
1579
|
const rl = createInterface({ input: child.stdout });
|
|
1548
1580
|
let turnCount = 0;
|
|
1549
1581
|
child.on("error", (err) => {
|
|
@@ -1558,6 +1590,7 @@ function invokeClaudeCodeVerbose(prompt, yolo, model) {
|
|
|
1558
1590
|
}
|
|
1559
1591
|
});
|
|
1560
1592
|
child.on("close", (code) => {
|
|
1593
|
+
activeChildren.delete(child);
|
|
1561
1594
|
handleExitCode(code, "Claude Code");
|
|
1562
1595
|
resolve2();
|
|
1563
1596
|
});
|
|
@@ -1629,6 +1662,13 @@ function summarizeTool(name, input) {
|
|
|
1629
1662
|
|
|
1630
1663
|
// src/codex/codexService.ts
|
|
1631
1664
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
1665
|
+
function buildCodexArgs(prompt, options = {}) {
|
|
1666
|
+
const args = ["exec"];
|
|
1667
|
+
if (options.yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
1668
|
+
if (options.model) args.push("--model", options.model);
|
|
1669
|
+
args.push(prompt);
|
|
1670
|
+
return args;
|
|
1671
|
+
}
|
|
1632
1672
|
function invokeCodexCode(prompt, options = {}) {
|
|
1633
1673
|
if (options.verbose) {
|
|
1634
1674
|
process.stderr.write("Warning: --verbose is not supported for Codex and will be ignored.\n");
|
|
@@ -1637,10 +1677,7 @@ function invokeCodexCode(prompt, options = {}) {
|
|
|
1637
1677
|
}
|
|
1638
1678
|
function invokeCodexCodeSync(prompt, yolo, model) {
|
|
1639
1679
|
const codexBin = resolveCommand("codex");
|
|
1640
|
-
const args =
|
|
1641
|
-
if (yolo) args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
1642
|
-
if (model) args.push("--model", model);
|
|
1643
|
-
args.push(prompt);
|
|
1680
|
+
const args = buildCodexArgs(prompt, { yolo, model });
|
|
1644
1681
|
const result = spawnSync5(codexBin, args, { encoding: "utf8", stdio: "inherit" });
|
|
1645
1682
|
handleSpawnError(result.error, "codex");
|
|
1646
1683
|
handleExitCode(result.status, "Codex");
|
|
@@ -2301,42 +2338,64 @@ function getIssueSurface(issueNumber) {
|
|
|
2301
2338
|
};
|
|
2302
2339
|
}
|
|
2303
2340
|
var LINK_MAP_QUERY = `
|
|
2304
|
-
query($owner:String!,$repo:String
|
|
2341
|
+
query($owner:String!,$repo:String!,$cursor:String){
|
|
2305
2342
|
repository(owner:$owner,name:$repo){
|
|
2306
|
-
pullRequests(states:OPEN, first:100, orderBy:{field:UPDATED_AT, direction:DESC}){
|
|
2343
|
+
pullRequests(states:OPEN, first:100, after:$cursor, orderBy:{field:UPDATED_AT, direction:DESC}){
|
|
2344
|
+
pageInfo{ hasNextPage endCursor }
|
|
2307
2345
|
nodes{
|
|
2308
2346
|
number url title headRefName isDraft updatedAt
|
|
2309
|
-
closingIssuesReferences(first:
|
|
2347
|
+
closingIssuesReferences(first:50){
|
|
2348
|
+
pageInfo{ hasNextPage }
|
|
2349
|
+
nodes{ number }
|
|
2350
|
+
}
|
|
2310
2351
|
}
|
|
2311
2352
|
}
|
|
2312
2353
|
}
|
|
2313
2354
|
}`.trim();
|
|
2355
|
+
var MAX_LINK_MAP_PAGES = 50;
|
|
2314
2356
|
function getOpenPrLinkMap() {
|
|
2315
2357
|
const { owner, repo } = getRepoSlug();
|
|
2316
|
-
const response = ghJson(
|
|
2317
|
-
["api", "graphql", "-f", `query=${LINK_MAP_QUERY}`, "-f", `owner=${owner}`, "-f", `repo=${repo}`],
|
|
2318
|
-
"query open pull requests"
|
|
2319
|
-
);
|
|
2320
2358
|
const map = /* @__PURE__ */ new Map();
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2359
|
+
let cursor = null;
|
|
2360
|
+
for (let page = 0; page < MAX_LINK_MAP_PAGES; page++) {
|
|
2361
|
+
const args = ["api", "graphql", "-f", `query=${LINK_MAP_QUERY}`, "-f", `owner=${owner}`, "-f", `repo=${repo}`];
|
|
2362
|
+
if (cursor !== null) args.push("-f", `cursor=${cursor}`);
|
|
2363
|
+
const response = ghJson(args, "query open pull requests");
|
|
2364
|
+
const connection = response.data.repository.pullRequests;
|
|
2365
|
+
for (const node of connection.nodes) {
|
|
2366
|
+
const ref = {
|
|
2367
|
+
number: node.number,
|
|
2368
|
+
url: node.url,
|
|
2369
|
+
title: node.title,
|
|
2370
|
+
headRefName: node.headRefName,
|
|
2371
|
+
state: "OPEN",
|
|
2372
|
+
isDraft: node.isDraft,
|
|
2373
|
+
updatedAt: node.updatedAt
|
|
2374
|
+
};
|
|
2375
|
+
if (node.closingIssuesReferences.pageInfo?.hasNextPage) {
|
|
2376
|
+
process.stderr.write(
|
|
2377
|
+
`Warning: pull request #${String(node.number)} closes more than 50 issues; some links were not read.
|
|
2378
|
+
`
|
|
2379
|
+
);
|
|
2337
2380
|
}
|
|
2381
|
+
for (const issue of node.closingIssuesReferences.nodes) {
|
|
2382
|
+
const existing = map.get(issue.number);
|
|
2383
|
+
if (existing) {
|
|
2384
|
+
existing.push(ref);
|
|
2385
|
+
} else {
|
|
2386
|
+
map.set(issue.number, [ref]);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
if (!connection.pageInfo?.hasNextPage || connection.pageInfo.endCursor === null) {
|
|
2391
|
+
return map;
|
|
2338
2392
|
}
|
|
2393
|
+
cursor = connection.pageInfo.endCursor;
|
|
2339
2394
|
}
|
|
2395
|
+
process.stderr.write(
|
|
2396
|
+
`Warning: stopped paginating open pull requests after ${String(MAX_LINK_MAP_PAGES)} pages; the issue-to-pull-request map may be incomplete.
|
|
2397
|
+
`
|
|
2398
|
+
);
|
|
2340
2399
|
return map;
|
|
2341
2400
|
}
|
|
2342
2401
|
var REVIEW_THREADS_QUERY2 = `
|
|
@@ -2491,9 +2550,19 @@ function isAssignedToAgent(assignees, agentUser) {
|
|
|
2491
2550
|
return assignees.some((name) => name.toLowerCase() === agent);
|
|
2492
2551
|
}
|
|
2493
2552
|
function findActionableThreads(threads, p) {
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2553
|
+
const actionable = [];
|
|
2554
|
+
for (const thread of threads) {
|
|
2555
|
+
if (thread.isResolved) continue;
|
|
2556
|
+
const comments = thread.comments.filter((comment) => classifyForThread(comment.author, p) !== "other");
|
|
2557
|
+
if (lastAuthorClass(comments, p) !== "authorized") continue;
|
|
2558
|
+
actionable.push({ ...thread, comments });
|
|
2559
|
+
}
|
|
2560
|
+
return actionable;
|
|
2561
|
+
}
|
|
2562
|
+
function classifyForThread(author, p) {
|
|
2563
|
+
const login2 = author.toLowerCase();
|
|
2564
|
+
if (login2 === p.agentUser.toLowerCase()) return "agent";
|
|
2565
|
+
return p.allowedUsers.some((user) => user.toLowerCase() === login2) ? "authorized" : "other";
|
|
2497
2566
|
}
|
|
2498
2567
|
function newestPr(prs) {
|
|
2499
2568
|
return [...prs].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
|
|
@@ -2700,6 +2769,7 @@ function preparePrBranch(headRefName) {
|
|
|
2700
2769
|
|
|
2701
2770
|
// src/run/runLock.ts
|
|
2702
2771
|
import { writeFileSync, readFileSync as readFileSync3, unlinkSync, mkdirSync } from "fs";
|
|
2772
|
+
import { randomUUID } from "crypto";
|
|
2703
2773
|
import { hostname } from "os";
|
|
2704
2774
|
import { join as join2 } from "path";
|
|
2705
2775
|
var LOCK_DIR = ".automata";
|
|
@@ -2723,7 +2793,8 @@ function readOwner(path) {
|
|
|
2723
2793
|
pid: parsed.pid,
|
|
2724
2794
|
startedAt: parsed.startedAt,
|
|
2725
2795
|
host: parsed.host ?? "unknown",
|
|
2726
|
-
command: parsed.command ?? "unknown"
|
|
2796
|
+
command: parsed.command ?? "unknown",
|
|
2797
|
+
token: parsed.token ?? ""
|
|
2727
2798
|
};
|
|
2728
2799
|
} catch {
|
|
2729
2800
|
return null;
|
|
@@ -2731,17 +2802,23 @@ function readOwner(path) {
|
|
|
2731
2802
|
}
|
|
2732
2803
|
function isStale(owner, staleMinutes) {
|
|
2733
2804
|
if (owner === null) return true;
|
|
2734
|
-
if (owner.host === hostname()
|
|
2805
|
+
if (owner.host === hostname()) {
|
|
2806
|
+
return !isAlive(owner.pid);
|
|
2807
|
+
}
|
|
2735
2808
|
const startedAt = Date.parse(owner.startedAt);
|
|
2736
2809
|
if (Number.isNaN(startedAt)) return true;
|
|
2737
2810
|
return Date.now() - startedAt > staleMinutes * 60 * 1e3;
|
|
2738
2811
|
}
|
|
2739
|
-
function makeHandle(path) {
|
|
2812
|
+
function makeHandle(path, token) {
|
|
2740
2813
|
let released = false;
|
|
2741
2814
|
return {
|
|
2742
2815
|
release() {
|
|
2743
2816
|
if (released) return;
|
|
2744
2817
|
released = true;
|
|
2818
|
+
const current = readOwner(path);
|
|
2819
|
+
if (current !== null && current.token !== token) {
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2745
2822
|
try {
|
|
2746
2823
|
unlinkSync(path);
|
|
2747
2824
|
} catch {
|
|
@@ -2749,21 +2826,23 @@ function makeHandle(path) {
|
|
|
2749
2826
|
}
|
|
2750
2827
|
};
|
|
2751
2828
|
}
|
|
2752
|
-
function write(path, command) {
|
|
2829
|
+
function write(path, command, token) {
|
|
2753
2830
|
const owner = {
|
|
2754
2831
|
pid: process.pid,
|
|
2755
2832
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2756
2833
|
host: hostname(),
|
|
2757
|
-
command
|
|
2834
|
+
command,
|
|
2835
|
+
token
|
|
2758
2836
|
};
|
|
2759
2837
|
writeFileSync(path, JSON.stringify(owner, null, 2) + "\n", { encoding: "utf8", flag: "wx" });
|
|
2760
2838
|
}
|
|
2761
2839
|
function acquireRunLock(command, staleMinutes) {
|
|
2762
2840
|
const path = lockPath();
|
|
2841
|
+
const token = randomUUID();
|
|
2763
2842
|
mkdirSync(join2(process.cwd(), LOCK_DIR), { recursive: true });
|
|
2764
2843
|
try {
|
|
2765
|
-
write(path, command);
|
|
2766
|
-
return { ok: true, handle: makeHandle(path) };
|
|
2844
|
+
write(path, command, token);
|
|
2845
|
+
return { ok: true, handle: makeHandle(path, token) };
|
|
2767
2846
|
} catch (err) {
|
|
2768
2847
|
if (err.code !== "EEXIST") throw err;
|
|
2769
2848
|
}
|
|
@@ -2776,14 +2855,20 @@ function acquireRunLock(command, staleMinutes) {
|
|
|
2776
2855
|
} catch {
|
|
2777
2856
|
}
|
|
2778
2857
|
try {
|
|
2779
|
-
write(path, command);
|
|
2780
|
-
return { ok: true, handle: makeHandle(path) };
|
|
2858
|
+
write(path, command, token);
|
|
2859
|
+
return { ok: true, handle: makeHandle(path, token) };
|
|
2781
2860
|
} catch (err) {
|
|
2782
2861
|
if (err.code !== "EEXIST") throw err;
|
|
2783
2862
|
const winner = readOwner(path);
|
|
2784
2863
|
return {
|
|
2785
2864
|
ok: false,
|
|
2786
|
-
heldBy: winner ?? {
|
|
2865
|
+
heldBy: winner ?? {
|
|
2866
|
+
pid: 0,
|
|
2867
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2868
|
+
host: "unknown",
|
|
2869
|
+
command: "unknown",
|
|
2870
|
+
token: ""
|
|
2871
|
+
}
|
|
2787
2872
|
};
|
|
2788
2873
|
}
|
|
2789
2874
|
}
|
|
@@ -2876,11 +2961,49 @@ function checkAuthenticatedIdentity(agentUser, allowedUsers) {
|
|
|
2876
2961
|
`\`gh\` is authenticated as "${login2}", which is listed in allowedUsers. Everything do-work posts would be attributed to an account that is allowed to instruct the agent, so its own marker comment would look like a new instruction and each tick would answer the previous tick forever. Authenticate \`gh\` as the agent account (${agentUser}) in this environment, or correct \`agentUser\`.`
|
|
2877
2962
|
);
|
|
2878
2963
|
}
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
`
|
|
2964
|
+
fail(
|
|
2965
|
+
`\`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\`.`
|
|
2882
2966
|
);
|
|
2883
2967
|
}
|
|
2968
|
+
function planRun(item, settings, silent) {
|
|
2969
|
+
const prompt = composePrompt({
|
|
2970
|
+
item,
|
|
2971
|
+
repo: getRepoSlug(),
|
|
2972
|
+
agentUser: settings.participants.agentUser,
|
|
2973
|
+
baseBranch: settings.baseBranch,
|
|
2974
|
+
frame: settings.prompts[item.turn]
|
|
2975
|
+
});
|
|
2976
|
+
const bin = resolveCommand(settings.executor === "codex" ? "codex" : "claude");
|
|
2977
|
+
const args = settings.executor === "codex" ? buildCodexArgs(prompt, { yolo: true, model: settings.model }) : buildClaudeArgs(prompt, { yolo: true, verbose: !silent, model: settings.model });
|
|
2978
|
+
return { prompt, bin, args, command: [bin, ...args].map(shellQuote).join(" ") };
|
|
2979
|
+
}
|
|
2980
|
+
function describePlannedRun(item, settings, run5) {
|
|
2981
|
+
const rule = "\u2500".repeat(72);
|
|
2982
|
+
const lines = [
|
|
2983
|
+
rule,
|
|
2984
|
+
`Issue #${String(item.issue.number)} \u2014 ${item.issue.title}`,
|
|
2985
|
+
rule,
|
|
2986
|
+
` Turn ${item.turn}`,
|
|
2987
|
+
` Why ${item.reason}`,
|
|
2988
|
+
` Branch ${item.branch} (would check out${item.turn === "pr-work" ? " and fast-forward" : " and pull"})`,
|
|
2989
|
+
` Assign ${item.needsAssignment ? `would assign to ${settings.participants.agentUser}` : "already assigned"}`,
|
|
2990
|
+
` Marker would post on ${item.turn === "pr-work" && item.pr ? `pull request #${String(item.pr.number)}` : `issue #${String(item.issue.number)}`}`,
|
|
2991
|
+
` Executor ${settings.executor}${settings.model === void 0 ? " (no model override)" : ` \xB7 model ${settings.model}`}`,
|
|
2992
|
+
` Permissions bypassed (do-work always runs unattended)`,
|
|
2993
|
+
` Prompt ${String(run5.prompt.length)} chars \u2014 frame + assembled context`,
|
|
2994
|
+
"",
|
|
2995
|
+
" Command that would be launched:",
|
|
2996
|
+
// Printed flush-left and unindented on purpose: the prompt is a multi-line
|
|
2997
|
+
// quoted argument, so indenting the continuation lines would inject leading
|
|
2998
|
+
// whitespace into the prompt itself and the command would no longer be the
|
|
2999
|
+
// one that runs.
|
|
3000
|
+
rule,
|
|
3001
|
+
run5.command,
|
|
3002
|
+
rule,
|
|
3003
|
+
""
|
|
3004
|
+
];
|
|
3005
|
+
return lines.join("\n") + "\n";
|
|
3006
|
+
}
|
|
2884
3007
|
function discoverIssues(settings) {
|
|
2885
3008
|
const candidates = listCandidateIssues(settings.technique, settings.discoveryValue, settings.limit);
|
|
2886
3009
|
if (settings.onlyIssue === void 0) {
|
|
@@ -2967,8 +3090,14 @@ async function invokeExecutor(prompt, settings, silent) {
|
|
|
2967
3090
|
}
|
|
2968
3091
|
await invokeClaudeCode(prompt, { yolo: true, verbose: !silent, model: settings.model });
|
|
2969
3092
|
}
|
|
2970
|
-
function repairIssueLink(item) {
|
|
3093
|
+
function repairIssueLink(item, baseBranch) {
|
|
2971
3094
|
try {
|
|
3095
|
+
const branch = getCurrentBranch();
|
|
3096
|
+
if (branch === baseBranch) {
|
|
3097
|
+
progress(` issue #${String(item.issue.number)} is still in discussion (no branch was created).
|
|
3098
|
+
`);
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
2972
3101
|
const pr = getCurrentBranchPr();
|
|
2973
3102
|
if (!pr) {
|
|
2974
3103
|
progress(` issue #${String(item.issue.number)} is still in discussion (no pull request).
|
|
@@ -3039,7 +3168,7 @@ async function processItem(item, settings, silent) {
|
|
|
3039
3168
|
progress(` ${reconciled.detail}
|
|
3040
3169
|
`);
|
|
3041
3170
|
if (item.turn === "issue-discuss") {
|
|
3042
|
-
repairIssueLink(item);
|
|
3171
|
+
repairIssueLink(item, settings.baseBranch);
|
|
3043
3172
|
}
|
|
3044
3173
|
return { ...base, outcome: reconciled.outcome, detail: reconciled.detail };
|
|
3045
3174
|
}
|
|
@@ -3056,7 +3185,10 @@ function summarize(reports) {
|
|
|
3056
3185
|
}
|
|
3057
3186
|
var doWorkCommand = new Command6("do-work").description(
|
|
3058
3187
|
"Run one tick of the autonomous loop: find the issues whose newest authorized message the agent has not answered, and answer them"
|
|
3059
|
-
).option("--with <executor>", "Executor to use: claude or codex (default: from config, else claude)").option("--model <string>", "Model identifier to pass to the executor, overriding the configured default for it").option("--issue <number>", "Restrict the tick to a single issue").option("--limit <n>", "Maximum number of issues to fetch", "10").option("--max-runs <n>", "Maximum number of model runs this tick").option(
|
|
3188
|
+
).option("--with <executor>", "Executor to use: claude or codex (default: from config, else claude)").option("--model <string>", "Model identifier to pass to the executor, overriding the configured default for it").option("--issue <number>", "Restrict the tick to a single issue").option("--limit <n>", "Maximum number of issues to fetch", "10").option("--max-runs <n>", "Maximum number of model runs this tick").option(
|
|
3189
|
+
"--dry-run",
|
|
3190
|
+
"Print the work plan, plus a summary and the exact command that would be launched for each item, and exit without changing anything"
|
|
3191
|
+
).option("--json", "Emit the work plan and outcomes as JSON on stdout").option("--silent", "Suppress step-by-step Claude output; show only the final summary").action(async (options) => {
|
|
3060
3192
|
const settings = resolveSettings(options);
|
|
3061
3193
|
const lock = acquireRunLock("do-work", settings.lockStaleMinutes);
|
|
3062
3194
|
if (!lock.ok) {
|
|
@@ -3067,9 +3199,15 @@ var doWorkCommand = new Command6("do-work").description(
|
|
|
3067
3199
|
return;
|
|
3068
3200
|
}
|
|
3069
3201
|
const handle = lock.handle;
|
|
3202
|
+
let shuttingDown = false;
|
|
3070
3203
|
const onSignal = () => {
|
|
3071
|
-
|
|
3072
|
-
|
|
3204
|
+
if (shuttingDown) return;
|
|
3205
|
+
shuttingDown = true;
|
|
3206
|
+
progress("\nInterrupted: stopping the executor before releasing the run lock\u2026\n");
|
|
3207
|
+
void terminateActiveClaudeProcesses().then(() => {
|
|
3208
|
+
handle.release();
|
|
3209
|
+
process.exit(130);
|
|
3210
|
+
});
|
|
3073
3211
|
};
|
|
3074
3212
|
process.once("SIGINT", onSignal);
|
|
3075
3213
|
process.once("SIGTERM", onSignal);
|
|
@@ -3100,9 +3238,40 @@ ${describePlan(decisions)}`;
|
|
|
3100
3238
|
out(planText);
|
|
3101
3239
|
}
|
|
3102
3240
|
if (options.dryRun) {
|
|
3241
|
+
const runnableInPlan = settings.maxRuns > 0 ? items.slice(0, settings.maxRuns) : items;
|
|
3242
|
+
const planned = runnableInPlan.map((item) => planRun(item, settings, options.silent === true));
|
|
3103
3243
|
if (options.json) {
|
|
3104
|
-
out(
|
|
3244
|
+
out(
|
|
3245
|
+
JSON.stringify(
|
|
3246
|
+
{
|
|
3247
|
+
dryRun: true,
|
|
3248
|
+
plan: decisions.map(toPlanJson),
|
|
3249
|
+
runs: planned.map((run5, index) => ({
|
|
3250
|
+
issue: runnableInPlan[index].issue.number,
|
|
3251
|
+
turn: runnableInPlan[index].turn,
|
|
3252
|
+
executor: settings.executor,
|
|
3253
|
+
model: settings.model ?? null,
|
|
3254
|
+
bin: run5.bin,
|
|
3255
|
+
args: run5.args,
|
|
3256
|
+
command: run5.command,
|
|
3257
|
+
prompt: run5.prompt
|
|
3258
|
+
}))
|
|
3259
|
+
},
|
|
3260
|
+
null,
|
|
3261
|
+
2
|
|
3262
|
+
) + "\n"
|
|
3263
|
+
);
|
|
3264
|
+
return 0;
|
|
3265
|
+
}
|
|
3266
|
+
for (const [index, run5] of planned.entries()) {
|
|
3267
|
+
out("\n" + describePlannedRun(runnableInPlan[index], settings, run5));
|
|
3268
|
+
}
|
|
3269
|
+
if (items.length > runnableInPlan.length) {
|
|
3270
|
+
out(`
|
|
3271
|
+
(${String(items.length - runnableInPlan.length)} further item(s) deferred by the run cap.)
|
|
3272
|
+
`);
|
|
3105
3273
|
}
|
|
3274
|
+
out("\nDry run: nothing was assigned, posted, checked out or executed.\n");
|
|
3106
3275
|
return 0;
|
|
3107
3276
|
}
|
|
3108
3277
|
const runnable = settings.maxRuns > 0 ? items.slice(0, settings.maxRuns) : items;
|