automata-cli 0.3.0-develop.96 → 0.4.0-develop.124

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.
Files changed (2) hide show
  1. package/dist/index.js +557 -54
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -19,10 +19,12 @@ import React2 from "react";
19
19
  // src/config/ConfigWizard.tsx
20
20
  import { useState } from "react";
21
21
  import { Box, Text, useInput, useApp } from "ink";
22
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
23
+ import { join as join2 } from "path";
22
24
 
23
25
  // src/config/configStore.ts
24
26
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
25
- import { join } from "path";
27
+ import { join, resolve as resolve2, sep } from "path";
26
28
  var DEFAULT_CLAUDE_SYSTEM_PROMPT = "You are an expert software engineer. Implement the following issue according to the project's existing conventions and style. Make minimal, targeted changes that satisfy the requirements. Run tests and linting before finishing.";
27
29
  var DEFAULT_FIX_COMMENTS_PROMPT = "You are an expert software engineer reviewing a pull request. Below are the open review comments left by reviewers on this PR. Please address each comment by making the appropriate code changes. Focus on the reviewer's concerns and make minimal, targeted changes that resolve each comment without altering unrelated code.";
28
30
  var DEFAULT_SONAR_PROMPT = "You are an expert software engineer. You have been given the URL of a SonarCloud analysis for this pull request. If the `sonar-quality-gate` skill is available in this repository, use it. The project is public, so use the SonarCloud REST API directly (no authentication required) rather than scraping the URL. Inspect both the quality gate and the list of issues for this pull request. If the quality gate fails because of duplication or another metric-based condition, use the relevant Sonar APIs to identify the affected files and details instead of relying only on the issues endpoint. Fix all new issues and quality-gate failures reported. Focus on code smells, bugs, vulnerabilities, and blocking quality-gate conditions flagged in this PR. Make targeted, minimal changes that resolve each issue without altering unrelated code.";
@@ -31,7 +33,32 @@ var CONFIG_FILE = "config.json";
31
33
  function configPath() {
32
34
  return join(process.cwd(), CONFIG_DIR, CONFIG_FILE);
33
35
  }
34
- function readConfig() {
36
+ function automataDir() {
37
+ return join(process.cwd(), CONFIG_DIR);
38
+ }
39
+ function resolvePromptRef(value, dir) {
40
+ if (!value.endsWith(".md")) return value;
41
+ if (value.includes("/") || value.includes("\\")) {
42
+ throw new Error(`Prompt file "${value}" must be a plain filename with no subdirectories`);
43
+ }
44
+ const fullPath = resolve2(dir, value);
45
+ const safeBase = resolve2(dir) + sep;
46
+ if (!fullPath.startsWith(safeBase)) {
47
+ throw new Error(`Prompt file "${value}" resolves outside .automata/`);
48
+ }
49
+ try {
50
+ return readFileSync2(fullPath, "utf8");
51
+ } catch (err) {
52
+ if (err?.code === "ENOENT") {
53
+ throw new Error(
54
+ `Prompt file "${value}" not found in "${dir}". Expected path: ${fullPath}`,
55
+ { cause: err }
56
+ );
57
+ }
58
+ throw err;
59
+ }
60
+ }
61
+ function readRawConfig() {
35
62
  try {
36
63
  const raw = readFileSync2(configPath(), "utf8");
37
64
  return JSON.parse(raw);
@@ -39,6 +66,26 @@ function readConfig() {
39
66
  return {};
40
67
  }
41
68
  }
69
+ function readConfig() {
70
+ let config;
71
+ try {
72
+ const raw = readFileSync2(configPath(), "utf8");
73
+ config = JSON.parse(raw);
74
+ } catch {
75
+ return {};
76
+ }
77
+ const dir = automataDir();
78
+ if (config.claudeSystemPrompt) {
79
+ config.claudeSystemPrompt = resolvePromptRef(config.claudeSystemPrompt, dir);
80
+ }
81
+ if (config.prompts?.sonar) {
82
+ config.prompts.sonar = resolvePromptRef(config.prompts.sonar, dir);
83
+ }
84
+ if (config.prompts?.fixComments) {
85
+ config.prompts.fixComments = resolvePromptRef(config.prompts.fixComments, dir);
86
+ }
87
+ return config;
88
+ }
42
89
  function writeConfig(config) {
43
90
  const dir = join(process.cwd(), CONFIG_DIR);
44
91
  mkdirSync(dir, { recursive: true });
@@ -47,6 +94,11 @@ function writeConfig(config) {
47
94
 
48
95
  // src/config/ConfigWizard.tsx
49
96
  import { jsx, jsxs } from "react/jsx-runtime";
97
+ function writePromptFile(filename, content) {
98
+ const dir = join2(process.cwd(), ".automata");
99
+ mkdirSync2(dir, { recursive: true });
100
+ writeFileSync2(join2(dir, filename), content, "utf8");
101
+ }
50
102
  var REMOTE_OPTIONS = [
51
103
  { label: "GitHub", value: "gh" },
52
104
  { label: "Azure DevOps", value: "azdo" }
@@ -60,6 +112,7 @@ var MAIN_MENU_OPTIONS = ["Remote / Mode", "Implement-Next", "Prompts"];
60
112
  var PROMPTS_MENU_OPTIONS = ["Sonar", "Fix-Comments"];
61
113
  function ConfigWizard() {
62
114
  const existing = readConfig();
115
+ const rawExisting = readRawConfig();
63
116
  const initialRemoteIndex = REMOTE_OPTIONS.findIndex((o) => o.value === existing.remoteType);
64
117
  const initialTechIndex = TECHNIQUE_OPTIONS.findIndex((o) => o.value === existing.issueDiscoveryTechnique);
65
118
  const [screen, setScreen] = useState("main");
@@ -107,7 +160,7 @@ function ConfigWizard() {
107
160
  if (chosen.value === "gh") {
108
161
  setScreen("technique");
109
162
  } else {
110
- writeConfig({ ...existing, remoteType: chosen.value });
163
+ writeConfig({ ...rawExisting, remoteType: chosen.value });
111
164
  exit();
112
165
  }
113
166
  } else if (key.escape) {
@@ -143,12 +196,17 @@ function ConfigWizard() {
143
196
  }
144
197
  } else if (screen === "system-prompt") {
145
198
  if (key.return) {
199
+ let claudeSystemPromptValue;
200
+ if (systemPrompt) {
201
+ writePromptFile("claude-system-prompt.md", systemPrompt);
202
+ claudeSystemPromptValue = "claude-system-prompt.md";
203
+ }
146
204
  writeConfig({
147
- ...existing,
205
+ ...rawExisting,
148
206
  remoteType: pendingRemote,
149
207
  issueDiscoveryTechnique: pendingTechnique,
150
208
  issueDiscoveryValue: discoveryValue || void 0,
151
- claudeSystemPrompt: systemPrompt || void 0
209
+ claudeSystemPrompt: claudeSystemPromptValue
152
210
  });
153
211
  exit();
154
212
  } else if (key.backspace || key.delete) {
@@ -179,10 +237,15 @@ function ConfigWizard() {
179
237
  }
180
238
  } else if (screen === "sonar-prompt") {
181
239
  if (key.return) {
182
- const current = readConfig();
240
+ let sonarValue;
241
+ if (sonarPrompt) {
242
+ writePromptFile("sonar-prompt.md", sonarPrompt);
243
+ sonarValue = "sonar-prompt.md";
244
+ }
245
+ const current = readRawConfig();
183
246
  writeConfig({
184
247
  ...current,
185
- prompts: { ...current.prompts, sonar: sonarPrompt || void 0 }
248
+ prompts: { ...current.prompts, sonar: sonarValue }
186
249
  });
187
250
  setScreen("prompts-menu");
188
251
  } else if (key.backspace || key.delete) {
@@ -196,10 +259,15 @@ function ConfigWizard() {
196
259
  }
197
260
  } else if (screen === "fix-comments-prompt") {
198
261
  if (key.return) {
199
- const current = readConfig();
262
+ let fixCommentsValue;
263
+ if (fixCommentsPrompt) {
264
+ writePromptFile("fix-comments-prompt.md", fixCommentsPrompt);
265
+ fixCommentsValue = "fix-comments-prompt.md";
266
+ }
267
+ const current = readRawConfig();
200
268
  writeConfig({
201
269
  ...current,
202
- prompts: { ...current.prompts, fixComments: fixCommentsPrompt || void 0 }
270
+ prompts: { ...current.prompts, fixComments: fixCommentsValue }
203
271
  });
204
272
  setScreen("prompts-menu");
205
273
  } else if (key.backspace || key.delete) {
@@ -338,7 +406,7 @@ var configSetType = new Command("type").description("Set the remote environment
338
406
  `);
339
407
  process.exit(1);
340
408
  }
341
- const current = readConfig();
409
+ const current = readRawConfig();
342
410
  writeConfig({ ...current, remoteType: value });
343
411
  process.stdout.write(`Remote type set to: ${value}
344
412
  `);
@@ -349,19 +417,19 @@ var configSetIssueDiscoveryTechnique = new Command("issue-discovery-technique").
349
417
  `);
350
418
  process.exit(1);
351
419
  }
352
- const current = readConfig();
420
+ const current = readRawConfig();
353
421
  writeConfig({ ...current, issueDiscoveryTechnique: value });
354
422
  process.stdout.write(`Issue discovery technique set to: ${value}
355
423
  `);
356
424
  });
357
425
  var configSetIssueDiscoveryValue = new Command("issue-discovery-value").description("Set the value for the issue discovery technique (label name, username, or search string)").argument("<value>", "The filter value").action((value) => {
358
- const current = readConfig();
426
+ const current = readRawConfig();
359
427
  writeConfig({ ...current, issueDiscoveryValue: value });
360
428
  process.stdout.write(`Issue discovery value set to: ${value}
361
429
  `);
362
430
  });
363
431
  var configSetClaudeSystemPrompt = new Command("claude-system-prompt").description("Set the system prompt used when invoking Claude Code").argument("<value>", "System prompt text").action((value) => {
364
- const current = readConfig();
432
+ const current = readRawConfig();
365
433
  writeConfig({ ...current, claudeSystemPrompt: value });
366
434
  process.stdout.write(`Claude system prompt set.
367
435
  `);
@@ -442,6 +510,8 @@ function getCurrentBranch() {
442
510
  }
443
511
  return stdout.trim();
444
512
  }
513
+ var SONAR_FETCH_TIMEOUT_MS = 5e3;
514
+ var SONAR_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"]);
445
515
  function parseOwnerRepo() {
446
516
  const { stdout, status } = run2("git", ["remote", "get-url", "origin"]);
447
517
  if (status !== 0) return null;
@@ -487,21 +557,285 @@ function extractSonarProjectKey(url) {
487
557
  return null;
488
558
  }
489
559
  }
490
- async function fetchSonarNewIssues(projectKey, prNumber) {
491
- const apiUrl = `https://sonarcloud.io/api/issues/search?componentKeys=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&resolved=false&ps=1`;
560
+ function isSonarUrl(url) {
561
+ try {
562
+ const hostname = new URL(url).hostname;
563
+ return hostname === "sonarcloud.io" || hostname.endsWith(".sonarcloud.io");
564
+ } catch {
565
+ return false;
566
+ }
567
+ }
568
+ function stripHtml(text) {
569
+ let stripped = "";
570
+ let inTag = false;
571
+ for (const char of text) {
572
+ if (char === "<") {
573
+ inTag = true;
574
+ stripped += " ";
575
+ continue;
576
+ }
577
+ if (char === ">") {
578
+ inTag = false;
579
+ continue;
580
+ }
581
+ if (!inTag) {
582
+ stripped += char;
583
+ }
584
+ }
585
+ return stripped;
586
+ }
587
+ function normalizeText(text) {
588
+ if (!text) return void 0;
589
+ const normalized = stripHtml(text).replaceAll(/\s+/g, " ").trim();
590
+ return normalized || void 0;
591
+ }
592
+ function resolveSonarPath(componentKey, components) {
593
+ if (!componentKey) return void 0;
594
+ const mapped = components.get(componentKey);
595
+ if (mapped) return mapped;
596
+ const separatorIndex = componentKey.indexOf(":");
597
+ if (separatorIndex === -1) return void 0;
598
+ const path = componentKey.slice(separatorIndex + 1).trim();
599
+ return path || void 0;
600
+ }
601
+ function mapSonarIssuesPage(data, targetIssues, components, rules) {
602
+ for (const component of data.components ?? []) {
603
+ if (component.key && component.path) {
604
+ components.set(component.key, component.path);
605
+ }
606
+ }
607
+ for (const rule of data.rules ?? []) {
608
+ if (!rule.key) continue;
609
+ const explanation = normalizeText(rule.htmlDesc) ?? normalizeText(rule.htmlNote) ?? normalizeText(rule.name);
610
+ if (explanation) {
611
+ rules.set(rule.key, explanation);
612
+ }
613
+ }
614
+ for (const issue of data.issues ?? []) {
615
+ if (!issue.key || !issue.message) continue;
616
+ const ruleKey = issue.rule;
617
+ const mappedIssue = {
618
+ key: issue.key,
619
+ severity: issue.severity,
620
+ type: issue.type,
621
+ message: issue.message,
622
+ path: resolveSonarPath(issue.component, components),
623
+ line: issue.line ?? issue.textRange?.startLine ?? null,
624
+ ...ruleKey ? { rule: ruleKey } : {},
625
+ ...ruleKey && rules.has(ruleKey) ? { explanation: rules.get(ruleKey) } : {}
626
+ };
627
+ targetIssues.push(mappedIssue);
628
+ }
629
+ }
630
+ function mapSonarHotspotsPage(data, targetHotspots, components) {
631
+ for (const component of data.components ?? []) {
632
+ if (component.key && component.path) {
633
+ components.set(component.key, component.path);
634
+ }
635
+ }
636
+ targetHotspots.push(...data.hotspots ?? []);
637
+ }
638
+ function mapSonarHotspot(hotspot, components, detail) {
639
+ const rule = detail?.rule;
640
+ return {
641
+ key: detail?.key ?? hotspot.key ?? "",
642
+ rule: hotspot.ruleKey ?? rule?.key ?? "",
643
+ ruleName: normalizeText(rule?.name),
644
+ status: detail?.status ?? hotspot.status ?? "UNKNOWN",
645
+ message: detail?.message ?? hotspot.message ?? "",
646
+ path: detail?.component?.path ?? resolveSonarPath(hotspot.component, components),
647
+ line: detail?.line ?? detail?.textRange?.startLine ?? hotspot.line ?? hotspot.textRange?.startLine ?? null,
648
+ securityCategory: rule?.securityCategory ?? hotspot.securityCategory,
649
+ vulnerabilityProbability: rule?.vulnerabilityProbability ?? hotspot.vulnerabilityProbability,
650
+ riskDescription: normalizeText(rule?.riskDescription),
651
+ vulnerabilityDescription: normalizeText(rule?.vulnerabilityDescription),
652
+ fixRecommendations: normalizeText(rule?.fixRecommendations)
653
+ };
654
+ }
655
+ async function fetchSonarJson(apiUrl) {
492
656
  const controller = new AbortController();
493
- const timeoutId = setTimeout(() => controller.abort(), 5e3);
657
+ const timeoutId = setTimeout(() => controller.abort(), SONAR_FETCH_TIMEOUT_MS);
494
658
  try {
495
659
  const response = await fetch(apiUrl, { signal: controller.signal });
496
- if (!response.ok) return null;
497
- const data = await response.json();
498
- return data.paging?.total ?? null;
660
+ if (!response.ok) {
661
+ return { ok: false, status: response.status };
662
+ }
663
+ return {
664
+ ok: true,
665
+ status: response.status,
666
+ data: await response.json()
667
+ };
499
668
  } catch {
500
- return null;
669
+ return { ok: false, status: null };
501
670
  } finally {
502
671
  clearTimeout(timeoutId);
503
672
  }
504
673
  }
674
+ async function fetchSonarNewIssues(projectKey, prNumber) {
675
+ const apiUrl = `https://sonarcloud.io/api/issues/search?componentKeys=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&resolved=false&ps=1`;
676
+ const response = await fetchSonarJson(apiUrl);
677
+ if (!response.ok) {
678
+ return null;
679
+ }
680
+ return response.data.paging?.total ?? null;
681
+ }
682
+ async function fetchSonarGateViolations(projectKey, prNumber) {
683
+ const apiUrl = `https://sonarcloud.io/api/qualitygates/project_status?projectKey=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}`;
684
+ const response = await fetchSonarJson(apiUrl);
685
+ if (!response.ok) {
686
+ return response;
687
+ }
688
+ const projectStatus = response.data.projectStatus;
689
+ const gateViolations = (projectStatus?.conditions ?? []).filter((condition) => condition.status !== void 0 && condition.status !== "OK").map((condition) => ({
690
+ metricKey: condition.metricKey ?? "unknown",
691
+ status: condition.status ?? "ERROR",
692
+ comparator: condition.comparator,
693
+ actualValue: condition.actualValue,
694
+ errorThreshold: condition.errorThreshold
695
+ }));
696
+ return {
697
+ ok: true,
698
+ status: response.status,
699
+ data: {
700
+ status: "available",
701
+ qualityGateStatus: projectStatus?.status,
702
+ gateViolations,
703
+ issues: [],
704
+ securityHotspots: []
705
+ }
706
+ };
707
+ }
708
+ async function fetchSonarIssues(projectKey, prNumber) {
709
+ const pageSize = 100;
710
+ const issues = [];
711
+ const components = /* @__PURE__ */ new Map();
712
+ const rules = /* @__PURE__ */ new Map();
713
+ let page = 1;
714
+ let total = null;
715
+ while (true) {
716
+ const apiUrl = `https://sonarcloud.io/api/issues/search?componentKeys=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&resolved=false&ps=${String(pageSize)}&p=${String(page)}&additionalFields=_all`;
717
+ const response = await fetchSonarJson(apiUrl);
718
+ if (!response.ok) {
719
+ return response;
720
+ }
721
+ const paging = response.data.paging;
722
+ total ??= paging?.total ?? null;
723
+ mapSonarIssuesPage(response.data, issues, components, rules);
724
+ const fetchedCount = page * (paging?.pageSize ?? pageSize);
725
+ if (paging?.total === void 0 || fetchedCount >= paging.total) {
726
+ break;
727
+ }
728
+ page += 1;
729
+ }
730
+ return {
731
+ ok: true,
732
+ status: 200,
733
+ data: {
734
+ total,
735
+ issues
736
+ }
737
+ };
738
+ }
739
+ async function fetchSonarHotspotDetail(hotspotKey) {
740
+ const apiUrl = `https://sonarcloud.io/api/hotspots/show?hotspot=${encodeURIComponent(hotspotKey)}`;
741
+ return fetchSonarJson(apiUrl);
742
+ }
743
+ async function fetchSonarHotspots(projectKey, prNumber) {
744
+ const pageSize = 100;
745
+ const rawHotspots = [];
746
+ const components = /* @__PURE__ */ new Map();
747
+ let page = 1;
748
+ while (true) {
749
+ const apiUrl = `https://sonarcloud.io/api/hotspots/search?projectKey=${encodeURIComponent(projectKey)}&pullRequest=${String(prNumber)}&onlyMine=false&sinceLeakPeriod=true&ps=${String(pageSize)}&p=${String(page)}`;
750
+ const response = await fetchSonarJson(apiUrl);
751
+ if (!response.ok) {
752
+ return response;
753
+ }
754
+ const paging = response.data.paging;
755
+ mapSonarHotspotsPage(response.data, rawHotspots, components);
756
+ const fetchedCount = page * (paging?.pageSize ?? pageSize);
757
+ if (paging?.total === void 0 || fetchedCount >= paging.total) {
758
+ break;
759
+ }
760
+ page += 1;
761
+ }
762
+ const detailResults = await Promise.all(rawHotspots.map((hotspot) => fetchSonarHotspotDetail(hotspot.key ?? "")));
763
+ const securityHotspots = [];
764
+ for (let index = 0; index < rawHotspots.length; index += 1) {
765
+ const hotspot = rawHotspots[index];
766
+ const detailResult = detailResults[index];
767
+ if (detailResult && !detailResult.ok && detailResult.status === 401) {
768
+ return detailResult;
769
+ }
770
+ securityHotspots.push(mapSonarHotspot(hotspot, components, detailResult?.ok ? detailResult.data : void 0));
771
+ }
772
+ return {
773
+ ok: true,
774
+ status: 200,
775
+ data: securityHotspots
776
+ };
777
+ }
778
+ function sonarPrivateFailureSummary(sonarcloudUrl) {
779
+ return {
780
+ status: "private",
781
+ gateViolations: [],
782
+ issues: [],
783
+ securityHotspots: [],
784
+ privateMessage: `SonarCloud project is private. Open the Sonar URL in an authenticated browser: ${sonarcloudUrl}`
785
+ };
786
+ }
787
+ async function fetchSonarFailureSummary(projectKey, prNumber, sonarcloudUrl) {
788
+ const [gateResult, issuesResult, hotspotsResult] = await Promise.all([
789
+ fetchSonarGateViolations(projectKey, prNumber),
790
+ fetchSonarIssues(projectKey, prNumber),
791
+ fetchSonarHotspots(projectKey, prNumber)
792
+ ]);
793
+ if (!gateResult.ok && gateResult.status === 401) {
794
+ return {
795
+ summary: sonarPrivateFailureSummary(sonarcloudUrl),
796
+ issueTotal: null
797
+ };
798
+ }
799
+ if (!issuesResult.ok && issuesResult.status === 401) {
800
+ return {
801
+ summary: sonarPrivateFailureSummary(sonarcloudUrl),
802
+ issueTotal: null
803
+ };
804
+ }
805
+ if (!hotspotsResult.ok && hotspotsResult.status === 401) {
806
+ return {
807
+ summary: sonarPrivateFailureSummary(sonarcloudUrl),
808
+ issueTotal: null
809
+ };
810
+ }
811
+ const gateViolations = gateResult.ok ? gateResult.data.gateViolations : [];
812
+ const issues = issuesResult.ok ? issuesResult.data.issues : [];
813
+ const securityHotspots = hotspotsResult.ok ? hotspotsResult.data : [];
814
+ const qualityGateStatus = gateResult.ok ? gateResult.data.qualityGateStatus : void 0;
815
+ const issueTotal = issuesResult.ok ? issuesResult.data.total : null;
816
+ if (gateViolations.length === 0 && issues.length === 0 && securityHotspots.length === 0 && !qualityGateStatus) {
817
+ return {
818
+ summary: {
819
+ status: "unavailable",
820
+ gateViolations: [],
821
+ issues: [],
822
+ securityHotspots: [],
823
+ unavailableMessage: "SonarCloud failure details are unavailable right now."
824
+ },
825
+ issueTotal
826
+ };
827
+ }
828
+ return {
829
+ summary: {
830
+ status: "available",
831
+ qualityGateStatus,
832
+ gateViolations,
833
+ issues,
834
+ securityHotspots
835
+ },
836
+ issueTotal
837
+ };
838
+ }
505
839
  async function getPrInfoGh(branch) {
506
840
  const { stdout, stderr, status } = run2("gh", [
507
841
  "pr",
@@ -532,21 +866,21 @@ async function getPrInfoGh(branch) {
532
866
  detailsUrl: enriched?.detailsUrl || c.detailsUrl || ""
533
867
  };
534
868
  });
535
- const sonarCheck = checks.find((c) => {
536
- try {
537
- const hostname = new URL(c.detailsUrl).hostname;
538
- return hostname === "sonarcloud.io" || hostname.endsWith(".sonarcloud.io");
539
- } catch {
540
- return false;
541
- }
542
- });
869
+ const sonarCheck = checks.find((c) => isSonarUrl(c.detailsUrl));
543
870
  let sonarcloudUrl;
544
871
  let sonarNewIssues;
872
+ let sonarFailures;
545
873
  if (sonarCheck) {
546
874
  sonarcloudUrl = sonarCheck.detailsUrl;
547
875
  const projectKey = extractSonarProjectKey(sonarCheck.detailsUrl);
548
876
  if (projectKey) {
549
- sonarNewIssues = await fetchSonarNewIssues(projectKey, raw.number);
877
+ if (sonarCheck.conclusion !== null && SONAR_FAIL_CONCLUSIONS.has(sonarCheck.conclusion)) {
878
+ const sonarSummary = await fetchSonarFailureSummary(projectKey, raw.number, sonarcloudUrl);
879
+ sonarFailures = sonarSummary.summary;
880
+ sonarNewIssues = sonarSummary.issueTotal;
881
+ } else {
882
+ sonarNewIssues = await fetchSonarNewIssues(projectKey, raw.number);
883
+ }
550
884
  } else {
551
885
  sonarNewIssues = null;
552
886
  }
@@ -557,7 +891,8 @@ async function getPrInfoGh(branch) {
557
891
  state: raw.state,
558
892
  url: raw.url,
559
893
  checks,
560
- ...sonarcloudUrl === void 0 ? {} : { sonarcloudUrl, sonarNewIssues }
894
+ ...sonarcloudUrl === void 0 ? {} : { sonarcloudUrl, sonarNewIssues },
895
+ ...sonarFailures === void 0 ? {} : { sonarFailures }
561
896
  };
562
897
  }
563
898
  async function getPrInfo2(branch) {
@@ -767,7 +1102,7 @@ function formatChecks(checks) {
767
1102
  return lines.join("\n") + "\n";
768
1103
  }
769
1104
  function sleep(ms) {
770
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1105
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
771
1106
  }
772
1107
  function isSonarCheck(check) {
773
1108
  try {
@@ -789,6 +1124,103 @@ function formatFailedChecks(failed) {
789
1124
  }
790
1125
  return lines.join("\n") + "\n";
791
1126
  }
1127
+ function formatGateViolation(violation) {
1128
+ const parts = [sanitizeText(violation.metricKey)];
1129
+ if (violation.actualValue) parts.push(`actual ${sanitizeText(violation.actualValue)}`);
1130
+ if (violation.comparator && violation.errorThreshold) {
1131
+ parts.push(`${sanitizeText(violation.comparator)} ${sanitizeText(violation.errorThreshold)}`);
1132
+ } else if (violation.errorThreshold) {
1133
+ parts.push(`threshold ${sanitizeText(violation.errorThreshold)}`);
1134
+ }
1135
+ return parts.join(" | ");
1136
+ }
1137
+ function formatLocation(path, line) {
1138
+ if (!path) return void 0;
1139
+ const safePath = sanitizeText(path);
1140
+ if (!line) return safePath;
1141
+ return `${safePath}:${String(line)}`;
1142
+ }
1143
+ function formatRuleWithName(rule, ruleName) {
1144
+ const safeRule = sanitizeText(rule);
1145
+ if (!ruleName) return safeRule;
1146
+ return `${safeRule} (${sanitizeText(ruleName)})`;
1147
+ }
1148
+ function formatSonarIssue(issue) {
1149
+ const lines = [` - ${sanitizeText(issue.message)}`];
1150
+ const location = formatLocation(issue.path, issue.line);
1151
+ if (location) lines.push(` Location: ${location}`);
1152
+ if (issue.severity || issue.type) {
1153
+ const labels = [issue.severity, issue.type].filter((label) => Boolean(label)).map(sanitizeText).join(" / ");
1154
+ lines.push(` Classification: ${labels}`);
1155
+ }
1156
+ if (issue.rule) lines.push(` Rule: ${sanitizeText(issue.rule)}`);
1157
+ if (issue.explanation) lines.push(` Explanation: ${sanitizeText(issue.explanation)}`);
1158
+ return lines;
1159
+ }
1160
+ function formatSonarHotspot(hotspot) {
1161
+ const lines = [` - ${sanitizeText(hotspot.message)}`];
1162
+ const location = formatLocation(hotspot.path, hotspot.line);
1163
+ if (location) lines.push(` Location: ${location}`);
1164
+ if (hotspot.status) lines.push(` Status: ${sanitizeText(hotspot.status)}`);
1165
+ if (hotspot.vulnerabilityProbability || hotspot.securityCategory) {
1166
+ const labels = [hotspot.vulnerabilityProbability, hotspot.securityCategory].filter((label) => Boolean(label)).map(sanitizeText).join(" / ");
1167
+ lines.push(` Classification: ${labels}`);
1168
+ }
1169
+ if (hotspot.rule) {
1170
+ lines.push(` Rule: ${formatRuleWithName(hotspot.rule, hotspot.ruleName)}`);
1171
+ }
1172
+ if (hotspot.riskDescription) lines.push(` Risk: ${sanitizeText(hotspot.riskDescription)}`);
1173
+ if (hotspot.vulnerabilityDescription) lines.push(` Review: ${sanitizeText(hotspot.vulnerabilityDescription)}`);
1174
+ if (hotspot.fixRecommendations) lines.push(` Fix: ${sanitizeText(hotspot.fixRecommendations)}`);
1175
+ return lines;
1176
+ }
1177
+ function appendGateViolations(lines, gateViolations) {
1178
+ if (gateViolations.length === 0) return;
1179
+ lines.push(" Gate Violations:");
1180
+ for (const violation of gateViolations) {
1181
+ lines.push(` - ${formatGateViolation(violation)}`);
1182
+ }
1183
+ }
1184
+ function appendSonarIssues(lines, issues) {
1185
+ if (issues.length === 0) return;
1186
+ lines.push(" Issues:");
1187
+ for (const issue of issues) {
1188
+ lines.push(...formatSonarIssue(issue));
1189
+ }
1190
+ }
1191
+ function appendSecurityHotspots(lines, securityHotspots) {
1192
+ if (securityHotspots.length === 0) return;
1193
+ lines.push(" Security Hotspots:");
1194
+ for (const hotspot of securityHotspots) {
1195
+ lines.push(...formatSonarHotspot(hotspot));
1196
+ }
1197
+ }
1198
+ function hasSonarFailureDetails(sonarFailures) {
1199
+ return Boolean(sonarFailures.qualityGateStatus) || sonarFailures.gateViolations.length > 0 || sonarFailures.issues.length > 0 || sonarFailures.securityHotspots.length > 0;
1200
+ }
1201
+ function formatSonarFailures(sonarFailures, sonarcloudUrl) {
1202
+ const lines = ["Sonar Failures:"];
1203
+ if (sonarFailures.status === "private") {
1204
+ lines.push(` Note: ${sanitizeText(sonarFailures.privateMessage ?? "SonarCloud project is private.")}`);
1205
+ return lines.join("\n") + "\n";
1206
+ }
1207
+ if (sonarFailures.status === "unavailable") {
1208
+ lines.push(` Note: ${sanitizeText(sonarFailures.unavailableMessage ?? "SonarCloud failure details are unavailable.")}`);
1209
+ if (sonarcloudUrl) lines.push(` URL: ${sonarcloudUrl}`);
1210
+ return lines.join("\n") + "\n";
1211
+ }
1212
+ if (sonarFailures.qualityGateStatus) {
1213
+ lines.push(` Quality Gate: ${sanitizeText(sonarFailures.qualityGateStatus)}`);
1214
+ }
1215
+ appendGateViolations(lines, sonarFailures.gateViolations);
1216
+ appendSonarIssues(lines, sonarFailures.issues);
1217
+ appendSecurityHotspots(lines, sonarFailures.securityHotspots);
1218
+ if (!hasSonarFailureDetails(sonarFailures)) {
1219
+ lines.push(" Note: SonarCloud reported a failure but returned no violation, issue, or hotspot details.");
1220
+ if (sonarcloudUrl) lines.push(` URL: ${sonarcloudUrl}`);
1221
+ }
1222
+ return lines.join("\n") + "\n";
1223
+ }
792
1224
  var POLL_INTERVAL_MS = 1e4;
793
1225
  var getPrInfoCmd = new Command2("get-pr-info").description("Show pull request info for the current branch").option("--json", "Output as JSON").option("--wait-finish-checks", "Poll until all checks complete, then print the normal get-pr-info output").addHelpText(
794
1226
  "after",
@@ -864,6 +1296,9 @@ URL: ${pr.url}
864
1296
  if (failed.length > 0) {
865
1297
  process.stdout.write(formatFailedChecks(failed));
866
1298
  }
1299
+ if (pr.sonarFailures !== void 0) {
1300
+ process.stdout.write(formatSonarFailures(pr.sonarFailures, pr.sonarcloudUrl));
1301
+ }
867
1302
  }
868
1303
  });
869
1304
  var ANSI_ESCAPE_RE = new RegExp("\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "g");
@@ -1075,6 +1510,7 @@ minor segment is incremented (e.g. 1.2.0 \u2192 1.3.0).`
1075
1510
  var gitCommand = new Command2("git").description("Git workflow commands (some require gh CLI)").addCommand(getPrInfoCmd).addCommand(getPrCommentsCmd).addCommand(finishFeatureCmd).addCommand(publishReleaseCmd);
1076
1511
 
1077
1512
  // src/commands/getReady.ts
1513
+ import { createInterface as createInterface2 } from "readline";
1078
1514
  import { Command as Command3 } from "commander";
1079
1515
 
1080
1516
  // src/config/githubService.ts
@@ -1094,14 +1530,14 @@ function run3(cmd, args) {
1094
1530
  status: result.status ?? 1
1095
1531
  };
1096
1532
  }
1097
- function listIssues(technique, value) {
1533
+ function listIssues(technique, value, limit = 10) {
1098
1534
  const baseArgs = [
1099
1535
  "issue",
1100
1536
  "list",
1101
1537
  "--state",
1102
1538
  "open",
1103
1539
  "--limit",
1104
- "1",
1540
+ String(limit),
1105
1541
  "--json",
1106
1542
  "number,title,body,url"
1107
1543
  ];
@@ -1121,11 +1557,7 @@ function listIssues(technique, value) {
1121
1557
  if (status !== 0) {
1122
1558
  throw new Error(stderr.trim() || "Failed to query GitHub issues. Is `gh` installed and authenticated?");
1123
1559
  }
1124
- const issues = JSON.parse(stdout);
1125
- if (issues.length === 0) {
1126
- return null;
1127
- }
1128
- return issues[0];
1560
+ return JSON.parse(stdout);
1129
1561
  }
1130
1562
  function postComment(issueNumber, body) {
1131
1563
  const { stderr, status } = run3("gh", ["issue", "comment", String(issueNumber), "--body", body]);
@@ -1138,7 +1570,7 @@ function postComment(issueNumber, body) {
1138
1570
  import { spawn, spawnSync as spawnSync4 } from "child_process";
1139
1571
  import { createInterface } from "readline";
1140
1572
  import { existsSync } from "fs";
1141
- import { delimiter, join as join2 } from "path";
1573
+ import { delimiter, join as join3 } from "path";
1142
1574
 
1143
1575
  // src/cli/spawnUtils.ts
1144
1576
  function truncate(str, max) {
@@ -1173,7 +1605,7 @@ function handleExitCode(status, toolName) {
1173
1605
  function resolveCommand(name) {
1174
1606
  const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
1175
1607
  for (const dir of pathDirs) {
1176
- const candidate = join2(dir, name);
1608
+ const candidate = join3(dir, name);
1177
1609
  if (existsSync(candidate)) return candidate;
1178
1610
  }
1179
1611
  return name;
@@ -1209,7 +1641,7 @@ function invokeClaudeCodeSync(prompt, yolo, model) {
1209
1641
  handleExitCode(result.status, "Claude Code");
1210
1642
  }
1211
1643
  function invokeClaudeCodeVerbose(prompt, yolo, model) {
1212
- return new Promise((resolve2) => {
1644
+ return new Promise((resolve3) => {
1213
1645
  const claudeBin = resolveCommand("claude");
1214
1646
  const args = [];
1215
1647
  if (yolo) args.push("--dangerously-skip-permissions");
@@ -1231,7 +1663,7 @@ function invokeClaudeCodeVerbose(prompt, yolo, model) {
1231
1663
  });
1232
1664
  child.on("close", (code) => {
1233
1665
  handleExitCode(code, "Claude Code");
1234
- resolve2();
1666
+ resolve3();
1235
1667
  });
1236
1668
  });
1237
1669
  }
@@ -1318,8 +1750,37 @@ function invokeCodexCodeSync(prompt, yolo) {
1318
1750
  }
1319
1751
 
1320
1752
  // src/commands/getReady.ts
1321
- var implementNextCommand = new Command3("implement-next").description("Find the next open GitHub issue matching the configured filter, claim it, and invoke the AI code assistant (Claude or Codex)").option("--json", "Output issue details as JSON").option("--no-claude", "Skip all AI invocation (Claude or Codex) after claiming the issue").option("--codex", "Use Codex CLI instead of Claude Code").option("--query-only", "Print issue content and exit without claiming or invoking any AI tools").option("--yolo", "Launch with --dangerously-skip-permissions (Claude) or --dangerously-bypass-approvals-and-sandbox (Codex)").option("--verbose", "Show step-by-step progress summary and final result").option("--opus", "Use claude-opus-4-6").option("--sonnet", "Use claude-sonnet-4-6").option("--haiku", "Use claude-haiku-4-5-20251001").action(async (options) => {
1322
- const config = readConfig();
1753
+ function writeOverflowHint(output, issues, limit) {
1754
+ if (issues.length === limit) {
1755
+ output.write(`(Showing first ${limit} matching issues \u2014 there may be more. Use --limit to fetch more.)
1756
+ `);
1757
+ }
1758
+ }
1759
+ function writeIssueList(output, issues, limit) {
1760
+ output.write("\nAvailable issues:\n");
1761
+ for (let i = 0; i < issues.length; i++) {
1762
+ output.write(` [${i + 1}] #${issues[i].number} - ${issues[i].title}
1763
+ `);
1764
+ }
1765
+ writeOverflowHint(output, issues, limit);
1766
+ }
1767
+ async function promptSelection(issues, limit, output) {
1768
+ writeIssueList(output, issues, limit);
1769
+ const rl = createInterface2({ input: process.stdin, output });
1770
+ const answer = await new Promise(
1771
+ (resolve3) => rl.question(`
1772
+ Select issue (1-${issues.length}): `, resolve3)
1773
+ );
1774
+ rl.close();
1775
+ const n = Number.parseInt(answer.trim(), 10);
1776
+ if (Number.isNaN(n) || n < 1 || n > issues.length) {
1777
+ process.stderr.write(`Error: Invalid selection "${answer.trim()}". Enter a number between 1 and ${issues.length}.
1778
+ `);
1779
+ process.exit(1);
1780
+ }
1781
+ return issues[n - 1];
1782
+ }
1783
+ function validateConfig(config) {
1323
1784
  if (config.remoteType !== "gh") {
1324
1785
  process.stderr.write(
1325
1786
  "Error: implement-next is not supported in Azure DevOps mode. Work item discovery is not available in azdo-cli. See docs/azdo-gap.md for details.\n"
@@ -1338,24 +1799,58 @@ var implementNextCommand = new Command3("implement-next").description("Find the
1338
1799
  );
1339
1800
  process.exit(1);
1340
1801
  }
1802
+ }
1803
+ async function resolveIssue(issues, options, limit) {
1804
+ const selectionOutput = options.json ? process.stderr : process.stdout;
1805
+ if (issues.length === 0) {
1806
+ process.stdout.write("No issues found matching the configured filter.\n");
1807
+ process.exit(0);
1808
+ }
1809
+ if (issues.length > 1 && options.queryOnly) {
1810
+ writeIssueList(selectionOutput, issues, limit);
1811
+ process.exit(0);
1812
+ }
1341
1813
  let issue;
1814
+ if (issues.length === 1) {
1815
+ issue = issues[0];
1816
+ selectionOutput.write(`Issue: #${issue.number}
1817
+ Title: ${issue.title}
1818
+ `);
1819
+ } else if (options.takeFirst) {
1820
+ issue = issues[0];
1821
+ selectionOutput.write(`Selecting issue #${issue.number}: ${issue.title}
1822
+ `);
1823
+ } else {
1824
+ issue = await promptSelection(issues, limit, selectionOutput);
1825
+ selectionOutput.write(`
1826
+ Issue: #${issue.number}
1827
+ Title: ${issue.title}
1828
+ `);
1829
+ }
1830
+ return issue;
1831
+ }
1832
+ var implementNextCommand = new Command3("implement-next").description("Find the next open GitHub issue matching the configured filter, claim it, and invoke the AI code assistant (Claude or Codex)").option("--json", "Output issue details as JSON").option("--no-claude", "Skip all AI invocation (Claude or Codex) after claiming the issue").option("--codex", "Use Codex CLI instead of Claude Code").option("--query-only", "Print issue content and exit without claiming or invoking any AI tools").option("--yolo", "Launch with --dangerously-skip-permissions (Claude) or --dangerously-bypass-approvals-and-sandbox (Codex)").option("--verbose", "Show step-by-step progress summary and final result").option("--opus", "Use claude-opus-4-6").option("--sonnet", "Use claude-sonnet-4-6").option("--haiku", "Use claude-haiku-4-5-20251001").option("--take-first", "When multiple issues match, pick the first without prompting").option("--limit <n>", "Max issues to fetch and display (default: 10)", "10").action(async (options) => {
1833
+ const config = readConfig();
1834
+ validateConfig(config);
1835
+ const limit = Number.parseInt(options.limit, 10);
1836
+ if (Number.isNaN(limit) || limit <= 0) {
1837
+ process.stderr.write(`Error: --limit must be a positive integer (got "${options.limit}").
1838
+ `);
1839
+ process.exit(1);
1840
+ }
1841
+ let issues;
1342
1842
  try {
1343
- issue = listIssues(config.issueDiscoveryTechnique, config.issueDiscoveryValue);
1843
+ issues = listIssues(config.issueDiscoveryTechnique, config.issueDiscoveryValue, limit);
1344
1844
  } catch (err) {
1345
1845
  process.stderr.write(`Error: ${err.message}
1346
1846
  `);
1347
1847
  process.exit(1);
1348
1848
  }
1349
- if (issue === null) {
1350
- process.stdout.write("No issues found matching the configured filter.\n");
1351
- process.exit(0);
1352
- }
1849
+ const issue = await resolveIssue(issues, options, limit);
1353
1850
  if (options.json) {
1354
1851
  process.stdout.write(JSON.stringify({ number: issue.number, title: issue.title, body: issue.body, url: issue.url }, null, 2) + "\n");
1355
1852
  } else {
1356
- process.stdout.write(`Issue: #${issue.number}
1357
- Title: ${issue.title}
1358
- URL: ${issue.url}
1853
+ process.stdout.write(`URL: ${issue.url}
1359
1854
 
1360
1855
  ${issue.body}
1361
1856
  `);
@@ -1372,7 +1867,9 @@ ${issue.body}
1372
1867
  }
1373
1868
  if (options.claude !== false) {
1374
1869
  const systemPrompt = config.claudeSystemPrompt ?? DEFAULT_CLAUDE_SYSTEM_PROMPT;
1375
- const prompt = `${systemPrompt}
1870
+ const prompt = `Resolving issue #${issue.number}:
1871
+
1872
+ ${systemPrompt}
1376
1873
 
1377
1874
  ${issue.body}`;
1378
1875
  if (options.codex) {
@@ -1403,6 +1900,9 @@ function withPush(prompt, push) {
1403
1900
 
1404
1901
  ${PUSH_INSTRUCTION}` : prompt;
1405
1902
  }
1903
+ function formatPrInfoContext(pr) {
1904
+ return JSON.stringify(pr, null, 2);
1905
+ }
1406
1906
  function addAiOptions(cmd) {
1407
1907
  return cmd.option("--codex", "Use Codex CLI instead of Claude Code").option("--verbose", "Show step-by-step progress (Claude only; ignored for Codex)").option("--push", "Append instruction to commit and push changes after the AI finishes").option("--opus", "Use claude-opus-4-6 (Claude only)").option("--sonnet", "Use claude-sonnet-4-6 (Claude only)").option("--haiku", "Use claude-haiku-4-5-20251001 (Claude only)");
1408
1908
  }
@@ -1444,7 +1944,10 @@ var executeSonarCmd = addAiOptions(
1444
1944
  const fullPrompt = withPush(
1445
1945
  `${sonarPromptText}
1446
1946
 
1447
- SonarCloud analysis URL: ${pr.sonarcloudUrl}`,
1947
+ SonarCloud analysis URL: ${pr.sonarcloudUrl}
1948
+
1949
+ Current PR context from automata git get-pr-info --json:
1950
+ ${formatPrInfoContext(pr)}`,
1448
1951
  options.push
1449
1952
  );
1450
1953
  if (options.codex) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.3.0-develop.96",
3
+ "version": "0.4.0-develop.124",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {