automata-cli 0.3.0-develop.93 → 0.3.0

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 +481 -38
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -19,10 +19,13 @@ 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";
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.";
26
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.";
27
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.";
28
31
  var CONFIG_DIR = ".automata";
@@ -30,7 +33,32 @@ var CONFIG_FILE = "config.json";
30
33
  function configPath() {
31
34
  return join(process.cwd(), CONFIG_DIR, CONFIG_FILE);
32
35
  }
33
- 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() {
34
62
  try {
35
63
  const raw = readFileSync2(configPath(), "utf8");
36
64
  return JSON.parse(raw);
@@ -38,6 +66,26 @@ function readConfig() {
38
66
  return {};
39
67
  }
40
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
+ }
41
89
  function writeConfig(config) {
42
90
  const dir = join(process.cwd(), CONFIG_DIR);
43
91
  mkdirSync(dir, { recursive: true });
@@ -46,6 +94,11 @@ function writeConfig(config) {
46
94
 
47
95
  // src/config/ConfigWizard.tsx
48
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
+ }
49
102
  var REMOTE_OPTIONS = [
50
103
  { label: "GitHub", value: "gh" },
51
104
  { label: "Azure DevOps", value: "azdo" }
@@ -59,6 +112,7 @@ var MAIN_MENU_OPTIONS = ["Remote / Mode", "Implement-Next", "Prompts"];
59
112
  var PROMPTS_MENU_OPTIONS = ["Sonar", "Fix-Comments"];
60
113
  function ConfigWizard() {
61
114
  const existing = readConfig();
115
+ const rawExisting = readRawConfig();
62
116
  const initialRemoteIndex = REMOTE_OPTIONS.findIndex((o) => o.value === existing.remoteType);
63
117
  const initialTechIndex = TECHNIQUE_OPTIONS.findIndex((o) => o.value === existing.issueDiscoveryTechnique);
64
118
  const [screen, setScreen] = useState("main");
@@ -106,7 +160,7 @@ function ConfigWizard() {
106
160
  if (chosen.value === "gh") {
107
161
  setScreen("technique");
108
162
  } else {
109
- writeConfig({ ...existing, remoteType: chosen.value });
163
+ writeConfig({ ...rawExisting, remoteType: chosen.value });
110
164
  exit();
111
165
  }
112
166
  } else if (key.escape) {
@@ -142,12 +196,17 @@ function ConfigWizard() {
142
196
  }
143
197
  } else if (screen === "system-prompt") {
144
198
  if (key.return) {
199
+ let claudeSystemPromptValue;
200
+ if (systemPrompt) {
201
+ writePromptFile("claude-system-prompt.md", systemPrompt);
202
+ claudeSystemPromptValue = "claude-system-prompt.md";
203
+ }
145
204
  writeConfig({
146
- ...existing,
205
+ ...rawExisting,
147
206
  remoteType: pendingRemote,
148
207
  issueDiscoveryTechnique: pendingTechnique,
149
208
  issueDiscoveryValue: discoveryValue || void 0,
150
- claudeSystemPrompt: systemPrompt || void 0
209
+ claudeSystemPrompt: claudeSystemPromptValue
151
210
  });
152
211
  exit();
153
212
  } else if (key.backspace || key.delete) {
@@ -178,10 +237,15 @@ function ConfigWizard() {
178
237
  }
179
238
  } else if (screen === "sonar-prompt") {
180
239
  if (key.return) {
181
- 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();
182
246
  writeConfig({
183
247
  ...current,
184
- prompts: { ...current.prompts, sonar: sonarPrompt || void 0 }
248
+ prompts: { ...current.prompts, sonar: sonarValue }
185
249
  });
186
250
  setScreen("prompts-menu");
187
251
  } else if (key.backspace || key.delete) {
@@ -195,10 +259,15 @@ function ConfigWizard() {
195
259
  }
196
260
  } else if (screen === "fix-comments-prompt") {
197
261
  if (key.return) {
198
- 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();
199
268
  writeConfig({
200
269
  ...current,
201
- prompts: { ...current.prompts, fixComments: fixCommentsPrompt || void 0 }
270
+ prompts: { ...current.prompts, fixComments: fixCommentsValue }
202
271
  });
203
272
  setScreen("prompts-menu");
204
273
  } else if (key.backspace || key.delete) {
@@ -337,7 +406,7 @@ var configSetType = new Command("type").description("Set the remote environment
337
406
  `);
338
407
  process.exit(1);
339
408
  }
340
- const current = readConfig();
409
+ const current = readRawConfig();
341
410
  writeConfig({ ...current, remoteType: value });
342
411
  process.stdout.write(`Remote type set to: ${value}
343
412
  `);
@@ -348,19 +417,19 @@ var configSetIssueDiscoveryTechnique = new Command("issue-discovery-technique").
348
417
  `);
349
418
  process.exit(1);
350
419
  }
351
- const current = readConfig();
420
+ const current = readRawConfig();
352
421
  writeConfig({ ...current, issueDiscoveryTechnique: value });
353
422
  process.stdout.write(`Issue discovery technique set to: ${value}
354
423
  `);
355
424
  });
356
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) => {
357
- const current = readConfig();
426
+ const current = readRawConfig();
358
427
  writeConfig({ ...current, issueDiscoveryValue: value });
359
428
  process.stdout.write(`Issue discovery value set to: ${value}
360
429
  `);
361
430
  });
362
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) => {
363
- const current = readConfig();
432
+ const current = readRawConfig();
364
433
  writeConfig({ ...current, claudeSystemPrompt: value });
365
434
  process.stdout.write(`Claude system prompt set.
366
435
  `);
@@ -441,6 +510,8 @@ function getCurrentBranch() {
441
510
  }
442
511
  return stdout.trim();
443
512
  }
513
+ var SONAR_FETCH_TIMEOUT_MS = 5e3;
514
+ var SONAR_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED"]);
444
515
  function parseOwnerRepo() {
445
516
  const { stdout, status } = run2("git", ["remote", "get-url", "origin"]);
446
517
  if (status !== 0) return null;
@@ -486,21 +557,285 @@ function extractSonarProjectKey(url) {
486
557
  return null;
487
558
  }
488
559
  }
489
- async function fetchSonarNewIssues(projectKey, prNumber) {
490
- 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) {
491
656
  const controller = new AbortController();
492
- const timeoutId = setTimeout(() => controller.abort(), 5e3);
657
+ const timeoutId = setTimeout(() => controller.abort(), SONAR_FETCH_TIMEOUT_MS);
493
658
  try {
494
659
  const response = await fetch(apiUrl, { signal: controller.signal });
495
- if (!response.ok) return null;
496
- const data = await response.json();
497
- 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
+ };
498
668
  } catch {
499
- return null;
669
+ return { ok: false, status: null };
500
670
  } finally {
501
671
  clearTimeout(timeoutId);
502
672
  }
503
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
+ }
504
839
  async function getPrInfoGh(branch) {
505
840
  const { stdout, stderr, status } = run2("gh", [
506
841
  "pr",
@@ -531,21 +866,21 @@ async function getPrInfoGh(branch) {
531
866
  detailsUrl: enriched?.detailsUrl || c.detailsUrl || ""
532
867
  };
533
868
  });
534
- const sonarCheck = checks.find((c) => {
535
- try {
536
- const hostname = new URL(c.detailsUrl).hostname;
537
- return hostname === "sonarcloud.io" || hostname.endsWith(".sonarcloud.io");
538
- } catch {
539
- return false;
540
- }
541
- });
869
+ const sonarCheck = checks.find((c) => isSonarUrl(c.detailsUrl));
542
870
  let sonarcloudUrl;
543
871
  let sonarNewIssues;
872
+ let sonarFailures;
544
873
  if (sonarCheck) {
545
874
  sonarcloudUrl = sonarCheck.detailsUrl;
546
875
  const projectKey = extractSonarProjectKey(sonarCheck.detailsUrl);
547
876
  if (projectKey) {
548
- 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
+ }
549
884
  } else {
550
885
  sonarNewIssues = null;
551
886
  }
@@ -556,7 +891,8 @@ async function getPrInfoGh(branch) {
556
891
  state: raw.state,
557
892
  url: raw.url,
558
893
  checks,
559
- ...sonarcloudUrl === void 0 ? {} : { sonarcloudUrl, sonarNewIssues }
894
+ ...sonarcloudUrl === void 0 ? {} : { sonarcloudUrl, sonarNewIssues },
895
+ ...sonarFailures === void 0 ? {} : { sonarFailures }
560
896
  };
561
897
  }
562
898
  async function getPrInfo2(branch) {
@@ -766,7 +1102,7 @@ function formatChecks(checks) {
766
1102
  return lines.join("\n") + "\n";
767
1103
  }
768
1104
  function sleep(ms) {
769
- return new Promise((resolve2) => setTimeout(resolve2, ms));
1105
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
770
1106
  }
771
1107
  function isSonarCheck(check) {
772
1108
  try {
@@ -788,6 +1124,103 @@ function formatFailedChecks(failed) {
788
1124
  }
789
1125
  return lines.join("\n") + "\n";
790
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
+ }
791
1224
  var POLL_INTERVAL_MS = 1e4;
792
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(
793
1226
  "after",
@@ -863,6 +1296,9 @@ URL: ${pr.url}
863
1296
  if (failed.length > 0) {
864
1297
  process.stdout.write(formatFailedChecks(failed));
865
1298
  }
1299
+ if (pr.sonarFailures !== void 0) {
1300
+ process.stdout.write(formatSonarFailures(pr.sonarFailures, pr.sonarcloudUrl));
1301
+ }
866
1302
  }
867
1303
  });
868
1304
  var ANSI_ESCAPE_RE = new RegExp("\x1B(?:[@-Z\\\\-_]|\\[[0-?]*[ -/]*[@-~])", "g");
@@ -1137,7 +1573,7 @@ function postComment(issueNumber, body) {
1137
1573
  import { spawn, spawnSync as spawnSync4 } from "child_process";
1138
1574
  import { createInterface } from "readline";
1139
1575
  import { existsSync } from "fs";
1140
- import { delimiter, join as join2 } from "path";
1576
+ import { delimiter, join as join3 } from "path";
1141
1577
 
1142
1578
  // src/cli/spawnUtils.ts
1143
1579
  function truncate(str, max) {
@@ -1172,7 +1608,7 @@ function handleExitCode(status, toolName) {
1172
1608
  function resolveCommand(name) {
1173
1609
  const pathDirs = (process.env["PATH"] ?? "").split(delimiter);
1174
1610
  for (const dir of pathDirs) {
1175
- const candidate = join2(dir, name);
1611
+ const candidate = join3(dir, name);
1176
1612
  if (existsSync(candidate)) return candidate;
1177
1613
  }
1178
1614
  return name;
@@ -1208,7 +1644,7 @@ function invokeClaudeCodeSync(prompt, yolo, model) {
1208
1644
  handleExitCode(result.status, "Claude Code");
1209
1645
  }
1210
1646
  function invokeClaudeCodeVerbose(prompt, yolo, model) {
1211
- return new Promise((resolve2) => {
1647
+ return new Promise((resolve3) => {
1212
1648
  const claudeBin = resolveCommand("claude");
1213
1649
  const args = [];
1214
1650
  if (yolo) args.push("--dangerously-skip-permissions");
@@ -1230,7 +1666,7 @@ function invokeClaudeCodeVerbose(prompt, yolo, model) {
1230
1666
  });
1231
1667
  child.on("close", (code) => {
1232
1668
  handleExitCode(code, "Claude Code");
1233
- resolve2();
1669
+ resolve3();
1234
1670
  });
1235
1671
  });
1236
1672
  }
@@ -1370,9 +1806,10 @@ ${issue.body}
1370
1806
  process.exit(1);
1371
1807
  }
1372
1808
  if (options.claude !== false) {
1373
- const prompt = config.claudeSystemPrompt ? `${config.claudeSystemPrompt}
1809
+ const systemPrompt = config.claudeSystemPrompt ?? DEFAULT_CLAUDE_SYSTEM_PROMPT;
1810
+ const prompt = `${systemPrompt}
1374
1811
 
1375
- ${issue.body}` : issue.body;
1812
+ ${issue.body}`;
1376
1813
  if (options.codex) {
1377
1814
  await invokeCodexCode(prompt, { yolo: options.yolo, verbose: options.verbose });
1378
1815
  } else {
@@ -1401,6 +1838,9 @@ function withPush(prompt, push) {
1401
1838
 
1402
1839
  ${PUSH_INSTRUCTION}` : prompt;
1403
1840
  }
1841
+ function formatPrInfoContext(pr) {
1842
+ return JSON.stringify(pr, null, 2);
1843
+ }
1404
1844
  function addAiOptions(cmd) {
1405
1845
  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)");
1406
1846
  }
@@ -1442,7 +1882,10 @@ var executeSonarCmd = addAiOptions(
1442
1882
  const fullPrompt = withPush(
1443
1883
  `${sonarPromptText}
1444
1884
 
1445
- SonarCloud analysis URL: ${pr.sonarcloudUrl}`,
1885
+ SonarCloud analysis URL: ${pr.sonarcloudUrl}
1886
+
1887
+ Current PR context from automata git get-pr-info --json:
1888
+ ${formatPrInfoContext(pr)}`,
1446
1889
  options.push
1447
1890
  );
1448
1891
  if (options.codex) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "automata-cli",
3
- "version": "0.3.0-develop.93",
3
+ "version": "0.3.0",
4
4
  "description": "Automata CLI tool",
5
5
  "type": "module",
6
6
  "bin": {