pi-jev-guard 0.4.0 → 0.6.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 { VerificationDefaultProfile, VerificationManifestSource, VerificationMode } from "./verification.ts";
22
23
 
23
24
  export interface GateStatusView {
24
25
  active: boolean;
@@ -79,6 +80,31 @@ 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
+ defaultProfile: VerificationDefaultProfile;
88
+ manifestLoaded: boolean;
89
+ manifestSource?: VerificationManifestSource;
90
+ manifestSourceLabel?: string;
91
+ manifestError?: string;
92
+ changedFiles: string[];
93
+ checks: Array<{ id: string; state: "passed" | "failed" | "pending" }>;
94
+ };
95
+ setVerification?: (mode: "off" | VerificationMode) => JevConfig;
96
+ reloadVerificationManifest?: (cwd: string) => {
97
+ ok: boolean;
98
+ error?: string;
99
+ path?: string;
100
+ sourceLabel?: string;
101
+ };
102
+ runVerification?: (args: {
103
+ cwd: string;
104
+ selection: string | undefined;
105
+ signal?: AbortSignal;
106
+ trusted: boolean;
107
+ }) => Promise<{ ok: boolean; message: string; results: unknown[] }>;
82
108
  }
83
109
 
84
110
  export const JEV_SUBCOMMANDS = [
@@ -96,6 +122,8 @@ export const JEV_SUBCOMMANDS = [
96
122
  "output",
97
123
  "last",
98
124
  "grounding",
125
+ "verification",
126
+ "verify",
99
127
  "backend",
100
128
  "save-key",
101
129
  "help",
@@ -121,6 +149,10 @@ export const JEV_HELP_TEXT = [
121
149
  " /jev last Ultimo verdetto del gate (twin): esito e controlli",
122
150
  " /jev grounding off|warn|repair|hold",
123
151
  " Grounding affermazioni sessione (solo automatic, opt-in)",
152
+ " /jev verification off|warn|repair|hold|reload",
153
+ " Footer deterministico; locale > manifest globale > profilo common (opt-in)",
154
+ " /jev verify [recommended|all|id]",
155
+ " Esegue solo check della policy acquisita, mai shell arbitraria",
124
156
  " /jev backend Mostra backend attivo e chiavi rilevate",
125
157
  " /jev backend auto|openrouter|typesafe",
126
158
  " Cambia backend (salvato in jev-config.json)",
@@ -140,7 +172,7 @@ export const JEV_HELP_TEXT = [
140
172
  export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
141
173
  pi.registerCommand("jev", {
142
174
  description:
143
- "Jev guard: verifica con TypeSafe Jev. Sottocomandi: status, mode, models, upstream, off, check, stats, help.",
175
+ "Jev guard: gate, grounding e verifica progetto. Sottocomandi: status, mode, upstream, check, verification, verify, stats, help.",
144
176
  getArgumentCompletions: (argumentPrefix) => {
145
177
  const prefix = argumentPrefix.trim().toLowerCase();
146
178
  return JEV_SUBCOMMANDS.filter((s) => s.startsWith(prefix)).map(
@@ -188,6 +220,7 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
188
220
  const info = deps.reviewerInfo();
189
221
  const gate = deps.gateStatus();
190
222
  const twinView = deps.twinStatus();
223
+ const verification = deps.verificationStatus?.(ctx.cwd);
191
224
  const authProvider = twinView.primary?.provider ?? cfg.automatic.upstreamProvider;
192
225
  let guardedAuth = "—";
193
226
  if (gate.active) {
@@ -250,6 +283,11 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
250
283
  `Exfil check: ${cfg.exfilCheck.enabled ? "attivo in automatic (confirm in TUI, fail-open)" : "disattivo (config)"}`,
251
284
  `Output judge: ${cfg.outputJudge.enabled ? `attivo in automatic (tool: ${cfg.outputJudge.tools.join(",") || "—"}, fail-open)` : "disattivo (config)"}`,
252
285
  `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)"}`,
286
+ verification
287
+ ? `Verifica progetto: ${verification.enabled
288
+ ? `attiva (mode=${verification.mode}, policy=${verification.manifestLoaded ? verification.manifestSourceLabel ?? "OK" : verification.manifestError ?? "assente"}, modifiche=${verification.changedFiles.length}, check=${verification.checks.map((check) => `${check.id}:${check.state}`).join(",") || "—"})`
289
+ : `disattiva (opt-in: /jev verification warn; locale=${verification.manifestPath}, fallback=${verification.defaultProfile})`}`
290
+ : "Verifica progetto: non disponibile in questa estensione",
253
291
  cfg.mode === "automatic"
254
292
  ? "ATTENZIONE: in automatic testo/codice vengono inviati al verificatore. Crea il twin con /jev upstream e seleziona il modello __jev per enforcement."
255
293
  : "on-demand: nessuna verifica automatica.",
@@ -465,6 +503,9 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
465
503
  last.report.grounding
466
504
  ? `grounding: ${last.report.grounding.action} (${last.report.grounding.reasons.join(", ") || "n/a"})`
467
505
  : "grounding: non intervenuto",
506
+ last.report.verification
507
+ ? `verifica progetto: ${last.report.verification.action} (file=${last.report.verification.changedFiles.join(",") || "—"}; check=${last.report.verification.checks.map((check) => `${check.id}:${check.state}`).join(",") || "—"})`
508
+ : "verifica progetto: non intervenuta",
468
509
  v.errorCode ? `errore: ${v.errorCode}` : `backend: ${v.backend ?? "?"} · modello: ${v.model ?? "?"}`,
469
510
  v.truncated ? `troncato: ${JSON.stringify(v.truncated)}` : "copertura intera",
470
511
  `tempo verifica: ${Math.round(v.elapsedMs)}ms`,
@@ -521,6 +562,75 @@ export function registerJevCommand(pi: ExtensionAPI, deps: JevCommandDeps) {
521
562
  return;
522
563
  }
523
564
 
565
+ if (sub === "verification") {
566
+ if (!deps.verificationStatus || !deps.setVerification) {
567
+ ctx.ui.notify("Verifica progetto non disponibile in questa versione dell'estensione.", "warning");
568
+ return;
569
+ }
570
+ const value = parts[1];
571
+ if (value === undefined) {
572
+ const st = deps.verificationStatus(ctx.cwd);
573
+ ctx.ui.notify(
574
+ st.enabled
575
+ ? [
576
+ `Verifica progetto attiva in automatic (mode=${st.mode}).`,
577
+ `Policy: ${st.manifestLoaded ? st.manifestSourceLabel ?? "OK" : st.manifestError ?? "non disponibile"}.`,
578
+ `Override locale: ${st.manifestPath}; fallback integrato: ${st.defaultProfile}.`,
579
+ `Modifiche osservate: ${st.changedFiles.join(", ") || "nessuna"}.`,
580
+ `Check: ${st.checks.map((check) => `${check.id}:${check.state}`).join(", ") || "nessuno pertinente"}.`,
581
+ "Esegui quelli mancanti con /jev verify recommended. Dopo una modifica manuale della policy: /jev verification reload.",
582
+ ].join("\n")
583
+ : `Verifica progetto disattiva (opt-in). Override locale: ${st.manifestPath}; fallback: ${st.defaultProfile}. Attiva con /jev verification warn|repair|hold.`,
584
+ "info",
585
+ );
586
+ return;
587
+ }
588
+ if (value === "reload") {
589
+ if (!deps.reloadVerificationManifest) {
590
+ ctx.ui.notify("Reload manifest non disponibile in questa versione dell'estensione.", "warning");
591
+ return;
592
+ }
593
+ const reloaded = deps.reloadVerificationManifest(ctx.cwd);
594
+ ctx.ui.notify(
595
+ reloaded.ok
596
+ ? `Policy verifica acquisita: ${reloaded.sourceLabel ?? reloaded.path}. I comandi verranno eseguiti solo da questa snapshot.`
597
+ : `Policy verifica non acquisita: ${reloaded.error}`,
598
+ reloaded.ok ? "info" : "warning",
599
+ );
600
+ return;
601
+ }
602
+ if (value !== "off" && value !== "warn" && value !== "repair" && value !== "hold") {
603
+ ctx.ui.notify("Uso: /jev verification off|warn|repair|hold|reload", "warning");
604
+ return;
605
+ }
606
+ deps.setVerification(value);
607
+ ctx.ui.notify(
608
+ value === "off"
609
+ ? "Verifica progetto disattivata."
610
+ : `Verifica progetto attiva (mode=${value}). Usa il profilo globale common automaticamente; un manifest locale in ${deps.verificationStatus(ctx.cwd).manifestPath} lo sovrascrive. Poi usa /jev verify recommended.`,
611
+ "info",
612
+ );
613
+ return;
614
+ }
615
+
616
+ if (sub === "verify") {
617
+ if (!deps.runVerification) {
618
+ ctx.ui.notify("Verifica progetto non disponibile in questa versione dell'estensione.", "warning");
619
+ return;
620
+ }
621
+ const trusted = typeof (ctx as { isProjectTrusted?: unknown }).isProjectTrusted === "function"
622
+ ? ctx.isProjectTrusted()
623
+ : false;
624
+ const result = await deps.runVerification({
625
+ cwd: ctx.cwd,
626
+ selection: parts[1],
627
+ signal: ctx.signal ?? undefined,
628
+ trusted,
629
+ });
630
+ ctx.ui.notify(result.message, result.ok ? "info" : "warning");
631
+ return;
632
+ }
633
+
524
634
  if (sub === "reload") {
525
635
  const cfg = deps.reload();
526
636
  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,20 @@ export const DEFAULT_CONFIG: JevConfig = {
194
196
  cacheSeconds: 120,
195
197
  invalidateTestsOnEdit: true,
196
198
  },
199
+ verification: {
200
+ // Opt-in: solo una policy acquisita può autorizzare check; non abilitarla
201
+ // silenziosamente in installazioni esistenti.
202
+ enabled: false,
203
+ mode: "warn",
204
+ manifestPath: ".pi/jev-verification.json",
205
+ // Profilo built-in per nuovi progetti: si attiva solo dopo il consenso
206
+ // esplicito /jev verification warn|repair|hold (enabled resta false).
207
+ defaultProfile: "common",
208
+ maxRepairs: 1,
209
+ commandTimeoutMs: 120000,
210
+ outputChars: 6000,
211
+ maxCommands: 4,
212
+ },
197
213
  exfilCheck: {
198
214
  enabled: true,
199
215
  tools: ["bash", "powershell"],
@@ -241,6 +257,21 @@ export function loadConfig(configPath?: string): JevConfig {
241
257
  fileConfig,
242
258
  ) as JevConfig;
243
259
 
260
+ // Normalizza il fallback verifica: config precedenti non lo contengono;
261
+ // valori manuali ignoti non devono disattivare/alterare la policy a caso.
262
+ if (!isRecord(loaded.verification)) {
263
+ loaded.verification = { ...DEFAULT_CONFIG.verification };
264
+ }
265
+ if (typeof loaded.verification.manifestPath !== "string" || !loaded.verification.manifestPath.trim()) {
266
+ loaded.verification.manifestPath = DEFAULT_CONFIG.verification.manifestPath;
267
+ }
268
+ if (loaded.verification.defaultProfile !== "common" && loaded.verification.defaultProfile !== "off") {
269
+ loaded.verification.defaultProfile = DEFAULT_CONFIG.verification.defaultProfile;
270
+ }
271
+ if (typeof loaded.verification.globalManifestPath !== "string") {
272
+ delete loaded.verification.globalManifestPath;
273
+ }
274
+
244
275
  // Normalizza la lista twin (config scritta a mano o da versioni vecchie):
245
276
  // dedup, scarta voci malformate, garantisce che il primario sia incluso.
246
277
  let merged = withTwinRefs(loaded, twinRefs(loaded));
@@ -0,0 +1,177 @@
1
+ /** Esecuzione bounded dei soli comandi dichiarati dalla policy di verifica acquisita. */
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 dalla policy. `command` non arriva mai dal modello;
38
+ * il chiamante deve aver già selezionato un VerificationCheck acquisito.
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 per comandi normali come `npm run typecheck`; la
102
+ // fiducia arriva dalla policy acquisita + 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 typecheck, lint, or test authorized by the acquired local/global verification policy. " +
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
+ }