netsky 0.1.4

netsky CLI: the viable system launcher and subcommand dispatcher
Documentation
//! `netsky agent <N> [--type claude|codex] [--fresh]` — spawn a single agent.
//!
//! N=0 spawns agent0, N>0 spawns a clone. Idempotent by default: a
//! live session is left alone. With `--fresh` the session is killed
//! first, forcing a clean context. Used by agent0 to reset a clone
//! whose context is polluted with unrelated prior work.
//!
//! Runtime selection:
//! - `--type claude` (default): Claude Code in a tmux-resident session
//!   (the original shape).
//! - `--type codex`: codex REPL in a tmux-resident session
//!   (resident-codex parity v0). Inbox delivery uses
//!   `netsky channel drain <agent>` from inside the REPL; outbound
//!   replies use `netsky channel send <target> <text>`.

use std::time::Duration;

use netsky_core::agent::AgentId;
use netsky_core::consts::RESTART_AGENT0_TOS_WAIT_S;
use netsky_core::runtime::CodexConfig;
use netsky_core::runtime::Runtime;
use netsky_core::spawn::{self, SpawnOptions, SpawnOutcome};

/// Spawn a resident agent (claude default, codex on explicit opt-in).
pub fn run(n: u32, fresh: bool) -> netsky_core::Result<()> {
    spawn_resident(n, fresh, RuntimeKind::Claude)
}

/// Spawn a tmux-resident codex agent. Called by cli::dispatch when
/// `--type codex` is passed without `--prompt` or `--drain`.
pub fn run_codex_resident(n: u32, fresh: bool) -> netsky_core::Result<()> {
    spawn_resident(n, fresh, RuntimeKind::Codex)
}

#[derive(Clone, Copy)]
enum RuntimeKind {
    Claude,
    Codex,
}

fn spawn_resident(n: u32, fresh: bool, kind: RuntimeKind) -> netsky_core::Result<()> {
    let agent = AgentId::from_number(n);
    // Pin spawn cwd to the resolved netsky root, NOT whatever shell
    // cwd agent0 happened to be in. Closes briefs/clone-cwd-pin.md
    // (agent5): a clone spawned from `workspaces/foo/repo` previously
    // inherited that cwd silently and broke /up.
    let cwd = netsky_core::paths::netsky_root_or_cwd()?;
    let runtime = match kind {
        RuntimeKind::Claude => Runtime::defaults_for(agent),
        RuntimeKind::Codex => Runtime::Codex(CodexConfig::defaults_for()),
    };
    let opts = SpawnOptions {
        runtime,
        cwd: cwd.clone(),
    };

    if fresh && spawn::is_up(agent) {
        spawn::kill(agent)?;
        println!("[agent] killed existing session '{agent}' for fresh spawn");
    }

    match spawn::spawn(agent, &opts)? {
        SpawnOutcome::AlreadyUp => {
            println!("[agent] session '{agent}' already up; skipping spawn");
        }
        SpawnOutcome::Spawned => {
            let tag = if fresh { "fresh" } else { "spawned" };
            println!("[agent] {tag} '{agent}' ({})", opts.runtime.describe());
            // Auto-dismiss the dev-channels TOS dialog for claude. Codex
            // has no equivalent one-shot consent dialog on spawn, so
            // skip the probe there (it would just burn up to the
            // RESTART_AGENT0_TOS_WAIT_S wait waiting for a string that
            // never appears).
            if matches!(kind, RuntimeKind::Claude)
                && spawn::dismiss_tos(
                    &agent.name(),
                    Duration::from_secs(RESTART_AGENT0_TOS_WAIT_S),
                )
            {
                println!("[agent] {agent}: TOS dismissed");
            }
        }
    }
    Ok(())
}