@nukcole-xinluo9510/pi-critic-guy 0.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/LICENSE +21 -0
- package/README.md +57 -0
- package/extensions/critic-guy.ts +185 -0
- package/package.json +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nuckcole
|
|
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,57 @@
|
|
|
1
|
+
# pi-critic-guy
|
|
2
|
+
|
|
3
|
+
Pi extension — spawn a second-opinion code reviewer by typing `critic` into your pi session.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# From npm
|
|
9
|
+
pi add npm:@nukcole-xinluo9510/pi-critic-guy
|
|
10
|
+
|
|
11
|
+
# Or from local path during development
|
|
12
|
+
pi add /path/to/pi-critic-guy
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
In any pi session, just type:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
critic
|
|
21
|
+
critic review the auth code
|
|
22
|
+
critic model=deepseek-v4-flash review the error handling
|
|
23
|
+
critic using claude check for security issues
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The extension injects "Critic Guy" instructions into the system prompt on turns where you mention `critic`. It resolves the model dynamically from your current session, the model registry, or the model you specify via `using <name>` or `model=<id>`.
|
|
27
|
+
|
|
28
|
+
### What it does
|
|
29
|
+
|
|
30
|
+
- Detects the word `critic` in your prompt (word boundary, won't match "critical")
|
|
31
|
+
- Resolves the reviewer model (your current model, or one you specify)
|
|
32
|
+
- Injects instructions to spawn a subagent via the `subagent` tool
|
|
33
|
+
- Keeps the reviewer on a short leash — read-only tools (`read`, `grep`, `find`, `ls`)
|
|
34
|
+
|
|
35
|
+
### Parallel reviews
|
|
36
|
+
|
|
37
|
+
For large codebases, the injected instructions tell the LLM to split the review into parallel subagents focusing on different aspects (correctness, design, error handling).
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
The extension hooks into `before_agent_start`. When `critic` is detected:
|
|
42
|
+
|
|
43
|
+
1. It appends a "Capability: Critic Guy" section to the system prompt
|
|
44
|
+
2. The capability tells the LLM to spawn a `reviewer` subagent
|
|
45
|
+
3. The reviewer runs in an isolated context with read-only tools
|
|
46
|
+
|
|
47
|
+
The reviewer agent is defined in your pi agent directory (`~/.pi/agent/agents/reviewer.md`). It uses `claude-sonnet-4-5` and has access to `bash` for `git diff`.
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
- pi 0.79+
|
|
52
|
+
- `subagent` extension enabled (built-in example, see `pi subagent list`)
|
|
53
|
+
- `reviewer` agent defined (example at `packages/coding-agent/examples/extensions/subagent/agents/reviewer.md`)
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Critic Guy — trigger by user typing "critic", inject critic capability
|
|
3
|
+
*
|
|
4
|
+
* Hook: before_agent_start
|
|
5
|
+
* - Detects if user's prompt contains "critic" (word boundary)
|
|
6
|
+
* - Parses optional model specification: "critic using claude" / "critic model=gpt"
|
|
7
|
+
* - Matches model name against available models from registry
|
|
8
|
+
* - Injects critic instructions on every turn that mentions "critic"
|
|
9
|
+
* (cheap, bounded; dedup marker prevents double-injection within a turn)
|
|
10
|
+
* - Dynamically injects the resolved model ID so the LLM knows what to use
|
|
11
|
+
* - On oversize system prompt, injects a short visible note instead of silently
|
|
12
|
+
* doing nothing, so the LLM can explain why critic didn't activate
|
|
13
|
+
* - No context overhead when critic isn't requested
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
// Max system prompt length before we skip injection (≈ 50K chars → ~12-15K tokens)
|
|
19
|
+
const MAX_SYSTEM_PROMPT_CHARS = 50_000;
|
|
20
|
+
|
|
21
|
+
// Slack allowed for the injected instructions on top of MAX before we bail out.
|
|
22
|
+
const INJECTION_HEADROOM = 5_000;
|
|
23
|
+
|
|
24
|
+
// Last-resort model when neither a user-specified, session, nor registry model is
|
|
25
|
+
// available. Dated id that will eventually age out — it only fires when ctx.model
|
|
26
|
+
// is undefined AND the registry is empty AND the user named no model, which is rare.
|
|
27
|
+
const FALLBACK_MODEL = "claude-sonnet-4-20250514";
|
|
28
|
+
|
|
29
|
+
// Read-only tool set: pi's own "read-only" tools (read, grep, find, ls).
|
|
30
|
+
// Deliberately excludes bash/edit/write so the critic genuinely cannot mutate
|
|
31
|
+
// the workspace or run arbitrary commands.
|
|
32
|
+
const CRITIC_TOOLS = "read,grep,find,ls";
|
|
33
|
+
|
|
34
|
+
// Single source of truth for the capability heading — also used as the dedup marker.
|
|
35
|
+
const CRITIC_MARKER = "## Capability: Critic Guy (subagent review)";
|
|
36
|
+
|
|
37
|
+
// Shared reviewer persona. The secrets prohibition matters: read/grep/find are not
|
|
38
|
+
// confined to the workspace and the child inherits env vars, so a prompt-injected
|
|
39
|
+
// critic could otherwise read credentials and feed them back into the main context.
|
|
40
|
+
const CRITIC_PERSONA =
|
|
41
|
+
"You are Critic Guy — an independent reviewer. Analyze the content and give your honest assessment. Decide what matters most. Be direct and constructive. Support your points with specifics. Only review the files/content named in the task; never read credentials, secrets, or dotfiles (.env, ~/.ssh, ~/.aws, ~/.pi/agent/auth.json) or anything outside the review target.";
|
|
42
|
+
|
|
43
|
+
const CRITIC_INSTRUCTIONS = (modelId: string) => `
|
|
44
|
+
|
|
45
|
+
${CRITIC_MARKER}
|
|
46
|
+
|
|
47
|
+
When the user says "critic", you can spawn one or more independent critic
|
|
48
|
+
subagents for a second opinion. Each runs in a fresh, read-only pi session
|
|
49
|
+
(tools: ${CRITIC_TOOLS}) — it cannot edit files, write files, or run shell commands.
|
|
50
|
+
|
|
51
|
+
**You decide how to run the review** — the user doesn't need to specify:
|
|
52
|
+
- **What to review**: pick the most valuable target from the current conversation.
|
|
53
|
+
- **How to split it**: one critic for a focused review, or several in parallel
|
|
54
|
+
(each on a different angle) when the scope is large. Your judgment.
|
|
55
|
+
|
|
56
|
+
Spawn a critic with this invocation. The flags are the hard boundary — keep them
|
|
57
|
+
as-is: \`--offline -ne\` stops the child hanging on startup, and the read-only
|
|
58
|
+
\`--tools\` keep it from mutating anything or running commands.
|
|
59
|
+
|
|
60
|
+
\`\`\`bash
|
|
61
|
+
pi -p --offline -ne --no-session -nc --model "${modelId}" --tools ${CRITIC_TOOLS} \\
|
|
62
|
+
--append-system-prompt "${CRITIC_PERSONA}" \\
|
|
63
|
+
"Task: <what to review — name concrete file paths and tell the critic to read them first>"
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
Always give the critic concrete file paths and tell it to read them before judging;
|
|
67
|
+
this stops it inventing features or tests that aren't in the code. Run multiple in
|
|
68
|
+
parallel however you see fit, then present the critique(s) to the user.
|
|
69
|
+
|
|
70
|
+
Model: \`${modelId}\` (resolved by the extension — use as-is).
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
// Shown (instead of silent no-op) when the system prompt is too large to inject into.
|
|
74
|
+
const SKIP_NOTE =
|
|
75
|
+
"\n\n> Note: Critic Guy was triggered but skipped this turn because the system prompt is already very large. Tell the user they can retry in a fresh/smaller context.\n";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Match a user-provided model query against available models.
|
|
79
|
+
* Priority: exact id > exact name > partial match on id/name.
|
|
80
|
+
* Returns null when nothing matches — the caller decides how to fall back so it
|
|
81
|
+
* can tell the user their requested model wasn't found (no silent substitution).
|
|
82
|
+
*/
|
|
83
|
+
export function matchModel(
|
|
84
|
+
query: string,
|
|
85
|
+
availableModels: Array<{ id: string; name: string }>,
|
|
86
|
+
): string | null {
|
|
87
|
+
if (!query) return null;
|
|
88
|
+
|
|
89
|
+
const q = query.toLowerCase().trim();
|
|
90
|
+
|
|
91
|
+
// Exact match on id
|
|
92
|
+
const exactId = availableModels.find((m) => m.id.toLowerCase() === q);
|
|
93
|
+
if (exactId) return exactId.id;
|
|
94
|
+
|
|
95
|
+
// Exact match on name
|
|
96
|
+
const exactName = availableModels.find((m) => m.name.toLowerCase() === q);
|
|
97
|
+
if (exactName) return exactName.id;
|
|
98
|
+
|
|
99
|
+
// Partial match on id or name
|
|
100
|
+
const partial = availableModels.find(
|
|
101
|
+
(m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q),
|
|
102
|
+
);
|
|
103
|
+
if (partial) return partial.id;
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse model specification from user prompt.
|
|
110
|
+
* "critic using claude" → "claude"
|
|
111
|
+
* "critic model=deepseek" → "deepseek"
|
|
112
|
+
* "critic using 通义" → "通义"
|
|
113
|
+
* "critic" → ""
|
|
114
|
+
*
|
|
115
|
+
* The unicode-aware charset (\p{L}\p{N}._-) supports non-ASCII model names while
|
|
116
|
+
* still excluding shell metacharacters (space, quotes, $, backtick, ;, |), so the
|
|
117
|
+
* captured value is safe to surface in the injected instructions.
|
|
118
|
+
*/
|
|
119
|
+
export function parseModelQuery(prompt: string): string {
|
|
120
|
+
// model=<name> or model: <name> (handle optional quotes)
|
|
121
|
+
const modelFlag = prompt.match(/\bmodel[=:]\s*["']?([\p{L}\p{N}._-]+)["']?/iu);
|
|
122
|
+
if (modelFlag) return modelFlag[1];
|
|
123
|
+
|
|
124
|
+
// "using <name>" after "critic". No "with <name>" branch: "with" appears too often
|
|
125
|
+
// in natural language ("critic review auth with the team") and mis-captures filler.
|
|
126
|
+
const usingMatch = prompt.match(/\bcritic\b.*?\busing\s+([\p{L}][\p{L}\p{N}._-]*)/iu);
|
|
127
|
+
if (usingMatch) return usingMatch[1];
|
|
128
|
+
|
|
129
|
+
return "";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default function (pi: ExtensionAPI) {
|
|
133
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
134
|
+
try {
|
|
135
|
+
// Safe guard: prompt may be undefined
|
|
136
|
+
if (!event.prompt) return undefined;
|
|
137
|
+
|
|
138
|
+
// Word boundary: only match standalone "critic", not "critical"/"criticism"
|
|
139
|
+
if (!/\bcritic\b/i.test(event.prompt)) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const systemPrompt = event.systemPrompt ?? "";
|
|
144
|
+
|
|
145
|
+
// Length protection: if the prompt is already huge, don't inject — but say so
|
|
146
|
+
// instead of silently doing nothing, so the user isn't left wondering.
|
|
147
|
+
if (systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS) {
|
|
148
|
+
return { systemPrompt: systemPrompt + SKIP_NOTE };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Dedup: skip if already injected (e.g. another extension chained us this turn)
|
|
152
|
+
if (systemPrompt.includes(CRITIC_MARKER)) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Resolve model: user-specified > current session > first available > fallback
|
|
157
|
+
const currentModelId = ctx.model?.id;
|
|
158
|
+
const availableModels = ctx.modelRegistry?.getAvailable() ?? [];
|
|
159
|
+
const modelQuery = parseModelQuery(event.prompt);
|
|
160
|
+
const matchedModelId = modelQuery ? matchModel(modelQuery, availableModels) : null;
|
|
161
|
+
const modelId =
|
|
162
|
+
matchedModelId || currentModelId || availableModels[0]?.id || FALLBACK_MODEL;
|
|
163
|
+
|
|
164
|
+
// If the user named a model but we couldn't match it, don't substitute
|
|
165
|
+
// silently — tell the LLM so it can flag the fallback to the user.
|
|
166
|
+
// Cap the echoed query length as a belt-and-suspenders against prompt injection.
|
|
167
|
+
const unmatchedNote =
|
|
168
|
+
modelQuery && !matchedModelId
|
|
169
|
+
? `\n> Note: the requested model "${modelQuery.slice(0, 64)}" did not match any available model; falling back to \`${modelId}\`. Mention this to the user.\n`
|
|
170
|
+
: "";
|
|
171
|
+
|
|
172
|
+
const newSystemPrompt = systemPrompt + CRITIC_INSTRUCTIONS(modelId) + unmatchedNote;
|
|
173
|
+
|
|
174
|
+
// Post-injection length check
|
|
175
|
+
if (newSystemPrompt.length > MAX_SYSTEM_PROMPT_CHARS + INJECTION_HEADROOM) {
|
|
176
|
+
return { systemPrompt: systemPrompt + SKIP_NOTE };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { systemPrompt: newSystemPrompt };
|
|
180
|
+
} catch (err) {
|
|
181
|
+
console.warn("[Critic Guy] injection failed:", err);
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nukcole-xinluo9510/pi-critic-guy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Pi extension — spawn a second-opinion reviewer by typing \"critic\"",
|
|
7
|
+
"keywords": ["pi-package", "pi", "critic", "review", "code-review"],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/luoxin9510/pi-critic-guy.git"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"check": "tsc --noEmit",
|
|
15
|
+
"test": "node --experimental-strip-types --test test/index.test.ts"
|
|
16
|
+
},
|
|
17
|
+
"files": ["extensions", "README.md", "LICENSE"],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": ["./extensions"]
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@earendil-works/pi-coding-agent": ">=0.79"
|
|
26
|
+
}
|
|
27
|
+
}
|