@fyeeme/pi-review 2.0.1 → 2.1.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/README.md +14 -13
- package/index.ts +2 -2
- package/package.json +7 -7
- package/prompts/{review.md → review.parallel.md} +4 -4
- package/prompts/review.single.md +16 -0
- package/prompts/simplify.parallel.md +1 -1
- package/prompts/simplify.single.md +1 -1
- package/skills/{review → code-review}/SKILL.md +164 -70
- package/skills/{simplify → code-simplify}/SKILL.md +15 -15
- package/src/config.ts +16 -10
- package/src/diff.ts +2 -2
- package/src/dispatch.ts +94 -31
- package/src/loop.ts +266 -0
- package/src/tools/review_report.ts +19 -5
package/README.md
CHANGED
|
@@ -8,22 +8,22 @@
|
|
|
8
8
|
|
|
9
9
|
- **`review_report` structured findings sink** — Chinese Markdown rendered back to the conversation plus machine-readable JSON under `<cwd>/.pi/review/` for CI, `--fix` re-reports, and `--comment`.
|
|
10
10
|
|
|
11
|
-
- **Effort
|
|
11
|
+
- **Effort split (v2.1)** — `/code-review [low|medium|high|xhigh|max]`: low/medium/high review the diff in ONE pass in the main session (no subagents: rubric-ported flag criteria, P0–P3 priorities, in-session self-verify at medium/high); xhigh/max keep the opt-in deep sweep — quad tuples `{correctnessAngles, perAngle, maxFindings, sweep}`, grouped-by-location independent verification, and the gap-hunt.
|
|
12
12
|
|
|
13
|
-
- **`/simplify` dual-mode** — the dispatcher measures context usage and diff size against the declared strategy, then renders either the PARALLEL template (4 cleaner agents via `subagent`) or the SINGLE-PASS one.
|
|
13
|
+
- **`/code-simplify` dual-mode** — the dispatcher measures context usage and diff size against the declared strategy, then renders either the PARALLEL template (4 cleaner agents via `subagent`) or the SINGLE-PASS one.
|
|
14
14
|
|
|
15
|
-
-
|
|
15
|
+
- Commands: `/code-review` and `/code-simplify` — restored v1 names (2.0.0 briefly renamed them `/review` / `/simplify`).
|
|
16
16
|
|
|
17
17
|
Review & cleanup assets for [pi](https://github.com/earendil-works/pi-mono), in the sandwich shape (skills + prompts + agents on top of a thin plugin entry):
|
|
18
18
|
|
|
19
19
|
```
|
|
20
|
-
skills/ methodology (review, simplify) — registered natively via the `pi` manifest
|
|
20
|
+
skills/ methodology (code-review, code-simplify) — registered natively via the `pi` manifest
|
|
21
21
|
prompts/ orchestration strategy as data — parallel-when guards in frontmatter,
|
|
22
22
|
CC-parity phase structure in the body; rendered by the generic dispatcher
|
|
23
23
|
agents/ the review roles as subagent definitions (finder-*, cleaner-*, verifier,
|
|
24
24
|
gap-hunter) invoked via the `subagent` tool of @fyeeme/pi-subagents
|
|
25
25
|
index.ts plugin entry: the review_report structured findings sink + the
|
|
26
|
-
/review and /simplify dispatcher commands
|
|
26
|
+
/code-review and /code-simplify dispatcher commands
|
|
27
27
|
src/ dispatch.ts (variable gathering, guard evaluation, rendering),
|
|
28
28
|
diff.ts (deterministic diff ladder — unchanged v1 semantics),
|
|
29
29
|
strategy.ts (guard evaluator), tools/review_report.ts
|
|
@@ -39,8 +39,8 @@ composition is idempotent.
|
|
|
39
39
|
|
|
40
40
|
## Commands
|
|
41
41
|
|
|
42
|
-
- `/review [low|medium|high|xhigh|max] [--fix] [--comment] [--share] [<pr#>|<branch>|<path>]` — effort-level code review via the review skill. Effort is sticky: an explicit level is remembered; the next bare `/review` reuses it.
|
|
43
|
-
- `/simplify [<target>]` — cleanup of the changed code (reuse/simplification/efficiency/altitude). The dispatcher resolves the diff (upstream merge-base → HEAD worktree → staged → unstaged; submodule-aware), evaluates the strategy declared in `prompts/simplify.parallel.md` frontmatter (context usage < 80%, diff < 400k chars, fan-out available), and renders either the PARALLEL template (Phase 0 visible diff read → `subagent` parallel dispatch of the 4 cleaner agents with `maxTurns: 15` → Phase 2 apply/verify/report) or the SINGLE-PASS template (angles worked inline).
|
|
42
|
+
- `/code-review [low|medium|high|xhigh|max] [--fix] [--loop] [--comment] [--share] [<pr#>|<branch>|<path>]` — effort-level code review via the code-review skill. low/medium/high run as a single pass in this session (fast path, default); xhigh/max fan out finder/verifier/gap-hunt agents through `subagent`. Effort is sticky: an explicit level is remembered; the next bare `/code-review` reuses it. `--loop` (single-pass levels only) drives extension-orchestrated fix→re-review rounds (≤ `maxTurns.loop`, default 3) until no P0/P1 findings remain.
|
|
43
|
+
- `/code-simplify [<target>]` — cleanup of the changed code (reuse/simplification/efficiency/altitude). The dispatcher resolves the diff (upstream merge-base → HEAD worktree → staged → unstaged; submodule-aware), evaluates the strategy declared in `prompts/simplify.parallel.md` frontmatter (context usage < 80%, diff < 400k chars, fan-out available), and renders either the PARALLEL template (Phase 0 visible diff read → `subagent` parallel dispatch of the 4 cleaner agents with `maxTurns: 15` → Phase 2 apply/verify/report) or the SINGLE-PASS template (angles worked inline).
|
|
44
44
|
|
|
45
45
|
Reports land via the `review_report` tool: Chinese Markdown back to the conversation plus machine-readable JSON under `<cwd>/.pi/review/`.
|
|
46
46
|
|
|
@@ -69,19 +69,20 @@ pattern as pi-subagents' `pi-subagent.json` (project overrides global):
|
|
|
69
69
|
// <any layer>/pi-review.json — all keys optional
|
|
70
70
|
{
|
|
71
71
|
"maxTurns": {
|
|
72
|
-
"subagent": 20, // each /review finder-batch subagent call
|
|
73
|
-
"verifier": 15, // each /review Phase 2 verifier call
|
|
74
|
-
"gapHunt": 15, // the /review Phase 3 gap-hunter
|
|
75
|
-
"simplify": 15
|
|
72
|
+
"subagent": 20, // each /code-review finder-batch subagent call
|
|
73
|
+
"verifier": 15, // each /code-review Phase 2 verifier call
|
|
74
|
+
"gapHunt": 15, // the /code-review Phase 3 gap-hunter (xhigh/max)
|
|
75
|
+
"simplify": 15, // each /code-simplify PARALLEL cleaner agent
|
|
76
|
+
"loop": 3 // --loop fix→re-review round cap (single-pass levels)
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
```
|
|
79
80
|
|
|
80
81
|
Values must be positive integers; anything else (or an absent file) falls back
|
|
81
|
-
to the built-in defaults — `20` / `15` / `15` / `15`, the numbers the bundled
|
|
82
|
+
to the built-in defaults — `20` / `15` / `15` / `15` / `3`, the numbers the bundled
|
|
82
83
|
prompts and skills were written with — so with no configuration the rendered
|
|
83
84
|
instructions are byte-identical to the pre-config behavior. Files are read at
|
|
84
|
-
command time: an edit takes effect on the next `/review` or `/simplify`
|
|
85
|
+
command time: an edit takes effect on the next `/code-review` or `/code-simplify`
|
|
85
86
|
without a restart. When a budget is configured, the trigger message states it
|
|
86
87
|
and the skills defer to it over their built-in defaults.
|
|
87
88
|
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Sandwich architecture (see openspec change subagent-sandwich-refactor):
|
|
5
5
|
*
|
|
6
|
-
* Skills skills/review, skills/simplify — review methodology,
|
|
6
|
+
* Skills skills/code-review, skills/code-simplify — review methodology,
|
|
7
7
|
* registered natively via the pi manifest (`pi.skills`); they
|
|
8
8
|
* reference capabilities by stable tool/agent names only.
|
|
9
9
|
* Prompts prompts/ — the orchestration strategy as data: parallel-when
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* manifest path wiring and no separate install step), registers
|
|
21
21
|
* this package's agents directory as a discovery source, and adds
|
|
22
22
|
* the `review_report` structured findings sink plus the
|
|
23
|
-
* /review and /simplify dispatcher commands.
|
|
23
|
+
* /code-review and /code-simplify dispatcher commands.
|
|
24
24
|
*
|
|
25
25
|
* The `subagent` tool registers exactly once per process: if pi-subagents
|
|
26
26
|
* is ALSO installed standalone (or another consumer composes it), the guard
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fyeeme/pi-review",
|
|
3
|
-
"version": "2.0
|
|
4
|
-
"description": "Review & cleanup assets for pi: /review and /simplify commands dispatching declarative prompt templates (parallel strategy as frontmatter data) plus review methodology skills and finder/verifier agent definitions. Spawning lives in @fyeeme/pi-subagents; this package registers the review_report findings sink and the generic dispatcher.",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Review & cleanup assets for pi: /code-review and /code-simplify commands dispatching declarative prompt templates (parallel strategy as frontmatter data) plus review methodology skills and finder/verifier agent definitions. Spawning lives in @fyeeme/pi-subagents; this package registers the review_report findings sink and the generic dispatcher.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "fyeeme",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"./index.ts"
|
|
31
31
|
],
|
|
32
32
|
"skills": [
|
|
33
|
-
"skills/review",
|
|
34
|
-
"skills/simplify"
|
|
33
|
+
"skills/code-review",
|
|
34
|
+
"skills/code-simplify"
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"typebox": ">=1.0.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@earendil-works/pi-ai": "0.
|
|
52
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
53
|
-
"@earendil-works/pi-tui": "0.
|
|
51
|
+
"@earendil-works/pi-ai": "0.85.1",
|
|
52
|
+
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
53
|
+
"@earendil-works/pi-tui": "0.85.1",
|
|
54
54
|
"@types/node": "22.19.19",
|
|
55
55
|
"jiti": "2.7.0",
|
|
56
56
|
"typebox": "1.1.38",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "/review trigger —
|
|
2
|
+
description: "/code-review trigger — xhigh/max deep sweep: finder/verifier/gap-hunt fan-out via the code-review skill"
|
|
3
3
|
vars: [effort, effort-source, extra-args, skill, finder-max-turns, verifier-max-turns, gap-hunt-max-turns, verify]
|
|
4
4
|
---
|
|
5
5
|
Run a code review now. Effective effort: {{effort}} ({{effort-source}}){{extra-args}}.
|
|
6
6
|
|
|
7
|
-
First load the review skill with the read tool: {{skill}}. Then follow
|
|
8
|
-
exactly — dispatch the finder / verifier / gap-hunter agents it
|
|
9
|
-
through the `subagent` tool (bundled agents: finder-diff-scan,
|
|
7
|
+
First load the code-review skill with the read tool: {{skill}}. Then follow its
|
|
8
|
+
XHIGH/MAX FLOW exactly — dispatch the finder / verifier / gap-hunter agents it
|
|
9
|
+
calls for through the `subagent` tool (bundled agents: finder-diff-scan,
|
|
10
10
|
finder-removed-behavior, finder-cross-file, finder-language-pitfall,
|
|
11
11
|
finder-wrapper-proxy, cleaner-reuse, cleaner-simplification,
|
|
12
12
|
cleaner-efficiency, cleaner-altitude, finder-conventions, verifier,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "/code-review trigger — single-pass main-session review via the code-review skill (low/medium/high)"
|
|
3
|
+
vars: [effort, effort-source, extra-args, skill, verify, loop-note]
|
|
4
|
+
---
|
|
5
|
+
Run a code review now. Effective effort: {{effort}} ({{effort-source}}){{extra-args}}.
|
|
6
|
+
|
|
7
|
+
First load the code-review skill with the read tool: {{skill}}. Then follow its
|
|
8
|
+
SINGLE-PASS FLOW for effort {{effort}}: review the diff yourself in this
|
|
9
|
+
session — read it, surface candidates against the skill's rubric, self-verify
|
|
10
|
+
them (medium/high), and report via the `review_report` tool. No subagent
|
|
11
|
+
fan-out at this level: do NOT dispatch finder or verifier agents, even though
|
|
12
|
+
the `subagent` tool may be in your session toolset.
|
|
13
|
+
{{loop-note}}
|
|
14
|
+
Verification guidance (the skill's `--fix` flow consumes it):
|
|
15
|
+
|
|
16
|
+
{{verify}}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "/simplify trigger — SINGLE-PASS mode (angles worked inline, no fan-out)"
|
|
2
|
+
description: "/code-simplify trigger — SINGLE-PASS mode (angles worked inline, no fan-out)"
|
|
3
3
|
vars: [target, reasons, scope-label, too-large, git-command, context-package, skill, verify]
|
|
4
4
|
---
|
|
5
5
|
Clean up the changed code now. Target: {{target}}.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: review
|
|
3
|
-
description: "Review the current diff, or a PR number/branch/path target, for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium:
|
|
2
|
+
name: code-review
|
|
3
|
+
description: "Review the current diff, or a PR number/branch/path target, for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium/high: single-pass in-session review — medium precision, high recall; xhigh/max: subagent fan-out deep sweep). Fresh reverse of CC `/review` (its own name there is `code-review`), re-verified against CLI v2.1.261 (2026-09-05; originally reversed from v2.1.223). Effort semantics: medium = precision, high+ = recall. Pass --fix to apply, --loop to cycle fix→re-review until no P0/P1 findings remain, --comment to post findings (GitHub inline / GitLab MR note), --share to publish a review page."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
<!--
|
|
@@ -10,6 +10,19 @@ description: "Review the current diff, or a PR number/branch/path target, for co
|
|
|
10
10
|
earlier v2.1.220 reconstruction. Every section below was located in the
|
|
11
11
|
extracted strings (cc_strings_223.txt) and verified.
|
|
12
12
|
|
|
13
|
+
── v2.1 redesign: effort split (single-pass default) ──
|
|
14
|
+
- low/medium/high → SINGLE-PASS FLOW in the main session (no subagents):
|
|
15
|
+
rubric-ported flag criteria, P0–P3 priorities, in-session self-verify.
|
|
16
|
+
Rationale: the medium+ fan-out pipeline (8–10 finder subprocesses +
|
|
17
|
+
grouped verifiers) cost tens of minutes per run and returned zero
|
|
18
|
+
findings when spawned subprocesses failed to boot — unacceptable ROI
|
|
19
|
+
for the default path.
|
|
20
|
+
- xhigh/max keep the fan-out pipeline unchanged (opt-in deep sweep).
|
|
21
|
+
- NEW --loop: extension-driven fix→re-review rounds (≤ maxTurns.loop,
|
|
22
|
+
default 3) until no P0/P1 findings remain; blocking decisions read
|
|
23
|
+
the structured review_report JSON (never markdown scraping).
|
|
24
|
+
- review_report findings gained an optional `priority` (P0–P3).
|
|
25
|
+
|
|
13
26
|
── RE-VERIFIED against CLI v2.1.261 (bin/claude.exe raw bytes, 2026-09-05) ──
|
|
14
27
|
- The 2.1.217-era background Workflow (phases Scope/Find/Verify/Sweep/
|
|
15
28
|
Synthesize) is GONE — the phase prompts now live inline in the skill
|
|
@@ -61,9 +74,11 @@ description: "Review the current diff, or a PR number/branch/path target, for co
|
|
|
61
74
|
- Fixed-later obligation (CC Q8m): later fixes in the session must
|
|
62
75
|
re-report findings with updated outcome.
|
|
63
76
|
|
|
64
|
-
Invocation: /review [low|medium|high|xhigh|max] [--fix] [--comment] [--share] [<target>]
|
|
77
|
+
Invocation: /code-review [low|medium|high|xhigh|max] [--fix] [--loop] [--comment] [--share] [<target>]
|
|
65
78
|
target = Class#method | file path | PR number | branch name
|
|
66
|
-
|
|
79
|
+
--loop = extension-driven fix→re-review rounds (single-pass levels only,
|
|
80
|
+
≤ maxTurns.loop, default 3) until no P0/P1 findings remain
|
|
81
|
+
With no level given, the /code-review HANDLER reuses the last level you
|
|
67
82
|
typed (CC 2.1.223 codeReviewLastEffort); the skill always receives a
|
|
68
83
|
concrete level.
|
|
69
84
|
(CC also supports `ultra` — deep multi-agent review in the cloud.
|
|
@@ -82,6 +97,8 @@ description: "Review the current diff, or a PR number/branch/path target, for co
|
|
|
82
97
|
Markdown as text.
|
|
83
98
|
2. Fan-out — CC uses the Agent tool; Pi uses the `subagent` tool
|
|
84
99
|
(mode: parallel), or runs angles sequentially if unavailable.
|
|
100
|
+
v2.1: fan-out is the XHIGH/MAX path only — low/medium/high
|
|
101
|
+
run as a single pass in the main session (no subagents).
|
|
85
102
|
3. Verify — CC uses the Agent tool; Pi uses `subagent` for the
|
|
86
103
|
independent verify agent (fallback: self-check).
|
|
87
104
|
4. Workflow — CC 2.1.217 routed high/xhigh/max to a background Workflow
|
|
@@ -101,8 +118,9 @@ description: "Review the current diff, or a PR number/branch/path target, for co
|
|
|
101
118
|
mirrors both fallbacks (see the --comment section).
|
|
102
119
|
|
|
103
120
|
Prerequisite: the `subagent` tool (@fyeeme/pi-subagents; parallel mode) for
|
|
104
|
-
|
|
105
|
-
for --share. low
|
|
121
|
+
xhigh/max only (finder/verifier/gap-hunt fan-out). lavish-axi
|
|
122
|
+
for --share. low/medium/high run standalone in this session
|
|
123
|
+
(no subagents).
|
|
106
124
|
-->
|
|
107
125
|
|
|
108
126
|
You are reviewing the current diff for correctness bugs and reuse /
|
|
@@ -111,22 +129,30 @@ altitude, and conventions findings when the output cap forces a cut.
|
|
|
111
129
|
|
|
112
130
|
## Effort levels
|
|
113
131
|
|
|
114
|
-
| Level |
|
|
132
|
+
| Level | Path | Intent | Verify | Cap |
|
|
115
133
|
|-------|--------|--------|-----------|------------|
|
|
116
|
-
| low (default) | quick scan | no |
|
|
117
|
-
| medium | **precision** — surface only findings a maintainer would act on |
|
|
118
|
-
| high | **recall** — catch every real bug a careful reviewer would; **err on the side of surfacing** |
|
|
119
|
-
| xhigh | recall + **gap-hunt** |
|
|
120
|
-
| max |
|
|
134
|
+
| low (default) | SINGLE-PASS | quick scan | no | `min(files_changed, 4)` |
|
|
135
|
+
| medium | SINGLE-PASS | **precision** — surface only findings a maintainer would act on | self-verify (in-session) | 8 |
|
|
136
|
+
| high | SINGLE-PASS | **recall** — catch every real bug a careful reviewer would; **err on the side of surfacing** | self-verify (in-session) | 10 |
|
|
137
|
+
| xhigh | FAN-OUT (below) | recall + **gap-hunt** | independent verifier agents (grouped) | `{5, 8, 15, true}` |
|
|
138
|
+
| max | FAN-OUT(同 xhigh) | 同 xhigh | 同 xhigh | 同 xhigh |
|
|
139
|
+
|
|
140
|
+
**low/medium/high never dispatch subagents** — one pass in this session:
|
|
141
|
+
read the diff (Turn 1), surface candidates against the rubric (Turn 2),
|
|
142
|
+
self-verify them (Turn 3, medium/high only), report (Turn 4). This is the
|
|
143
|
+
default path: the 8–10 finder + grouped-verifier pipeline cost tens of
|
|
144
|
+
minutes per run and twice produced zero findings when spawned subprocesses
|
|
145
|
+
failed to boot — unacceptable ROI for a daily-driver review.
|
|
121
146
|
|
|
122
147
|
**max 与 xhigh 结构相同**:fan-out / verify / sweep 完全一致,差别仅在模型 reasoning effort(CC v2.1.226 注释实证:`max → same structure as xhigh (the API reasoning effort differs, not the fan-out)`)。若运行时不支持调节 reasoning effort,max 在结构上退化为 xhigh——不要因档名而期待更多 fan-out。
|
|
123
148
|
|
|
124
|
-
The quad tuple parameterizes the
|
|
149
|
+
The quad tuple parameterizes the XHIGH/MAX fan-out only (CC inline semantics,
|
|
150
|
+
verified 2.1.227):
|
|
125
151
|
|
|
126
|
-
- `correctnessAngles` —
|
|
127
|
-
- `perAngle` — candidate cap per finder (
|
|
128
|
-
- `maxFindings` — the report cap after verify (
|
|
129
|
-
- `sweep` — whether Phase 3 gap-hunt runs (
|
|
152
|
+
- `correctnessAngles` — 5 at xhigh/max (angles A–E all run).
|
|
153
|
+
- `perAngle` — candidate cap per finder (8).
|
|
154
|
+
- `maxFindings` — the report cap after verify (15).
|
|
155
|
+
- `sweep` — whether Phase 3 gap-hunt runs (≤ 8 new candidates).
|
|
130
156
|
|
|
131
157
|
Each finder surfaces up to `perAngle` candidate findings with `file`, `line`, a
|
|
132
158
|
one-line `summary`, a ≤60-char `short_summary`, and a concrete
|
|
@@ -177,52 +203,123 @@ actions based on it>
|
|
|
177
203
|
```
|
|
178
204
|
|
|
179
205
|
Embed this block verbatim at the top of **every** finder / verifier / gap-hunt
|
|
180
|
-
subagent prompt. Subagents do not re-discover the diff or
|
|
181
|
-
target argument travels as a scope constraint only, never as an
|
|
182
|
-
a subagent.
|
|
206
|
+
subagent prompt (XHIGH/MAX FLOW). Subagents do not re-discover the diff or
|
|
207
|
+
CLAUDE.md; the target argument travels as a scope constraint only, never as an
|
|
208
|
+
instruction to a subagent. In the SINGLE-PASS FLOW, keep the assembled block
|
|
209
|
+
as your own working notes — conventions come from it, not from re-discovery.
|
|
183
210
|
|
|
184
211
|
---
|
|
185
212
|
|
|
186
|
-
#
|
|
213
|
+
# SINGLE-PASS FLOW (default: low / medium / high — no subagents)
|
|
214
|
+
|
|
215
|
+
You review the diff yourself, in this session. Do NOT dispatch finder or
|
|
216
|
+
verifier agents at these levels, even if the `subagent` tool is available.
|
|
187
217
|
|
|
188
|
-
`low
|
|
218
|
+
- `low` — 1 diff pass, no self-verify, cap `min(files_changed, 4)`.
|
|
219
|
+
- `medium` — 1 pass + self-verify, cap 8, **precision**.
|
|
220
|
+
- `high` — 1 pass + self-verify, cap 10, **recall**.
|
|
189
221
|
|
|
190
222
|
## Turn 1 — read
|
|
191
223
|
|
|
192
224
|
One tool call: read the unified diff (`git diff @{upstream}...HEAD; git diff HEAD`
|
|
193
225
|
to cover both committed and uncommitted changes, or `git diff main...HEAD` / the
|
|
194
|
-
target passed as an argument).
|
|
195
|
-
`__tests__/`, `*_test.*`, `*.test.*`, `fixtures/`, `testdata/`) —
|
|
196
|
-
changes are not reviewed at
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
226
|
+
target passed as an argument). At low, skip test/fixture hunks (`test/`,
|
|
227
|
+
`spec/`, `__tests__/`, `*_test.*`, `*.test.*`, `fixtures/`, `testdata/`) —
|
|
228
|
+
test-file changes are not reviewed at that level; medium/high include them.
|
|
229
|
+
Then read the enclosing function for each nontrivial hunk; the applicable
|
|
230
|
+
CLAUDE.md conventions are already pinned in the Phase 0.5 scope block.
|
|
231
|
+
|
|
232
|
+
## Turn 2 — candidates (the rubric)
|
|
233
|
+
|
|
234
|
+
Work the finder angles inline — their definitions live in the XHIGH/MAX FLOW
|
|
235
|
+
below and are shared with the subagent definitions:
|
|
236
|
+
|
|
237
|
+
- **low** — Angle A over the hunks only: runtime-correctness bugs visible
|
|
238
|
+
from the hunk alone (inverted/wrong condition, off-by-one, null/undefined
|
|
239
|
+
deref where adjacent lines show the value can be absent, removed guard,
|
|
240
|
+
falsy-zero check, missing `await`, wrong-variable copy-paste, error
|
|
241
|
+
swallowed in a catch that should propagate), plus new code duplicating an
|
|
242
|
+
existing helper visible in the diff context, plus dead code the diff leaves
|
|
243
|
+
behind. Do **not** flag style, naming, perf, missing tests, or anything
|
|
244
|
+
outside the hunk. If you have fewer than the cap, do one more pass focused
|
|
245
|
+
on the largest changed file and on any **removed** code blocks. Output
|
|
246
|
+
exactly `(none)` only if the diff is trivially correct after that pass.
|
|
247
|
+
- **medium** — Angles A, B, C, then a quick Reuse / Simplification /
|
|
248
|
+
Efficiency pass over the changed code.
|
|
249
|
+
- **high** — the full angle set: A–E, then Reuse / Simplification /
|
|
250
|
+
Efficiency / Altitude / Conventions.
|
|
251
|
+
|
|
252
|
+
Flag issues that (rubric ported from the reference /review implementation):
|
|
253
|
+
|
|
254
|
+
1. Meaningfully impact the accuracy, performance, security, or
|
|
255
|
+
maintainability of the code.
|
|
256
|
+
2. Are discrete and actionable (not general issues or multiple combined
|
|
257
|
+
issues).
|
|
258
|
+
3. Don't demand rigor inconsistent with the rest of the codebase.
|
|
259
|
+
4. Were introduced in the changes being reviewed (not pre-existing bugs).
|
|
260
|
+
5. The author would likely fix if made aware of them.
|
|
261
|
+
6. Don't rely on unstated assumptions about the codebase or the author's
|
|
262
|
+
intent.
|
|
263
|
+
|
|
264
|
+
Every candidate carries `file`, `line`, `category`, a one-line `summary`, a
|
|
265
|
+
≤60-char `short_summary`, a concrete `failure_scenario`, and a **priority**
|
|
266
|
+
(`--loop` treats P0/P1 as blocking):
|
|
267
|
+
|
|
268
|
+
- **P0** — data loss, security hole, crash on a main path, broken build.
|
|
269
|
+
- **P1** — real bug on a plausible path; broken invariant with visible
|
|
270
|
+
effect.
|
|
271
|
+
- **P2** — worthwhile cleanup (duplication, wasted work, wrong altitude) or
|
|
272
|
+
an uncertain-trigger correctness issue.
|
|
273
|
+
- **P3** — nice-to-have.
|
|
274
|
+
|
|
275
|
+
Correctness outranks cleanup when the cap forces a cut.
|
|
276
|
+
|
|
277
|
+
## Turn 3 — self-verify (medium / high; low skips)
|
|
278
|
+
|
|
279
|
+
Re-read every candidate against the code once, in this session:
|
|
280
|
+
|
|
281
|
+
- Drop anything whose `failure_scenario` you cannot make concrete.
|
|
282
|
+
- Set the verdict: **`CONFIRMED`** — you can name the inputs/state that
|
|
283
|
+
trigger it and the wrong output or crash (quote the line); **`PLAUSIBLE`**
|
|
284
|
+
— the mechanism is real but the trigger is uncertain (timing, env,
|
|
285
|
+
config); state what would confirm it.
|
|
286
|
+
- **`PLAUSIBLE` by default** — do not drop a candidate for being
|
|
287
|
+
"speculative" or "depends on runtime state" when the state is realistic:
|
|
288
|
+
concurrency races, nil/undefined on a rare-but-reachable path (error
|
|
289
|
+
handler, cold cache, missing optional field), falsy-zero treated as
|
|
290
|
+
missing, off-by-one on a boundary the code does not exclude, retry storms
|
|
291
|
+
/ partial failures, regex/allowlist that lost an anchor.
|
|
292
|
+
- At medium (precision), additionally drop what a maintainer would not act
|
|
293
|
+
on. At high (recall), keep every surviving candidate — a missed bug ships.
|
|
294
|
+
|
|
295
|
+
## Turn 4 — report
|
|
296
|
+
|
|
297
|
+
Report via the `review_report` tool exactly as the Output section below
|
|
298
|
+
specifies, with `fanned_out: false` (honesty: this was a single-pass
|
|
299
|
+
self-review). At low the candidates ARE the findings (unverified — leave
|
|
300
|
+
`verdict` unset so the reader can discount them); if the `review_report` tool
|
|
301
|
+
is unavailable, print the findings as text (one line per finding:
|
|
302
|
+
`path/to/file.ext:123 — 问题与失败后果`), `(none)` when empty.
|
|
303
|
+
|
|
304
|
+
## Loop fixing (--loop)
|
|
305
|
+
|
|
306
|
+
When the trigger message says loop fixing is armed, the extension takes over
|
|
307
|
+
after your report: it reads the newest `review_report` JSON under
|
|
308
|
+
`.pi/review/`, and while P0/P1 findings remain it sends a fix prompt (apply
|
|
309
|
+
them per the --fix section's rules), waits, then asks you to re-run this
|
|
310
|
+
single-pass flow. Treat each re-review as a fresh pass with a fresh
|
|
311
|
+
`report_id` and an honest fresh findings list — do not rubber-stamp the
|
|
312
|
+
previous run.
|
|
215
313
|
|
|
216
314
|
---
|
|
217
315
|
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
## Phase 1 — Find candidates (single pass or parallel fan-out)
|
|
316
|
+
# XHIGH/MAX FLOW (deep sweep: fan-out + verify)
|
|
221
317
|
|
|
222
|
-
|
|
223
|
-
finder agents in a single batch
|
|
224
|
-
|
|
225
|
-
|
|
318
|
+
Reached only at effort xhigh/max — low/medium/high use the SINGLE-PASS FLOW
|
|
319
|
+
above. Launch finder agents through the `subagent` tool in a single batch
|
|
320
|
+
(mode: parallel) so they run concurrently; if it is unavailable, do not fake
|
|
321
|
+
the fan-out — work the angles yourself in sequence in this same context, or
|
|
322
|
+
report that the subagent capability is unavailable.
|
|
226
323
|
|
|
227
324
|
**Checking `subagent` availability** — wherever this skill says "if the
|
|
228
325
|
`subagent` tool is available", decide from THIS session's tool list, never by
|
|
@@ -255,15 +352,11 @@ every finder batch:
|
|
|
255
352
|
before Phase 2 (or fold it into the xhigh/max gap-hunt), and note the
|
|
256
353
|
re-dispatch in the report.
|
|
257
354
|
|
|
258
|
-
**Finder allocation** (CC inline, verified 2.1.227):
|
|
259
|
-
angles
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
finder each for Reuse, Simplification, Efficiency + one Altitude + one
|
|
264
|
-
Conventions.
|
|
265
|
-
- **xhigh / max** (5 correctness angles): **10 finders** — A, B, C, D, E + the
|
|
266
|
-
same 3 cleanup finders + Altitude + Conventions.
|
|
355
|
+
**Finder allocation** (CC inline, verified 2.1.227): xhigh/max run all five
|
|
356
|
+
correctness angles — **10 finders**: A, B, C, D, E + one finder each for
|
|
357
|
+
Reuse, Simplification, Efficiency + one Altitude + one Conventions. The quad
|
|
358
|
+
tuple's angles are taken **in order A→E** (`slice(0, N)` — do not hand-pick
|
|
359
|
+
angles; that makes runs unreproducible).
|
|
267
360
|
|
|
268
361
|
Each cleanup angle (Reuse / Simplification / Efficiency) gets its own finder;
|
|
269
362
|
Altitude and Conventions are independent finders. Never silently drop an
|
|
@@ -410,12 +503,11 @@ optional field), falsy-zero treated as missing, off-by-one on a boundary the
|
|
|
410
503
|
code does not exclude, retry storms / partial failures, regex/allowlist that
|
|
411
504
|
lost an anchor. These are PLAUSIBLE.
|
|
412
505
|
|
|
413
|
-
**Recall bias
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
side of surfacing hardest there.
|
|
506
|
+
**Recall bias** — a single non-REFUTED verdict keeps the candidate: do NOT
|
|
507
|
+
drop it on uncertainty ("speculative", "depends on runtime state"). This
|
|
508
|
+
flow is the recall contract of xhigh/max — a missed bug ships, so err on the
|
|
509
|
+
side of surfacing hardest here. (Medium's precision filter lives in the
|
|
510
|
+
single-pass self-verify; it never reaches this flow.)
|
|
419
511
|
|
|
420
512
|
**REFUTED** only when constructible from the code: factually wrong (quote the
|
|
421
513
|
actual line); provably impossible (type/constant/invariant — show it); already
|
|
@@ -452,8 +544,6 @@ Feed anything it finds back through Phase 2 verify before keeping it. If the
|
|
|
452
544
|
`subagent` tool is unavailable, take one self-sweep instead and note the
|
|
453
545
|
gap-hunt was self-run (lacks the independent fresh-eyes benefit).
|
|
454
546
|
|
|
455
|
-
At **high and below**, skip Phase 3.
|
|
456
|
-
|
|
457
547
|
## Output
|
|
458
548
|
|
|
459
549
|
Report the findings via the `review_report` tool (this extension's counterpart
|
|
@@ -471,7 +561,9 @@ or publish an artifact of the review — the tool call is the report");
|
|
|
471
561
|
Each finding in the array carries: `file`, `line` (optional), `category`
|
|
472
562
|
(`correctness` / `reuse` / `simplification` / `efficiency` / `altitude` /
|
|
473
563
|
`conventions`, or a more specific slug like `test-coverage`), `verdict`
|
|
474
|
-
(`CONFIRMED` / `PLAUSIBLE`), `
|
|
564
|
+
(`CONFIRMED` / `PLAUSIBLE`), `priority` (`P0`–`P3`; single-pass levels
|
|
565
|
+
always set it — `--loop` treats P0/P1 as blocking; xhigh/max may omit it),
|
|
566
|
+
`short_summary` (≤60 字符、纯声明——去掉理由与
|
|
475
567
|
后果,汇总表概述列优先使用它;示例:`"off-by-one in loop bound"`),
|
|
476
568
|
`summary` (一行中文,含理由与后果,详情块使用), `failure_scenario`
|
|
477
569
|
(concrete input/state → wrong output/crash; for cleanup findings, the
|
|
@@ -498,7 +590,7 @@ the files by id.
|
|
|
498
590
|
`outcome` 作为标识符保留英文 token。
|
|
499
591
|
|
|
500
592
|
**`fanned_out` 诚实** — 准确设置:仅当多智能体 fan-out 真的跑起来(subagent
|
|
501
|
-
finder + verify agent)才为 `true`;low
|
|
593
|
+
finder + verify agent,xhigh/max)才为 `true`;low/medium/high 单遍或任何自审降级为 `false`。该
|
|
502
594
|
字段会出现在报告表头,让读者不被误导(替代旧的 Single-pass honesty 小节)。
|
|
503
595
|
|
|
504
596
|
**降级** — 若 `review_report` 工具未注册(这份 SKILL.md 跑在 pi-review 扩展之外),
|
|
@@ -508,7 +600,9 @@ finder + verify agent)才为 `true`;low effort 或任何单遍/自审降级
|
|
|
508
600
|
|
|
509
601
|
## Applying fixes (--fix)
|
|
510
602
|
|
|
511
|
-
The `--fix` flag was passed
|
|
603
|
+
The `--fix` flag was passed (the extension-driven `--loop` sends the same
|
|
604
|
+
fix prompts between re-review passes — follow them identically). After
|
|
605
|
+
producing the findings list, apply the
|
|
512
606
|
findings to the working tree instead of stopping at the report: fix each one
|
|
513
607
|
directly — correctness bugs and reuse/simplification/efficiency cleanups alike.
|
|
514
608
|
Skip any finding whose fix would change intended behavior, require changes well
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: simplify
|
|
3
|
-
description: "Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /review for that. v3 (from Claude Code CLI v2.1.227, symbol-level verified; re-verified against v2.1.261 on 2026-09-05 — bodies unchanged except Altitude) — 4 cleanup agents fan out in parallel when context allows, else a single-pass inline cleanup; either way the fixes are applied, verified against the project's check command, and auto-reverted on failure, then reported as structured outcomes via review_report."
|
|
2
|
+
name: code-simplify
|
|
3
|
+
description: "Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that. v3 (from Claude Code CLI v2.1.227, symbol-level verified; re-verified against v2.1.261 on 2026-09-05 — bodies unchanged except Altitude) — 4 cleanup agents fan out in parallel when context allows, else a single-pass inline cleanup; either way the fixes are applied, verified against the project's check command, and auto-reverted on failure, then reported as structured outcomes via review_report."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
<!--
|
|
7
7
|
Origin: Claude Code built-in skill `/simplify` (CLI v2.1.227), reverse-
|
|
8
|
-
engineered from bin/claude.exe raw bytes. Pi registers it as /simplify.
|
|
8
|
+
engineered from bin/claude.exe raw bytes. Pi registers it as /code-simplify.
|
|
9
9
|
|
|
10
10
|
Lineage:
|
|
11
11
|
v2.1.220 → the first reconstruction (v1)
|
|
@@ -26,7 +26,7 @@ description: "Review the changed code for reuse, simplification, efficiency, and
|
|
|
26
26
|
behavior"; "Quality only — it does not hunt for bugs; use
|
|
27
27
|
/code-review for that") and the Agent-tool fan-out ("all in a
|
|
28
28
|
single message so they run concurrently") are unchanged.
|
|
29
|
-
The /code-review↔/simplify division of labor is now stated
|
|
29
|
+
The /code-review↔/code-simplify division of labor is now stated
|
|
30
30
|
explicitly in both skills upstream — same as here.
|
|
31
31
|
|
|
32
32
|
CC 2.1.227 empirical evidence (symbol-level, extracted from bin/claude.exe):
|
|
@@ -46,9 +46,9 @@ description: "Review the changed code for reuse, simplification, efficiency, and
|
|
|
46
46
|
FORKED_AGENT_DEFAULT_MAX_TURNS = 50 — mirrored as the subagent tool's
|
|
47
47
|
defaults (PI_MAX_CONCURRENT_SUBAGENTS env still overrides the ceiling).
|
|
48
48
|
|
|
49
|
-
Bundled: ships inside the pi-review extension (skills/simplify/SKILL.md).
|
|
49
|
+
Bundled: ships inside the pi-review extension (skills/code-simplify/SKILL.md).
|
|
50
50
|
|
|
51
|
-
Invocation: /simplify [<target>]
|
|
51
|
+
Invocation: /code-simplify [<target>]
|
|
52
52
|
target = file path | PR number | branch name
|
|
53
53
|
|
|
54
54
|
════════════════════════════════════════════════════════════════════════
|
|
@@ -69,10 +69,10 @@ description: "Review the changed code for reuse, simplification, efficiency, and
|
|
|
69
69
|
Dii. The cleanup agents' tool whitelist (read/grep/find/ls/bash) never
|
|
70
70
|
includes a fan-out tool, so recursion stays physically bounded
|
|
71
71
|
regardless of tool registration. The decision is made
|
|
72
|
-
DETERMINISTICALLY by the /simplify handler — it can
|
|
72
|
+
DETERMINISTICALLY by the /code-simplify handler — it can
|
|
73
73
|
read ctx.getContextUsage(), which a pure-prompt skill cannot — and announced
|
|
74
74
|
in the trigger message; this skill just provides the two mode bodies.
|
|
75
|
-
3. Command — CC: /simplify; Pi: /simplify.
|
|
75
|
+
3. Command — CC: /simplify; Pi: /code-simplify.
|
|
76
76
|
4. Dispatch — CC's lead model writes the 4 Agent prompts itself after its
|
|
77
77
|
visible Phase 0. Pi keeps the same TIMELINE but moves the packaging into
|
|
78
78
|
code: the trigger message carries the handler-resolved scope, the
|
|
@@ -94,9 +94,9 @@ description: "Review the changed code for reuse, simplification, efficiency, and
|
|
|
94
94
|
|
|
95
95
|
You are improving the quality of the changed code, not hunting for bugs. Review
|
|
96
96
|
it for reuse, simplification, efficiency, and altitude issues, then fix what you
|
|
97
|
-
find. Do not look for correctness bugs — that is what `/review` is for.
|
|
97
|
+
find. Do not look for correctness bugs — that is what `/code-review` is for.
|
|
98
98
|
|
|
99
|
-
The `/simplify` handler has already chosen the mode (PARALLEL or
|
|
99
|
+
The `/code-simplify` handler has already chosen the mode (PARALLEL or
|
|
100
100
|
SINGLE-PASS) from real context usage and announced it in the trigger message.
|
|
101
101
|
Follow the body that matches; do not fake the mode you weren't asked to run.
|
|
102
102
|
Both modes open the same way: the trigger message carries the handler-resolved
|
|
@@ -106,7 +106,7 @@ VISIBLE, model-run step before anything launches.
|
|
|
106
106
|
## Phase 0 — Gather the diff
|
|
107
107
|
|
|
108
108
|
When the trigger message carries a handler-resolved scope (it always does for
|
|
109
|
-
/simplify), use THAT: run the exact `git -C … diff …` command the trigger
|
|
109
|
+
/code-simplify), use THAT: run the exact `git -C … diff …` command the trigger
|
|
110
110
|
provides — the handler already ran the cascade (merge-base → HEAD → staged →
|
|
111
111
|
unstaged) to pick it — read the full diff, and write a 2–4 line change-intent
|
|
112
112
|
summary before anything else. Do not re-derive a different range. That summary
|
|
@@ -125,7 +125,7 @@ review that target instead. Treat this diff as the review scope.)
|
|
|
125
125
|
|
|
126
126
|
# PARALLEL MODE (context not near-full AND diff under the fan-out threshold AND fan-out available)
|
|
127
127
|
|
|
128
|
-
`/simplify → visible Phase 0 (read the diff, summarize) → subagent tool (parallel, 4 cleaner agents) → apply the fixes`
|
|
128
|
+
`/code-simplify → visible Phase 0 (read the diff, summarize) → subagent tool (parallel, 4 cleaner agents) → apply the fixes`
|
|
129
129
|
|
|
130
130
|
## Phase 1 — Review (4 cleanup agents in parallel)
|
|
131
131
|
|
|
@@ -184,7 +184,7 @@ Follow the shared **Phase 2** procedure at the end of this skill (snapshot → a
|
|
|
184
184
|
|
|
185
185
|
# SINGLE-PASS MODE (context near-full OR diff too large OR fan-out unavailable)
|
|
186
186
|
|
|
187
|
-
`/simplify → handler decided single-pass (reasons in the trigger message) → inline cleanup → apply the fixes`
|
|
187
|
+
`/code-simplify → handler decided single-pass (reasons in the trigger message) → inline cleanup → apply the fixes`
|
|
188
188
|
|
|
189
189
|
The handler decided against the 4-agent fan-out (context near-full, diff too
|
|
190
190
|
large, fan-out unavailable, or usage unmeasurable — the exact reasons are in
|
|
@@ -237,7 +237,7 @@ Follow the shared **Phase 2** procedure at the end of this skill (snapshot → a
|
|
|
237
237
|
# Phase 2 — Apply, verify, and report (shared by both modes)
|
|
238
238
|
|
|
239
239
|
Dedup findings that point at the same line or mechanism first. Then apply,
|
|
240
|
-
verify, and report. This safety net is what distinguishes `/simplify` from
|
|
240
|
+
verify, and report. This safety net is what distinguishes `/code-simplify` from
|
|
241
241
|
a blind cleanup: a finding is only "done" once it is applied AND the project
|
|
242
242
|
still verifies — otherwise it is reverted.
|
|
243
243
|
|
|
@@ -258,7 +258,7 @@ for any file in a subdirectory. If a fix CREATES a new file, record its path so
|
|
|
258
258
|
Step 3a can remove it on rollback (it has no baseline entry).
|
|
259
259
|
|
|
260
260
|
This baseline captures the working-tree state **including** the user's
|
|
261
|
-
uncommitted changes — reverting to it undoes only `/simplify`'s fixes,
|
|
261
|
+
uncommitted changes — reverting to it undoes only `/code-simplify`'s fixes,
|
|
262
262
|
never the user's diff. Do **not** use `git checkout` / `git restore` to revert:
|
|
263
263
|
that would discard the user's intended changes too.
|
|
264
264
|
|
package/src/config.ts
CHANGED
|
@@ -10,19 +10,20 @@
|
|
|
10
10
|
*
|
|
11
11
|
* {
|
|
12
12
|
* "maxTurns": {
|
|
13
|
-
* "subagent": 20, // per-call budget for each /review finder batch
|
|
14
|
-
* "gapHunt": 15, // budget for the /review Phase 3 gap-hunter
|
|
15
|
-
* "simplify": 15
|
|
13
|
+
* "subagent": 20, // per-call budget for each /code-review finder batch (xhigh/max)
|
|
14
|
+
* "gapHunt": 15, // budget for the /code-review Phase 3 gap-hunter (xhigh/max)
|
|
15
|
+
* "simplify": 15, // budget for each /code-simplify PARALLEL cleaner agent
|
|
16
|
+
* "loop": 3 // --loop fix→re-review round cap (single-pass levels)
|
|
16
17
|
* }
|
|
17
18
|
* }
|
|
18
19
|
*
|
|
19
20
|
* The defaults here are the numbers the bundled prompts and skills were
|
|
20
|
-
* written with (finder 20 / gap-hunt 15 / simplify 15). With no config file
|
|
21
|
+
* written with (finder 20 / gap-hunt 15 / simplify 15 / loop 3). With no config file
|
|
21
22
|
* — or with any key absent or invalid — the rendered instructions carry
|
|
22
23
|
* exactly those numbers, so absence of configuration changes nothing.
|
|
23
24
|
*
|
|
24
25
|
* Read at command time (like pi-subagents' maxConcurrency): an edited file
|
|
25
|
-
* takes effect on the next /review or /simplify without a restart. Malformed
|
|
26
|
+
* takes effect on the next /code-review or /code-simplify without a restart. Malformed
|
|
26
27
|
* files are ignored with a stderr warning (never fatal); unknown/garbage
|
|
27
28
|
* fields are dropped on read.
|
|
28
29
|
*/
|
|
@@ -33,16 +34,18 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
33
34
|
/** Settings file name (both layers). */
|
|
34
35
|
const CONFIG_FILE = "pi-review.json";
|
|
35
36
|
|
|
36
|
-
/** The
|
|
37
|
+
/** The dispatchable turn budgets, keyed by what they throttle. */
|
|
37
38
|
export interface TurnBudgets {
|
|
38
|
-
/** `maxTurns` set on each /review finder-batch `subagent` call. */
|
|
39
|
+
/** `maxTurns` set on each /code-review finder-batch `subagent` call. */
|
|
39
40
|
subagent: number;
|
|
40
|
-
/** `maxTurns` set on each /review Phase 2 verifier `subagent` call. */
|
|
41
|
+
/** `maxTurns` set on each /code-review Phase 2 verifier `subagent` call. */
|
|
41
42
|
verifier: number;
|
|
42
|
-
/** `maxTurns` set on the /review Phase 3 gap-hunt `subagent` call. */
|
|
43
|
+
/** `maxTurns` set on the /code-review Phase 3 gap-hunt `subagent` call. */
|
|
43
44
|
gapHunt: number;
|
|
44
|
-
/** `maxTurns` set on each /simplify PARALLEL cleaner `subagent` call. */
|
|
45
|
+
/** `maxTurns` set on each /code-simplify PARALLEL cleaner `subagent` call. */
|
|
45
46
|
simplify: number;
|
|
47
|
+
/** Max fix→re-review rounds when /code-review runs with --loop. */
|
|
48
|
+
loop: number;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
/** Built-in budgets — identical to the literals in prompts/ and skills/. */
|
|
@@ -51,6 +54,7 @@ export const DEFAULT_TURN_BUDGETS: TurnBudgets = {
|
|
|
51
54
|
verifier: 15,
|
|
52
55
|
gapHunt: 15,
|
|
53
56
|
simplify: 15,
|
|
57
|
+
loop: 3,
|
|
54
58
|
};
|
|
55
59
|
|
|
56
60
|
function globalPath(): string {
|
|
@@ -85,6 +89,8 @@ function readBudgetsFile(path: string): Partial<TurnBudgets> {
|
|
|
85
89
|
if (gapHunt !== undefined) out.gapHunt = gapHunt;
|
|
86
90
|
const simplify = sanitizeBudget(mt.simplify);
|
|
87
91
|
if (simplify !== undefined) out.simplify = simplify;
|
|
92
|
+
const loop = sanitizeBudget(mt.loop);
|
|
93
|
+
if (loop !== undefined) out.loop = loop;
|
|
88
94
|
return out;
|
|
89
95
|
} catch (err) {
|
|
90
96
|
const reason = err instanceof Error ? err.message : String(err);
|
package/src/diff.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function findGitRoot(from: string): string | null {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Normalize a /simplify target argument: trimmed, with an optional
|
|
33
|
+
/** Normalize a /code-simplify target argument: trimmed, with an optional
|
|
34
34
|
* path-prefix `@` PRESERVED — a real directory may itself start with `@`
|
|
35
35
|
* (e.g. node_modules/@scope/pkg), so the resolver tries the literal path
|
|
36
36
|
* first and only falls back to the @-stripped form when it does not exist. */
|
|
@@ -38,7 +38,7 @@ function normalizeTarget(target: string | undefined): string {
|
|
|
38
38
|
return (target ?? "").trim();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** Resolve the diff scope for a review/simplify target. Pure — unit-testable.
|
|
41
|
+
/** Resolve the diff scope for a code-review/code-simplify target. Pure — unit-testable.
|
|
42
42
|
*
|
|
43
43
|
* - target absent/unresolvable → the nearest git root of `cwd`, full diff.
|
|
44
44
|
* - target is a path → its nearest git root; the relative path inside that
|
package/src/dispatch.ts
CHANGED
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* change here — the templates and skills are the registration surface.
|
|
14
14
|
*/
|
|
15
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { isFanoutToolAllowed } from "@fyeeme/pi-subagents";
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
|
-
import * as os from "node:os";
|
|
20
19
|
import * as path from "node:path";
|
|
21
20
|
import { fileURLToPath } from "node:url";
|
|
22
21
|
import { loadTurnBudgets } from "./config.ts";
|
|
23
22
|
import { DIFF_SCOPES, buildContextPackage, getRepoDiff, verifyLine } from "./diff.ts";
|
|
23
|
+
import { extractLoopFlag, runLoopFixing } from "./loop.ts";
|
|
24
24
|
import { bundledSkillPath } from "./skills.ts";
|
|
25
25
|
import { parseGuards, selectVariant } from "./strategy.ts";
|
|
26
26
|
|
|
@@ -51,17 +51,19 @@ export function render(body: string, vars: Record<string, string>): string {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
// ---------------------------------------------------------------------------
|
|
54
|
-
// /review — effort-level code review (v1 command semantics, relocated)
|
|
54
|
+
// /code-review — effort-level code review (v1 command semantics, relocated)
|
|
55
55
|
// ---------------------------------------------------------------------------
|
|
56
56
|
|
|
57
|
-
/** Effort levels the /review command accepts (mirrors CC's effort enum). */
|
|
57
|
+
/** Effort levels the /code-review command accepts (mirrors CC's effort enum). */
|
|
58
58
|
export const REVIEW_LEVELS = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
59
59
|
export type ReviewLevel = (typeof REVIEW_LEVELS)[number];
|
|
60
60
|
|
|
61
61
|
const DEFAULT_LEVEL: ReviewLevel = "low";
|
|
62
62
|
|
|
63
|
-
/** Where the last explicitly-typed effort is persisted (CC 2.1.223
|
|
64
|
-
|
|
63
|
+
/** Where the last explicitly-typed effort is persisted (CC 2.1.223
|
|
64
|
+
* codeReviewLastEffort). Shares the config file so users have a single
|
|
65
|
+
* pi-review.json; the field is written alongside `maxTurns`, never over it. */
|
|
66
|
+
const STATE_FILE = path.join(getAgentDir(), "pi-review.json");
|
|
65
67
|
|
|
66
68
|
export type EffortSource = "explicit" | "last-used" | "default";
|
|
67
69
|
|
|
@@ -69,6 +71,12 @@ export type EffortSource = "explicit" | "last-used" | "default";
|
|
|
69
71
|
* Parse a leading effort level out of raw args; the remainder (flags + target)
|
|
70
72
|
* is returned verbatim. Pure — unit-testable.
|
|
71
73
|
*/
|
|
74
|
+
/** True when the effort level uses the xhigh/max finder/verifier fan-out;
|
|
75
|
+
* false for the single-pass levels (low/medium/high). Pure — unit-testable. */
|
|
76
|
+
export function usesFanout(level: ReviewLevel): boolean {
|
|
77
|
+
return level === "xhigh" || level === "max";
|
|
78
|
+
}
|
|
79
|
+
|
|
72
80
|
export function parseReviewArgs(args: string): { level: ReviewLevel | undefined; rest: string } {
|
|
73
81
|
const tokens = (args ?? "").trim().split(/\s+/).filter(Boolean);
|
|
74
82
|
if (tokens.length === 0) return { level: undefined, rest: "" };
|
|
@@ -108,15 +116,30 @@ function readLastEffort(): ReviewLevel | undefined {
|
|
|
108
116
|
}
|
|
109
117
|
function writeLastEffort(level: ReviewLevel): void {
|
|
110
118
|
try {
|
|
119
|
+
// The state field shares a file with user-authored config (maxTurns), so
|
|
120
|
+
// read-modify-write instead of overwriting, and never clobber a
|
|
121
|
+
// hand-edited file we cannot parse.
|
|
122
|
+
let existing: Record<string, unknown> = {};
|
|
123
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
124
|
+
let raw: unknown;
|
|
125
|
+
try {
|
|
126
|
+
raw = JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
|
|
127
|
+
} catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return;
|
|
131
|
+
existing = raw as Record<string, unknown>;
|
|
132
|
+
}
|
|
133
|
+
existing.codeReviewLastEffort = level;
|
|
111
134
|
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
112
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(
|
|
135
|
+
fs.writeFileSync(STATE_FILE, `${JSON.stringify(existing, null, 2)}\n`);
|
|
113
136
|
} catch {
|
|
114
137
|
/* ignore — non-critical */
|
|
115
138
|
}
|
|
116
139
|
}
|
|
117
140
|
|
|
118
141
|
// ---------------------------------------------------------------------------
|
|
119
|
-
// /simplify — cleanup fan-out with the declared parallel strategy
|
|
142
|
+
// /code-simplify — cleanup fan-out with the declared parallel strategy
|
|
120
143
|
// ---------------------------------------------------------------------------
|
|
121
144
|
|
|
122
145
|
/** v1 DIFF_TOO_LARGE_CHARS — kept for the single-pass "too large to read at
|
|
@@ -128,11 +151,11 @@ const DIFF_TOO_LARGE_CHARS = 400_000;
|
|
|
128
151
|
// ---------------------------------------------------------------------------
|
|
129
152
|
|
|
130
153
|
export function registerDispatcher(pi: ExtensionAPI): void {
|
|
131
|
-
pi.registerCommand("review", {
|
|
154
|
+
pi.registerCommand("code-review", {
|
|
132
155
|
description:
|
|
133
|
-
"Review the current diff using the review skill. Usage: /review [low|medium|high|xhigh|max] [--fix] [--comment] [--share] [<pr#>|<branch>|<path>]",
|
|
156
|
+
"Review the current diff using the code-review skill. low/medium/high review in a single pass in this session; xhigh/max fan out finder/verifier agents. Usage: /code-review [low|medium|high|xhigh|max] [--fix] [--loop] [--comment] [--share] [<pr#>|<branch>|<path>]",
|
|
134
157
|
getArgumentCompletions(prefix) {
|
|
135
|
-
const tokens = ["low", "medium", "high", "xhigh", "max", "--fix", "--comment", "--share"];
|
|
158
|
+
const tokens = ["low", "medium", "high", "xhigh", "max", "--fix", "--loop", "--comment", "--share"];
|
|
136
159
|
return tokens.filter((t) => t.startsWith(prefix)).map((t) => ({ label: t, value: t }));
|
|
137
160
|
},
|
|
138
161
|
async handler(args, ctx) {
|
|
@@ -141,42 +164,82 @@ export function registerDispatcher(pi: ExtensionAPI): void {
|
|
|
141
164
|
const lastUsed = explicit ? undefined : readLastEffort();
|
|
142
165
|
if (explicit) writeLastEffort(explicit); // remember the explicit level
|
|
143
166
|
const { level, source } = resolveEffort(explicit, lastUsed);
|
|
144
|
-
const { body } = loadTemplate("review.md");
|
|
145
167
|
const budgets = loadTurnBudgets();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
|
|
169
|
+
// Effort split: low/medium/high review in ONE pass in the main session
|
|
170
|
+
// (no subprocess fan-out); xhigh/max keep the finder/verifier pipeline.
|
|
171
|
+
const fanout = usesFanout(level);
|
|
172
|
+
|
|
173
|
+
// --loop is extension-level (drives fix→re-review rounds against the
|
|
174
|
+
// structured report), so it never reaches the skill text. It needs a
|
|
175
|
+
// single report turn to loop on — fan-out levels have no such turn.
|
|
176
|
+
const { wantLoop, rest: flagsRest } = extractLoopFlag(rest);
|
|
177
|
+
const loopArmed = wantLoop && !fanout;
|
|
178
|
+
if (wantLoop && fanout) {
|
|
179
|
+
ctx.ui.notify(
|
|
180
|
+
`/code-review: --loop applies to single-pass levels (low/medium/high) — ignored for ${level}.`,
|
|
181
|
+
"warning",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { body } = loadTemplate(fanout ? "review.parallel.md" : "review.single.md");
|
|
186
|
+
const shared = {
|
|
187
|
+
effort: level,
|
|
188
|
+
"effort-source": source,
|
|
189
|
+
"extra-args": flagsRest ? `; extra args: ${flagsRest}` : "",
|
|
190
|
+
skill: bundledSkillPath("code-review/SKILL.md"),
|
|
191
|
+
// Consumed by the skill's --fix flow (apply → verify → re-report).
|
|
192
|
+
verify: verifyLine(ctx.cwd),
|
|
193
|
+
};
|
|
194
|
+
void pi.sendUserMessage(
|
|
195
|
+
render(
|
|
196
|
+
body,
|
|
197
|
+
fanout
|
|
198
|
+
? {
|
|
199
|
+
...shared,
|
|
200
|
+
"finder-max-turns": String(budgets.subagent),
|
|
201
|
+
"verifier-max-turns": String(budgets.verifier),
|
|
202
|
+
"gap-hunt-max-turns": String(budgets.gapHunt),
|
|
203
|
+
}
|
|
204
|
+
: {
|
|
205
|
+
...shared,
|
|
206
|
+
// Told to the session so every finding carries a P0–P3 priority.
|
|
207
|
+
"loop-note": loopArmed
|
|
208
|
+
? `\nLoop fixing is armed: after your report, the extension drives up to ${budgets.loop} fix→re-review rounds until no P0/P1 findings remain — so tag every finding with a priority (P0–P3).\n`
|
|
209
|
+
: "",
|
|
210
|
+
},
|
|
211
|
+
),
|
|
158
212
|
);
|
|
213
|
+
if (loopArmed) {
|
|
214
|
+
await runLoopFixing(pi, ctx, {
|
|
215
|
+
level,
|
|
216
|
+
passes: budgets.loop,
|
|
217
|
+
// Must match where review_report writes (CONFIG_DIR_NAME,
|
|
218
|
+
// not necessarily ".pi") or the loop never finds a report.
|
|
219
|
+
reviewDir: path.join(ctx.cwd, CONFIG_DIR_NAME, "review"),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
159
222
|
},
|
|
160
223
|
});
|
|
161
224
|
|
|
162
|
-
pi.registerCommand("simplify", {
|
|
225
|
+
pi.registerCommand("code-simplify", {
|
|
163
226
|
description:
|
|
164
|
-
"Clean up the changed code (reuse/simplification/efficiency/altitude) using the simplify skill. Mode (parallel 4-agent vs single-pass) is decided from the strategy declared in prompts/simplify.*.md (context usage, diff size, fan-out availability); PARALLEL opens with a visible Phase 0 before the subagent tool launches the agents. Usage: /simplify [<target>]",
|
|
227
|
+
"Clean up the changed code (reuse/simplification/efficiency/altitude) using the code-simplify skill. Mode (parallel 4-agent vs single-pass) is decided from the strategy declared in prompts/simplify.*.md (context usage, diff size, fan-out availability); PARALLEL opens with a visible Phase 0 before the subagent tool launches the agents. Usage: /code-simplify [<target>]",
|
|
165
228
|
async handler(args, ctx) {
|
|
166
229
|
try {
|
|
167
230
|
// ctx.signal (undefined while idle) lets Esc abort an in-flight diff.
|
|
168
231
|
const outcome = await getRepoDiff(ctx.cwd, args?.trim() || undefined, undefined, ctx.signal);
|
|
169
232
|
if (outcome.kind === "no-repo") {
|
|
170
|
-
ctx.ui.notify(`/simplify: ${ctx.cwd} is not inside a git repo — nothing to clean up.`, "warning");
|
|
233
|
+
ctx.ui.notify(`/code-simplify: ${ctx.cwd} is not inside a git repo — nothing to clean up.`, "warning");
|
|
171
234
|
return;
|
|
172
235
|
}
|
|
173
236
|
if (outcome.kind === "git-error") {
|
|
174
|
-
ctx.ui.notify(`/simplify: git failed — ${outcome.message}`, "error");
|
|
237
|
+
ctx.ui.notify(`/code-simplify: git failed — ${outcome.message}`, "error");
|
|
175
238
|
return;
|
|
176
239
|
}
|
|
177
240
|
if (outcome.kind === "empty") {
|
|
178
241
|
ctx.ui.notify(
|
|
179
|
-
`/simplify: no changes found (checked unpushed+uncommitted vs @{upstream}, uncommitted vs HEAD, staged, unstaged) — nothing to clean up.`,
|
|
242
|
+
`/code-simplify: no changes found (checked unpushed+uncommitted vs @{upstream}, uncommitted vs HEAD, staged, unstaged) — nothing to clean up.`,
|
|
180
243
|
"warning",
|
|
181
244
|
);
|
|
182
245
|
return;
|
|
@@ -193,7 +256,7 @@ export function registerDispatcher(pi: ExtensionAPI): void {
|
|
|
193
256
|
});
|
|
194
257
|
const pct = usage && usage.percent != null ? `${Math.round(usage.percent)}%` : "?";
|
|
195
258
|
const target = args || "(whole diff)";
|
|
196
|
-
const skill = bundledSkillPath("simplify/SKILL.md");
|
|
259
|
+
const skill = bundledSkillPath("code-simplify/SKILL.md");
|
|
197
260
|
const scopeLabel = DIFF_SCOPES[outcome.scopeKind];
|
|
198
261
|
const contextPackage = buildContextPackage(outcome.diff, outcome.gitRoot, scopeLabel);
|
|
199
262
|
const verify = verifyLine(outcome.gitRoot);
|
|
@@ -231,7 +294,7 @@ export function registerDispatcher(pi: ExtensionAPI): void {
|
|
|
231
294
|
}),
|
|
232
295
|
);
|
|
233
296
|
} catch (err) {
|
|
234
|
-
ctx.ui.notify(`/simplify failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
297
|
+
ctx.ui.notify(`/code-simplify failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
235
298
|
}
|
|
236
299
|
},
|
|
237
300
|
});
|
package/src/loop.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/loop.ts — /code-review --loop: extension-driven fix→re-review cycles.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the standalone Codex-style review extension's loop fixing
|
|
5
|
+
* (review → blocking-check → fix → re-review, bounded), with one deliberate
|
|
6
|
+
* deviation: the blocking decision reads the structured `review_report` JSON
|
|
7
|
+
* the skill must have written under the project's pi config dir
|
|
8
|
+
* (<cwd>/.pi/review/ by default — CONFIG_DIR_NAME) instead of scraping the
|
|
9
|
+
* assistant's markdown. The tool call is the report, so the JSON is the
|
|
10
|
+
* reliable artifact; markdown scraping was only ever a fallback.
|
|
11
|
+
*
|
|
12
|
+
* Loop shape (single-pass levels only — low/medium/high):
|
|
13
|
+
* review turn → read newest report → OPEN P0/P1 findings (P0/P1 without a
|
|
14
|
+
* decided outcome — a fix turn's re-report marks its findings
|
|
15
|
+
* fixed/skipped/no_change_needed)?
|
|
16
|
+
* none → done
|
|
17
|
+
* some, rounds left → fix prompt (followUp) → idle → re-review prompt → next round
|
|
18
|
+
* some, rounds spent → stop with a safety-limit note
|
|
19
|
+
* Esc/abort or a missing report stops the loop.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from "node:fs";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { OUTCOME_VALUES } from "./tools/review_report.ts";
|
|
25
|
+
|
|
26
|
+
// --- pure helpers (unit-tested) ---------------------------------------------
|
|
27
|
+
|
|
28
|
+
export const BLOCKING_PRIORITIES = ["P0", "P1"] as const;
|
|
29
|
+
|
|
30
|
+
/** Strip a --loop flag out of the trailing args; report whether it was there.
|
|
31
|
+
* Pure — unit-testable. */
|
|
32
|
+
export function extractLoopFlag(rest: string): { wantLoop: boolean; rest: string } {
|
|
33
|
+
const tokens = (rest ?? "").split(/\s+/).filter(Boolean);
|
|
34
|
+
const kept = tokens.filter((t) => t !== "--loop");
|
|
35
|
+
return { wantLoop: kept.length !== tokens.length, rest: kept.join(" ") };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A blocking finding as surfaced back to the fix prompt. */
|
|
39
|
+
export interface BlockingFinding {
|
|
40
|
+
file: string;
|
|
41
|
+
line?: number;
|
|
42
|
+
priority: string;
|
|
43
|
+
summary: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Extract P0/P1 findings from a parsed review_report JSON (lenient: any
|
|
47
|
+
* shape mismatch → no findings rather than a throw). Pure — unit-testable. */
|
|
48
|
+
export function blockingFindings(report: unknown): BlockingFinding[] {
|
|
49
|
+
if (!report || typeof report !== "object" || Array.isArray(report)) return [];
|
|
50
|
+
const raw = (report as { findings?: unknown }).findings;
|
|
51
|
+
if (!Array.isArray(raw)) return [];
|
|
52
|
+
const out: BlockingFinding[] = [];
|
|
53
|
+
for (const f of raw) {
|
|
54
|
+
if (!f || typeof f !== "object" || Array.isArray(f)) continue;
|
|
55
|
+
const rec = f as Record<string, unknown>;
|
|
56
|
+
if (rec.priority !== "P0" && rec.priority !== "P1") continue;
|
|
57
|
+
if (typeof rec.file !== "string" || rec.file.length === 0) continue;
|
|
58
|
+
// A decided outcome (a fix turn re-reports its findings with one, per
|
|
59
|
+
// the skill's fixed-later obligation) un-blocks the finding —
|
|
60
|
+
// re-prompting a fixed/skipped/declined finding just burns rounds.
|
|
61
|
+
if (typeof rec.outcome === "string" && (OUTCOME_VALUES as readonly string[]).includes(rec.outcome)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
out.push({
|
|
65
|
+
file: rec.file,
|
|
66
|
+
line: typeof rec.line === "number" ? rec.line : undefined,
|
|
67
|
+
priority: rec.priority,
|
|
68
|
+
summary: typeof rec.summary === "string" ? rec.summary : "",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Newest `*.json` report under `dir` modified after `sinceMs`, or null.
|
|
75
|
+
* Missing/unreadable dir → null. Pure — unit-testable. */
|
|
76
|
+
export function latestReportFile(dir: string, sinceMs: number): string | null {
|
|
77
|
+
let entries: fs.Dirent[];
|
|
78
|
+
try {
|
|
79
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
let newest: { file: string; mtime: number } | null = null;
|
|
84
|
+
for (const e of entries) {
|
|
85
|
+
if (!e.isFile() || !e.name.endsWith(".json")) continue;
|
|
86
|
+
const file = path.join(dir, e.name);
|
|
87
|
+
try {
|
|
88
|
+
const mtime = fs.statSync(file).mtimeMs;
|
|
89
|
+
if (mtime <= sinceMs) continue;
|
|
90
|
+
if (!newest || mtime > newest.mtime) newest = { file, mtime };
|
|
91
|
+
} catch {
|
|
92
|
+
/* stat failed — skip this entry */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return newest?.file ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Parse a report JSON file; garbage → null (the loop must not crash on a
|
|
99
|
+
* half-written or hand-edited file). */
|
|
100
|
+
function readReport(file: string): unknown {
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- loop driver -------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/** Minimal session view for the quiescence wait — structural, so
|
|
111
|
+
* ExtensionCommandContext satisfies it and tests can drive the logic
|
|
112
|
+
* without a live session. */
|
|
113
|
+
export interface QuiescenceView {
|
|
114
|
+
isIdle(): boolean;
|
|
115
|
+
hasPendingMessages(): boolean;
|
|
116
|
+
waitForIdle(): Promise<void>;
|
|
117
|
+
signal?: { aborted: boolean };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Wait until the session is fully quiescent: nothing running AND nothing
|
|
121
|
+
* queued. A single `waitForIdle()` is NOT enough — it resolves at the first
|
|
122
|
+
* idle point even while follow-ups are still queued (a queued message does
|
|
123
|
+
* not flip `isIdle` until its run actually starts), which is exactly the
|
|
124
|
+
* window between `sendUserMessage(…, followUp)` and that turn's first
|
|
125
|
+
* token. Aborts return false; there is no timeout — Esc is the escape
|
|
126
|
+
* hatch, same as for the bare waitForIdle call. */
|
|
127
|
+
export async function waitForQuiescent(session: QuiescenceView): Promise<boolean> {
|
|
128
|
+
for (;;) {
|
|
129
|
+
if (session.signal?.aborted) return false;
|
|
130
|
+
if (!session.isIdle() || session.hasPendingMessages()) {
|
|
131
|
+
await session.waitForIdle();
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Poll until the review turn has started (idle → busy or a new assistant
|
|
139
|
+
* message appears), then wait for it to finish. Returns false on timeout
|
|
140
|
+
* or abort. Mirrors the reference extension's waitForLoopTurnToStart. */
|
|
141
|
+
async function waitForTurnSettled(ctx: ExtensionCommandContext, baselineAssistantId: string): Promise<boolean> {
|
|
142
|
+
const START_TIMEOUT_MS = 15_000;
|
|
143
|
+
const POLL_MS = 50;
|
|
144
|
+
const deadline = Date.now() + START_TIMEOUT_MS;
|
|
145
|
+
|
|
146
|
+
const lastAssistantId = (): string | undefined => {
|
|
147
|
+
const branch = ctx.sessionManager.getBranch();
|
|
148
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
149
|
+
const entry = branch[i]!;
|
|
150
|
+
if (entry.type === "message" && entry.message.role === "assistant") return entry.id;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
while (Date.now() < deadline) {
|
|
156
|
+
if (ctx.signal?.aborted) return false;
|
|
157
|
+
const current = lastAssistantId();
|
|
158
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages() || (current && current !== baselineAssistantId)) {
|
|
159
|
+
// Wait past every queued message, not merely to the next idle
|
|
160
|
+
// point — waitForIdle() alone returns inside the gap between
|
|
161
|
+
// queueing a followUp and its run actually starting.
|
|
162
|
+
return waitForQuiescent(ctx);
|
|
163
|
+
}
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function baselineAssistantId(ctx: ExtensionCommandContext): string {
|
|
170
|
+
const branch = ctx.sessionManager.getBranch();
|
|
171
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
172
|
+
const entry = branch[i]!;
|
|
173
|
+
if (entry.type === "message" && entry.message.role === "assistant") return entry.id;
|
|
174
|
+
}
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function fixPrompt(findings: BlockingFinding[]): string {
|
|
179
|
+
const list = findings
|
|
180
|
+
.map((f) => `- \`${f.file}${f.line != null ? `:${f.line}` : ""}\` [${f.priority}] ${f.summary}`)
|
|
181
|
+
.join("\n");
|
|
182
|
+
return [
|
|
183
|
+
"Fix the following blocking findings from the code review you just reported",
|
|
184
|
+
"(full failure scenarios are in the latest report JSON under .pi/review/):",
|
|
185
|
+
"",
|
|
186
|
+
list,
|
|
187
|
+
"",
|
|
188
|
+
"Apply minimal, surgical fixes — no drive-by refactors. Then re-report these",
|
|
189
|
+
"findings via the `review_report` tool with `outcome` set per finding",
|
|
190
|
+
"(fixed / skipped / no_change_needed) and run the verification guidance from",
|
|
191
|
+
"the review trigger message. Never leave the working tree verified-broken.",
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function reReviewPrompt(level: string): string {
|
|
196
|
+
return [
|
|
197
|
+
`Fixes applied. Re-run the code-review SINGLE-PASS flow for effort ${level} now`,
|
|
198
|
+
"— the diff has changed: re-resolve it, re-check the fixed locations and",
|
|
199
|
+
"sweep for regressions or newly exposed issues, then report via the",
|
|
200
|
+
"`review_report` tool again (fresh findings list, empty array if clean).",
|
|
201
|
+
].join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface LoopOptions {
|
|
205
|
+
/** The effort level the review runs at (echoed in re-review prompts). */
|
|
206
|
+
level: string;
|
|
207
|
+
/** Max fix→re-review rounds (config maxTurns.loop). */
|
|
208
|
+
passes: number;
|
|
209
|
+
/** Directory the review_report JSON files land in. */
|
|
210
|
+
reviewDir: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Drive the fix→re-review rounds after the FIRST review prompt has already
|
|
215
|
+
* been sent by the dispatcher. Each iteration reads the newest report (the
|
|
216
|
+
* first iteration sees the initial review's report, later ones the previous
|
|
217
|
+
* round's re-review) and sends at most one fix prompt. Runs at most `passes`
|
|
218
|
+
* fix→re-review rounds; the final iteration only reads the last re-review's
|
|
219
|
+
* verdict for the safety-limit message. Returns the number of fix rounds run.
|
|
220
|
+
*/
|
|
221
|
+
export async function runLoopFixing(
|
|
222
|
+
pi: ExtensionAPI,
|
|
223
|
+
ctx: ExtensionCommandContext,
|
|
224
|
+
options: LoopOptions,
|
|
225
|
+
): Promise<number> {
|
|
226
|
+
const { level, passes, reviewDir } = options;
|
|
227
|
+
const baseline = baselineAssistantId(ctx);
|
|
228
|
+
const loopStart = Date.now();
|
|
229
|
+
|
|
230
|
+
let fixes = 0;
|
|
231
|
+
for (;;) {
|
|
232
|
+
if (!(await waitForTurnSettled(ctx, baseline))) return fixes;
|
|
233
|
+
|
|
234
|
+
const reportFile = latestReportFile(reviewDir, loopStart);
|
|
235
|
+
if (reportFile === null) {
|
|
236
|
+
ctx.ui.notify("/code-review --loop: no review_report JSON found — stopping the loop.", "warning");
|
|
237
|
+
return fixes;
|
|
238
|
+
}
|
|
239
|
+
const findings = blockingFindings(readReport(reportFile));
|
|
240
|
+
if (findings.length === 0) {
|
|
241
|
+
ctx.ui.notify(
|
|
242
|
+
fixes === 0
|
|
243
|
+
? "/code-review --loop: no P0/P1 findings — nothing to fix, loop done."
|
|
244
|
+
: `/code-review --loop: clean after ${fixes} fix round(s) — no open P0/P1 findings remain.`,
|
|
245
|
+
"info",
|
|
246
|
+
);
|
|
247
|
+
return fixes;
|
|
248
|
+
}
|
|
249
|
+
if (fixes === passes) {
|
|
250
|
+
ctx.ui.notify(
|
|
251
|
+
`/code-review --loop: ${findings.length} P0/P1 finding(s) still open after ${passes} fix round(s) — safety limit reached, stopping.`,
|
|
252
|
+
"warning",
|
|
253
|
+
);
|
|
254
|
+
return fixes;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fixes++;
|
|
258
|
+
ctx.ui.notify(
|
|
259
|
+
`/code-review --loop: ${findings.length} blocking finding(s) — fixing (round ${fixes}/${passes})…`,
|
|
260
|
+
"info",
|
|
261
|
+
);
|
|
262
|
+
await pi.sendUserMessage(fixPrompt(findings), { deliverAs: "followUp" });
|
|
263
|
+
if (!(await waitForTurnSettled(ctx, baseline))) return fixes;
|
|
264
|
+
pi.sendUserMessage(reReviewPrompt(level), { deliverAs: "followUp" });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -31,12 +31,19 @@ import * as path from "node:path";
|
|
|
31
31
|
const VERDICT_VALUES = ["CONFIRMED", "PLAUSIBLE"] as const;
|
|
32
32
|
const Verdict = StringEnum(VERDICT_VALUES);
|
|
33
33
|
|
|
34
|
-
const OUTCOME_VALUES = ["fixed", "skipped", "no_change_needed"] as const;
|
|
35
34
|
/** CC ReportFindings `outcome` 三档(2.1.227 二进制实证)。fixed-later 再上报时更新。 */
|
|
35
|
+
export const OUTCOME_VALUES = ["fixed", "skipped", "no_change_needed"] as const;
|
|
36
36
|
const Outcome = StringEnum(OUTCOME_VALUES);
|
|
37
37
|
|
|
38
|
+
/** --loop 的 blocking 阈值:P0/P1 触发修复→再评审一轮(P2/P3 只入报告)。 */
|
|
39
|
+
const PRIORITY_VALUES = ["P0", "P1", "P2", "P3"] as const;
|
|
40
|
+
const Priority = StringEnum(PRIORITY_VALUES, {
|
|
41
|
+
description:
|
|
42
|
+
"优先级 P0(阻断,立刻修)/ P1(高)/ P2(中)/ P3(低)。--loop 循环修复以 P0/P1 为 blocking 阈值;省略视为 P2。",
|
|
43
|
+
});
|
|
44
|
+
|
|
38
45
|
// 供 SKILL-schema 同步测试引用(防漂移:SKILL 流程契约不得与常量脱节)。
|
|
39
|
-
export {
|
|
46
|
+
export { PRIORITY_VALUES, VERDICT_VALUES };
|
|
40
47
|
|
|
41
48
|
const Level = StringEnum([
|
|
42
49
|
"low",
|
|
@@ -59,6 +66,7 @@ const FindingParams = Type.Object({
|
|
|
59
66
|
"产生该发现的角度 slug:correctness / reuse / simplification / efficiency / altitude / conventions(或更具体如 test-coverage)。",
|
|
60
67
|
}),
|
|
61
68
|
verdict: Type.Optional(Verdict),
|
|
69
|
+
priority: Type.Optional(Priority),
|
|
62
70
|
short_summary: Type.Optional(
|
|
63
71
|
Type.String({
|
|
64
72
|
description:
|
|
@@ -108,6 +116,7 @@ type LooseFinding = {
|
|
|
108
116
|
line?: number;
|
|
109
117
|
category: string;
|
|
110
118
|
verdict?: string;
|
|
119
|
+
priority?: string;
|
|
111
120
|
short_summary?: string;
|
|
112
121
|
summary: string;
|
|
113
122
|
failure_scenario: string;
|
|
@@ -126,7 +135,10 @@ function sanitizeFinding(f: LooseFinding): { f: LooseFinding; note?: string } |
|
|
|
126
135
|
note = `(outcome "${outcome}" 非法,已归一化为 skipped)`;
|
|
127
136
|
outcome = "skipped";
|
|
128
137
|
}
|
|
129
|
-
|
|
138
|
+
// 非法 priority 静默丢弃(降至未标注),不影响该条 finding 存活。
|
|
139
|
+
const priority =
|
|
140
|
+
f.priority !== undefined && (PRIORITY_VALUES as readonly string[]).includes(f.priority) ? f.priority : undefined;
|
|
141
|
+
return { f: { ...f, outcome, priority }, note };
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
/**
|
|
@@ -155,6 +167,7 @@ interface FindingInput {
|
|
|
155
167
|
line?: number;
|
|
156
168
|
category: string;
|
|
157
169
|
verdict?: string;
|
|
170
|
+
priority?: string;
|
|
158
171
|
short_summary?: string;
|
|
159
172
|
summary: string;
|
|
160
173
|
failure_scenario: string;
|
|
@@ -202,13 +215,14 @@ function renderReport(p: ReportInput): string {
|
|
|
202
215
|
lines.push("|---|------|------|------|------|");
|
|
203
216
|
for (let i = 0; i < p.findings.length; i++) {
|
|
204
217
|
const f = p.findings[i]!;
|
|
205
|
-
|
|
218
|
+
const verdictCell = [f.priority, f.verdict].filter(Boolean).join(" · ");
|
|
219
|
+
lines.push(`| ${i + 1} | ${escapeCell(verdictCell)} | ${escapeCell(f.category)} | ${escapeCell(fmtLoc(f))} | ${escapeCell(f.short_summary ?? f.summary)} |`);
|
|
206
220
|
}
|
|
207
221
|
lines.push("");
|
|
208
222
|
lines.push("**详情**");
|
|
209
223
|
lines.push("");
|
|
210
224
|
p.findings.forEach((f, i) => {
|
|
211
|
-
const v = f.verdict ? ` *(${f.verdict})*` : "";
|
|
225
|
+
const v = [f.priority, f.verdict].filter(Boolean).length > 0 ? ` *(${[f.priority, f.verdict].filter(Boolean).join(" · ")})*` : "";
|
|
212
226
|
const out = f.outcome ? `\n修复结果:\`${f.outcome}\`` : "";
|
|
213
227
|
const note = f.note ? `\n${f.note}` : "";
|
|
214
228
|
lines.push(`**${i + 1}. ${fmtLoc(f)} — ${f.category}**${v}`);
|