@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.
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +17 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +25 -33
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/logs.d.ts.map +1 -1
- package/dist/cli/commands/logs.js +4 -11
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/commands/setup-app.d.ts.map +1 -1
- package/dist/cli/commands/setup-app.js +19 -15
- package/dist/cli/commands/setup-app.js.map +1 -1
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/context.d.ts.map +1 -1
- package/dist/cli/context.js +8 -2
- package/dist/cli/context.js.map +1 -1
- package/dist/cli/files.d.ts.map +1 -1
- package/dist/cli/files.js +27 -30
- package/dist/cli/files.js.map +1 -1
- package/dist/cli/git.d.ts +8 -3
- package/dist/cli/git.d.ts.map +1 -1
- package/dist/cli/git.js +24 -13
- package/dist/cli/git.js.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/input.d.ts +7 -0
- package/dist/cli/input.d.ts.map +1 -1
- package/dist/cli/input.js +13 -2
- package/dist/cli/input.js.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +62 -19
- package/dist/cli/main.js.map +1 -1
- package/dist/config/writer.d.ts.map +1 -1
- package/dist/config/writer.js +18 -0
- package/dist/config/writer.js.map +1 -1
- package/dist/evals/index.js +1 -1
- package/dist/evals/index.js.map +1 -1
- package/dist/output/github-issues.d.ts.map +1 -1
- package/dist/output/github-issues.js +14 -8
- package/dist/output/github-issues.js.map +1 -1
- package/dist/sdk/analyze.d.ts.map +1 -1
- package/dist/sdk/analyze.js +2 -2
- package/dist/sdk/analyze.js.map +1 -1
- package/dist/sdk/auth.d.ts.map +1 -1
- package/dist/sdk/auth.js +2 -2
- package/dist/sdk/auth.js.map +1 -1
- package/dist/sdk/errors.d.ts +3 -1
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/errors.js +2 -2
- package/dist/sdk/errors.js.map +1 -1
- package/dist/skills/remote.js +1 -1
- package/dist/skills/remote.js.map +1 -1
- package/dist/utils/exec.d.ts +4 -1
- package/dist/utils/exec.d.ts.map +1 -1
- package/dist/utils/exec.js +6 -4
- package/dist/utils/exec.js.map +1 -1
- package/package.json +1 -1
- package/skills/warden-sweep/SKILL.md +67 -74
- package/skills/warden-sweep/references/patch-prompt.md +72 -0
- package/skills/warden-sweep/references/verify-prompt.md +25 -0
- package/skills/warden-sweep/scripts/_utils.py +62 -0
- package/skills/warden-sweep/scripts/create_issue.py +189 -0
- package/skills/warden-sweep/scripts/find_reviewers.py +16 -17
- package/skills/warden-sweep/scripts/generate_report.py +20 -25
- package/skills/warden-sweep/scripts/organize.py +128 -21
- 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
|
-
|
|
148
|
-
|
|
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 =
|
|
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
|
-
|
|
354
|
-
if
|
|
355
|
-
skills.add(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
485
|
+
with index_lock:
|
|
486
|
+
with open(scan_index_path, "a") as f:
|
|
487
|
+
f.write(json.dumps(entry) + "\n")
|
|
555
488
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
-
|
|
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 -
|
|
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 -
|
|
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
|
|
579
|
+
if total_failed > 0:
|
|
628
580
|
sys.exit(2)
|
|
629
581
|
|
|
630
582
|
|