1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! `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 chrono::Utc;
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};
use netsky_db::CloneDispatchRecord;
use crate::observability;
/// 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, true).map(|_| ())
}
/// 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, true).map(|_| ())
}
#[derive(Clone, Copy)]
pub(crate) enum RuntimeKind {
Claude,
Codex,
}
pub(crate) fn spawn_resident(
n: u32,
fresh: bool,
kind: RuntimeKind,
announce: bool,
) -> netsky_core::Result<SpawnOutcome> {
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(),
claude_settings_template: clone_settings_template(agent, kind),
};
if fresh && spawn::is_up(agent) {
spawn::kill(agent)?;
if announce {
println!("[agent] killed existing session '{agent}' for fresh spawn");
}
}
match spawn::spawn(agent, &opts)? {
SpawnOutcome::AlreadyUp => {
if announce {
println!("[agent] session '{agent}' already up; skipping spawn");
}
Ok(SpawnOutcome::AlreadyUp)
}
SpawnOutcome::Spawned => {
let tag = if fresh { "fresh" } else { "spawned" };
if announce {
println!("[agent] {tag} '{agent}' ({})", opts.runtime.describe());
}
// Record clone-dispatch start. Lifetime end is populated
// either by the clone's own /down skill or by a /gc sweep.
// Clone lifetime analytics derive from clone_dispatches;
// see the clone_lifetimes view in netsky-db.
let runtime_label = match kind {
RuntimeKind::Claude => "claude",
RuntimeKind::Codex => "codex",
};
let agent_name = agent.name();
observability::record_clone_dispatch(CloneDispatchRecord {
ts_utc_start: Utc::now(),
ts_utc_end: None,
agent_id: &agent_name,
runtime: Some(runtime_label),
brief_path: None,
brief: None,
workspace: None,
branch: None,
status: Some("spawned"),
exit_code: None,
detail_json: None,
});
// 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),
)
&& announce
{
println!("[agent] {agent}: TOS dismissed");
}
Ok(SpawnOutcome::Spawned)
}
}
}
fn clone_settings_template(agent: AgentId, kind: RuntimeKind) -> Option<String> {
if matches!(agent, AgentId::Clone(_)) && matches!(kind, RuntimeKind::Claude) {
Some(crate::clone_tools::CLAUDE_CLONE_SETTINGS_TEMPLATE.to_string())
} else {
None
}
}