@mechanai/deepreview 2.2.2 → 2.3.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/.opencode/agents/deepreview-applier.md +5 -0
- package/.opencode/agents/deepreview-plan-validator.md +91 -0
- package/.opencode/agents/deepreview-review-formatter.md +32 -0
- package/.opencode/commands/deepreview-loop.md +29 -12
- package/.opencode/commands/deepreview-spec-loop.md +18 -3
- package/.opencode/commands/deepreview-spec.md +11 -4
- package/.opencode/commands/deepreview.md +11 -4
- package/package.json +1 -1
- package/src/diff-classifier.ts +2 -0
- package/src/post-review.ts +2 -2
- package/src/review-helpers.test.ts +170 -0
- package/src/review-helpers.ts +132 -2
|
@@ -23,8 +23,11 @@ You will receive a path to an implementation plan file. Read it.
|
|
|
23
23
|
|
|
24
24
|
For each fix in the plan, in the order specified by the "Order of Operations" section (or top-to-bottom if fixes are independent):
|
|
25
25
|
|
|
26
|
+
- If the fix is marked `**Validation:** rejected`, skip it entirely. Do not read the file or attempt the change. Note it as `SKIPPED (rejected): path — reason from validation notes`.
|
|
27
|
+
|
|
26
28
|
1. Read the current file at the referenced location
|
|
27
29
|
2. Apply the code change exactly as specified in the plan
|
|
30
|
+
- For fixes marked `**Validation:** revised`, the `**Code change:**` field contains the validator's corrected version — apply it normally. Use `APPLIED (revised):` in your response.
|
|
28
31
|
3. **Globalize check:** After applying, check whether other files _listed in input.txt or the plan_ have the same pattern. If so, apply the equivalent fix there too. Do NOT search the broader codebase. To identify "listed files": for diff inputs, use files from `diff --git a/... b/...` headers; for concatenated file inputs, use files from `=== filename ===` headers. Common cases:
|
|
29
32
|
- A loop command fix that applies to the other loop command (code-loop ↔ spec-loop)
|
|
30
33
|
- A prompt/contract change affecting multiple agent files
|
|
@@ -58,6 +61,8 @@ Your ONLY response must be a list of files modified, one per line, in this forma
|
|
|
58
61
|
|
|
59
62
|
```
|
|
60
63
|
APPLIED: path/to/file.ts — [one-line description of change]
|
|
64
|
+
APPLIED (revised): path/to/file.ts — [one-line description; code was revised by validator]
|
|
65
|
+
SKIPPED (rejected): path/to/file.ts — [reason from validation notes]
|
|
61
66
|
SKIPPED: path/to/other.ts — [reason it couldn't be applied]
|
|
62
67
|
FAILED: path/to/broken.ts — [lint/test error message]
|
|
63
68
|
VERIFICATION: [PASS | FAIL — summary of fmt/lint/test results]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Validates proposed fixes from the implementation plan against actual source code or spec documents before application. Part of the deepreview pipeline."
|
|
3
|
+
mode: subagent
|
|
4
|
+
temperature: 0.1
|
|
5
|
+
permission:
|
|
6
|
+
# Read access is implicitly unrestricted (OpenCode default) — needed to inspect source files.
|
|
7
|
+
edit:
|
|
8
|
+
".ai/deepreview/**": allow
|
|
9
|
+
"*": deny
|
|
10
|
+
bash:
|
|
11
|
+
"git log*": allow
|
|
12
|
+
"git blame*": allow
|
|
13
|
+
"*": deny
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
You are a skeptical senior engineer. Your job is to independently verify each proposed fix in an implementation plan before it gets applied to the codebase. You are not here to agree with the planner — you are here to catch mistakes.
|
|
17
|
+
|
|
18
|
+
## Input
|
|
19
|
+
|
|
20
|
+
You will receive paths to:
|
|
21
|
+
|
|
22
|
+
1. An implementation plan file (the planner's output)
|
|
23
|
+
2. An input file (the original diff or file content being reviewed)
|
|
24
|
+
3. A synthesis file (the review findings the fixes address)
|
|
25
|
+
|
|
26
|
+
Read all three.
|
|
27
|
+
|
|
28
|
+
## Process
|
|
29
|
+
|
|
30
|
+
For each fix in the implementation plan, in order:
|
|
31
|
+
|
|
32
|
+
1. **Read broader context** — Read the full function/class containing the fix target (up to ~200 lines per fix). If the fix changes a function signature or behavior, also read direct callers. Use the Read tool with offset/limit. Do NOT read entire files.
|
|
33
|
+
- _If the input is spec/plan documents (not source code):_ skip caller/callee checks. Instead, validate logical consistency, cross-reference accuracy, and that the fix matches the synthesis finding.
|
|
34
|
+
2. **Verify correctness** — Does the proposed code change actually fix the identified issue? Check for:
|
|
35
|
+
- Logic errors in the fix itself
|
|
36
|
+
- Missing imports or dependencies
|
|
37
|
+
- Wrong variable names or types
|
|
38
|
+
- Broken callers/callees from signature changes
|
|
39
|
+
- Whether the fix matches what the synthesis finding actually describes
|
|
40
|
+
3. **Check scope** — Does the fix stay within what the finding requires? Flag if it adds unnecessary validation, refactoring, or unrelated changes.
|
|
41
|
+
4. **Detect conflicts** (best-effort) — Do any fixes modify the same file region or interact in ways that would break when applied together? When a conflict is detected, reject the lower-priority fix. When conflicting fixes have equal priority, reject the one that appears later in the plan.
|
|
42
|
+
|
|
43
|
+
## Verdict per fix
|
|
44
|
+
|
|
45
|
+
- **approved** — Fix is correct and safe to apply as-is.
|
|
46
|
+
- **revised** — Fix addresses the right issue but needs adjustment. You provide a corrected code change. Note: revised code reflects the validator's correction but has not been re-validated by a second pass.
|
|
47
|
+
- **rejected** — Fix is wrong, introduces a new bug, or is out of scope. Explain why. The applier will skip this fix.
|
|
48
|
+
|
|
49
|
+
## Output format
|
|
50
|
+
|
|
51
|
+
Write your validated plan to the output path provided. Use this structure:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
# Validated Implementation Plan — [PR/branch] — [date]
|
|
55
|
+
|
|
56
|
+
## Summary
|
|
57
|
+
[Original summary + validation stats: N approved, N revised, N rejected]
|
|
58
|
+
|
|
59
|
+
## Fix Plan
|
|
60
|
+
|
|
61
|
+
### Fix [N]: [Issue Title]
|
|
62
|
+
**File(s):** path/to/file:line
|
|
63
|
+
**Priority:** critical | warning | suggestion
|
|
64
|
+
**Validation:** approved | revised | rejected
|
|
65
|
+
**Validation notes:** [1-2 sentences: what was checked, what was found]
|
|
66
|
+
**Approach:** [original or revised approach]
|
|
67
|
+
**Code change:**
|
|
68
|
+
[Original code if approved, corrected code if revised, "[rejected — see validation notes]" if rejected]
|
|
69
|
+
**Verification:** [from original plan]
|
|
70
|
+
|
|
71
|
+
## Order of Operations
|
|
72
|
+
[Revised if any fixes were rejected or reordering is needed]
|
|
73
|
+
|
|
74
|
+
## Risk
|
|
75
|
+
[Updated with any new risks identified during validation]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Critical fixes first, then warnings, then suggestions. Preserve the original ordering within each priority level unless conflict resolution requires reordering.
|
|
79
|
+
|
|
80
|
+
Be concise. No preamble or filler.
|
|
81
|
+
|
|
82
|
+
## Quality rules
|
|
83
|
+
|
|
84
|
+
- **Verify, don't assume.** Read the actual source code before judging a fix. Do not approve or reject based on the plan text alone.
|
|
85
|
+
- **Stay within scope.** Only validate what the plan proposes. Do not suggest additional fixes, improvements, or refactoring.
|
|
86
|
+
- **Preserve approved fixes exactly.** If a fix is approved, copy its code change verbatim — do not edit it.
|
|
87
|
+
- **Reject decisively.** If a fix would introduce a bug, reject it with a clear explanation. Do not try to salvage it with a revision unless the fix is close to correct.
|
|
88
|
+
|
|
89
|
+
## Response contract
|
|
90
|
+
|
|
91
|
+
After writing your validated plan file, your ONLY response must be the absolute path to your output file and a single stats line (e.g., "4 approved, 1 revised, 1 rejected"). Do not include any other text.
|
|
@@ -77,6 +77,38 @@ line: <line number>
|
|
|
77
77
|
- If the synthesis says "line 42" but looking at the diff that corresponds to a different new-side line, use the correct new-side line
|
|
78
78
|
- If you cannot determine an exact line, use the first line of the relevant function/block
|
|
79
79
|
|
|
80
|
+
## Suggestion block rules
|
|
81
|
+
|
|
82
|
+
When a finding contains a ` ```suggestion ` block, the `startLine..line` anchor MUST cover every line that the suggestion replaces — not just the line referenced in the synthesis.
|
|
83
|
+
|
|
84
|
+
Before writing a suggestion:
|
|
85
|
+
|
|
86
|
+
1. Identify the exact lines in the diff that will be replaced by the suggestion content
|
|
87
|
+
2. Count those lines — this is your replacement scope
|
|
88
|
+
3. Set `startLine` to the first line being replaced and `line` to the last
|
|
89
|
+
4. The anchor range (startLine..line) MUST cover all original lines being removed or modified. The suggestion may be shorter than the anchor range (deletions are fine), but should not be longer — if your replacement has more lines than the anchor covers, widen the anchor to include all affected lines.
|
|
90
|
+
|
|
91
|
+
Example — replacing a 5-line callout block (lines 246-250). Note: use exactly three backticks for suggestion blocks in output — the four-backtick fence below is only for illustration:
|
|
92
|
+
|
|
93
|
+
````
|
|
94
|
+
---
|
|
95
|
+
path: docs/migration.md
|
|
96
|
+
startLine: 246
|
|
97
|
+
line: 250
|
|
98
|
+
---
|
|
99
|
+
```suggestion
|
|
100
|
+
> [!WARNING]
|
|
101
|
+
> The actual default is 1 MB, not 10 MB.
|
|
102
|
+
```
|
|
103
|
+
````
|
|
104
|
+
|
|
105
|
+
Common mistakes to avoid:
|
|
106
|
+
|
|
107
|
+
- Anchoring to a single line inside a multi-line block (e.g., `line: 247` when replacing lines 246-250) — this duplicates surrounding lines when applied
|
|
108
|
+
- Including lines in the suggestion body that already exist outside the anchor range — this creates duplicates
|
|
109
|
+
- If you cannot determine the exact replacement scope from the diff, use prose instead of a suggestion block
|
|
110
|
+
- As a safety net, the posting pipeline strips suggestion blocks that exceed the anchor range — if your suggestion disappears, widen the anchor
|
|
111
|
+
|
|
80
112
|
## Response contract
|
|
81
113
|
|
|
82
114
|
After writing the threads file, your ONLY response must be the absolute path to your output file and a count line (e.g., "12 threads written"). Do not summarize findings.
|
|
@@ -31,12 +31,26 @@ Run the full deepreview pipeline (Stages 1-5 from the deepreview command):
|
|
|
31
31
|
- Append SESSION_DIR to ALL_SESSION_DIRS
|
|
32
32
|
- Stage 1: 5 parallel reviewers — prepend PRIOR_CONTEXT (if non-empty) to each reviewer's prompt as "${PRIOR_CONTEXT}You are reviewing ... Read the content at $SESSION_DIR/input.txt. Write your review to $SESSION_DIR/review-{perspective}.md."
|
|
33
33
|
- Stage 2: 5 parallel validators (cross-validation)
|
|
34
|
-
- Note: validators do NOT receive PRIOR_CONTEXT. This is intentional — validators independently verify reviewer claims without being influenced by design context
|
|
34
|
+
- Note: validators do NOT receive PRIOR_CONTEXT. This is intentional — validators independently verify reviewer claims without being influenced by design context.
|
|
35
|
+
- Stage 3: Synthesizer
|
|
35
36
|
- Stage 4: Implementation planner
|
|
37
|
+
- Stage 5: Plan validator — dispatch plan-validator with implementation-plan.md, synthesis.md, and input.txt.
|
|
38
|
+
If it fails, warn and set PLAN_FILE="$SESSION_DIR/implementation-plan.md".
|
|
39
|
+
Otherwise set PLAN_FILE="$SESSION_DIR/validated-plan.md".
|
|
36
40
|
|
|
37
41
|
Record the stats from the synthesis return: count of critical, warning, and suggestion findings.
|
|
38
42
|
|
|
39
43
|
STEP 3: CHECK EXIT CONDITION
|
|
44
|
+
DEADLOCK CHECK (iter 2+ only):
|
|
45
|
+
Compare this iteration's findings (file:line + issue title) against the previous iteration's findings. If two consecutive iterations produce the SAME findings, this indicates a deadlock — the applier is making changes that don't resolve the issue, or the reviewer keeps flagging the same thing.
|
|
46
|
+
|
|
47
|
+
When deadlock is detected:
|
|
48
|
+
|
|
49
|
+
- Tell the user: "Deadlock detected: the following findings persist across iterations:"
|
|
50
|
+
- List the repeated findings.
|
|
51
|
+
- Ask: "How would you like to resolve these? Options: skip these findings, provide guidance, or stop the loop."
|
|
52
|
+
- Follow the user's instruction.
|
|
53
|
+
|
|
40
54
|
If the synthesis/review has 0 critical AND 0 warning AND 0 suggestion findings:
|
|
41
55
|
|
|
42
56
|
- Tell the user: "deepreviewloop complete after $ITERATION iteration(s). No findings remain."
|
|
@@ -45,7 +59,7 @@ If the synthesis/review has 0 critical AND 0 warning AND 0 suggestion findings:
|
|
|
45
59
|
STEP 4: APPLY ALL FIXES
|
|
46
60
|
Dispatch the applier automatically — do NOT ask the user for permission.
|
|
47
61
|
Use the Task tool with subagent_type="deepreview-applier":
|
|
48
|
-
"Read the implementation plan at $
|
|
62
|
+
"Read the implementation plan at $PLAN_FILE. Apply the fixes."
|
|
49
63
|
|
|
50
64
|
Wait for the applier to return. Parse the applier's response for VERIFICATION status.
|
|
51
65
|
|
|
@@ -54,7 +68,14 @@ If the applier reports VERIFICATION: FAIL:
|
|
|
54
68
|
|
|
55
69
|
- Show the user the error summary from the applier's response
|
|
56
70
|
- Ask: "Applied fixes failed verification (lint/test). Options: revert and skip failing fix, continue anyway, or stop?"
|
|
57
|
-
- If revert:
|
|
71
|
+
- If revert:
|
|
72
|
+
1. Run `git checkout -- .` to undo all changes from this iteration.
|
|
73
|
+
2. Note which fix failed, add it to a SKIP_LIST, and re-run the planner without that fix, writing to `$SESSION_DIR/implementation-plan-retry.md`.
|
|
74
|
+
3. Dispatch plan-validator — Use the Task tool with subagent_type="deepreview-plan-validator":
|
|
75
|
+
"Read the implementation plan at $SESSION_DIR/implementation-plan-retry.md, the synthesis at $SESSION_DIR/synthesis.md, and the original input at $SESSION_DIR/input.txt. Write the validated plan to $SESSION_DIR/validated-plan.md. Note: the following findings were intentionally excluded due to verification failures: [SKIP_LIST]"
|
|
76
|
+
If it fails, set PLAN_FILE="$SESSION_DIR/implementation-plan-retry.md".
|
|
77
|
+
Otherwise set PLAN_FILE="$SESSION_DIR/validated-plan.md".
|
|
78
|
+
4. Pass PLAN_FILE to the applier.
|
|
58
79
|
- If continue: proceed to STEP 5 (the next iteration's reviewers will likely catch the introduced error).
|
|
59
80
|
- If stop: STOP.
|
|
60
81
|
|
|
@@ -192,17 +213,13 @@ Task 12 — Use the Task tool with subagent_type="deepreview-planner":
|
|
|
192
213
|
|
|
193
214
|
Record the summary line.
|
|
194
215
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
If two consecutive iterations produce the SAME findings (same file:line, same issue title), this indicates a deadlock — the applier is making changes that don't resolve the issue, or the reviewer keeps flagging the same thing.
|
|
216
|
+
Stage 5 — DISPATCH PLAN VALIDATOR:
|
|
217
|
+
Task 13 — Use the Task tool with subagent_type="deepreview-plan-validator":
|
|
218
|
+
"Read the implementation plan at $SESSION_DIR/implementation-plan.md, the synthesis at $SESSION_DIR/synthesis.md, and the original input at $SESSION_DIR/input.txt. Write the validated plan to $SESSION_DIR/validated-plan.md."
|
|
199
219
|
|
|
200
|
-
|
|
220
|
+
If this task fails, emit a warning: "Plan validation failed — applying unvalidated plan." and set PLAN_FILE="$SESSION_DIR/implementation-plan.md". Otherwise set PLAN_FILE="$SESSION_DIR/validated-plan.md" and record the stats line.
|
|
201
221
|
|
|
202
|
-
|
|
203
|
-
- List the repeated findings.
|
|
204
|
-
- Ask: "How would you like to resolve these? Options: skip these findings, provide guidance, or stop the loop."
|
|
205
|
-
- Follow the user's instruction.
|
|
222
|
+
Go to STEP 3.
|
|
206
223
|
|
|
207
224
|
IMPORTANT RULES:
|
|
208
225
|
|
|
@@ -26,6 +26,9 @@ Run the full deepreview-spec pipeline (Stages 1-5 from the deepreview-spec comma
|
|
|
26
26
|
- Stage 2: 5 parallel validators (cross-validation)
|
|
27
27
|
- Stage 3: Synthesizer
|
|
28
28
|
- Stage 4: Implementation planner (spec changes, not code changes)
|
|
29
|
+
- Stage 5: Plan validator — dispatch plan-validator with implementation-plan.md, synthesis.md, and input.txt.
|
|
30
|
+
If it fails, warn and set PLAN_FILE="$SESSION_DIR/implementation-plan.md".
|
|
31
|
+
Otherwise set PLAN_FILE="$SESSION_DIR/validated-plan.md".
|
|
29
32
|
|
|
30
33
|
Record the stats from the synthesis return: count of critical, warning, and suggestion findings.
|
|
31
34
|
|
|
@@ -46,10 +49,12 @@ B) PLATEAU EXIT: If ITERATION >= 3 and the total has not decreased compared to t
|
|
|
46
49
|
STEP 4: APPLY ALL FIXES
|
|
47
50
|
Dispatch the applier automatically — do NOT ask the user for permission.
|
|
48
51
|
Use the Task tool with subagent_type="deepreview-applier":
|
|
49
|
-
"Read the implementation plan at $
|
|
52
|
+
"Read the implementation plan at $PLAN_FILE. Apply the fixes."
|
|
50
53
|
|
|
51
54
|
Wait for the applier to return.
|
|
52
55
|
|
|
56
|
+
<!-- Note: No verification failure handling here (unlike code-loop) because spec changes don't trigger lint/test failures. -->
|
|
57
|
+
|
|
53
58
|
STEP 5: INCREMENT AND RE-REVIEW
|
|
54
59
|
Set ITERATION = ITERATION + 1
|
|
55
60
|
|
|
@@ -71,17 +76,21 @@ Check if input.txt is empty. If empty, tell user "Nothing to review — files ar
|
|
|
71
76
|
BUILD PRIOR CONTEXT FOR THIS ITERATION:
|
|
72
77
|
Dispatch a helper task to extract findings from ALL previous syntheses:
|
|
73
78
|
Task — Use the Task tool with subagent_type="general":
|
|
74
|
-
"Read the synthesis files from
|
|
79
|
+
"Read the synthesis files AND implementation plan files from these directories: [LIST EACH PATH FROM ALL_SESSION_DIRS EXCLUDING CURRENT]. If any file does not exist, skip it. Extract:
|
|
75
80
|
|
|
76
81
|
## Prior Findings (already reported — do not re-report or verify)
|
|
77
82
|
|
|
78
83
|
- [Short Issue Title] ([category]) — [file:line or section reference]
|
|
79
84
|
|
|
85
|
+
## Applied Fixes (changes made by previous iterations — new bugs here are regressions)
|
|
86
|
+
|
|
87
|
+
- [Fix title from implementation plan] — [file:line or section reference] (applied in iter N)
|
|
88
|
+
|
|
80
89
|
## Covered Regions (already examined — prioritize elsewhere)
|
|
81
90
|
|
|
82
91
|
- [file or section references, padded generously around each finding location]
|
|
83
92
|
|
|
84
|
-
Deduplicate findings that appear in multiple syntheses. Return ONLY these
|
|
93
|
+
Deduplicate findings that appear in multiple syntheses. Return ONLY these three sections, nothing else."
|
|
85
94
|
|
|
86
95
|
Set PRIOR_CONTEXT to the returned text. Validate that it contains "## Prior Findings" — if not, warn the user ("Helper returned malformed prior context — proceeding without deduplication") and set PRIOR_CONTEXT="". If CONTEXT_FILE exists, prepend:
|
|
87
96
|
"## Design Decisions (intentional — do not flag)\nThe following are deliberate design choices. Do NOT flag these as issues or suggest alternatives.\n`\n" + contents of CONTEXT_FILE + "\n`\n\n"
|
|
@@ -148,6 +157,12 @@ Task 12 — Use the Task tool with subagent_type="deepreview-planner":
|
|
|
148
157
|
|
|
149
158
|
Record the summary line.
|
|
150
159
|
|
|
160
|
+
Stage 5 — DISPATCH PLAN VALIDATOR:
|
|
161
|
+
Task 13 — Use the Task tool with subagent_type="deepreview-plan-validator":
|
|
162
|
+
"Read the implementation plan at $SESSION_DIR/implementation-plan.md, the synthesis at $SESSION_DIR/synthesis.md, and the original input at $SESSION_DIR/input.txt. Write the validated plan to $SESSION_DIR/validated-plan.md."
|
|
163
|
+
|
|
164
|
+
If this task fails, emit a warning: "Plan validation failed — applying unvalidated plan." and set PLAN_FILE="$SESSION_DIR/implementation-plan.md". Otherwise set PLAN_FILE="$SESSION_DIR/validated-plan.md" and record the stats line.
|
|
165
|
+
|
|
151
166
|
Go to STEP 3.
|
|
152
167
|
|
|
153
168
|
STEP 6: DIVERGENCE AND DEADLOCK DETECTION
|
|
@@ -80,18 +80,25 @@ Task 12 — Use the Task tool with subagent_type="deepreview-planner":
|
|
|
80
80
|
|
|
81
81
|
Record the summary line from its return.
|
|
82
82
|
|
|
83
|
-
STEP 7:
|
|
83
|
+
STEP 7: DISPATCH STAGE 5 — PLAN VALIDATION (1 task)
|
|
84
|
+
Task 13 — Use the Task tool with subagent_type="deepreview-plan-validator":
|
|
85
|
+
"Read the implementation plan at $SESSION_DIR/implementation-plan.md, the synthesis at $SESSION_DIR/synthesis.md, and the original input at $SESSION_DIR/input.txt. Write the validated plan to $SESSION_DIR/validated-plan.md."
|
|
86
|
+
|
|
87
|
+
If this task fails (agent error, timeout, or does not produce validated-plan.md), emit a warning: "Plan validation failed — applying unvalidated plan." and set PLAN_FILE="$SESSION_DIR/implementation-plan.md". Otherwise set PLAN_FILE="$SESSION_DIR/validated-plan.md" and record the stats line.
|
|
88
|
+
|
|
89
|
+
STEP 8: PRESENT RESULTS
|
|
84
90
|
Show the user:
|
|
85
91
|
|
|
86
92
|
- Session directory: $SESSION_DIR/
|
|
87
93
|
- Which reviewers completed (and any that failed)
|
|
88
94
|
- Stats from synthesis (the stats line from Step 5)
|
|
89
95
|
- Summary from planner (the summary line from Step 6)
|
|
96
|
+
- Plan validation stats (if available, from Step 7)
|
|
90
97
|
- Ask: "Do you want me to apply the fixes to the spec?"
|
|
91
98
|
|
|
92
|
-
STEP
|
|
93
|
-
Task
|
|
94
|
-
"Read the implementation plan at $
|
|
99
|
+
STEP 9: IF USER SAYS YES — DISPATCH STAGE 6 (1 task)
|
|
100
|
+
Task 14 — Use the Task tool with subagent_type="deepreview-applier":
|
|
101
|
+
"Read the implementation plan at $PLAN_FILE. Apply the fixes."
|
|
95
102
|
|
|
96
103
|
Show the user the list of files changed from the applier's return.
|
|
97
104
|
|
|
@@ -98,18 +98,25 @@ Task 12 — Use the Task tool with subagent_type="deepreview-planner":
|
|
|
98
98
|
|
|
99
99
|
Record the summary line from its return.
|
|
100
100
|
|
|
101
|
-
STEP 7:
|
|
101
|
+
STEP 7: DISPATCH STAGE 5 — PLAN VALIDATION (1 task)
|
|
102
|
+
Task 13 — Use the Task tool with subagent_type="deepreview-plan-validator":
|
|
103
|
+
"Read the implementation plan at $SESSION_DIR/implementation-plan.md, the synthesis at $SESSION_DIR/synthesis.md, and the original input at $SESSION_DIR/input.txt. Write the validated plan to $SESSION_DIR/validated-plan.md."
|
|
104
|
+
|
|
105
|
+
If this task fails (agent error, timeout, or does not produce validated-plan.md), emit a warning: "Plan validation failed — applying unvalidated plan." and set PLAN_FILE="$SESSION_DIR/implementation-plan.md". Otherwise set PLAN_FILE="$SESSION_DIR/validated-plan.md" and record the stats line.
|
|
106
|
+
|
|
107
|
+
STEP 8: PRESENT RESULTS
|
|
102
108
|
Show the user:
|
|
103
109
|
|
|
104
110
|
- Session directory: $SESSION_DIR/
|
|
105
111
|
- Which reviewers completed (and any that failed)
|
|
106
112
|
- Stats from synthesis (the stats line from Step 5)
|
|
107
113
|
- Summary from planner (the summary line from Step 6)
|
|
114
|
+
- Plan validation stats (if available, from Step 7)
|
|
108
115
|
- Ask: "Do you want me to apply the fixes?"
|
|
109
116
|
|
|
110
|
-
STEP
|
|
111
|
-
Task
|
|
112
|
-
"Read the implementation plan at $
|
|
117
|
+
STEP 9: IF USER SAYS YES — DISPATCH STAGE 6 (1 task)
|
|
118
|
+
Task 14 — Use the Task tool with subagent_type="deepreview-applier":
|
|
119
|
+
"Read the implementation plan at $PLAN_FILE. Apply the fixes."
|
|
113
120
|
|
|
114
121
|
Show the user the list of files changed from the applier's return.
|
|
115
122
|
|
package/package.json
CHANGED
package/src/diff-classifier.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface Finding {
|
|
|
9
9
|
|
|
10
10
|
export interface ClassifiedFinding extends Finding {
|
|
11
11
|
tier: 1 | 2 | 3;
|
|
12
|
+
/** Body with suggestion blocks stripped for rendering. Set for tier-1 findings with oversized suggestions and all tier-2 findings containing suggestions. Original `body` is used for ID computation. Set in review-helpers.ts, not in classifyFindings. */
|
|
13
|
+
renderedBody?: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
interface FileHunks {
|
package/src/post-review.ts
CHANGED
|
@@ -65,7 +65,7 @@ async function updateExistingThreads(
|
|
|
65
65
|
const batch = toUpdate.slice(i, i + CONCURRENCY);
|
|
66
66
|
await Promise.all(
|
|
67
67
|
batch.map(async ({ finding, commentId, id }) => {
|
|
68
|
-
const body = embedFindingId(finding.body, id);
|
|
68
|
+
const body = embedFindingId(finding.renderedBody ?? finding.body, id);
|
|
69
69
|
await updateReviewComment(commentId, body);
|
|
70
70
|
}),
|
|
71
71
|
);
|
|
@@ -147,7 +147,7 @@ async function postInlineThreads(
|
|
|
147
147
|
batch.map(async (f) => {
|
|
148
148
|
const id = findingId(f.path, f.startLine, f.line, f.body);
|
|
149
149
|
// Body rendered by GitHub's Markdown sanitizer; no escaping needed here
|
|
150
|
-
const body = embedFindingId(f.body, id);
|
|
150
|
+
const body = embedFindingId(f.renderedBody ?? f.body, id);
|
|
151
151
|
const ok = await postWithRetry(reviewId, f, body);
|
|
152
152
|
return ok ? null : id;
|
|
153
153
|
}),
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it } from "bun:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { validateSuggestionAnchors, classifyAndLog } from "./review-helpers.ts";
|
|
4
|
+
|
|
5
|
+
describe("validateSuggestionAnchors — no warnings", () => {
|
|
6
|
+
it("returns no warnings for findings without suggestion blocks", () => {
|
|
7
|
+
const findings = [
|
|
8
|
+
{ path: "foo.go", line: 10, body: "This is just prose feedback.", tier: 1 as const },
|
|
9
|
+
];
|
|
10
|
+
const warnings = validateSuggestionAnchors(findings);
|
|
11
|
+
assert.equal(warnings.length, 0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns no warnings when suggestion lines fit within anchor range", () => {
|
|
15
|
+
const body = [
|
|
16
|
+
"Replace this function:",
|
|
17
|
+
"```suggestion",
|
|
18
|
+
"func foo() {",
|
|
19
|
+
" return 42",
|
|
20
|
+
"}",
|
|
21
|
+
"```",
|
|
22
|
+
].join("\n");
|
|
23
|
+
// Anchor covers 3 lines (8..10), suggestion has 3 lines — matches
|
|
24
|
+
const findings = [{ path: "foo.go", startLine: 8, line: 10, body, tier: 1 as const }];
|
|
25
|
+
const warnings = validateSuggestionAnchors(findings);
|
|
26
|
+
assert.equal(warnings.length, 0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("ignores non-suggestion fenced code blocks", () => {
|
|
30
|
+
const body = [
|
|
31
|
+
"Example:",
|
|
32
|
+
"```go",
|
|
33
|
+
"func foo() {}",
|
|
34
|
+
"func bar() {}",
|
|
35
|
+
"func baz() {}",
|
|
36
|
+
"```",
|
|
37
|
+
].join("\n");
|
|
38
|
+
const findings = [{ path: "foo.go", line: 5, body, tier: 1 as const }];
|
|
39
|
+
const warnings = validateSuggestionAnchors(findings);
|
|
40
|
+
assert.equal(warnings.length, 0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("skips tier 2 and tier 3 findings", () => {
|
|
44
|
+
const body = ["```suggestion", "a", "b", "c", "```"].join("\n");
|
|
45
|
+
const findings = [
|
|
46
|
+
{ path: "foo.go", line: 5, body, tier: 2 as const },
|
|
47
|
+
{ path: "bar.go", line: 10, body, tier: 3 as const },
|
|
48
|
+
];
|
|
49
|
+
const warnings = validateSuggestionAnchors(findings);
|
|
50
|
+
assert.equal(warnings.length, 0);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("validateSuggestionAnchors — warnings", () => {
|
|
55
|
+
it("warns when suggestion has more lines than anchor range", () => {
|
|
56
|
+
const body = [
|
|
57
|
+
"Fix this:",
|
|
58
|
+
"```suggestion",
|
|
59
|
+
"line1",
|
|
60
|
+
"line2",
|
|
61
|
+
"line3",
|
|
62
|
+
"line4",
|
|
63
|
+
"line5",
|
|
64
|
+
"```",
|
|
65
|
+
].join("\n");
|
|
66
|
+
// Anchor is single-line (31), suggestion has 5 lines
|
|
67
|
+
const findings = [{ path: "mise.toml", line: 31, body, tier: 1 as const }];
|
|
68
|
+
const warnings = validateSuggestionAnchors(findings);
|
|
69
|
+
assert.equal(warnings.length, 1);
|
|
70
|
+
assert.ok(warnings[0].includes("mise.toml:31"));
|
|
71
|
+
assert.ok(warnings[0].includes("1-line anchor"));
|
|
72
|
+
assert.ok(warnings[0].includes("5-line suggestion"));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("warns for multi-line anchor that is still too narrow", () => {
|
|
76
|
+
const body = ["```suggestion", "line1", "line2", "line3", "line4", "line5", "```"].join("\n");
|
|
77
|
+
// Anchor covers 2 lines (893..894), suggestion has 5 lines
|
|
78
|
+
const findings = [{ path: "migration.md", startLine: 893, line: 894, body, tier: 1 as const }];
|
|
79
|
+
const warnings = validateSuggestionAnchors(findings);
|
|
80
|
+
assert.equal(warnings.length, 1);
|
|
81
|
+
assert.ok(warnings[0].includes("migration.md:893-894"));
|
|
82
|
+
assert.ok(warnings[0].includes("2-line anchor"));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("handles multiple suggestion blocks — warns based on largest", () => {
|
|
86
|
+
const body = [
|
|
87
|
+
"First fix:",
|
|
88
|
+
"```suggestion",
|
|
89
|
+
"a",
|
|
90
|
+
"b",
|
|
91
|
+
"c",
|
|
92
|
+
"```",
|
|
93
|
+
"Second fix:",
|
|
94
|
+
"```suggestion",
|
|
95
|
+
"x",
|
|
96
|
+
"y",
|
|
97
|
+
"```",
|
|
98
|
+
].join("\n");
|
|
99
|
+
// Single-line anchor, two suggestion blocks (3 lines and 2 lines)
|
|
100
|
+
const findings = [{ path: "foo.go", line: 5, body, tier: 1 as const }];
|
|
101
|
+
const warnings = validateSuggestionAnchors(findings);
|
|
102
|
+
// Should warn — the largest suggestion (3 lines) exceeds the 1-line anchor
|
|
103
|
+
assert.equal(warnings.length, 1);
|
|
104
|
+
assert.ok(warnings[0].includes("3-line suggestion"));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Minimal diff that puts foo.go line 10 in a hunk (tier 1)
|
|
109
|
+
const DIFF_FOO = `diff --git a/foo.go b/foo.go
|
|
110
|
+
--- a/foo.go
|
|
111
|
+
+++ b/foo.go
|
|
112
|
+
@@ -1,15 +1,15 @@
|
|
113
|
+
line1
|
|
114
|
+
line2
|
|
115
|
+
line3
|
|
116
|
+
line4
|
|
117
|
+
line5
|
|
118
|
+
line6
|
|
119
|
+
line7
|
|
120
|
+
line8
|
|
121
|
+
line9
|
|
122
|
+
-old
|
|
123
|
+
+new
|
|
124
|
+
line11
|
|
125
|
+
line12
|
|
126
|
+
line13
|
|
127
|
+
line14
|
|
128
|
+
line15
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
describe("classifyAndLog — suggestion stripping", () => {
|
|
132
|
+
it("sets renderedBody when suggestion exceeds anchor", () => {
|
|
133
|
+
const body = ["Replace this:", "```suggestion", "line A", "line B", "line C", "```"].join("\n");
|
|
134
|
+
// Single-line anchor (no startLine), 3-line suggestion — exceeds
|
|
135
|
+
const findings = [{ path: "foo.go", line: 10, body }];
|
|
136
|
+
const { tier1 } = classifyAndLog(findings, DIFF_FOO);
|
|
137
|
+
assert.equal(tier1.length, 1);
|
|
138
|
+
assert.notEqual(tier1[0].renderedBody, undefined);
|
|
139
|
+
assert.ok(!tier1[0].renderedBody!.includes("```suggestion"));
|
|
140
|
+
// Original body is untouched
|
|
141
|
+
assert.ok(tier1[0].body.includes("```suggestion"));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("preserves content from unclosed suggestion blocks", () => {
|
|
145
|
+
const body = [
|
|
146
|
+
"Some prose",
|
|
147
|
+
"```suggestion",
|
|
148
|
+
"line A",
|
|
149
|
+
"line B",
|
|
150
|
+
// No closing fence
|
|
151
|
+
].join("\n");
|
|
152
|
+
const findings = [{ path: "foo.go", line: 10, body }];
|
|
153
|
+
const { tier1 } = classifyAndLog(findings, DIFF_FOO);
|
|
154
|
+
assert.equal(tier1.length, 1);
|
|
155
|
+
// renderedBody should contain the unclosed block content
|
|
156
|
+
const rendered = tier1[0].renderedBody ?? tier1[0].body;
|
|
157
|
+
assert.ok(rendered.includes("line A"));
|
|
158
|
+
assert.ok(rendered.includes("line B"));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("handles inverted startLine/line without negative anchorLines", () => {
|
|
162
|
+
const body = ["Fix:", "```suggestion", "x", "```"].join("\n");
|
|
163
|
+
// startLine > line (inverted) — anchorLines should clamp to 1
|
|
164
|
+
const findings = [{ path: "foo.go", startLine: 12, line: 10, body }];
|
|
165
|
+
const { tier1 } = classifyAndLog(findings, DIFF_FOO);
|
|
166
|
+
assert.equal(tier1.length, 1);
|
|
167
|
+
// 1-line suggestion vs clamped 1-line anchor — fits, no stripping
|
|
168
|
+
assert.equal(tier1[0].renderedBody, undefined);
|
|
169
|
+
});
|
|
170
|
+
});
|
package/src/review-helpers.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { classifyFindings, type Finding, type ClassifiedFinding } from "./diff-c
|
|
|
4
4
|
const FINDING_ID_RE = /<!-- finding:([a-f0-9]+) -->/u;
|
|
5
5
|
const AI_TRAILER = "\n\n---\n🤖 Review generated by AI";
|
|
6
6
|
const GITHUB_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$/u;
|
|
7
|
+
const SUGGESTION_OPEN_RE = /^```suggestion\b/u;
|
|
7
8
|
|
|
8
9
|
export function escapeHtml(s: string): string {
|
|
9
10
|
return s
|
|
@@ -96,9 +97,9 @@ export function buildReviewBody(
|
|
|
96
97
|
}
|
|
97
98
|
if (validOid && validRepo) {
|
|
98
99
|
const permalink = `https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/blob/${headOid}/${f.path.split("/").map(encodeURIComponent).join("/")}#L${f.line}`;
|
|
99
|
-
body += `<details>\n<summary><a href="${permalink}"><code>${escapeHtml(f.path)}:${f.line}</code></a></summary>\n\n${f.body}\n</details>\n\n`;
|
|
100
|
+
body += `<details>\n<summary><a href="${permalink}"><code>${escapeHtml(f.path)}:${f.line}</code></a></summary>\n\n${f.renderedBody ?? f.body}\n</details>\n\n`;
|
|
100
101
|
} else {
|
|
101
|
-
body += `<details>\n<summary><code>${escapeHtml(f.path)}:${f.line}</code></summary>\n\n${f.body}\n</details>\n\n`;
|
|
102
|
+
body += `<details>\n<summary><code>${escapeHtml(f.path)}:${f.line}</code></summary>\n\n${f.renderedBody ?? f.body}\n</details>\n\n`;
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
body += AI_TRAILER;
|
|
@@ -127,5 +128,134 @@ export function classifyAndLog(
|
|
|
127
128
|
for (const f of tier3) {
|
|
128
129
|
console.warn(`WARN: ${f.path}:${f.line} demoted to review body`);
|
|
129
130
|
}
|
|
131
|
+
// Identify findings whose suggestion blocks exceed the anchor range
|
|
132
|
+
const oversized = findingsExceedingAnchor(classified);
|
|
133
|
+
const suggestionWarnings = formatSuggestionWarnings(oversized);
|
|
134
|
+
for (const w of suggestionWarnings) {
|
|
135
|
+
console.warn(w);
|
|
136
|
+
}
|
|
137
|
+
// Strip oversized suggestion blocks from tier-1 findings
|
|
138
|
+
for (const f of tier1) {
|
|
139
|
+
if (oversized.has(f)) {
|
|
140
|
+
f.renderedBody = stripSuggestionBlocks(f.body);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Tier-2 findings are posted as file-level comments where suggestion blocks
|
|
144
|
+
// render as inert code — strip them unconditionally.
|
|
145
|
+
for (const f of tier2) {
|
|
146
|
+
const stripped = stripSuggestionBlocks(f.body);
|
|
147
|
+
if (stripped !== f.body) {
|
|
148
|
+
f.renderedBody = stripped;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Tier-3 findings are posted inside the review body where suggestion blocks
|
|
152
|
+
// render as inert code — strip them unconditionally.
|
|
153
|
+
for (const f of tier3) {
|
|
154
|
+
const stripped = stripSuggestionBlocks(f.body);
|
|
155
|
+
if (stripped !== f.body) {
|
|
156
|
+
f.renderedBody = stripped;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
130
159
|
return { tier1, tier2, tier3 };
|
|
131
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Find the largest ```suggestion block in a finding body by line count.
|
|
164
|
+
* Returns the maximum line count across all suggestion blocks, or 0 if none.
|
|
165
|
+
*/
|
|
166
|
+
function maxSuggestionLines(body: string): number {
|
|
167
|
+
let max = 0;
|
|
168
|
+
let inSuggestion = false;
|
|
169
|
+
let count = 0;
|
|
170
|
+
for (const line of body.split("\n")) {
|
|
171
|
+
if (!inSuggestion && SUGGESTION_OPEN_RE.test(line.trim())) {
|
|
172
|
+
inSuggestion = true;
|
|
173
|
+
count = 0;
|
|
174
|
+
} else if (inSuggestion && line.trimEnd() === "```") {
|
|
175
|
+
inSuggestion = false;
|
|
176
|
+
max = Math.max(max, count);
|
|
177
|
+
} else if (inSuggestion) {
|
|
178
|
+
count++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (inSuggestion) {
|
|
182
|
+
max = Math.max(max, count);
|
|
183
|
+
}
|
|
184
|
+
return max;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Remove all ```suggestion blocks from a finding body, preserving other content.
|
|
189
|
+
*/
|
|
190
|
+
function stripSuggestionBlocks(body: string): string {
|
|
191
|
+
const lines = body.split("\n");
|
|
192
|
+
const result: string[] = [];
|
|
193
|
+
let inSuggestion = false;
|
|
194
|
+
let pendingBlock: string[] = [];
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
if (!inSuggestion && SUGGESTION_OPEN_RE.test(line.trim())) {
|
|
197
|
+
inSuggestion = true;
|
|
198
|
+
pendingBlock = [line];
|
|
199
|
+
} else if (inSuggestion && line.trimEnd() === "```") {
|
|
200
|
+
inSuggestion = false;
|
|
201
|
+
pendingBlock = [];
|
|
202
|
+
} else if (inSuggestion) {
|
|
203
|
+
pendingBlock.push(line);
|
|
204
|
+
} else {
|
|
205
|
+
result.push(line);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (inSuggestion) {
|
|
209
|
+
// Unclosed block — preserve all lines as-is
|
|
210
|
+
result.push(...pendingBlock);
|
|
211
|
+
}
|
|
212
|
+
return result.join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function anchorLineCount(f: Finding): number {
|
|
216
|
+
return f.startLine === undefined ? 1 : Math.max(1, f.line - f.startLine + 1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Identify tier-1 findings whose largest suggestion block exceeds the anchor range.
|
|
221
|
+
* Returns a Set of findings that need their suggestion blocks stripped.
|
|
222
|
+
*/
|
|
223
|
+
function findingsExceedingAnchor(findings: ClassifiedFinding[]): Set<ClassifiedFinding> {
|
|
224
|
+
const exceeded = new Set<ClassifiedFinding>();
|
|
225
|
+
for (const f of findings) {
|
|
226
|
+
if (f.tier !== 1) continue;
|
|
227
|
+
const suggLines = maxSuggestionLines(f.body);
|
|
228
|
+
if (suggLines === 0) continue;
|
|
229
|
+
const anchorLines = anchorLineCount(f);
|
|
230
|
+
if (suggLines > anchorLines) {
|
|
231
|
+
exceeded.add(f);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return exceeded;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Format warning messages for findings whose suggestion blocks exceed their anchor range.
|
|
239
|
+
* @internal
|
|
240
|
+
*/
|
|
241
|
+
export function formatSuggestionWarnings(oversized: Set<ClassifiedFinding>): string[] {
|
|
242
|
+
const warnings: string[] = [];
|
|
243
|
+
for (const f of oversized) {
|
|
244
|
+
const suggLines = maxSuggestionLines(f.body);
|
|
245
|
+
const anchorLines = anchorLineCount(f);
|
|
246
|
+
const anchor =
|
|
247
|
+
f.startLine === undefined ? `${f.path}:${f.line}` : `${f.path}:${f.startLine}-${f.line}`;
|
|
248
|
+
warnings.push(
|
|
249
|
+
`WARN: ${anchor} has ${anchorLines}-line anchor but ${suggLines}-line suggestion — anchor should be widened to cover all lines being replaced`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return warnings;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Validate that suggestion blocks fit within their anchor ranges.
|
|
257
|
+
* @internal
|
|
258
|
+
*/
|
|
259
|
+
export function validateSuggestionAnchors(findings: ClassifiedFinding[]): string[] {
|
|
260
|
+
return formatSuggestionWarnings(findingsExceedingAnchor(findings));
|
|
261
|
+
}
|