netsky 0.1.5

netsky CLI: the viable system launcher and subcommand dispatcher
//! `netsky quiet <seconds> [--reason <str>]` — arm a quiet-sentinel
//! that suppresses the agentinfinity hang detector for `seconds` from
//! now.
//!
//! Write-side primitive for the collision fix documented in
//! `briefs/hang-detector-codify/`. /loop (Anthropic-built-in) legitimately
//! sleeps up to 3600s via ScheduleWakeup, and `/loop stop` is a valid
//! terminal state; both look like a hang to the watchdog's 1800s
//! pane-stable detector. Agent0 calls this subcommand before a long nap
//! or an intentional stop to announce the quiet window.
//!
//! Contract:
//! - Filename embeds the expiry epoch: `agent0-quiet-until-<unix>`.
//! - Body is one human-readable line for debugging; never parsed.
//! - Multiple sentinels may co-exist; the watchdog picks the max epoch.
//! - Past-epoch sentinels are reaped by the watchdog on next read.

use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};

use netsky_core::paths::{agent0_quiet_sentinel_for, ensure_state_dir};

/// Minimum arm window. Zero or negative is pointless (would write a
/// stale sentinel that the next watchdog tick reaps). One second is
/// legal but useless; the test suite still covers it for symmetry.
const MIN_SECONDS: u64 = 1;

pub fn run(seconds: u64, reason: Option<&str>) -> netsky_core::Result<()> {
    if seconds < MIN_SECONDS {
        netsky_core::bail!("seconds must be >= {MIN_SECONDS}");
    }
    ensure_state_dir()?;

    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    let expiry = now.saturating_add(seconds);

    let path = agent0_quiet_sentinel_for(expiry);
    let iso = chrono::DateTime::<chrono::Utc>::from_timestamp(expiry as i64, 0)
        .map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
        .unwrap_or_else(|| format!("epoch={expiry}"));
    let reason_line = reason.unwrap_or("(no reason)");
    let body = format!("until {iso} ({seconds}s from arm); reason: {reason_line}\n");

    fs::write(&path, body)?;
    println!("quiet sentinel armed: {}", path.display());
    println!("expires at {iso} ({seconds}s from now)");
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rejects_zero_seconds() {
        let r = run(0, None);
        assert!(r.is_err(), "0s arm should be rejected");
    }
}