automata-cli 0.6.0-develop.225 → 0.6.0-develop.231

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.
@@ -130,8 +130,8 @@ function ConfigWizard() {
130
130
  const initialTechIndex = TECHNIQUE_OPTIONS.findIndex((o) => o.value === existing.issueDiscoveryTechnique);
131
131
  const [screen, setScreen] = useState("main");
132
132
  const [mainMenuIndex, setMainMenuIndex] = useState(0);
133
- const [selectedRemoteIndex, setSelectedRemoteIndex] = useState(initialRemoteIndex >= 0 ? initialRemoteIndex : 0);
134
- const [selectedTechIndex, setSelectedTechIndex] = useState(initialTechIndex >= 0 ? initialTechIndex : 0);
133
+ const [selectedRemoteIndex, setSelectedRemoteIndex] = useState(Math.max(initialRemoteIndex, 0));
134
+ const [selectedTechIndex, setSelectedTechIndex] = useState(Math.max(initialTechIndex, 0));
135
135
  const [discoveryValue, setDiscoveryValue] = useState(existing.issueDiscoveryValue ?? "");
136
136
  const [systemPrompt, setSystemPrompt] = useState(existing.claudeSystemPrompt ?? "");
137
137
  const [promptsMenuIndex, setPromptsMenuIndex] = useState(0);
package/dist/index.js CHANGED
@@ -194,7 +194,7 @@ var configCommand = new Command("config").description("Configure automata settin
194
194
  const [{ render }, React, { ConfigWizard }] = await Promise.all([
195
195
  import("ink"),
196
196
  import("react"),
197
- import("./ConfigWizard-5B5AOGUU.js")
197
+ import("./ConfigWizard-J67TY22Z.js")
198
198
  ]);
199
199
  const { waitUntilExit } = render(React.createElement(ConfigWizard));
200
200
  await waitUntilExit();
@@ -276,15 +276,15 @@ function parseOwnerRepo() {
276
276
  const { stdout, status } = run2("git", ["remote", "get-url", "origin"]);
277
277
  if (status !== 0) return null;
278
278
  const url = stdout.trim();
279
- const https = url.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
279
+ const https = /github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
280
280
  if (https) return https[1];
281
- const ssh = url.match(/github\.com:([^/]+\/[^/]+?)(?:\.git)?$/);
281
+ const ssh = /github\.com:([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
282
282
  if (ssh) return ssh[1];
283
283
  return null;
284
284
  }
285
285
  function extractLastMarkdownUrl(markdown) {
286
286
  const matches = [...markdown.matchAll(/\]\((https?:\/\/[^)]+)\)/g)];
287
- return matches.length > 0 ? matches[matches.length - 1][1] ?? null : null;
287
+ return matches.at(-1)?.[1] ?? null;
288
288
  }
289
289
  function fetchCheckRunOutputs(ownerRepo, sha) {
290
290
  const { stdout, status } = run2("gh", [
@@ -366,12 +366,14 @@ function resolveSonarPath(componentKey, components) {
366
366
  const path = componentKey.slice(separatorIndex + 1).trim();
367
367
  return path || void 0;
368
368
  }
369
- function mapSonarIssuesPage(data, targetIssues, components, rules) {
369
+ function collectSonarComponents(data, components) {
370
370
  for (const component of data.components ?? []) {
371
371
  if (component.key && component.path) {
372
372
  components.set(component.key, component.path);
373
373
  }
374
374
  }
375
+ }
376
+ function collectSonarRules(data, rules) {
375
377
  for (const rule of data.rules ?? []) {
376
378
  if (!rule.key) continue;
377
379
  const explanation = normalizeText(rule.htmlDesc) ?? normalizeText(rule.htmlNote) ?? normalizeText(rule.name);
@@ -379,6 +381,10 @@ function mapSonarIssuesPage(data, targetIssues, components, rules) {
379
381
  rules.set(rule.key, explanation);
380
382
  }
381
383
  }
384
+ }
385
+ function mapSonarIssuesPage(data, targetIssues, components, rules) {
386
+ collectSonarComponents(data, components);
387
+ collectSonarRules(data, rules);
382
388
  for (const issue of data.issues ?? []) {
383
389
  if (!issue.key || !issue.message) continue;
384
390
  const ruleKey = issue.rule;
@@ -396,11 +402,7 @@ function mapSonarIssuesPage(data, targetIssues, components, rules) {
396
402
  }
397
403
  }
398
404
  function mapSonarHotspotsPage(data, targetHotspots, components) {
399
- for (const component of data.components ?? []) {
400
- if (component.key && component.path) {
401
- components.set(component.key, component.path);
402
- }
403
- }
405
+ collectSonarComponents(data, components);
404
406
  targetHotspots.push(...data.hotspots ?? []);
405
407
  }
406
408
  function mapSonarHotspot(hotspot, components, detail) {
@@ -625,6 +627,32 @@ async function fetchSonarFailureSummary(projectKey, prNumber, sonarcloudUrl) {
625
627
  issueNote
626
628
  };
627
629
  }
630
+ async function describeSonarCheck(sonarCheck, prNumber) {
631
+ const sonarcloudUrl = resolveSonarPullRequestUrl(sonarCheck.detailsUrl, prNumber);
632
+ const projectKey = extractSonarProjectKey(sonarcloudUrl);
633
+ if (!projectKey) {
634
+ return {
635
+ sonarcloudUrl,
636
+ sonarNewIssues: null,
637
+ sonarNewIssuesNote: "Could not determine the SonarCloud project key from the check URL."
638
+ };
639
+ }
640
+ if (sonarCheck.conclusion !== null && SONAR_FAIL_CONCLUSIONS.has(sonarCheck.conclusion)) {
641
+ const sonarSummary = await fetchSonarFailureSummary(projectKey, prNumber, sonarcloudUrl);
642
+ return {
643
+ sonarcloudUrl,
644
+ sonarNewIssues: sonarSummary.issueTotal,
645
+ sonarNewIssuesNote: sonarSummary.issueNote,
646
+ sonarFailures: sonarSummary.summary
647
+ };
648
+ }
649
+ const sonarNewIssuesResult = await fetchSonarNewIssues(projectKey, prNumber, sonarcloudUrl);
650
+ return {
651
+ sonarcloudUrl,
652
+ sonarNewIssues: sonarNewIssuesResult.total,
653
+ sonarNewIssuesNote: sonarNewIssuesResult.note
654
+ };
655
+ }
628
656
  async function getPrInfoGh(branch) {
629
657
  const { stdout, stderr, status } = run2("gh", [
630
658
  "pr",
@@ -660,37 +688,19 @@ async function getPrInfoGh(branch) {
660
688
  };
661
689
  });
662
690
  const sonarCheck = checks.find((c) => isSonarUrl(c.detailsUrl));
663
- let sonarcloudUrl;
664
- let sonarNewIssues;
665
- let sonarNewIssuesNote;
666
- let sonarFailures;
667
- if (sonarCheck) {
668
- sonarcloudUrl = resolveSonarPullRequestUrl(sonarCheck.detailsUrl, raw.number);
669
- const projectKey = extractSonarProjectKey(sonarcloudUrl);
670
- if (projectKey) {
671
- if (sonarCheck.conclusion !== null && SONAR_FAIL_CONCLUSIONS.has(sonarCheck.conclusion)) {
672
- const sonarSummary = await fetchSonarFailureSummary(projectKey, raw.number, sonarcloudUrl);
673
- sonarFailures = sonarSummary.summary;
674
- sonarNewIssues = sonarSummary.issueTotal;
675
- sonarNewIssuesNote = sonarSummary.issueNote;
676
- } else {
677
- const sonarNewIssuesResult = await fetchSonarNewIssues(projectKey, raw.number, sonarcloudUrl);
678
- sonarNewIssues = sonarNewIssuesResult.total;
679
- sonarNewIssuesNote = sonarNewIssuesResult.note;
680
- }
681
- } else {
682
- sonarNewIssues = null;
683
- sonarNewIssuesNote = "Could not determine the SonarCloud project key from the check URL.";
684
- }
685
- }
691
+ const sonar = sonarCheck === void 0 ? void 0 : await describeSonarCheck(sonarCheck, raw.number);
686
692
  return {
687
693
  number: raw.number,
688
694
  title: raw.title,
689
695
  state: raw.state,
690
696
  url: raw.url,
691
697
  checks,
692
- ...sonarcloudUrl === void 0 ? {} : { sonarcloudUrl, sonarNewIssues, sonarNewIssuesNote },
693
- ...sonarFailures === void 0 ? {} : { sonarFailures }
698
+ ...sonar === void 0 ? {} : {
699
+ sonarcloudUrl: sonar.sonarcloudUrl,
700
+ sonarNewIssues: sonar.sonarNewIssues,
701
+ sonarNewIssuesNote: sonar.sonarNewIssuesNote
702
+ },
703
+ ...sonar?.sonarFailures === void 0 ? {} : { sonarFailures: sonar.sonarFailures }
694
704
  };
695
705
  }
696
706
  async function getPrInfo2(branch) {
@@ -1059,7 +1069,6 @@ When checks fail, details are printed in a trailing FailedChecks section.
1059
1069
  See docs/git.md for full output reference.`
1060
1070
  ).action(async (options) => {
1061
1071
  let branch;
1062
- let pr = null;
1063
1072
  try {
1064
1073
  branch = getCurrentBranch();
1065
1074
  } catch (err) {
@@ -1067,33 +1076,7 @@ See docs/git.md for full output reference.`
1067
1076
  `);
1068
1077
  process.exit(1);
1069
1078
  }
1070
- if (options.waitFinishChecks) {
1071
- while (true) {
1072
- try {
1073
- pr = await getPrInfo2(branch);
1074
- } catch (err) {
1075
- process.stderr.write(`Error: ${err.message}
1076
- `);
1077
- process.exit(1);
1078
- }
1079
- if (pr === null) {
1080
- break;
1081
- }
1082
- const running = pr.checks.filter((c) => c.status !== "COMPLETED");
1083
- if (running.length === 0) break;
1084
- process.stdout.write(`Waiting for ${running.length} check(s) to complete...
1085
- `);
1086
- await sleep(POLL_INTERVAL_MS);
1087
- }
1088
- } else {
1089
- try {
1090
- pr = await getPrInfo2(branch);
1091
- } catch (err) {
1092
- process.stderr.write(`Error: ${err.message}
1093
- `);
1094
- process.exit(1);
1095
- }
1096
- }
1079
+ const pr = options.waitFinishChecks ? await pollUntilChecksComplete(branch) : await readPrInfoOrExit(branch);
1097
1080
  if (pr === null) {
1098
1081
  process.stdout.write(`No pull request found for branch: ${branch}
1099
1082
  `);
@@ -1102,36 +1085,61 @@ See docs/git.md for full output reference.`
1102
1085
  if (options.json) {
1103
1086
  process.stdout.write(JSON.stringify(pr, null, 2) + "\n");
1104
1087
  } else {
1105
- const failed = pr.checks.filter((c) => c.conclusion !== null && FAIL_CONCLUSIONS.has(c.conclusion));
1106
- process.stdout.write(`PR: #${pr.number}
1107
- Title: ${pr.title}
1108
- State: ${pr.state}
1109
- URL: ${pr.url}
1088
+ printPrStatus(pr);
1089
+ }
1090
+ });
1091
+ async function readPrInfoOrExit(branch) {
1092
+ try {
1093
+ return await getPrInfo2(branch);
1094
+ } catch (err) {
1095
+ process.stderr.write(`Error: ${err.message}
1110
1096
  `);
1111
- if (pr.sonarcloudUrl !== void 0) {
1112
- process.stdout.write(`Sonar: ${pr.sonarcloudUrl}
1097
+ process.exit(1);
1098
+ }
1099
+ }
1100
+ async function pollUntilChecksComplete(branch) {
1101
+ for (; ; ) {
1102
+ const pr = await readPrInfoOrExit(branch);
1103
+ if (pr === null) return null;
1104
+ const running = pr.checks.filter((c) => c.status !== "COMPLETED");
1105
+ if (running.length === 0) return pr;
1106
+ process.stdout.write(`Waiting for ${running.length} check(s) to complete...
1113
1107
  `);
1114
- const issueStr = pr.sonarNewIssues === null || pr.sonarNewIssues === void 0 ? "unavailable" : String(pr.sonarNewIssues);
1115
- process.stdout.write(`Sonar New Issues: ${issueStr}
1108
+ await sleep(POLL_INTERVAL_MS);
1109
+ }
1110
+ }
1111
+ function printSonarStatus(pr) {
1112
+ if (pr.sonarcloudUrl === void 0) return;
1113
+ process.stdout.write(`Sonar: ${pr.sonarcloudUrl}
1116
1114
  `);
1117
- const failureNote = sonarFailureNote(pr.sonarFailures);
1118
- if (pr.sonarNewIssuesNote && pr.sonarNewIssuesNote !== failureNote) {
1119
- process.stdout.write(`Sonar Note: ${sanitizeText(pr.sonarNewIssuesNote)}
1115
+ const issueStr = pr.sonarNewIssues === null || pr.sonarNewIssues === void 0 ? "unavailable" : String(pr.sonarNewIssues);
1116
+ process.stdout.write(`Sonar New Issues: ${issueStr}
1117
+ `);
1118
+ const failureNote = sonarFailureNote(pr.sonarFailures);
1119
+ if (pr.sonarNewIssuesNote && pr.sonarNewIssuesNote !== failureNote) {
1120
+ process.stdout.write(`Sonar Note: ${sanitizeText(pr.sonarNewIssuesNote)}
1120
1121
  `);
1121
- }
1122
- }
1123
- process.stdout.write(formatCheckSummary(pr.checks));
1124
- process.stdout.write(formatChecks(pr.checks));
1125
- if (failed.length > 0) {
1126
- process.stdout.write(formatFailedChecks(failed));
1127
- }
1128
- if (pr.sonarFailures !== void 0) {
1129
- process.stdout.write(formatSonarFailures(pr.sonarFailures, pr.sonarcloudUrl));
1130
- }
1131
1122
  }
1132
- });
1133
- var ANSI_ESCAPE_RE = new RegExp("\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "g");
1134
- var CONTROL_CHARS_RE = new RegExp("[\0-\b\v\f-\x7F]", "g");
1123
+ }
1124
+ function printPrStatus(pr) {
1125
+ const failed = pr.checks.filter((c) => c.conclusion !== null && FAIL_CONCLUSIONS.has(c.conclusion));
1126
+ process.stdout.write(`PR: #${pr.number}
1127
+ Title: ${pr.title}
1128
+ State: ${pr.state}
1129
+ URL: ${pr.url}
1130
+ `);
1131
+ printSonarStatus(pr);
1132
+ process.stdout.write(formatCheckSummary(pr.checks));
1133
+ process.stdout.write(formatChecks(pr.checks));
1134
+ if (failed.length > 0) {
1135
+ process.stdout.write(formatFailedChecks(failed));
1136
+ }
1137
+ if (pr.sonarFailures !== void 0) {
1138
+ process.stdout.write(formatSonarFailures(pr.sonarFailures, pr.sonarcloudUrl));
1139
+ }
1140
+ }
1141
+ var ANSI_ESCAPE_RE = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
1142
+ var CONTROL_CHARS_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
1135
1143
  function sanitizeText(text) {
1136
1144
  return text.replace(ANSI_ESCAPE_RE, "").replace(CONTROL_CHARS_RE, "");
1137
1145
  }
@@ -1666,65 +1674,78 @@ function invokeClaudeCodeVerbose(prompt, yolo, model) {
1666
1674
  });
1667
1675
  });
1668
1676
  }
1677
+ function formatAssistantEvent(event, turnCount) {
1678
+ const message = event["message"];
1679
+ const content = message?.["content"];
1680
+ if (!content) return;
1681
+ for (const block of content) {
1682
+ const line = formatContentBlock(block);
1683
+ if (line !== null) {
1684
+ process.stderr.write(` [step ${turnCount + 1}] ${line}
1685
+ `);
1686
+ }
1687
+ }
1688
+ }
1689
+ function formatContentBlock(block) {
1690
+ if (block["type"] === "tool_use") {
1691
+ const toolName = block["name"];
1692
+ const input = block["input"];
1693
+ return summarizeTool(toolName, input);
1694
+ }
1695
+ if (block["type"] === "text") {
1696
+ const text = block["text"] ?? "";
1697
+ if (text.length === 0) return null;
1698
+ const preview = text.length > 120 ? text.slice(0, 120) + "..." : text;
1699
+ return preview.split("\n")[0] ?? null;
1700
+ }
1701
+ return null;
1702
+ }
1703
+ function formatResultEvent(event) {
1704
+ const result = event["result"];
1705
+ const cost = event["cost_usd"];
1706
+ const duration = event["duration_ms"];
1707
+ const turns = event["num_turns"];
1708
+ process.stderr.write("\n--- Result ---\n");
1709
+ const parts = [];
1710
+ if (turns !== void 0) parts.push(`${turns} turns`);
1711
+ if (duration !== void 0) parts.push(`${(duration / 1e3).toFixed(1)}s`);
1712
+ if (cost !== void 0) parts.push(`$${cost.toFixed(4)}`);
1713
+ if (parts.length > 0) {
1714
+ process.stderr.write(` [info] ${parts.join(" | ")}
1715
+ `);
1716
+ }
1717
+ if (result) {
1718
+ process.stdout.write(result + "\n");
1719
+ }
1720
+ }
1669
1721
  function formatEvent(event, turnCount) {
1670
1722
  const type = event["type"];
1671
1723
  if (type === "assistant") {
1672
- const message = event["message"];
1673
- const content = message?.["content"];
1674
- if (!content) return;
1675
- for (const block of content) {
1676
- if (block["type"] === "tool_use") {
1677
- const toolName = block["name"];
1678
- const input = block["input"];
1679
- const summary = summarizeTool(toolName, input);
1680
- process.stderr.write(` [step ${turnCount + 1}] ${summary}
1681
- `);
1682
- } else if (block["type"] === "text") {
1683
- const text = block["text"] ?? "";
1684
- if (text.length > 0) {
1685
- const preview = text.length > 120 ? text.slice(0, 120) + "..." : text;
1686
- const firstLine = preview.split("\n")[0];
1687
- process.stderr.write(` [step ${turnCount + 1}] ${firstLine}
1688
- `);
1689
- }
1690
- }
1691
- }
1724
+ formatAssistantEvent(event, turnCount);
1692
1725
  } else if (type === "result") {
1693
- const result = event["result"];
1694
- const cost = event["cost_usd"];
1695
- const duration = event["duration_ms"];
1696
- const turns = event["num_turns"];
1697
- process.stderr.write("\n--- Result ---\n");
1698
- if (cost !== void 0 || duration !== void 0 || turns !== void 0) {
1699
- const parts = [];
1700
- if (turns !== void 0) parts.push(`${turns} turns`);
1701
- if (duration !== void 0) parts.push(`${(duration / 1e3).toFixed(1)}s`);
1702
- if (cost !== void 0) parts.push(`$${cost.toFixed(4)}`);
1703
- process.stderr.write(` [info] ${parts.join(" | ")}
1704
- `);
1705
- }
1706
- if (result) {
1707
- process.stdout.write(result + "\n");
1708
- }
1726
+ formatResultEvent(event);
1709
1727
  }
1710
1728
  }
1729
+ function asText(value, fallback) {
1730
+ return typeof value === "string" ? value : fallback;
1731
+ }
1711
1732
  function summarizeTool(name, input) {
1712
1733
  if (!input) return `tool: ${name}`;
1713
1734
  switch (name) {
1714
1735
  case "Read":
1715
- return `reading ${input["file_path"] ?? "file"}`;
1736
+ return `reading ${asText(input["file_path"], "file")}`;
1716
1737
  case "Write":
1717
- return `writing ${input["file_path"] ?? "file"}`;
1738
+ return `writing ${asText(input["file_path"], "file")}`;
1718
1739
  case "Edit":
1719
- return `editing ${input["file_path"] ?? "file"}`;
1740
+ return `editing ${asText(input["file_path"], "file")}`;
1720
1741
  case "Bash":
1721
- return `running: ${truncate(String(input["command"] ?? ""), 80)}`;
1742
+ return `running: ${truncate(asText(input["command"], ""), 80)}`;
1722
1743
  case "Glob":
1723
- return `searching files: ${input["pattern"] ?? ""}`;
1744
+ return `searching files: ${asText(input["pattern"], "")}`;
1724
1745
  case "Grep":
1725
- return `searching content: ${truncate(String(input["pattern"] ?? ""), 60)}`;
1746
+ return `searching content: ${truncate(asText(input["pattern"], ""), 60)}`;
1726
1747
  case "Agent":
1727
- return `spawning agent: ${input["description"] ?? name}`;
1748
+ return `spawning agent: ${asText(input["description"], name)}`;
1728
1749
  default:
1729
1750
  return `tool: ${name}`;
1730
1751
  }
@@ -1889,16 +1910,7 @@ ${issue.body}
1889
1910
  if (options.queryOnly) {
1890
1911
  process.exit(0);
1891
1912
  }
1892
- let executor;
1893
- if (options.claude !== false) {
1894
- const requestedExecutor = options.with.toLowerCase();
1895
- if (requestedExecutor !== "claude" && requestedExecutor !== "codex") {
1896
- process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${options.with}'.
1897
- `);
1898
- process.exit(1);
1899
- }
1900
- executor = requestedExecutor;
1901
- }
1913
+ const executor = options.claude === false ? void 0 : resolveExecutor(options.with);
1902
1914
  let commentUrl;
1903
1915
  try {
1904
1916
  commentUrl = postComment(issue.number, "working");
@@ -1923,37 +1935,49 @@ ${issue.body}`;
1923
1935
  await invokeClaudeCode(prompt, { yolo: options.yolo, verbose: !options.silent, model: options.model });
1924
1936
  }
1925
1937
  }
1926
- try {
1927
- const pr = getCurrentBranchPr();
1928
- if (pr) {
1929
- if (commentUrl) {
1930
- try {
1931
- editComment(commentUrl, `Working on this in PR #${pr.number} \u2014 ${pr.url}`);
1932
- } catch (err) {
1933
- process.stderr.write(`Warning: could not update issue comment: ${err.message}
1934
- `);
1935
- }
1936
- }
1937
- try {
1938
- addClosesRefToPr(pr.number, issue.number);
1939
- } catch (err) {
1940
- process.stderr.write(`Warning: could not add Closes #${issue.number} to PR: ${err.message}
1938
+ linkPrToIssue(issue.number, commentUrl, options.askCopilotReview === true);
1939
+ });
1940
+ function resolveExecutor(requested) {
1941
+ const executor = requested.toLowerCase();
1942
+ if (executor !== "claude" && executor !== "codex") {
1943
+ process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${requested}'.
1941
1944
  `);
1942
- }
1943
- if (options.askCopilotReview) {
1944
- try {
1945
- addCopilotReviewer(pr.number);
1946
- } catch (err) {
1947
- process.stderr.write(`Warning: could not request Copilot review: ${err.message}
1945
+ process.exit(1);
1946
+ }
1947
+ return executor;
1948
+ }
1949
+ function warnOnFailure(what, action) {
1950
+ try {
1951
+ action();
1952
+ } catch (err) {
1953
+ process.stderr.write(`Warning: could not ${what}: ${err.message}
1948
1954
  `);
1949
- }
1950
- }
1951
- }
1955
+ }
1956
+ }
1957
+ function linkPrToIssue(issueNumber, commentUrl, askCopilotReview) {
1958
+ let pr;
1959
+ try {
1960
+ pr = getCurrentBranchPr();
1952
1961
  } catch (err) {
1953
1962
  process.stderr.write(`Warning: could not detect current branch PR: ${err.message}
1954
1963
  `);
1964
+ return;
1955
1965
  }
1956
- });
1966
+ if (!pr) return;
1967
+ if (commentUrl) {
1968
+ warnOnFailure("update issue comment", () => {
1969
+ editComment(commentUrl, `Working on this in PR #${pr.number} \u2014 ${pr.url}`);
1970
+ });
1971
+ }
1972
+ warnOnFailure(`add Closes #${String(issueNumber)} to PR`, () => {
1973
+ addClosesRefToPr(pr.number, issueNumber);
1974
+ });
1975
+ if (askCopilotReview) {
1976
+ warnOnFailure("request Copilot review", () => {
1977
+ addCopilotReviewer(pr.number);
1978
+ });
1979
+ }
1980
+ }
1957
1981
 
1958
1982
  // src/commands/execute.ts
1959
1983
  import { readFileSync as readFileSync2 } from "fs";
@@ -2132,7 +2156,7 @@ function pluralSuffix(count) {
2132
2156
  function addAiOptions(cmd) {
2133
2157
  return cmd.requiredOption("--with <executor>", "Executor to use: claude or codex").option("--model <string>", "Model identifier to pass to the executor").option("--silent", "Suppress step-by-step Claude output; show only the final summary").option("--push", "Append instruction to commit and push changes after the AI finishes");
2134
2158
  }
2135
- function resolveExecutor(withOption) {
2159
+ function resolveExecutor2(withOption) {
2136
2160
  const executor = withOption.toLowerCase();
2137
2161
  if (executor !== "claude" && executor !== "codex") {
2138
2162
  process.stderr.write(`Error: --with must be 'claude' or 'codex', got '${withOption}'.
@@ -2153,7 +2177,7 @@ var executeSonarCmd = addAiOptions(
2153
2177
  "Check the current branch for a SonarCloud analysis and invoke the AI with the Sonar prompt and analysis URL"
2154
2178
  )
2155
2179
  ).action(async (options) => {
2156
- const executor = resolveExecutor(options.with);
2180
+ const executor = resolveExecutor2(options.with);
2157
2181
  let branch;
2158
2182
  try {
2159
2183
  branch = getCurrentBranch();
@@ -2207,7 +2231,7 @@ var executeFixCommentsCmd = addAiOptions(
2207
2231
  "Fetch open review comments on the current PR and invoke the AI with the Fix-Comments prompt"
2208
2232
  )
2209
2233
  ).action(async (options) => {
2210
- const executor = resolveExecutor(options.with);
2234
+ const executor = resolveExecutor2(options.with);
2211
2235
  const result = resolveCurrentBranchComments();
2212
2236
  if (!result.ok) {
2213
2237
  if (result.kind === "error") {
@@ -2251,7 +2275,7 @@ var executeCheckIssueCmd = addAiOptions(
2251
2275
  "Check a GitHub issue for a new message from an allowed user since the last agent run and invoke the AI with the issue conversation"
2252
2276
  ).argument("<issue-number>", "GitHub issue number to check")
2253
2277
  ).option("--force", "Skip the new-message check and invoke the AI directly").action(async (issueNumberArg, options) => {
2254
- const executor = resolveExecutor(options.with);
2278
+ const executor = resolveExecutor2(options.with);
2255
2279
  const issueNumber = Number.parseInt(issueNumberArg, 10);
2256
2280
  if (Number.isNaN(issueNumber) || issueNumber <= 0) {
2257
2281
  process.stderr.write(`Error: <issue-number> must be a positive integer (got '${issueNumberArg}').
@@ -3401,26 +3425,28 @@ function validateDoWorkConfig(section) {
3401
3425
  validateOptionalString(section, "baseBranch", "doWork.baseBranch");
3402
3426
  validateOptionalInt(section, "maxRunsPerTick", "doWork.maxRunsPerTick", 0, "a non-negative integer (0 = unlimited)");
3403
3427
  validateOptionalInt(section, "lockStaleMinutes", "doWork.lockStaleMinutes", 1, "a positive integer");
3404
- const protectedBranches = section["protectedBranches"];
3405
- if (protectedBranches !== void 0 && protectedBranches !== null) {
3406
- if (!Array.isArray(protectedBranches) || protectedBranches.some((b) => typeof b !== "string" || b.trim().length === 0)) {
3407
- fail("doWork.protectedBranches must be an array of non-empty strings.");
3408
- }
3428
+ validateProtectedBranches(section["protectedBranches"]);
3429
+ validateSettingContainer(section["models"], "models", ["claude", "codex"]);
3430
+ validateSettingContainer(section["prompts"], "prompts", ["issueDiscuss", "prWork"]);
3431
+ }
3432
+ function validateProtectedBranches(value) {
3433
+ if (value === void 0 || value === null) return;
3434
+ const isNonEmptyString = (b) => typeof b === "string" && b.trim().length > 0;
3435
+ if (!Array.isArray(value) || !value.every(isNonEmptyString)) {
3436
+ fail("doWork.protectedBranches must be an array of non-empty strings.");
3409
3437
  }
3410
- for (const container of ["models", "prompts"]) {
3411
- const value = section[container];
3412
- if (value === void 0 || value === null) continue;
3413
- if (!isPlainObject(value)) {
3414
- fail(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
3415
- }
3416
- const keys = container === "models" ? ["claude", "codex"] : ["issueDiscuss", "prWork"];
3417
- for (const key of keys) {
3418
- validateOptionalString(value, key, `doWork.${container}.${key}`);
3419
- }
3420
- for (const key of Object.keys(value)) {
3421
- if (!keys.includes(key)) {
3422
- fail(`doWork.${container}.${key} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
3423
- }
3438
+ }
3439
+ function validateSettingContainer(value, container, keys) {
3440
+ if (value === void 0 || value === null) return;
3441
+ if (!isPlainObject(value)) {
3442
+ fail(`doWork.${container} must be an object, got ${JSON.stringify(value)}.`);
3443
+ }
3444
+ for (const key of keys) {
3445
+ validateOptionalString(value, key, `doWork.${container}.${key}`);
3446
+ }
3447
+ for (const key of Object.keys(value)) {
3448
+ if (!keys.includes(key)) {
3449
+ fail(`doWork.${container}.${key} is not a recognised setting; expected one of: ${keys.join(", ")}.`);
3424
3450
  }
3425
3451
  }
3426
3452
  }
@@ -3736,10 +3762,9 @@ async function processItem(planned, settings, silent) {
3736
3762
  baseBranch: settings.baseBranch,
3737
3763
  frame: settings.prompts[item.turn]
3738
3764
  });
3739
- const MAX_PROMPT_BYTES = 96 * 1024;
3740
- const promptBytes = Buffer.byteLength(prompt, "utf8");
3741
- if (promptBytes > MAX_PROMPT_BYTES) {
3742
- const detail = `the composed prompt is ${String(Math.round(promptBytes / 1024))} KiB, over the ${String(MAX_PROMPT_BYTES / 1024)} KiB limit for a single command-line argument. The conversation is too long to hand to the executor this way.`;
3765
+ const oversized = describeOversizedPrompt(prompt);
3766
+ if (oversized !== null) {
3767
+ const detail = oversized;
3743
3768
  progress(` failed: ${detail}
3744
3769
  `);
3745
3770
  inFlightMarker = null;
@@ -3765,6 +3790,16 @@ async function processItem(planned, settings, silent) {
3765
3790
  const reconciled = reconcileMarker(item, marker, settings.participants, watermark, runError);
3766
3791
  progress(` ${reconciled.detail}
3767
3792
  `);
3793
+ const outcome = adjustOutcome(reconciled, item, settings, buriedByNote);
3794
+ return { ...base, outcome: outcome.outcome, detail: outcome.detail, ranExecutor };
3795
+ }
3796
+ function describeOversizedPrompt(prompt) {
3797
+ const MAX_PROMPT_BYTES = 96 * 1024;
3798
+ const promptBytes = Buffer.byteLength(prompt, "utf8");
3799
+ if (promptBytes <= MAX_PROMPT_BYTES) return null;
3800
+ return `the composed prompt is ${String(Math.round(promptBytes / 1024))} KiB, over the ${String(MAX_PROMPT_BYTES / 1024)} KiB limit for a single command-line argument. The conversation is too long to hand to the executor this way.`;
3801
+ }
3802
+ function adjustOutcome(reconciled, item, settings, buriedByNote) {
3768
3803
  let outcome = reconciled;
3769
3804
  if (buriedByNote > 0 && outcome.outcome === "answered") {
3770
3805
  outcome = {
@@ -3773,18 +3808,17 @@ async function processItem(planned, settings, silent) {
3773
3808
  reason: "flagged"
3774
3809
  };
3775
3810
  }
3776
- if (item.turn === "issue-discuss") {
3777
- const linked = repairIssueLink(item, settings.baseBranch);
3778
- if (linked && outcome.reason === "no-answer") {
3779
- outcome = {
3780
- outcome: "answered",
3781
- detail: "opened a pull request (no issue comment)",
3782
- reason: "answered"
3783
- };
3784
- progress(" a pull request was opened, so the turn is counted as answered.\n");
3785
- }
3811
+ if (item.turn !== "issue-discuss") return outcome;
3812
+ const linked = repairIssueLink(item, settings.baseBranch);
3813
+ if (linked && outcome.reason === "no-answer") {
3814
+ progress(" a pull request was opened, so the turn is counted as answered.\n");
3815
+ return {
3816
+ outcome: "answered",
3817
+ detail: "opened a pull request (no issue comment)",
3818
+ reason: "answered"
3819
+ };
3786
3820
  }
3787
- return { ...base, outcome: outcome.outcome, detail: outcome.detail, ranExecutor };
3821
+ return outcome;
3788
3822
  }
3789
3823
  function summarize(reports) {
3790
3824
  out("\nTick summary:\n");
@@ -3811,25 +3845,7 @@ var doWorkCommand = new Command6("do-work").description(
3811
3845
  }
3812
3846
  const lock = acquireRunLock("do-work", settings.lockStaleMinutes);
3813
3847
  if (!lock.ok) {
3814
- const held = lock.heldBy;
3815
- const sentence = `Another automata instance is already running here (pid ${String(held.pid)} on ${held.host}, started ${held.startedAt}, command ${held.command}). Doing nothing.
3816
- `;
3817
- const suspectSentence = lock.suspect ? `Warning: that lock has been held longer than ${String(settings.lockStaleMinutes)} minutes. If no tick is really running, its process id was probably reused; remove ${RUN_LOCK_RELATIVE_PATH} once you have confirmed that.
3818
- ` : "";
3819
- const exitCode2 = lock.suspect ? 2 : 0;
3820
- if (options.json === true) {
3821
- progress(sentence + suspectSentence);
3822
- out(
3823
- JSON.stringify(
3824
- { lockHeld: true, suspect: lock.suspect, heldBy: held, plan: [], items: [], exitCode: exitCode2 },
3825
- null,
3826
- 2
3827
- ) + "\n"
3828
- );
3829
- } else {
3830
- out(sentence);
3831
- if (suspectSentence) progress(suspectSentence);
3832
- }
3848
+ const exitCode2 = reportLockHeld(lock, settings, options);
3833
3849
  if (exitCode2 !== 0) process.exit(exitCode2);
3834
3850
  return;
3835
3851
  }
@@ -3839,18 +3855,7 @@ var doWorkCommand = new Command6("do-work").description(
3839
3855
  if (shuttingDown) return;
3840
3856
  shuttingDown = true;
3841
3857
  progress("\nInterrupted: stopping the executor before releasing the run lock\u2026\n");
3842
- const pending = inFlightMarker;
3843
- if (pending !== null) {
3844
- try {
3845
- updateMarker(
3846
- pending.marker,
3847
- `automata do-work: this run was interrupted before it finished, so no answer was produced. The branch \`${pending.item.branch}\` may have been changed. Reply here to have another attempt made.`
3848
- );
3849
- } catch (err) {
3850
- progress(`Warning: could not update the in-flight marker: ${err.message}
3851
- `);
3852
- }
3853
- }
3858
+ explainInterruptedMarker();
3854
3859
  void terminateTrackedChildren().then((allExited) => {
3855
3860
  if (allExited) {
3856
3861
  handle.release();
@@ -3878,6 +3883,37 @@ var doWorkCommand = new Command6("do-work").description(
3878
3883
  }
3879
3884
  if (exitCode !== 0) process.exit(exitCode);
3880
3885
  });
3886
+ function reportLockHeld(lock, settings, options) {
3887
+ const held = lock.heldBy;
3888
+ const sentence = `Another automata instance is already running here (pid ${String(held.pid)} on ${held.host}, started ${held.startedAt}, command ${held.command}). Doing nothing.
3889
+ `;
3890
+ const suspectSentence = lock.suspect ? `Warning: that lock has been held longer than ${String(settings.lockStaleMinutes)} minutes. If no tick is really running, its process id was probably reused; remove ${RUN_LOCK_RELATIVE_PATH} once you have confirmed that.
3891
+ ` : "";
3892
+ const exitCode = lock.suspect ? 2 : 0;
3893
+ if (options.json === true) {
3894
+ progress(sentence + suspectSentence);
3895
+ out(
3896
+ JSON.stringify({ lockHeld: true, suspect: lock.suspect, heldBy: held, plan: [], items: [], exitCode }, null, 2) + "\n"
3897
+ );
3898
+ } else {
3899
+ out(sentence);
3900
+ if (suspectSentence) progress(suspectSentence);
3901
+ }
3902
+ return exitCode;
3903
+ }
3904
+ function explainInterruptedMarker() {
3905
+ const pending = inFlightMarker;
3906
+ if (pending === null) return;
3907
+ try {
3908
+ updateMarker(
3909
+ pending.marker,
3910
+ `automata do-work: this run was interrupted before it finished, so no answer was produced. The branch \`${pending.item.branch}\` may have been changed. Reply here to have another attempt made.`
3911
+ );
3912
+ } catch (err) {
3913
+ progress(`Warning: could not update the in-flight marker: ${err.message}
3914
+ `);
3915
+ }
3916
+ }
3881
3917
  async function runTick(settings, options) {
3882
3918
  const issues = discoverIssues(settings);
3883
3919
  const linkMap = getOpenPrLinkMap();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.6.0-develop.225",
3
+ "version": "0.6.0-develop.231",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "typecheck": "tsc --noEmit",
17
17
  "format": "prettier --check src/",
18
18
  "test": "npm run build && vitest run tests/unit",
19
+ "test:unit": "vitest run tests/unit",
19
20
  "test:integration": "npm run build && vitest run tests/integration"
20
21
  },
21
22
  "repository": {