@sreetej510/pi-shipd-checks 0.1.1 → 0.1.2

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/src/agents.ts DELETED
@@ -1,198 +0,0 @@
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
- }
package/src/command.ts DELETED
@@ -1,387 +0,0 @@
1
- /** Registers the `/checks` command: argument parsing, the --config flow, and the main run orchestration. */
2
-
3
- import { randomUUID } from "node:crypto";
4
- import { copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
5
- import { tmpdir } from "node:os";
6
- import { join } from "node:path";
7
- import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
8
- import { runGapFinder, runGapValidator, runReviewer } from "./agents.js";
9
- import {
10
- getSupportedThinkingLevels,
11
- loadEnabledModelRefs,
12
- loadReviewConfig,
13
- saveReviewConfig,
14
- splitProviderModel,
15
- } from "./config.js";
16
- import { snapshotGitHead } from "./git.js";
17
- import { PROGRESS_WIDGET_KEY, renderProgressLines } from "./progress.js";
18
- import { buildRunSummary, loadExistingReport, mergeReport, REQUIRED_FILES } from "./report.js";
19
- import { ROLES } from "./roles.js";
20
- import { loadFairnessRules, loadGuidelinesSections } from "./rubric.js";
21
- import { endReview, isReviewInProgress, startReview } from "./state.js";
22
- import type {
23
- CommandOption,
24
- GapStageResult,
25
- ReviewerRole,
26
- ReviewReport,
27
- TestGapCandidate,
28
- TestGapFinal,
29
- ThinkingLevel,
30
- } from "./types.js";
31
-
32
- export const CANCEL_SHORTCUT_LABEL = "Ctrl+Shift+X";
33
-
34
- const COMMAND_COMPLETIONS: readonly CommandOption[] = [
35
- { value: "--all", label: "--all", description: "Run everything: 3 focus reviewers + test-gap finder/filter" },
36
- {
37
- value: "--review",
38
- label: "--review",
39
- description: "Run only the 3 focus reviewer agents (description/tests/solution)",
40
- },
41
- {
42
- value: "--description",
43
- label: "--description",
44
- description: "Run only the problem-description (prompt) focus reviewer",
45
- },
46
- { value: "--tests", label: "--tests", description: "Run only the tests focus reviewer" },
47
- { value: "--solution", label: "--solution", description: "Run only the solution focus reviewer" },
48
- {
49
- value: "--gap-finder",
50
- label: "--gap-finder",
51
- description: "Run only the 2-step behavioral test-gap finder + strict filter agents",
52
- },
53
- { value: "--config", label: "--config", description: "Set the reviewer model and thinking level" },
54
- ];
55
-
56
- function getArgumentCompletions(prefix: string) {
57
- // Flags are additive (e.g. "--tests --gap-finder"), so completions must
58
- // account for flags already typed: drop them from suggestions, and keep
59
- // --config mutually exclusive with everything else.
60
- const trimmed = prefix.trimStart();
61
- const trailingSpace = /\s$/.test(trimmed);
62
- const tokens = trimmed.trimEnd().split(/\s+/).filter(Boolean);
63
- const current = trailingSpace ? "" : (tokens.at(-1) ?? "");
64
- const usedTokens = new Set(trailingSpace ? tokens : tokens.slice(0, -1));
65
- const completionPrefix = trailingSpace ? trimmed : trimmed.slice(0, trimmed.length - current.length);
66
-
67
- const hasConfig = usedTokens.has("--config");
68
- const hasOtherFlag = [...usedTokens].some((t) => t !== "--config");
69
-
70
- const candidates = COMMAND_COMPLETIONS.filter((o) => {
71
- if (usedTokens.has(o.value)) return false;
72
- if (hasConfig) return false;
73
- if (hasOtherFlag && o.value === "--config") return false;
74
- return o.value.startsWith(current);
75
- });
76
-
77
- return candidates.length > 0 ? candidates.map((o) => ({ ...o, value: `${completionPrefix}${o.value}` })) : null;
78
- }
79
-
80
- /** Interactive `/checks --config` flow: pick a model (highlighting the current one), then a thinking level. */
81
- async function runConfigFlow(ctx: ExtensionCommandContext) {
82
- if (ctx.mode !== "tui") {
83
- ctx.ui.notify("/checks --config requires interactive mode", "error");
84
- return;
85
- }
86
- const refs = loadEnabledModelRefs();
87
- if (refs.length === 0) {
88
- ctx.ui.notify("No enabledModels configured in settings.json.", "error");
89
- return;
90
- }
91
- const existingConfig = loadReviewConfig();
92
- const available = ctx.modelRegistry.getAll();
93
- const labeled = refs
94
- .map((ref) => {
95
- const parsed = splitProviderModel(ref);
96
- if (!parsed) return null;
97
- const found = available.find((m: any) => m.provider === parsed.provider && m.id === parsed.modelId);
98
- const isCurrent = existingConfig?.provider === parsed.provider && existingConfig?.modelId === parsed.modelId;
99
- const base = found ? `${found.name} (${ref})` : ref;
100
- return { ref, parsed, display: isCurrent ? `${base} [current]` : base, found, isCurrent };
101
- })
102
- .filter((x): x is NonNullable<typeof x> => x !== null);
103
-
104
- if (labeled.length === 0) {
105
- ctx.ui.notify("Could not resolve any enabledModels entries.", "error");
106
- return;
107
- }
108
-
109
- // Surface the currently configured model at the top of the list, alongside
110
- // the "[current]" label, since ctx.ui.select can't pre-position the cursor.
111
- const orderedModels = [...labeled].sort((a, b) => (a.isCurrent === b.isCurrent ? 0 : a.isCurrent ? -1 : 1));
112
-
113
- const choice = await ctx.ui.select(
114
- "Select model for checks reviewer agents",
115
- orderedModels.map((l) => l.display),
116
- );
117
- if (!choice) return;
118
- const selected = labeled.find((l) => l.display === choice);
119
- if (!selected) return;
120
-
121
- let thinkingLevel: ThinkingLevel = "off";
122
- const supportedLevels = getSupportedThinkingLevels(selected.found);
123
- if (supportedLevels.length > 1) {
124
- const currentLevel = selected.isCurrent ? existingConfig?.thinkingLevel : undefined;
125
- const levelOptions = [...supportedLevels].sort((a, b) =>
126
- (a === currentLevel) === (b === currentLevel) ? 0 : a === currentLevel ? -1 : 1,
127
- );
128
- const levelDisplay = (l: ThinkingLevel) => (l === currentLevel ? `${l} [current]` : l);
129
- const level = await ctx.ui.select("Select thinking level for reviewer agents", levelOptions.map(levelDisplay));
130
- if (!level) return;
131
- thinkingLevel = levelOptions.find((l) => levelDisplay(l) === level) ?? (level as ThinkingLevel);
132
- }
133
-
134
- saveReviewConfig({ provider: selected.parsed.provider, modelId: selected.parsed.modelId, thinkingLevel });
135
- ctx.ui.notify(
136
- `checks config saved: ${selected.parsed.provider}/${selected.parsed.modelId} (thinking: ${thinkingLevel})`,
137
- "info",
138
- );
139
- }
140
-
141
- export function registerChecksCommand(pi: ExtensionAPI) {
142
- pi.registerCommand("checks", {
143
- description: "Strict review of agent_prompt.md/test.patch/solution.patch. Requires an option — see /checks.",
144
- getArgumentCompletions,
145
-
146
- handler: async (args, ctx) => {
147
- const sub = args.trim();
148
-
149
- // ── /checks (no args) — list options, run nothing ──
150
- if (sub === "") {
151
- ctx.ui.notify(COMMAND_COMPLETIONS.map((o) => `${o.value} — ${o.description}`).join("\n"), "info");
152
- return;
153
- }
154
-
155
- const tokens = [...new Set(sub.split(/\s+/).filter(Boolean))];
156
- const knownFlags = new Set(COMMAND_COMPLETIONS.map((o) => o.value));
157
- const unknown = tokens.filter((t) => !knownFlags.has(t));
158
- if (unknown.length > 0) {
159
- ctx.ui.notify(
160
- `Unknown option(s): ${unknown.join(", ")}. Run /checks with no arguments to see the available options.`,
161
- "warning",
162
- );
163
- return;
164
- }
165
-
166
- // ── /checks --config ───────────────────────────
167
- if (tokens.includes("--config")) {
168
- if (tokens.length > 1) {
169
- ctx.ui.notify("--config cannot be combined with other options.", "warning");
170
- return;
171
- }
172
- await runConfigFlow(ctx);
173
- return;
174
- }
175
-
176
- // Flags are additive — any combination of role flags / --review / --all /
177
- // --gap-finder may be passed together (e.g. "--tests --gap-finder" runs
178
- // just the tests reviewer plus the gap-finder/filter stages).
179
- const roleKeys = new Set<ReviewerRole["key"]>();
180
- if (tokens.includes("--all") || tokens.includes("--review")) {
181
- for (const role of ROLES) roleKeys.add(role.key);
182
- }
183
- for (const role of ROLES) {
184
- if (tokens.includes(`--${role.key}`)) roleKeys.add(role.key);
185
- }
186
- const runGapStages = tokens.includes("--all") || tokens.includes("--gap-finder");
187
- const activeRoles = ROLES.filter((r) => roleKeys.has(r.key));
188
- const runReviewers = activeRoles.length > 0;
189
-
190
- if (!runReviewers && !runGapStages) {
191
- ctx.ui.notify(
192
- `Nothing to run for: ${tokens.join(" ")}. Run /checks with no arguments to see the available options.`,
193
- "warning",
194
- );
195
- return;
196
- }
197
- const runLabel = tokens.join(" ");
198
-
199
- if (isReviewInProgress()) {
200
- ctx.ui.notify("A checks run is already in progress.", "warning");
201
- return;
202
- }
203
-
204
- const config = loadReviewConfig();
205
- if (!config) {
206
- ctx.ui.notify("No reviewer model configured. Run /checks --config first.", "error");
207
- return;
208
- }
209
-
210
- const model = ctx.modelRegistry.find(config.provider, config.modelId);
211
- if (!model) {
212
- ctx.ui.notify(
213
- `Configured model ${config.provider}/${config.modelId} not found. Run /checks --config again.`,
214
- "error",
215
- );
216
- return;
217
- }
218
- if (!ctx.modelRegistry.hasConfiguredAuth(model)) {
219
- ctx.ui.notify(`No auth configured for ${config.provider}/${config.modelId}.`, "error");
220
- return;
221
- }
222
-
223
- const missing = REQUIRED_FILES.filter((f) => !existsSync(join(ctx.cwd, f)));
224
- if (missing.length > 0) {
225
- ctx.ui.notify(`Missing required file(s) in project root: ${missing.join(", ")}`, "error");
226
- return;
227
- }
228
-
229
- const reviewAbort = startReview();
230
- // Stage count depends on which flags were passed: however many focus
231
- // reviewers were selected, plus the gap-finder + gap-filter stages if requested.
232
- const TOTAL_STAGES = (runReviewers ? activeRoles.length : 0) + (runGapStages ? 2 : 0);
233
- ctx.ui.setWidget(PROGRESS_WIDGET_KEY, renderProgressLines("preparing clean snapshot", 0, TOTAL_STAGES));
234
- ctx.ui.notify(`checks (${runLabel}) started. Press ${CANCEL_SHORTCUT_LABEL} to cancel.`, "info");
235
-
236
- let tempDir: string | undefined;
237
- try {
238
- // Unique per-run directory (UUID) so concurrent runs — even across different
239
- // projects/sessions — never collide on the same temp path.
240
- const dir = join(tmpdir(), `checks-${randomUUID()}`);
241
- mkdirSync(dir, { recursive: true });
242
- tempDir = dir;
243
-
244
- const snapshot = await snapshotGitHead(pi, ctx.cwd, dir);
245
- if (snapshot.status === "error") {
246
- ctx.ui.notify(`checks: ${snapshot.error}`, "error");
247
- return;
248
- }
249
-
250
- for (const f of REQUIRED_FILES) {
251
- copyFileSync(join(ctx.cwd, f), join(dir, f));
252
- }
253
-
254
- const sections = loadGuidelinesSections();
255
- const fairnessRules = loadFairnessRules();
256
- let completed = 0;
257
-
258
- let reports: ReviewReport[] = [];
259
- if (runReviewers) {
260
- ctx.ui.setWidget(PROGRESS_WIDGET_KEY, renderProgressLines("reviewing", completed, TOTAL_STAGES));
261
- reports = await Promise.all(
262
- activeRoles.map((role) =>
263
- runReviewer({
264
- pi,
265
- role,
266
- tempDir: dir,
267
- model,
268
- thinkingLevel: config.thinkingLevel,
269
- rubric: sections[role.key],
270
- fairnessRules,
271
- cancelSignal: reviewAbort.signal,
272
- }).then((report) => {
273
- completed += 1;
274
- ctx.ui.setWidget(PROGRESS_WIDGET_KEY, renderProgressLines("reviewing", completed, TOTAL_STAGES));
275
- return report;
276
- }),
277
- ),
278
- );
279
-
280
- if (reviewAbort.signal.aborted) {
281
- ctx.ui.notify("checks: cancelled.", "warning");
282
- return;
283
- }
284
- }
285
-
286
- // ── Test-gap analysis: a research agent proposes candidate behavioral
287
- // gaps (required-but-untested edge cases that could let an incorrect
288
- // solution slip past test.patch), then a strict, independent filter
289
- // agent verifies each one against the fairness rules before it's kept.
290
- // This never turns a PASS into a FAIL — it only annotates the report.
291
- let gapFinding: GapStageResult<TestGapCandidate> = { status: "ok", gaps: [] };
292
- let gapFiltering: GapStageResult<TestGapFinal> = { status: "ok", gaps: [] };
293
- if (runGapStages) {
294
- ctx.ui.setWidget(PROGRESS_WIDGET_KEY, renderProgressLines("finding test gaps", completed, TOTAL_STAGES));
295
- gapFinding = await runGapFinder({
296
- tempDir: dir,
297
- model,
298
- thinkingLevel: config.thinkingLevel,
299
- testRubric: sections.tests,
300
- fairnessRules,
301
- cancelSignal: reviewAbort.signal,
302
- });
303
- completed += 1;
304
- ctx.ui.setWidget(PROGRESS_WIDGET_KEY, renderProgressLines("finding test gaps", completed, TOTAL_STAGES));
305
-
306
- if (reviewAbort.signal.aborted) {
307
- ctx.ui.notify("checks: cancelled.", "warning");
308
- return;
309
- }
310
-
311
- if (gapFinding.status === "ok" && gapFinding.gaps.length > 0) {
312
- ctx.ui.setWidget(PROGRESS_WIDGET_KEY, renderProgressLines("validating test gaps", completed, TOTAL_STAGES));
313
- gapFiltering = await runGapValidator({
314
- tempDir: dir,
315
- model,
316
- thinkingLevel: config.thinkingLevel,
317
- testRubric: sections.tests,
318
- fairnessRules,
319
- candidates: gapFinding.gaps,
320
- cancelSignal: reviewAbort.signal,
321
- });
322
- }
323
- completed += 1;
324
- ctx.ui.setWidget(PROGRESS_WIDGET_KEY, renderProgressLines("validating test gaps", completed, TOTAL_STAGES));
325
-
326
- if (reviewAbort.signal.aborted) {
327
- ctx.ui.notify("checks: cancelled.", "warning");
328
- return;
329
- }
330
- }
331
-
332
- ctx.ui.setWidget(PROGRESS_WIDGET_KEY, renderProgressLines("finalizing report", TOTAL_STAGES, TOTAL_STAGES));
333
-
334
- // Each reviewer's structured report lives under its own key so per-agent
335
- // detail is always preserved. Test gaps never affect `overall` — they're
336
- // supplementary, filtered notes.
337
- const byRole: Record<string, ReviewReport> = {};
338
- activeRoles.forEach((role, i) => {
339
- if (reports[i]) byRole[role.key] = reports[i];
340
- });
341
- const testGaps = gapFiltering.gaps;
342
- const gapAnalysisIncomplete = runGapStages && (gapFinding.status !== "ok" || gapFiltering.status !== "ok");
343
-
344
- // Merge into any existing shipd_report.json instead of clobbering it —
345
- // running --review/--description/--tests/--solution/--gap-finder
346
- // separately (in any order) should build up one combined report rather
347
- // than each overwriting the others' results.
348
- const reportPath = join(ctx.cwd, "shipd_report.json");
349
- const existingReport = loadExistingReport(reportPath);
350
- const merged = mergeReport({
351
- existingReport,
352
- config,
353
- runReviewers,
354
- byRole,
355
- runGapStages,
356
- testGaps,
357
- gapAnalysisIncomplete,
358
- gapFinderStatus: gapFinding.status,
359
- gapFilterStatus: gapFiltering.status,
360
- });
361
-
362
- const summary = buildRunSummary({ merged, runReviewers, activeRoles, byRole, runGapStages });
363
- pi.sendMessage({
364
- customType: "shipd_checks_report",
365
- content: summary.content,
366
- display: true,
367
- details: summary.details,
368
- });
369
-
370
- writeFileSync(reportPath, JSON.stringify(merged, null, 2), "utf-8");
371
- ctx.ui.notify(`checks: wrote details to ${reportPath}`, "info");
372
- } catch (err) {
373
- ctx.ui.notify(`checks failed: ${err instanceof Error ? err.message : String(err)}`, "error");
374
- } finally {
375
- ctx.ui.setWidget(PROGRESS_WIDGET_KEY, undefined);
376
- if (tempDir) {
377
- try {
378
- rmSync(tempDir, { recursive: true, force: true });
379
- } catch {
380
- // best effort cleanup
381
- }
382
- }
383
- endReview();
384
- }
385
- },
386
- });
387
- }
package/src/config.ts DELETED
@@ -1,84 +0,0 @@
1
- /**
2
- * Global config (~/.pi/agent/checks-config.json) for the reviewer model +
3
- * thinking level, plus helpers for reading pi's own settings.json (enabled
4
- * models, shell path) that the config flow and git snapshot step need.
5
- */
6
-
7
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
- import { dirname, join } from "node:path";
9
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
10
- import type { ChecksConfig, ThinkingLevel } from "./types.js";
11
-
12
- export const CONFIG_PATH = join(getAgentDir(), "checks-config.json");
13
- export const SETTINGS_PATH = join(getAgentDir(), "settings.json");
14
-
15
- /**
16
- * Mirrors `getSupportedThinkingLevels` from `@earendil-works/pi-ai` (not part of
17
- * pi-coding-agent's public export surface, so re-implemented here): a level is
18
- * available if the model supports reasoning at all, isn't explicitly mapped to
19
- * `null` in `thinkingLevelMap`, and — for the opt-in `xhigh`/`max` tiers — is
20
- * explicitly present (non-undefined) in that map.
21
- */
22
- const EXTENDED_THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
23
-
24
- export function getSupportedThinkingLevels(
25
- model: { reasoning?: boolean; thinkingLevelMap?: Record<string, string | null> } | undefined,
26
- ): ThinkingLevel[] {
27
- if (!model?.reasoning) return ["off"];
28
- return EXTENDED_THINKING_LEVELS.filter((level) => {
29
- const mapped = model.thinkingLevelMap?.[level];
30
- if (mapped === null) return false;
31
- if (level === "xhigh" || level === "max") return mapped !== undefined;
32
- return true;
33
- });
34
- }
35
-
36
- export function loadReviewConfig(): ChecksConfig | null {
37
- try {
38
- if (!existsSync(CONFIG_PATH)) return null;
39
- const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf-8")) as ChecksConfig;
40
- if (parsed.provider && parsed.modelId && parsed.thinkingLevel) return parsed;
41
- } catch {
42
- // fall through
43
- }
44
- return null;
45
- }
46
-
47
- export function saveReviewConfig(config: ChecksConfig): void {
48
- const dir = dirname(CONFIG_PATH);
49
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
50
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
51
- }
52
-
53
- export function loadEnabledModelRefs(): string[] {
54
- try {
55
- if (!existsSync(SETTINGS_PATH)) return [];
56
- const settings = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8")) as { enabledModels?: string[] };
57
- return settings.enabledModels ?? [];
58
- } catch {
59
- return [];
60
- }
61
- }
62
-
63
- export function splitProviderModel(ref: string): { provider: string; modelId: string } | null {
64
- const idx = ref.indexOf("/");
65
- if (idx <= 0 || idx === ref.length - 1) return null;
66
- return { provider: ref.slice(0, idx), modelId: ref.slice(idx + 1) };
67
- }
68
-
69
- function getAgentSettings(): { shellPath?: string } {
70
- try {
71
- if (existsSync(SETTINGS_PATH)) {
72
- return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8")) as { shellPath?: string };
73
- }
74
- } catch {
75
- // ignore
76
- }
77
- return {};
78
- }
79
-
80
- export function getShellExecutable(): string {
81
- const fromSettings = getAgentSettings().shellPath;
82
- if (fromSettings) return fromSettings;
83
- return process.platform === "win32" ? "C:\\Program Files\\Git\\bin\\bash.exe" : "bash";
84
- }