pushkin 0.2.1

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
//! `pushkin waive <rule> --path <glob> --ttl <duration> --reason "…"`:
//! the human plane (integration doc §7). Appends a signed record (author
//! from git identity, timestamp, expiry) to `pushkin/waivers.toml`, logs
//! the grant in the event stream, and prints the stored record. Scoped,
//! temporary by default — an audit trail, not a mute button. The file it
//! writes is gate surface: agents that run this verb get the record, but
//! their write path to `pushkin/` stays denied (pipeline built-in).

use anyhow::{Context, Result};
use pushkin_core::events::{EventLog, Telemetry};
use pushkin_core::waivers::{parse_ttl, GrantRequest, Waiver, WaiverSet};
use std::path::Path;

use super::events_db_path;

pub const WAIVERS_FILE: &str = "pushkin/waivers.toml";

/// Event-stream rule tag for a waiver grant (spec §14: waivers are
/// "logged in the event stream").
pub const WAIVER_GRANTED_RULE: &str = "pushkin.waiver.granted";

pub struct WaiveArgs {
    pub rule: String,
    pub path: String,
    pub ttl: String,
    pub reason: String,
    pub supersedes: Option<String>,
    pub contradicts: Option<String>,
}

pub fn run(args: WaiveArgs) -> Result<i32> {
    let ttl = parse_ttl(&args.ttl)?;
    let waiver = Waiver::grant(GrantRequest {
        rule: args.rule,
        path: args.path,
        reason: args.reason,
        author: git_identity(),
        ttl,
        supersedes: args.supersedes,
        contradicts: args.contradicts,
    })?;
    let stored = WaiverSet::append(Path::new(WAIVERS_FILE), waiver)
        .context("cannot append to pushkin/waivers.toml")?;

    let log = EventLog::open(events_db_path()?)?;
    let session = log.begin_session()?;
    log.append_telemetry(
        &session,
        Telemetry {
            rule: WAIVER_GRANTED_RULE,
            payload: serde_json::to_string(&stored)?,
        },
    )?;

    println!(
        "pushkin: waiver {} granted — {} on {} until {} (by {}). \
         Recorded in {WAIVERS_FILE} and the event log.",
        stored.id, stored.rule, stored.path, stored.expires_at, stored.author
    );
    Ok(0)
}

/// Author from git identity (integration doc §7): env override first
/// (also the test seam), then `git config user.name` via the git facade.
fn git_identity() -> String {
    if let Ok(name) = std::env::var("GIT_AUTHOR_NAME") {
        if !name.trim().is_empty() {
            return name;
        }
    }
    super::git::config_user_name().unwrap_or_else(|| "unknown".to_owned())
}