@sentry/warden 0.14.0 → 0.15.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 (67) hide show
  1. package/dist/cli/args.d.ts +1 -0
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +17 -2
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/cli/commands/add.d.ts.map +1 -1
  6. package/dist/cli/commands/add.js +25 -33
  7. package/dist/cli/commands/add.js.map +1 -1
  8. package/dist/cli/commands/logs.d.ts.map +1 -1
  9. package/dist/cli/commands/logs.js +4 -11
  10. package/dist/cli/commands/logs.js.map +1 -1
  11. package/dist/cli/commands/setup-app.d.ts.map +1 -1
  12. package/dist/cli/commands/setup-app.js +19 -15
  13. package/dist/cli/commands/setup-app.js.map +1 -1
  14. package/dist/cli/context.d.ts +2 -0
  15. package/dist/cli/context.d.ts.map +1 -1
  16. package/dist/cli/context.js +8 -2
  17. package/dist/cli/context.js.map +1 -1
  18. package/dist/cli/files.d.ts.map +1 -1
  19. package/dist/cli/files.js +27 -30
  20. package/dist/cli/files.js.map +1 -1
  21. package/dist/cli/git.d.ts +8 -3
  22. package/dist/cli/git.d.ts.map +1 -1
  23. package/dist/cli/git.js +24 -13
  24. package/dist/cli/git.js.map +1 -1
  25. package/dist/cli/index.js +10 -0
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/input.d.ts +7 -0
  28. package/dist/cli/input.d.ts.map +1 -1
  29. package/dist/cli/input.js +13 -2
  30. package/dist/cli/input.js.map +1 -1
  31. package/dist/cli/main.d.ts.map +1 -1
  32. package/dist/cli/main.js +62 -19
  33. package/dist/cli/main.js.map +1 -1
  34. package/dist/config/writer.d.ts.map +1 -1
  35. package/dist/config/writer.js +18 -0
  36. package/dist/config/writer.js.map +1 -1
  37. package/dist/evals/index.js +1 -1
  38. package/dist/evals/index.js.map +1 -1
  39. package/dist/output/github-issues.d.ts.map +1 -1
  40. package/dist/output/github-issues.js +14 -8
  41. package/dist/output/github-issues.js.map +1 -1
  42. package/dist/sdk/analyze.d.ts.map +1 -1
  43. package/dist/sdk/analyze.js +2 -2
  44. package/dist/sdk/analyze.js.map +1 -1
  45. package/dist/sdk/auth.d.ts.map +1 -1
  46. package/dist/sdk/auth.js +2 -2
  47. package/dist/sdk/auth.js.map +1 -1
  48. package/dist/sdk/errors.d.ts +3 -1
  49. package/dist/sdk/errors.d.ts.map +1 -1
  50. package/dist/sdk/errors.js +2 -2
  51. package/dist/sdk/errors.js.map +1 -1
  52. package/dist/skills/remote.js +1 -1
  53. package/dist/skills/remote.js.map +1 -1
  54. package/dist/utils/exec.d.ts +4 -1
  55. package/dist/utils/exec.d.ts.map +1 -1
  56. package/dist/utils/exec.js +6 -4
  57. package/dist/utils/exec.js.map +1 -1
  58. package/package.json +1 -1
  59. package/skills/warden-sweep/SKILL.md +67 -74
  60. package/skills/warden-sweep/references/patch-prompt.md +72 -0
  61. package/skills/warden-sweep/references/verify-prompt.md +25 -0
  62. package/skills/warden-sweep/scripts/_utils.py +62 -0
  63. package/skills/warden-sweep/scripts/create_issue.py +189 -0
  64. package/skills/warden-sweep/scripts/find_reviewers.py +16 -17
  65. package/skills/warden-sweep/scripts/generate_report.py +20 -25
  66. package/skills/warden-sweep/scripts/organize.py +128 -21
  67. package/skills/warden-sweep/scripts/scan.py +82 -130
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
3
  # requires-python = ">=3.9"
4
+ # dependencies = ["tomli; python_version < '3.11'"]
4
5
  # ///
5
6
  """
6
7
  Warden Sweep: Scan phase.
@@ -28,12 +29,19 @@ import os
28
29
  import secrets
29
30
  import subprocess
30
31
  import sys
32
+ import threading
33
+ from concurrent.futures import ThreadPoolExecutor, as_completed
31
34
  from datetime import datetime, timezone
32
35
  from pathlib import Path
33
36
  from typing import Any
34
37
 
38
+ try:
39
+ import tomllib
40
+ except ModuleNotFoundError:
41
+ import tomli as tomllib # type: ignore[no-redefine]
42
+
35
43
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
36
- from _utils import run_cmd # noqa: E402
44
+ from _utils import ensure_github_label, run_cmd # noqa: E402
37
45
 
38
46
 
39
47
  SUPPORTED_EXTENSIONS = {
@@ -66,22 +74,6 @@ def create_sweep_dir(sweep_dir: str) -> None:
66
74
  os.makedirs(os.path.join(sweep_dir, subdir), exist_ok=True)
67
75
 
68
76
 
69
- def create_warden_label() -> None:
70
- """Create the warden label on GitHub (idempotent)."""
71
- try:
72
- subprocess.run(
73
- [
74
- "gh", "label", "create", "warden",
75
- "--color", "5319E7",
76
- "--description", "Automated fix from Warden Sweep",
77
- ],
78
- capture_output=True,
79
- timeout=15,
80
- )
81
- except (subprocess.TimeoutExpired, FileNotFoundError):
82
- pass
83
-
84
-
85
77
  def write_manifest(sweep_dir: str, run_id: str) -> None:
86
78
  """Write the initial manifest.json."""
87
79
  repo = "unknown"
@@ -101,6 +93,7 @@ def write_manifest(sweep_dir: str, run_id: str) -> None:
101
93
  "phases": {
102
94
  "scan": "pending",
103
95
  "verify": "pending",
96
+ "issue": "pending",
104
97
  "patch": "pending",
105
98
  "organize": "pending",
106
99
  },
@@ -112,84 +105,16 @@ def write_manifest(sweep_dir: str, run_id: str) -> None:
112
105
  f.write("\n")
113
106
 
114
107
 
115
- def _strip_toml_inline_comment(line: str) -> str:
116
- """Strip inline TOML comments (# outside of quoted strings)."""
117
- in_quote = False
118
- quote_char = ""
119
- for i, ch in enumerate(line):
120
- if in_quote:
121
- if ch == quote_char:
122
- in_quote = False
123
- elif ch in ('"', "'"):
124
- in_quote = True
125
- quote_char = ch
126
- elif ch == "#":
127
- return line[:i].rstrip()
128
- return line
129
-
130
-
131
- def _toml_array_to_json(value: str) -> str:
132
- """Convert a TOML array string to JSON-compatible format.
133
-
134
- Handles TOML single-quoted strings and trailing commas.
135
- Inline comments should be stripped before calling this function.
136
- """
137
- import re
138
- # Replace single-quoted strings with double-quoted (TOML literal strings)
139
- value = re.sub(r"'([^']*)'", r'"\1"', value)
140
- # Strip trailing comma before closing bracket
141
- value = re.sub(r",\s*]", "]", value)
142
- return value
143
-
144
-
145
108
  def load_ignore_paths() -> list[str]:
146
109
  """Load ignorePaths from warden.toml defaults if present."""
147
- try:
148
- # Try to parse warden.toml for defaults.ignorePaths
149
- toml_path = "warden.toml"
150
- if not os.path.exists(toml_path):
151
- return []
152
-
153
- with open(toml_path) as f:
154
- content = f.read()
155
-
156
- # Simple TOML parsing for ignorePaths in [defaults] section
157
- in_defaults = False
158
- collecting_value = False
159
- value_parts: list[str] = []
160
- for line in content.splitlines():
161
- stripped = line.strip()
162
- if collecting_value:
163
- # Skip TOML comment lines inside multiline arrays
164
- if stripped.startswith("#"):
165
- continue
166
- # Strip inline comments before accumulating
167
- stripped = _strip_toml_inline_comment(stripped)
168
- value_parts.append(stripped)
169
- combined = "".join(value_parts)
170
- if combined.count("[") <= combined.count("]"):
171
- try:
172
- return json.loads(_toml_array_to_json(combined))
173
- except json.JSONDecodeError:
174
- return []
175
- continue
176
- if stripped == "[defaults]":
177
- in_defaults = True
178
- continue
179
- if stripped.startswith("[") and stripped != "[defaults]":
180
- in_defaults = False
181
- continue
182
- if in_defaults and stripped.startswith("ignorePaths"):
183
- _, _, value = stripped.partition("=")
184
- value = _strip_toml_inline_comment(value.strip())
185
- if not value:
186
- continue
187
- try:
188
- return json.loads(_toml_array_to_json(value))
189
- except json.JSONDecodeError:
190
- value_parts = [value]
191
- collecting_value = True
110
+ toml_path = "warden.toml"
111
+ if not os.path.exists(toml_path):
192
112
  return []
113
+ try:
114
+ with open(toml_path, "rb") as f:
115
+ config = tomllib.load(f)
116
+ paths = config.get("defaults", {}).get("ignorePaths", [])
117
+ return paths if isinstance(paths, list) else []
193
118
  except Exception:
194
119
  return []
195
120
 
@@ -246,7 +171,7 @@ def enumerate_files(
246
171
  ) -> list[str]:
247
172
  """Enumerate files to scan using git ls-files, filtered by extension."""
248
173
  if specific_files:
249
- return specific_files
174
+ return [f for f in specific_files if not should_ignore(f, ignore_patterns)]
250
175
 
251
176
  result = run_cmd(["git", "ls-files"])
252
177
  if result.returncode != 0:
@@ -301,19 +226,22 @@ def log_path_for_file(sweep_dir: str, file_path: str) -> str:
301
226
 
302
227
 
303
228
  def scan_file(
304
- file_path: str, log_file: str, timeout: int = 300
229
+ file_path: str, log_file: str, timeout: int = 600, skill: str | None = None
305
230
  ) -> dict[str, Any]:
306
231
  """Run warden on a single file. Returns scan-index entry."""
307
232
  try:
233
+ cmd = [
234
+ "warden", file_path,
235
+ "--json", "--log",
236
+ "--min-confidence", "off",
237
+ "--fail-on", "off",
238
+ "--quiet",
239
+ "--output", log_file,
240
+ ]
241
+ if skill:
242
+ cmd.extend(["--skill", skill])
308
243
  result = subprocess.run(
309
- [
310
- "warden", file_path,
311
- "--json", "--log",
312
- "--min-confidence", "off",
313
- "--fail-on", "off",
314
- "--quiet",
315
- "--output", log_file,
316
- ],
244
+ cmd,
317
245
  capture_output=True,
318
246
  text=True,
319
247
  timeout=timeout,
@@ -350,9 +278,9 @@ def scan_file(
350
278
  record = json.loads(line)
351
279
  if record.get("type") == "summary":
352
280
  continue
353
- skill = record.get("skill", "")
354
- if skill:
355
- skills.add(skill)
281
+ record_skill = record.get("skill", "")
282
+ if record_skill:
283
+ skills.add(record_skill)
356
284
  findings = record.get("findings", [])
357
285
  finding_count += len(findings)
358
286
  except json.JSONDecodeError:
@@ -482,6 +410,10 @@ def main() -> None:
482
410
  "--sweep-dir",
483
411
  help="Resume into an existing sweep directory",
484
412
  )
413
+ parser.add_argument(
414
+ "--skill",
415
+ help="Run only this skill (passed through to warden --skill)",
416
+ )
485
417
  args = parser.parse_args()
486
418
 
487
419
  # Check dependencies
@@ -510,7 +442,7 @@ def main() -> None:
510
442
  if not os.path.exists(manifest_path):
511
443
  write_manifest(sweep_dir, run_id)
512
444
 
513
- create_warden_label()
445
+ ensure_github_label("warden", "5319E7", "Automated fix from Warden Sweep")
514
446
 
515
447
  # Enumerate files
516
448
  ignore_patterns = load_ignore_paths()
@@ -542,30 +474,41 @@ def main() -> None:
542
474
  file=sys.stderr,
543
475
  )
544
476
 
545
- # Scan remaining files
477
+ # Scan remaining files concurrently
546
478
  scanned = already_done
479
+ index_lock = threading.Lock()
547
480
 
548
- for i, file_path in enumerate(remaining, start=1):
481
+ def _scan_and_record(file_path: str) -> dict[str, Any]:
549
482
  log_file = log_path_for_file(sweep_dir, file_path)
550
- entry = scan_file(file_path, log_file)
483
+ entry = scan_file(file_path, log_file, skill=args.skill)
551
484
 
552
- # Append to scan-index.jsonl
553
- with open(scan_index_path, "a") as f:
554
- f.write(json.dumps(entry) + "\n")
485
+ with index_lock:
486
+ with open(scan_index_path, "a") as f:
487
+ f.write(json.dumps(entry) + "\n")
555
488
 
556
- scanned += 1
557
- if entry["status"] == "error":
558
- print(
559
- f"[{scanned}/{total}] {file_path} (ERROR: {entry.get('error', 'unknown')})",
560
- file=sys.stderr,
561
- )
562
- else:
563
- count = entry.get("findingCount", 0)
564
- suffix = f"({count} finding{'s' if count != 1 else ''})" if count > 0 else ""
565
- print(
566
- f"[{scanned}/{total}] {file_path} {suffix}".rstrip(),
567
- file=sys.stderr,
568
- )
489
+ return entry
490
+
491
+ with ThreadPoolExecutor(max_workers=4) as pool:
492
+ futures = {
493
+ pool.submit(_scan_and_record, fp): fp for fp in remaining
494
+ }
495
+ for future in as_completed(futures):
496
+ entry = future.result()
497
+ scanned += 1
498
+ file_path = entry.get("file", futures[future])
499
+ if entry["status"] == "error":
500
+ label = "TIMEOUT" if entry.get("error") == "timeout" else "ERROR"
501
+ print(
502
+ f"[{scanned}/{total}] {file_path} ({label}: {entry.get('error', 'unknown')})",
503
+ file=sys.stderr,
504
+ )
505
+ else:
506
+ count = entry.get("findingCount", 0)
507
+ suffix = f"({count} finding{'s' if count != 1 else ''})" if count > 0 else ""
508
+ print(
509
+ f"[{scanned}/{total}] {file_path} {suffix}".rstrip(),
510
+ file=sys.stderr,
511
+ )
569
512
 
570
513
  # Extract findings
571
514
  script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -578,6 +521,7 @@ def main() -> None:
578
521
  # so that resumed scans don't include stale errors for files that later succeeded.
579
522
  # Scope to current file list so counts stay consistent with `scanned`.
580
523
  files_set = set(files)
524
+ timeouts: list[dict[str, Any]] = []
581
525
  errors: list[dict[str, Any]] = []
582
526
  if os.path.exists(scan_index_path):
583
527
  last_status: dict[str, dict[str, Any]] = {}
@@ -595,36 +539,44 @@ def main() -> None:
595
539
  continue
596
540
  for entry in last_status.values():
597
541
  if entry.get("status") == "error":
598
- errors.append({
542
+ item = {
599
543
  "file": entry.get("file", ""),
600
544
  "error": entry.get("error", "unknown"),
601
545
  "exitCode": entry.get("exitCode", -1),
602
- })
546
+ }
547
+ if entry.get("error") == "timeout":
548
+ timeouts.append(item)
549
+ else:
550
+ errors.append(item)
551
+
552
+ total_failed = len(timeouts) + len(errors)
603
553
 
604
554
  # Output JSON summary
605
555
  output = {
606
556
  "runId": run_id,
607
557
  "sweepDir": sweep_dir,
608
- "filesScanned": scanned - len(errors),
558
+ "filesScanned": scanned - total_failed,
559
+ "filesTimedOut": len(timeouts),
609
560
  "filesErrored": len(errors),
610
561
  "totalFindings": len(findings),
611
562
  "bySeverity": by_severity,
612
563
  "findingsPath": os.path.join(sweep_dir, "data", "all-findings.jsonl"),
613
564
  "findings": findings,
565
+ "timeouts": timeouts,
614
566
  "errors": errors,
615
567
  }
616
568
 
617
569
  print(json.dumps(output, indent=2))
618
570
 
619
571
  # Fatal only if every file across all runs errored (no successful scans at all)
620
- successful = scanned - len(errors)
572
+ successful = scanned - total_failed
621
573
  if successful == 0 and scanned > 0:
622
574
  update_manifest_phase(sweep_dir, "scan", "error")
623
575
  sys.exit(1)
624
576
 
625
577
  update_manifest_phase(sweep_dir, "scan", "complete")
626
578
 
627
- if len(errors) > 0:
579
+ if total_failed > 0:
628
580
  sys.exit(2)
629
581
 
630
582