@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 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
+ }