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
- Run one tick of the autonomous loop: find the open issues whose newest message from an authorized account the agent has not answered on the issue or on its pull request — and answer them. Designed to run from cron in a disposable VM or container.
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-executor");
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-SS7OOS5T.js")
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 = ["exec"];
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:10){ nodes{ number } }
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
- for (const node of response.data.repository.pullRequests.nodes) {
2322
- const ref = {
2323
- number: node.number,
2324
- url: node.url,
2325
- title: node.title,
2326
- headRefName: node.headRefName,
2327
- state: "OPEN",
2328
- isDraft: node.isDraft,
2329
- updatedAt: node.updatedAt
2330
- };
2331
- for (const issue of node.closingIssuesReferences.nodes) {
2332
- const existing = map.get(issue.number);
2333
- if (existing) {
2334
- existing.push(ref);
2335
- } else {
2336
- map.set(issue.number, [ref]);
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
- return threads.filter(
2495
- (thread) => !thread.isResolved && lastAuthorClass(thread.comments, p) === "authorized"
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() && !isAlive(owner.pid)) return true;
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 ?? { pid: 0, startedAt: (/* @__PURE__ */ new Date()).toISOString(), host: "unknown", command: "unknown" }
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
- progress(
2880
- `Warning: \`gh\` is authenticated as "${login2}" but agentUser is "${agentUser}". The agent will not recognise its own messages and may repeat itself. Authenticate as the agent account, or correct \`agentUser\`.
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("--dry-run", "Print the work plan and exit without changing anything").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) => {
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
- handle.release();
3072
- process.exit(130);
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(JSON.stringify({ dryRun: true, plan: decisions.map(toPlanJson) }, null, 2) + "\n");
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.1.0-feature-030-do-work.190",
3
+ "version": "0.1.0-feature-030-do-work.194",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {