@sentry/warden 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents.lock +7 -0
- package/dist/cli/args.d.ts +14 -12
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +44 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/init.d.ts +0 -3
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +206 -19
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/logs.d.ts +19 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/logs.js +419 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +54 -21
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/output/formatters.d.ts +2 -1
- package/dist/cli/output/formatters.d.ts.map +1 -1
- package/dist/cli/output/formatters.js +22 -19
- package/dist/cli/output/formatters.js.map +1 -1
- package/dist/cli/output/index.d.ts +1 -1
- package/dist/cli/output/index.d.ts.map +1 -1
- package/dist/cli/output/index.js +1 -1
- package/dist/cli/output/index.js.map +1 -1
- package/dist/cli/output/ink-runner.js +1 -1
- package/dist/cli/output/ink-runner.js.map +1 -1
- package/dist/cli/output/jsonl.d.ts +49 -13
- package/dist/cli/output/jsonl.d.ts.map +1 -1
- package/dist/cli/output/jsonl.js +137 -4
- package/dist/cli/output/jsonl.js.map +1 -1
- package/dist/cli/output/tasks.d.ts.map +1 -1
- package/dist/cli/output/tasks.js +1 -22
- package/dist/cli/output/tasks.js.map +1 -1
- package/dist/cli/terminal.d.ts.map +1 -1
- package/dist/cli/terminal.js +0 -2
- package/dist/cli/terminal.js.map +1 -1
- package/dist/config/schema.d.ts +49 -98
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +0 -12
- package/dist/config/schema.js.map +1 -1
- package/dist/evals/runner.d.ts.map +1 -1
- package/dist/evals/runner.js +0 -1
- package/dist/evals/runner.js.map +1 -1
- package/dist/evals/types.d.ts +9 -15
- package/dist/evals/types.d.ts.map +1 -1
- package/dist/output/github-checks.d.ts +1 -1
- package/dist/output/github-checks.d.ts.map +1 -1
- package/dist/output/github-checks.js +2 -6
- package/dist/output/github-checks.js.map +1 -1
- package/dist/output/issue-renderer.js +1 -1
- package/dist/output/issue-renderer.js.map +1 -1
- package/dist/sdk/analyze.d.ts.map +1 -1
- package/dist/sdk/analyze.js +13 -26
- package/dist/sdk/analyze.js.map +1 -1
- package/dist/sdk/auth.d.ts +16 -0
- package/dist/sdk/auth.d.ts.map +1 -0
- package/dist/sdk/auth.js +37 -0
- package/dist/sdk/auth.js.map +1 -0
- package/dist/sdk/errors.d.ts +5 -0
- package/dist/sdk/errors.d.ts.map +1 -1
- package/dist/sdk/errors.js +20 -0
- package/dist/sdk/errors.js.map +1 -1
- package/dist/sdk/prompt.js +1 -1
- package/dist/sdk/runner.d.ts +2 -1
- package/dist/sdk/runner.d.ts.map +1 -1
- package/dist/sdk/runner.js +3 -1
- package/dist/sdk/runner.js.map +1 -1
- package/dist/sdk/types.d.ts +0 -3
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/types.js.map +1 -1
- package/dist/types/index.d.ts +23 -24
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +19 -7
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/warden/SKILL.md +76 -0
- package/skills/warden/references/cli-reference.md +142 -0
- package/skills/warden/references/config-schema.md +111 -0
- package/skills/warden/references/configuration.md +110 -0
- package/skills/warden/references/creating-skills.md +84 -0
- package/skills/warden-sweep/SKILL.md +407 -0
- package/skills/warden-sweep/scripts/_utils.py +37 -0
- package/skills/warden-sweep/scripts/extract_findings.py +219 -0
- package/skills/warden-sweep/scripts/find_reviewers.py +115 -0
- package/skills/warden-sweep/scripts/generate_report.py +271 -0
- package/skills/warden-sweep/scripts/index_prs.py +187 -0
- package/skills/warden-sweep/scripts/organize.py +315 -0
- package/skills/warden-sweep/scripts/scan.py +632 -0
- package/dist/sdk/session.d.ts +0 -43
- package/dist/sdk/session.d.ts.map +0 -1
- package/dist/sdk/session.js +0 -105
- package/dist/sdk/session.js.map +0 -1
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: warden-sweep
|
|
3
|
+
description: Full-repository code sweep. Scans every file with warden, verifies findings via deep tracing, creates draft PRs for validated issues. Use when asked to "sweep the repo", "scan everything", "find all bugs", "full codebase review", "batch code analysis", or run warden across the entire repository.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Warden Sweep
|
|
7
|
+
|
|
8
|
+
Full-repository code sweep: scan every file, verify findings with deep tracing, create draft PRs for validated issues.
|
|
9
|
+
|
|
10
|
+
**Requires**: `warden`, `gh`, `git`, `jq`, `uv`
|
|
11
|
+
|
|
12
|
+
**Important**: Run all scripts from the repository root using `${CLAUDE_SKILL_ROOT}`. Output goes to `.warden/sweeps/<run-id>/`.
|
|
13
|
+
|
|
14
|
+
## Bundled Scripts
|
|
15
|
+
|
|
16
|
+
### `scripts/scan.py`
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/scan.py [file ...]
|
|
22
|
+
--sweep-dir DIR # Resume into existing sweep dir
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### `scripts/index_prs.py`
|
|
26
|
+
|
|
27
|
+
Fetches open warden-labeled PRs, builds file-to-PR dedup index, caches diffs for overlapping PRs.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/index_prs.py <sweep-dir>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `scripts/organize.py`
|
|
34
|
+
|
|
35
|
+
Tags security findings, labels security PRs, updates finding reports with PR links, generates summary report, finalizes manifest.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/organize.py <sweep-dir>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `scripts/extract_findings.py`
|
|
42
|
+
|
|
43
|
+
Parses warden JSONL log files and extracts normalized findings. Called automatically by `scan.py`.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/extract_findings.py <log-path-or-directory> -o <output.jsonl>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `scripts/generate_report.py`
|
|
50
|
+
|
|
51
|
+
Builds `summary.md` and `report.json` from sweep data. Called automatically by `organize.py`.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/generate_report.py <sweep-dir>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `scripts/find_reviewers.py`
|
|
58
|
+
|
|
59
|
+
Finds top 2 git contributors for a file (last 12 months).
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/find_reviewers.py <file-path>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Returns JSON: `{"reviewers": ["user1", "user2"]}`
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Phase 1: Scan
|
|
70
|
+
|
|
71
|
+
**Run** (1 tool call):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/scan.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
To resume a partial scan:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/scan.py --sweep-dir .warden/sweeps/<run-id>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Parse the JSON stdout. Save `runId` and `sweepDir` for subsequent phases.
|
|
84
|
+
|
|
85
|
+
**Report** to user:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
## Scan Complete
|
|
89
|
+
|
|
90
|
+
Scanned **{filesScanned}** files, **{filesErrored}** errors.
|
|
91
|
+
|
|
92
|
+
### Findings ({totalFindings} total)
|
|
93
|
+
|
|
94
|
+
| # | Severity | Skill | File | Title |
|
|
95
|
+
|---|----------|-------|------|-------|
|
|
96
|
+
| 1 | **HIGH** | security-review | `src/db/query.ts:42` | SQL injection in query builder |
|
|
97
|
+
...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Render every finding from the `findings` array. Bold severity for high and above.
|
|
101
|
+
|
|
102
|
+
**On failure**: If exit code 1, show the error JSON and stop. If exit code 2, show the partial results and note which files errored.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Phase 2: Verify
|
|
107
|
+
|
|
108
|
+
Deep-trace each finding using Task subagents to qualify or disqualify.
|
|
109
|
+
|
|
110
|
+
**For each finding in `data/all-findings.jsonl`:**
|
|
111
|
+
|
|
112
|
+
Check if `data/verify/<finding-id>.json` already exists (incrementality). If it does, skip.
|
|
113
|
+
|
|
114
|
+
Launch a Task subagent (`subagent_type: "general-purpose"`) for each finding. Process findings sequentially (one at a time) to keep output organized.
|
|
115
|
+
|
|
116
|
+
**Task prompt for each finding:**
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
Verify a code analysis finding. Determine if this is a TRUE issue or a FALSE POSITIVE.
|
|
120
|
+
Do NOT write or edit any files. Research only.
|
|
121
|
+
|
|
122
|
+
## Finding
|
|
123
|
+
- Title: ${TITLE}
|
|
124
|
+
- Severity: ${SEVERITY} | Confidence: ${CONFIDENCE}
|
|
125
|
+
- Skill: ${SKILL}
|
|
126
|
+
- Location: ${FILE_PATH}:${START_LINE}-${END_LINE}
|
|
127
|
+
- Description: ${DESCRIPTION}
|
|
128
|
+
- Verification hint: ${VERIFICATION}
|
|
129
|
+
|
|
130
|
+
## Instructions
|
|
131
|
+
1. Read the file at the reported location. Examine at least 50 lines of surrounding context.
|
|
132
|
+
2. Trace data flow to/from the flagged code using Grep/Glob.
|
|
133
|
+
3. Check if the issue is mitigated elsewhere (guards, validation, try/catch upstream).
|
|
134
|
+
4. Check if the issue is actually reachable in practice.
|
|
135
|
+
|
|
136
|
+
Return your verdict as JSON:
|
|
137
|
+
{
|
|
138
|
+
"findingId": "${FINDING_ID}",
|
|
139
|
+
"verdict": "verified" or "rejected",
|
|
140
|
+
"confidence": "high" or "medium" or "low",
|
|
141
|
+
"reasoning": "2-3 sentence explanation",
|
|
142
|
+
"traceNotes": "What code paths you examined"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Process results:**
|
|
147
|
+
|
|
148
|
+
Parse the JSON from the subagent response and:
|
|
149
|
+
- Write result to `data/verify/<finding-id>.json`
|
|
150
|
+
- Append to `data/verified.jsonl` or `data/rejected.jsonl`
|
|
151
|
+
- For verified findings, generate `findings/<finding-id>.md`:
|
|
152
|
+
|
|
153
|
+
```markdown
|
|
154
|
+
# ${TITLE}
|
|
155
|
+
|
|
156
|
+
**ID**: ${FINDING_ID} | **Severity**: ${SEVERITY} | **Confidence**: ${CONFIDENCE}
|
|
157
|
+
**Skill**: ${SKILL} | **File**: ${FILE_PATH}:${START_LINE}
|
|
158
|
+
|
|
159
|
+
## Description
|
|
160
|
+
${DESCRIPTION}
|
|
161
|
+
|
|
162
|
+
## Verification
|
|
163
|
+
**Verdict**: Verified (${VERIFICATION_CONFIDENCE})
|
|
164
|
+
**Reasoning**: ${REASONING}
|
|
165
|
+
**Code trace**: ${TRACE_NOTES}
|
|
166
|
+
|
|
167
|
+
## Suggested Fix
|
|
168
|
+
${FIX_DESCRIPTION}
|
|
169
|
+
```diff
|
|
170
|
+
${FIX_DIFF}
|
|
171
|
+
```
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Update manifest: set `phases.verify` to `"complete"`.
|
|
175
|
+
|
|
176
|
+
**Report** to user after all verifications:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
## Verification Complete
|
|
180
|
+
|
|
181
|
+
**{verified}** verified, **{rejected}** rejected.
|
|
182
|
+
|
|
183
|
+
### Verified Findings
|
|
184
|
+
|
|
185
|
+
| # | Severity | Confidence | File | Title | Reasoning |
|
|
186
|
+
|---|----------|------------|------|-------|-----------|
|
|
187
|
+
| 1 | **HIGH** | high | `src/db/query.ts:42` | SQL injection in query builder | User input flows directly into... |
|
|
188
|
+
...
|
|
189
|
+
|
|
190
|
+
### Rejected ({rejected_count})
|
|
191
|
+
|
|
192
|
+
- `{findingId}` {file}: {reasoning}
|
|
193
|
+
...
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Phase 3: Patch
|
|
199
|
+
|
|
200
|
+
For each verified finding, create a worktree, fix the code, and open a draft PR.
|
|
201
|
+
|
|
202
|
+
**Step 0: Index existing PRs** (1 tool call):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/index_prs.py ${SWEEP_DIR}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Parse the JSON stdout. Use `fileIndex` for dedup checks.
|
|
209
|
+
|
|
210
|
+
**For each finding in `data/verified.jsonl`:**
|
|
211
|
+
|
|
212
|
+
Check if finding ID already exists in `data/patches.jsonl` (incrementality). If it does, skip.
|
|
213
|
+
|
|
214
|
+
**Dedup check**: Use the file index from `index_prs.py` output to determine if an existing open PR already addresses the same issue.
|
|
215
|
+
|
|
216
|
+
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.
|
|
217
|
+
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.
|
|
218
|
+
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.
|
|
219
|
+
|
|
220
|
+
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.
|
|
221
|
+
|
|
222
|
+
**Step 1: Create worktree**
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
BRANCH="warden-sweep/${RUN_ID}/${FINDING_ID}"
|
|
226
|
+
WORKTREE="${SWEEP_DIR}/worktrees/${FINDING_ID}"
|
|
227
|
+
git worktree add "${WORKTREE}" -b "${BRANCH}"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Each finding branches from the current HEAD to avoid merge conflicts between PRs.
|
|
231
|
+
|
|
232
|
+
**Step 2: Generate fix**
|
|
233
|
+
|
|
234
|
+
Launch a Task subagent (`subagent_type: "general-purpose"`) to apply the fix in the worktree:
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
Fix a verified code issue and add test coverage. You are working in a git worktree at: ${WORKTREE}
|
|
238
|
+
|
|
239
|
+
## Finding
|
|
240
|
+
- Title: ${TITLE}
|
|
241
|
+
- File: ${FILE_PATH}:${START_LINE}
|
|
242
|
+
- Description: ${DESCRIPTION}
|
|
243
|
+
- Verification: ${REASONING}
|
|
244
|
+
- Suggested Fix: ${FIX_DESCRIPTION}
|
|
245
|
+
```diff
|
|
246
|
+
${FIX_DIFF}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Instructions
|
|
250
|
+
1. Read the file at the reported location (use the worktree path: ${WORKTREE}/${FILE_PATH}).
|
|
251
|
+
2. Apply the suggested fix. If the diff doesn't apply cleanly, adapt it while preserving intent.
|
|
252
|
+
3. Write or update tests that verify the fix:
|
|
253
|
+
- Follow existing test patterns (co-located files, same framework)
|
|
254
|
+
- At minimum, write a test that would have caught the original bug
|
|
255
|
+
4. Only modify the fix target and its test file.
|
|
256
|
+
5. Do NOT run tests locally. CI will validate the changes.
|
|
257
|
+
6. Stage and commit with this exact message:
|
|
258
|
+
|
|
259
|
+
fix: ${TITLE}
|
|
260
|
+
|
|
261
|
+
Warden finding ${FINDING_ID}
|
|
262
|
+
Severity: ${SEVERITY}
|
|
263
|
+
|
|
264
|
+
Co-Authored-By: Warden <noreply@getsentry.com>
|
|
265
|
+
|
|
266
|
+
Report what you changed: files modified, test files added/updated, any notes.
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Step 3: Find reviewers**
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/find_reviewers.py "${FILE_PATH}"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Step 4: Create draft PR**
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
cd "${WORKTREE}"
|
|
279
|
+
git push -u origin "${BRANCH}"
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Create the PR with a 1-2 sentence "What" summary based on the finding and fix, followed by the finding description and verification reasoning:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
REVIEWERS=""
|
|
286
|
+
# If find_reviewers.py returned reviewers, build the flags
|
|
287
|
+
# e.g., REVIEWERS="--reviewer user1 --reviewer user2"
|
|
288
|
+
|
|
289
|
+
gh pr create --draft \
|
|
290
|
+
--label "warden" \
|
|
291
|
+
--title "fix: ${TITLE}" \
|
|
292
|
+
--body "$(cat <<'EOF'
|
|
293
|
+
${FIX_WHAT_DESCRIPTION}
|
|
294
|
+
|
|
295
|
+
${DESCRIPTION}
|
|
296
|
+
|
|
297
|
+
${REASONING}
|
|
298
|
+
|
|
299
|
+
Automated fix for Warden finding ${FINDING_ID} (${SEVERITY}, detected by ${SKILL}).
|
|
300
|
+
|
|
301
|
+
> This PR was auto-generated by a Warden Sweep (run ${RUN_ID}).
|
|
302
|
+
> The finding has been validated through automated deep tracing,
|
|
303
|
+
> but human confirmation is requested as this is batch work.
|
|
304
|
+
EOF
|
|
305
|
+
)" ${REVIEWERS}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Save the PR URL.
|
|
309
|
+
|
|
310
|
+
**Step 5: Record and cleanup**
|
|
311
|
+
|
|
312
|
+
Append to `data/patches.jsonl`:
|
|
313
|
+
```json
|
|
314
|
+
{"findingId": "...", "prUrl": "https://...", "branch": "...", "reviewers": ["user1", "user2"], "filesChanged": ["..."], "status": "created|existing|error"}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Remove the worktree:
|
|
318
|
+
```bash
|
|
319
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
320
|
+
git worktree remove "${WORKTREE}" --force
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Error 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.
|
|
324
|
+
|
|
325
|
+
Update manifest: set `phases.patch` to `"complete"`.
|
|
326
|
+
|
|
327
|
+
**Report** to user after all patches:
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
## PRs Created
|
|
331
|
+
|
|
332
|
+
**{created}** created, **{skipped}** skipped (existing), **{failed}** failed.
|
|
333
|
+
|
|
334
|
+
| # | Finding | PR | Status |
|
|
335
|
+
|---|---------|-----|--------|
|
|
336
|
+
| 1 | `security-review-a1b2c3d4` SQL injection in query builder | #142 | created |
|
|
337
|
+
| 2 | `code-review-e5f6g7h8` Null pointer in handler | - | existing (#138) |
|
|
338
|
+
...
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Phase 4: Organize
|
|
344
|
+
|
|
345
|
+
**Run** (1 tool call):
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
uv run ${CLAUDE_SKILL_ROOT}/scripts/organize.py ${SWEEP_DIR}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Parse the JSON stdout.
|
|
352
|
+
|
|
353
|
+
**Report** to user:
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
## Sweep Complete
|
|
357
|
+
|
|
358
|
+
| Metric | Count |
|
|
359
|
+
|--------|-------|
|
|
360
|
+
| Files scanned | {filesScanned} |
|
|
361
|
+
| Findings verified | {verified} |
|
|
362
|
+
| PRs created | {prsCreated} |
|
|
363
|
+
| Security findings | {securityFindings} |
|
|
364
|
+
|
|
365
|
+
Full report: `{summaryPath}`
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**On failure**: Show the error and note which steps completed.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Resuming a Sweep
|
|
373
|
+
|
|
374
|
+
Each phase is incremental. To resume from where you left off:
|
|
375
|
+
|
|
376
|
+
1. Check `data/manifest.json` to see which phases are complete
|
|
377
|
+
2. For scan: pass `--sweep-dir` to `scan.py`
|
|
378
|
+
3. For verify: existing `data/verify/<id>.json` files are skipped
|
|
379
|
+
4. For patch: existing entries in `data/patches.jsonl` are skipped
|
|
380
|
+
5. For organize: safe to re-run (idempotent)
|
|
381
|
+
|
|
382
|
+
## Output Directory Structure
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
.warden/sweeps/<run-id>/
|
|
386
|
+
summary.md # Stats, key findings, PR links
|
|
387
|
+
findings/ # One markdown per verified finding
|
|
388
|
+
<finding-id>.md
|
|
389
|
+
security/ # Security-specific view
|
|
390
|
+
index.jsonl # Security findings index
|
|
391
|
+
<finding-id>.md # Copies of security findings
|
|
392
|
+
data/ # Structured data for tooling
|
|
393
|
+
manifest.json # Run metadata, phase state
|
|
394
|
+
scan-index.jsonl # Per-file scan tracking
|
|
395
|
+
all-findings.jsonl # Every finding from scan
|
|
396
|
+
verified.jsonl # Findings that passed verification
|
|
397
|
+
rejected.jsonl # Findings that failed verification
|
|
398
|
+
patches.jsonl # Finding -> PR URL -> reviewers
|
|
399
|
+
existing-prs.json # Cached open warden PRs
|
|
400
|
+
report.json # Machine-readable summary
|
|
401
|
+
verify/ # Individual verification results
|
|
402
|
+
<finding-id>.json
|
|
403
|
+
logs/ # Warden JSONL logs per file
|
|
404
|
+
<hash>.jsonl
|
|
405
|
+
pr-diffs/ # Cached PR diffs for dedup
|
|
406
|
+
<number>.diff
|
|
407
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Shared utilities for warden-sweep scripts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_cmd(
|
|
11
|
+
args: list[str], timeout: int = 30, cwd: str | None = None
|
|
12
|
+
) -> subprocess.CompletedProcess[str]:
|
|
13
|
+
"""Run a command and return the result."""
|
|
14
|
+
return subprocess.run(
|
|
15
|
+
args,
|
|
16
|
+
capture_output=True,
|
|
17
|
+
text=True,
|
|
18
|
+
timeout=timeout,
|
|
19
|
+
cwd=cwd,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def read_jsonl(path: str) -> list[dict[str, Any]]:
|
|
24
|
+
"""Read a JSONL file and return list of parsed objects."""
|
|
25
|
+
entries: list[dict[str, Any]] = []
|
|
26
|
+
if not os.path.exists(path):
|
|
27
|
+
return entries
|
|
28
|
+
with open(path) as f:
|
|
29
|
+
for line in f:
|
|
30
|
+
line = line.strip()
|
|
31
|
+
if not line:
|
|
32
|
+
continue
|
|
33
|
+
try:
|
|
34
|
+
entries.append(json.loads(line))
|
|
35
|
+
except json.JSONDecodeError:
|
|
36
|
+
continue
|
|
37
|
+
return entries
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.9"
|
|
4
|
+
# ///
|
|
5
|
+
"""
|
|
6
|
+
Extract individual findings from warden JSONL log files.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python extract_findings.py <log-path-or-directory> -o <output.jsonl>
|
|
10
|
+
python extract_findings.py .warden/logs/ --scan-index data/scan-index.jsonl -o findings.jsonl
|
|
11
|
+
|
|
12
|
+
Reads warden JSONL logs (one skill record per line, summary as last line),
|
|
13
|
+
extracts each finding as a standalone record with a stable ID, and writes
|
|
14
|
+
one finding per line to the output file.
|
|
15
|
+
|
|
16
|
+
Finding ID format: <skill>-<sha256(title+path+line)[:8]>
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def generate_finding_id(skill: str, title: str, path: str, line: int | None) -> str:
|
|
30
|
+
"""Generate a stable, deterministic finding ID."""
|
|
31
|
+
raw = f"{title}:{path}:{line or 0}"
|
|
32
|
+
digest = hashlib.sha256(raw.encode()).hexdigest()[:8]
|
|
33
|
+
# Sanitize skill name for use in ID
|
|
34
|
+
safe_skill = skill.replace("/", "-").replace(" ", "-").lower()
|
|
35
|
+
return f"{safe_skill}-{digest}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_jsonl_log(log_path: str) -> list[dict[str, Any]]:
|
|
39
|
+
"""Parse a warden JSONL log file and extract individual findings.
|
|
40
|
+
|
|
41
|
+
Each non-summary line has the shape:
|
|
42
|
+
{
|
|
43
|
+
"run": {...},
|
|
44
|
+
"skill": "...",
|
|
45
|
+
"findings": [{...}, ...],
|
|
46
|
+
...
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
The last line is a summary record with "type": "summary" which we skip.
|
|
50
|
+
"""
|
|
51
|
+
findings = []
|
|
52
|
+
try:
|
|
53
|
+
with open(log_path) as f:
|
|
54
|
+
for line in f:
|
|
55
|
+
line = line.strip()
|
|
56
|
+
if not line:
|
|
57
|
+
continue
|
|
58
|
+
try:
|
|
59
|
+
record = json.loads(line)
|
|
60
|
+
except json.JSONDecodeError:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# Skip summary records
|
|
64
|
+
if record.get("type") == "summary":
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
skill = record.get("skill", "unknown")
|
|
68
|
+
run_meta = record.get("run", {})
|
|
69
|
+
record_findings = record.get("findings", [])
|
|
70
|
+
|
|
71
|
+
for finding in record_findings:
|
|
72
|
+
location = finding.get("location", {})
|
|
73
|
+
file_path = location.get("path", "")
|
|
74
|
+
start_line = location.get("startLine")
|
|
75
|
+
end_line = location.get("endLine")
|
|
76
|
+
|
|
77
|
+
finding_id = generate_finding_id(
|
|
78
|
+
skill=skill,
|
|
79
|
+
title=finding.get("title", ""),
|
|
80
|
+
path=file_path,
|
|
81
|
+
line=start_line,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
normalized = {
|
|
85
|
+
"findingId": finding_id,
|
|
86
|
+
"file": file_path,
|
|
87
|
+
"skill": skill,
|
|
88
|
+
"severity": finding.get("severity", "info"),
|
|
89
|
+
"confidence": finding.get("confidence"),
|
|
90
|
+
"title": finding.get("title", ""),
|
|
91
|
+
"description": finding.get("description", ""),
|
|
92
|
+
"verification": finding.get("verification"),
|
|
93
|
+
"location": {
|
|
94
|
+
"path": file_path,
|
|
95
|
+
"startLine": start_line,
|
|
96
|
+
"endLine": end_line,
|
|
97
|
+
},
|
|
98
|
+
"suggestedFix": finding.get("suggestedFix"),
|
|
99
|
+
"logPath": log_path,
|
|
100
|
+
"runId": run_meta.get("runId", ""),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
findings.append(normalized)
|
|
104
|
+
|
|
105
|
+
except (OSError, IOError) as e:
|
|
106
|
+
print(f"Error reading {log_path}: {e}", file=sys.stderr)
|
|
107
|
+
|
|
108
|
+
return findings
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def collect_log_paths(source: str, scan_index: str | None = None) -> list[str]:
|
|
112
|
+
"""Collect log file paths from a directory or scan index."""
|
|
113
|
+
paths: list[str] = []
|
|
114
|
+
|
|
115
|
+
if scan_index and os.path.exists(scan_index):
|
|
116
|
+
# Read log paths from scan-index.jsonl
|
|
117
|
+
seen = set()
|
|
118
|
+
total_entries = 0
|
|
119
|
+
missing = 0
|
|
120
|
+
with open(scan_index) as f:
|
|
121
|
+
for line in f:
|
|
122
|
+
line = line.strip()
|
|
123
|
+
if not line:
|
|
124
|
+
continue
|
|
125
|
+
try:
|
|
126
|
+
entry = json.loads(line)
|
|
127
|
+
except json.JSONDecodeError:
|
|
128
|
+
continue
|
|
129
|
+
if entry.get("status") != "complete":
|
|
130
|
+
continue
|
|
131
|
+
total_entries += 1
|
|
132
|
+
log_path = entry.get("logPath", "")
|
|
133
|
+
if log_path and log_path not in seen:
|
|
134
|
+
seen.add(log_path)
|
|
135
|
+
if os.path.isfile(log_path):
|
|
136
|
+
paths.append(log_path)
|
|
137
|
+
else:
|
|
138
|
+
missing += 1
|
|
139
|
+
if missing > 0:
|
|
140
|
+
print(
|
|
141
|
+
f"Warning: {missing} log path(s) from scan-index not found on disk",
|
|
142
|
+
file=sys.stderr,
|
|
143
|
+
)
|
|
144
|
+
# Only use scan-index results if we actually found logs;
|
|
145
|
+
# fall through to source directory otherwise
|
|
146
|
+
if paths:
|
|
147
|
+
return paths
|
|
148
|
+
if total_entries > 0:
|
|
149
|
+
print(
|
|
150
|
+
"Warning: scan-index had entries but no valid log paths; "
|
|
151
|
+
"falling back to source directory",
|
|
152
|
+
file=sys.stderr,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
source_path = Path(source)
|
|
156
|
+
if source_path.is_file():
|
|
157
|
+
return [str(source_path)]
|
|
158
|
+
|
|
159
|
+
if source_path.is_dir():
|
|
160
|
+
for f in sorted(source_path.glob("*.jsonl")):
|
|
161
|
+
paths.append(str(f))
|
|
162
|
+
return paths
|
|
163
|
+
|
|
164
|
+
print(f"Source not found: {source}", file=sys.stderr)
|
|
165
|
+
return paths
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def main():
|
|
169
|
+
parser = argparse.ArgumentParser(
|
|
170
|
+
description="Extract findings from warden JSONL logs"
|
|
171
|
+
)
|
|
172
|
+
parser.add_argument(
|
|
173
|
+
"source",
|
|
174
|
+
help="Path to a JSONL log file or directory of log files",
|
|
175
|
+
)
|
|
176
|
+
parser.add_argument(
|
|
177
|
+
"-o", "--output",
|
|
178
|
+
required=True,
|
|
179
|
+
help="Output path for normalized findings JSONL",
|
|
180
|
+
)
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"--scan-index",
|
|
183
|
+
help="Path to scan-index.jsonl (uses log paths from completed scans)",
|
|
184
|
+
)
|
|
185
|
+
args = parser.parse_args()
|
|
186
|
+
|
|
187
|
+
log_paths = collect_log_paths(args.source, args.scan_index)
|
|
188
|
+
if not log_paths:
|
|
189
|
+
print("No log files found.", file=sys.stderr)
|
|
190
|
+
sys.exit(1)
|
|
191
|
+
|
|
192
|
+
all_findings: list[dict[str, Any]] = []
|
|
193
|
+
seen_ids: set[str] = set()
|
|
194
|
+
|
|
195
|
+
for log_path in log_paths:
|
|
196
|
+
findings = parse_jsonl_log(log_path)
|
|
197
|
+
for f in findings:
|
|
198
|
+
fid = f["findingId"]
|
|
199
|
+
if fid not in seen_ids:
|
|
200
|
+
seen_ids.add(fid)
|
|
201
|
+
all_findings.append(f)
|
|
202
|
+
|
|
203
|
+
# Write output
|
|
204
|
+
os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
|
|
205
|
+
with open(args.output, "w") as out:
|
|
206
|
+
for finding in all_findings:
|
|
207
|
+
out.write(json.dumps(finding) + "\n")
|
|
208
|
+
|
|
209
|
+
print(
|
|
210
|
+
json.dumps({
|
|
211
|
+
"logsProcessed": len(log_paths),
|
|
212
|
+
"findingsExtracted": len(all_findings),
|
|
213
|
+
"outputPath": args.output,
|
|
214
|
+
})
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
main()
|