use crate::mailbox;
use crate::paths::Paths;
use crate::{events, sensor, session};
use anyhow::Result;
use std::fs;
use std::process::ExitCode;
pub fn sense(paths: &Paths) -> String {
let _ = crate::seed::ensure_dirs(paths);
events::emit(paths, "tick_start", serde_json::json!({}));
session::prune_aged(
paths,
std::time::Duration::from_secs(crate::run::session_ttl_secs(paths)),
);
crate::gate::reap_stale_claims(paths);
crate::executor::warn_if_interrupted(paths);
let snap = paths.snapshots_dir();
let _ = fs::remove_dir_all(&snap);
let _ = fs::create_dir_all(&snap);
sensor::run_all(paths, &snap, true);
events::emit(paths, "sense_done", serde_json::json!({}));
crate::worldhash::world_hash(paths)
}
fn snapshots(paths: &Paths) -> serde_json::Map<String, serde_json::Value> {
let mut out = serde_json::Map::new();
for e in fs::read_dir(paths.snapshots_dir())
.into_iter()
.flatten()
.flatten()
{
let p = e.path();
if p.extension().map(|x| x == "json").unwrap_or(false)
&& let Some(stem) = p.file_stem().map(|s| s.to_string_lossy().to_string())
&& let Ok(raw) = fs::read_to_string(&p)
&& let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw)
{
out.insert(stem, v);
}
}
out
}
fn goal_ids(paths: &Paths) -> Vec<String> {
let mut v: Vec<String> = fs::read_dir(paths.goals_dir())
.into_iter()
.flatten()
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().map(|x| x == "md").unwrap_or(false))
.filter_map(|p| p.file_stem().map(|s| s.to_string_lossy().to_string()))
.collect();
v.sort();
v
}
fn journal_tail(paths: &Paths, n: usize) -> Vec<String> {
let text = fs::read_to_string(paths.journal()).unwrap_or_default();
let lines: Vec<&str> = text.lines().collect();
let start = lines.len().saturating_sub(n);
lines[start..].iter().map(|s| s.to_string()).collect()
}
pub fn state(paths: &Paths) -> serde_json::Value {
let hash = crate::worldhash::world_hash(paths);
let asks: Vec<serde_json::Value> = mailbox::pending(paths)
.into_iter()
.map(|a| serde_json::to_value(a).unwrap_or_default())
.collect();
let workers: Vec<serde_json::Value> = session::list_workers(paths)
.into_iter()
.map(|s| {
serde_json::json!({
"id": s.id,
"state": s.state,
"alive": s.alive,
"exit_code": s.exit_code,
})
})
.collect();
serde_json::json!({
"world_hash": hash,
"snapshots": snapshots(paths),
"asks": asks,
"workers": workers,
"goals": goal_ids(paths),
"journal_tail": journal_tail(paths, 20),
"data_dir": paths.data_dir.to_string_lossy(),
})
}
fn wait_for_change(paths: &Paths) {
let poll = std::time::Duration::from_millis(
std::env::var("LOOOP_WAIT_POLL_MS")
.ok()
.and_then(|v| v.trim().parse().ok())
.unwrap_or(1000),
);
let baseline = crate::worldhash::world_hash(paths);
loop {
if !mailbox::pending(paths).is_empty() {
return; }
if crate::worldhash::world_hash(paths) != baseline {
return; }
std::thread::sleep(poll);
}
}
fn print_state(paths: &Paths, args: &[String]) -> Result<ExitCode> {
let s = state(paths);
if args.iter().any(|a| a == "--json") {
println!("{}", serde_json::to_string_pretty(&s)?);
return Ok(ExitCode::SUCCESS);
}
let asks = s["asks"].as_array().map(|a| a.len()).unwrap_or(0);
let workers = s["workers"].as_array().map(|a| a.len()).unwrap_or(0);
let goals = s["goals"].as_array().map(|a| a.len()).unwrap_or(0);
println!("asks: {asks} · workers: {workers} · goals: {goals}");
for a in s["asks"].as_array().cloned().unwrap_or_default() {
println!(
" ⚑ {} ({}): {}",
a["id"].as_str().unwrap_or("?"),
a["worker"].as_str().unwrap_or("?"),
a["prompt"].as_str().unwrap_or("")
);
}
Ok(ExitCode::SUCCESS)
}
pub fn cmd_state(paths: &Paths, args: &[String]) -> Result<ExitCode> {
let _ = crate::seed::ensure_dirs(paths);
print_state(paths, args)
}
pub fn cmd_wait(paths: &Paths, args: &[String]) -> Result<ExitCode> {
let _ = crate::seed::ensure_dirs(paths);
wait_for_change(paths);
print_state(paths, args)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn state_reports_goals_and_pending_asks() {
let p = Paths::temp();
let _ = crate::seed::ensure_dirs(&p);
fs::write(p.goals_dir().join("triage.md"), b"triage\n").unwrap();
fs::write(
p.asks_dir().join("triage-1.json"),
serde_json::json!({"id":"triage-1","worker":"triage","prompt":"merge?","ts":1})
.to_string(),
)
.unwrap();
let s = state(&p);
let goals: Vec<String> = s["goals"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap().to_string())
.collect();
assert!(goals.contains(&"triage".to_string()));
assert_eq!(s["asks"].as_array().unwrap().len(), 1);
assert_eq!(s["asks"][0]["id"], "triage-1");
}
}