pi-review 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,61 @@
1
+ # pi-review
2
+
3
+ Run a strict maintainer review in a new [pi](https://github.com/badlogic/pi-mono) coding agent branch.
4
+
5
+ ## Preview
6
+
7
+ ![Example pi-review output showing prioritized findings and recommendations](assets/review-output.png)
8
+
9
+ ## What it does
10
+
11
+ Adds a `/review` command that starts a new branch from the current conversation and asks pi to review the available work. The review includes user and assistant conversation messages from the current branch, with thinking and tool calls removed.
12
+
13
+ The reviewer focuses on concrete, high-confidence issues in correctness, security, performance, operability, and maintainability. If nothing material stands out, it reports `looks good`.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pi install npm:pi-review
19
+ ```
20
+
21
+ Or try it temporarily:
22
+
23
+ ```bash
24
+ pi -e npm:pi-review
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```text
30
+ /review
31
+ ```
32
+
33
+ Add optional focus text:
34
+
35
+ ```text
36
+ /review focus on release safety and backward compatibility
37
+ ```
38
+
39
+ ## How it works
40
+
41
+ 1. Waits for the current agent turn to finish if needed
42
+ 2. Extracts user and assistant text from the active branch
43
+ 3. Switches thinking level to `high` for the review turn
44
+ 4. Creates a new branch from the current conversation
45
+ 5. Sends a maintainer-style review prompt with optional focus text
46
+ 6. Restores your previous thinking level when the review turn ends
47
+
48
+ ## Review output
49
+
50
+ Findings are sorted by priority:
51
+
52
+ - `[P0]` severe breakage, data loss, or security issue
53
+ - `[P1]` likely user-facing breakage or major regression
54
+ - `[P2]` limited-scope correctness, performance, or maintainability issue
55
+ - `[P3]` minor but real issue
56
+
57
+ Each finding includes location, summary, affected behavior/invariant/code path, and a specific recommendation.
58
+
59
+ ## License
60
+
61
+ MIT
Binary file
@@ -0,0 +1,3 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ export default function reviewExtension(pi: ExtensionAPI): void;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA8ClE,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,QA8CvD"}
package/dist/index.js ADDED
@@ -0,0 +1,77 @@
1
+ import { sendMessageInNewBranch } from "./lib/child-session.js";
2
+ import { extractConversation, formatConversation } from "./lib/conversation-context.js";
3
+ const REVIEW_THINKING_LEVEL = "high";
4
+ const REVIEW_INSTRUCTION = `Review the available work and context.
5
+ Put your strict maintainer hat on.
6
+ Find concrete, high-confidence, material issues introduced by the work or revealed by the additional context.
7
+ Focus on correctness, security, performance, operability, and maintainability.
8
+ Do not speculate; point to the affected behavior, invariant, or code path.
9
+ Prefer issues the author would likely fix before merge.
10
+ Assume existing interfaces and behavior should remain backward compatible unless the user or project instructions explicitly say otherwise.
11
+ If nothing material stands out, say \`looks good\`; otherwise return numbered sections for findings, sorted by priority.
12
+ Use [P0] for certain severe breakage, data loss, or security issues; [P1] for likely user-facing breakage or major regressions; [P2] for limited-scope correctness, performance, or maintainability issues; [P3] for minor but real issues.
13
+ For each finding, include a [P0]-[P3] tag, location, a concise summary, a concise explanation of the affected behavior, invariant, or code path, and \`Recommendation:\` with the top specific, actionable fix, stated concisely.`;
14
+ function buildReviewInstruction(args) {
15
+ const focusText = args.trim();
16
+ if (!focusText) {
17
+ return REVIEW_INSTRUCTION;
18
+ }
19
+ return [REVIEW_INSTRUCTION, "Additional review context:", focusText].join("\n\n");
20
+ }
21
+ function buildReviewMessage(args, conversationXml) {
22
+ const reviewInstruction = buildReviewInstruction(args);
23
+ if (!conversationXml) {
24
+ return reviewInstruction;
25
+ }
26
+ return [
27
+ "Conversation context copied from the current branch (user + assistant messages only; thinking and tool calls removed):",
28
+ "",
29
+ "````xml",
30
+ conversationXml,
31
+ "````",
32
+ "",
33
+ reviewInstruction,
34
+ ].join("\n");
35
+ }
36
+ export default function reviewExtension(pi) {
37
+ let originalThinkingLevel;
38
+ function restoreThinkingLevel() {
39
+ if (!originalThinkingLevel)
40
+ return;
41
+ pi.setThinkingLevel(originalThinkingLevel);
42
+ originalThinkingLevel = undefined;
43
+ }
44
+ pi.on("agent_end", () => {
45
+ restoreThinkingLevel();
46
+ });
47
+ pi.registerCommand("review", {
48
+ description: "Review current work in new branch (optional focus text)",
49
+ handler: async (args, ctx) => {
50
+ if (!ctx.isIdle()) {
51
+ await ctx.waitForIdle();
52
+ }
53
+ const branch = ctx.sessionManager.getBranch();
54
+ const extractedConversation = extractConversation(branch);
55
+ const conversationXml = extractedConversation.length === 0 ? undefined : formatConversation(extractedConversation);
56
+ const reviewMessage = buildReviewMessage(args, conversationXml);
57
+ const currentThinkingLevel = pi.getThinkingLevel();
58
+ if (currentThinkingLevel !== REVIEW_THINKING_LEVEL) {
59
+ originalThinkingLevel = currentThinkingLevel;
60
+ pi.setThinkingLevel(REVIEW_THINKING_LEVEL);
61
+ }
62
+ let started = false;
63
+ try {
64
+ started = await sendMessageInNewBranch(pi, ctx, branch, reviewMessage, "review");
65
+ }
66
+ finally {
67
+ if (!started)
68
+ restoreThinkingLevel();
69
+ }
70
+ if (!started)
71
+ return;
72
+ if (ctx.hasUI) {
73
+ ctx.ui.setEditorText("");
74
+ }
75
+ },
76
+ });
77
+ }
@@ -0,0 +1,3 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext, SessionEntry } from "@mariozechner/pi-coding-agent";
2
+ export declare function sendMessageInNewBranch(pi: ExtensionAPI, ctx: ExtensionCommandContext, branch: SessionEntry[], message: string, purpose: string): Promise<boolean>;
3
+ //# sourceMappingURL=child-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"child-session.d.ts","sourceRoot":"","sources":["../../src/lib/child-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,uBAAuB,EACvB,YAAY,EACb,MAAM,+BAA+B,CAAC;AAgBvC,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,YAAY,EAChB,GAAG,EAAE,uBAAuB,EAC5B,MAAM,EAAE,YAAY,EAAE,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CAclB"}
@@ -0,0 +1,26 @@
1
+ function getEmptyBranchTargetId(branch) {
2
+ for (const entry of branch) {
3
+ if (entry.type === "custom_message") {
4
+ return entry.id;
5
+ }
6
+ if (entry.type === "message" && entry.message.role === "user") {
7
+ return entry.id;
8
+ }
9
+ }
10
+ return undefined;
11
+ }
12
+ export async function sendMessageInNewBranch(pi, ctx, branch, message, purpose) {
13
+ const targetId = getEmptyBranchTargetId(branch);
14
+ if (targetId) {
15
+ const result = await ctx.navigateTree(targetId, { summarize: false });
16
+ if (result.cancelled) {
17
+ if (ctx.hasUI)
18
+ ctx.ui.notify(`New ${purpose} branch cancelled`, "info");
19
+ return false;
20
+ }
21
+ }
22
+ pi.sendUserMessage(message);
23
+ if (ctx.hasUI)
24
+ ctx.ui.notify(`Started ${purpose} branch`, "info");
25
+ return true;
26
+ }
@@ -0,0 +1,8 @@
1
+ import type { SessionEntry } from "@mariozechner/pi-coding-agent";
2
+ export type ExtractedMessage = {
3
+ role: "user" | "assistant";
4
+ text: string;
5
+ };
6
+ export declare function extractConversation(branch: SessionEntry[]): ExtractedMessage[];
7
+ export declare function formatConversation(messages: ExtractedMessage[]): string;
8
+ //# sourceMappingURL=conversation-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-context.d.ts","sourceRoot":"","sources":["../../src/lib/conversation-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAElE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAkCF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,gBAAgB,EAAE,CAkB9E;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAOvE"}
@@ -0,0 +1,45 @@
1
+ function isTextBlock(block) {
2
+ return (!!block &&
3
+ typeof block === "object" &&
4
+ "type" in block &&
5
+ block.type === "text" &&
6
+ "text" in block &&
7
+ typeof block.text === "string");
8
+ }
9
+ function extractText(content, allowString) {
10
+ if (allowString && typeof content === "string") {
11
+ return content.trim();
12
+ }
13
+ if (!Array.isArray(content)) {
14
+ return "";
15
+ }
16
+ return content
17
+ .filter(isTextBlock)
18
+ .map((block) => block.text.trim())
19
+ .filter(Boolean)
20
+ .join("\n\n");
21
+ }
22
+ export function extractConversation(branch) {
23
+ const messages = branch
24
+ .filter((entry) => entry.type === "message")
25
+ .map((entry) => entry.message);
26
+ return messages.flatMap((message) => {
27
+ if (message.role === "user") {
28
+ const text = extractText(message.content, true);
29
+ return text ? [{ role: "user", text }] : [];
30
+ }
31
+ if (message.role === "assistant") {
32
+ const text = extractText(message.content, false);
33
+ return text ? [{ role: "assistant", text }] : [];
34
+ }
35
+ return [];
36
+ });
37
+ }
38
+ export function formatConversation(messages) {
39
+ return messages
40
+ .map((message, index) => {
41
+ const tag = message.role;
42
+ return `<${tag} index="${index + 1}">\n${message.text}\n</${tag}>`;
43
+ })
44
+ .join("\n\n");
45
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "pi-review",
3
+ "version": "1.0.0",
4
+ "description": "Review current pi work in a new branch with conversation context",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "review",
10
+ "code-review"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Anton Kuzmenko <hotk@hey.com>",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/default-anton/pi-review.git"
17
+ },
18
+ "main": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "files": [
21
+ "assets/",
22
+ "dist",
23
+ "src/",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "clean": "rm -rf dist",
30
+ "check": "tsc --noEmit",
31
+ "prepublishOnly": "npm run build",
32
+ "release:verify-tag": "node scripts/verify-release-tag.mjs",
33
+ "release:notes": "node scripts/changelog-release-notes.mjs",
34
+ "release:gate": "npm run check && npm run build"
35
+ },
36
+ "pi": {
37
+ "extensions": [
38
+ "./src/index.ts"
39
+ ]
40
+ },
41
+ "peerDependencies": {
42
+ "@mariozechner/pi-coding-agent": "^0.69.0"
43
+ },
44
+ "devDependencies": {
45
+ "@mariozechner/pi-coding-agent": "^0.69.0",
46
+ "@types/node": "^25.0.10",
47
+ "typescript": "^5.0.0"
48
+ }
49
+ }
package/src/index.ts ADDED
@@ -0,0 +1,93 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ import { sendMessageInNewBranch } from "./lib/child-session.js";
4
+ import { extractConversation, formatConversation } from "./lib/conversation-context.js";
5
+
6
+ const REVIEW_THINKING_LEVEL = "high";
7
+
8
+ type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
9
+
10
+ const REVIEW_INSTRUCTION = `Review the available work and context.
11
+ Put your strict maintainer hat on.
12
+ Find concrete, high-confidence, material issues introduced by the work or revealed by the additional context.
13
+ Focus on correctness, security, performance, operability, and maintainability.
14
+ Do not speculate; point to the affected behavior, invariant, or code path.
15
+ Prefer issues the author would likely fix before merge.
16
+ Assume existing interfaces and behavior should remain backward compatible unless the user or project instructions explicitly say otherwise.
17
+ If nothing material stands out, say \`looks good\`; otherwise return numbered sections for findings, sorted by priority.
18
+ Use [P0] for certain severe breakage, data loss, or security issues; [P1] for likely user-facing breakage or major regressions; [P2] for limited-scope correctness, performance, or maintainability issues; [P3] for minor but real issues.
19
+ For each finding, include a [P0]-[P3] tag, location, a concise summary, a concise explanation of the affected behavior, invariant, or code path, and \`Recommendation:\` with the top specific, actionable fix, stated concisely.`;
20
+
21
+ function buildReviewInstruction(args: string): string {
22
+ const focusText = args.trim();
23
+ if (!focusText) {
24
+ return REVIEW_INSTRUCTION;
25
+ }
26
+
27
+ return [REVIEW_INSTRUCTION, "Additional review context:", focusText].join("\n\n");
28
+ }
29
+
30
+ function buildReviewMessage(args: string, conversationXml?: string): string {
31
+ const reviewInstruction = buildReviewInstruction(args);
32
+ if (!conversationXml) {
33
+ return reviewInstruction;
34
+ }
35
+
36
+ return [
37
+ "Conversation context copied from the current branch (user + assistant messages only; thinking and tool calls removed):",
38
+ "",
39
+ "````xml",
40
+ conversationXml,
41
+ "````",
42
+ "",
43
+ reviewInstruction,
44
+ ].join("\n");
45
+ }
46
+
47
+ export default function reviewExtension(pi: ExtensionAPI) {
48
+ let originalThinkingLevel: ThinkingLevel | undefined;
49
+
50
+ function restoreThinkingLevel(): void {
51
+ if (!originalThinkingLevel) return;
52
+
53
+ pi.setThinkingLevel(originalThinkingLevel);
54
+ originalThinkingLevel = undefined;
55
+ }
56
+
57
+ pi.on("agent_end", () => {
58
+ restoreThinkingLevel();
59
+ });
60
+
61
+ pi.registerCommand("review", {
62
+ description: "Review current work in new branch (optional focus text)",
63
+ handler: async (args, ctx) => {
64
+ if (!ctx.isIdle()) {
65
+ await ctx.waitForIdle();
66
+ }
67
+
68
+ const branch = ctx.sessionManager.getBranch();
69
+ const extractedConversation = extractConversation(branch);
70
+ const conversationXml =
71
+ extractedConversation.length === 0 ? undefined : formatConversation(extractedConversation);
72
+ const reviewMessage = buildReviewMessage(args, conversationXml);
73
+
74
+ const currentThinkingLevel = pi.getThinkingLevel();
75
+ if (currentThinkingLevel !== REVIEW_THINKING_LEVEL) {
76
+ originalThinkingLevel = currentThinkingLevel;
77
+ pi.setThinkingLevel(REVIEW_THINKING_LEVEL);
78
+ }
79
+
80
+ let started = false;
81
+ try {
82
+ started = await sendMessageInNewBranch(pi, ctx, branch, reviewMessage, "review");
83
+ } finally {
84
+ if (!started) restoreThinkingLevel();
85
+ }
86
+ if (!started) return;
87
+
88
+ if (ctx.hasUI) {
89
+ ctx.ui.setEditorText("");
90
+ }
91
+ },
92
+ });
93
+ }
@@ -0,0 +1,41 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ SessionEntry,
5
+ } from "@mariozechner/pi-coding-agent";
6
+
7
+ function getEmptyBranchTargetId(branch: SessionEntry[]): string | undefined {
8
+ for (const entry of branch) {
9
+ if (entry.type === "custom_message") {
10
+ return entry.id;
11
+ }
12
+
13
+ if (entry.type === "message" && entry.message.role === "user") {
14
+ return entry.id;
15
+ }
16
+ }
17
+
18
+ return undefined;
19
+ }
20
+
21
+ export async function sendMessageInNewBranch(
22
+ pi: ExtensionAPI,
23
+ ctx: ExtensionCommandContext,
24
+ branch: SessionEntry[],
25
+ message: string,
26
+ purpose: string,
27
+ ): Promise<boolean> {
28
+ const targetId = getEmptyBranchTargetId(branch);
29
+ if (targetId) {
30
+ const result = await ctx.navigateTree(targetId, { summarize: false });
31
+ if (result.cancelled) {
32
+ if (ctx.hasUI) ctx.ui.notify(`New ${purpose} branch cancelled`, "info");
33
+ return false;
34
+ }
35
+ }
36
+
37
+ pi.sendUserMessage(message);
38
+
39
+ if (ctx.hasUI) ctx.ui.notify(`Started ${purpose} branch`, "info");
40
+ return true;
41
+ }
@@ -0,0 +1,67 @@
1
+ import type { SessionEntry } from "@mariozechner/pi-coding-agent";
2
+
3
+ export type ExtractedMessage = {
4
+ role: "user" | "assistant";
5
+ text: string;
6
+ };
7
+
8
+ type TextBlock = {
9
+ type: "text";
10
+ text: string;
11
+ };
12
+
13
+ function isTextBlock(block: unknown): block is TextBlock {
14
+ return (
15
+ !!block &&
16
+ typeof block === "object" &&
17
+ "type" in block &&
18
+ block.type === "text" &&
19
+ "text" in block &&
20
+ typeof block.text === "string"
21
+ );
22
+ }
23
+
24
+ function extractText(content: unknown, allowString: boolean): string {
25
+ if (allowString && typeof content === "string") {
26
+ return content.trim();
27
+ }
28
+
29
+ if (!Array.isArray(content)) {
30
+ return "";
31
+ }
32
+
33
+ return content
34
+ .filter(isTextBlock)
35
+ .map((block) => block.text.trim())
36
+ .filter(Boolean)
37
+ .join("\n\n");
38
+ }
39
+
40
+ export function extractConversation(branch: SessionEntry[]): ExtractedMessage[] {
41
+ const messages = branch
42
+ .filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
43
+ .map((entry) => entry.message);
44
+
45
+ return messages.flatMap((message): ExtractedMessage[] => {
46
+ if (message.role === "user") {
47
+ const text = extractText(message.content, true);
48
+ return text ? [{ role: "user", text }] : [];
49
+ }
50
+
51
+ if (message.role === "assistant") {
52
+ const text = extractText(message.content, false);
53
+ return text ? [{ role: "assistant", text }] : [];
54
+ }
55
+
56
+ return [];
57
+ });
58
+ }
59
+
60
+ export function formatConversation(messages: ExtractedMessage[]): string {
61
+ return messages
62
+ .map((message, index) => {
63
+ const tag = message.role;
64
+ return `<${tag} index="${index + 1}">\n${message.text}\n</${tag}>`;
65
+ })
66
+ .join("\n\n");
67
+ }