use std::fs;
use std::path::Path;
use std::process::Command;
use netsky_core::consts::{NETSKY_BIN, TICKER_LOG_PATH};
use crate::observability;
use netsky_core::paths::{
agent0_hang_marker, agentinit_escalation_marker, loop_resume_file, state_dir,
};
const LEGACY_LOOP_RESUME: &str = "/tmp/netsky-loop-resume.txt";
const BRIEF_PATH: &str = "briefs/overnight-resilience-adversarial-review.md";
const MORNING_MARKER_PREFIX: &str = "morning-brief-";
const MORNING_MARKER_RETENTION_DAYS: i64 = 14;
pub fn run(send: bool) -> netsky_core::Result<()> {
reap_morning_markers()?;
section("health");
let out = Command::new(NETSKY_BIN)
.args(["doctor", "--brief"])
.output()?;
print!("{}", String::from_utf8_lossy(&out.stdout));
if !out.status.success() {
println!();
println!(
"doctor exited {} — full output:",
out.status.code().unwrap_or(-1)
);
let full = Command::new(NETSKY_BIN).arg("doctor").output()?;
let text = String::from_utf8_lossy(&full.stdout);
for l in text
.lines()
.rev()
.take(30)
.collect::<Vec<_>>()
.into_iter()
.rev()
{
println!("{l}");
}
}
let hang = agent0_hang_marker();
let esc = agentinit_escalation_marker();
let markers: Vec<_> = [&hang, &esc].into_iter().filter(|p| p.exists()).collect();
if !markers.is_empty() {
section("ACTIVE MARKERS (owner action needed)");
for m in markers {
println!("{}", m.display());
if let Ok(body) = fs::read_to_string(m) {
for l in body.lines() {
println!(" {l}");
}
}
}
}
section("overnight commits (last 12h)");
let log = Command::new("git")
.args(["log", "--since=12 hours ago", "--pretty=format:%h %s"])
.output()?;
for l in String::from_utf8_lossy(&log.stdout).lines().take(30) {
println!("{l}");
}
println!();
if Path::new(BRIEF_PATH).exists() {
section("brief: latest iterations");
if let Ok(content) = fs::read_to_string(BRIEF_PATH) {
let it_lines: Vec<_> = content
.lines()
.filter(|l| l.starts_with("### iteration"))
.collect();
for l in it_lines
.iter()
.rev()
.take(8)
.collect::<Vec<_>>()
.into_iter()
.rev()
{
println!("{l}");
}
}
}
for resume in [
loop_resume_file(),
Path::new(LEGACY_LOOP_RESUME).to_path_buf(),
] {
if resume.exists() {
let name = resume
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("loop-resume");
section(&format!("loop-resume PENDING ({name})"));
if let Ok(body) = fs::read_to_string(&resume) {
let mut flag = false;
let mut printed = 0;
for l in body.lines() {
if l.starts_with("PENDING") {
flag = true;
continue;
}
if l.starts_with("Stop conditions:") {
flag = false;
}
if flag && printed < 20 {
println!("{l}");
printed += 1;
}
}
}
break;
}
}
section("watchdog (last 5 lines)");
match fs::read_to_string(TICKER_LOG_PATH) {
Ok(body) => {
let lines: Vec<_> = body.lines().collect();
let start = lines.len().saturating_sub(5);
for l in &lines[start..] {
println!("{l}");
}
}
Err(_) => println!("(log not present)"),
}
if send {
observability::record_directive(
"morning",
None,
"netsky morning --send",
Some("morning_brief"),
Some(netsky_core::consts::AGENT0_NAME),
Some("requested"),
serde_json::json!({}),
);
println!();
println!("--send requested; agent0 should pipe this into a reply tool call.");
}
Ok(())
}
fn reap_morning_markers() -> netsky_core::Result<()> {
let today = chrono::Utc::now().date_naive();
let cutoff = today - chrono::Duration::days(MORNING_MARKER_RETENTION_DAYS);
let reaped = reap_morning_markers_in(&state_dir(), cutoff)?;
if reaped > 0 {
section("morning marker reaper");
println!("reaped {reaped} stale marker(s)");
}
Ok(())
}
fn reap_morning_markers_in(dir: &Path, cutoff: chrono::NaiveDate) -> std::io::Result<usize> {
let Ok(entries) = fs::read_dir(dir) else {
return Ok(0);
};
let mut reaped = 0;
for entry in entries.filter_map(|e| e.ok()) {
let path = entry.path();
let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
continue;
};
if !is_stale_morning_marker(name, cutoff) {
continue;
}
fs::remove_file(path)?;
reaped += 1;
}
Ok(reaped)
}
fn is_stale_morning_marker(name: &str, cutoff: chrono::NaiveDate) -> bool {
let Some(suffix) = name.strip_prefix(MORNING_MARKER_PREFIX) else {
return false;
};
chrono::NaiveDate::parse_from_str(suffix, "%Y-%m-%d").is_ok_and(|date| date < cutoff)
}
fn section(label: &str) {
println!();
println!("── {label} ──");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn morning_marker_reaper_removes_only_old_dated_markers() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("morning-brief-2026-03-31"), "").unwrap();
fs::write(dir.path().join("morning-brief-2026-04-15"), "").unwrap();
fs::write(dir.path().join("morning-brief-not-a-date"), "").unwrap();
fs::write(dir.path().join("other-2026-03-31"), "").unwrap();
let cutoff = chrono::NaiveDate::from_ymd_opt(2026, 4, 2).unwrap();
let reaped = reap_morning_markers_in(dir.path(), cutoff).unwrap();
assert_eq!(reaped, 1);
assert!(!dir.path().join("morning-brief-2026-03-31").exists());
assert!(dir.path().join("morning-brief-2026-04-15").exists());
assert!(dir.path().join("morning-brief-not-a-date").exists());
assert!(dir.path().join("other-2026-03-31").exists());
}
}