@sreetej510/pi-shipd-checks 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 +21 -0
- package/README.md +104 -0
- package/package.json +48 -0
- package/src/agents.ts +198 -0
- package/src/command.ts +387 -0
- package/src/config.ts +84 -0
- package/src/git.ts +30 -0
- package/src/index.ts +125 -0
- package/src/progress.ts +11 -0
- package/src/prompts.ts +202 -0
- package/src/report.ts +142 -0
- package/src/roles.ts +20 -0
- package/src/rubric.ts +32 -0
- package/src/state.ts +31 -0
- package/src/tools.ts +140 -0
- package/src/types.ts +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 omega
|
|
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,104 @@
|
|
|
1
|
+
# @sreetej510/pi-shipd-checks
|
|
2
|
+
|
|
3
|
+
A [pi](https://github.com/earendil-works/pi) coding agent extension that runs a strict,
|
|
4
|
+
multi-agent review of a benchmark task's `agent_prompt.md`, `test.patch`, and `solution.patch`
|
|
5
|
+
against a fairness rubric, and finds behavioral test-coverage gaps that could let an incorrect
|
|
6
|
+
agent solution slip past the hidden tests.
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
For the flags you pass, `/checks`:
|
|
11
|
+
|
|
12
|
+
1. Snapshots the current git `HEAD` into a throwaway temp directory (via
|
|
13
|
+
`git archive HEAD | tar -x`) — it never touches your working directory, staged, or
|
|
14
|
+
uncommitted changes.
|
|
15
|
+
2. Copies `agent_prompt.md`, `solution.patch`, and `test.patch` from your project root into
|
|
16
|
+
that temp dir.
|
|
17
|
+
3. Spawns one read-only reviewer agent per focus area you selected — **description**, **tests**,
|
|
18
|
+
**solution** — each restricted to `read`/`grep`/`find`/`ls` tools plus a single
|
|
19
|
+
`submit_review_report` tool it must call with a structured `PASS`/`FAIL` verdict, reasons,
|
|
20
|
+
and notes.
|
|
21
|
+
4. Optionally runs a 2-stage behavioral test-gap analysis: an exhaustive researcher agent
|
|
22
|
+
proposes candidate gaps (required-but-untested edge cases), then a strict, independent
|
|
23
|
+
filter agent re-verifies each one against the fairness rules. This never turns a `PASS` into
|
|
24
|
+
a `FAIL` — it's purely informational.
|
|
25
|
+
5. Posts a one-line chat summary and merges the results into `shipd_report.json` in your
|
|
26
|
+
project root. Running flags separately, in any order, builds up one combined report instead
|
|
27
|
+
of overwriting it — `overall` only becomes a confident `PASS`/`FAIL` once all 3 focus
|
|
28
|
+
reviewers have run at least once.
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
All flags except `--config` are additive/combinable, e.g. `/checks --tests --gap-finder` runs
|
|
33
|
+
just the tests reviewer plus the gap-finder/filter stages. `--config` must be used alone.
|
|
34
|
+
|
|
35
|
+
| Command | Effect |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `/checks` | List available options (runs nothing) |
|
|
38
|
+
| `/checks --all` | Run all 3 focus reviewers + test-gap analysis |
|
|
39
|
+
| `/checks --review` | Run only the 3 focus reviewer agents |
|
|
40
|
+
| `/checks --description` | Run only the problem-description reviewer |
|
|
41
|
+
| `/checks --tests` | Run only the tests reviewer |
|
|
42
|
+
| `/checks --solution` | Run only the solution reviewer |
|
|
43
|
+
| `/checks --gap-finder` | Run only the test-gap finder + filter agents |
|
|
44
|
+
| `/checks --config` | Set the reviewer model and thinking level |
|
|
45
|
+
|
|
46
|
+
**Shortcut:** `Ctrl+Shift+X` cancels an in-progress `/checks` run.
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
`/checks --config` lets you pick a model from your `enabledModels` list in `settings.json`,
|
|
51
|
+
then (if the model supports more than one) a thinking level. The currently configured
|
|
52
|
+
model/level is highlighted `[current]` in the picker. Settings are saved globally to
|
|
53
|
+
`~/.pi/agent/checks-config.json` and shared by all reviewer/gap-finder/validator agents.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install -g @sreetej510/pi-shipd-checks
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then add it to your pi `settings.json`:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"packages": ["npm:@sreetej510/pi-shipd-checks"]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or, for local development, point at the entry point directly:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"extensions": ["/absolute/path/to/pi-extensions/extensions/pi-shipd-checks/src/index.ts"]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## File layout
|
|
78
|
+
|
|
79
|
+
| File | Responsibility |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `src/index.ts` | Extension entry point: message renderer, cancel shortcut, command registration |
|
|
82
|
+
| `src/command.ts` | The `/checks` command: argument parsing, `--config` flow, run orchestration |
|
|
83
|
+
| `src/agents.ts` | Spawns and races the reviewer + gap-finder/validator agent sessions |
|
|
84
|
+
| `src/prompts.ts` | All prompt text sent to those agents |
|
|
85
|
+
| `src/tools.ts` | Custom tools the agents call to submit their structured results |
|
|
86
|
+
| `src/rubric.ts` | Embedded guidelines/fairness rubric text + per-role section loaders |
|
|
87
|
+
| `src/roles.ts` | The 3 reviewer roles (description/tests/solution) metadata |
|
|
88
|
+
| `src/report.ts` | `shipd_report.json` load/merge/summary logic |
|
|
89
|
+
| `src/config.ts` | `~/.pi/agent/checks-config.json` + `settings.json` helpers (models, thinking levels, shell path) |
|
|
90
|
+
| `src/git.ts` | Clean, non-mutating git `HEAD` snapshot into a scratch directory |
|
|
91
|
+
| `src/progress.ts` | Progress-bar widget rendering |
|
|
92
|
+
| `src/state.ts` | Shared "run in progress" / cancel state between the command and the shortcut |
|
|
93
|
+
| `src/types.ts` | Shared TypeScript types |
|
|
94
|
+
|
|
95
|
+
To change reviewer strictness or wording, edit `prompts.ts`. To change the rubric/fairness text
|
|
96
|
+
itself, edit `rubric.ts`. To add a new tool, add it in `tools.ts` and wire it up in `agents.ts`.
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npm install
|
|
102
|
+
npm run --workspace @sreetej510/pi-shipd-checks check # biome + typecheck
|
|
103
|
+
npm run --workspace @sreetej510/pi-shipd-checks format
|
|
104
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sreetej510/pi-shipd-checks",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Pi extension that runs a strict, multi-agent fairness review of a benchmark task's agent_prompt.md, test.patch, and solution.patch, plus behavioral test-gap analysis, via /checks.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"pi",
|
|
12
|
+
"review",
|
|
13
|
+
"checks",
|
|
14
|
+
"shipd"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./src/index.ts"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"check": "biome check . && npm run typecheck",
|
|
28
|
+
"format": "biome check --write .",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"typebox": "^1.3.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "^2.5.3",
|
|
36
|
+
"@earendil-works/pi-coding-agent": "^0.80.3",
|
|
37
|
+
"@earendil-works/pi-tui": "^0.80.3",
|
|
38
|
+
"typescript": "^5.7.3"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/Sreetej510/pi-extensions",
|
|
43
|
+
"directory": "extensions/pi-shipd-checks"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawns the throwaway reviewer / gap-finder / gap-validator agent sessions,
|
|
3
|
+
* races each against a timeout + external cancel signal, and pulls the
|
|
4
|
+
* structured result back out of the tool-call capture object.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { buildGapFinderPrompt, buildGapValidatorPrompt, buildReviewerPrompt } from "./prompts.js";
|
|
10
|
+
import {
|
|
11
|
+
createGapFinderTool,
|
|
12
|
+
createGapValidatorTool,
|
|
13
|
+
createReportTool,
|
|
14
|
+
GAP_FINDER_TOOL_NAME,
|
|
15
|
+
GAP_VALIDATOR_TOOL_NAME,
|
|
16
|
+
REPORT_TOOL_NAME,
|
|
17
|
+
} from "./tools.js";
|
|
18
|
+
import type {
|
|
19
|
+
GapStageResult,
|
|
20
|
+
ReviewerRole,
|
|
21
|
+
ReviewReport,
|
|
22
|
+
TestGapCandidate,
|
|
23
|
+
TestGapFinal,
|
|
24
|
+
ThinkingLevel,
|
|
25
|
+
} from "./types.js";
|
|
26
|
+
|
|
27
|
+
export const REVIEWER_TIMEOUT_MS = 15 * 60 * 1000;
|
|
28
|
+
export const REVIEWER_TOOLS = ["read", "grep", "find", "ls"] as const;
|
|
29
|
+
|
|
30
|
+
/** Outcome of racing an agent turn against a timeout and an external cancel signal. */
|
|
31
|
+
type AgentTurnOutcome = "done" | "timedOut" | "cancelled";
|
|
32
|
+
|
|
33
|
+
async function raceAgentTurn(work: () => Promise<void>, cancelSignal: AbortSignal): Promise<AgentTurnOutcome> {
|
|
34
|
+
let outcome: AgentTurnOutcome = cancelSignal.aborted ? "cancelled" : "done";
|
|
35
|
+
await Promise.race([
|
|
36
|
+
work().then(() => {
|
|
37
|
+
if (!cancelSignal.aborted) outcome = "done";
|
|
38
|
+
}),
|
|
39
|
+
new Promise<void>((resolve) => {
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
outcome = "timedOut";
|
|
42
|
+
resolve();
|
|
43
|
+
}, REVIEWER_TIMEOUT_MS);
|
|
44
|
+
}),
|
|
45
|
+
new Promise<void>((resolve) => {
|
|
46
|
+
if (cancelSignal.aborted) return resolve();
|
|
47
|
+
cancelSignal.addEventListener(
|
|
48
|
+
"abort",
|
|
49
|
+
() => {
|
|
50
|
+
outcome = "cancelled";
|
|
51
|
+
resolve();
|
|
52
|
+
},
|
|
53
|
+
{ once: true },
|
|
54
|
+
);
|
|
55
|
+
}),
|
|
56
|
+
]);
|
|
57
|
+
return outcome;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function runReviewer(opts: {
|
|
61
|
+
pi: ExtensionAPI;
|
|
62
|
+
role: ReviewerRole;
|
|
63
|
+
tempDir: string;
|
|
64
|
+
model: unknown;
|
|
65
|
+
thinkingLevel: ThinkingLevel;
|
|
66
|
+
rubric: string;
|
|
67
|
+
fairnessRules: string;
|
|
68
|
+
cancelSignal: AbortSignal;
|
|
69
|
+
}): Promise<ReviewReport> {
|
|
70
|
+
const capture: { report?: ReviewReport } = {};
|
|
71
|
+
// biome-ignore lint: model typed loosely to avoid depending on internal Model<Api> generics
|
|
72
|
+
const model = opts.model as any;
|
|
73
|
+
const { session } = await createAgentSession({
|
|
74
|
+
cwd: opts.tempDir,
|
|
75
|
+
model,
|
|
76
|
+
thinkingLevel: opts.thinkingLevel === "off" ? undefined : (opts.thinkingLevel as any),
|
|
77
|
+
tools: [...REVIEWER_TOOLS, REPORT_TOOL_NAME],
|
|
78
|
+
customTools: [createReportTool(capture)],
|
|
79
|
+
sessionManager: SessionManager.inMemory(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const outcome = await raceAgentTurn(async () => {
|
|
84
|
+
await session.prompt(buildReviewerPrompt(opts.role, opts.rubric, opts.fairnessRules));
|
|
85
|
+
await session.waitForIdle();
|
|
86
|
+
}, opts.cancelSignal);
|
|
87
|
+
|
|
88
|
+
if (outcome === "cancelled") {
|
|
89
|
+
await session.abort();
|
|
90
|
+
return {
|
|
91
|
+
verdict: "FAIL",
|
|
92
|
+
summary: `${opts.role.label} reviewer cancelled`,
|
|
93
|
+
reasons: ["Cancelled by user."],
|
|
94
|
+
notes: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (outcome === "timedOut") {
|
|
99
|
+
await session.abort();
|
|
100
|
+
return {
|
|
101
|
+
verdict: "FAIL",
|
|
102
|
+
summary: `${opts.role.label} reviewer timed out`,
|
|
103
|
+
reasons: [`Reviewer did not finish within ${REVIEWER_TIMEOUT_MS / 1000}s.`],
|
|
104
|
+
notes: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return {
|
|
109
|
+
verdict: "FAIL",
|
|
110
|
+
summary: `${opts.role.label} reviewer errored`,
|
|
111
|
+
reasons: [`Reviewer agent failed: ${err instanceof Error ? err.message : String(err)}`],
|
|
112
|
+
notes: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!capture.report) {
|
|
117
|
+
return {
|
|
118
|
+
verdict: "FAIL",
|
|
119
|
+
summary: `${opts.role.label} reviewer did not submit a report`,
|
|
120
|
+
reasons: [`The reviewer agent finished without calling ${REPORT_TOOL_NAME}.`],
|
|
121
|
+
notes: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return capture.report;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function runGapFinder(opts: {
|
|
128
|
+
tempDir: string;
|
|
129
|
+
model: unknown;
|
|
130
|
+
thinkingLevel: ThinkingLevel;
|
|
131
|
+
testRubric: string;
|
|
132
|
+
fairnessRules: string;
|
|
133
|
+
cancelSignal: AbortSignal;
|
|
134
|
+
}): Promise<GapStageResult<TestGapCandidate>> {
|
|
135
|
+
const capture: { gaps?: TestGapCandidate[] } = {};
|
|
136
|
+
// biome-ignore lint: model typed loosely to avoid depending on internal Model<Api> generics
|
|
137
|
+
const model = opts.model as any;
|
|
138
|
+
const { session } = await createAgentSession({
|
|
139
|
+
cwd: opts.tempDir,
|
|
140
|
+
model,
|
|
141
|
+
thinkingLevel: opts.thinkingLevel === "off" ? undefined : (opts.thinkingLevel as any),
|
|
142
|
+
tools: [...REVIEWER_TOOLS, GAP_FINDER_TOOL_NAME],
|
|
143
|
+
customTools: [createGapFinderTool(capture)],
|
|
144
|
+
sessionManager: SessionManager.inMemory(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const outcome = await raceAgentTurn(async () => {
|
|
149
|
+
await session.prompt(buildGapFinderPrompt(opts.testRubric, opts.fairnessRules));
|
|
150
|
+
await session.waitForIdle();
|
|
151
|
+
}, opts.cancelSignal);
|
|
152
|
+
if (outcome !== "done") {
|
|
153
|
+
await session.abort();
|
|
154
|
+
return { status: outcome, gaps: [] };
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
return { status: "error", gaps: [] };
|
|
158
|
+
}
|
|
159
|
+
if (!capture.gaps) return { status: "noSubmission", gaps: [] };
|
|
160
|
+
return { status: "ok", gaps: capture.gaps };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function runGapValidator(opts: {
|
|
164
|
+
tempDir: string;
|
|
165
|
+
model: unknown;
|
|
166
|
+
thinkingLevel: ThinkingLevel;
|
|
167
|
+
testRubric: string;
|
|
168
|
+
fairnessRules: string;
|
|
169
|
+
candidates: TestGapCandidate[];
|
|
170
|
+
cancelSignal: AbortSignal;
|
|
171
|
+
}): Promise<GapStageResult<TestGapFinal>> {
|
|
172
|
+
const capture: { gaps?: TestGapFinal[] } = {};
|
|
173
|
+
// biome-ignore lint: model typed loosely to avoid depending on internal Model<Api> generics
|
|
174
|
+
const model = opts.model as any;
|
|
175
|
+
const { session } = await createAgentSession({
|
|
176
|
+
cwd: opts.tempDir,
|
|
177
|
+
model,
|
|
178
|
+
thinkingLevel: opts.thinkingLevel === "off" ? undefined : (opts.thinkingLevel as any),
|
|
179
|
+
tools: [...REVIEWER_TOOLS, GAP_VALIDATOR_TOOL_NAME],
|
|
180
|
+
customTools: [createGapValidatorTool(capture)],
|
|
181
|
+
sessionManager: SessionManager.inMemory(),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const outcome = await raceAgentTurn(async () => {
|
|
186
|
+
await session.prompt(buildGapValidatorPrompt(opts.candidates, opts.testRubric, opts.fairnessRules));
|
|
187
|
+
await session.waitForIdle();
|
|
188
|
+
}, opts.cancelSignal);
|
|
189
|
+
if (outcome !== "done") {
|
|
190
|
+
await session.abort();
|
|
191
|
+
return { status: outcome, gaps: [] };
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
return { status: "error", gaps: [] };
|
|
195
|
+
}
|
|
196
|
+
if (!capture.gaps) return { status: "noSubmission", gaps: [] };
|
|
197
|
+
return { status: "ok", gaps: capture.gaps };
|
|
198
|
+
}
|