netsky 0.2.0

netsky CLI: the viable system launcher and subcommand dispatcher
Documentation
use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct Gate {
    pub name: &'static str,
    pub signal: &'static str,
    pub condition: &'static str,
    pub action: &'static str,
    pub memory: &'static str,
    pub reversibility: &'static str,
}

pub const GATES: &[Gate] = &[
    Gate {
        name: "prompt_drift",
        signal: "commit touches bundled prompts or prompt source tree",
        condition: "source bytes != bundled asset bytes",
        action: "bin/check runs bin/check-prompt-drift and fails",
        memory: "test stdout plus src/crates/netsky-prompts/tests/prompt_drift.rs:49",
        reversibility: "trivial: sync source, rebuild, rerun gate",
    },
    Gate {
        name: "mutation_ledger",
        signal: "MCP mutating tool call",
        condition: "args_hash seen recently or call times out after dispatch",
        action: "dedupe exact retry; surface timeout_race with request_id",
        memory: "mutation JSONL with request_id and args_hash at src/crates/netsky-io/src/mcp.rs:51",
        reversibility: "compensating: read-before-retry, then decide next mutation",
    },
    Gate {
        name: "email_draft_sentinel_consume",
        signal: "send_draft with NETSKY_EMAIL_AUTO_SEND=1",
        condition: "fresh approval sentinel exists for composite_draft_id",
        action: "rename sentinel to .used before POST",
        memory: ".used sentinel and success delete at src/crates/netsky-io/src/sources/email/send.rs:643",
        reversibility: "repairable: restore fresh sentinel only on pre-send failure",
    },
    Gate {
        name: "drive_empty_trash_surface",
        signal: "request to empty Google Drive trash",
        condition: "call arrives through MCP tool surface",
        action: "reject unknown tool; allow CLI-only path",
        memory: "test proves absence at src/crates/netsky-io/src/sources/drive/mod.rs:491",
        reversibility: "irreversible if executed; gate keeps execution off MCP",
    },
    Gate {
        name: "graceful_down_preflight",
        signal: "netsky down without --force",
        condition: "target tmux sessions exist",
        action: "send shutdown envelopes, wait for ack or close, then force fallback",
        memory: "shutdown request envelope at src/crates/netsky-cli/src/cmd/down.rs:221",
        reversibility: "repairable: rerun agent startup for any missing session",
    },
    Gate {
        name: "watchdog_clone_idle_discriminator",
        signal: "clone pane hash is stable past hang threshold",
        condition: "inbox history == Drained and pane_stable_since >= drain_since",
        action: "suppress hang page only with proof of drained work",
        memory: "state file agentN-inbox.state at src/crates/netsky-cli/src/cmd/watchdog.rs:1406",
        reversibility: "trivial: delete malformed state or let next tick rewrite it",
    },
    Gate {
        name: "escalate_retry_failed_marker",
        signal: "netsky escalate sends an owner page",
        condition: "first osascript attempt times out or exits nonzero",
        action: "retry once; if second fails, write failed marker and return error",
        memory: "marker plus event at src/crates/netsky-cli/src/cmd/escalate.rs:105",
        reversibility: "repairable: owner or doctor reads marker, fixes path, retries page",
    },
    Gate {
        name: "channel_watch_delivery",
        signal: "netsky channel watch agent --tmux session",
        condition: "pending envelope validates and tmux paste succeeds",
        action: "claim, wrap, paste, submit, ack, archive",
        memory: "received ack JSONL at src/crates/netsky-cli/src/cmd/channel.rs:306",
        reversibility: "repairable: failed submit leaves claimed file for retry",
    },
    Gate {
        name: "envelope_wrapper_token_validator",
        signal: "inbound or outbound agent-bus envelope",
        condition: "body contains </channel> or <channel source=",
        action: "refuse send or quarantine read before model framing",
        memory: "poison/quarantine record at src/crates/netsky-cli/src/cmd/channel.rs:230",
        reversibility: "trivial: remove wrapper token from body, resend",
    },
    Gate {
        name: "atomic_envelope_write",
        signal: "producer writes envelope into inbox",
        condition: "final filename may collide or writer may die mid-write",
        action: "write tempfile, hardlink create-new to final, unlink tempfile",
        memory: "final JSON file or leftover tmp at src/crates/netsky-core/src/envelope.rs:94",
        reversibility: "repairable: retry with fresh filename, inspect leftover tmp",
    },
];

pub fn run(json: bool, name: Option<&str>) -> netsky_core::Result<()> {
    match (json, name) {
        (true, Some(name)) => {
            let gate = find_gate(name)?;
            println!("{}", serde_json::to_string_pretty(gate)?);
        }
        (true, None) => println!("{}", serde_json::to_string_pretty(GATES)?),
        (false, Some(name)) => print_gate(find_gate(name)?),
        (false, None) => {
            for gate in GATES {
                println!("{}: {}", gate.name, gate.signal);
            }
        }
    }
    Ok(())
}

fn find_gate(name: &str) -> netsky_core::Result<&'static Gate> {
    GATES.iter().find(|gate| gate.name == name).ok_or_else(|| {
        let known = GATES.iter().map(|g| g.name).collect::<Vec<_>>().join(", ");
        netsky_core::Error::Invalid(format!(
            "unknown gate '{name}' — known gates: {known} (run `netsky gates` to list)"
        ))
    })
}

fn print_gate(gate: &Gate) {
    println!("{}", gate.name);
    println!("  signal: {}", gate.signal);
    println!("  condition: {}", gate.condition);
    println!("  action: {}", gate.action);
    println!("  memory: {}", gate.memory);
    println!("  reversibility: {}", gate.reversibility);
}

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

    #[test]
    fn every_gate_memory_citation_exists() {
        let root = repo_root();
        for gate in GATES {
            let (path, line) = memory_citation(gate.memory)
                .unwrap_or_else(|| panic!("{} memory lacks path:line citation", gate.name));
            let full_path = root.join(path);
            let content = std::fs::read_to_string(&full_path)
                .unwrap_or_else(|err| panic!("{} citation missing {path}: {err}", gate.name));
            assert!(
                content.lines().nth(line - 1).is_some(),
                "{} citation {path}:{line} is beyond EOF",
                gate.name
            );
        }
    }

    fn memory_citation(memory: &str) -> Option<(&str, usize)> {
        memory.split_whitespace().find_map(|token| {
            let (path, line) = token.rsplit_once(':')?;
            if !path.ends_with(".rs") || !Path::new(path).is_relative() {
                return None;
            }
            Some((path, line.parse().ok()?))
        })
    }

    fn repo_root() -> &'static Path {
        Path::new(env!("CARGO_MANIFEST_DIR"))
            .ancestors()
            .nth(3)
            .expect("repo root")
    }
}