pi-review-loop 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nico Bailon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,332 @@
1
+ <p>
2
+ <img src="review-loop.png" alt="pi-review-loop" width="1100">
3
+ </p>
4
+
5
+ # Pi Review Loop
6
+
7
+ Automated code review loop for pi. Repeatedly prompts the agent to review its own work until it confirms no issues remain.
8
+
9
+ ```
10
+ > implement the plan
11
+
12
+ [agent implements...]
13
+
14
+ Review mode (1/7) ← status appears in footer
15
+
16
+ [agent reviews, finds bug, fixes it]
17
+
18
+ Review mode (2/7)
19
+
20
+ [agent reviews again]
21
+
22
+ "No issues found."
23
+
24
+ Review mode ended: no issues found ← auto-exits
25
+ ```
26
+
27
+ ## Why
28
+
29
+ Agents make mistakes. They miss edge cases, introduce typos, forget error handling. Asking them to review their own code catches a surprising number of issues, but you have to remember to ask, and then ask again if they found something.
30
+
31
+ Reviewer Loop automates this:
32
+
33
+ **Auto-Trigger** - Detects phrases like "implement the plan" or the `/double-check` template. No need to manually activate.
34
+
35
+ **Persistent Loop** - After each response, sends a review prompt. If the agent found and fixed issues, it loops again. Only exits when the agent genuinely finds nothing.
36
+
37
+ **Smart Exit Detection** - Won't be fooled by "Fixed 3 issues. No further issues found." Detects when issues were fixed and keeps looping.
38
+
39
+ **Fully Configurable** - Every pattern is customizable. Change what triggers the loop, what exits it, and what prompt gets sent. Extend the defaults or replace them entirely.
40
+
41
+ ## Typical Workflow
42
+
43
+ The loop shines in two scenarios:
44
+
45
+ **Before implementing** — You've got a plan doc and want to sanity-check it against the actual codebase. Run `/double-check-plan` and let the agent compare the plan to what exists. It'll catch things like outdated assumptions, conflicting patterns, or unnecessary complexity. The funny thing is, it rarely finds everything on the first pass. Second pass catches different issues. Third pass, more still. That's the whole point of the loop.
46
+
47
+ **After implementing** — You just finished building a feature and want to catch bugs before calling it done. Run `/double-check` and the agent reviews its own work with fresh eyes. Typos, missed edge cases, forgotten error handling — it finds stuff you'd miss staring at the same code. Again, multiple passes tend to surface different issues each time.
48
+
49
+ The pattern is the same: keep reviewing until there's genuinely nothing left to find. The loop handles the "ask again" part automatically. You'll see `Review mode (2/7)` in the footer so you know it's working and how many passes it's done.
50
+
51
+ ## Install
52
+
53
+ **npm** (recommended):
54
+
55
+ ```bash
56
+ npm install -g pi-review-loop
57
+ ```
58
+
59
+ The extension is automatically copied to `~/.pi/agent/extensions/pi-review-loop/`.
60
+
61
+ **Or clone directly:**
62
+
63
+ ```bash
64
+ git clone https://github.com/nicobailon/pi-review-loop ~/.pi/agent/extensions/pi-review-loop
65
+ ```
66
+
67
+ Restart pi to load the extension. On activation, you'll see status in the footer:
68
+
69
+ ```
70
+ Review mode (2/7)
71
+ ```
72
+
73
+ ## Prompt Templates
74
+
75
+ The package includes two prompt templates that are automatically installed to `~/.pi/agent/prompts/` (if they don't already exist):
76
+
77
+ | Template | Command | Description |
78
+ |----------|---------|-------------|
79
+ | `double-check.md` | `/double-check` | Review code with fresh eyes, fix any issues found |
80
+ | `double-check-plan.md` | `/double-check-plan` | Review implementation plan against codebase |
81
+
82
+ These prompts are designed to work with the review loop:
83
+ - They include the "fresh eyes" phrase that auto-triggers the loop
84
+ - They instruct the agent to respond with "No issues found." when done (triggering exit)
85
+ - They tell the agent to end with "Fixed [N] issue(s). Ready for another review." when issues are fixed (continuing the loop)
86
+
87
+ **Manual installation** (if cloning instead of npm):
88
+
89
+ ```bash
90
+ cp ~/.pi/agent/extensions/pi-review-loop/prompts/*.md ~/.pi/agent/prompts/
91
+ ```
92
+
93
+ ## Quick Start
94
+
95
+ ### Automatic Activation
96
+
97
+ Just use trigger phrases naturally:
98
+
99
+ ```
100
+ > implement the plan
101
+ > implement the spec
102
+ > let's implement this plan
103
+ ```
104
+
105
+ Or use the `/double-check` prompt template.
106
+
107
+ ### Manual Activation
108
+
109
+ ```
110
+ /review-start
111
+ ```
112
+
113
+ Activates review mode and immediately sends the review prompt.
114
+
115
+ ### Check Status
116
+
117
+ ```
118
+ /review-status
119
+ ```
120
+
121
+ Shows whether review mode is active and current iteration.
122
+
123
+ ### Exit Early
124
+
125
+ ```
126
+ /review-exit
127
+ ```
128
+
129
+ Or just type something else. Any non-trigger input exits review mode.
130
+
131
+ ### Adjust Iterations
132
+
133
+ ```
134
+ /review-max 5
135
+ ```
136
+
137
+ Changes max iterations for current session.
138
+
139
+ ## Configuration
140
+
141
+ Configure in `~/.pi/agent/settings.json`. Works out of the box, but everything is customizable:
142
+
143
+ ```json
144
+ {
145
+ "reviewerLoop": {
146
+ "maxIterations": 7,
147
+ "reviewPrompt": "template:double-check",
148
+ "triggerPatterns": {
149
+ "mode": "extend",
150
+ "patterns": ["execute the plan"]
151
+ },
152
+ "exitPatterns": {
153
+ "mode": "extend",
154
+ "patterns": ["ship it", "ready to merge"]
155
+ },
156
+ "issuesFixedPatterns": {
157
+ "mode": "extend",
158
+ "patterns": ["addressed the following"]
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ ### Options
165
+
166
+ | Option | Description |
167
+ |--------|-------------|
168
+ | `maxIterations` | Max review prompts before auto-exit (default: 7) |
169
+ | `reviewPrompt` | The prompt to send each iteration |
170
+ | `triggerPatterns` | What activates review mode |
171
+ | `exitPatterns` | What indicates "review complete" |
172
+ | `issuesFixedPatterns` | What indicates issues were fixed (prevents false exits) |
173
+
174
+ ### Review Prompt Sources
175
+
176
+ Three formats for `reviewPrompt`:
177
+
178
+ | Format | Example | Description |
179
+ |--------|---------|-------------|
180
+ | Template | `"template:double-check"` | Loads `~/.pi/agent/prompts/double-check.md` |
181
+ | File | `"~/prompts/review.md"` | Loads from any file path |
182
+ | Inline | `"Review the code carefully..."` | Uses text directly |
183
+
184
+ Templates and files reload on each use. Edit them and changes take effect immediately.
185
+
186
+ ### Pattern Configuration
187
+
188
+ Each pattern setting accepts:
189
+
190
+ ```json
191
+ {
192
+ "mode": "extend",
193
+ "patterns": ["simple string", "/regex\\s+pattern/i"]
194
+ }
195
+ ```
196
+
197
+ **Modes:**
198
+ - `"extend"` (default, recommended) - Add your patterns to the built-in defaults
199
+ - `"replace"` - Use only your patterns, discard defaults entirely
200
+
201
+ **Pattern formats:**
202
+ - Simple string → auto-escaped, case-insensitive literal match
203
+ - `/pattern/flags` → full regex with custom flags
204
+
205
+ **Why extend mode is recommended:** The built-in defaults use sophisticated regex patterns to handle edge cases (e.g., distinguishing "No issues found" from "Issues found and fixed"). If you use replace mode, you take full responsibility for handling these nuances. Extend mode lets you add simple patterns like `"ship it"` while the defaults handle the tricky stuff.
206
+
207
+ ## Default Patterns
208
+
209
+ These are the built-ins (all customizable):
210
+
211
+ **Triggers:**
212
+ - "implement plan/spec", "implement the plan/spec"
213
+ - "start implementing", "let's implement", "go ahead and implement"
214
+ - `/double-check` template content
215
+
216
+ **Exit phrases:**
217
+ - "no issues found", "no bugs found"
218
+ - "looks good", "all good" (on own line)
219
+
220
+ **Issues-fixed indicators:**
221
+ - "fixed the following", "issues fixed", "bugs fixed"
222
+ - "Issues:", "Bugs:", "Changes:" (headers)
223
+ - "ready for another review"
224
+
225
+ ## Exit Conditions
226
+
227
+ The loop exits when:
228
+
229
+ 1. **Exit phrase without fixes** - Agent says "no issues" and didn't fix anything
230
+ 2. **Max iterations** - Safety limit reached (default: 7)
231
+ 3. **User interrupts** - You type something that isn't a trigger
232
+ 4. **Manual exit** - `/review-exit` command
233
+ 5. **Abort** - Press ESC or agent response is empty
234
+
235
+ ## Commands
236
+
237
+ | Command | Description |
238
+ |---------|-------------|
239
+ | `/review-start` | Activate and send review prompt immediately |
240
+ | `/review-exit` | Exit review mode |
241
+ | `/review-max <n>` | Set max iterations (session only) |
242
+ | `/review-status` | Show current state |
243
+
244
+ ## Tool API
245
+
246
+ The `review_loop` tool lets the agent programmatically control review mode:
247
+
248
+ ```typescript
249
+ // Check status (default)
250
+ review_loop({})
251
+
252
+ // Start review mode
253
+ review_loop({ start: true })
254
+
255
+ // Start with custom max iterations
256
+ review_loop({ start: true, maxIterations: 5 })
257
+
258
+ // Stop review mode
259
+ review_loop({ stop: true })
260
+
261
+ // Just update max iterations
262
+ review_loop({ maxIterations: 10 })
263
+ ```
264
+
265
+ **Returns:**
266
+ ```json
267
+ {
268
+ "active": true,
269
+ "currentIteration": 2,
270
+ "maxIterations": 7,
271
+ "message": "Review mode active: iteration 2/7"
272
+ }
273
+ ```
274
+
275
+ **Mode priority:** `start` > `stop` > status (default)
276
+
277
+ ## How It Works
278
+
279
+ ```
280
+ input event
281
+
282
+ matches trigger? → enter review mode
283
+
284
+ agent responds
285
+
286
+ agent_end event
287
+
288
+ matches exit + no fixes? → exit review mode
289
+
290
+ iteration < max? → send review prompt → loop
291
+
292
+ otherwise → exit (max reached)
293
+ ```
294
+
295
+ **Events used:**
296
+ - `session_start` - Reload settings
297
+ - `input` - Detect triggers, handle interrupts
298
+ - `before_agent_start` - Check expanded prompts for triggers
299
+ - `agent_end` - Analyze response, decide to loop or exit
300
+
301
+ ## Limitations
302
+
303
+ - **User templates only** - `template:name` loads from `~/.pi/agent/prompts/`, not project templates
304
+ - **Session-scoped /review-max** - Doesn't persist across sessions
305
+ - **Pattern failures are silent** - Invalid regex patterns are skipped without error
306
+
307
+ ## File Structure
308
+
309
+ ```
310
+ pi-review-loop/
311
+ ├── index.ts # Extension entry, event handlers, commands
312
+ ├── settings.ts # Configuration loading, pattern parsing, defaults
313
+ ├── scripts/
314
+ │ ├── install.js # npm postinstall - copies extension + prompts
315
+ │ └── uninstall.js # npm preuninstall - removes extension
316
+ ├── prompts/
317
+ │ ├── double-check.md # Review code template
318
+ │ └── double-check-plan.md # Review plan template
319
+ ├── package.json
320
+ ├── review-loop.png # Banner image
321
+ ├── README.md
322
+ ├── CHANGELOG.md
323
+ └── LICENSE
324
+ ```
325
+
326
+ ## Credits
327
+
328
+ Inspired by and built on ideas from:
329
+
330
+ - **[Ralph Wiggum Loop](https://ghuntley.com/ralph/)** by [@GeoffreyHuntley](https://x.com/GeoffreyHuntley) - Loosely based on the "Ralph" loop
331
+ - **["Fresh eyes" review prompt](https://x.com/doodlestein/status/1956228999945806049)** by [@doodlestein](https://x.com/doodlestein)
332
+ - **[pi](https://github.com/badlogic/pi-mono/)** by [@badlogicgames](https://x.com/badlogicgames) — The agent framework powering all of this
package/index.ts ADDED
@@ -0,0 +1,280 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { loadSettings, getReviewPrompt, type ReviewerLoopSettings } from "./settings.js";
4
+
5
+ export default function (pi: ExtensionAPI) {
6
+ let settings: ReviewerLoopSettings = loadSettings();
7
+ let reviewModeActive = false;
8
+ let currentIteration = 0;
9
+
10
+ function updateStatus(ctx: ExtensionContext) {
11
+ if (reviewModeActive) {
12
+ ctx.ui.setStatus(
13
+ "review-loop",
14
+ `Review mode (${currentIteration}/${settings.maxIterations})`
15
+ );
16
+ } else {
17
+ ctx.ui.setStatus("review-loop", undefined);
18
+ }
19
+ }
20
+
21
+ function exitReviewMode(ctx: ExtensionContext, reason: string) {
22
+ reviewModeActive = false;
23
+ currentIteration = 0;
24
+ updateStatus(ctx);
25
+ ctx.ui.notify(`Review mode ended: ${reason}`, "info");
26
+ }
27
+
28
+ function enterReviewMode(ctx: ExtensionContext) {
29
+ reviewModeActive = true;
30
+ currentIteration = 0;
31
+ updateStatus(ctx);
32
+ ctx.ui.notify("Review mode activated", "info");
33
+ }
34
+
35
+ pi.on("session_start", async () => {
36
+ settings = loadSettings();
37
+ });
38
+
39
+ pi.on("input", async (event, ctx) => {
40
+ if (!ctx.hasUI) return { action: "continue" as const };
41
+
42
+ const isTrigger = settings.triggerPatterns.some((p) => p.test(event.text));
43
+
44
+ if (reviewModeActive && event.source === "interactive" && !isTrigger) {
45
+ exitReviewMode(ctx, "user interrupted");
46
+ return { action: "continue" as const };
47
+ }
48
+
49
+ if (isTrigger && !reviewModeActive) {
50
+ enterReviewMode(ctx);
51
+ }
52
+
53
+ return { action: "continue" as const };
54
+ });
55
+
56
+ pi.on("before_agent_start", async (event, ctx) => {
57
+ if (!ctx.hasUI) return;
58
+ if (reviewModeActive) return;
59
+
60
+ const isTrigger = settings.triggerPatterns.some((p) => p.test(event.prompt));
61
+ if (isTrigger) {
62
+ enterReviewMode(ctx);
63
+ }
64
+ });
65
+
66
+ pi.on("agent_end", async (event, ctx) => {
67
+ if (!ctx.hasUI) return;
68
+ if (!reviewModeActive) return;
69
+
70
+ const assistantMessages = event.messages.filter((m) => m.role === "assistant");
71
+ const lastAssistantMessage = assistantMessages[assistantMessages.length - 1];
72
+
73
+ if (!lastAssistantMessage) {
74
+ exitReviewMode(ctx, "aborted");
75
+ return;
76
+ }
77
+
78
+ const textContent = lastAssistantMessage.content
79
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
80
+ .map((c) => c.text)
81
+ .join("\n");
82
+
83
+ if (!textContent.trim()) {
84
+ exitReviewMode(ctx, "aborted");
85
+ return;
86
+ }
87
+
88
+ const hasExitPhrase = settings.exitPatterns.some((p) => p.test(textContent));
89
+ const hasIssuesFixed = settings.issuesFixedPatterns.some((p) =>
90
+ p.test(textContent)
91
+ );
92
+
93
+ if (hasExitPhrase && !hasIssuesFixed) {
94
+ exitReviewMode(ctx, "no issues found");
95
+ return;
96
+ }
97
+
98
+ currentIteration++;
99
+ if (currentIteration > settings.maxIterations) {
100
+ exitReviewMode(ctx, `max iterations (${settings.maxIterations}) reached`);
101
+ return;
102
+ }
103
+
104
+ updateStatus(ctx);
105
+ pi.sendUserMessage(getReviewPrompt(settings.reviewPromptConfig), {
106
+ deliverAs: "followUp",
107
+ });
108
+ });
109
+
110
+ pi.registerCommand("review-start", {
111
+ description: "Activate review loop and send review prompt immediately",
112
+ handler: async (_args, ctx) => {
113
+ if (reviewModeActive) {
114
+ ctx.ui.notify("Review mode is already active", "info");
115
+ } else {
116
+ enterReviewMode(ctx);
117
+ pi.sendUserMessage(getReviewPrompt(settings.reviewPromptConfig));
118
+ }
119
+ },
120
+ });
121
+
122
+ pi.registerCommand("review-max", {
123
+ description: "Set max review iterations (default: 7)",
124
+ handler: async (args, ctx) => {
125
+ const num = parseInt(args, 10);
126
+ if (isNaN(num) || num < 1) {
127
+ ctx.ui.notify("Usage: /review-max <number>", "error");
128
+ return;
129
+ }
130
+ settings.maxIterations = num;
131
+ ctx.ui.notify(`Max review iterations set to ${settings.maxIterations}`, "info");
132
+ },
133
+ });
134
+
135
+ pi.registerCommand("review-exit", {
136
+ description: "Exit review mode manually",
137
+ handler: async (_args, ctx) => {
138
+ if (reviewModeActive) {
139
+ exitReviewMode(ctx, "manual exit");
140
+ } else {
141
+ ctx.ui.notify("Review mode is not active", "info");
142
+ }
143
+ },
144
+ });
145
+
146
+ pi.registerCommand("review-status", {
147
+ description: "Show review mode status",
148
+ handler: async (_args, ctx) => {
149
+ if (reviewModeActive) {
150
+ ctx.ui.notify(
151
+ `Review mode active: iteration ${currentIteration}/${settings.maxIterations}`,
152
+ "info"
153
+ );
154
+ } else {
155
+ ctx.ui.notify(
156
+ `Review mode inactive (max: ${settings.maxIterations})`,
157
+ "info"
158
+ );
159
+ }
160
+ },
161
+ });
162
+
163
+ pi.registerTool({
164
+ name: "review_loop",
165
+ description:
166
+ "Control the automated code review loop. Start/stop review mode or check status. When started, the loop repeatedly prompts for code review until 'No issues found' or max iterations reached.",
167
+ parameters: Type.Object({
168
+ start: Type.Optional(
169
+ Type.Boolean({
170
+ description: "Start review mode and send the review prompt",
171
+ })
172
+ ),
173
+ stop: Type.Optional(
174
+ Type.Boolean({
175
+ description: "Stop review mode",
176
+ })
177
+ ),
178
+ maxIterations: Type.Optional(
179
+ Type.Number({
180
+ description: "Set max iterations (can be combined with start)",
181
+ minimum: 1,
182
+ })
183
+ ),
184
+ }),
185
+
186
+ async execute(_toolCallId, params, _onUpdate, ctx) {
187
+ // Update maxIterations if provided
188
+ if (typeof params.maxIterations === "number" && params.maxIterations >= 1) {
189
+ settings.maxIterations = params.maxIterations;
190
+ }
191
+
192
+ // Mode: start > stop > status
193
+ if (params.start) {
194
+ if (reviewModeActive) {
195
+ return {
196
+ content: [
197
+ {
198
+ type: "text",
199
+ text: JSON.stringify({
200
+ active: true,
201
+ currentIteration,
202
+ maxIterations: settings.maxIterations,
203
+ message: "Review mode is already active",
204
+ }),
205
+ },
206
+ ],
207
+ };
208
+ }
209
+
210
+ enterReviewMode(ctx);
211
+ pi.sendUserMessage(getReviewPrompt(settings.reviewPromptConfig));
212
+
213
+ return {
214
+ content: [
215
+ {
216
+ type: "text",
217
+ text: JSON.stringify({
218
+ active: true,
219
+ currentIteration,
220
+ maxIterations: settings.maxIterations,
221
+ message: "Review mode started. Review prompt sent.",
222
+ }),
223
+ },
224
+ ],
225
+ };
226
+ }
227
+
228
+ if (params.stop) {
229
+ if (!reviewModeActive) {
230
+ return {
231
+ content: [
232
+ {
233
+ type: "text",
234
+ text: JSON.stringify({
235
+ active: false,
236
+ currentIteration: 0,
237
+ maxIterations: settings.maxIterations,
238
+ message: "Review mode is not active",
239
+ }),
240
+ },
241
+ ],
242
+ };
243
+ }
244
+
245
+ exitReviewMode(ctx, "stopped by agent");
246
+
247
+ return {
248
+ content: [
249
+ {
250
+ type: "text",
251
+ text: JSON.stringify({
252
+ active: false,
253
+ currentIteration: 0,
254
+ maxIterations: settings.maxIterations,
255
+ message: "Review mode stopped",
256
+ }),
257
+ },
258
+ ],
259
+ };
260
+ }
261
+
262
+ // Default: status
263
+ return {
264
+ content: [
265
+ {
266
+ type: "text",
267
+ text: JSON.stringify({
268
+ active: reviewModeActive,
269
+ currentIteration,
270
+ maxIterations: settings.maxIterations,
271
+ message: reviewModeActive
272
+ ? `Review mode active: iteration ${currentIteration}/${settings.maxIterations}`
273
+ : "Review mode inactive",
274
+ }),
275
+ },
276
+ ],
277
+ };
278
+ },
279
+ });
280
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "pi-review-loop",
3
+ "version": "0.1.1",
4
+ "description": "Automated code review loop extension for pi coding agent",
5
+ "scripts": {
6
+ "postinstall": "node scripts/install.js",
7
+ "preuninstall": "node scripts/uninstall.js"
8
+ },
9
+ "files": [
10
+ "index.ts",
11
+ "settings.ts",
12
+ "review-loop.png",
13
+ "scripts/",
14
+ "prompts/"
15
+ ],
16
+ "keywords": [
17
+ "pi",
18
+ "pi-coding-agent",
19
+ "extension",
20
+ "code-review",
21
+ "automation",
22
+ "ralph-wiggum-loop"
23
+ ],
24
+ "author": "Nico Bailon",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/nicobailon/pi-review-loop.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/nicobailon/pi-review-loop/issues"
32
+ },
33
+ "homepage": "https://github.com/nicobailon/pi-review-loop#readme"
34
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Double check / critique implementation plan with fresh eyes
3
+ ---
4
+ Great, now I want you to carefully read over the plan/spec and compare against the codebase with "fresh eyes," looking super carefully for any obvious bugs, errors, problems, issues, confusion, etc. Also double check that the patterns and coding styles match existing codebase conventions and preferences. Consider whether there is any unnecessary complexity or duplication, or if the changes can be more minimal without changing scope of requirements and without causing regressions or increasing the risk of bugs. Also keep in mind that this is not an MVP - so don't skip anything. If any issues are found, proceed to fix them without being asked to do so.
5
+
6
+ **Response format:**
7
+ - If you find ANY issues: fix them, then list what you fixed. Do NOT say "no issues found" - instead end with "Fixed [N] issue(s). Ready for another review."
8
+ - If you find ZERO issues: respond with exactly "No issues found."
9
+
10
+ $@
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Double check / review implementation with fresh eyes
3
+ ---
4
+ Great, now I want you to carefully read over all of the new code you just wrote and other existing code with "fresh eyes," looking super carefully for any obvious bugs, errors, problems, issues, confusion, etc. Also, if you notice any pre-existing issues/bugs those should be addressed.
5
+
6
+ **Response format:**
7
+ - If you find ANY issues: fix them, then list what you fixed. Do NOT say "no issues found" - instead end with "Fixed [N] issue(s). Ready for another review."
8
+ - If you find ZERO issues: respond with exactly "No issues found."
9
+
10
+ $@
Binary file
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const EXTENSION_NAME = "pi-review-loop";
8
+ const TARGET_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", EXTENSION_NAME);
9
+ const PROMPTS_DIR = path.join(os.homedir(), ".pi", "agent", "prompts");
10
+ const SOURCE_DIR = path.dirname(__dirname);
11
+ const FILES = ["index.ts", "settings.ts", "review-loop.png"];
12
+ const PROMPTS = ["double-check.md", "double-check-plan.md"];
13
+
14
+ try {
15
+ // Install extension files
16
+ fs.mkdirSync(TARGET_DIR, { recursive: true });
17
+
18
+ for (const file of FILES) {
19
+ const src = path.join(SOURCE_DIR, file);
20
+ const dest = path.join(TARGET_DIR, file);
21
+
22
+ if (fs.existsSync(src)) {
23
+ fs.copyFileSync(src, dest);
24
+ }
25
+ }
26
+
27
+ // Install prompt templates (skip if already exist)
28
+ fs.mkdirSync(PROMPTS_DIR, { recursive: true });
29
+ const installedPrompts = [];
30
+
31
+ for (const file of PROMPTS) {
32
+ const src = path.join(SOURCE_DIR, "prompts", file);
33
+ const dest = path.join(PROMPTS_DIR, file);
34
+
35
+ if (fs.existsSync(src) && !fs.existsSync(dest)) {
36
+ fs.copyFileSync(src, dest);
37
+ installedPrompts.push(file);
38
+ }
39
+ }
40
+
41
+ console.log("");
42
+ console.log("✓ pi-review-loop installed to ~/.pi/agent/extensions/pi-review-loop/");
43
+ if (installedPrompts.length > 0) {
44
+ console.log(`✓ Prompt templates installed: ${installedPrompts.join(", ")}`);
45
+ }
46
+ console.log(" Restart pi to load the extension.");
47
+ console.log("");
48
+ } catch (err) {
49
+ console.error("Failed to install pi-review-loop:", err.message);
50
+ process.exit(1);
51
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const EXTENSION_NAME = "pi-review-loop";
8
+ const TARGET_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", EXTENSION_NAME);
9
+
10
+ try {
11
+ if (fs.existsSync(TARGET_DIR)) {
12
+ fs.rmSync(TARGET_DIR, { recursive: true });
13
+ console.log("");
14
+ console.log("✓ pi-review-loop removed from ~/.pi/agent/extensions/pi-review-loop/");
15
+ console.log("");
16
+ }
17
+ } catch (err) {
18
+ console.error("Failed to uninstall pi-review-loop:", err.message);
19
+ }
package/settings.ts ADDED
@@ -0,0 +1,211 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+
5
+ export const SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json");
6
+
7
+ export const DEFAULT_MAX_ITERATIONS = 7;
8
+
9
+ export const DEFAULT_REVIEW_PROMPT = `Great, now I want you to carefully read over all of the new code you just wrote and other existing code you just modified with "fresh eyes," looking super carefully for any obvious bugs, errors, problems, issues, confusion, etc. If any issues are found, proceed to fix them without being asked to do so.
10
+
11
+ **Response format:**
12
+ - If you find ANY issues: fix them, then list what you fixed. Do NOT say "no issues found" - instead end with "Fixed [N] issue(s). Ready for another review."
13
+ - If you find ZERO issues: respond with exactly "No issues found."`;
14
+
15
+ export const DEFAULT_TRIGGER_PATTERNS: RegExp[] = [
16
+ /\bimplement\s+(the\s+)?plan\b/i,
17
+ /\bimplement\s+(the\s+)?spec\b/i,
18
+ /\bimplement\s+(this\s+)?plan\b/i,
19
+ /\bimplement\s+(this\s+)?spec\b/i,
20
+ /\bstart\s+implementing\b.*\b(plan|spec)\b/i,
21
+ /\bgo\s+ahead\s+and\s+implement\b.*\b(plan|spec)\b/i,
22
+ /\blet'?s\s+implement\b.*\b(plan|spec)\b/i,
23
+ /\b(plan|spec)\b.*\bstart\s+implementing\b/i,
24
+ /\b(plan|spec)\b.*\bgo\s+ahead\s+and\s+implement\b/i,
25
+ /\b(plan|spec)\b.*\blet'?s\s+implement\b/i,
26
+ /read over all of the new code.*fresh eyes/i,
27
+ ];
28
+
29
+ export const DEFAULT_EXIT_PATTERNS: RegExp[] = [
30
+ /no\s+(\w+\s+)?issues\s+found/i,
31
+ /no\s+(\w+\s+)?bugs\s+found/i,
32
+ /(?:^|\n)\s*(?:looks\s+good|all\s+good)[\s.,!]*(?:$|\n)/im,
33
+ ];
34
+
35
+ export const DEFAULT_ISSUES_FIXED_PATTERNS: RegExp[] = [
36
+ /issues?\s+(i\s+)?fixed/i,
37
+ /fixed\s+(the\s+)?(following|these|this|issues?|bugs?)/i,
38
+ /fixed\s+\d+\s+issues?/i,
39
+ /found\s+and\s+(fixed|corrected|resolved)/i,
40
+ /bugs?\s+(i\s+)?fixed/i,
41
+ /corrected\s+(the\s+)?(following|these|this)/i,
42
+ /(?<!no\s)issues?\s+(i\s+)?(found|identified|discovered)/i,
43
+ /(?<!no\s)problems?\s+(i\s+)?(found|identified|discovered)/i,
44
+ /changes?\s+(i\s+)?made/i,
45
+ /here'?s?\s+what\s+(i\s+)?(fixed|changed|corrected)/i,
46
+ /(issues|bugs|problems|changes|fixes)\s*:/i,
47
+ /ready\s+for\s+(another|the\s+next)\s+review/i,
48
+ ];
49
+
50
+ export interface PatternConfig {
51
+ mode?: "extend" | "replace";
52
+ patterns: string[];
53
+ }
54
+
55
+ export interface ReviewPromptConfig {
56
+ type: "inline" | "file" | "template";
57
+ value: string;
58
+ }
59
+
60
+ export interface ReviewerLoopSettingsRaw {
61
+ maxIterations?: number;
62
+ reviewPrompt?: string;
63
+ triggerPatterns?: PatternConfig;
64
+ exitPatterns?: PatternConfig;
65
+ issuesFixedPatterns?: PatternConfig;
66
+ }
67
+
68
+ export interface ReviewerLoopSettings {
69
+ maxIterations: number;
70
+ reviewPromptConfig: ReviewPromptConfig;
71
+ triggerPatterns: RegExp[];
72
+ exitPatterns: RegExp[];
73
+ issuesFixedPatterns: RegExp[];
74
+ }
75
+
76
+ function parsePattern(input: unknown): RegExp | null {
77
+ if (typeof input !== "string") return null;
78
+
79
+ const match = input.match(/^\/(.+)\/([gimsuy]*)$/);
80
+ if (match) {
81
+ try {
82
+ return new RegExp(match[1], match[2]);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ const escaped = input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
88
+ return new RegExp(escaped, "i");
89
+ }
90
+
91
+ function loadPatterns(
92
+ config: PatternConfig | undefined,
93
+ defaults: RegExp[]
94
+ ): RegExp[] {
95
+ if (!config?.patterns || !Array.isArray(config.patterns) || config.patterns.length === 0) {
96
+ return defaults;
97
+ }
98
+
99
+ const userPatterns = config.patterns
100
+ .map(parsePattern)
101
+ .filter((p): p is RegExp => p !== null);
102
+
103
+ if (config.mode === "replace") {
104
+ return userPatterns.length > 0 ? userPatterns : defaults;
105
+ }
106
+
107
+ return [...defaults, ...userPatterns];
108
+ }
109
+
110
+ function isFilePath(value: string): boolean {
111
+ return (
112
+ value.startsWith("~/") ||
113
+ value.startsWith("/") ||
114
+ value.startsWith("./") ||
115
+ value.endsWith(".md") ||
116
+ value.endsWith(".txt")
117
+ );
118
+ }
119
+
120
+ function parseReviewPromptConfig(value: string | undefined): ReviewPromptConfig {
121
+ if (!value) {
122
+ return { type: "inline", value: DEFAULT_REVIEW_PROMPT };
123
+ }
124
+
125
+ if (value.startsWith("template:")) {
126
+ const templateName = value.slice("template:".length).trim();
127
+ return { type: "template", value: templateName };
128
+ }
129
+
130
+ if (isFilePath(value)) {
131
+ return { type: "file", value };
132
+ }
133
+
134
+ return { type: "inline", value };
135
+ }
136
+
137
+ function stripFrontmatter(content: string): string {
138
+ const match = content.match(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n)?([\s\S]*)$/);
139
+ return match ? match[1].trim() : content.trim();
140
+ }
141
+
142
+ function resolvePath(value: string): string {
143
+ if (value.startsWith("~/")) {
144
+ return join(homedir(), value.slice(2));
145
+ }
146
+ return value;
147
+ }
148
+
149
+ export function getReviewPrompt(config: ReviewPromptConfig): string {
150
+ switch (config.type) {
151
+ case "inline":
152
+ return config.value || DEFAULT_REVIEW_PROMPT;
153
+
154
+ case "file": {
155
+ const resolvedPath = resolvePath(config.value);
156
+ try {
157
+ const content = readFileSync(resolvedPath, "utf-8").trim();
158
+ return content || DEFAULT_REVIEW_PROMPT;
159
+ } catch {
160
+ return DEFAULT_REVIEW_PROMPT;
161
+ }
162
+ }
163
+
164
+ case "template": {
165
+ const templatePath = join(
166
+ homedir(),
167
+ ".pi",
168
+ "agent",
169
+ "prompts",
170
+ `${config.value}.md`
171
+ );
172
+ try {
173
+ if (!existsSync(templatePath)) {
174
+ return DEFAULT_REVIEW_PROMPT;
175
+ }
176
+ let content = readFileSync(templatePath, "utf-8");
177
+ content = stripFrontmatter(content);
178
+ content = content.replace(/\$@/g, "").trim();
179
+ return content || DEFAULT_REVIEW_PROMPT;
180
+ } catch {
181
+ return DEFAULT_REVIEW_PROMPT;
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ export function loadSettings(): ReviewerLoopSettings {
188
+ let raw: ReviewerLoopSettingsRaw = {};
189
+
190
+ try {
191
+ const content = readFileSync(SETTINGS_PATH, "utf-8");
192
+ const parsed = JSON.parse(content);
193
+ raw = parsed?.reviewerLoop ?? {};
194
+ } catch {
195
+ // Use all defaults
196
+ }
197
+
198
+ return {
199
+ maxIterations:
200
+ typeof raw.maxIterations === "number" && raw.maxIterations >= 1
201
+ ? raw.maxIterations
202
+ : DEFAULT_MAX_ITERATIONS,
203
+ reviewPromptConfig: parseReviewPromptConfig(raw.reviewPrompt),
204
+ triggerPatterns: loadPatterns(raw.triggerPatterns, DEFAULT_TRIGGER_PATTERNS),
205
+ exitPatterns: loadPatterns(raw.exitPatterns, DEFAULT_EXIT_PATTERNS),
206
+ issuesFixedPatterns: loadPatterns(
207
+ raw.issuesFixedPatterns,
208
+ DEFAULT_ISSUES_FIXED_PATTERNS
209
+ ),
210
+ };
211
+ }