From f9effba3ef1d89e1351196d621428b140b509406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kuras?= Date: Thu, 28 May 2026 12:37:39 +0200 Subject: [PATCH 1/5] feat(core)!: rename validation window to verification phase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demo feedback: 'validation window' was misleading — people heard 'window' as a passive time range and missed that the feature is an active gate locking the agent into review-only mode before push. Rename across the domain layer to match the user mental model. This is a breaking change for any external caller of the policy or streaming API. Renames (core crate + migration): * PolicyScope::ValidationWindow → PolicyScope::VerificationPhase * ValidationWindowMode → VerificationPhaseMode * StreamEventType::ValidationWindowStart → ::VerificationPhaseStart * sessions.validation_window_started_at → sessions.verification_phase_started_at * repos.validation_window_mode → repos.verification_phase_mode * policies.scope value 'validation_window' → 'verification_phase' * Agent-policies renderer text rewritten to lead with the gate story (what is forbidden) instead of the time-range story (when measured). Migration 026_rename_validation_window_to_verification_phase.sql performs the column renames, drops + re-adds the relevant CHECK constraints with the new column names, and rewrites policies.scope='validation_window' rows in place. Closes the demo-presenter UX complaint. --- ...dation_window.rs => verification_phase.rs} | 0 crates/tracevault-core/src/agent_policies.rs | 119 +++++++++--------- crates/tracevault-core/src/policy.rs | 28 +++-- crates/tracevault-core/src/policy_eval.rs | 2 +- crates/tracevault-core/src/streaming.rs | 7 +- ...alidation_window_to_verification_phase.sql | 32 +++++ ... get_sessions_verification_phase_data.sql} | 0 ...de.sql => get_verification_phase_mode.sql} | 0 ...et_verification_phase_tool_call_stats.sql} | 0 ...> update_repo_verification_phase_mode.sql} | 0 ... => update_session_verification_phase.sql} | 0 11 files changed, 114 insertions(+), 74 deletions(-) rename crates/tracevault-cli/src/commands/{validation_window.rs => verification_phase.rs} (100%) create mode 100644 crates/tracevault-server/migrations/026_rename_validation_window_to_verification_phase.sql rename crates/tracevault-server/src/repo/sql/{get_sessions_window_data.sql => get_sessions_verification_phase_data.sql} (100%) rename crates/tracevault-server/src/repo/sql/{get_validation_window_mode.sql => get_verification_phase_mode.sql} (100%) rename crates/tracevault-server/src/repo/sql/{get_window_tool_call_stats.sql => get_verification_phase_tool_call_stats.sql} (100%) rename crates/tracevault-server/src/repo/sql/{update_repo_validation_window_mode.sql => update_repo_verification_phase_mode.sql} (100%) rename crates/tracevault-server/src/repo/sql/{update_session_validation_window.sql => update_session_verification_phase.sql} (100%) diff --git a/crates/tracevault-cli/src/commands/validation_window.rs b/crates/tracevault-cli/src/commands/verification_phase.rs similarity index 100% rename from crates/tracevault-cli/src/commands/validation_window.rs rename to crates/tracevault-cli/src/commands/verification_phase.rs diff --git a/crates/tracevault-core/src/agent_policies.rs b/crates/tracevault-core/src/agent_policies.rs index 300a6c3f..c91f4539 100644 --- a/crates/tracevault-core/src/agent_policies.rs +++ b/crates/tracevault-core/src/agent_policies.rs @@ -4,7 +4,9 @@ //! the dashboard preview. Lives in `tracevault-core` so there is exactly one //! rendering implementation regardless of which frontend asks for it. -use crate::policy::{PolicyAction, PolicyCondition, PolicyRule, PolicyScope, ValidationWindowMode}; +use crate::policy::{ + PolicyAction, PolicyCondition, PolicyRule, PolicyScope, VerificationPhaseMode, +}; /// One rendered tool-requirement line. #[derive(Debug, Clone)] @@ -83,22 +85,22 @@ fn scope_applies_to_session(scope: &PolicyScope) -> bool { matches!(scope, PolicyScope::Session | PolicyScope::Both) } -fn scope_applies_to_validation(scope: &PolicyScope) -> bool { - matches!(scope, PolicyScope::ValidationWindow | PolicyScope::Both) +fn scope_applies_to_verification(scope: &PolicyScope) -> bool { + matches!(scope, PolicyScope::VerificationPhase | PolicyScope::Both) } /// Render agent instructions as Markdown. /// /// `policies` may include disabled entries — they're filtered out here. -/// `validation_window_mode` controls whether the validation window section is +/// `verification_phase_mode` controls whether the verification phase section is /// rendered at all (it is hidden entirely when mode is `Disabled`). pub fn render_markdown( policies: &[PolicyRule], - validation_window_mode: &ValidationWindowMode, + verification_phase_mode: &VerificationPhaseMode, ) -> String { let mut session_reqs: Vec = Vec::new(); - let mut validation_required: Vec = Vec::new(); - let mut validation_allowed: Vec = Vec::new(); + let mut verification_required: Vec = Vec::new(); + let mut verification_allowed: Vec = Vec::new(); for p in policies.iter().filter(|p| p.enabled) { let reqs = condition_to_requirements(&p.condition, p.action); @@ -109,24 +111,24 @@ pub fn render_markdown( if scope_applies_to_session(&p.scope) { session_reqs.extend(reqs.iter().cloned()); } - if scope_applies_to_validation(&p.scope) - && !matches!(validation_window_mode, ValidationWindowMode::Disabled) + if scope_applies_to_verification(&p.scope) + && !matches!(verification_phase_mode, VerificationPhaseMode::Disabled) { for r in &reqs { if r.action == PolicyAction::Allow { - validation_allowed.push(r.clone()); + verification_allowed.push(r.clone()); } else { - validation_required.push(r.clone()); + verification_required.push(r.clone()); } } } } let has_session = !session_reqs.is_empty(); - let has_validation = (!validation_required.is_empty() || !validation_allowed.is_empty()) - && !matches!(validation_window_mode, ValidationWindowMode::Disabled); + let has_verification = (!verification_required.is_empty() || !verification_allowed.is_empty()) + && !matches!(verification_phase_mode, VerificationPhaseMode::Disabled); - if !has_session && !has_validation { + if !has_session && !has_verification { return "## Visdom Trace — agent policy instructions\n\n\ No active policies for this repository.\n" .into(); @@ -148,36 +150,35 @@ pub fn render_markdown( } } - if has_validation { - out.push_str("\n### Validation window (pre-push gating)\n"); + if has_verification { + out.push_str("\n### Verification phase (pre-push gating)\n"); out.push_str( - "A validation window restricts which tools can be called before push, \ - gating the push on a clean validation run. Before pushing, open a \ - validation window:\n\n tracevault validation-start\n\n\ - The window stays open until you push, or until you open a new window. \ - Opening a new window invalidates the prior one.\n", + "When you are done changing code and ready to push, you must enter a \ + **verification phase** by running:\n\n tracevault verify-start\n\n\ + From that point until the push, you are only allowed to call the tools \ + listed below. Any other tool call will fail the push.\n\n\ + The intent: catch agents that pretend to review while still editing. \ + If you need to make a code change after entering the phase, that is fine — \ + make the change, then run `tracevault verify-start` again to restart the \ + phase and rerun the required tools. The most recent `verify-start` is the \ + only one that counts.\n", ); - if !validation_required.is_empty() { - out.push_str("\nRequired tools (must be called inside the window):\n"); - for r in &validation_required { + if !verification_required.is_empty() { + out.push_str("\n**Required** — must be called inside the phase:\n"); + for r in &verification_required { out.push_str(&r.render_line()); out.push('\n'); } } - if !validation_allowed.is_empty() { - out.push_str("\nAllowed tools (may be called freely inside the window):\n"); - for r in &validation_allowed { + if !verification_allowed.is_empty() { + out.push_str("\n**Allowed** — may be called inside the phase without restriction:\n"); + for r in &verification_allowed { out.push_str(&r.render_line()); out.push('\n'); } } - - out.push_str( - "\nIf you need to call additional tools after opening the window, open a new window \ - afterwards and rerun all required tools.\n", - ); } out @@ -226,10 +227,10 @@ mod tests { #[test] fn no_policies_renders_terse_message() { - let out = render_markdown(&[], &ValidationWindowMode::Disabled); + let out = render_markdown(&[], &VerificationPhaseMode::Disabled); assert!(out.contains("No active policies")); assert!(!out.contains("Before push")); - assert!(!out.contains("Validation window")); + assert!(!out.contains("Verification phase")); } #[test] @@ -240,7 +241,7 @@ mod tests { PolicyScope::Session, false, ); - let out = render_markdown(&[p], &ValidationWindowMode::Disabled); + let out = render_markdown(&[p], &VerificationPhaseMode::Disabled); assert!(out.contains("No active policies")); } @@ -252,7 +253,7 @@ mod tests { PolicyScope::Session, true, ); - let out = render_markdown(&[p], &ValidationWindowMode::Disabled); + let out = render_markdown(&[p], &VerificationPhaseMode::Disabled); assert!(out.contains("### Before push")); assert!(out.contains("`cargo_fmt`")); assert!(out.contains("must be called")); @@ -266,7 +267,7 @@ mod tests { PolicyScope::Session, true, ); - let out = render_markdown(&[p], &ValidationWindowMode::Disabled); + let out = render_markdown(&[p], &VerificationPhaseMode::Disabled); let tool_line = out .lines() .find(|l| l.starts_with("- `cargo_fmt`")) @@ -283,7 +284,7 @@ mod tests { PolicyScope::Session, true, ); - let out_block = render_markdown(&[p_block], &ValidationWindowMode::Disabled); + let out_block = render_markdown(&[p_block], &VerificationPhaseMode::Disabled); assert!(out_block.contains("must be called and must succeed")); let p_warn = rule( @@ -292,7 +293,7 @@ mod tests { PolicyScope::Session, true, ); - let out_warn = render_markdown(&[p_warn], &ValidationWindowMode::Disabled); + let out_warn = render_markdown(&[p_warn], &VerificationPhaseMode::Disabled); assert!(out_warn.contains("should be called and should succeed")); } @@ -304,35 +305,35 @@ mod tests { PolicyScope::Session, true, ); - let out = render_markdown(&[p], &ValidationWindowMode::Disabled); + let out = render_markdown(&[p], &VerificationPhaseMode::Disabled); assert!(out.contains("when files matching `Cargo.lock` are changed")); assert!(out.contains("must be called and must succeed")); } #[test] - fn validation_window_required_section_renders() { + fn verification_phase_required_section_renders() { let p = rule( required(&["agent_review"], true), PolicyAction::BlockPush, - PolicyScope::ValidationWindow, + PolicyScope::VerificationPhase, true, ); - let out = render_markdown(&[p], &ValidationWindowMode::Block); - assert!(out.contains("### Validation window")); - assert!(out.contains("Required tools")); + let out = render_markdown(&[p], &VerificationPhaseMode::Block); + assert!(out.contains("### Verification phase")); + assert!(out.contains("**Required**")); assert!(out.contains("`agent_review`")); } #[test] - fn validation_window_allowed_section_renders() { + fn verification_phase_allowed_section_renders() { let p = rule( required(&["Read", "Grep"], false), PolicyAction::Allow, - PolicyScope::ValidationWindow, + PolicyScope::VerificationPhase, true, ); - let out = render_markdown(&[p], &ValidationWindowMode::Block); - assert!(out.contains("Allowed tools")); + let out = render_markdown(&[p], &VerificationPhaseMode::Block); + assert!(out.contains("**Allowed**")); assert!(out.contains("`Read`")); assert!(out.contains("`Grep`")); // Allow-listed tools render bare — no must/should language on those lines. @@ -342,15 +343,15 @@ mod tests { } #[test] - fn validation_window_section_hidden_when_mode_disabled() { + fn verification_phase_section_hidden_when_mode_disabled() { let p = rule( required(&["agent_review"], false), PolicyAction::BlockPush, - PolicyScope::ValidationWindow, + PolicyScope::VerificationPhase, true, ); - let out = render_markdown(&[p], &ValidationWindowMode::Disabled); - assert!(!out.contains("### Validation window")); + let out = render_markdown(&[p], &VerificationPhaseMode::Disabled); + assert!(!out.contains("### Verification phase")); assert!(out.contains("No active policies")); } @@ -365,7 +366,7 @@ mod tests { PolicyScope::Session, true, ); - let out = render_markdown(&[p], &ValidationWindowMode::Disabled); + let out = render_markdown(&[p], &VerificationPhaseMode::Disabled); assert!(out.contains("No active policies")); } @@ -377,9 +378,9 @@ mod tests { PolicyScope::Both, true, ); - let out = render_markdown(&[p], &ValidationWindowMode::Block); + let out = render_markdown(&[p], &VerificationPhaseMode::Block); assert!(out.contains("### Before push")); - assert!(out.contains("### Validation window")); + assert!(out.contains("### Verification phase")); assert_eq!(out.matches("`cargo_fmt`").count(), 2); } @@ -388,11 +389,11 @@ mod tests { let p = rule( required(&["agent_review"], false), PolicyAction::Warn, - PolicyScope::ValidationWindow, + PolicyScope::VerificationPhase, true, ); - let out = render_markdown(&[p], &ValidationWindowMode::Warn); - assert!(out.contains("### Validation window")); + let out = render_markdown(&[p], &VerificationPhaseMode::Warn); + assert!(out.contains("### Verification phase")); assert!(!out.contains("will block the push")); assert!(!out.contains("blocked")); assert!(out.contains("should be called")); diff --git a/crates/tracevault-core/src/policy.rs b/crates/tracevault-core/src/policy.rs index 7c3fb35c..cdce168a 100644 --- a/crates/tracevault-core/src/policy.rs +++ b/crates/tracevault-core/src/policy.rs @@ -46,15 +46,20 @@ pub enum PolicyCondition { }, } -/// Which evaluation window this policy applies to. +/// Which evaluation phase this policy applies to. +/// +/// The "verification phase" is the period after the agent declares it is +/// done changing code (via `tracevault verify-start`) and is now running +/// pre-push checks. Policies scoped to that phase evaluate only tool calls +/// made inside it; policies scoped to `Session` evaluate the whole push. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(rename_all = "snake_case")] pub enum PolicyScope { - /// Evaluated over the entire push window (default). + /// Evaluated over the entire push (default). #[default] Session, - /// Evaluated only against events inside the last validation window. - ValidationWindow, + /// Evaluated only against events inside the current verification phase. + VerificationPhase, /// Evaluated in both contexts. Both, } @@ -64,21 +69,22 @@ pub enum PolicyScope { pub enum PolicyAction { BlockPush, Warn, - /// Tool is explicitly permitted inside a validation window without a count - /// requirement. Prevents it from triggering the unknown-tool gate. + /// Tool is explicitly permitted inside the verification phase without + /// a count requirement. Prevents it from triggering the unknown-tool + /// gate. Allow, } /// Org/repo-level setting controlling what happens when an unknown tool -/// (not covered by any validation_window-scoped policy) is called inside -/// the validation window. +/// (not covered by any `verification_phase`-scoped policy) is called inside +/// an active verification phase. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(rename_all = "snake_case")] -pub enum ValidationWindowMode { - /// No window enforcement (default). +pub enum VerificationPhaseMode { + /// No verification-phase enforcement (default). #[default] Disabled, - /// Unknown tool call is flagged but push succeeds. + /// Unknown tool call is flagged but the push still succeeds. Warn, /// Unknown tool call blocks the push. Block, diff --git a/crates/tracevault-core/src/policy_eval.rs b/crates/tracevault-core/src/policy_eval.rs index b1d6b19c..c540a582 100644 --- a/crates/tracevault-core/src/policy_eval.rs +++ b/crates/tracevault-core/src/policy_eval.rs @@ -20,7 +20,7 @@ pub struct WindowGateOutcome { /// is an "unknown tool" violation. /// /// `window_tool_calls` — tool name → stats for calls *inside* the window. -/// `covered_tools` — tool names that have a `validation_window` or `both` +/// `covered_tools` — tool names that have a `verification_phase` or `both` /// scoped policy (regardless of action — even `allow` covers the tool). pub fn evaluate_window_gate( window_tool_calls: &HashMap, diff --git a/crates/tracevault-core/src/streaming.rs b/crates/tracevault-core/src/streaming.rs index 87416bf2..65ba26cf 100644 --- a/crates/tracevault-core/src/streaming.rs +++ b/crates/tracevault-core/src/streaming.rs @@ -9,9 +9,10 @@ pub enum StreamEventType { Transcript, SessionStart, SessionEnd, - /// Client declares the start of a validation window. Only the most recent - /// event of this type per session is meaningful — re-opening is idempotent. - ValidationWindowStart, + /// Client declares the start of a verification phase. Only the most + /// recent event of this type per session is meaningful — re-opening + /// is idempotent and just moves the cursor forward. + VerificationPhaseStart, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/tracevault-server/migrations/026_rename_validation_window_to_verification_phase.sql b/crates/tracevault-server/migrations/026_rename_validation_window_to_verification_phase.sql new file mode 100644 index 00000000..63e8c240 --- /dev/null +++ b/crates/tracevault-server/migrations/026_rename_validation_window_to_verification_phase.sql @@ -0,0 +1,32 @@ +-- Rename the user-facing "validation window" concept to "verification +-- phase". The old name was unclear in demos — people read "window" as +-- a passive time range and missed that the feature is an active gate +-- that locks the agent into verification mode before push. +-- +-- Breaking change: the policies.scope value, the repos column, and the +-- sessions column all rename together so the wire format and the DB +-- stay aligned. No back-compat aliases. + +-- 1. sessions: rename the timestamp column. +ALTER TABLE sessions + RENAME COLUMN validation_window_started_at TO verification_phase_started_at; + +-- 2. policies: migrate any rows with scope = 'validation_window' to +-- 'verification_phase', then swap the CHECK constraint. +UPDATE policies SET scope = 'verification_phase' WHERE scope = 'validation_window'; + +ALTER TABLE policies + DROP CONSTRAINT IF EXISTS policies_scope_check; +ALTER TABLE policies + ADD CONSTRAINT policies_scope_check + CHECK (scope IN ('session', 'verification_phase', 'both')); + +-- 3. repos: rename the per-repo mode column and reapply the CHECK. +ALTER TABLE repos + RENAME COLUMN validation_window_mode TO verification_phase_mode; + +ALTER TABLE repos + DROP CONSTRAINT IF EXISTS repos_validation_window_mode_check; +ALTER TABLE repos + ADD CONSTRAINT repos_verification_phase_mode_check + CHECK (verification_phase_mode IN ('disabled', 'warn', 'block')); diff --git a/crates/tracevault-server/src/repo/sql/get_sessions_window_data.sql b/crates/tracevault-server/src/repo/sql/get_sessions_verification_phase_data.sql similarity index 100% rename from crates/tracevault-server/src/repo/sql/get_sessions_window_data.sql rename to crates/tracevault-server/src/repo/sql/get_sessions_verification_phase_data.sql diff --git a/crates/tracevault-server/src/repo/sql/get_validation_window_mode.sql b/crates/tracevault-server/src/repo/sql/get_verification_phase_mode.sql similarity index 100% rename from crates/tracevault-server/src/repo/sql/get_validation_window_mode.sql rename to crates/tracevault-server/src/repo/sql/get_verification_phase_mode.sql diff --git a/crates/tracevault-server/src/repo/sql/get_window_tool_call_stats.sql b/crates/tracevault-server/src/repo/sql/get_verification_phase_tool_call_stats.sql similarity index 100% rename from crates/tracevault-server/src/repo/sql/get_window_tool_call_stats.sql rename to crates/tracevault-server/src/repo/sql/get_verification_phase_tool_call_stats.sql diff --git a/crates/tracevault-server/src/repo/sql/update_repo_validation_window_mode.sql b/crates/tracevault-server/src/repo/sql/update_repo_verification_phase_mode.sql similarity index 100% rename from crates/tracevault-server/src/repo/sql/update_repo_validation_window_mode.sql rename to crates/tracevault-server/src/repo/sql/update_repo_verification_phase_mode.sql diff --git a/crates/tracevault-server/src/repo/sql/update_session_validation_window.sql b/crates/tracevault-server/src/repo/sql/update_session_verification_phase.sql similarity index 100% rename from crates/tracevault-server/src/repo/sql/update_session_validation_window.sql rename to crates/tracevault-server/src/repo/sql/update_session_verification_phase.sql From ee93a258150b3fd10125219a7a68b70f465ae57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kuras?= Date: Thu, 28 May 2026 12:38:02 +0200 Subject: [PATCH 2/5] feat!: wire verification phase rename through server, CLI, web, MCP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the core rename. Wires the new domain types through: * Server API: PUT/GET repo settings JSON now uses verification_phase_mode; check endpoint matches policy scope on 'verification_phase'; SQL files renamed in lockstep with column renames from migration 026. * CLI: `tracevault validation-start` → `tracevault verify-start`. No alias for the old name; existing scripts will need to update. Command module renamed validation_window.rs → verification_phase.rs. * Web Settings: 'Validation Window' section heading and copy rewritten to lead with what the feature DOES (lock the agent into review-only mode before push) rather than what it MEASURES (a time range). New dropdown labels: 'Disabled (no enforcement)', 'Warn (record but allow push)', 'Block the push' — each option now says outcome explicitly. * Web Policies editor: scope dropdown renamed 'Validation' → 'Verification'. * MCP integration: tool description updated to match. * tracevault-server tests updated for new names; same coverage. Breaking changes for external integrators: * CLI scripts calling `tracevault validation-start` must change. * Anything reading the policies/check API for scope values must expect 'verification_phase' instead of 'validation_window'. * Anything reading the repo settings API for validation_window_mode must read verification_phase_mode instead. * StreamEventType wire value 'validation_window_start' is now 'verification_phase_start'. --- crates/tracevault-cli/src/commands/mod.rs | 2 +- .../src/commands/verification_phase.rs | 18 ++++----- crates/tracevault-cli/src/main.rs | 16 ++++---- .../src/api/agent_instructions.rs | 6 +-- crates/tracevault-server/src/api/policies.rs | 34 +++++++++-------- crates/tracevault-server/src/api/repos.rs | 20 +++++----- crates/tracevault-server/src/repo/events.rs | 15 ++++---- crates/tracevault-server/src/repo/policies.rs | 6 +-- .../src/repo/sql/get_repo_settings.sql | 2 +- .../src/repo/sql/get_repo_settings_by_id.sql | 2 +- .../get_sessions_verification_phase_data.sql | 2 +- .../repo/sql/get_verification_phase_mode.sql | 2 +- .../update_repo_verification_phase_mode.sql | 2 +- .../sql/update_session_verification_phase.sql | 2 +- .../tracevault-server/src/service/stream.rs | 6 +-- .../tests/repo_policies_test.rs | 22 +++++------ integrations/tracevault-mcp/src/index.ts | 2 +- web/src/lib/components/RepoPolicies.svelte | 8 ++-- .../[slug]/repos/[id]/settings/+page.svelte | 38 +++++++++++-------- 19 files changed, 107 insertions(+), 98 deletions(-) diff --git a/crates/tracevault-cli/src/commands/mod.rs b/crates/tracevault-cli/src/commands/mod.rs index 8a3adbc0..76337990 100644 --- a/crates/tracevault-cli/src/commands/mod.rs +++ b/crates/tracevault-cli/src/commands/mod.rs @@ -9,5 +9,5 @@ pub mod stats; pub mod status; pub mod stream; pub mod sync; -pub mod validation_window; +pub mod verification_phase; pub mod verify; diff --git a/crates/tracevault-cli/src/commands/verification_phase.rs b/crates/tracevault-cli/src/commands/verification_phase.rs index 71b346a3..da31144f 100644 --- a/crates/tracevault-cli/src/commands/verification_phase.rs +++ b/crates/tracevault-cli/src/commands/verification_phase.rs @@ -8,16 +8,16 @@ use crate::commands::stream::next_event_index; use crate::config::TracevaultConfig; use crate::credentials::Credentials; -/// Send a ValidationWindowStart event to the server, recording the current -/// timestamp as the start of the validation window for this session. +/// Send a VerificationPhaseStart event to the server, recording the current +/// timestamp as the start of the verification phase for this session. /// /// Only the most recent call per session matters — calling this again simply -/// moves the window cursor forward, discarding events from the previous window. +/// moves the phase cursor forward, discarding events from the previous phase. /// /// `explicit_session_id` — when Some, targets that session directly. /// When None, the most recently modified session directory is used (suitable /// for single-agent setups; pass `--session-id` in multi-agent setups). -pub async fn open_validation_window( +pub async fn open_verification_phase( project_root: &Path, explicit_session_id: Option<&str>, ) -> Result<(), String> { @@ -66,7 +66,7 @@ pub async fn open_validation_window( let event = StreamEventRequest { protocol_version: 2, tool: None, - event_type: StreamEventType::ValidationWindowStart, + event_type: StreamEventType::VerificationPhaseStart, session_id: session_id.clone(), timestamp: chrono::Utc::now(), hook_event_name: None, @@ -93,11 +93,11 @@ pub async fn open_validation_window( client .stream_event(org_slug, repo_id, &event) .await - .map_err(|e| format!("Failed to send validation window event: {e}"))?; + .map_err(|e| format!("Failed to send verification phase event: {e}"))?; - println!("✓ Validation window opened for session {session_id}"); - println!(" Tool calls from this point are evaluated by validation_window-scoped policies."); - println!(" Run `tracevault validation-start` again to reset the window if needed."); + println!("✓ Verification phase opened for session {session_id}"); + println!(" Tool calls from this point are evaluated by verification_phase-scoped policies."); + println!(" Run `tracevault verify-start` again to reset the phase if needed."); Ok(()) } diff --git a/crates/tracevault-cli/src/main.rs b/crates/tracevault-cli/src/main.rs index b4a69907..5edb60d9 100644 --- a/crates/tracevault-cli/src/main.rs +++ b/crates/tracevault-cli/src/main.rs @@ -70,17 +70,17 @@ enum Cli { #[arg(long)] range: Option, }, - /// Open (or re-open) a validation window for the current session. + /// Open (or re-open) a verification phase for the current session. /// - /// A validation window declares that the agent has finished making changes + /// A verification phase declares that the agent has finished making changes /// and is now running quality checks. Only tool calls made after this point - /// are evaluated by validation_window-scoped policies. Calling this again - /// resets the window, discarding earlier window events. + /// are evaluated by verification_phase-scoped policies. Calling this again + /// resets the phase, discarding earlier verification events. /// /// In single-agent setups the current session is detected automatically. /// In multi-agent setups, pass --session-id to target the correct session. - #[command(name = "validation-start")] - ValidationStart { + #[command(name = "verify-start")] + VerifyStart { /// Explicit session ID to open the window for. When omitted, the most /// recently active session under .tracevault/sessions/ is used. #[arg(long)] @@ -195,10 +195,10 @@ async fn main() { std::process::exit(1); } } - Cli::ValidationStart { session_id } => { + Cli::VerifyStart { session_id } => { let cwd = env::current_dir().expect("Cannot determine current directory"); if let Err(e) = - commands::validation_window::open_validation_window(&cwd, session_id.as_deref()) + commands::verification_phase::open_verification_phase(&cwd, session_id.as_deref()) .await { eprintln!("Error: {e}"); diff --git a/crates/tracevault-server/src/api/agent_instructions.rs b/crates/tracevault-server/src/api/agent_instructions.rs index 34c7261f..d68f0cf7 100644 --- a/crates/tracevault-server/src/api/agent_instructions.rs +++ b/crates/tracevault-server/src/api/agent_instructions.rs @@ -37,7 +37,7 @@ pub async fn get_agent_instructions( } let rows = PolicyRepo::list_for_repo(&state.pool, auth.org_id, repo_id).await?; - let mode_str = PolicyRepo::get_validation_window_mode(&state.pool, repo_id).await?; + let mode_str = PolicyRepo::get_verification_phase_mode(&state.pool, repo_id).await?; // Skip rows that don't parse cleanly — they're invalid but shouldn't fail // the whole render. The renderer operates on core's native domain types. @@ -46,9 +46,9 @@ pub async fn get_agent_instructions( .filter_map(map_row_to_policy_rule) .collect(); - let mode: tracevault_core::policy::ValidationWindowMode = + let mode: tracevault_core::policy::VerificationPhaseMode = serde_json::from_value(serde_json::Value::String(mode_str)) - .unwrap_or(tracevault_core::policy::ValidationWindowMode::Disabled); + .unwrap_or(tracevault_core::policy::VerificationPhaseMode::Disabled); let content = tracevault_core::agent_policies::render_markdown(&rules, &mode); diff --git a/crates/tracevault-server/src/api/policies.rs b/crates/tracevault-server/src/api/policies.rs index 34d6e984..26888b1f 100644 --- a/crates/tracevault-server/src/api/policies.rs +++ b/crates/tracevault-server/src/api/policies.rs @@ -317,7 +317,7 @@ pub async fn check_policies( let rows = PolicyRepo::list_enabled_for_check(&state.pool, auth.org_id, repo_id).await?; // Fetch validation window mode for this repo - let window_mode = PolicyRepo::get_validation_window_mode(&state.pool, repo_id).await?; + let window_mode = PolicyRepo::get_verification_phase_mode(&state.pool, repo_id).await?; // Aggregate session-level tool calls and files across all sessions let mut all_tool_calls: std::collections::HashMap = @@ -344,15 +344,16 @@ pub async fn check_policies( } // Batch-resolve window stats: single query fetches (session_id, db_id, - // validation_window_started_at) for all sessions in this push, then a + // verification_phase_started_at) for all sessions in this push, then a // second query aggregates window tool-call stats for all sessions that // have an open window. This avoids N+1 round-trips. - let window_rows: Vec<(String, Uuid, Option>)> = - sqlx::query_as(include_str!("../repo/sql/get_sessions_window_data.sql")) - .bind(repo_id) - .bind(&session_ids) - .fetch_all(&state.pool) - .await?; + let window_rows: Vec<(String, Uuid, Option>)> = sqlx::query_as( + include_str!("../repo/sql/get_sessions_verification_phase_data.sql"), + ) + .bind(repo_id) + .bind(&session_ids) + .fetch_all(&state.pool) + .await?; let mut window_tool_calls: std::collections::HashMap = std::collections::HashMap::new(); @@ -363,7 +364,8 @@ pub async fn check_policies( continue; }; has_any_window = true; - let stats = EventRepo::get_window_tool_call_stats(&state.pool, *db_id, *ts).await?; + let stats = + EventRepo::get_verification_phase_tool_call_stats(&state.pool, *db_id, *ts).await?; for (name, s) in stats { let entry = window_tool_calls.entry(name).or_default(); entry.total += s.total; @@ -419,12 +421,12 @@ pub async fn check_policies( } // Select the evaluation dataset based on scope. - // "validation_window" evaluates only tool calls after validation-start; + // "verification_phase" evaluates only tool calls after verify-start; // "session" (default) evaluates all tool calls in the session. // Legacy "both" entries fall through to session evaluation. let datasets: &[(&std::collections::HashMap, &str)] = match scope.as_str() { - "validation_window" => { + "verification_phase" => { if !has_any_window { // No window opened — skip entirely. results.push(CheckResult { @@ -502,7 +504,7 @@ pub async fn check_policies( if has_any_window && window_mode != "disabled" { let covered_tools: Vec = rows .iter() - .filter(|(_, _, _, _, _, scope)| scope == "validation_window") + .filter(|(_, _, _, _, _, scope)| scope == "verification_phase") .flat_map(|(_, _, condition, _, _, _)| extract_policy_tool_names(condition)) .collect(); @@ -534,7 +536,7 @@ pub async fn check_policies( auth.org_id, repo_id, None, - "validation_window_gate", + "verification_phase_gate", session_id_for_log, req.commit_sha.as_deref(), gate_result, @@ -546,11 +548,11 @@ pub async fn check_policies( ) .await { - tracing::warn!(error = %e, "failed to record validation_window_gate evaluation"); + tracing::warn!(error = %e, "failed to record verification_phase_gate evaluation"); } results.push(CheckResult { - rule_name: "validation_window_gate".into(), + rule_name: "verification_phase_gate".into(), result: gate_result.into(), action: gate_action.into(), severity: "medium".into(), @@ -733,7 +735,7 @@ fn classify_result(outcome: &tracevault_core::policy_eval::EvalOutcome) -> &'sta const VALID_ACTIONS: &[&str] = &["block_push", "warn", "allow"]; /// Valid scope values. Keep in sync with `PolicyScope` in tracevault-core. -const VALID_SCOPES: &[&str] = &["session", "validation_window"]; +const VALID_SCOPES: &[&str] = &["session", "verification_phase"]; fn validate_action(action: &str) -> Result<(), AppError> { if !VALID_ACTIONS.contains(&action) { diff --git a/crates/tracevault-server/src/api/repos.rs b/crates/tracevault-server/src/api/repos.rs index 99af22c5..1218581d 100644 --- a/crates/tracevault-server/src/api/repos.rs +++ b/crates/tracevault-server/src/api/repos.rs @@ -213,7 +213,7 @@ pub struct RepoSettingsResponse { pub has_deploy_key: bool, pub has_webhook_secret: bool, pub last_fetched_at: Option>, - pub validation_window_mode: String, + pub verification_phase_mode: String, } pub async fn get_settings( @@ -244,7 +244,7 @@ pub async fn get_settings( has_deploy_key: row.2.is_some(), has_webhook_secret: row.3.is_some(), last_fetched_at: row.4, - validation_window_mode: row.5, + verification_phase_mode: row.5, })) } @@ -253,7 +253,7 @@ pub struct UpdateSettingsRequest { pub github_url: Option, pub deploy_key: Option, pub webhook_secret: Option, - pub validation_window_mode: Option, + pub verification_phase_mode: Option, } pub async fn update_settings( @@ -302,17 +302,17 @@ pub async fn update_settings( .await?; } - // Update validation_window_mode if provided + // Update verification_phase_mode if provided const VALID_WINDOW_MODES: &[&str] = &["disabled", "warn", "block"]; - if let Some(ref mode) = req.validation_window_mode { + if let Some(ref mode) = req.verification_phase_mode { if !VALID_WINDOW_MODES.contains(&mode.as_str()) { return Err(AppError::BadRequest(format!( - "validation_window_mode must be one of: {}", + "verification_phase_mode must be one of: {}", VALID_WINDOW_MODES.join(", ") ))); } sqlx::query(include_str!( - "../repo/sql/update_repo_validation_window_mode.sql" + "../repo/sql/update_repo_verification_phase_mode.sql" )) .bind(mode) .bind(id) @@ -360,7 +360,7 @@ pub async fn update_settings( let has_deploy_key = row.2.is_some(); let has_webhook_secret = row.4.is_some(); let last_fetched_at = row.5; - let validation_window_mode = row.6.clone(); + let verification_phase_mode = row.6.clone(); // Auto-trigger clone/sync if both github_url and deploy_key are set if let Some(url) = &github_url { @@ -385,7 +385,7 @@ pub async fn update_settings( has_deploy_key, has_webhook_secret, last_fetched_at, - validation_window_mode, + verification_phase_mode, })); } "ready" => { @@ -412,6 +412,6 @@ pub async fn update_settings( has_deploy_key, has_webhook_secret, last_fetched_at, - validation_window_mode, + verification_phase_mode, })) } diff --git a/crates/tracevault-server/src/repo/events.rs b/crates/tracevault-server/src/repo/events.rs index cdb819f8..088f4438 100644 --- a/crates/tracevault-server/src/repo/events.rs +++ b/crates/tracevault-server/src/repo/events.rs @@ -132,7 +132,7 @@ impl EventRepo { /// Aggregate tool call stats for a session that occurred *after* a given /// timestamp (i.e. inside the validation window). /// Returns a map of tool_name → (total, successful). - pub async fn get_window_tool_call_stats( + pub async fn get_verification_phase_tool_call_stats( pool: &PgPool, session_db_id: Uuid, window_started_at: DateTime, @@ -140,12 +140,13 @@ impl EventRepo { std::collections::HashMap, AppError, > { - let rows: Vec<(String, i64, i64)> = - sqlx::query_as(include_str!("sql/get_window_tool_call_stats.sql")) - .bind(session_db_id) - .bind(window_started_at) - .fetch_all(pool) - .await?; + let rows: Vec<(String, i64, i64)> = sqlx::query_as(include_str!( + "sql/get_verification_phase_tool_call_stats.sql" + )) + .bind(session_db_id) + .bind(window_started_at) + .fetch_all(pool) + .await?; let map = rows .into_iter() diff --git a/crates/tracevault-server/src/repo/policies.rs b/crates/tracevault-server/src/repo/policies.rs index eefc617d..4286d16d 100644 --- a/crates/tracevault-server/src/repo/policies.rs +++ b/crates/tracevault-server/src/repo/policies.rs @@ -230,13 +230,13 @@ impl PolicyRepo { Ok(rows) } - /// Fetch the validation_window_mode for a repo. - pub async fn get_validation_window_mode( + /// Fetch the verification_phase_mode for a repo. + pub async fn get_verification_phase_mode( pool: &PgPool, repo_id: Uuid, ) -> Result { let mode: Option = - sqlx::query_scalar(include_str!("sql/get_validation_window_mode.sql")) + sqlx::query_scalar(include_str!("sql/get_verification_phase_mode.sql")) .bind(repo_id) .fetch_optional(pool) .await? diff --git a/crates/tracevault-server/src/repo/sql/get_repo_settings.sql b/crates/tracevault-server/src/repo/sql/get_repo_settings.sql index b2b922ad..7efee7b6 100644 --- a/crates/tracevault-server/src/repo/sql/get_repo_settings.sql +++ b/crates/tracevault-server/src/repo/sql/get_repo_settings.sql @@ -1,3 +1,3 @@ -SELECT github_url, clone_status, deploy_key_encrypted, webhook_secret_encrypted, last_fetched_at, validation_window_mode +SELECT github_url, clone_status, deploy_key_encrypted, webhook_secret_encrypted, last_fetched_at, verification_phase_mode FROM repos WHERE id = $1 AND org_id = $2 diff --git a/crates/tracevault-server/src/repo/sql/get_repo_settings_by_id.sql b/crates/tracevault-server/src/repo/sql/get_repo_settings_by_id.sql index a967f2e0..c565e27c 100644 --- a/crates/tracevault-server/src/repo/sql/get_repo_settings_by_id.sql +++ b/crates/tracevault-server/src/repo/sql/get_repo_settings_by_id.sql @@ -1,3 +1,3 @@ -SELECT github_url, clone_status, deploy_key_encrypted, deploy_key_nonce, webhook_secret_encrypted, last_fetched_at, validation_window_mode +SELECT github_url, clone_status, deploy_key_encrypted, deploy_key_nonce, webhook_secret_encrypted, last_fetched_at, verification_phase_mode FROM repos WHERE id = $1 diff --git a/crates/tracevault-server/src/repo/sql/get_sessions_verification_phase_data.sql b/crates/tracevault-server/src/repo/sql/get_sessions_verification_phase_data.sql index 6672b87d..05d03adf 100644 --- a/crates/tracevault-server/src/repo/sql/get_sessions_verification_phase_data.sql +++ b/crates/tracevault-server/src/repo/sql/get_sessions_verification_phase_data.sql @@ -1,4 +1,4 @@ -SELECT DISTINCT ON (session_id) session_id, id, validation_window_started_at +SELECT DISTINCT ON (session_id) session_id, id, verification_phase_started_at FROM sessions WHERE repo_id = $1 AND session_id = ANY($2) ORDER BY session_id, started_at DESC diff --git a/crates/tracevault-server/src/repo/sql/get_verification_phase_mode.sql b/crates/tracevault-server/src/repo/sql/get_verification_phase_mode.sql index 86e0dd1d..c82071c8 100644 --- a/crates/tracevault-server/src/repo/sql/get_verification_phase_mode.sql +++ b/crates/tracevault-server/src/repo/sql/get_verification_phase_mode.sql @@ -1 +1 @@ -SELECT validation_window_mode FROM repos WHERE id = $1 +SELECT verification_phase_mode FROM repos WHERE id = $1 diff --git a/crates/tracevault-server/src/repo/sql/update_repo_verification_phase_mode.sql b/crates/tracevault-server/src/repo/sql/update_repo_verification_phase_mode.sql index cc41e89f..b2728ea8 100644 --- a/crates/tracevault-server/src/repo/sql/update_repo_verification_phase_mode.sql +++ b/crates/tracevault-server/src/repo/sql/update_repo_verification_phase_mode.sql @@ -1 +1 @@ -UPDATE repos SET validation_window_mode = $1 WHERE id = $2 +UPDATE repos SET verification_phase_mode = $1 WHERE id = $2 diff --git a/crates/tracevault-server/src/repo/sql/update_session_verification_phase.sql b/crates/tracevault-server/src/repo/sql/update_session_verification_phase.sql index 61420a47..7b230ea0 100644 --- a/crates/tracevault-server/src/repo/sql/update_session_verification_phase.sql +++ b/crates/tracevault-server/src/repo/sql/update_session_verification_phase.sql @@ -1 +1 @@ -UPDATE sessions SET validation_window_started_at = $1 WHERE id = $2 +UPDATE sessions SET verification_phase_started_at = $1 WHERE id = $2 diff --git a/crates/tracevault-server/src/service/stream.rs b/crates/tracevault-server/src/service/stream.rs index 5a140057..ed4375ec 100644 --- a/crates/tracevault-server/src/service/stream.rs +++ b/crates/tracevault-server/src/service/stream.rs @@ -327,10 +327,10 @@ impl StreamService { // 8. SessionStart -- session already upserted above StreamEventType::SessionStart => {} - // 9. ValidationWindowStart — record timestamp; only the latest matters - StreamEventType::ValidationWindowStart => { + // 9. VerificationPhaseStart — record timestamp; only the latest matters + StreamEventType::VerificationPhaseStart => { sqlx::query(include_str!( - "../repo/sql/update_session_validation_window.sql" + "../repo/sql/update_session_verification_phase.sql" )) .bind(req.timestamp) .bind(session_db_id) diff --git a/crates/tracevault-server/tests/repo_policies_test.rs b/crates/tracevault-server/tests/repo_policies_test.rs index 2891707e..c5e2e246 100644 --- a/crates/tracevault-server/tests/repo_policies_test.rs +++ b/crates/tracevault-server/tests/repo_policies_test.rs @@ -329,10 +329,10 @@ async fn evaluation_row_survives_policy_delete(pool: sqlx::PgPool) { assert_eq!(rows[0].policy_name, "will-be-deleted"); } -// ── Validation window scope tests ───────────────────────────────────────────── +// ── Verification phase scope tests ───────────────────────────────────────────── #[sqlx::test(migrations = "./migrations")] -async fn validation_window_policy_skipped_when_no_window(pool: sqlx::PgPool) { +async fn verification_phase_policy_skipped_when_no_phase(pool: sqlx::PgPool) { use tracevault_server::repo::policies::PolicyRepo; let org_id = common::seed_org(&pool).await; @@ -347,7 +347,7 @@ async fn validation_window_policy_skipped_when_no_window(pool: sqlx::PgPool) { &json!({"type": "RequiredToolCall", "tool_names": ["cargo_fmt"]}), "block_push", "medium", - "validation_window", + "verification_phase", true, ) .await @@ -357,12 +357,12 @@ async fn validation_window_policy_skipped_when_no_window(pool: sqlx::PgPool) { .await .unwrap(); - // A validation_window-scoped policy with no tool calls and no window + // A verification_phase-scoped policy with no tool calls and no window // should be skipped (not fail) — the check_policies handler handles this, // but here we verify the DB layer returns scope correctly. assert!(policies .iter() - .any(|(id, _, _, _, _, scope)| *id == policy_id && scope == "validation_window")); + .any(|(id, _, _, _, _, scope)| *id == policy_id && scope == "verification_phase")); } #[sqlx::test(migrations = "./migrations")] @@ -381,7 +381,7 @@ async fn allow_scope_policy_stored_correctly(pool: sqlx::PgPool) { &json!({"type": "RequiredToolCall", "tool_names": ["Read"]}), "allow", "low", - "validation_window", + "verification_phase", true, ) .await @@ -395,7 +395,7 @@ async fn allow_scope_policy_stored_correctly(pool: sqlx::PgPool) { assert!(row.is_some()); let (_, _, _, action, _, scope) = row.unwrap(); assert_eq!(action, "allow"); - assert_eq!(scope, "validation_window"); + assert_eq!(scope, "verification_phase"); } #[sqlx::test(migrations = "./migrations")] @@ -431,13 +431,13 @@ async fn both_scope_policy_stored_correctly(pool: sqlx::PgPool) { } #[sqlx::test(migrations = "./migrations")] -async fn get_validation_window_mode_default_is_disabled(pool: sqlx::PgPool) { +async fn get_verification_phase_mode_default_is_disabled(pool: sqlx::PgPool) { use tracevault_server::repo::policies::PolicyRepo; let org_id = common::seed_org(&pool).await; let repo_id = common::seed_repo(&pool, org_id).await; - let mode = PolicyRepo::get_validation_window_mode(&pool, repo_id) + let mode = PolicyRepo::get_verification_phase_mode(&pool, repo_id) .await .unwrap(); assert_eq!(mode, "disabled"); @@ -487,7 +487,7 @@ async fn window_tool_call_stats_after_timestamp(pool: sqlx::PgPool) { .await .unwrap(); - let stats = EventRepo::get_window_tool_call_stats(&pool, session_db_id, before_window) + let stats = EventRepo::get_verification_phase_tool_call_stats(&pool, session_db_id, before_window) .await .unwrap(); @@ -540,7 +540,7 @@ async fn window_tool_call_stats_excludes_pre_window_events(pool: sqlx::PgPool) { let window_start = chrono::Utc::now(); // Stats should be empty — event happened before the window - let stats = EventRepo::get_window_tool_call_stats(&pool, session_db_id, window_start) + let stats = EventRepo::get_verification_phase_tool_call_stats(&pool, session_db_id, window_start) .await .unwrap(); diff --git a/integrations/tracevault-mcp/src/index.ts b/integrations/tracevault-mcp/src/index.ts index e676f4f6..bde7b7e8 100644 --- a/integrations/tracevault-mcp/src/index.ts +++ b/integrations/tracevault-mcp/src/index.ts @@ -291,7 +291,7 @@ const AGENT_POLICIES_DESCRIPTION = "Fetch agent-readable Markdown instructions describing the active policies " + "for the current repository — which tools must be called before push, which " + "must succeed, which file patterns trigger conditional tool calls, and how " + - "the validation window works. Call this at session start so your behaviour " + + "the verification phase works. Call this at session start so your behaviour " + "matches the policies configured on the TraceVault server. The instructions " + "take precedence over any manual project rules."; diff --git a/web/src/lib/components/RepoPolicies.svelte b/web/src/lib/components/RepoPolicies.svelte index c9990a89..4b7ec3ef 100644 --- a/web/src/lib/components/RepoPolicies.svelte +++ b/web/src/lib/components/RepoPolicies.svelte @@ -296,10 +296,10 @@
{ if (v) newScope = v; }}> - {{ session: 'Session', validation_window: 'Validation', both: 'Session' }[newScope] ?? newScope} + {{ session: 'Session', verification_phase: 'Verification', both: 'Session' }[newScope] ?? newScope} Session - Validation + Verification
@@ -351,8 +351,8 @@ {/if} - {#if policy.scope === 'validation_window'} - Validation + {#if policy.scope === 'verification_phase'} + Verification {:else} Session diff --git a/web/src/routes/orgs/[slug]/repos/[id]/settings/+page.svelte b/web/src/routes/orgs/[slug]/repos/[id]/settings/+page.svelte index 1c9f3355..720edb1d 100644 --- a/web/src/routes/orgs/[slug]/repos/[id]/settings/+page.svelte +++ b/web/src/routes/orgs/[slug]/repos/[id]/settings/+page.svelte @@ -13,7 +13,7 @@ clone_status: string; has_deploy_key: boolean; last_fetched_at: string | null; - validation_window_mode: string; + verification_phase_mode: string; } interface Repo { @@ -36,7 +36,7 @@ let githubUrl = $state(''); let deployKey = $state(''); - let validationWindowMode = $state('disabled'); + let verificationPhaseMode = $state('disabled'); let pollTimer: ReturnType | null = $state(null); onMount(async () => { @@ -61,7 +61,7 @@ try { settings = await api.get(`/api/v1/orgs/${slug}/repos/${repoId}/settings`); githubUrl = settings.github_url ?? ''; - validationWindowMode = settings.validation_window_mode ?? 'disabled'; + verificationPhaseMode = settings.verification_phase_mode ?? 'disabled'; } catch (err) { error = err instanceof Error ? err.message : 'Failed to load settings'; } finally { @@ -77,7 +77,7 @@ const body: Record = {}; if (githubUrl) body.github_url = githubUrl; if (deployKey.trim()) body.deploy_key = deployKey.trim(); - body.validation_window_mode = validationWindowMode; + body.verification_phase_mode = verificationPhaseMode; settings = await api.put(`/api/v1/orgs/${slug}/repos/${repoId}/settings`, body); @@ -269,25 +269,31 @@ - +
-
Validation Window
-
+
Verification phase
+
+

+ Lock the agent into a "review only" mode before each push. When an agent runs + tracevault verify-start, it declares that it is done changing code. + From that point until the push, only tools you whitelist below + (verification_phase-scoped policies) are allowed to run. +

- Controls what happens when an unknown tool (not covered by any - validation_window-scoped policy) is called inside a declared validation window. - Agents open a window with tracevault validation-start. + Catches agents that pretend to self-review while still making code changes — + any tool call inside the phase that is not on the allow-list will either warn + or block the push, depending on the mode you pick.

- - { if (v) validationWindowMode = v; }}> + + { if (v) verificationPhaseMode = v; }}> - {{ disabled: 'Disabled', warn: 'Warn', block: 'Block Push' }[validationWindowMode] ?? validationWindowMode} + {{ disabled: 'Disabled (no enforcement)', warn: 'Warn (record but allow push)', block: 'Block the push' }[verificationPhaseMode] ?? verificationPhaseMode} - Disabled - Warn - Block Push + Disabled (no enforcement) + Warn (record but allow push) + Block the push
From f532d2615e3a57e4eb2cfab7cd2d0e6527520bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kuras?= Date: Thu, 28 May 2026 12:38:09 +0200 Subject: [PATCH 3/5] docs: update CLAUDE.md + README + Cargo.lock for verification phase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the project's own CLAUDE.md instructions (tracevault uses its own validation feature against this repo) and the public README to use 'verification phase' / 'tracevault verify-start' consistently. Cargo.lock churn from the v0.16.0 dep updates that landed via release-plz on main — picks up the same lockfile state as PRs #211 and #212. --- CLAUDE.md | 12 +- Cargo.lock | 471 +++++++++++++++++++++++++++++++++++++++++++++++++++-- README.md | 14 +- 3 files changed, 469 insertions(+), 28 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bb34a0cd..c38a402f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,18 +10,18 @@ These tools must be called at some point during the session before pushing: - mcp__cargo__cargo_fmt (format code — call before committing) - mcp__cargo__cargo_check (clippy + tests — must succeed) -## When work is complete (validation window) +## When work is complete (verification phase) -When ready to push, open a validation window and run the review tool: +When ready to push, open a verification phase and run the review tool: -1. tracevault validation-start -2. mcp__review__agent_review (self-review of the diff — validation window policy) +1. tracevault verify-start +2. mcp__review__agent_review (self-review of the diff — verification phase policy) -Only mcp__review__agent_review and Read should be called between tracevault validation-start and git push. +Only mcp__review__agent_review and Read should be called between tracevault verify-start and git push. If you need to fix something after the review: - Make the fix - Run mcp__cargo__cargo_fmt and mcp__cargo__cargo_check again (session-scoped) -- Run tracevault validation-start again (invalidates previous window) +- Run tracevault verify-start again (invalidates the previous phase) - Run mcp__review__agent_review again - Push diff --git a/Cargo.lock b/Cargo.lock index 5f97a7c3..5ad62a5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,12 +370,24 @@ dependencies = [ "syn", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -439,6 +451,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "built" version = "0.8.0" @@ -772,6 +793,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -843,8 +876,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -861,13 +904,37 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn", ] @@ -924,6 +991,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -939,7 +1016,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn", @@ -1023,6 +1100,26 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -1057,6 +1154,27 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -1189,6 +1307,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1403,6 +1531,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1509,6 +1638,17 @@ dependencies = [ "web-time", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.14" @@ -1521,7 +1661,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1539,6 +1679,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1742,6 +1888,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots 1.0.7", ] [[package]] @@ -1976,6 +2123,17 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.13.0" @@ -2062,6 +2220,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2547,6 +2714,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-derive" version = "0.4.2" @@ -2615,6 +2788,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.17", + "http", + "rand 0.8.6", + "reqwest 0.12.28", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.10.9", + "thiserror 1.0.69", + "url", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2666,6 +2859,37 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openidconnect" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac 0.12.1", + "http", + "itertools 0.10.5", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.6", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2 0.10.9", + "subtle", + "thiserror 1.0.69", + "url", +] + [[package]] name = "openssl" version = "0.10.80" @@ -2721,6 +2945,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ort" version = "2.0.0-rc.9" @@ -2745,6 +2978,30 @@ dependencies = [ "ureq", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "parking" version = "2.2.1" @@ -2935,6 +3192,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2954,6 +3217,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -3164,7 +3436,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.14.0", "libc", "libfuzzer-sys", "log", @@ -3230,7 +3502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" dependencies = [ "either", - "itertools", + "itertools 0.14.0", "rayon", ] @@ -3273,6 +3545,26 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.3" @@ -3327,6 +3619,8 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -3334,6 +3628,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3343,6 +3638,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots 1.0.7", ] [[package]] @@ -3385,6 +3681,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "rgb" version = "0.8.53" @@ -3560,12 +3866,50 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -3605,6 +3949,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -3649,6 +4003,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3661,6 +4024,38 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3852,7 +4247,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -4196,6 +4591,37 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -4234,7 +4660,7 @@ dependencies = [ "derive_builder", "esaxx-rs", "getrandom 0.3.4", - "itertools", + "itertools 0.14.0", "log", "macro_rules_attribute", "monostate", @@ -4363,7 +4789,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 2.13.0", "pin-project-lite", "slab", "sync_wrapper", @@ -4469,8 +4895,22 @@ dependencies = [ name = "tracevault-enterprise" version = "0.1.0" dependencies = [ + "aes-gcm", "async-trait", + "base64 0.22.1", + "chrono", + "ed25519-dalek", + "glob-match", + "hex", + "openidconnect", + "rand 0.8.6", + "reqwest 0.13.2", + "serde", + "serde_json", + "sha2 0.11.0", "tracevault-core", + "tracing", + "uuid", ] [[package]] @@ -4780,6 +5220,7 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -4966,7 +5407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap", + "indexmap 2.13.0", "wasm-encoder", "wasmparser", ] @@ -4992,7 +5433,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.13.0", "semver", ] @@ -5508,7 +5949,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap", + "indexmap 2.13.0", "prettyplease", "syn", "wasm-metadata", @@ -5539,7 +5980,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap", + "indexmap 2.13.0", "log", "serde", "serde_derive", @@ -5558,7 +5999,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap", + "indexmap 2.13.0", "log", "semver", "serde", diff --git a/README.md b/README.md index 45c25572..dd66a72e 100644 --- a/README.md +++ b/README.md @@ -307,7 +307,7 @@ npm install --prefix tools/review-mcp - **`mcp__cargo__cargo_audit`** — runs `cargo audit`. Required when `Cargo.lock` changes. **`tools/review-mcp/`** — required by the validation-scoped self-review policy: -- **`mcp__review__agent_review`** — assembles the diff + full context of touched files and returns a review prompt. Call this inside a validation window before pushing to Rust files. +- **`mcp__review__agent_review`** — assembles the diff + full context of touched files and returns a review prompt. Call this inside a verification phase before pushing to Rust files. **Optional — session history queries:** @@ -341,7 +341,7 @@ configured on the server and take precedence over anything below. Use conventional commits: https://www.conventionalcommits.org/en/v1.0.0/ ``` -`tracevault agent-policies` fetches Markdown instructions rendered by the server from the active policies — which tools must be called before push, which must succeed, what file patterns trigger conditional checks, and how the validation window works. The same output is available via the `agent_policies` MCP tool and as a **Preview** button on each repo page in the dashboard. When policies change, the output updates automatically. +`tracevault agent-policies` fetches Markdown instructions rendered by the server from the active policies — which tools must be called before push, which must succeed, what file patterns trigger conditional checks, and how the verification phase works. The same output is available via the `agent_policies` MCP tool and as a **Preview** button on each repo page in the dashboard. When policies change, the output updates automatically. > **Tool installation:** Rendered instructions reference tools by their literal name (e.g. `mcp__cargo__cargo_fmt`). Make sure those tools are installed in your agent setup. See [Install project-local MCP tools](#4-install-project-local-mcp-tools) for the bundled tools shipped with this repo. @@ -358,7 +358,7 @@ In the Visdom Trace dashboard, under **Repos → [your repo] → Policies**, you Actions: **Block push** (exit non-zero, prevents `git push`) or **Warn** (logs but allows). -Scope: **Session** (evaluate all tool calls in the session) or **Validation** (evaluate only tools called after `tracevault validation-start`). +Scope: **Session** (evaluate all tool calls in the session) or **Verification phase** (evaluate only tools called after `tracevault verify-start`). ## Keys & Secrets @@ -418,7 +418,7 @@ export DATABASE_URL=postgres://user:password@host:5432/tracevault?sslmode=requir | `tracevault stream --event ` | Handle a Claude Code hook event (reads JSON from stdin) and stream it to the server | | `tracevault sync` | Sync repo metadata with the server | | `tracevault check` | Evaluate policies against server rules, exit non-zero if blocked | -| `tracevault validation-start [--session-id ID]` | Open a validation window. Call this when work is complete and you are ready to run pre-push validation tools. Validation-scoped policies only evaluate tools called after this point. Calling it again invalidates the previous window. | +| `tracevault verify-start [--session-id ID]` | Open a verification phase. Call this when work is complete and you are ready to run pre-push validation tools. Verification-phase-scoped policies only evaluate tools called after this point. Calling it again invalidates the previous phase. | | `tracevault stats` | Show local session statistics | | `tracevault verify` | Verify commits are registered and sealed on the server (`--commits` or `--range`) | | `tracevault status` | Show current session status (not yet implemented) | @@ -443,11 +443,11 @@ Each policy has a configurable action: Fail-closed: if the server is unreachable, `tracevault check` blocks the push. -### Validation Scope +### Verification Scope -A validation window is a pre-push gate phase. When work is complete and the agent is ready to push, it opens a validation window with `tracevault validation-start`, then runs all tools required by Validation-scoped policies (formatters, linters, security scanners, code review tools). Only tools explicitly allowed by those policies should be called between the window opening and the push. +A verification phase is a pre-push gate phase. When work is complete and the agent is ready to push, it opens a verification phase with `tracevault verify-start`, then runs all tools required by Verification-phase-scoped policies (formatters, linters, security scanners, code review tools). Only tools explicitly allowed by those policies should be called between entering the phase and the push. -If a validation tool fails and the agent needs to make a fix, it must restart the window (`tracevault validation-start` again) before rerunning the tools — the previous window is invalidated and all expected tools must be called again from scratch. +If a validation tool fails and the agent needs to make a fix, it must restart the phase (`tracevault verify-start` again) before rerunning the tools — the previous phase is invalidated and all expected tools must be called again from scratch. This ensures the audit trail proves that every required quality gate was run against the final state of the code, not just at some earlier point in a long session. From 4ee4f8f26acc320776706b0c563c9bb07e7aadc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kuras?= Date: Thu, 28 May 2026 12:50:06 +0200 Subject: [PATCH 4/5] style: rustfmt repo_policies_test.rs after rename The function rename to `get_verification_phase_tool_call_stats` pushed two call sites past the 100-column limit. `cargo fmt --all` rewraps them onto a continuation line. CI's `cargo fmt -- --check` caught this on PR #213. --- .../tracevault-server/tests/repo_policies_test.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/tracevault-server/tests/repo_policies_test.rs b/crates/tracevault-server/tests/repo_policies_test.rs index c5e2e246..420d0221 100644 --- a/crates/tracevault-server/tests/repo_policies_test.rs +++ b/crates/tracevault-server/tests/repo_policies_test.rs @@ -487,9 +487,10 @@ async fn window_tool_call_stats_after_timestamp(pool: sqlx::PgPool) { .await .unwrap(); - let stats = EventRepo::get_verification_phase_tool_call_stats(&pool, session_db_id, before_window) - .await - .unwrap(); + let stats = + EventRepo::get_verification_phase_tool_call_stats(&pool, session_db_id, before_window) + .await + .unwrap(); assert_eq!(stats.get("cargo_fmt").map(|s| s.total), Some(1)); assert_eq!(stats.get("cargo_fmt").map(|s| s.successful), Some(1)); @@ -540,9 +541,10 @@ async fn window_tool_call_stats_excludes_pre_window_events(pool: sqlx::PgPool) { let window_start = chrono::Utc::now(); // Stats should be empty — event happened before the window - let stats = EventRepo::get_verification_phase_tool_call_stats(&pool, session_db_id, window_start) - .await - .unwrap(); + let stats = + EventRepo::get_verification_phase_tool_call_stats(&pool, session_db_id, window_start) + .await + .unwrap(); assert!(stats.is_empty()); } From 0b940abc58bfe87f7abbf51127ef66b011ae222c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kuras?= Date: Thu, 28 May 2026 13:02:23 +0200 Subject: [PATCH 5/5] fix(agent-policies): per-mode consequence sentence in phase intro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rewritten verification-phase intro unconditionally said 'Any other tool call will fail the push.' That sentence is true in Block mode but wrong in Warn mode — the same section renders in both, so warn-mode agents were getting incorrect guidance. Switch on `verification_phase_mode` to emit: * Block: 'Any other tool call will fail the push.' * Warn: 'Any other tool call will be recorded as a warning on the push.' * Disabled: unreachable (gated upstream by has_verification). Two tests lock the per-mode wording in place so this can't drift again. Addresses Aikido review comment on PR #213. --- crates/tracevault-core/src/agent_policies.rs | 51 +++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/crates/tracevault-core/src/agent_policies.rs b/crates/tracevault-core/src/agent_policies.rs index c91f4539..6d0bb9f1 100644 --- a/crates/tracevault-core/src/agent_policies.rs +++ b/crates/tracevault-core/src/agent_policies.rs @@ -151,18 +151,31 @@ pub fn render_markdown( } if has_verification { + // The consequence sentence depends on the enforcement mode. We render + // this whole section only when mode is Warn or Block (Disabled cases + // never get here because has_verification short-circuits above), so + // it is safe to switch on those two arms and `unreachable!` Disabled. + let consequence = match verification_phase_mode { + VerificationPhaseMode::Block => "Any other tool call will fail the push.", + VerificationPhaseMode::Warn => { + "Any other tool call will be recorded as a warning on the push." + } + VerificationPhaseMode::Disabled => unreachable!( + "verification phase section is only rendered when mode is Warn or Block" + ), + }; out.push_str("\n### Verification phase (pre-push gating)\n"); - out.push_str( + out.push_str(&format!( "When you are done changing code and ready to push, you must enter a \ **verification phase** by running:\n\n tracevault verify-start\n\n\ From that point until the push, you are only allowed to call the tools \ - listed below. Any other tool call will fail the push.\n\n\ + listed below. {consequence}\n\n\ The intent: catch agents that pretend to review while still editing. \ If you need to make a code change after entering the phase, that is fine — \ make the change, then run `tracevault verify-start` again to restart the \ phase and rerun the required tools. The most recent `verify-start` is the \ only one that counts.\n", - ); + )); if !verification_required.is_empty() { out.push_str("\n**Required** — must be called inside the phase:\n"); @@ -385,7 +398,7 @@ mod tests { } #[test] - fn warn_mode_does_not_advertise_blocking_in_window() { + fn warn_mode_does_not_advertise_blocking_in_phase() { let p = rule( required(&["agent_review"], false), PolicyAction::Warn, @@ -394,8 +407,34 @@ mod tests { ); let out = render_markdown(&[p], &VerificationPhaseMode::Warn); assert!(out.contains("### Verification phase")); - assert!(!out.contains("will block the push")); - assert!(!out.contains("blocked")); + // Critical: in Warn mode the consequence sentence must NOT promise to + // fail the push — that wording belongs to Block mode only. + assert!( + !out.contains("will fail the push"), + "Warn mode must not advertise blocking behavior; output: {out}" + ); + assert!(out.contains("recorded as a warning")); assert!(out.contains("should be called")); } + + #[test] + fn block_mode_advertises_failing_the_push() { + let p = rule( + required(&["agent_review"], false), + PolicyAction::BlockPush, + PolicyScope::VerificationPhase, + true, + ); + let out = render_markdown(&[p], &VerificationPhaseMode::Block); + assert!(out.contains("### Verification phase")); + // Critical: in Block mode the consequence sentence must say so. + assert!( + out.contains("will fail the push"), + "Block mode must explicitly state the push will fail; output: {out}" + ); + assert!( + !out.contains("recorded as a warning"), + "Block mode must not use Warn-mode phrasing; output: {out}" + ); + } }