
Warden Sweep
- 15 installs
- 6.2k repo stars
- Updated August 4, 2026
- getsentry/xcodebuildmcp
warden-sweep scans the full repo, verifies issues, and opens draft PRs.
About
The warden-sweep skill runs phased full-repo analysis: scan every file, deep-verify findings, create issues, generate patches, and organize results under .warden/sweeps with manifest.json tracking phase state. Scripts scan.py, verify, create_issue.py, and organize.py run via uv with incremental resume skipping completed verify JSON and patches.jsonl entries. Output includes summary.md, per-finding markdown, security index, and PR links table distinguishing created versus existing PRs. Designed for batch code analysis requests like sweep the repo or find all bugs. Phases are idempotent for resume after partial runs.
- Scans all repository files with Warden batch tooling.
- Verifies findings via deep tracing before PR creation.
- Creates draft PRs for validated issues only.
- Stores structured manifest and JSONL under .warden/sweeps.
- Supports incremental resume across scan phases.
Warden Sweep by the numbers
- 15 all-time installs (skills.sh)
- Ranked #778 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
warden-sweep capabilities & compatibility
- Capabilities
- phased scan verify issue patch organize pipeline · resuming a sweep incremental rules · output directory structure
- Works with
- github
- Use cases
- code review · security audit
- Platforms
- macOS · Linux
What warden-sweep says it does
Full-repository code sweep
npx skills add https://github.com/getsentry/xcodebuildmcp --skill warden-sweepAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 6.2k |
| Last updated | August 4, 2026 |
| Repository | getsentry/xcodebuildmcp ↗ |
How do I run a full codebase Warden sweep?
Sweep an entire repository with Warden, verify findings, and open draft PRs for validated issues.
Who is it for?
Maintainers running periodic full-repo Warden audits.
Skip if: Skip for single-file pre-commit checks use warden skill.
When should I use this skill?
User requests full repo sweep, scan everything, or batch analysis.
What you get
Sweep summary with verified findings and created PR links.
Files
Warden Sweep
Full-repository code sweep: scan every file, verify findings with deep tracing, create draft PRs for validated issues.
Requires: warden, gh, git, jq, uv
Important: Run all scripts from the repository root using ${CLAUDE_SKILL_ROOT}. Output goes to .warden/sweeps/<run-id>/.
Bundled Scripts
scripts/scan.py
Runs setup and scan in one call: generates run ID, creates sweep dir, checks deps, creates warden label, enumerates files, runs warden per file, extracts findings.
uv run ${CLAUDE_SKILL_ROOT}/scripts/scan.py [file ...]
--sweep-dir DIR # Resume into existing sweep dirscripts/index_prs.py
Fetches open warden-labeled PRs, builds file-to-PR dedup index, caches diffs for overlapping PRs.
uv run ${CLAUDE_SKILL_ROOT}/scripts/index_prs.py <sweep-dir>scripts/create_issue.py
Creates a GitHub tracking issue summarizing sweep results. Run after verification, before patching.
uv run ${CLAUDE_SKILL_ROOT}/scripts/create_issue.py <sweep-dir>scripts/organize.py
Tags security findings, labels security PRs, updates finding reports with PR links, posts final results to tracking issue, generates summary report, finalizes manifest.
uv run ${CLAUDE_SKILL_ROOT}/scripts/organize.py <sweep-dir>scripts/extract_findings.py
Parses warden JSONL log files and extracts normalized findings. Called automatically by scan.py.
uv run ${CLAUDE_SKILL_ROOT}/scripts/extract_findings.py <log-path-or-directory> -o <output.jsonl>scripts/generate_report.py
Builds summary.md and report.json from sweep data. Called automatically by organize.py.
uv run ${CLAUDE_SKILL_ROOT}/scripts/generate_report.py <sweep-dir>scripts/find_reviewers.py
Finds top 2 git contributors for a file (last 12 months).
uv run ${CLAUDE_SKILL_ROOT}/scripts/find_reviewers.py <file-path>Returns JSON: {"reviewers": ["user1", "user2"]}
---
Phase 1: Scan
Run (1 tool call):
uv run ${CLAUDE_SKILL_ROOT}/scripts/scan.pyTo resume a partial scan:
uv run ${CLAUDE_SKILL_ROOT}/scripts/scan.py --sweep-dir .warden/sweeps/<run-id>Parse the JSON stdout. Save runId and sweepDir for subsequent phases.
Report to user:
## Scan Complete
Scanned **{filesScanned}** files, **{filesTimedOut}** timed out, **{filesErrored}** errors.
### Findings ({totalFindings} total)
| # | Severity | Skill | File | Title |
|---|----------|-------|------|-------|
| 1 | **HIGH** | security-review | `src/db/query.ts:42` | SQL injection in query builder |
...Render every finding from the findings array. Bold severity for high and above.
On failure: If exit code 1, show the error JSON and stop. If exit code 2, show the partial results. List timed-out files separately from errored files so users know which can be retried.
---
Phase 2: Verify
Deep-trace each finding using Task subagents to qualify or disqualify.
For each finding in `data/all-findings.jsonl`:
Check if data/verify/<finding-id>.json already exists (incrementality). If it does, skip.
Launch a Task subagent (subagent_type: "general-purpose") for each finding. Process findings in parallel batches of up to 8 to improve throughput.
Task prompt for each finding:
Read ${CLAUDE_SKILL_ROOT}/references/verify-prompt.md for the prompt template. Substitute the finding's values into the ${...} placeholders.
Process results:
Parse the JSON from the subagent response and:
- Write result to
data/verify/<finding-id>.json - Append to
data/verified.jsonlordata/rejected.jsonl - For verified findings, generate
findings/<finding-id>.md:
# ${TITLE}
**ID**: ${FINDING_ID} | **Severity**: ${SEVERITY} | **Confidence**: ${CONFIDENCE}
**Skill**: ${SKILL} | **File**: ${FILE_PATH}:${START_LINE}
## Description
${DESCRIPTION}
## Verification
**Verdict**: Verified (${VERIFICATION_CONFIDENCE})
**Reasoning**: ${REASONING}
**Code trace**: ${TRACE_NOTES}
## Suggested Fix
${FIX_DESCRIPTION}${FIX_DIFF}
Update manifest: set phases.verify to "complete".
Report to user after all verifications:
## Verification Complete
**{verified}** verified, **{rejected}** rejected.
### Verified Findings
| # | Severity | Confidence | File | Title | Reasoning |
|---|----------|------------|------|-------|-----------|
| 1 | **HIGH** | high | `src/db/query.ts:42` | SQL injection in query builder | User input flows directly into... |
...
### Rejected ({rejected_count})
- `{findingId}` {file}: {reasoning}
...---
Phase 3: Issue
Create a tracking issue that ties all PRs together and gives reviewers a single overview.
Run (1 tool call):
uv run ${CLAUDE_SKILL_ROOT}/scripts/create_issue.py ${SWEEP_DIR}Parse the JSON stdout. Save issueUrl and issueNumber for Phase 4.
Report to user:
## Tracking Issue Created
{issueUrl}On failure: Show the error. Continue to Phase 4 (PRs can still be created without a tracking issue).
---
Phase 4: Patch
For each verified finding, create a worktree, fix the code, and open a draft PR. Process findings sequentially (one at a time) since parallel subagents cross-contaminate worktrees.
Severity triage: Patch HIGH and above. For MEDIUM, only patch findings from bug-detection skills (e.g., code-review, security-review). Skip LOW and INFO findings.
Step 0: Setup (run once before the loop):
uv run ${CLAUDE_SKILL_ROOT}/scripts/index_prs.py ${SWEEP_DIR}Parse the JSON stdout. Use fileIndex for dedup checks.
Determine the default branch and fetch latest so worktrees branch from current upstream:
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
git fetch origin "${DEFAULT_BRANCH}"For each finding in `data/verified.jsonl`:
Check if finding ID already exists in data/patches.jsonl (incrementality). If it does, skip.
Dedup check: Use the file index from index_prs.py output to determine if an existing open PR already addresses the same issue.
1. File match: Look up the finding's file path in the fileIndex. If no PR touches that file, no conflict; proceed to Step 1. 2. Chunk overlap: If a PR does touch the same file, read its cached diff from data/pr-diffs/<number>.diff and check whether the PR's changed hunks overlap with the finding's line range (startLine-endLine). Overlapping or adjacent hunks (within ~10 lines) indicate the same code region. 3. Same concern: If the hunks overlap, compare the PR title and the finding title/description. Are they fixing the same kind of defect? A PR fixing an off-by-one error and a finding about a null check in the same function are different issues; both should proceed.
Skip the finding only when there is both chunk overlap AND the PR addresses the same concern. Record it in data/patches.jsonl with "status": "existing" and "prUrl" pointing to the matching PR, then continue to the next finding.
Step 1: Create worktree
BRANCH="warden-sweep/${RUN_ID}/${FINDING_ID}"
WORKTREE="${SWEEP_DIR}/worktrees/${FINDING_ID}"
git worktree add "${WORKTREE}" -b "${BRANCH}" "origin/${DEFAULT_BRANCH}"Each finding branches from the repo's default branch so PRs contain only the fix commit.
Step 2: Generate fix
Launch a Task subagent (subagent_type: "general-purpose") to apply the fix in the worktree. Read ${CLAUDE_SKILL_ROOT}/references/patch-prompt.md for the prompt template. Substitute the finding's values and worktree path into the ${...} placeholders.
Step 2b: Handle skipped findings
If the subagent returned "status": "skipped" (not "applied"), do NOT proceed to Steps 3-4. Instead: 1. Record the finding in data/patches.jsonl with "status": "error" and "error": "Subagent skipped: ${skipReason}" 2. Clean up the worktree 3. Continue to the next finding
Step 3: Find reviewers
uv run ${CLAUDE_SKILL_ROOT}/scripts/find_reviewers.py "${FILE_PATH}"Step 4: Create draft PR
cd "${WORKTREE}" && git push -u origin HEAD:"${BRANCH}"Create the PR with a 1-2 sentence "What" summary based on the finding and fix, followed by the finding description and verification reasoning:
REVIEWERS=""
# If find_reviewers.py returned reviewers, build the flags
# e.g., REVIEWERS="--reviewer user1 --reviewer user2"
gh pr create --draft \
--label "warden" \
--title "fix: ${TITLE}" \
--body "$(cat <<'EOF'
${FIX_WHAT_DESCRIPTION}
${DESCRIPTION}
${REASONING}
Automated fix for Warden finding ${FINDING_ID} (${SEVERITY}, detected by ${SKILL}).
<!-- Only include the next line if Phase 3 succeeded and ISSUE_NUMBER is available -->
Ref #${ISSUE_NUMBER}
> This PR was auto-generated by a Warden Sweep (run ${RUN_ID}).
> The finding has been validated through automated deep tracing,
> but human confirmation is requested as this is batch work.
EOF
)" ${REVIEWERS}Save the PR URL.
Step 5: Record and cleanup
Append to data/patches.jsonl (use "created" as status for successful PRs, not the subagent's "applied"):
{"findingId": "...", "prUrl": "https://...", "branch": "...", "reviewers": ["user1", "user2"], "filesChanged": ["..."], "status": "created|existing|error"}Remove the worktree:
cd "$(git rev-parse --show-toplevel)"
git worktree remove "${WORKTREE}" --forceError handling: On failure at any step, write to data/patches.jsonl with "status": "error" and "error": "...", clean up the worktree, and continue to the next finding.
Update manifest: set phases.patch to "complete".
Report to user after all patches:
## PRs Created
**{created}** created, **{skipped}** skipped (existing), **{failed}** failed.
| # | Finding | PR | Status |
|---|---------|-----|--------|
| 1 | `security-review-a1b2c3d4` SQL injection in query builder | #142 | created |
| 2 | `code-review-e5f6g7h8` Null pointer in handler | - | existing (#138) |
...---
Phase 5: Organize
Run (1 tool call):
uv run ${CLAUDE_SKILL_ROOT}/scripts/organize.py ${SWEEP_DIR}Parse the JSON stdout.
Report to user:
## Sweep Complete
| Metric | Count |
|--------|-------|
| Files scanned | {filesScanned} |
| Findings verified | {verified} |
| PRs created | {prsCreated} |
| Security findings | {securityFindings} |
Full report: `{summaryPath}`On failure: Show the error and note which steps completed.
---
Resuming a Sweep
Each phase is incremental. To resume from where you left off:
1. Check data/manifest.json to see which phases are complete 2. For scan: pass --sweep-dir to scan.py 3. For verify: existing data/verify/<id>.json files are skipped 4. For issue: create_issue.py is idempotent (skips if issueUrl in manifest) 5. For patch: existing entries in data/patches.jsonl are skipped 6. For organize: safe to re-run (idempotent)
Output Directory Structure
.warden/sweeps/<run-id>/
summary.md # Stats, key findings, PR links
findings/ # One markdown per verified finding
<finding-id>.md
security/ # Security-specific view
index.jsonl # Security findings index
<finding-id>.md # Copies of security findings
data/ # Structured data for tooling
manifest.json # Run metadata, phase state
scan-index.jsonl # Per-file scan tracking
all-findings.jsonl # Every finding from scan
verified.jsonl # Findings that passed verification
rejected.jsonl # Findings that failed verification
patches.jsonl # Finding -> PR URL -> reviewers
existing-prs.json # Cached open warden PRs
report.json # Machine-readable summary
verify/ # Individual verification results
<finding-id>.json
logs/ # Warden JSONL logs per file
<hash>.jsonl
pr-diffs/ # Cached PR diffs for dedup
<number>.diffFix a verified code issue. You are working in a git worktree at: ${WORKTREE}
Finding
- Title: ${TITLE}
- File: ${FILE_PATH}:${START_LINE}
- Description: ${DESCRIPTION}
- Verification: ${REASONING}
- Suggested Fix: ${FIX_DESCRIPTION}
${FIX_DIFF}Instructions
Step 1: Understand the code
Read the file at ${WORKTREE}/${FILE_PATH}. Read at least 50 lines above and below the reported location. Trace callers and callees of the affected code using Grep/Glob to understand how it is used. Do NOT skip this step.
Step 2: Apply a minimal fix
Apply the smallest change that addresses the finding. If the suggested diff doesn't apply cleanly, adapt it while preserving intent. Do NOT refactor surrounding code, rename variables, add comments, or make any change beyond what the finding requires.
Step 3: Write tests
Write or update tests that verify the fix:
- Follow existing test patterns (co-located files, same framework)
- At minimum, write a test that would have caught the original bug
- Test the specific edge case, not just the happy path
Only modify the fix target and its test file.
Step 4: Self-review
Before staging, run git diff in the worktree and review every changed line. Verify: 1. The change addresses the specific finding described, not something else 2. No unrelated code was modified (no drive-by cleanups, no formatting changes) 3. Trace through changed code paths: does the fix introduce any new bug, null reference, type error, or broken import? 4. Tests exercise the fix (the failure case), not just that the code runs
If ANY check fails, fix the problem before proceeding. If the suggested fix is wrong or would introduce a regression you cannot resolve, do NOT commit. Instead, skip to the output step and report why.
Step 5: Commit
Do NOT run tests locally. CI will validate the changes.
Stage and commit with this exact message:
fix: ${TITLE}
Warden finding ${FINDING_ID} Severity: ${SEVERITY}
Co-Authored-By: Warden <noreply@getsentry.com>
Step 6: Output
Return ONLY valid JSON (no surrounding text). Use "status": "applied" if you committed a fix, or "status": "skipped" if you did not.
{
"status": "applied",
"filesChanged": ["src/example.ts"],
"testFilesChanged": ["src/example.test.ts"],
"selfReview": "Verified the fix addresses the null check and test covers the failure case",
"skipReason": null
}When skipping:
{
"status": "skipped",
"filesChanged": [],
"testFilesChanged": [],
"selfReview": null,
"skipReason": "The suggested fix would introduce a regression in the error handling path"
}Verify a code analysis finding. Determine if this is a TRUE issue or a FALSE POSITIVE. Do NOT write or edit any files. Research only.
Finding
- Title: ${TITLE}
- Severity: ${SEVERITY} | Confidence: ${CONFIDENCE}
- Skill: ${SKILL}
- Location: ${FILE_PATH}:${START_LINE}-${END_LINE}
- Description: ${DESCRIPTION}
- Verification hint: ${VERIFICATION}
Instructions
1. Read the file at the reported location. Examine at least 50 lines of surrounding context. 2. Trace data flow to/from the flagged code using Grep/Glob. 3. Check if the issue is mitigated elsewhere (guards, validation, try/catch upstream). 4. Check if the issue is actually reachable in practice.
Return your verdict as JSON: { "findingId": "${FINDING_ID}", "verdict": "verified" or "rejected", "confidence": "high" or "medium" or "low", "reasoning": "2-3 sentence explanation", "traceNotes": "What code paths you examined" }
"""Shared utilities for warden-sweep scripts."""
from __future__ import annotations
import json
import os
import subprocess
from typing import Any
def run_cmd(
args: list[str], timeout: int = 30, cwd: str | None = None
) -> subprocess.CompletedProcess[str]:
"""Run a command and return the result."""
return subprocess.run(
args,
capture_output=True,
text=True,
timeout=timeout,
cwd=cwd,
)
def run_cmd_stdout(
args: list[str], timeout: int = 30, cwd: str | None = None
) -> str | None:
"""Run a command and return stripped stdout, or None on failure."""
try:
result = run_cmd(args, timeout=timeout, cwd=cwd)
return result.stdout.strip() if result.returncode == 0 else None
except (subprocess.TimeoutExpired, FileNotFoundError):
return None
def read_json(path: str) -> dict[str, Any] | None:
"""Read a JSON file and return parsed object, or None on failure."""
if not os.path.exists(path):
return None
try:
with open(path) as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return None
def write_json(path: str, data: dict[str, Any]) -> None:
"""Write a dict to a JSON file with trailing newline."""
with open(path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
def read_jsonl(path: str) -> list[dict[str, Any]]:
"""Read a JSONL file and return list of parsed objects."""
entries: list[dict[str, Any]] = []
if not os.path.exists(path):
return entries
with open(path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entries.append(json.loads(line))
except json.JSONDecodeError:
continue
return entries
def severity_badge(severity: str) -> str:
"""Return a markdown-friendly severity indicator."""
badges = {
"critical": "**CRITICAL**",
"high": "**HIGH**",
"medium": "MEDIUM",
"low": "LOW",
"info": "info",
}
return badges.get(severity, severity)
def pr_number_from_url(pr_url: str) -> str:
"""Extract the PR or issue number from a GitHub URL's last path segment."""
return pr_url.rstrip("/").split("/")[-1]
def ensure_github_label(name: str, color: str, description: str) -> None:
"""Create a GitHub label if it doesn't exist (idempotent)."""
try:
subprocess.run(
[
"gh", "label", "create", name,
"--color", color,
"--description", description,
],
capture_output=True,
timeout=15,
)
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# ///
"""
Warden Sweep: Create tracking issue.
Creates a GitHub issue summarizing the sweep results after verification
but before patching. Gives every PR a parent to reference and gives
reviewers a single place to see the full picture.
Usage:
uv run create_issue.py <sweep-dir>
Stdout: JSON with issueUrl and issueNumber
Stderr: Progress lines
Idempotent: if issueUrl already exists in manifest, skips creation.
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from typing import Any
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _utils import ( # noqa: E402
ensure_github_label,
pr_number_from_url,
read_json,
read_jsonl,
severity_badge,
write_json,
)
def build_issue_body(
run_id: str,
scan_index: list[dict[str, Any]],
all_findings: list[dict[str, Any]],
verified: list[dict[str, Any]],
rejected: list[dict[str, Any]],
) -> str:
"""Build the GitHub issue body markdown."""
files_scanned = sum(1 for e in scan_index if e.get("status") == "complete")
files_timed_out = sum(
1 for e in scan_index
if e.get("status") == "error" and e.get("error") == "timeout"
)
files_errored = sum(
1 for e in scan_index
if e.get("status") == "error" and e.get("error") != "timeout"
)
# Collect unique skills from scan index
skills: set[str] = set()
for entry in scan_index:
for skill in entry.get("skills", []):
skills.add(skill)
lines = [
f"## Warden Sweep `{run_id}`",
"",
"| Metric | Count |",
"|--------|-------|",
f"| Files scanned | {files_scanned} |",
f"| Files timed out | {files_timed_out} |",
f"| Files errored | {files_errored} |",
f"| Total findings | {len(all_findings)} |",
f"| Verified | {len(verified)} |",
f"| Rejected | {len(rejected)} |",
"",
]
if verified:
lines.append("### Verified Findings")
lines.append("")
lines.append("| Severity | Skill | File | Title |")
lines.append("|----------|-------|------|-------|")
for f in verified:
sev = severity_badge(f.get("severity", "info"))
skill = f.get("skill", "")
file_path = f.get("file", "")
start_line = f.get("startLine")
location = f"{file_path}:{start_line}" if start_line else file_path
title = f.get("title", "")
lines.append(f"| {sev} | {skill} | `{location}` | {title} |")
lines.append("")
if skills:
lines.append("### Skills Run")
lines.append("")
lines.append(", ".join(sorted(skills)))
lines.append("")
lines.append("> Generated by Warden Sweep. PRs referencing this issue will appear below.")
return "\n".join(lines) + "\n"
def create_github_issue(title: str, body: str) -> dict[str, Any]:
"""Create a GitHub issue with the warden label. Returns issueUrl and issueNumber."""
ensure_github_label("warden", "5319E7", "Automated fix from Warden Sweep")
result = subprocess.run(
[
"gh", "issue", "create",
"--label", "warden",
"--title", title,
"--body", body,
],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
raise RuntimeError(f"gh issue create failed: {result.stderr.strip()}")
issue_url = result.stdout.strip()
try:
issue_number = int(pr_number_from_url(issue_url))
except (ValueError, IndexError):
raise RuntimeError(f"Could not parse issue number from gh output: {issue_url}")
return {"issueUrl": issue_url, "issueNumber": issue_number}
def main() -> None:
parser = argparse.ArgumentParser(
description="Warden Sweep: Create tracking issue"
)
parser.add_argument("sweep_dir", help="Path to the sweep directory")
args = parser.parse_args()
sweep_dir = args.sweep_dir
data_dir = os.path.join(sweep_dir, "data")
manifest_path = os.path.join(data_dir, "manifest.json")
if not os.path.isdir(sweep_dir):
print(
json.dumps({"error": f"Sweep directory not found: {sweep_dir}"}),
file=sys.stdout,
)
sys.exit(1)
manifest = read_json(manifest_path) or {}
# Idempotency: if issue already exists, return existing values
if manifest.get("issueUrl"):
output = {
"issueUrl": manifest["issueUrl"],
"issueNumber": manifest.get("issueNumber", 0),
}
print(json.dumps(output))
return
run_id = manifest.get("runId", "unknown")
# Read sweep data
scan_index = read_jsonl(os.path.join(data_dir, "scan-index.jsonl"))
all_findings = read_jsonl(os.path.join(data_dir, "all-findings.jsonl"))
verified = read_jsonl(os.path.join(data_dir, "verified.jsonl"))
rejected = read_jsonl(os.path.join(data_dir, "rejected.jsonl"))
files_scanned = sum(1 for e in scan_index if e.get("status") == "complete")
# Build issue
title = f"Warden Sweep {run_id}: {len(verified)} findings across {files_scanned} files"
body = build_issue_body(run_id, scan_index, all_findings, verified, rejected)
print("Creating tracking issue...", file=sys.stderr)
result = create_github_issue(title, body)
print(f"Created issue: {result['issueUrl']}", file=sys.stderr)
# Write issueUrl and issueNumber to manifest
manifest["issueUrl"] = result["issueUrl"]
manifest["issueNumber"] = result["issueNumber"]
manifest.setdefault("phases", {})["issue"] = "complete"
write_json(manifest_path, manifest)
print(json.dumps(result))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# ///
"""
Extract individual findings from warden JSONL log files.
Usage:
python extract_findings.py <log-path-or-directory> -o <output.jsonl>
python extract_findings.py .warden/logs/ --scan-index data/scan-index.jsonl -o findings.jsonl
Reads warden JSONL logs (one skill record per line, summary as last line),
extracts each finding as a standalone record with a stable ID, and writes
one finding per line to the output file.
Finding ID format: <skill>-<sha256(title+path+line)[:8]>
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import sys
from pathlib import Path
from typing import Any
def generate_finding_id(skill: str, title: str, path: str, line: int | None) -> str:
"""Generate a stable, deterministic finding ID."""
raw = f"{title}:{path}:{line or 0}"
digest = hashlib.sha256(raw.encode()).hexdigest()[:8]
# Sanitize skill name for use in ID
safe_skill = skill.replace("/", "-").replace(" ", "-").lower()
return f"{safe_skill}-{digest}"
def parse_jsonl_log(log_path: str) -> list[dict[str, Any]]:
"""Parse a warden JSONL log file and extract individual findings.
Each non-summary line has the shape:
{
"run": {...},
"skill": "...",
"findings": [{...}, ...],
...
}
The last line is a summary record with "type": "summary" which we skip.
"""
findings = []
try:
with open(log_path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
except json.JSONDecodeError:
continue
# Skip summary records
if record.get("type") == "summary":
continue
skill = record.get("skill", "unknown")
run_meta = record.get("run", {})
record_findings = record.get("findings", [])
for finding in record_findings:
location = finding.get("location", {})
file_path = location.get("path", "")
start_line = location.get("startLine")
end_line = location.get("endLine")
finding_id = generate_finding_id(
skill=skill,
title=finding.get("title", ""),
path=file_path,
line=start_line,
)
normalized = {
"findingId": finding_id,
"file": file_path,
"skill": skill,
"severity": finding.get("severity", "info"),
"confidence": finding.get("confidence"),
"title": finding.get("title", ""),
"description": finding.get("description", ""),
"verification": finding.get("verification"),
"location": {
"path": file_path,
"startLine": start_line,
"endLine": end_line,
},
"suggestedFix": finding.get("suggestedFix"),
"logPath": log_path,
"runId": run_meta.get("runId", ""),
}
findings.append(normalized)
except (OSError, IOError) as e:
print(f"Error reading {log_path}: {e}", file=sys.stderr)
return findings
def collect_log_paths(source: str, scan_index: str | None = None) -> list[str]:
"""Collect log file paths from a directory or scan index."""
paths: list[str] = []
if scan_index and os.path.exists(scan_index):
# Read log paths from scan-index.jsonl
seen = set()
total_entries = 0
missing = 0
with open(scan_index) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if entry.get("status") != "complete":
continue
total_entries += 1
log_path = entry.get("logPath", "")
if log_path and log_path not in seen:
seen.add(log_path)
if os.path.isfile(log_path):
paths.append(log_path)
else:
missing += 1
if missing > 0:
print(
f"Warning: {missing} log path(s) from scan-index not found on disk",
file=sys.stderr,
)
# Only use scan-index results if we actually found logs;
# fall through to source directory otherwise
if paths:
return paths
if total_entries > 0:
print(
"Warning: scan-index had entries but no valid log paths; "
"falling back to source directory",
file=sys.stderr,
)
source_path = Path(source)
if source_path.is_file():
return [str(source_path)]
if source_path.is_dir():
for f in sorted(source_path.glob("*.jsonl")):
paths.append(str(f))
return paths
print(f"Source not found: {source}", file=sys.stderr)
return paths
def main():
parser = argparse.ArgumentParser(
description="Extract findings from warden JSONL logs"
)
parser.add_argument(
"source",
help="Path to a JSONL log file or directory of log files",
)
parser.add_argument(
"-o", "--output",
required=True,
help="Output path for normalized findings JSONL",
)
parser.add_argument(
"--scan-index",
help="Path to scan-index.jsonl (uses log paths from completed scans)",
)
args = parser.parse_args()
log_paths = collect_log_paths(args.source, args.scan_index)
if not log_paths:
print("No log files found.", file=sys.stderr)
sys.exit(1)
all_findings: list[dict[str, Any]] = []
seen_ids: set[str] = set()
for log_path in log_paths:
findings = parse_jsonl_log(log_path)
for f in findings:
fid = f["findingId"]
if fid not in seen_ids:
seen_ids.add(fid)
all_findings.append(f)
# Write output
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
with open(args.output, "w") as out:
for finding in all_findings:
out.write(json.dumps(finding) + "\n")
print(
json.dumps({
"logsProcessed": len(log_paths),
"findingsExtracted": len(all_findings),
"outputPath": args.output,
})
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# ///
"""
Find top git contributors for a file to use as PR reviewers.
Usage:
python find_reviewers.py <file-path>
python find_reviewers.py src/foo.ts
Output: JSON to stdout with GitHub usernames of top 2 contributors
from the last 12 months.
{"reviewers": ["user1", "user2"]}
If no contributors found or mapping fails, returns empty list.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _utils import run_cmd_stdout as run_cmd # noqa: E402
def get_top_authors(file_path: str, count: int = 2) -> list[str]:
"""Get top N author emails for a file from git log (last 12 months)."""
output = run_cmd([
"git", "log",
"--format=%ae",
"--since=12 months ago",
"--", file_path,
])
if not output:
return []
# Count occurrences of each email
email_counts: dict[str, int] = {}
for email in output.splitlines():
email = email.strip()
if email:
email_counts[email] = email_counts.get(email, 0) + 1
# Sort by count descending
sorted_emails = sorted(email_counts.items(), key=lambda x: x[1], reverse=True)
return [email for email, _ in sorted_emails[:count]]
def email_to_github_username(email: str) -> str | None:
"""Try to map a git email to a GitHub username.
Extracts from noreply emails directly. For other emails,
uses the GitHub search-by-email API via gh CLI.
"""
# Handle GitHub noreply emails directly
if email.endswith("@users.noreply.github.com"):
# Format: 12345+username@users.noreply.github.com
# or: username@users.noreply.github.com
local = email.split("@")[0]
if "+" in local:
return local.split("+", 1)[1]
return local
# gh api handles URL encoding; pass email directly in the query
output = run_cmd([
"gh", "api", f"search/users?q={email}+in:email",
"--jq", ".items[0].login",
])
return output if output else None
def get_current_github_user() -> str | None:
"""Get the currently authenticated GitHub username."""
output = run_cmd(["gh", "api", "/user", "--jq", ".login"])
return output if output else None
def main():
parser = argparse.ArgumentParser(
description="Find top git contributors for PR reviewer assignment"
)
parser.add_argument("file_path", help="Path to the file to find reviewers for")
parser.add_argument(
"--count", type=int, default=2,
help="Number of reviewers to find (default: 2)",
)
args = parser.parse_args()
current_user = get_current_github_user()
# Request extra candidates to compensate for self-exclusion
fetch_count = args.count + 1 if current_user else args.count
emails = get_top_authors(args.file_path, fetch_count)
if not emails:
print(json.dumps({"reviewers": [], "note": "No recent authors found"}))
return
reviewers: list[str] = []
for email in emails:
username = email_to_github_username(email)
if username and username != current_user:
reviewers.append(username)
print(json.dumps({"reviewers": reviewers[:args.count]}))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# ///
"""
Generate summary.md and report.json from a completed sweep.
Usage:
python generate_report.py <sweep-dir>
Reads the data/ subdirectory for all-findings.jsonl, verified.jsonl,
rejected.jsonl, patches.jsonl, and security/index.jsonl, then produces:
- <sweep-dir>/summary.md
- <sweep-dir>/data/report.json
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from datetime import datetime, timezone
from typing import Any
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _utils import read_json, read_jsonl, severity_badge # noqa: E402
def generate_summary_md(
manifest: dict[str, Any],
scan_index: list[dict[str, Any]],
all_findings: list[dict[str, Any]],
verified: list[dict[str, Any]],
rejected: list[dict[str, Any]],
patches: list[dict[str, Any]],
security_index: list[dict[str, Any]],
) -> str:
"""Generate the summary.md content."""
run_id = manifest.get("runId", "unknown")
started_at = manifest.get("startedAt", "unknown")
repo = manifest.get("repo", "unknown")
completed_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
files_scanned = sum(1 for e in scan_index if e.get("status") == "complete")
files_timed_out = sum(
1 for e in scan_index
if e.get("status") == "error" and e.get("error") == "timeout"
)
files_errored = sum(
1 for e in scan_index
if e.get("status") == "error" and e.get("error") != "timeout"
)
prs_created = sum(1 for p in patches if p.get("status") == "created")
prs_failed = sum(1 for p in patches if p.get("status") == "error")
# Severity breakdown of verified findings
by_severity: dict[str, int] = {}
for f in verified:
sev = f.get("severity", "info")
by_severity[sev] = by_severity.get(sev, 0) + 1
lines = [
f"# Warden Sweep: `{run_id}`",
"",
f"**Repo**: {repo}",
f"**Started**: {started_at}",
f"**Completed**: {completed_at}",
"",
"## Stats",
"",
f"| Metric | Count |",
f"|--------|-------|",
f"| Files scanned | {files_scanned} |",
f"| Files timed out | {files_timed_out} |",
f"| Files errored | {files_errored} |",
f"| Total findings | {len(all_findings)} |",
f"| Verified | {len(verified)} |",
f"| Rejected | {len(rejected)} |",
f"| PRs created | {prs_created} |",
f"| PRs failed | {prs_failed} |",
f"| Security findings | {len(security_index)} |",
"",
]
if by_severity:
lines.append("### By Severity")
lines.append("")
for sev in ["critical", "high", "medium", "low", "info"]:
count = by_severity.get(sev, 0)
if count > 0:
lines.append(f"- {severity_badge(sev)}: {count}")
lines.append("")
# Security callout
if security_index:
lines.append("## Security Findings")
lines.append("")
lines.append("The following findings are security-related and may need priority review:")
lines.append("")
lines.append("| ID | Severity | Skill | File | Title |")
lines.append("|----|----------|-------|------|-------|")
for sf in security_index:
fid = sf.get("findingId", "")
sev = severity_badge(sf.get("severity", "info"))
skill = sf.get("skill", "")
filepath = sf.get("file", "")
title = sf.get("title", "")
lines.append(f"| `{fid}` | {sev} | {skill} | `{filepath}` | {title} |")
lines.append("")
# Verified findings table
if verified:
lines.append("## Verified Findings")
lines.append("")
lines.append("| ID | Severity | Skill | File | Title | PR |")
lines.append("|----|----------|-------|------|-------|-----|")
# Build patches lookup
pr_lookup: dict[str, str] = {}
for p in patches:
if p.get("status") == "created" and p.get("findingId"):
pr_lookup[p["findingId"]] = p.get("prUrl", "")
for f in verified:
fid = f.get("findingId", "")
sev = severity_badge(f.get("severity", "info"))
skill = f.get("skill", "")
filepath = f.get("file", "")
title = f.get("title", "")
pr_url = pr_lookup.get(fid, "")
pr_link = f"[PR]({pr_url})" if pr_url else "-"
lines.append(f"| `{fid}` | {sev} | {skill} | `{filepath}` | {title} | {pr_link} |")
lines.append("")
# Rejected findings summary
if rejected:
lines.append(f"## Rejected Findings ({len(rejected)})")
lines.append("")
lines.append("These findings were evaluated and determined to be false positives.")
lines.append("See `data/rejected.jsonl` for details.")
lines.append("")
lines.append("---")
lines.append(f"*Generated by Warden Sweep `{run_id}`*")
return "\n".join(lines) + "\n"
def generate_report_json(
manifest: dict[str, Any],
scan_index: list[dict[str, Any]],
all_findings: list[dict[str, Any]],
verified: list[dict[str, Any]],
rejected: list[dict[str, Any]],
patches: list[dict[str, Any]],
security_index: list[dict[str, Any]],
) -> dict[str, Any]:
"""Generate the report.json data."""
run_id = manifest.get("runId", "unknown")
completed_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
files_scanned = sum(1 for e in scan_index if e.get("status") == "complete")
files_timed_out = sum(
1 for e in scan_index
if e.get("status") == "error" and e.get("error") == "timeout"
)
files_errored = sum(
1 for e in scan_index
if e.get("status") == "error" and e.get("error") != "timeout"
)
prs_created = sum(1 for p in patches if p.get("status") == "created")
prs_failed = sum(1 for p in patches if p.get("status") == "error")
# Count verify errors (findings in all but not in verified or rejected)
verified_ids = {f["findingId"] for f in verified if "findingId" in f}
rejected_ids = {f["findingId"] for f in rejected if "findingId" in f}
all_ids = {f["findingId"] for f in all_findings if "findingId" in f}
verify_errors = len(all_ids - verified_ids - rejected_ids)
return {
"runId": run_id,
"completedAt": completed_at,
"scan": {
"filesScanned": files_scanned,
"filesTimedOut": files_timed_out,
"filesErrored": files_errored,
"totalFindings": len(all_findings),
},
"verify": {
"verified": len(verified),
"rejected": len(rejected),
"errors": verify_errors,
},
"patch": {
"prsCreated": prs_created,
"prsFailed": prs_failed,
},
"security": {
"count": len(security_index),
},
"prs": [
{
"findingId": p.get("findingId", ""),
"url": p.get("prUrl", ""),
"severity": next(
(f.get("severity", "") for f in verified if f.get("findingId") == p.get("findingId")),
"",
),
}
for p in patches
if p.get("status") == "created"
],
}
def main():
parser = argparse.ArgumentParser(
description="Generate sweep summary and report"
)
parser.add_argument("sweep_dir", help="Path to the sweep output directory")
args = parser.parse_args()
sweep_dir = args.sweep_dir
data_dir = os.path.join(sweep_dir, "data")
# Read inputs
manifest = read_json(os.path.join(data_dir, "manifest.json")) or {}
scan_index = read_jsonl(os.path.join(data_dir, "scan-index.jsonl"))
all_findings = read_jsonl(os.path.join(data_dir, "all-findings.jsonl"))
verified = read_jsonl(os.path.join(data_dir, "verified.jsonl"))
rejected = read_jsonl(os.path.join(data_dir, "rejected.jsonl"))
patches = read_jsonl(os.path.join(data_dir, "patches.jsonl"))
security_index = read_jsonl(os.path.join(sweep_dir, "security", "index.jsonl"))
# Generate summary.md
summary_md = generate_summary_md(
manifest, scan_index,
all_findings, verified, rejected, patches, security_index,
)
summary_path = os.path.join(sweep_dir, "summary.md")
with open(summary_path, "w") as f:
f.write(summary_md)
# Generate report.json
report = generate_report_json(
manifest, scan_index, all_findings,
verified, rejected, patches, security_index,
)
report_path = os.path.join(data_dir, "report.json")
with open(report_path, "w") as f:
json.dump(report, f, indent=2)
f.write("\n")
print(json.dumps({
"summaryPath": summary_path,
"reportPath": report_path,
"verified": len(verified),
"rejected": len(rejected),
"prsCreated": report["patch"]["prsCreated"],
"securityFindings": len(security_index),
}))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# ///
"""
Warden Sweep: Index existing PRs for deduplication.
Fetches open warden-labeled PRs via gh, identifies file overlap with
verified findings, and caches diffs for overlapping PRs.
Usage:
uv run index_prs.py <sweep-dir>
Stdout: JSON summary (for LLM consumption)
Stderr: Progress lines
Side effects:
- Creates data/existing-prs.json
- Creates data/pr-diffs/<number>.diff for overlapping PRs
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from typing import Any
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _utils import read_jsonl, run_cmd # noqa: E402
def fetch_warden_prs(sweep_dir: str) -> list[dict[str, Any]]:
"""Fetch open PRs with the warden label."""
result = run_cmd(
[
"gh", "pr", "list",
"--label", "warden",
"--state", "open",
"--json", "number,title,url,files",
"--limit", "100",
],
timeout=30,
)
if result.returncode != 0:
print(f"Warning: gh pr list failed: {result.stderr}", file=sys.stderr)
return []
try:
prs = json.loads(result.stdout)
except json.JSONDecodeError:
print("Warning: Failed to parse gh pr list output", file=sys.stderr)
return []
# Save raw PR data
prs_path = os.path.join(sweep_dir, "data", "existing-prs.json")
with open(prs_path, "w") as f:
json.dump(prs, f, indent=2)
f.write("\n")
return prs
def build_file_index(
prs: list[dict[str, Any]],
) -> dict[str, list[dict[str, Any]]]:
"""Build a file-to-PR lookup from the PR list."""
index: dict[str, list[dict[str, Any]]] = {}
for pr in prs:
pr_info = {
"number": pr.get("number"),
"title": pr.get("title", ""),
"url": pr.get("url", ""),
}
files = pr.get("files") or []
for file_entry in files:
# gh returns files as objects with "path" key
if isinstance(file_entry, dict):
path = file_entry.get("path", "")
else:
path = str(file_entry)
if path:
index.setdefault(path, []).append(pr_info)
return index
def get_verified_files(sweep_dir: str) -> set[str]:
"""Get the set of files that have verified findings."""
verified_path = os.path.join(sweep_dir, "data", "verified.jsonl")
entries = read_jsonl(verified_path)
return {e.get("file", "") for e in entries if e.get("file")}
def fetch_pr_diff(pr_number: int, sweep_dir: str) -> bool:
"""Fetch and cache a PR diff. Returns True on success."""
diff_path = os.path.join(
sweep_dir, "data", "pr-diffs", f"{pr_number}.diff"
)
# Skip if already cached
if os.path.exists(diff_path):
return True
result = run_cmd(
["gh", "pr", "diff", str(pr_number)],
timeout=30,
)
if result.returncode != 0:
print(
f"Warning: Failed to fetch diff for PR #{pr_number}: {result.stderr}",
file=sys.stderr,
)
return False
with open(diff_path, "w") as f:
f.write(result.stdout)
return True
def main() -> None:
parser = argparse.ArgumentParser(
description="Warden Sweep: Index existing PRs for dedup"
)
parser.add_argument("sweep_dir", help="Path to the sweep directory")
args = parser.parse_args()
sweep_dir = args.sweep_dir
if not os.path.isdir(sweep_dir):
print(
json.dumps({"error": f"Sweep directory not found: {sweep_dir}"}),
file=sys.stdout,
)
sys.exit(1)
# Ensure pr-diffs directory exists
os.makedirs(os.path.join(sweep_dir, "data", "pr-diffs"), exist_ok=True)
# Fetch open warden PRs
print("Fetching open warden-labeled PRs...", file=sys.stderr)
prs = fetch_warden_prs(sweep_dir)
print(f"Found {len(prs)} open warden PR(s)", file=sys.stderr)
# Build file index
file_index = build_file_index(prs)
# Find overlap with verified findings
verified_files = get_verified_files(sweep_dir)
overlapping_prs: set[int] = set()
for vfile in verified_files:
if vfile in file_index:
for pr_info in file_index[vfile]:
overlapping_prs.add(pr_info["number"])
# Fetch diffs for overlapping PRs
diffs_cached = 0
for pr_number in sorted(overlapping_prs):
print(f"Caching diff for PR #{pr_number}...", file=sys.stderr)
if fetch_pr_diff(pr_number, sweep_dir):
diffs_cached += 1
# Build output file index (only for files that have verified findings)
output_file_index: dict[str, list[dict[str, Any]]] = {}
for vfile in verified_files:
if vfile in file_index:
output_file_index[vfile] = file_index[vfile]
# Output summary
output = {
"totalPRs": len(prs),
"overlappingPRs": len(overlapping_prs),
"fileIndex": output_file_index,
"diffsCached": diffs_cached,
}
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# ///
"""
Warden Sweep: Organize phase.
Identifies security findings, creates security indexes, labels security PRs,
updates finding reports with PR links, generates summary report, and
finalizes the manifest.
Usage:
uv run organize.py <sweep-dir>
Stdout: JSON summary (for LLM consumption)
Stderr: Progress lines
Side effects:
- Creates security/index.jsonl with security findings
- Copies security finding .md files to security/
- Creates "security" label on GitHub
- Labels security PRs with "security"
- Appends PR links to findings/*.md
- Runs generate_report.py for summary.md and report.json
- Updates manifest phases.organize to "complete"
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
from datetime import datetime, timezone
from typing import Any
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _utils import ensure_github_label, pr_number_from_url, read_json, read_jsonl, write_json # noqa: E402
SECURITY_SKILL_PATTERNS = [
"security-review",
"owasp-review",
"security-audit",
]
def is_security_skill(skill_name: str) -> bool:
"""Check if a skill name indicates a security-related skill."""
name_lower = skill_name.lower()
if "security" in name_lower:
return True
return name_lower in SECURITY_SKILL_PATTERNS
def severity_label(severity: str) -> str:
"""Format a severity string for inline display in issue comments."""
if not severity:
return ""
if severity in ("critical", "high"):
return f" (**{severity.upper()}**)"
return f" ({severity.upper()})"
def identify_security_findings(
sweep_dir: str,
) -> list[dict[str, Any]]:
"""Find security-related verified findings and write security/index.jsonl."""
verified = read_jsonl(os.path.join(sweep_dir, "data", "verified.jsonl"))
security_findings: list[dict[str, Any]] = []
for finding in verified:
skill = finding.get("skill", "")
if is_security_skill(skill):
entry = {
"findingId": finding.get("findingId", ""),
"skill": skill,
"severity": finding.get("severity", "info"),
"file": finding.get("file", ""),
"title": finding.get("title", ""),
}
security_findings.append(entry)
# Write security index
security_dir = os.path.join(sweep_dir, "security")
os.makedirs(security_dir, exist_ok=True)
index_path = os.path.join(security_dir, "index.jsonl")
with open(index_path, "w") as f:
for entry in security_findings:
f.write(json.dumps(entry) + "\n")
return security_findings
def copy_security_findings(
sweep_dir: str, security_findings: list[dict[str, Any]]
) -> None:
"""Copy security finding .md files to security/ directory."""
findings_dir = os.path.join(sweep_dir, "findings")
security_dir = os.path.join(sweep_dir, "security")
for finding in security_findings:
fid = finding.get("findingId", "")
src = os.path.join(findings_dir, f"{fid}.md")
dst = os.path.join(security_dir, f"{fid}.md")
if os.path.exists(src):
shutil.copy2(src, dst)
def create_security_label() -> None:
"""Create the security label on GitHub (idempotent)."""
ensure_github_label("security", "D93F0B", "Security-related changes")
def label_security_prs(
sweep_dir: str, security_findings: list[dict[str, Any]]
) -> int:
"""Add "security" label to PRs for security findings. Returns count labeled."""
patches = read_jsonl(os.path.join(sweep_dir, "data", "patches.jsonl"))
security_ids = {f.get("findingId", "") for f in security_findings}
labeled = 0
for patch in patches:
if patch.get("status") != "created":
continue
if patch.get("findingId", "") not in security_ids:
continue
pr_url = patch.get("prUrl", "")
if not pr_url:
continue
try:
result = subprocess.run(
["gh", "pr", "edit", pr_url, "--add-label", "security"],
capture_output=True,
text=True,
timeout=15,
)
if result.returncode == 0:
labeled += 1
else:
print(
f"Warning: Failed to label PR {pr_url}: {result.stderr.strip()}",
file=sys.stderr,
)
except (subprocess.TimeoutExpired, FileNotFoundError):
print(
f"Warning: Failed to label PR {pr_url}",
file=sys.stderr,
)
return labeled
def _has_sweep_complete_comment(issue_url: str) -> bool:
"""Check if the tracking issue already has a 'Sweep Complete' comment."""
try:
result = subprocess.run(
["gh", "issue", "view", issue_url, "--json", "comments", "--jq",
'.comments[].body | select(startswith("## Sweep Complete"))'],
capture_output=True,
text=True,
timeout=15,
)
return result.returncode == 0 and result.stdout.strip() != ""
except (subprocess.TimeoutExpired, FileNotFoundError):
return False
def update_tracking_issue(sweep_dir: str) -> None:
"""Post a comment on the tracking issue with final PR results. Idempotent."""
manifest = read_json(os.path.join(sweep_dir, "data", "manifest.json"))
if not manifest:
return
issue_url = manifest.get("issueUrl")
if not issue_url:
return
if _has_sweep_complete_comment(issue_url):
print("Tracking issue already has completion comment, skipping.", file=sys.stderr)
return
patches = read_jsonl(os.path.join(sweep_dir, "data", "patches.jsonl"))
verified = read_jsonl(os.path.join(sweep_dir, "data", "verified.jsonl"))
security_index = read_jsonl(os.path.join(sweep_dir, "security", "index.jsonl"))
# Build lookup from findingId to verified finding
verified_lookup: dict[str, dict[str, Any]] = {}
for f in verified:
fid = f.get("findingId", "")
if fid:
verified_lookup[fid] = f
security_ids = {f.get("findingId", "") for f in security_index}
created = sum(1 for p in patches if p.get("status") == "created")
existing = sum(1 for p in patches if p.get("status") == "existing")
failed = sum(1 for p in patches if p.get("status") == "error")
lines = [
"## Sweep Complete",
"",
"| PRs Created | PRs Skipped (existing) | PRs Failed | Security Findings |",
"|-------------|------------------------|------------|-------------------|",
f"| {created} | {existing} | {failed} | {len(security_index)} |",
"",
]
# PR task list
pr_entries = [p for p in patches if p.get("status") == "created" and p.get("prUrl")]
if pr_entries:
lines.append("### PRs")
lines.append("")
for p in pr_entries:
fid = p.get("findingId", "")
pr_number = pr_number_from_url(p.get("prUrl", ""))
finding = verified_lookup.get(fid, {})
title = finding.get("title", fid)
sev = severity_label(finding.get("severity", ""))
lines.append(f"- [ ] #{pr_number} - fix: {title}{sev}")
lines.append("")
# Security findings section
security_prs = [
p for p in patches
if p.get("status") == "created"
and p.get("findingId", "") in security_ids
and p.get("prUrl")
]
if security_prs:
lines.append("### Security Findings")
lines.append("")
for p in security_prs:
fid = p.get("findingId", "")
pr_number = pr_number_from_url(p.get("prUrl", ""))
finding = verified_lookup.get(fid, {})
title = finding.get("title", fid)
sev = severity_label(finding.get("severity", ""))
lines.append(f"- #{pr_number} - {title}{sev}")
lines.append("")
body = "\n".join(lines)
try:
result = subprocess.run(
["gh", "issue", "comment", issue_url, "--body", body],
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
print(
f"Warning: Failed to comment on tracking issue: {result.stderr.strip()}",
file=sys.stderr,
)
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
print(
f"Warning: Failed to comment on tracking issue: {e}",
file=sys.stderr,
)
def update_findings_with_pr_links(sweep_dir: str) -> None:
"""Append PR links to findings/*.md for created PRs."""
patches = read_jsonl(os.path.join(sweep_dir, "data", "patches.jsonl"))
findings_dir = os.path.join(sweep_dir, "findings")
for patch in patches:
if patch.get("status") != "created":
continue
fid = patch.get("findingId", "")
pr_url = patch.get("prUrl", "")
branch = patch.get("branch", "")
reviewers = patch.get("reviewers", [])
if not fid or not pr_url:
continue
md_path = os.path.join(findings_dir, f"{fid}.md")
if not os.path.exists(md_path):
continue
# Check if PR section already appended
with open(md_path) as f:
content = f.read()
if "## Pull Request" in content:
continue
reviewers_str = ", ".join(reviewers) if reviewers else "none"
pr_section = (
f"\n\n## Pull Request\n"
f"**PR**: {pr_url}\n"
f"**Branch**: {branch}\n"
f"**Reviewers**: {reviewers_str}\n"
)
with open(md_path, "a") as f:
f.write(pr_section)
def run_generate_report(sweep_dir: str, script_dir: str) -> None:
"""Run generate_report.py as a subprocess."""
report_script = os.path.join(script_dir, "generate_report.py")
try:
result = subprocess.run(
[sys.executable, report_script, sweep_dir],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode != 0:
print(
f"Warning: generate_report.py failed: {result.stderr}",
file=sys.stderr,
)
except Exception as e:
print(f"Warning: generate_report.py failed: {e}", file=sys.stderr)
def update_manifest(sweep_dir: str) -> None:
"""Mark organize phase complete and add completedAt timestamp."""
manifest_path = os.path.join(sweep_dir, "data", "manifest.json")
manifest = read_json(manifest_path)
if not manifest:
return
manifest.setdefault("phases", {})["organize"] = "complete"
manifest["completedAt"] = datetime.now(timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
write_json(manifest_path, manifest)
def main() -> None:
parser = argparse.ArgumentParser(
description="Warden Sweep: Organize phase"
)
parser.add_argument("sweep_dir", help="Path to the sweep directory")
args = parser.parse_args()
sweep_dir = args.sweep_dir
if not os.path.isdir(sweep_dir):
print(
json.dumps({"error": f"Sweep directory not found: {sweep_dir}"}),
file=sys.stdout,
)
sys.exit(1)
script_dir = os.path.dirname(os.path.abspath(__file__))
# Step 1: Identify security findings
print("Identifying security findings...", file=sys.stderr)
security_findings = identify_security_findings(sweep_dir)
print(
f"Found {len(security_findings)} security finding(s)",
file=sys.stderr,
)
# Step 2: Label security PRs
security_prs_labeled = 0
if security_findings:
print("Creating security label...", file=sys.stderr)
create_security_label()
print("Labeling security PRs...", file=sys.stderr)
security_prs_labeled = label_security_prs(sweep_dir, security_findings)
# Step 3: Update finding reports with PR links
print("Updating finding reports with PR links...", file=sys.stderr)
update_findings_with_pr_links(sweep_dir)
# Step 4: Copy security finding reports (after PR links are added)
copy_security_findings(sweep_dir, security_findings)
# Step 5: Generate summary and report
print("Generating summary and report...", file=sys.stderr)
run_generate_report(sweep_dir, script_dir)
# Step 6: Update tracking issue with PR results
print("Updating tracking issue...", file=sys.stderr)
update_tracking_issue(sweep_dir)
# Step 7: Update manifest
update_manifest(sweep_dir)
# Gather stats for output
scan_index = read_jsonl(os.path.join(sweep_dir, "data", "scan-index.jsonl"))
verified = read_jsonl(os.path.join(sweep_dir, "data", "verified.jsonl"))
rejected = read_jsonl(os.path.join(sweep_dir, "data", "rejected.jsonl"))
patches = read_jsonl(os.path.join(sweep_dir, "data", "patches.jsonl"))
files_scanned = sum(1 for e in scan_index if e.get("status") == "complete")
prs_created = sum(1 for p in patches if p.get("status") == "created")
summary_path = os.path.join(sweep_dir, "summary.md")
report_path = os.path.join(sweep_dir, "data", "report.json")
output = {
"securityFindings": len(security_findings),
"securityPRsLabeled": security_prs_labeled,
"summaryPath": summary_path,
"reportPath": report_path,
"stats": {
"filesScanned": files_scanned,
"verified": len(verified),
"rejected": len(rejected),
"prsCreated": prs_created,
"securityFindings": len(security_findings),
},
}
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# dependencies = ["tomli; python_version < '3.11'"]
# ///
"""
Warden Sweep: Scan phase.
Replaces Phase 0 (setup) and Phase 1 (scan) with a single script.
Generates a run ID, creates the sweep directory, checks dependencies,
creates the warden label, enumerates files, runs warden on each file,
writes scan-index.jsonl, and calls extract_findings.py.
Usage:
uv run scan.py [file ...]
uv run scan.py --sweep-dir .warden/sweeps/abc123
uv run scan.py src/foo.ts src/bar.ts
Stdout: JSON summary (for LLM consumption)
Stderr: Progress lines as files complete
Exit codes: 0 = success, 1 = fatal, 2 = partial (some files errored)
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import secrets
import subprocess
import sys
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib # type: ignore[no-redefine]
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _utils import ensure_github_label, run_cmd # noqa: E402
SUPPORTED_EXTENSIONS = {
".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java",
".rb", ".php", ".c", ".cpp", ".h", ".hpp", ".cs", ".swift",
".kt", ".scala", ".sh", ".bash", ".zsh",
}
def generate_run_id() -> str:
"""Generate a short random run ID."""
return secrets.token_hex(4)
def check_dependencies() -> list[str]:
"""Check that required commands are available. Return list of missing."""
import shutil
return [cmd for cmd in ["warden", "gh", "git"] if shutil.which(cmd) is None]
def create_sweep_dir(sweep_dir: str) -> None:
"""Create the sweep directory structure."""
for subdir in [
"findings",
"security",
"data/verify",
"data/logs",
"data/pr-diffs",
]:
os.makedirs(os.path.join(sweep_dir, subdir), exist_ok=True)
def write_manifest(sweep_dir: str, run_id: str) -> None:
"""Write the initial manifest.json."""
repo = "unknown"
try:
result = run_cmd(["git", "remote", "get-url", "origin"])
if result.returncode == 0 and result.stdout.strip():
repo = result.stdout.strip()
else:
repo = os.path.basename(os.getcwd())
except Exception:
repo = os.path.basename(os.getcwd())
manifest = {
"runId": run_id,
"startedAt": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"repo": repo,
"phases": {
"scan": "pending",
"verify": "pending",
"issue": "pending",
"patch": "pending",
"organize": "pending",
},
}
manifest_path = os.path.join(sweep_dir, "data", "manifest.json")
with open(manifest_path, "w") as f:
json.dump(manifest, f, indent=2)
f.write("\n")
def load_ignore_paths() -> list[str]:
"""Load ignorePaths from warden.toml defaults if present."""
toml_path = "warden.toml"
if not os.path.exists(toml_path):
return []
try:
with open(toml_path, "rb") as f:
config = tomllib.load(f)
paths = config.get("defaults", {}).get("ignorePaths", [])
return paths if isinstance(paths, list) else []
except Exception:
return []
def should_ignore(path: str, ignore_patterns: list[str]) -> bool:
"""Check if a path matches any ignore pattern (simple glob matching)."""
if not ignore_patterns:
return False
from fnmatch import fnmatch
for pattern in ignore_patterns:
if fnmatch(path, pattern):
return True
# Handle ** patterns
if "**" in pattern:
# Convert ** glob to work with fnmatch
simple = pattern.replace("**/", "*/")
if fnmatch(path, simple):
return True
# Also try zero-directory match (** matches zero directories)
collapsed = pattern.replace("**/", "")
if fnmatch(path, collapsed):
return True
# Also try matching any subdirectory
parts = path.split("/")
glob_parts = pattern.split("/")
if glob_parts[0] == "**":
# Match from any point
rest = "/".join(glob_parts[1:])
for i in range(len(parts)):
if fnmatch("/".join(parts[i:]), rest):
return True
elif glob_parts[-1].startswith("*"):
# e.g., dist/** matches dist/anything, src/**/*.py matches src/x/y.py
prefix = pattern.split("**")[0].rstrip("/")
if path.startswith(prefix + "/") or path == prefix:
suffix = pattern.split("**")[-1]
if not suffix or suffix == "/":
# Pure prefix pattern like dist/** - any subpath matches
return True
# Has suffix like **/*.py - check with fnmatch on the remaining path
remaining = path[len(prefix) :].lstrip("/")
suffix_pattern = suffix.lstrip("/")
if fnmatch(remaining, suffix_pattern) or fnmatch(
remaining.split("/")[-1], suffix_pattern
):
return True
return False
def enumerate_files(
specific_files: list[str] | None, ignore_patterns: list[str]
) -> list[str]:
"""Enumerate files to scan using git ls-files, filtered by extension."""
if specific_files:
return [f for f in specific_files if not should_ignore(f, ignore_patterns)]
result = run_cmd(["git", "ls-files"])
if result.returncode != 0:
print(f"git ls-files failed: {result.stderr}", file=sys.stderr)
return []
files = []
for line in result.stdout.splitlines():
path = line.strip()
if not path:
continue
# Filter by extension
ext = os.path.splitext(path)[1].lower()
if ext not in SUPPORTED_EXTENSIONS:
continue
# Filter by ignore patterns
if should_ignore(path, ignore_patterns):
continue
files.append(path)
return files
def load_completed_files(sweep_dir: str) -> set[str]:
"""Load already-completed files from scan-index.jsonl for incrementality."""
index_path = os.path.join(sweep_dir, "data", "scan-index.jsonl")
completed: set[str] = set()
if not os.path.exists(index_path):
return completed
with open(index_path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
if entry.get("status") == "complete":
completed.add(entry.get("file", ""))
except json.JSONDecodeError:
continue
return completed
def log_path_for_file(sweep_dir: str, file_path: str) -> str:
"""Generate a deterministic log path for a file."""
digest = hashlib.sha256(file_path.encode()).hexdigest()[:16]
return os.path.join(sweep_dir, "data", "logs", f"{digest}.jsonl")
def scan_file(
file_path: str, log_file: str, timeout: int = 600, skill: str | None = None
) -> dict[str, Any]:
"""Run warden on a single file. Returns scan-index entry."""
try:
cmd = [
"warden", file_path,
"--json", "--log",
"--min-confidence", "off",
"--fail-on", "off",
"--quiet",
"--output", log_file,
]
if skill:
cmd.extend(["--skill", skill])
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
)
# Check for warden failure
if result.returncode != 0:
error_msg = result.stderr.strip() if result.stderr else "non-zero exit"
return {
"file": file_path,
"status": "error",
"error": f"warden failed: {error_msg}",
"exitCode": result.returncode,
}
# Check that log file was created
if not os.path.exists(log_file):
return {
"file": file_path,
"status": "error",
"error": "log file not created",
"exitCode": result.returncode,
}
# Count findings from the log file
finding_count = 0
skills: set[str] = set()
with open(log_file) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
if record.get("type") == "summary":
continue
record_skill = record.get("skill", "")
if record_skill:
skills.add(record_skill)
findings = record.get("findings", [])
finding_count += len(findings)
except json.JSONDecodeError:
continue
return {
"file": file_path,
"logPath": log_file,
"skills": sorted(skills),
"findingCount": finding_count,
"status": "complete",
"exitCode": result.returncode,
}
except subprocess.TimeoutExpired:
return {
"file": file_path,
"status": "error",
"error": "timeout",
"exitCode": -1,
}
except FileNotFoundError:
return {
"file": file_path,
"status": "error",
"error": "warden not found",
"exitCode": -1,
}
except Exception as e:
return {
"file": file_path,
"status": "error",
"error": str(e),
"exitCode": -1,
}
def run_extract_findings(sweep_dir: str, script_dir: str) -> None:
"""Run extract_findings.py as a subprocess."""
extract_script = os.path.join(script_dir, "extract_findings.py")
logs_dir = os.path.join(sweep_dir, "data", "logs")
scan_index = os.path.join(sweep_dir, "data", "scan-index.jsonl")
output = os.path.join(sweep_dir, "data", "all-findings.jsonl")
try:
result = subprocess.run(
[
sys.executable, extract_script,
logs_dir,
"--scan-index", scan_index,
"-o", output,
],
capture_output=True,
text=True,
timeout=120,
)
if result.returncode != 0:
print(
f"Warning: extract_findings.py failed: {result.stderr}",
file=sys.stderr,
)
except Exception as e:
print(f"Warning: extract_findings.py failed: {e}", file=sys.stderr)
def load_findings_compact(sweep_dir: str) -> tuple[list[dict[str, Any]], dict[str, int]]:
"""Load findings from all-findings.jsonl and return compact list + severity counts."""
findings_path = os.path.join(sweep_dir, "data", "all-findings.jsonl")
findings: list[dict[str, Any]] = []
by_severity: dict[str, int] = {}
if not os.path.exists(findings_path):
return findings, by_severity
with open(findings_path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
severity = record.get("severity", "info")
by_severity[severity] = by_severity.get(severity, 0) + 1
location = record.get("location", {})
findings.append({
"findingId": record.get("findingId", ""),
"title": record.get("title", ""),
"file": record.get("file", ""),
"startLine": location.get("startLine"),
"severity": severity,
"confidence": record.get("confidence"),
"skill": record.get("skill", ""),
})
except json.JSONDecodeError:
continue
return findings, by_severity
def update_manifest_phase(sweep_dir: str, phase: str, status: str) -> None:
"""Update a phase status in manifest.json."""
manifest_path = os.path.join(sweep_dir, "data", "manifest.json")
if not os.path.exists(manifest_path):
return
with open(manifest_path) as f:
manifest = json.load(f)
manifest.setdefault("phases", {})[phase] = status
with open(manifest_path, "w") as f:
json.dump(manifest, f, indent=2)
f.write("\n")
def main() -> None:
parser = argparse.ArgumentParser(
description="Warden Sweep: Scan phase (setup + scan)"
)
parser.add_argument(
"files",
nargs="*",
help="Specific files to scan (default: all tracked files)",
)
parser.add_argument(
"--sweep-dir",
help="Resume into an existing sweep directory",
)
parser.add_argument(
"--skill",
help="Run only this skill (passed through to warden --skill)",
)
args = parser.parse_args()
# Check dependencies
missing = check_dependencies()
if missing:
print(
json.dumps({"error": f"Missing dependencies: {', '.join(missing)}"}),
file=sys.stdout,
)
sys.exit(1)
# Determine sweep dir and run ID
if args.sweep_dir:
sweep_dir = args.sweep_dir
# Extract run ID from path (normalize to handle trailing slashes)
run_id = os.path.basename(os.path.normpath(sweep_dir))
else:
run_id = generate_run_id()
sweep_dir = os.path.join(".warden", "sweeps", run_id)
# Setup
create_sweep_dir(sweep_dir)
# Only write manifest if it doesn't exist (for resume support)
manifest_path = os.path.join(sweep_dir, "data", "manifest.json")
if not os.path.exists(manifest_path):
write_manifest(sweep_dir, run_id)
ensure_github_label("warden", "5319E7", "Automated fix from Warden Sweep")
# Enumerate files
ignore_patterns = load_ignore_paths()
specific_files = args.files if args.files else None
files = enumerate_files(specific_files, ignore_patterns)
if not files:
print(
json.dumps({
"error": "No files to scan",
"runId": run_id,
"sweepDir": sweep_dir,
}),
file=sys.stdout,
)
sys.exit(1)
# Load completed files for incrementality
completed = load_completed_files(sweep_dir)
remaining = [f for f in files if f not in completed]
total = len(files)
already_done = len(completed & set(files))
scan_index_path = os.path.join(sweep_dir, "data", "scan-index.jsonl")
if already_done > 0:
print(
f"Resuming: {already_done}/{total} files already scanned",
file=sys.stderr,
)
# Scan remaining files concurrently
scanned = already_done
index_lock = threading.Lock()
def _scan_and_record(file_path: str) -> dict[str, Any]:
log_file = log_path_for_file(sweep_dir, file_path)
entry = scan_file(file_path, log_file, skill=args.skill)
with index_lock:
with open(scan_index_path, "a") as f:
f.write(json.dumps(entry) + "\n")
return entry
with ThreadPoolExecutor(max_workers=4) as pool:
futures = {
pool.submit(_scan_and_record, fp): fp for fp in remaining
}
for future in as_completed(futures):
entry = future.result()
scanned += 1
file_path = entry.get("file", futures[future])
if entry["status"] == "error":
label = "TIMEOUT" if entry.get("error") == "timeout" else "ERROR"
print(
f"[{scanned}/{total}] {file_path} ({label}: {entry.get('error', 'unknown')})",
file=sys.stderr,
)
else:
count = entry.get("findingCount", 0)
suffix = f"({count} finding{'s' if count != 1 else ''})" if count > 0 else ""
print(
f"[{scanned}/{total}] {file_path} {suffix}".rstrip(),
file=sys.stderr,
)
# Extract findings
script_dir = os.path.dirname(os.path.abspath(__file__))
run_extract_findings(sweep_dir, script_dir)
# Load findings for output
findings, by_severity = load_findings_compact(sweep_dir)
# Collect errors for output, deduplicating by file (last entry wins)
# so that resumed scans don't include stale errors for files that later succeeded.
# Scope to current file list so counts stay consistent with `scanned`.
files_set = set(files)
timeouts: list[dict[str, Any]] = []
errors: list[dict[str, Any]] = []
if os.path.exists(scan_index_path):
last_status: dict[str, dict[str, Any]] = {}
with open(scan_index_path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
file_path_key = entry.get("file", "")
if file_path_key in files_set:
last_status[file_path_key] = entry
except json.JSONDecodeError:
continue
for entry in last_status.values():
if entry.get("status") == "error":
item = {
"file": entry.get("file", ""),
"error": entry.get("error", "unknown"),
"exitCode": entry.get("exitCode", -1),
}
if entry.get("error") == "timeout":
timeouts.append(item)
else:
errors.append(item)
total_failed = len(timeouts) + len(errors)
# Output JSON summary
output = {
"runId": run_id,
"sweepDir": sweep_dir,
"filesScanned": scanned - total_failed,
"filesTimedOut": len(timeouts),
"filesErrored": len(errors),
"totalFindings": len(findings),
"bySeverity": by_severity,
"findingsPath": os.path.join(sweep_dir, "data", "all-findings.jsonl"),
"findings": findings,
"timeouts": timeouts,
"errors": errors,
}
print(json.dumps(output, indent=2))
# Fatal only if every file across all runs errored (no successful scans at all)
successful = scanned - total_failed
if successful == 0 and scanned > 0:
update_manifest_phase(sweep_dir, "scan", "error")
sys.exit(1)
update_manifest_phase(sweep_dir, "scan", "complete")
if total_failed > 0:
sys.exit(2)
if __name__ == "__main__":
main()
Related skills
FAQ
What does warden-sweep do?
warden-sweep scans the full repo, verifies issues, and opens draft PRs.
When should I use warden-sweep?
User requests full repo sweep, scan everything, or batch analysis.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.