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 parsedMaxRuns = Number.parseInt(doWorkMaxRuns, 10);
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.isNaN(parsedMaxRuns) || parsedMaxRuns < 0 ? void 0 : parsedMaxRuns
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-NTK7NLAE.js")
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 { stdout } = run2("git", ["status", "--porcelain"]);
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/claude/claudeService.ts
1521
- function resolveCommand(name) {
1522
- const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
1523
- for (const dir of pathDirs) {
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 buildClaudeArgs(prompt, options = {}) {
1530
- const args = [];
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
- var activeChildren = /* @__PURE__ */ new Set();
1538
- function terminateActiveClaudeProcesses(timeoutMs = 1e4) {
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
- process.stderr.write(
2370
- `Warning: pull request #${String(node.number)} closes more than 50 issues; some links were not read.
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
- process.stderr.write(
2401
- `Warning: stopped paginating open pull requests after ${String(MAX_LINK_MAP_PAGES)} pages; the issue-to-pull-request map may be incomplete.
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 { owner, repo } = getRepoSlug();
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: "wx" });
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
- unlinkSync(path);
2860
- } catch {
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
- try {
2863
- write(path, command, token);
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 parsed = Number.parseInt(value, 10);
2895
- if (Number.isNaN(parsed) || parsed <= 0) {
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
- checkAuthenticatedIdentity(agentUser, allowedUsers);
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, silent) {
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: !silent, model: settings.model });
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
- progress(
3030
- `Note: issue #${String(settings.onlyIssue)} does not match the configured discovery filter (${settings.technique} = ${settings.discoveryValue}); processing it anyway because --issue was given.
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
- return [getIssueSurface(settings.onlyIssue).issue];
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 = false;
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 explanation = runError === null ? "automata do-work: the agent run finished without posting an answer here. Nothing was changed on your behalf. Reply on this issue to have another attempt made." : `automata do-work: the agent run failed before posting an answer (${runError.message}). Reply on this issue to have another attempt made.`;
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
- invokeCodexCode(prompt, { yolo: true, model: settings.model });
3270
+ await runCodex(prompt, { model: settings.model });
3098
3271
  return;
3099
3272
  }
3100
- await invokeClaudeCode(prompt, { yolo: true, verbose: !silent, model: settings.model });
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(item, settings, silent) {
3302
+ async function processItem(planned, settings, linkMap, silent) {
3130
3303
  const base = {
3131
- issue: item.issue.number,
3132
- title: item.issue.title,
3133
- turn: item.turn
3304
+ issue: planned.issue.number,
3305
+ title: planned.issue.title,
3306
+ turn: planned.turn
3134
3307
  };
3135
3308
  progress(`
3136
- #${String(item.issue.number)} ${item.turn}: ${item.reason}
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 terminateActiveClaudeProcesses().then(() => {
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, options.silent === true));
3466
+ const planned = describable.map((item) => planRun(item, settings));
3283
3467
  if (options.json) {
3284
3468
  out(
3285
3469
  JSON.stringify(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.1.0-feature-030-do-work.198",
3
+ "version": "0.1.0-feature-030-do-work.202",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {