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.
package/README.md CHANGED
@@ -6,6 +6,7 @@ Verifica le risposte con [TypeSafe Jev](https://docs.typesafe.ai) dentro [pi](ht
6
6
  - **Domande tipate**: tool `jev_ask` per giudizi calibrati definiti dal modello (noul/choice/score) invece di prosa.
7
7
  - **Automatica**: gemello guarded `<modello>__jev` dentro lo stesso provider, che trattiene la risposta, la verifica con Jev e pubblica solo l'output approvato (con rigenerazione privata su `block`). Stessa auth del provider: nessun login extra, funziona anche con Codex/OAuth.
8
8
  - **Giudice output** (solo `automatic`, fail-open): dopo ogni `bash` cerca secret nel testo e classifica i fallimenti, allegando un consiglio al risultato.
9
+ - **Verifica progetto** (opt-in): footer deterministico da `edit`/`write` osservati e check autorizzati da override locale, manifest globale utente o profilo comune integrato; il modello può eseguire soltanto quegli id, mai shell arbitraria.
9
10
 
10
11
  ## Installazione
11
12
 
@@ -180,6 +181,104 @@ le contraddizioni ad alta confidenza. Fail-open: se il verificatore non
180
181
  risponde, il testo passa senza nota. `/jev last` mostra anche l'esito
181
182
  grounding (`warned`/`repaired`/`held`).
182
183
 
184
+ ### Verifica progetto deterministica (opt-in)
185
+
186
+ Il grounding è probabilistico; per **compilazione, lint e test** serve una
187
+ prova deterministica. La verifica progetto conserva, solo nella sessione,
188
+ `edit`/`write` riusciti e gli esiti osservati dei check. Il footer viene
189
+ generato dall'estensione, non dal modello: un test non eseguito non può
190
+ diventare “test passati”.
191
+
192
+ #### Nuovi progetti: zero manifest locale
193
+
194
+ Una volta, in una sessione col twin `automatic`, abilita la modalità scelta:
195
+
196
+ ```text
197
+ /jev verification repair
198
+ ```
199
+
200
+ Da quel momento ogni progetto usa questa precedenza:
201
+
202
+ 1. `.pi/jev-verification.json` locale, se presente;
203
+ 2. `verification.globalManifestPath`, se l'hai configurato nella config
204
+ utente;
205
+ 3. profilo integrato `common` (default).
206
+
207
+ Il profilo `common` non legge comandi dal repository: abilita esclusivamente
208
+ comandi fissi dell'estensione quando trova segnali convenzionali:
209
+
210
+ | Rilevamento | Check disponibili |
211
+ |---|---|
212
+ | `package.json` con script | `npm run typecheck`, `npm run lint`, uno tra `npm run test:unit`, `npm run test:ci`, `npm test` |
213
+ | `Cargo.toml` | `cargo check`, `cargo test` |
214
+ | `go.mod` | `go test ./...` |
215
+ | configurazione pytest | `python -m pytest` |
216
+ | repository Git | `git diff --check` |
217
+
218
+ Il profilo sceglie un solo ecosistema (Node → Rust → Go → Python) e aggiunge
219
+ il check Git quando applicabile. I comandi sono bounded da
220
+ `verification.maxCommands` (default `4`) e timeout. Se nessun segnale è
221
+ rilevato, non esegue nulla e il footer dichiara la limitazione.
222
+
223
+ Dopo un edit il modello può scegliere solo id già acquisiti:
224
+
225
+ ```text
226
+ /jev verify recommended # check pertinenti non ancora freschi
227
+ /jev verify all # tutti, fino a verification.maxCommands
228
+ /jev verify node-typecheck # un id esplicito della policy corrente
229
+ ```
230
+
231
+ Il tool LLM `jev_verify` ha lo stesso vincolo, richiede un progetto trusted e
232
+ non accetta shell arbitraria. Footer/repair/hold operano nel twin
233
+ `automatic`; `/jev verify` è utilizzabile manualmente anche senza footer.
234
+
235
+ #### Override per progetti speciali
236
+
237
+ Per una pipeline specifica, aggiungi un manifest al progetto: prevale sempre
238
+ sul profilo globale.
239
+
240
+ ```bash
241
+ mkdir -p .pi
242
+ cp config/jev-verification.example.json .pi/jev-verification.json
243
+ ```
244
+
245
+ Controllalo tu, poi acquisiscilo (e ripeti dopo ogni modifica manuale):
246
+
247
+ ```text
248
+ /jev verification reload
249
+ ```
250
+
251
+ In alternativa puoi definire **una** policy comune per tutti i tuoi progetti
252
+ nella config utente, purché esista davvero e sia sotto il tuo controllo:
253
+
254
+ ```json
255
+ {
256
+ "verification": {
257
+ "globalManifestPath": "~/.pi/agent/jev-verification.global.json",
258
+ "defaultProfile": "common"
259
+ }
260
+ }
261
+ ```
262
+
263
+ Un `globalManifestPath` configurato ma assente o malformato non degrada
264
+ silenziosamente al profilo integrato: la verifica segnala l'errore. Per
265
+ disattivare il fallback integrato imposta `"defaultProfile": "off"`.
266
+ `/jev verification` e `/jev status` mostrano l'origine effettiva della policy.
267
+
268
+ Manifest e profilo sono acquisiti a inizio sessione, all'attivazione esplicita
269
+ o con `/jev verification reload`. Se un manifest, un `package.json` o un file
270
+ di configurazione che abilita il profilo cambia dopo l'acquisizione, nessun
271
+ check parte fino al reload utente. In automatic, con tool policy e verifica
272
+ attive, `write`/`edit` del modello su tali file sono bloccati; una modifica via shell
273
+ viene comunque rilevata dal fingerprint. Questo evita che il modello trasformi
274
+ uno script o un manifest in un nuovo comando autorizzato.
275
+
276
+ Modalità: `warn` aggiunge il footer; `repair` rigenera una volta con feedback
277
+ che chiede `jev_verify`; `hold` trattiene dopo una failure concreta, o dopo il
278
+ tentativo bounded se restano check mancanti. Nessun check pertinente non è un
279
+ fallimento del codice e non produce hold. Le modifiche rilevate restano solo
280
+ quelle di `edit`/`write`: una modifica shell non viene attribuita al modello.
281
+
183
282
  ## Configurazione
184
283
 
185
284
  `~/.pi/agent/jev-config.json` (o `$JEV_CONFIG`). Env: `JEV_MODE`, `JEV_BACKEND`
@@ -234,3 +333,6 @@ echo '{"requirements":"...","candidate":"..."}' | npm run jev-check
234
333
  - Il grounding controlla solo affermazioni verificabili sulla sessione
235
334
  (comandi, file, test, modifiche): niente fact-check di fatti esterni,
236
335
  e con contesti lunghi l'evidenza oltre budget viene elisa.
336
+ - La verifica progetto dimostra solo i check dichiarati ed eseguiti dopo gli
337
+ `edit`/`write` osservati; non è una prova formale di correttezza, non vede
338
+ modifiche effettuate via shell e non sostituisce review umana.
@@ -65,6 +65,16 @@
65
65
  "minConfidence": 0.6,
66
66
  "cacheSeconds": 120
67
67
  },
68
+ "verification": {
69
+ "enabled": false,
70
+ "mode": "warn",
71
+ "manifestPath": ".pi/jev-verification.json",
72
+ "defaultProfile": "common",
73
+ "maxRepairs": 1,
74
+ "commandTimeoutMs": 120000,
75
+ "outputChars": 6000,
76
+ "maxCommands": 4
77
+ },
68
78
  "exfilCheck": {
69
79
  "enabled": true,
70
80
  "tools": [
@@ -0,0 +1,22 @@
1
+ {
2
+ "version": 1,
3
+ "checks": [
4
+ {
5
+ "id": "typecheck",
6
+ "description": "TypeScript static check",
7
+ "command": "npm run typecheck",
8
+ "files": ["src/**/*.ts", "extensions/**/*.ts", "test/**/*.ts"]
9
+ },
10
+ {
11
+ "id": "unit",
12
+ "description": "Unit tests",
13
+ "command": "npm run test:unit",
14
+ "files": ["src/**/*.ts", "extensions/**/*.ts", "test/**/*.ts"]
15
+ },
16
+ {
17
+ "id": "diff-check",
18
+ "description": "Whitespace and patch integrity",
19
+ "command": "git diff --check"
20
+ }
21
+ ]
22
+ }
@@ -3,6 +3,7 @@ import type { Model, Provider } from "@earendil-works/pi-ai";
3
3
  import type { Api } from "@earendil-works/pi-ai";
4
4
  import { registerJevCommand, type UpstreamResult } from "../src/commands.ts";
5
5
  import { registerJevAskTool } from "../src/ask.ts";
6
+ import { registerVerificationTool, type VerificationToolRunResult } from "../src/verification-tool.ts";
6
7
  import {
7
8
  configSnapshot,
8
9
  loadConfig,
@@ -58,11 +59,33 @@ import {
58
59
  groundingKey,
59
60
  recordEvidence,
60
61
  recordToolResult,
62
+ parseExitCode,
61
63
  GROUNDING_QUESTIONS,
62
64
  type EvidenceEntry,
63
65
  type GroundingEvaluation,
64
66
  } from "../src/grounding.ts";
65
- import type { PostPublishCheckFn } from "../src/automatic/guardian.ts";
67
+ import type { PostPublishCheckFn, PostPublishCheckResult } from "../src/automatic/guardian.ts";
68
+ import {
69
+ buildVerificationStatus,
70
+ createVerificationState,
71
+ isVerificationPolicyPath,
72
+ decideVerificationAction,
73
+ loadVerificationPolicy,
74
+ manifestCheckForCommand,
75
+ normalizeObservedPath,
76
+ recordObservedModification,
77
+ recordVerificationRun,
78
+ selectVerificationChecks,
79
+ trustVerificationPolicy,
80
+ trustedVerificationPolicy,
81
+ type VerificationManifestTrust,
82
+ type VerificationStatus,
83
+ } from "../src/verification.ts";
84
+ import {
85
+ formatVerificationExecutions,
86
+ runVerificationChecks,
87
+ type VerificationExecution,
88
+ } from "../src/verification-runner.ts";
66
89
 
67
90
  export interface GateStatus {
68
91
  active: boolean;
@@ -100,6 +123,17 @@ export default function (pi: ExtensionAPI) {
100
123
  // Resettato a ogni session_start; solo automatic lo alimenta e lo usa.
101
124
  let evidenceLedger: EvidenceEntry[] = [];
102
125
  let evidenceSeq = 0;
126
+ // Stato distinto dal grounding: resta utile anche quando il grounding
127
+ // probabilistico è spento. Registra solo edit/write osservati e run di
128
+ // comandi approvati dal manifest del progetto.
129
+ let verificationState = createVerificationState();
130
+ let verificationSeq = 0;
131
+ /** Check bash/powershell iniziati davvero: seq=start, non result order. */
132
+ const verificationCheckStarts = new Map<string, { id: string; command: string; seq: number; source: "bash" | "powershell" }>();
133
+ let verificationCwd: string | undefined;
134
+ // Acquisito solo all'avvio sessione o con un comando utente esplicito.
135
+ // Il modello non può trasformare un edit al manifest in un comando eseguito.
136
+ let verificationManifestTrust: VerificationManifestTrust | undefined;
103
137
  let lastOutput: { tool: string; verdict: OutputVerdict; at: number } | undefined;
104
138
  let lastGate: { report: GateVerdictReport; at: number } | undefined;
105
139
 
@@ -189,6 +223,104 @@ export default function (pi: ExtensionAPI) {
189
223
  return getConfig();
190
224
  }
191
225
 
226
+ function captureVerificationManifest(cwd: string): VerificationManifestTrust {
227
+ verificationCwd = cwd;
228
+ verificationManifestTrust = trustVerificationPolicy(cwd, config.verification);
229
+ return verificationManifestTrust;
230
+ }
231
+
232
+ function verificationSnapshot(cwd: string): {
233
+ loaded: ReturnType<typeof loadVerificationPolicy>;
234
+ status: VerificationStatus;
235
+ } {
236
+ const loaded = trustedVerificationPolicy(
237
+ verificationManifestTrust,
238
+ cwd,
239
+ config.verification,
240
+ );
241
+ return { loaded, status: buildVerificationStatus(verificationState, loaded) };
242
+ }
243
+
244
+ function verificationReport(
245
+ status: VerificationStatus,
246
+ action: "passed" | "warned" | "repaired" | "held",
247
+ ): NonNullable<GateVerdictReport["verification"]> {
248
+ return {
249
+ action,
250
+ changedFiles: [...status.changedFiles],
251
+ checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
252
+ };
253
+ }
254
+
255
+ function recordVerificationExecution(
256
+ execution: VerificationExecution,
257
+ source: "jev_verify" | "bash" | "powershell" | "manual",
258
+ /** Marker al momento dell'avvio, non al tool_result. */
259
+ startedSeq = ++verificationSeq,
260
+ ): void {
261
+ recordVerificationRun(verificationState, {
262
+ id: execution.id,
263
+ command: execution.command,
264
+ ok: execution.ok,
265
+ ...(execution.exitCode !== undefined ? { exitCode: execution.exitCode } : {}),
266
+ seq: startedSeq,
267
+ source,
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Esegue SOLO check estratti dalla policy acquisita (locale, globale o
273
+ * profilo integrato). Serve sia al tool del modello sia a /jev verify.
274
+ */
275
+ async function runProjectVerification(args: {
276
+ cwd: string;
277
+ selection: string | undefined;
278
+ signal?: AbortSignal;
279
+ trusted: boolean;
280
+ }): Promise<VerificationToolRunResult> {
281
+ const settings = config.verification;
282
+ if (!settings.enabled) {
283
+ return {
284
+ ok: false,
285
+ message: "[jev_verify] Verifica progetto disattivata. Attivala con /jev verification warn|repair|hold.",
286
+ results: [],
287
+ };
288
+ }
289
+ if (!args.trusted) {
290
+ return {
291
+ ok: false,
292
+ message: "[jev_verify] Progetto non trusted: nessun comando del manifest è stato eseguito.",
293
+ results: [],
294
+ };
295
+ }
296
+ const { loaded, status } = verificationSnapshot(args.cwd);
297
+ const selected = selectVerificationChecks(
298
+ loaded,
299
+ status,
300
+ args.selection,
301
+ settings.maxCommands,
302
+ );
303
+ if (!selected.ok) {
304
+ return { ok: false, message: `[jev_verify] ${selected.error}`, results: [] };
305
+ }
306
+ // Snapshot dell'ordine PRIMA dell'esecuzione: se un edit concorrente
307
+ // termina mentre il runner è attivo, questi risultati restano
308
+ // conservativamente stale/pending invece di essere falsamente freschi.
309
+ const startedSeq = ++verificationSeq;
310
+ const results = await runVerificationChecks({
311
+ checks: selected.checks,
312
+ cwd: args.cwd,
313
+ settings,
314
+ signal: args.signal,
315
+ });
316
+ for (const result of results) recordVerificationExecution(result, "jev_verify", startedSeq);
317
+ return {
318
+ ok: results.length > 0 && results.every((result) => result.ok),
319
+ message: formatVerificationExecutions(results),
320
+ results,
321
+ };
322
+ }
323
+
192
324
  /** Provider live corrente dal registry (sempre fresco, mai snapshot). */
193
325
  function liveUpstreamProvider(
194
326
  registry: ModelRegistry,
@@ -226,7 +358,7 @@ export default function (pi: ExtensionAPI) {
226
358
  getConfig,
227
359
  getReview: getGateReview,
228
360
  onVerdict: recordGateVerdict,
229
- createPostCheck: createGroundingCheck,
361
+ createPostCheck,
230
362
  });
231
363
  }
232
364
 
@@ -435,6 +567,8 @@ export default function (pi: ExtensionAPI) {
435
567
  keyPresent: () => resolveBackend(getConfig()).apiKeyPresent,
436
568
  });
437
569
 
570
+ registerVerificationTool(pi, { run: runProjectVerification });
571
+
438
572
  registerJevCommand(pi, {
439
573
  config: getConfig,
440
574
  setMode: (mode) => {
@@ -476,6 +610,43 @@ export default function (pi: ExtensionAPI) {
476
610
  saveConfig(next);
477
611
  return refresh();
478
612
  },
613
+ verificationStatus: (cwd) => {
614
+ const { loaded, status } = verificationSnapshot(cwd);
615
+ return {
616
+ enabled: config.verification.enabled,
617
+ mode: config.verification.mode,
618
+ manifestPath: config.verification.manifestPath,
619
+ defaultProfile: config.verification.defaultProfile,
620
+ ...(loaded.ok
621
+ ? { manifestLoaded: true, manifestSource: loaded.source, manifestSourceLabel: loaded.sourceLabel }
622
+ : { manifestLoaded: false, manifestError: loaded.error }),
623
+ changedFiles: status.changedFiles,
624
+ checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
625
+ };
626
+ },
627
+ setVerification: (mode) => {
628
+ const next = configSnapshot(config);
629
+ if (mode === "off") {
630
+ next.verification.enabled = false;
631
+ } else {
632
+ next.verification.enabled = true;
633
+ next.verification.mode = mode;
634
+ }
635
+ saveConfig(next);
636
+ const refreshed = refresh();
637
+ // L'attivazione è un'azione utente esplicita: acquisisce gli input
638
+ // correnti del fallback invece di riusare una snapshot presa quando era off.
639
+ if (mode !== "off" && verificationCwd) captureVerificationManifest(verificationCwd);
640
+ return refreshed;
641
+ },
642
+ reloadVerificationManifest: (cwd) => {
643
+ const trusted = captureVerificationManifest(cwd);
644
+ return trusted.ok
645
+ ? { ok: true, path: trusted.path, sourceLabel: trusted.sourceLabel }
646
+ : { ok: false, error: trusted.error };
647
+ },
648
+ runVerification: ({ cwd, selection, signal, trusted }) =>
649
+ runProjectVerification({ cwd, selection, signal, trusted }),
479
650
  getLastGate: () => lastGate,
480
651
  });
481
652
 
@@ -492,6 +663,10 @@ export default function (pi: ExtensionAPI) {
492
663
  // Nuova sessione = nuova evidenza: il ledger non attraversa le sessioni.
493
664
  evidenceLedger = [];
494
665
  evidenceSeq = 0;
666
+ verificationState = createVerificationState();
667
+ verificationSeq = 0;
668
+ verificationCheckStarts.clear();
669
+ captureVerificationManifest(ctx.cwd);
495
670
  // Patch dei twin salvati in OGNI modalità (nessun setModel qui): senza,
496
671
  // un twin persistito non esisterebbe nel catalogo. Silenzioso: la
497
672
  // diagnostica resta agli hook di turno.
@@ -637,6 +812,7 @@ export default function (pi: ExtensionAPI) {
637
812
  // before_agent_start, ripatcha in silenzio (mai register/unregister).
638
813
  pi.on("turn_start", async (_event, ctx) => {
639
814
  try {
815
+ verificationCwd = ctx.cwd;
640
816
  ensurePatches(ctx.modelRegistry);
641
817
  } catch {
642
818
  // Mai bloccare il turno.
@@ -645,6 +821,7 @@ export default function (pi: ExtensionAPI) {
645
821
 
646
822
  pi.on("before_agent_start", async (_event, ctx) => {
647
823
  try {
824
+ verificationCwd = ctx.cwd;
648
825
  noteReassert(ensurePatches(ctx.modelRegistry));
649
826
  } catch {
650
827
  // mai bloccare il turno
@@ -749,6 +926,38 @@ export default function (pi: ExtensionAPI) {
749
926
  }
750
927
  });
751
928
 
929
+ /**
930
+ * Marca l'INIZIO reale di un check shell già autorizzato. Non basta
931
+ * l'ordine dei tool_result: bash e edit possono essere concorrenti e un
932
+ * test avviato prima di un edit non deve risultare fresco dopo di esso.
933
+ */
934
+ pi.on("tool_execution_start", async (event, ctx) => {
935
+ try {
936
+ if (sessionMode !== "automatic") return;
937
+ if (event.toolName !== "bash" && event.toolName !== "powershell") return;
938
+ verificationCwd = ctx.cwd;
939
+ const { loaded } = verificationSnapshot(ctx.cwd);
940
+ if (!loaded.ok) return;
941
+ const args = event.args as { command?: unknown };
942
+ const check = manifestCheckForCommand(loaded.manifest, args.command);
943
+ if (!check) return;
944
+ verificationCheckStarts.set(event.toolCallId, {
945
+ id: check.id,
946
+ command: check.command,
947
+ seq: ++verificationSeq,
948
+ source: event.toolName,
949
+ });
950
+ // Difesa memoria contro toolcall senza result (abort esterno).
951
+ while (verificationCheckStarts.size > 100) {
952
+ const oldest = verificationCheckStarts.keys().next().value;
953
+ if (oldest === undefined) break;
954
+ verificationCheckStarts.delete(oldest);
955
+ }
956
+ } catch {
957
+ // Nessun impatto sull'esecuzione del tool.
958
+ }
959
+ });
960
+
752
961
  // Policy pre-esecuzione tool (solo automatic).
753
962
  pi.on("tool_call", async (event, ctx) => {
754
963
  try {
@@ -780,6 +989,36 @@ export default function (pi: ExtensionAPI) {
780
989
 
781
990
  if (event.toolName === "write" || event.toolName === "edit") {
782
991
  const input = event.input as { path?: unknown };
992
+ const profileInput = verificationManifestTrust?.ok && verificationManifestTrust.source === "global-profile";
993
+ if (
994
+ isVerificationPolicyPath(input.path, ctx.cwd, verificationManifestTrust) &&
995
+ // Il profilo comune è opt-in: non rendere package.json immutabile
996
+ // nelle installazioni che non hanno attivato la verifica progetto.
997
+ (!profileInput || config.verification.enabled)
998
+ ) {
999
+ const ruleId = profileInput
1000
+ ? "verification-profile-input-immutable"
1001
+ : "verification-manifest-immutable";
1002
+ pi.appendEntry("jev-tool-block", {
1003
+ tool: event.toolName,
1004
+ ruleId,
1005
+ revision: TOOL_POLICY_REVISION,
1006
+ });
1007
+ if (ctx.hasUI) {
1008
+ ctx.ui.notify(
1009
+ profileInput
1010
+ ? "Jev tool policy: il modello non può modificare un file che decide i check del profilo globale acquisito."
1011
+ : "Jev tool policy: il modello non può modificare il manifest che autorizza i suoi check.",
1012
+ "warning",
1013
+ );
1014
+ }
1015
+ return {
1016
+ block: true,
1017
+ reason: profileInput
1018
+ ? "Bloccato da jev tool policy (verification-profile-input-immutable): modifica manualmente la configurazione del progetto e poi usa /jev verification reload."
1019
+ : "Bloccato da jev tool policy (verification-manifest-immutable): modifica il manifest manualmente e poi usa /jev verification reload.",
1020
+ };
1021
+ }
783
1022
  const verdict = checkFilePath(input.path);
784
1023
  if (verdict.blocked) {
785
1024
  pi.appendEntry("jev-tool-block", {
@@ -872,6 +1111,60 @@ export default function (pi: ExtensionAPI) {
872
1111
  return allow ? undefined : { block: true, reason: `Bloccato da jev exfil check (${summary}) (declined).` };
873
1112
  }
874
1113
 
1114
+ /**
1115
+ * Registra modifiche e risultati dei check configurati. È deliberatamente
1116
+ * separato dal grounding: il footer deterministico deve funzionare anche
1117
+ * senza una chiamata Jev aggiuntiva. I bash nativi contano soltanto se il
1118
+ * loro comando coincide ESATTAMENTE con un id del manifest.
1119
+ */
1120
+ function recordVerificationResult(event: {
1121
+ toolCallId?: string;
1122
+ toolName: string;
1123
+ input: unknown;
1124
+ content: unknown;
1125
+ isError: boolean;
1126
+ }, cwd: string): void {
1127
+ try {
1128
+ // Raccolta locale senza costo anche se il footer è spento: attivarlo a
1129
+ // metà sessione conserva gli edit/write già osservati. Pubblicazione,
1130
+ // repair ed esecuzione restano rigorosamente opt-in in config.
1131
+ if (sessionMode !== "automatic") return;
1132
+ verificationCwd = cwd;
1133
+ if (event.toolName === "edit" || event.toolName === "write") {
1134
+ if (event.isError) return;
1135
+ const input = event.input as { path?: unknown };
1136
+ const path = normalizeObservedPath(input.path, cwd);
1137
+ if (path) recordObservedModification(verificationState, path, ++verificationSeq);
1138
+ return;
1139
+ }
1140
+ if (event.toolName !== "bash" && event.toolName !== "powershell") return;
1141
+ const input = event.input as { command?: unknown };
1142
+ const started = event.toolCallId
1143
+ ? verificationCheckStarts.get(event.toolCallId)
1144
+ : undefined;
1145
+ if (event.toolCallId) verificationCheckStarts.delete(event.toolCallId);
1146
+ const { loaded } = verificationSnapshot(cwd);
1147
+ if (!loaded.ok) return;
1148
+ const check = manifestCheckForCommand(loaded.manifest, input.command);
1149
+ if (!check) return;
1150
+ const output = toolResultText(event.content);
1151
+ const exitCode = parseExitCode(output);
1152
+ recordVerificationRun(verificationState, {
1153
+ id: check.id,
1154
+ command: check.command,
1155
+ ok: !event.isError,
1156
+ ...(exitCode !== undefined ? { exitCode } : {}),
1157
+ // In runtime il marker esiste perché tool_execution_start precede il
1158
+ // result. Fallback conservativo per adapter/test legacy senza evento:
1159
+ // allocato qui, mai riusa un pass precedente.
1160
+ seq: started?.seq ?? ++verificationSeq,
1161
+ source: event.toolName,
1162
+ });
1163
+ } catch {
1164
+ // Un footer non deve mai cambiare il risultato di un tool.
1165
+ }
1166
+ }
1167
+
875
1168
  /** Registra un tool_result nel ledger evidenza (solo automatic, mai bloccare). */
876
1169
  function recordGroundingResult(event: {
877
1170
  toolName: string;
@@ -1020,12 +1313,127 @@ export default function (pi: ExtensionAPI) {
1020
1313
  }
1021
1314
  }
1022
1315
 
1316
+ /**
1317
+ * Post-check deterministico del codice: non dichiara il codice corretto,
1318
+ * ma allega il fatto osservato (edit/write + check manifest). In repair il
1319
+ * modello ha un solo giro per invocare `jev_verify` e correggere l'errore;
1320
+ * in hold una failure concreta non viene pubblicata.
1321
+ */
1322
+ function createVerificationCheck(): PostPublishCheckFn | undefined {
1323
+ try {
1324
+ if (sessionMode !== "automatic" || !config.verification.enabled) return undefined;
1325
+ const cwd = verificationCwd;
1326
+ if (!cwd) return undefined;
1327
+ const mode = config.verification.mode;
1328
+ const maxRepairs = config.verification.maxRepairs;
1329
+ let repairsUsed = 0;
1330
+ return async () => {
1331
+ const { status } = verificationSnapshot(cwd);
1332
+ if (status.changedFiles.length === 0) return { action: "publish" };
1333
+ const decision = decideVerificationAction({
1334
+ status,
1335
+ mode,
1336
+ repairsUsed,
1337
+ maxRepairs,
1338
+ });
1339
+ if (decision.action === "regenerate") {
1340
+ repairsUsed++;
1341
+ try {
1342
+ pi.appendEntry("jev-verification-repair", {
1343
+ files: status.changedFiles,
1344
+ checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
1345
+ });
1346
+ } catch {
1347
+ // solo diagnostica
1348
+ }
1349
+ return {
1350
+ action: "regenerate",
1351
+ feedback: decision.feedback,
1352
+ verification: verificationReport(status, "repaired"),
1353
+ };
1354
+ }
1355
+ if (decision.action === "hold") {
1356
+ try {
1357
+ pi.appendEntry("jev-verification-hold", {
1358
+ files: status.changedFiles,
1359
+ checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
1360
+ });
1361
+ } catch {
1362
+ // solo diagnostica
1363
+ }
1364
+ return {
1365
+ action: "hold",
1366
+ holdText: decision.holdText,
1367
+ verification: verificationReport(status, "held"),
1368
+ };
1369
+ }
1370
+ // "repaired" solo se il giro di repair è arrivato davvero a check
1371
+ // freschi tutti passati; altrimenti resta un warning, mai un successo
1372
+ // inventato solo perché il modello ha ricevuto feedback.
1373
+ const action = status.allPassed
1374
+ ? repairsUsed > 0 ? "repaired" : "passed"
1375
+ : "warned";
1376
+ try {
1377
+ pi.appendEntry("jev-verification-status", {
1378
+ action,
1379
+ files: status.changedFiles,
1380
+ checks: status.checks.map((item) => ({ id: item.check.id, state: item.state })),
1381
+ ...(status.manifestError ? { manifestError: status.manifestError } : {}),
1382
+ });
1383
+ } catch {
1384
+ // solo diagnostica
1385
+ }
1386
+ return {
1387
+ action: "publish",
1388
+ ...(decision.notice ? { notice: decision.notice } : {}),
1389
+ verification: verificationReport(status, action),
1390
+ };
1391
+ };
1392
+ } catch {
1393
+ // Fail-open: errore del footer non deve bloccare il gate.
1394
+ return undefined;
1395
+ }
1396
+ }
1397
+
1398
+ /** Compone grounding probabilistico e verifica deterministica senza perderne le note. */
1399
+ function createPostCheck(): PostPublishCheckFn | undefined {
1400
+ const grounding = createGroundingCheck();
1401
+ const verification = createVerificationCheck();
1402
+ if (!grounding && !verification) return undefined;
1403
+ return async (args) => {
1404
+ let groundingResult: PostPublishCheckResult | undefined;
1405
+ if (grounding) {
1406
+ groundingResult = await grounding(args);
1407
+ if (groundingResult.action !== "publish") return groundingResult;
1408
+ }
1409
+ let verificationResult: PostPublishCheckResult | undefined;
1410
+ if (verification) {
1411
+ verificationResult = await verification(args);
1412
+ if (verificationResult.action !== "publish") {
1413
+ return {
1414
+ ...verificationResult,
1415
+ ...(groundingResult?.grounding ? { grounding: groundingResult.grounding } : {}),
1416
+ };
1417
+ }
1418
+ }
1419
+ const notices = [groundingResult?.notice, verificationResult?.notice]
1420
+ .filter((notice): notice is string => Boolean(notice));
1421
+ return {
1422
+ action: "publish",
1423
+ ...(notices.length > 0 ? { notice: notices.join("") } : {}),
1424
+ ...(groundingResult?.grounding ? { grounding: groundingResult.grounding } : {}),
1425
+ ...(verificationResult?.verification ? { verification: verificationResult.verification } : {}),
1426
+ };
1427
+ };
1428
+ }
1429
+
1023
1430
  // Giudice output (solo automatic, fail-open): dopo l'esecuzione cerca
1024
1431
  // secret nel testo e classifica i fallimenti. Non blocca mai: allega
1025
1432
  // una riga al risultato che il modello legge.
1026
1433
  pi.on("tool_result", async (event, ctx) => {
1027
1434
  try {
1028
1435
  // Ledger prima di tutto: vale anche con outputJudge disattivato.
1436
+ recordVerificationResult(event, ctx.cwd);
1029
1437
  recordGroundingResult(event);
1030
1438
  if (sessionMode !== "automatic") return;
1031
1439
  if (!config.outputJudge.enabled) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-guard",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Jev validation guard for pi: on-demand tool + automatic provider gate. Dual backend: TypeSafe direct + OpenRouter.",
6
6
  "keywords": [