pushkin 0.1.0

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`.
fn git_identity() -> String {
    if let Ok(name) = std::env::var("GIT_AUTHOR_NAME") {
        if !name.trim().is_empty() {
            return name;
        }
    }
    std::process::Command::new("git")
        .args(["config", "user.name"])
        .output()
        .ok()
        .filter(|output| output.status.success())
        .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_owned())
        .filter(|name| !name.is_empty())
        .unwrap_or_else(|| "unknown".to_owned())
}