
Warden Sweep
- 114 installs
- 358 repo stars
- Updated August 4, 2026
- getsentry/warden
Warden full-repo sweep scanning all files, verifying findings, and opening draft PRs.
About
Warden full-repository sweep skill. Scans every file with Warden analysis, verifies findings through deep tracing to reduce false positives, and creates draft PRs for validated issues. Triggered when asked to sweep the repo, scan all files, or run comprehensive code review. Combines automated detection with verification before opening PRs, ensuring only substantiated findings reach reviewers. Part of the Warden agent family for Sentry codebase quality at scale.
- Full-repository scan of every file with Warden analysis
- Deep tracing verification before reporting findings
- Draft PR creation for validated issues only
- Triggers on sweep the repo or scan all files requests
- False positive reduction through verification pass
Warden Sweep by the numbers
- 114 all-time installs (skills.sh)
- Ranked #432 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
- scan all files · verify findings · create draft prs
- Works with
- github · sentry
- Use cases
- code review · security audit
What warden-sweep says it does
Scans every file with Warden, verifies findings through deep tracing, creates draft PRs for validated issues.
npx skills add https://github.com/getsentry/warden --skill warden-sweepAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 114 |
|---|---|
| repo stars | ★ 358 |
| Last updated | August 4, 2026 |
| Repository | getsentry/warden ↗ |
How do I run a comprehensive code quality sweep across the entire repository?
Run a full-repository Warden code sweep scanning every file, verifying findings via deep tracing, and creating draft PRs for validated issues.
Who is it for?
Teams requesting full-repository Warden scans with PR-ready fixes.
Skip if: Single-file reviews or manual code review without Warden automation.
When should I use this skill?
User asks to sweep the repo, scan all files, or run comprehensive Warden review.
What you get
All files scanned, findings verified, and draft PRs opened for validated issues.
Files
Warden Sweep
Run a full-repository Warden sweep: scan files, verify findings, create a tracking issue, open draft PRs for validated issues, and organize the final report.
Requires: warden, gh, git, jq, uv.
Run commands from the repository root. Use the host's skill-root path for bundled scripts and references.
Output goes to .warden/sweeps/<run-id>/.
References
Load only the reference for the current phase:
| Need | Read |
|---|---|
| Script arguments, outputs, and side effects | references/script-interfaces.md |
| Phase 1 scan workflow | references/scan-phase.md |
| Phase 2 verification workflow | references/verify-phase.md |
| Phase 3 tracking issue workflow | references/issue-phase.md |
| Phase 4 patch and draft PR workflow | references/patch-phase.md |
| Phase 5 organize and final report workflow | references/organize-phase.md |
| Resume behavior and artifact layout | references/resume-and-artifacts.md |
| Verification task prompt template | references/verify-prompt.md |
| Patch task prompt template | references/patch-prompt.md |
Workflow
Track progress across phases:
- [ ] Phase 1: Scan repository files with Warden.
- [ ] Phase 2: Verify findings before patching.
- [ ] Phase 3: Create a tracking issue.
- [ ] Phase 4: Patch verified findings and open draft PRs.
- [ ] Phase 5: Organize results and produce the final report.
Phase Order
1. Read references/script-interfaces.md once before running scripts. 2. Run Phase 1 from references/scan-phase.md. Save runId and sweepDir. 3. Run Phase 2 from references/verify-phase.md. Verify every finding before patching. 4. Run Phase 3 from references/issue-phase.md. Continue if issue creation fails. 5. Run Phase 4 from references/patch-phase.md. Patch sequentially, one finding at a time. 6. Run Phase 5 from references/organize-phase.md. 7. For interrupted or partial runs, read references/resume-and-artifacts.md and continue from the first incomplete phase.
Non-Negotiable Rules
- Verify findings before creating fixes.
- Use draft PRs for generated patches.
- Branch every patch from the repository default branch.
- Patch findings sequentially; do not run patch workers in parallel.
- Skip existing entries in sweep artifacts instead of duplicating work.
- Record failures in sweep data and continue to the next finding when possible.
- Clean up each worktree after patch success or failure.
Final Response
After organizing, report:
## Sweep Complete
| Metric | Count |
|--------|-------|
| Files scanned | {filesScanned} |
| Findings verified | {verified} |
| PRs created | {prsCreated} |
| Security findings | {securityFindings} |
Full report: `{summaryPath}`Issue Phase
Create a tracking issue that ties all generated PRs together and gives reviewers one overview.
Run
uv run <skill-root>/scripts/create_issue.py ${SWEEP_DIR}Process
1. Parse the JSON stdout. 2. Save issueUrl and issueNumber. 3. If the script fails, show the error and continue to the patch phase. PRs can still be created without a tracking issue. 4. Update the checklist: Phase 3 complete.
Report Template
## Tracking Issue Created
{issueUrl}Organize Phase
Finalize sweep artifacts, security views, PR links, and the summary report.
Run
uv run <skill-root>/scripts/organize.py ${SWEEP_DIR}Process
1. Parse the JSON stdout. 2. Confirm summary.md and data/report.json were produced. 3. If the script fails, show the error and note which phases completed. 4. Update the checklist: Phase 5 complete.
Report Template
## Sweep Complete
| Metric | Count |
|--------|-------|
| Files scanned | {filesScanned} |
| Findings verified | {verified} |
| PRs created | {prsCreated} |
| Security findings | {securityFindings} |
Full report: `{summaryPath}`Patch Phase
Create isolated fixes for verified findings and open draft PRs.
Contents
- Rules
- Setup
- Per-Finding Process
- Dedup Check
- Worktree, Fix, Reviewers, And PR
- Report Template
Rules
- Patch high-severity and above.
- Patch medium findings only when they come from bug-detection skills such as
code-revieworsecurity-review. - Skip low and info findings.
- Process findings sequentially.
- Create one worktree and one branch per finding.
- Clean up worktrees after success or failure.
Setup
Index existing PRs before patching:
uv run <skill-root>/scripts/index_prs.py ${SWEEP_DIR}Parse the JSON stdout and use fileIndex for dedup checks.
Determine the default branch and fetch latest:
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
git fetch origin "${DEFAULT_BRANCH}"Per-Finding Process
For each finding in data/verified.jsonl:
1. If the finding ID already exists in data/patches.jsonl, skip it. 2. Run the dedup check. 3. Create a worktree. 4. Apply the fix using references/patch-prompt.md. 5. If the patch task returns "status": "skipped", record an error, clean up the worktree, and continue. 6. Find reviewers. 7. Push the branch. 8. Create a draft PR. 9. Record the result in data/patches.jsonl. 10. Clean up the worktree.
Dedup Check
Use the file index from index_prs.py:
1. File match: if no open Warden PR touches the finding file, proceed. 2. Chunk overlap: if a PR touches the same file, read data/pr-diffs/<number>.diff and check whether changed hunks overlap or sit within roughly 10 lines of the finding range. 3. Same concern: compare PR title and finding title/description.
Skip the finding only when there is both chunk overlap and the PR addresses the same concern. Record it with "status": "existing" and the matching prUrl.
Worktree, Fix, Reviewers, And PR
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 default branch so PRs contain only the fix commit.
Run patch work using the host agent's task/delegation mechanism when available. Read references/patch-prompt.md and substitute the finding values and worktree path into the ${...} placeholders.
If delegated tasks are not available, apply the prompt instructions yourself in the worktree.
uv run <skill-root>/scripts/find_reviewers.py "${FILE_PATH}"cd "${WORKTREE}" && git push -u origin HEAD:"${BRANCH}"Create the PR with a short "what" summary followed by the finding description and verification reasoning:
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}Record Result
Append to data/patches.jsonl. Use "created" for successful PRs, not the patch task's "applied" status.
{"findingId": "...", "prUrl": "https://...", "branch": "...", "reviewers": ["user1", "user2"], "filesChanged": ["..."], "status": "created|existing|error"}Clean up:
cd "$(git rev-parse --show-toplevel)"
git worktree remove "${WORKTREE}" --forceOn failure at any step, write "status": "error" with an "error" message, clean up the worktree, and continue.
Update the manifest: set phases.patch to "complete".
Report Template
## 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) |Fix 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"
}Resume And Artifacts
Use this reference when resuming a partial sweep or inspecting generated files.
Resume Rules
Each phase is incremental:
1. Check data/manifest.json for phase state. 2. For scan, pass --sweep-dir to scan.py. 3. For verify, skip existing data/verify/<id>.json files. 4. For issue, create_issue.py skips if issueUrl exists in the manifest. 5. For patch, skip existing entries in data/patches.jsonl. 6. For organize, rerun safely.
Continue from the first incomplete phase. Do not start a new sweep unless the user asks for a clean run.
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>.diffFailure Handling
- Preserve partial artifacts.
- Record per-finding errors in the relevant JSONL file.
- Distinguish timed-out files from errored files.
- Clean up worktrees before retrying patch work.
- Re-run organize after manual recovery to refresh reports.
Scan Phase
Run Warden across repository files and collect normalized findings.
Run
uv run <skill-root>/scripts/scan.pyTo scan only specific files:
uv run <skill-root>/scripts/scan.py src/foo.ts src/bar.tsTo resume a partial scan:
uv run <skill-root>/scripts/scan.py --sweep-dir .warden/sweeps/<run-id>Process
1. Parse the JSON stdout. 2. Save runId and sweepDir. 3. Treat exit code 1 as fatal and stop. 4. Treat exit code 2 as partial: report timed-out and errored files separately, then continue only if the user accepts the partial results. 5. Render every finding from the findings array. 6. Update the checklist: Phase 1 complete.
Report Template
## 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 |Bold severity for high and above.
Script Interfaces
Use this reference before running Warden Sweep scripts. Run scripts from the repository root and pass the host skill-root path.
Contents
scan.pyindex_prs.pycreate_issue.pyorganize.pyextract_findings.pygenerate_report.pyfind_reviewers.py
scripts/scan.py
Runs setup and scan in one call: generates a run ID, creates the sweep directory, checks dependencies, creates the warden label, enumerates files, runs Warden per file, writes scan-index.jsonl, and extracts findings.
uv run <skill-root>/scripts/scan.py [file ...]
uv run <skill-root>/scripts/scan.py --sweep-dir .warden/sweeps/<run-id>Stdout JSON:
{
"runId": "abc123",
"sweepDir": ".warden/sweeps/abc123",
"filesScanned": 10,
"filesTimedOut": 0,
"filesErrored": 0,
"totalFindings": 3,
"findings": []
}Exit codes: 0 success, 1 fatal error, 2 partial scan.
scripts/index_prs.py
Fetches open Warden-labeled PRs, builds a file-to-PR dedup index, and caches diffs for overlapping PRs.
uv run <skill-root>/scripts/index_prs.py <sweep-dir>Stdout JSON includes fileIndex. Side effects:
- writes
data/existing-prs.json - writes
data/pr-diffs/<number>.difffor overlapping PRs
scripts/create_issue.py
Creates a GitHub tracking issue summarizing verified sweep results.
uv run <skill-root>/scripts/create_issue.py <sweep-dir>Stdout JSON:
{
"issueUrl": "https://github.com/owner/repo/issues/123",
"issueNumber": 123
}Idempotent: skips creation when issueUrl already exists in the manifest.
scripts/organize.py
Tags security findings, labels security PRs, updates finding reports with PR links, posts final results to the tracking issue, generates the summary report, and finalizes the manifest.
uv run <skill-root>/scripts/organize.py <sweep-dir>Stdout JSON includes final sweep counts and report paths. Side effects:
- creates
security/index.jsonl - copies security finding reports to
security/ - creates or reuses the
securityGitHub label - labels security PRs
- appends PR links to
findings/*.md - writes
summary.mdanddata/report.json - updates
phases.organizeindata/manifest.json
scripts/extract_findings.py
Parses Warden JSONL log files and extracts normalized findings. Usually called by scan.py.
uv run <skill-root>/scripts/extract_findings.py <log-path-or-directory> -o <output.jsonl>Writes one normalized finding per line to <output.jsonl>.
scripts/generate_report.py
Builds summary.md and report.json from sweep data. Usually called by organize.py.
uv run <skill-root>/scripts/generate_report.py <sweep-dir>Side effects:
- writes
<sweep-dir>/summary.md - writes
<sweep-dir>/data/report.json
scripts/find_reviewers.py
Finds the top two git contributors for a file from the last 12 months.
uv run <skill-root>/scripts/find_reviewers.py <file-path>Stdout JSON:
{
"reviewers": ["user1", "user2"]
}Verify Phase
Deep-trace every finding before patching. This phase qualifies true issues and rejects false positives.
Input
Read findings from:
<sweep-dir>/data/all-findings.jsonlProcess
For each finding:
1. If data/verify/<finding-id>.json exists, skip it. 2. Launch verification work using the host agent's task/delegation mechanism when available. Process findings in parallel batches up to 8 if the host supports parallel work. 3. Read references/verify-prompt.md and substitute the finding values into the ${...} placeholders. 4. Parse the returned JSON. 5. Write the raw result to data/verify/<finding-id>.json. 6. Append verified findings to data/verified.jsonl. 7. Append rejected findings to data/rejected.jsonl. 8. For verified findings, generate findings/<finding-id>.md.
If the host does not support delegated tasks, run the same verification prompt serially.
Verified Finding Report
````markdown
${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 the manifest: set phases.verify to "complete".
Report Template
## 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}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()
Warden Sweep Sources
Source Inventory
| Source | Trust tier | Confidence | Usage constraints |
|---|---|---|---|
packages/warden/skills/warden-sweep/SKILL.md | canonical runtime | high | Keep as router and phase overview. |
packages/warden/skills/warden-sweep/references/*.md | bundled runtime references | high | Keep focused by phase or lookup need. |
packages/warden/skills/warden-sweep/scripts/*.py | executable workflow | high | Script interfaces in references must match these files. |
packages/warden/src/cli/output/jsonl.ts | Warden output contract | high | Verify JSONL parsing assumptions when Warden output changes. |
packages/warden/src/output/renderer.ts and packages/warden/src/types/ | finding/report semantics | high | Verify severity, confidence, and finding fields here. |
| GitHub CLI commands used by scripts | external tool contract | medium | Confirm command flags when GitHub CLI behavior changes. |
Coverage Matrix
| Dimension | Coverage status | Evidence |
|---|---|---|
| Workflow phases | covered | SKILL.md routes scan, verify, issue, patch, organize, and resume behavior to focused references. |
| Script interfaces | covered | references/script-interfaces.md lists scripts, arguments, outputs, and side effects. |
| Artifact schema | covered | references/resume-and-artifacts.md documents directories and key JSONL/JSON files. |
| Verification behavior | covered | references/verify-phase.md and references/verify-prompt.md define qualification and rejection behavior. |
| Patch behavior | covered | references/patch-phase.md and references/patch-prompt.md define triage, worktree isolation, draft PR creation, and cleanup. |
| Known issues/workarounds | partial | Resume, partial scans, skipped findings, and existing PR dedup are covered; CI follow-up and rate-limit recovery are not. |
| Version/migration variance | partial | Current artifact names and script interfaces are documented; no formal migration path exists for old sweep directories. |
Decisions
- Split phase detail out of
SKILL.mdso agents load only the current phase instructions. - Keep script interface documentation separate from phase runbooks because scripts are reused across phases and by resume workflows.
- Describe verification and patch work in host-neutral terms while allowing parallel agent tasks when the host supports them.
- Keep prompt templates as separate references because they are substituted into delegated verification and patch work.
- Keep generated sweep artifacts under
.warden/sweeps/<run-id>/so runs are resumable and isolated from normal source files.
Open Gaps
- Add a redacted fixture sweep to validate the full workflow without touching real GitHub repositories.
- Document rate-limit and permission failure recovery if these become common in real sweeps.
- Add migration notes if artifact schemas change after users have existing sweep directories.
- Consider adding a script-level dry-run mode for issue and PR creation.
Changelog
- 2026-04-27: Reverse-engineered maintenance spec and split the distributed
warden-sweepworkflow into phase references.
Warden Sweep Skill Specification
Intent
The warden-sweep skill runs a full-repository Warden scan, verifies findings through deeper code tracing, and creates draft PRs for validated issues.
It exists for batch remediation work where a normal targeted Warden run is too narrow. The workflow is intentionally conservative: scan broadly, verify before patching, deduplicate against existing PRs, and record every decision in sweep artifacts.
Scope
In scope:
- Scanning a repository file-by-file with Warden.
- Extracting and normalizing Warden findings.
- Verifying findings before any code changes are attempted.
- Creating a tracking issue for the sweep.
- Creating one draft PR per validated issue that passes patch triage.
- Organizing reports, security findings, PR links, and resumable sweep state.
Out of scope:
- Replacing human review of generated PRs.
- Applying fixes directly to the user's current branch.
- Patching low-confidence or unverified findings.
- Running generic codebase review without Warden scan artifacts.
- Managing CI iteration after PR creation.
Users And Trigger Context
- Primary users: maintainers asking an agent to perform broad Warden-backed repository cleanup.
- Common user requests: "sweep the repo", "scan everything", "find all bugs", "full codebase review", "batch code analysis", "run Warden across the whole repository".
- Should not trigger for: normal pre-commit Warden runs, single-file checks, generic code review, or PR feedback iteration.
Runtime Contract
- Required first actions:
- Confirm the repository has the required tools:
warden,gh,git,jq, anduv. - Run
scripts/scan.pyfrom the repository root using the host skill-root path. - Preserve the returned
runIdandsweepDir. - Resume existing sweep artifacts instead of duplicating work when a sweep directory is provided.
- Required outputs:
- Phase summaries after scan, verification, issue creation, patching, and organization.
- Final pointer to the generated summary report.
- Explicit counts for scanned files, timeouts/errors, verified/rejected findings, created/existing/failed PRs, and security findings.
- Non-negotiable constraints:
- Verify findings before patching.
- Patch findings sequentially to avoid worktree and branch cross-contamination.
- Create draft PRs, not direct commits to the default branch.
- Record errors in sweep data and continue to the next finding when possible.
- Clean up worktrees after patch attempts.
- Expected bundled files loaded at runtime:
references/script-interfaces.mdreferences/scan-phase.mdreferences/verify-phase.mdreferences/issue-phase.mdreferences/patch-phase.mdreferences/organize-phase.mdreferences/resume-and-artifacts.mdreferences/verify-prompt.mdreferences/patch-prompt.mdscripts/*.py
Source And Evidence Model
Authoritative sources:
skills/warden-sweep/SKILL.mdand bundled references.skills/warden-sweep/scripts/*.py.- Warden JSONL output schema and renderer code in
packages/warden/src/cli/output/. - GitHub CLI behavior for PRs, issues, labels, and repo metadata.
Useful improvement sources:
- positive examples: completed sweeps with verified findings, clean draft PRs, and accurate final reports
- negative examples: duplicate PRs, false positive patches, failed worktree cleanup, incorrect artifact state, or patch contamination across findings
- commit logs/changelogs: changes to Warden output, script behavior, or sweep artifact schema
- issue or PR feedback: reviewer complaints about generated PR quality, false positives, or sweep noise
- eval results: dry-run prompts for scan resume, verification, patch triage, and final organization
Data that must not be stored:
- secrets, credentials, or tokens
- private customer data
- raw issue/PR content unrelated to the sweep finding
- unredacted sensitive code excerpts beyond what is needed in local sweep artifacts
Reference Architecture
SKILL.mdcontains the phase overview, routing table, universal constraints, and completion contract.SOURCES.mdcontains source inventory, coverage, decisions, gaps, and changelog.references/contains focused phase runbooks, prompt templates, script interfaces, and artifact layout.references/evidence/is unused until durable examples are needed.scripts/contains repeatable automation for scan, extraction, issue creation, PR indexing, reviewer selection, report generation, and organization.assets/is unused.
Evaluation
- Lightweight validation:
- Run the skill validator against
skills/warden-sweep. - Confirm every script mentioned in
SKILL.mdand references exists. - Confirm every phase reference has one clear lookup purpose.
- Deeper evaluation:
- Run a dry sweep in a small fixture repository when script or artifact behavior changes.
- Exercise resume paths for scan, verify, issue, patch, and organize phases.
- Holdout examples:
- Store redacted false positive and duplicate-PR examples in
references/evidence/if these failures recur. - Acceptance gates:
- Findings are verified before patching.
- Patch phase creates isolated branches and draft PRs.
- Existing overlapping PRs are detected before creating new PRs.
- Final artifacts are resumable and summarize errors separately from successful work.
Known Limitations
- The workflow depends on external CLIs and repository permissions.
- Verification and patching quality depends on the host agent's ability to inspect code deeply.
- The skill uses host-agent delegation when available; hosts without parallel delegation can run the same verification steps serially.
- Broad scans can be expensive and noisy if repository Warden configuration is too broad.
Maintenance Notes
- Update
SKILL.mdwhen phase order, universal constraints, or routing changes. - Update
SOURCES.mdwhen source inventory, decisions, coverage, or known gaps change. - Update phase references when script arguments, output shapes, artifact schema, or error handling changes.
- Update prompt templates when verification or patch quality failures recur.
- Update
references/evidence/when preserving redacted examples will improve future iterations.
Related skills
FAQ
Does every finding become a PR?
Only validated findings after deep tracing verification get draft PRs.
What does deep tracing do?
Verifies findings through code path analysis to reduce false positives.
How much of the repo is scanned?
Every file in the repository is included in the sweep.