pi-jev-guard 0.4.0 → 0.5.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.
@@ -62,6 +62,8 @@ export interface PostPublishCheckResult {
62
62
  holdText?: string;
63
63
  /** Dettaglio per il report (publish con nota, hold). */
64
64
  grounding?: GateVerdictReport["grounding"];
65
+ /** Stato deterministico delle verifiche progetto osservate. */
66
+ verification?: GateVerdictReport["verification"];
65
67
  }
66
68
 
67
69
  /** Esito finale del gate con verdetto, per `/jev last` e diagnostica. */
@@ -76,6 +78,12 @@ export interface GateVerdictReport {
76
78
  action: "warned" | "repaired" | "held";
77
79
  reasons: string[];
78
80
  };
81
+ /** Mai generato dal modello: file edit/write + check configurati osservati. */
82
+ verification?: {
83
+ action: "passed" | "warned" | "repaired" | "held";
84
+ changedFiles: string[];
85
+ checks: Array<{ id: string; state: "passed" | "failed" | "pending" }>;
86
+ };
79
87
  }
80
88
 
81
89
  export class GuardianLimitError extends Error {
@@ -183,6 +191,10 @@ function withNotice(message: AssistantMessage, notice: string): AssistantMessage
183
191
  return { ...message, content };
184
192
  }
185
193
 
194
+ function hasToolCalls(message: AssistantMessage): boolean {
195
+ return message.content.some((block) => block.type === "toolCall");
196
+ }
197
+
186
198
  function assertCompleteCandidate(message: AssistantMessage): void {
187
199
  // Rifiuta terminali non supportati per pubblicazione.
188
200
  if (
@@ -284,12 +296,14 @@ export function runGuardedStream(
284
296
  const publishCandidate = async (
285
297
  notice?: string,
286
298
  grounding?: GateVerdictReport["grounding"],
299
+ verification?: GateVerdictReport["verification"],
287
300
  ): Promise<void> => {
288
301
  deps.onVerdict?.({
289
302
  verdict,
290
303
  attempts: attempt + 1,
291
304
  outcome: "published",
292
305
  ...(grounding ? { grounding } : {}),
306
+ ...(verification ? { verification } : {}),
293
307
  });
294
308
  const approved = notice ? withNotice(candidate, notice) : candidate;
295
309
  const aggregated = aggregateUsage(usages);
@@ -339,6 +353,15 @@ export function runGuardedStream(
339
353
  const runPostCheck = async (
340
354
  baseNotice?: string,
341
355
  ): Promise<"published" | "held" | undefined> => {
356
+ // I toolcall devono raggiungere il runtime: un post-check su una
357
+ // risposta intermedia potrebbe rigenerarla/holdarla e impedire il
358
+ // check stesso (es. `jev_verify`) prima che venga eseguito. Safety
359
+ // e tool policy restano già applicate; il post-check corre sul
360
+ // successivo messaggio finale dopo i tool result.
361
+ if (hasToolCalls(candidate)) {
362
+ await publishCandidate(baseNotice);
363
+ return "published";
364
+ }
342
365
  if (!deps.postPublishCheck) {
343
366
  await publishCandidate(baseNotice);
344
367
  return "published";
@@ -357,7 +380,7 @@ export function runGuardedStream(
357
380
  signal?.throwIfAborted();
358
381
  if (check.action === "publish") {
359
382
  const notice = [baseNotice, check.notice].filter(Boolean).join("") || undefined;
360
- await publishCandidate(notice, check.grounding);
383
+ await publishCandidate(notice, check.grounding, check.verification);
361
384
  return "published";
362
385
  }
363
386
  if (check.action === "hold") {
@@ -367,6 +390,7 @@ export function runGuardedStream(
367
390
  attempts,
368
391
  outcome: "held",
369
392
  ...(check.grounding ? { grounding: check.grounding } : {}),
393
+ ...(check.verification ? { verification: check.verification } : {}),
370
394
  });
371
395
  await publishHoldText(check.holdText ?? "[JEV — OUTPUT NON PUBBLICATO]");
372
396
  return "held";
@@ -376,7 +400,7 @@ export function runGuardedStream(
376
400
  rejectedText: rawCandidate,
377
401
  feedback:
378
402
  check.feedback ??
379
- "Grounding check failed; only assert session activities supported by evidence.",
403
+ "Post-publication check failed; correct the draft using the supplied evidence.",
380
404
  });
381
405
  return undefined;
382
406
  };
package/src/commands.ts CHANGED
@@ -19,6 +19,7 @@ import { reviewWithCoverage } from "./reviewer.ts";
19
19
  import type { Metrics } from "./metrics.ts";
20
20
  import type { OutputVerdict } from "./output-judge.ts";
21
21
  import type { GateVerdictReport } from "./automatic/guardian.ts";
22
+ import type { VerificationMode } from "./verification.ts";
22
23
 
23
24
  export interface GateStatusView {
24
25
  active: boolean;
@@ -79,6 +80,27 @@ export interface JevCommandDeps {
79
80
  hasStaleTests: boolean;
80
81
  };
81
82
  setGrounding: (mode: "off" | "warn" | "repair" | "hold") => JevConfig;
83
+ verificationStatus?: (cwd: string) => {
84
+ enabled: boolean;
85
+ mode: VerificationMode;
86
+ manifestPath: string;
87
+ manifestLoaded: boolean;
88
+ manifestError?: string;
89
+ changedFiles: string[];
90
+ checks: Array<{ id: string; state: "passed" | "failed" | "pending" }>;
91
+ };
92
+ setVerification?: (mode: "off" | VerificationMode) => JevConfig;
93
+ reloadVerificationManifest?: (cwd: string) => {
94
+ ok: boolean;
95
+ error?: string;
96
+ path?: string;
97
+ };
98
+ runVerification?: (args: {
99
+ cwd: string;
100
+ selection: string | undefined;
101
+ signal?: AbortSignal;
102
+ trusted: boolean;
103
+ }) => Promise<{ ok: boolean; message: string; results: unknown[] }>;
82
104
  }
83
105
 
84
106
  export const JEV_SUBCOMMANDS = [
@@ -96,6 +118,8 @@ export const JEV_SUBCOMMANDS = [
96
118
  "output",
97
119
  "last",
98
120
  "grounding",
121
+ "verification",
122
+ "verify",
99
123
  "backend",
100
124
  "save-key",
101
125
  "help",
@@ -121,6 +145,10 @@ export const JEV_HELP_TEXT = [
121
145
  " /jev last Ultimo verdetto del gate (twin): esito e controlli",
122
146
  " /jev grounding off|warn|repair|hold",
123
147
  " Grounding affermazioni sessione (solo automatic, opt-in)",
148
+ " /jev verification off|warn|repair|hold|reload",
149
+ " Footer deterministico + repair dai check progetto (opt-in; reload acquisisce manifest)",
150
+ " /jev verify [recommended|all|id]",
151
+ " Esegue solo check dichiarati in .pi/jev-verification.json",
124
152
  " /jev backend Mostra backend attivo e chiavi rilevate",
125
153
  " /jev backend auto|openrouter|typesafe",
126
154
  " Cambia backend (salvato in jev-config.json)",
@@ -140,7 +168,7 @@ export const JEV_HELP_TEXT = [
140
168
  export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
141
169
  pi.registerCommand("jev", {
142
170
  description:
143
- "Jev guard: verifica con TypeSafe Jev. Sottocomandi: status, mode, models, upstream, off, check, stats, help.",
171
+ "Jev guard: gate, grounding e verifica progetto. Sottocomandi: status, mode, upstream, check, verification, verify, stats, help.",
144
172
  getArgumentCompletions: (argumentPrefix) => {
145
173
  const prefix = argumentPrefix.trim().toLowerCase();
146
174
  return JEV_SUBCOMMANDS.filter((s) => s.startsWith(prefix)).map(
@@ -188,6 +216,7 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
188
216
  const info = deps.reviewerInfo();
189
217
  const gate = deps.gateStatus();
190
218
  const twinView = deps.twinStatus();
219
+ const verification = deps.verificationStatus?.(ctx.cwd);
191
220
  const authProvider = twinView.primary?.provider ?? cfg.automatic.upstreamProvider;
192
221
  let guardedAuth = "—";
193
222
  if (gate.active) {
@@ -250,6 +279,11 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
250
279
  `Exfil check: ${cfg.exfilCheck.enabled ? "attivo in automatic (confirm in TUI, fail-open)" : "disattivo (config)"}`,
251
280
  `Output judge: ${cfg.outputJudge.enabled ? `attivo in automatic (tool: ${cfg.outputJudge.tools.join(",") || "—"}, fail-open)` : "disattivo (config)"}`,
252
281
  `Grounding: ${cfg.grounding.enabled ? `attivo in automatic (mode=${cfg.grounding.mode}, evidenza=${deps.groundingStatus().entries}${deps.groundingStatus().hasStaleTests ? ", test stale" : ""})` : "disattivo (opt-in: /jev grounding warn)"}`,
282
+ verification
283
+ ? `Verifica progetto: ${verification.enabled
284
+ ? `attiva (mode=${verification.mode}, manifest=${verification.manifestLoaded ? "OK" : verification.manifestError ?? "assente"}, modifiche=${verification.changedFiles.length}, check=${verification.checks.map((check) => `${check.id}:${check.state}`).join(",") || "—"})`
285
+ : `disattiva (opt-in: /jev verification warn; manifest=${verification.manifestPath})`}`
286
+ : "Verifica progetto: non disponibile in questa estensione",
253
287
  cfg.mode === "automatic"
254
288
  ? "ATTENZIONE: in automatic testo/codice vengono inviati al verificatore. Crea il twin con /jev upstream e seleziona il modello __jev per enforcement."
255
289
  : "on-demand: nessuna verifica automatica.",
@@ -465,6 +499,9 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
465
499
  last.report.grounding
466
500
  ? `grounding: ${last.report.grounding.action} (${last.report.grounding.reasons.join(", ") || "n/a"})`
467
501
  : "grounding: non intervenuto",
502
+ last.report.verification
503
+ ? `verifica progetto: ${last.report.verification.action} (file=${last.report.verification.changedFiles.join(",") || "—"}; check=${last.report.verification.checks.map((check) => `${check.id}:${check.state}`).join(",") || "—"})`
504
+ : "verifica progetto: non intervenuta",
468
505
  v.errorCode ? `errore: ${v.errorCode}` : `backend: ${v.backend ?? "?"} · modello: ${v.model ?? "?"}`,
469
506
  v.truncated ? `troncato: ${JSON.stringify(v.truncated)}` : "copertura intera",
470
507
  `tempo verifica: ${Math.round(v.elapsedMs)}ms`,
@@ -521,6 +558,74 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
521
558
  return;
522
559
  }
523
560
 
561
+ if (sub === "verification") {
562
+ if (!deps.verificationStatus || !deps.setVerification) {
563
+ ctx.ui.notify("Verifica progetto non disponibile in questa versione dell'estensione.", "warning");
564
+ return;
565
+ }
566
+ const value = parts[1];
567
+ if (value === undefined) {
568
+ const st = deps.verificationStatus(ctx.cwd);
569
+ ctx.ui.notify(
570
+ st.enabled
571
+ ? [
572
+ `Verifica progetto attiva in automatic (mode=${st.mode}).`,
573
+ `Manifest: ${st.manifestPath} — ${st.manifestLoaded ? "OK" : st.manifestError ?? "non disponibile"}.`,
574
+ `Modifiche osservate: ${st.changedFiles.join(", ") || "nessuna"}.`,
575
+ `Check: ${st.checks.map((check) => `${check.id}:${check.state}`).join(", ") || "nessuno pertinente"}.`,
576
+ "Esegui quelli mancanti con /jev verify recommended. Dopo una modifica manuale del manifest: /jev verification reload.",
577
+ ].join("\n")
578
+ : `Verifica progetto disattiva (opt-in). Manifest previsto: ${st.manifestPath}. Attiva con /jev verification warn|repair|hold.`,
579
+ "info",
580
+ );
581
+ return;
582
+ }
583
+ if (value === "reload") {
584
+ if (!deps.reloadVerificationManifest) {
585
+ ctx.ui.notify("Reload manifest non disponibile in questa versione dell'estensione.", "warning");
586
+ return;
587
+ }
588
+ const reloaded = deps.reloadVerificationManifest(ctx.cwd);
589
+ ctx.ui.notify(
590
+ reloaded.ok
591
+ ? `Manifest verifica acquisito: ${reloaded.path}. I comandi verranno eseguiti solo da questa snapshot.`
592
+ : `Manifest verifica non acquisito: ${reloaded.error}`,
593
+ reloaded.ok ? "info" : "warning",
594
+ );
595
+ return;
596
+ }
597
+ if (value !== "off" && value !== "warn" && value !== "repair" && value !== "hold") {
598
+ ctx.ui.notify("Uso: /jev verification off|warn|repair|hold|reload", "warning");
599
+ return;
600
+ }
601
+ deps.setVerification(value);
602
+ ctx.ui.notify(
603
+ value === "off"
604
+ ? "Verifica progetto disattivata."
605
+ : `Verifica progetto attiva (mode=${value}). Crea ${deps.verificationStatus(ctx.cwd).manifestPath} con i soli comandi approvati, poi usa /jev verify recommended.`,
606
+ "info",
607
+ );
608
+ return;
609
+ }
610
+
611
+ if (sub === "verify") {
612
+ if (!deps.runVerification) {
613
+ ctx.ui.notify("Verifica progetto non disponibile in questa versione dell'estensione.", "warning");
614
+ return;
615
+ }
616
+ const trusted = typeof (ctx as { isProjectTrusted?: unknown }).isProjectTrusted === "function"
617
+ ? ctx.isProjectTrusted()
618
+ : false;
619
+ const result = await deps.runVerification({
620
+ cwd: ctx.cwd,
621
+ selection: parts[1],
622
+ signal: ctx.signal ?? undefined,
623
+ trusted,
624
+ });
625
+ ctx.ui.notify(result.message, result.ok ? "info" : "warning");
626
+ return;
627
+ }
628
+
524
629
  if (sub === "reload") {
525
630
  const cfg = deps.reload();
526
631
  ctx.ui.notify(`Config ricaricata. mode=${cfg.mode} backend=${cfg.jev.backend}`, "info");
package/src/config.ts CHANGED
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import type { JevBackend } from "./reviewer.ts";
5
+ import type { VerificationSettings } from "./verification.ts";
5
6
 
6
7
  export type JevMode = "on-demand" | "automatic";
7
8
  export type JevBackendSetting = JevBackend | "auto";
@@ -97,6 +98,7 @@ export interface JevConfig {
97
98
  /** Una modifica riuscita invalida i test precedenti (stale). */
98
99
  invalidateTestsOnEdit: boolean;
99
100
  };
101
+ verification: VerificationSettings;
100
102
  exfilCheck: {
101
103
  /** Check semantico sui comandi con tool di rete (solo automatic). */
102
104
  enabled: boolean;
@@ -194,6 +196,17 @@ export const DEFAULT_CONFIG: JevConfig = {
194
196
  cacheSeconds: 120,
195
197
  invalidateTestsOnEdit: true,
196
198
  },
199
+ verification: {
200
+ // Opt-in: il manifest può eseguire solo comandi esplicitamente approvati
201
+ // dal progetto; non abilitarlo silenziosamente in installazioni esistenti.
202
+ enabled: false,
203
+ mode: "warn",
204
+ manifestPath: ".pi/jev-verification.json",
205
+ maxRepairs: 1,
206
+ commandTimeoutMs: 120000,
207
+ outputChars: 6000,
208
+ maxCommands: 3,
209
+ },
197
210
  exfilCheck: {
198
211
  enabled: true,
199
212
  tools: ["bash", "powershell"],
@@ -0,0 +1,177 @@
1
+ /** Esecuzione bounded dei soli comandi dichiarati nel manifest di verifica. */
2
+ import { spawn } from "node:child_process";
3
+ import type { VerificationCheck, VerificationSettings } from "./verification.ts";
4
+
5
+ export interface VerificationExecution {
6
+ id: string;
7
+ command: string;
8
+ ok: boolean;
9
+ exitCode?: number;
10
+ timedOut?: boolean;
11
+ aborted?: boolean;
12
+ /** Solo output fallito, bounded; il successo non riversa log inutili nel contesto. */
13
+ output?: string;
14
+ }
15
+
16
+ function boundedTimeout(value: unknown): number {
17
+ // Config manuale può essere malformata: mai trasformare NaN in timeout 0.
18
+ const numeric = typeof value === "number" && Number.isFinite(value) ? value : 120_000;
19
+ // Non permettere manifest accidentali da ore: per check oltre 10 min l'utente
20
+ // può aumentare il setting ma resta una scelta esplicita e limitata.
21
+ return Math.max(1000, Math.min(600_000, Math.floor(numeric)));
22
+ }
23
+
24
+ function boundedOutputChars(value: unknown): number {
25
+ const numeric = typeof value === "number" && Number.isFinite(value) ? value : 6_000;
26
+ return Math.max(0, Math.min(100_000, Math.floor(numeric)));
27
+ }
28
+
29
+ function appendBounded(state: { text: string; elided: number }, chunk: Buffer, maxChars: number): void {
30
+ const value = chunk.toString("utf8");
31
+ const remaining = Math.max(0, maxChars - state.text.length);
32
+ state.text += value.slice(0, remaining);
33
+ state.elided += Math.max(0, value.length - remaining);
34
+ }
35
+
36
+ /**
37
+ * Esegue un comando scelto dal manifest. `command` non arriva mai dal modello;
38
+ * il chiamante deve aver già selezionato un VerificationCheck validato.
39
+ */
40
+ export async function runVerificationCheck(args: {
41
+ check: VerificationCheck;
42
+ cwd: string;
43
+ settings: VerificationSettings;
44
+ signal?: AbortSignal;
45
+ }): Promise<VerificationExecution> {
46
+ const { check, cwd, settings, signal } = args;
47
+ const configuredTimeout = boundedTimeout(settings.commandTimeoutMs);
48
+ const timeoutMs = Math.min(
49
+ check.timeoutMs === undefined ? configuredTimeout : boundedTimeout(check.timeoutMs),
50
+ configuredTimeout,
51
+ );
52
+ const outputChars = boundedOutputChars(settings.outputChars);
53
+ const captured = { text: "", elided: 0 };
54
+ let timedOut = false;
55
+ let aborted = false;
56
+
57
+ return new Promise<VerificationExecution>((resolveResult) => {
58
+ let settled = false;
59
+ let timeout: ReturnType<typeof setTimeout> | undefined;
60
+ let forceKill: ReturnType<typeof setTimeout> | undefined;
61
+ let child: ReturnType<typeof spawn> | undefined;
62
+ const killTree = (signalName: NodeJS.Signals) => {
63
+ if (!child?.pid) return;
64
+ try {
65
+ // Su POSIX il comando shell e i suoi figli sono un process group
66
+ // dedicato (detached:true): evitare che un npm/test appeso sopravviva
67
+ // al timeout del guard. Su Windows il fallback uccide almeno shell.
68
+ if (process.platform !== "win32") process.kill(-child.pid, signalName);
69
+ else child.kill(signalName);
70
+ } catch {
71
+ try {
72
+ child.kill(signalName);
73
+ } catch {
74
+ // Il processo può essere già terminato.
75
+ }
76
+ }
77
+ };
78
+ const settle = (result: VerificationExecution) => {
79
+ if (settled) return;
80
+ settled = true;
81
+ if (timeout) clearTimeout(timeout);
82
+ if (forceKill) clearTimeout(forceKill);
83
+ signal?.removeEventListener("abort", onAbort);
84
+ resolveResult(result);
85
+ };
86
+ const terminate = () => {
87
+ killTree("SIGTERM");
88
+ forceKill ??= setTimeout(() => killTree("SIGKILL"), 1_000);
89
+ };
90
+ const output = () => {
91
+ const base = captured.text.trim();
92
+ const suffix = captured.elided > 0 ? `\n…[${captured.elided} chars elided]` : "";
93
+ return `${base}${suffix}`.trim();
94
+ };
95
+ const onAbort = () => {
96
+ aborted = true;
97
+ terminate();
98
+ };
99
+
100
+ try {
101
+ // shell è necessario perché il manifest contiene comandi normali come
102
+ // `npm run typecheck`; la fiducia arriva dal file locale + project trust.
103
+ child = spawn(check.command, {
104
+ cwd,
105
+ shell: true,
106
+ windowsHide: true,
107
+ detached: process.platform !== "win32",
108
+ stdio: ["ignore", "pipe", "pipe"],
109
+ });
110
+ } catch (error) {
111
+ settle({
112
+ id: check.id,
113
+ command: check.command,
114
+ ok: false,
115
+ output: error instanceof Error ? error.message : String(error),
116
+ });
117
+ return;
118
+ }
119
+
120
+ child.stdout?.on("data", (chunk: Buffer) => appendBounded(captured, chunk, outputChars));
121
+ child.stderr?.on("data", (chunk: Buffer) => appendBounded(captured, chunk, outputChars));
122
+ child.on("error", (error) => {
123
+ settle({
124
+ id: check.id,
125
+ command: check.command,
126
+ ok: false,
127
+ output: error.message,
128
+ });
129
+ });
130
+ child.on("close", (exitCode) => {
131
+ const ok = exitCode === 0 && !timedOut && !aborted;
132
+ settle({
133
+ id: check.id,
134
+ command: check.command,
135
+ ok,
136
+ ...(typeof exitCode === "number" ? { exitCode } : {}),
137
+ ...(timedOut ? { timedOut: true } : {}),
138
+ ...(aborted ? { aborted: true } : {}),
139
+ ...(!ok ? { output: output() || (timedOut ? `Timeout after ${timeoutMs}ms` : aborted ? "Aborted" : "Command failed without output") } : {}),
140
+ });
141
+ });
142
+
143
+ timeout = setTimeout(() => {
144
+ timedOut = true;
145
+ terminate();
146
+ }, timeoutMs);
147
+ // Child può chiudersi molto rapidamente prima dell'assegnazione del timer.
148
+ if (settled && timeout) clearTimeout(timeout);
149
+ if (signal?.aborted) onAbort();
150
+ else signal?.addEventListener("abort", onAbort, { once: true });
151
+ });
152
+ }
153
+
154
+ export async function runVerificationChecks(args: {
155
+ checks: VerificationCheck[];
156
+ cwd: string;
157
+ settings: VerificationSettings;
158
+ signal?: AbortSignal;
159
+ }): Promise<VerificationExecution[]> {
160
+ const results: VerificationExecution[] = [];
161
+ for (const check of args.checks) {
162
+ if (args.signal?.aborted) break;
163
+ results.push(await runVerificationCheck({ ...args, check }));
164
+ }
165
+ return results;
166
+ }
167
+
168
+ /** Testo destinato al modello: esiti concreti, output diagnostico solo su errore. */
169
+ export function formatVerificationExecutions(results: readonly VerificationExecution[]): string {
170
+ if (results.length === 0) return "[jev_verify] Nessun check eseguito.";
171
+ return results.map((result) => {
172
+ const header = result.ok
173
+ ? `[jev_verify] ${result.id}: superato${result.exitCode !== undefined ? ` (exit ${result.exitCode})` : ""}.`
174
+ : `[jev_verify] ${result.id}: fallito${result.exitCode !== undefined ? ` (exit ${result.exitCode})` : ""}${result.timedOut ? " (timeout)" : ""}${result.aborted ? " (annullato)" : ""}.`;
175
+ return result.output ? `${header}\n${result.output}` : header;
176
+ }).join("\n\n");
177
+ }
@@ -0,0 +1,65 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import type { VerificationExecution } from "./verification-runner.ts";
4
+
5
+ export interface VerificationToolRunResult {
6
+ ok: boolean;
7
+ message: string;
8
+ results: VerificationExecution[];
9
+ }
10
+
11
+ /**
12
+ * Tool ristretto: il modello seleziona solo un id del manifest (o
13
+ * recommended/all), mai una stringa shell arbitraria.
14
+ */
15
+ export function registerVerificationTool(
16
+ pi: ExtensionAPI,
17
+ deps: {
18
+ run: (args: {
19
+ cwd: string;
20
+ selection: string | undefined;
21
+ signal?: AbortSignal;
22
+ trusted: boolean;
23
+ }) => Promise<VerificationToolRunResult>;
24
+ },
25
+ ): void {
26
+ pi.registerTool({
27
+ name: "jev_verify",
28
+ label: "Jev project verification",
29
+ description:
30
+ "Run a project-approved typecheck, lint, or test declared in .pi/jev-verification.json. " +
31
+ "It never accepts arbitrary shell commands.",
32
+ promptSnippet: "Run approved project verification checks after code edits.",
33
+ promptGuidelines: [
34
+ "After a successful edit/write, call jev_verify with check='recommended' when verification is enabled.",
35
+ "Use only this tool's declared check ids; never claim code compiles or tests pass without its observed result.",
36
+ "If a check fails, inspect its actual output, repair the code, then run the check again.",
37
+ ],
38
+ parameters: Type.Object({
39
+ check: Type.Optional(
40
+ Type.String({
41
+ minLength: 1,
42
+ maxLength: 64,
43
+ description: "Check id, or 'recommended' (default) / 'all'",
44
+ }),
45
+ ),
46
+ }),
47
+ executionMode: "sequential",
48
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
49
+ const result = await deps.run({
50
+ cwd: ctx.cwd,
51
+ selection: params.check,
52
+ signal: signal ?? undefined,
53
+ trusted: ctx.isProjectTrusted(),
54
+ });
55
+ return {
56
+ content: [{ type: "text", text: result.message }],
57
+ details: {
58
+ kind: "jev-verification",
59
+ ok: result.ok,
60
+ results: result.results,
61
+ },
62
+ };
63
+ },
64
+ });
65
+ }