pushkin 0.1.0

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
//! `pushkin board <verb> --run <id> --agent <id>` (spec ยง9): the CLI
//! twins of the peer-board tools โ€” subagents do not inherit MCP
//! connections, so the CLI is the subagent path. Board lives in
//! `.pushkin/board.db`, scoped by run ID.

use anyhow::{Context, Result};
use pushkin_core::board::Board;
use std::path::PathBuf;

pub const BOARD_DB: &str = ".pushkin/board.db";

pub enum BoardVerb {
    Register,
    Status { message: String },
    Peers,
    Broadcast { message: String },
    Send { to: String, message: String },
    Read,
    Claim { path: String },
    Release { path: String },
    Claims,
}

pub struct BoardArgs {
    pub verb: BoardVerb,
    pub run: String,
    pub agent: String,
}

pub fn run(args: BoardArgs) -> Result<i32> {
    let board = open_board(&args.run)?;
    match args.verb {
        BoardVerb::Register => {
            board.register(&args.agent)?;
            println!("pushkin board: {} registered in {}", args.agent, args.run);
        }
        BoardVerb::Status { message } => {
            board.set_status(&args.agent, &message)?;
            println!("pushkin board: status recorded");
        }
        BoardVerb::Peers => {
            for peer in board.peers(&args.agent)? {
                match peer.status {
                    Some(status) => println!("{} โ€” {status}", peer.agent),
                    None => println!("{}", peer.agent),
                }
            }
        }
        BoardVerb::Broadcast { message } => {
            board.broadcast(&args.agent, &message)?;
            println!("pushkin board: broadcast queued");
        }
        BoardVerb::Send { to, message } => {
            board.send(&args.agent, &to, &message)?;
            println!("pushkin board: message queued for {to}");
        }
        BoardVerb::Read => {
            for message in board.read(&args.agent)? {
                println!("[{}] {}", message.from_agent, message.body);
            }
        }
        BoardVerb::Claim { path } => {
            board
                .claim(&args.agent, &path)
                .with_context(|| format!("cannot claim {path}"))?;
            println!("pushkin board: {} claimed {path}", args.agent);
        }
        BoardVerb::Release { path } => {
            if board.release(&args.agent, &path)? {
                println!("pushkin board: {} released {path}", args.agent);
            } else {
                println!(
                    "pushkin board: no claim on {path} held by {} โ€” nothing released",
                    args.agent
                );
            }
        }
        BoardVerb::Claims => {
            for claim in board.claims()? {
                println!("{} held by {}", claim.path_glob, claim.agent);
            }
        }
    }
    Ok(0)
}

/// Opens the run-scoped board, creating `.pushkin/` as needed.
pub fn open_board(run: &str) -> Result<Board> {
    let path = PathBuf::from(BOARD_DB);
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).context("cannot create .pushkin/")?;
    }
    Ok(Board::open(&path, run)?)
}