@sentry/warden 0.35.1 → 0.36.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 +12 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +14 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/init.js +2 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +16 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.d.ts +4 -2
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +34 -13
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/output/jsonl-schema-gen.d.ts.map +1 -1
- package/dist/cli/output/jsonl-schema-gen.js +3 -1
- package/dist/cli/output/jsonl-schema-gen.js.map +1 -1
- package/dist/cli/output/jsonl.d.ts +54 -0
- package/dist/cli/output/jsonl.d.ts.map +1 -1
- package/dist/cli/output/jsonl.js +6 -1
- package/dist/cli/output/jsonl.js.map +1 -1
- package/dist/cli/output/tasks.d.ts.map +1 -1
- package/dist/cli/output/tasks.js +7 -0
- package/dist/cli/output/tasks.js.map +1 -1
- package/dist/config/loader.d.ts +2 -2
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +3 -3
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +5 -5
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +3 -3
- package/dist/config/schema.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/sdk/analyze.d.ts.map +1 -1
- package/dist/sdk/analyze.js +139 -10
- package/dist/sdk/analyze.js.map +1 -1
- package/dist/sdk/auth.d.ts +5 -4
- package/dist/sdk/auth.d.ts.map +1 -1
- package/dist/sdk/auth.js +9 -7
- package/dist/sdk/auth.js.map +1 -1
- package/dist/sdk/circuit-breaker.js +3 -3
- package/dist/sdk/circuit-breaker.js.map +1 -1
- package/dist/sdk/errors.d.ts +8 -0
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/errors.js +44 -1
- package/dist/sdk/errors.js.map +1 -1
- package/dist/sdk/haiku.d.ts.map +1 -1
- package/dist/sdk/haiku.js +21 -12
- package/dist/sdk/haiku.js.map +1 -1
- package/dist/sdk/otel.d.ts +15 -0
- package/dist/sdk/otel.d.ts.map +1 -1
- package/dist/sdk/otel.js +50 -6
- package/dist/sdk/otel.js.map +1 -1
- package/dist/sdk/post-process.d.ts +2 -2
- package/dist/sdk/post-process.d.ts.map +1 -1
- package/dist/sdk/post-process.js +1 -1
- package/dist/sdk/post-process.js.map +1 -1
- package/dist/sdk/runtimes/claude.d.ts.map +1 -1
- package/dist/sdk/runtimes/claude.js +45 -35
- package/dist/sdk/runtimes/claude.js.map +1 -1
- package/dist/sdk/runtimes/pi.d.ts.map +1 -1
- package/dist/sdk/runtimes/pi.js +90 -6
- package/dist/sdk/runtimes/pi.js.map +1 -1
- package/dist/sdk/runtimes/types.d.ts +2 -2
- package/dist/sdk/runtimes/types.d.ts.map +1 -1
- package/dist/sdk/types.d.ts +12 -4
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/verify.d.ts +2 -2
- package/dist/sdk/verify.d.ts.map +1 -1
- package/dist/sdk/verify.js +1 -1
- package/dist/sdk/verify.js.map +1 -1
- package/dist/sentry-trace.d.ts +29 -0
- package/dist/sentry-trace.d.ts.map +1 -0
- package/dist/sentry-trace.js +154 -0
- package/dist/sentry-trace.js.map +1 -0
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +56 -0
- package/dist/sentry.js.map +1 -1
- package/dist/types/index.d.ts +72 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +37 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/warden/SKILL.md +0 -76
- package/skills/warden/SOURCES.md +0 -39
- package/skills/warden/SPEC.md +0 -112
- package/skills/warden/references/cli-reference.md +0 -149
- package/skills/warden/references/config-schema.md +0 -141
- package/skills/warden/references/configuration.md +0 -143
- package/skills/warden/references/creating-skills.md +0 -87
- package/skills/warden-sweep/SKILL.md +0 -78
- package/skills/warden-sweep/SOURCES.md +0 -43
- package/skills/warden-sweep/SPEC.md +0 -126
- package/skills/warden-sweep/references/issue-phase.md +0 -24
- package/skills/warden-sweep/references/organize-phase.md +0 -31
- package/skills/warden-sweep/references/patch-phase.md +0 -142
- package/skills/warden-sweep/references/patch-prompt.md +0 -72
- package/skills/warden-sweep/references/resume-and-artifacts.md +0 -51
- package/skills/warden-sweep/references/scan-phase.md +0 -46
- package/skills/warden-sweep/references/script-interfaces.md +0 -127
- package/skills/warden-sweep/references/verify-phase.md +0 -69
- package/skills/warden-sweep/references/verify-prompt.md +0 -25
- package/skills/warden-sweep/scripts/_utils.py +0 -99
- package/skills/warden-sweep/scripts/create_issue.py +0 -189
- package/skills/warden-sweep/scripts/extract_findings.py +0 -219
- package/skills/warden-sweep/scripts/find_reviewers.py +0 -114
- package/skills/warden-sweep/scripts/generate_report.py +0 -266
- package/skills/warden-sweep/scripts/index_prs.py +0 -187
- package/skills/warden-sweep/scripts/organize.py +0 -422
- package/skills/warden-sweep/scripts/scan.py +0 -584
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.9"
|
|
4
|
-
# ///
|
|
5
|
-
"""
|
|
6
|
-
Warden Sweep: Index existing PRs for deduplication.
|
|
7
|
-
|
|
8
|
-
Fetches open warden-labeled PRs via gh, identifies file overlap with
|
|
9
|
-
verified findings, and caches diffs for overlapping PRs.
|
|
10
|
-
|
|
11
|
-
Usage:
|
|
12
|
-
uv run index_prs.py <sweep-dir>
|
|
13
|
-
|
|
14
|
-
Stdout: JSON summary (for LLM consumption)
|
|
15
|
-
Stderr: Progress lines
|
|
16
|
-
|
|
17
|
-
Side effects:
|
|
18
|
-
- Creates data/existing-prs.json
|
|
19
|
-
- Creates data/pr-diffs/<number>.diff for overlapping PRs
|
|
20
|
-
"""
|
|
21
|
-
from __future__ import annotations
|
|
22
|
-
|
|
23
|
-
import argparse
|
|
24
|
-
import json
|
|
25
|
-
import os
|
|
26
|
-
import subprocess
|
|
27
|
-
import sys
|
|
28
|
-
from typing import Any
|
|
29
|
-
|
|
30
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
31
|
-
from _utils import read_jsonl, run_cmd # noqa: E402
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def fetch_warden_prs(sweep_dir: str) -> list[dict[str, Any]]:
|
|
35
|
-
"""Fetch open PRs with the warden label."""
|
|
36
|
-
result = run_cmd(
|
|
37
|
-
[
|
|
38
|
-
"gh", "pr", "list",
|
|
39
|
-
"--label", "warden",
|
|
40
|
-
"--state", "open",
|
|
41
|
-
"--json", "number,title,url,files",
|
|
42
|
-
"--limit", "100",
|
|
43
|
-
],
|
|
44
|
-
timeout=30,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
if result.returncode != 0:
|
|
48
|
-
print(f"Warning: gh pr list failed: {result.stderr}", file=sys.stderr)
|
|
49
|
-
return []
|
|
50
|
-
|
|
51
|
-
try:
|
|
52
|
-
prs = json.loads(result.stdout)
|
|
53
|
-
except json.JSONDecodeError:
|
|
54
|
-
print("Warning: Failed to parse gh pr list output", file=sys.stderr)
|
|
55
|
-
return []
|
|
56
|
-
|
|
57
|
-
# Save raw PR data
|
|
58
|
-
prs_path = os.path.join(sweep_dir, "data", "existing-prs.json")
|
|
59
|
-
with open(prs_path, "w") as f:
|
|
60
|
-
json.dump(prs, f, indent=2)
|
|
61
|
-
f.write("\n")
|
|
62
|
-
|
|
63
|
-
return prs
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def build_file_index(
|
|
67
|
-
prs: list[dict[str, Any]],
|
|
68
|
-
) -> dict[str, list[dict[str, Any]]]:
|
|
69
|
-
"""Build a file-to-PR lookup from the PR list."""
|
|
70
|
-
index: dict[str, list[dict[str, Any]]] = {}
|
|
71
|
-
|
|
72
|
-
for pr in prs:
|
|
73
|
-
pr_info = {
|
|
74
|
-
"number": pr.get("number"),
|
|
75
|
-
"title": pr.get("title", ""),
|
|
76
|
-
"url": pr.get("url", ""),
|
|
77
|
-
}
|
|
78
|
-
files = pr.get("files") or []
|
|
79
|
-
for file_entry in files:
|
|
80
|
-
# gh returns files as objects with "path" key
|
|
81
|
-
if isinstance(file_entry, dict):
|
|
82
|
-
path = file_entry.get("path", "")
|
|
83
|
-
else:
|
|
84
|
-
path = str(file_entry)
|
|
85
|
-
if path:
|
|
86
|
-
index.setdefault(path, []).append(pr_info)
|
|
87
|
-
|
|
88
|
-
return index
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def get_verified_files(sweep_dir: str) -> set[str]:
|
|
92
|
-
"""Get the set of files that have verified findings."""
|
|
93
|
-
verified_path = os.path.join(sweep_dir, "data", "verified.jsonl")
|
|
94
|
-
entries = read_jsonl(verified_path)
|
|
95
|
-
return {e.get("file", "") for e in entries if e.get("file")}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def fetch_pr_diff(pr_number: int, sweep_dir: str) -> bool:
|
|
99
|
-
"""Fetch and cache a PR diff. Returns True on success."""
|
|
100
|
-
diff_path = os.path.join(
|
|
101
|
-
sweep_dir, "data", "pr-diffs", f"{pr_number}.diff"
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
# Skip if already cached
|
|
105
|
-
if os.path.exists(diff_path):
|
|
106
|
-
return True
|
|
107
|
-
|
|
108
|
-
result = run_cmd(
|
|
109
|
-
["gh", "pr", "diff", str(pr_number)],
|
|
110
|
-
timeout=30,
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
if result.returncode != 0:
|
|
114
|
-
print(
|
|
115
|
-
f"Warning: Failed to fetch diff for PR #{pr_number}: {result.stderr}",
|
|
116
|
-
file=sys.stderr,
|
|
117
|
-
)
|
|
118
|
-
return False
|
|
119
|
-
|
|
120
|
-
with open(diff_path, "w") as f:
|
|
121
|
-
f.write(result.stdout)
|
|
122
|
-
|
|
123
|
-
return True
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def main() -> None:
|
|
127
|
-
parser = argparse.ArgumentParser(
|
|
128
|
-
description="Warden Sweep: Index existing PRs for dedup"
|
|
129
|
-
)
|
|
130
|
-
parser.add_argument("sweep_dir", help="Path to the sweep directory")
|
|
131
|
-
args = parser.parse_args()
|
|
132
|
-
|
|
133
|
-
sweep_dir = args.sweep_dir
|
|
134
|
-
|
|
135
|
-
if not os.path.isdir(sweep_dir):
|
|
136
|
-
print(
|
|
137
|
-
json.dumps({"error": f"Sweep directory not found: {sweep_dir}"}),
|
|
138
|
-
file=sys.stdout,
|
|
139
|
-
)
|
|
140
|
-
sys.exit(1)
|
|
141
|
-
|
|
142
|
-
# Ensure pr-diffs directory exists
|
|
143
|
-
os.makedirs(os.path.join(sweep_dir, "data", "pr-diffs"), exist_ok=True)
|
|
144
|
-
|
|
145
|
-
# Fetch open warden PRs
|
|
146
|
-
print("Fetching open warden-labeled PRs...", file=sys.stderr)
|
|
147
|
-
prs = fetch_warden_prs(sweep_dir)
|
|
148
|
-
print(f"Found {len(prs)} open warden PR(s)", file=sys.stderr)
|
|
149
|
-
|
|
150
|
-
# Build file index
|
|
151
|
-
file_index = build_file_index(prs)
|
|
152
|
-
|
|
153
|
-
# Find overlap with verified findings
|
|
154
|
-
verified_files = get_verified_files(sweep_dir)
|
|
155
|
-
overlapping_prs: set[int] = set()
|
|
156
|
-
|
|
157
|
-
for vfile in verified_files:
|
|
158
|
-
if vfile in file_index:
|
|
159
|
-
for pr_info in file_index[vfile]:
|
|
160
|
-
overlapping_prs.add(pr_info["number"])
|
|
161
|
-
|
|
162
|
-
# Fetch diffs for overlapping PRs
|
|
163
|
-
diffs_cached = 0
|
|
164
|
-
for pr_number in sorted(overlapping_prs):
|
|
165
|
-
print(f"Caching diff for PR #{pr_number}...", file=sys.stderr)
|
|
166
|
-
if fetch_pr_diff(pr_number, sweep_dir):
|
|
167
|
-
diffs_cached += 1
|
|
168
|
-
|
|
169
|
-
# Build output file index (only for files that have verified findings)
|
|
170
|
-
output_file_index: dict[str, list[dict[str, Any]]] = {}
|
|
171
|
-
for vfile in verified_files:
|
|
172
|
-
if vfile in file_index:
|
|
173
|
-
output_file_index[vfile] = file_index[vfile]
|
|
174
|
-
|
|
175
|
-
# Output summary
|
|
176
|
-
output = {
|
|
177
|
-
"totalPRs": len(prs),
|
|
178
|
-
"overlappingPRs": len(overlapping_prs),
|
|
179
|
-
"fileIndex": output_file_index,
|
|
180
|
-
"diffsCached": diffs_cached,
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
print(json.dumps(output, indent=2))
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if __name__ == "__main__":
|
|
187
|
-
main()
|
|
@@ -1,422 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# /// script
|
|
3
|
-
# requires-python = ">=3.9"
|
|
4
|
-
# ///
|
|
5
|
-
"""
|
|
6
|
-
Warden Sweep: Organize phase.
|
|
7
|
-
|
|
8
|
-
Identifies security findings, creates security indexes, labels security PRs,
|
|
9
|
-
updates finding reports with PR links, generates summary report, and
|
|
10
|
-
finalizes the manifest.
|
|
11
|
-
|
|
12
|
-
Usage:
|
|
13
|
-
uv run organize.py <sweep-dir>
|
|
14
|
-
|
|
15
|
-
Stdout: JSON summary (for LLM consumption)
|
|
16
|
-
Stderr: Progress lines
|
|
17
|
-
|
|
18
|
-
Side effects:
|
|
19
|
-
- Creates security/index.jsonl with security findings
|
|
20
|
-
- Copies security finding .md files to security/
|
|
21
|
-
- Creates "security" label on GitHub
|
|
22
|
-
- Labels security PRs with "security"
|
|
23
|
-
- Appends PR links to findings/*.md
|
|
24
|
-
- Runs generate_report.py for summary.md and report.json
|
|
25
|
-
- Updates manifest phases.organize to "complete"
|
|
26
|
-
"""
|
|
27
|
-
from __future__ import annotations
|
|
28
|
-
|
|
29
|
-
import argparse
|
|
30
|
-
import json
|
|
31
|
-
import os
|
|
32
|
-
import shutil
|
|
33
|
-
import subprocess
|
|
34
|
-
import sys
|
|
35
|
-
from datetime import datetime, timezone
|
|
36
|
-
from typing import Any
|
|
37
|
-
|
|
38
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
39
|
-
from _utils import ensure_github_label, pr_number_from_url, read_json, read_jsonl, write_json # noqa: E402
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
SECURITY_SKILL_PATTERNS = [
|
|
43
|
-
"security-review",
|
|
44
|
-
"owasp-review",
|
|
45
|
-
"security-audit",
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def is_security_skill(skill_name: str) -> bool:
|
|
50
|
-
"""Check if a skill name indicates a security-related skill."""
|
|
51
|
-
name_lower = skill_name.lower()
|
|
52
|
-
if "security" in name_lower:
|
|
53
|
-
return True
|
|
54
|
-
return name_lower in SECURITY_SKILL_PATTERNS
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def severity_label(severity: str) -> str:
|
|
58
|
-
"""Format a severity string for inline display in issue comments."""
|
|
59
|
-
if not severity:
|
|
60
|
-
return ""
|
|
61
|
-
if severity in ("critical", "high"):
|
|
62
|
-
return f" (**{severity.upper()}**)"
|
|
63
|
-
return f" ({severity.upper()})"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def identify_security_findings(
|
|
67
|
-
sweep_dir: str,
|
|
68
|
-
) -> list[dict[str, Any]]:
|
|
69
|
-
"""Find security-related verified findings and write security/index.jsonl."""
|
|
70
|
-
verified = read_jsonl(os.path.join(sweep_dir, "data", "verified.jsonl"))
|
|
71
|
-
|
|
72
|
-
security_findings: list[dict[str, Any]] = []
|
|
73
|
-
for finding in verified:
|
|
74
|
-
skill = finding.get("skill", "")
|
|
75
|
-
if is_security_skill(skill):
|
|
76
|
-
entry = {
|
|
77
|
-
"findingId": finding.get("findingId", ""),
|
|
78
|
-
"skill": skill,
|
|
79
|
-
"severity": finding.get("severity", "info"),
|
|
80
|
-
"file": finding.get("file", ""),
|
|
81
|
-
"title": finding.get("title", ""),
|
|
82
|
-
}
|
|
83
|
-
security_findings.append(entry)
|
|
84
|
-
|
|
85
|
-
# Write security index
|
|
86
|
-
security_dir = os.path.join(sweep_dir, "security")
|
|
87
|
-
os.makedirs(security_dir, exist_ok=True)
|
|
88
|
-
index_path = os.path.join(security_dir, "index.jsonl")
|
|
89
|
-
with open(index_path, "w") as f:
|
|
90
|
-
for entry in security_findings:
|
|
91
|
-
f.write(json.dumps(entry) + "\n")
|
|
92
|
-
|
|
93
|
-
return security_findings
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def copy_security_findings(
|
|
97
|
-
sweep_dir: str, security_findings: list[dict[str, Any]]
|
|
98
|
-
) -> None:
|
|
99
|
-
"""Copy security finding .md files to security/ directory."""
|
|
100
|
-
findings_dir = os.path.join(sweep_dir, "findings")
|
|
101
|
-
security_dir = os.path.join(sweep_dir, "security")
|
|
102
|
-
|
|
103
|
-
for finding in security_findings:
|
|
104
|
-
fid = finding.get("findingId", "")
|
|
105
|
-
src = os.path.join(findings_dir, f"{fid}.md")
|
|
106
|
-
dst = os.path.join(security_dir, f"{fid}.md")
|
|
107
|
-
if os.path.exists(src):
|
|
108
|
-
shutil.copy2(src, dst)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def create_security_label() -> None:
|
|
112
|
-
"""Create the security label on GitHub (idempotent)."""
|
|
113
|
-
ensure_github_label("security", "D93F0B", "Security-related changes")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def label_security_prs(
|
|
117
|
-
sweep_dir: str, security_findings: list[dict[str, Any]]
|
|
118
|
-
) -> int:
|
|
119
|
-
"""Add "security" label to PRs for security findings. Returns count labeled."""
|
|
120
|
-
patches = read_jsonl(os.path.join(sweep_dir, "data", "patches.jsonl"))
|
|
121
|
-
security_ids = {f.get("findingId", "") for f in security_findings}
|
|
122
|
-
|
|
123
|
-
labeled = 0
|
|
124
|
-
for patch in patches:
|
|
125
|
-
if patch.get("status") != "created":
|
|
126
|
-
continue
|
|
127
|
-
if patch.get("findingId", "") not in security_ids:
|
|
128
|
-
continue
|
|
129
|
-
|
|
130
|
-
pr_url = patch.get("prUrl", "")
|
|
131
|
-
if not pr_url:
|
|
132
|
-
continue
|
|
133
|
-
|
|
134
|
-
try:
|
|
135
|
-
result = subprocess.run(
|
|
136
|
-
["gh", "pr", "edit", pr_url, "--add-label", "security"],
|
|
137
|
-
capture_output=True,
|
|
138
|
-
text=True,
|
|
139
|
-
timeout=15,
|
|
140
|
-
)
|
|
141
|
-
if result.returncode == 0:
|
|
142
|
-
labeled += 1
|
|
143
|
-
else:
|
|
144
|
-
print(
|
|
145
|
-
f"Warning: Failed to label PR {pr_url}: {result.stderr.strip()}",
|
|
146
|
-
file=sys.stderr,
|
|
147
|
-
)
|
|
148
|
-
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
149
|
-
print(
|
|
150
|
-
f"Warning: Failed to label PR {pr_url}",
|
|
151
|
-
file=sys.stderr,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
return labeled
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def _has_sweep_complete_comment(issue_url: str) -> bool:
|
|
158
|
-
"""Check if the tracking issue already has a 'Sweep Complete' comment."""
|
|
159
|
-
try:
|
|
160
|
-
result = subprocess.run(
|
|
161
|
-
["gh", "issue", "view", issue_url, "--json", "comments", "--jq",
|
|
162
|
-
'.comments[].body | select(startswith("## Sweep Complete"))'],
|
|
163
|
-
capture_output=True,
|
|
164
|
-
text=True,
|
|
165
|
-
timeout=15,
|
|
166
|
-
)
|
|
167
|
-
return result.returncode == 0 and result.stdout.strip() != ""
|
|
168
|
-
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
169
|
-
return False
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def update_tracking_issue(sweep_dir: str) -> None:
|
|
173
|
-
"""Post a comment on the tracking issue with final PR results. Idempotent."""
|
|
174
|
-
manifest = read_json(os.path.join(sweep_dir, "data", "manifest.json"))
|
|
175
|
-
if not manifest:
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
issue_url = manifest.get("issueUrl")
|
|
179
|
-
if not issue_url:
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
if _has_sweep_complete_comment(issue_url):
|
|
183
|
-
print("Tracking issue already has completion comment, skipping.", file=sys.stderr)
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
patches = read_jsonl(os.path.join(sweep_dir, "data", "patches.jsonl"))
|
|
187
|
-
verified = read_jsonl(os.path.join(sweep_dir, "data", "verified.jsonl"))
|
|
188
|
-
security_index = read_jsonl(os.path.join(sweep_dir, "security", "index.jsonl"))
|
|
189
|
-
|
|
190
|
-
# Build lookup from findingId to verified finding
|
|
191
|
-
verified_lookup: dict[str, dict[str, Any]] = {}
|
|
192
|
-
for f in verified:
|
|
193
|
-
fid = f.get("findingId", "")
|
|
194
|
-
if fid:
|
|
195
|
-
verified_lookup[fid] = f
|
|
196
|
-
|
|
197
|
-
security_ids = {f.get("findingId", "") for f in security_index}
|
|
198
|
-
|
|
199
|
-
created = sum(1 for p in patches if p.get("status") == "created")
|
|
200
|
-
existing = sum(1 for p in patches if p.get("status") == "existing")
|
|
201
|
-
failed = sum(1 for p in patches if p.get("status") == "error")
|
|
202
|
-
|
|
203
|
-
lines = [
|
|
204
|
-
"## Sweep Complete",
|
|
205
|
-
"",
|
|
206
|
-
"| PRs Created | PRs Skipped (existing) | PRs Failed | Security Findings |",
|
|
207
|
-
"|-------------|------------------------|------------|-------------------|",
|
|
208
|
-
f"| {created} | {existing} | {failed} | {len(security_index)} |",
|
|
209
|
-
"",
|
|
210
|
-
]
|
|
211
|
-
|
|
212
|
-
# PR task list
|
|
213
|
-
pr_entries = [p for p in patches if p.get("status") == "created" and p.get("prUrl")]
|
|
214
|
-
if pr_entries:
|
|
215
|
-
lines.append("### PRs")
|
|
216
|
-
lines.append("")
|
|
217
|
-
for p in pr_entries:
|
|
218
|
-
fid = p.get("findingId", "")
|
|
219
|
-
pr_number = pr_number_from_url(p.get("prUrl", ""))
|
|
220
|
-
finding = verified_lookup.get(fid, {})
|
|
221
|
-
title = finding.get("title", fid)
|
|
222
|
-
sev = severity_label(finding.get("severity", ""))
|
|
223
|
-
lines.append(f"- [ ] #{pr_number} - fix: {title}{sev}")
|
|
224
|
-
lines.append("")
|
|
225
|
-
|
|
226
|
-
# Security findings section
|
|
227
|
-
security_prs = [
|
|
228
|
-
p for p in patches
|
|
229
|
-
if p.get("status") == "created"
|
|
230
|
-
and p.get("findingId", "") in security_ids
|
|
231
|
-
and p.get("prUrl")
|
|
232
|
-
]
|
|
233
|
-
if security_prs:
|
|
234
|
-
lines.append("### Security Findings")
|
|
235
|
-
lines.append("")
|
|
236
|
-
for p in security_prs:
|
|
237
|
-
fid = p.get("findingId", "")
|
|
238
|
-
pr_number = pr_number_from_url(p.get("prUrl", ""))
|
|
239
|
-
finding = verified_lookup.get(fid, {})
|
|
240
|
-
title = finding.get("title", fid)
|
|
241
|
-
sev = severity_label(finding.get("severity", ""))
|
|
242
|
-
lines.append(f"- #{pr_number} - {title}{sev}")
|
|
243
|
-
lines.append("")
|
|
244
|
-
|
|
245
|
-
body = "\n".join(lines)
|
|
246
|
-
|
|
247
|
-
try:
|
|
248
|
-
result = subprocess.run(
|
|
249
|
-
["gh", "issue", "comment", issue_url, "--body", body],
|
|
250
|
-
capture_output=True,
|
|
251
|
-
text=True,
|
|
252
|
-
timeout=30,
|
|
253
|
-
)
|
|
254
|
-
if result.returncode != 0:
|
|
255
|
-
print(
|
|
256
|
-
f"Warning: Failed to comment on tracking issue: {result.stderr.strip()}",
|
|
257
|
-
file=sys.stderr,
|
|
258
|
-
)
|
|
259
|
-
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
260
|
-
print(
|
|
261
|
-
f"Warning: Failed to comment on tracking issue: {e}",
|
|
262
|
-
file=sys.stderr,
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def update_findings_with_pr_links(sweep_dir: str) -> None:
|
|
267
|
-
"""Append PR links to findings/*.md for created PRs."""
|
|
268
|
-
patches = read_jsonl(os.path.join(sweep_dir, "data", "patches.jsonl"))
|
|
269
|
-
findings_dir = os.path.join(sweep_dir, "findings")
|
|
270
|
-
|
|
271
|
-
for patch in patches:
|
|
272
|
-
if patch.get("status") != "created":
|
|
273
|
-
continue
|
|
274
|
-
|
|
275
|
-
fid = patch.get("findingId", "")
|
|
276
|
-
pr_url = patch.get("prUrl", "")
|
|
277
|
-
branch = patch.get("branch", "")
|
|
278
|
-
reviewers = patch.get("reviewers", [])
|
|
279
|
-
|
|
280
|
-
if not fid or not pr_url:
|
|
281
|
-
continue
|
|
282
|
-
|
|
283
|
-
md_path = os.path.join(findings_dir, f"{fid}.md")
|
|
284
|
-
if not os.path.exists(md_path):
|
|
285
|
-
continue
|
|
286
|
-
|
|
287
|
-
# Check if PR section already appended
|
|
288
|
-
with open(md_path) as f:
|
|
289
|
-
content = f.read()
|
|
290
|
-
if "## Pull Request" in content:
|
|
291
|
-
continue
|
|
292
|
-
|
|
293
|
-
reviewers_str = ", ".join(reviewers) if reviewers else "none"
|
|
294
|
-
pr_section = (
|
|
295
|
-
f"\n\n## Pull Request\n"
|
|
296
|
-
f"**PR**: {pr_url}\n"
|
|
297
|
-
f"**Branch**: {branch}\n"
|
|
298
|
-
f"**Reviewers**: {reviewers_str}\n"
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
with open(md_path, "a") as f:
|
|
302
|
-
f.write(pr_section)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def run_generate_report(sweep_dir: str, script_dir: str) -> None:
|
|
306
|
-
"""Run generate_report.py as a subprocess."""
|
|
307
|
-
report_script = os.path.join(script_dir, "generate_report.py")
|
|
308
|
-
|
|
309
|
-
try:
|
|
310
|
-
result = subprocess.run(
|
|
311
|
-
[sys.executable, report_script, sweep_dir],
|
|
312
|
-
capture_output=True,
|
|
313
|
-
text=True,
|
|
314
|
-
timeout=60,
|
|
315
|
-
)
|
|
316
|
-
if result.returncode != 0:
|
|
317
|
-
print(
|
|
318
|
-
f"Warning: generate_report.py failed: {result.stderr}",
|
|
319
|
-
file=sys.stderr,
|
|
320
|
-
)
|
|
321
|
-
except Exception as e:
|
|
322
|
-
print(f"Warning: generate_report.py failed: {e}", file=sys.stderr)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def update_manifest(sweep_dir: str) -> None:
|
|
326
|
-
"""Mark organize phase complete and add completedAt timestamp."""
|
|
327
|
-
manifest_path = os.path.join(sweep_dir, "data", "manifest.json")
|
|
328
|
-
manifest = read_json(manifest_path)
|
|
329
|
-
if not manifest:
|
|
330
|
-
return
|
|
331
|
-
|
|
332
|
-
manifest.setdefault("phases", {})["organize"] = "complete"
|
|
333
|
-
manifest["completedAt"] = datetime.now(timezone.utc).strftime(
|
|
334
|
-
"%Y-%m-%dT%H:%M:%SZ"
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
write_json(manifest_path, manifest)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def main() -> None:
|
|
341
|
-
parser = argparse.ArgumentParser(
|
|
342
|
-
description="Warden Sweep: Organize phase"
|
|
343
|
-
)
|
|
344
|
-
parser.add_argument("sweep_dir", help="Path to the sweep directory")
|
|
345
|
-
args = parser.parse_args()
|
|
346
|
-
|
|
347
|
-
sweep_dir = args.sweep_dir
|
|
348
|
-
|
|
349
|
-
if not os.path.isdir(sweep_dir):
|
|
350
|
-
print(
|
|
351
|
-
json.dumps({"error": f"Sweep directory not found: {sweep_dir}"}),
|
|
352
|
-
file=sys.stdout,
|
|
353
|
-
)
|
|
354
|
-
sys.exit(1)
|
|
355
|
-
|
|
356
|
-
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
357
|
-
|
|
358
|
-
# Step 1: Identify security findings
|
|
359
|
-
print("Identifying security findings...", file=sys.stderr)
|
|
360
|
-
security_findings = identify_security_findings(sweep_dir)
|
|
361
|
-
print(
|
|
362
|
-
f"Found {len(security_findings)} security finding(s)",
|
|
363
|
-
file=sys.stderr,
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
# Step 2: Label security PRs
|
|
367
|
-
security_prs_labeled = 0
|
|
368
|
-
if security_findings:
|
|
369
|
-
print("Creating security label...", file=sys.stderr)
|
|
370
|
-
create_security_label()
|
|
371
|
-
print("Labeling security PRs...", file=sys.stderr)
|
|
372
|
-
security_prs_labeled = label_security_prs(sweep_dir, security_findings)
|
|
373
|
-
|
|
374
|
-
# Step 3: Update finding reports with PR links
|
|
375
|
-
print("Updating finding reports with PR links...", file=sys.stderr)
|
|
376
|
-
update_findings_with_pr_links(sweep_dir)
|
|
377
|
-
|
|
378
|
-
# Step 4: Copy security finding reports (after PR links are added)
|
|
379
|
-
copy_security_findings(sweep_dir, security_findings)
|
|
380
|
-
|
|
381
|
-
# Step 5: Generate summary and report
|
|
382
|
-
print("Generating summary and report...", file=sys.stderr)
|
|
383
|
-
run_generate_report(sweep_dir, script_dir)
|
|
384
|
-
|
|
385
|
-
# Step 6: Update tracking issue with PR results
|
|
386
|
-
print("Updating tracking issue...", file=sys.stderr)
|
|
387
|
-
update_tracking_issue(sweep_dir)
|
|
388
|
-
|
|
389
|
-
# Step 7: Update manifest
|
|
390
|
-
update_manifest(sweep_dir)
|
|
391
|
-
|
|
392
|
-
# Gather stats for output
|
|
393
|
-
scan_index = read_jsonl(os.path.join(sweep_dir, "data", "scan-index.jsonl"))
|
|
394
|
-
verified = read_jsonl(os.path.join(sweep_dir, "data", "verified.jsonl"))
|
|
395
|
-
rejected = read_jsonl(os.path.join(sweep_dir, "data", "rejected.jsonl"))
|
|
396
|
-
patches = read_jsonl(os.path.join(sweep_dir, "data", "patches.jsonl"))
|
|
397
|
-
|
|
398
|
-
files_scanned = sum(1 for e in scan_index if e.get("status") == "complete")
|
|
399
|
-
prs_created = sum(1 for p in patches if p.get("status") == "created")
|
|
400
|
-
|
|
401
|
-
summary_path = os.path.join(sweep_dir, "summary.md")
|
|
402
|
-
report_path = os.path.join(sweep_dir, "data", "report.json")
|
|
403
|
-
|
|
404
|
-
output = {
|
|
405
|
-
"securityFindings": len(security_findings),
|
|
406
|
-
"securityPRsLabeled": security_prs_labeled,
|
|
407
|
-
"summaryPath": summary_path,
|
|
408
|
-
"reportPath": report_path,
|
|
409
|
-
"stats": {
|
|
410
|
-
"filesScanned": files_scanned,
|
|
411
|
-
"verified": len(verified),
|
|
412
|
-
"rejected": len(rejected),
|
|
413
|
-
"prsCreated": prs_created,
|
|
414
|
-
"securityFindings": len(security_findings),
|
|
415
|
-
},
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
print(json.dumps(output, indent=2))
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if __name__ == "__main__":
|
|
422
|
-
main()
|