//! `spectra brief`: what the owner tells the agents (spec §12.1), an //! overall brief and one per project, each a signed `brief.set` event. use super::identity::owner_home; use super::{call, ui, Ctx}; use anyhow::{bail, Context, Result}; use spectra_core::keys::KeyPair; use spectra_core::ledger::Ledger; use spectra_core::principals::signed_event; use std::path::PathBuf; fn label(project: Option<&str>) -> String { project.map(|p| format!("{p}'s brief")).unwrap_or_else(|| "the overall brief".into()) } /// A layer's current text: from the daemon when one serves, else the ledger. async fn current(ctx: &Ctx, project: Option<&str>) -> Result> { let home = owner_home(ctx, None); if let Some(c) = ctx.owner_client(&home).await? { let v = call(&c, "GET", "/api/v1/briefs", None).await?; return Ok(match project { None => v["overall"].as_str().map(String::from), Some(p) => v["projects"] .as_array() .and_then(|a| a.iter().find(|x| x["project"] == p)) .and_then(|x| x["text"].as_str()) .map(String::from), }); } let ledger = Ledger::open_read_only(&ctx.db()?).await?; spectra_core::rules::layer(ledger.pool(), project).await } pub async fn show(ctx: &Ctx, project: Option, as_json: bool) -> Result { let text = current(ctx, project.as_deref()).await?; if as_json { println!("{}", serde_json::json!({ "project": project, "text": text })); return Ok(0); } println!("{}", ui::section(label(project.as_deref()))); match text { Some(t) => println!("{}", t.trim_end()), None => println!("{} {}", ui::note(), ui::dim("not set: spectra brief set [--project P] \"…\", or spectra brief edit")), } Ok(0) } pub async fn set(ctx: &Ctx, project: Option, text: String, owner_home_flag: Option) -> Result { spectra_core::rules::check(&text)?; let home = owner_home(ctx, owner_home_flag); let w = ctx.owner_writer(&home).await?; if let Some(p) = &project { let known = match (w.client(), w.ledger()) { (Some(c), _) => { call(c, "GET", "/api/v1/projects", None).await?.as_array().is_some_and(|l| l.iter().any(|x| x["name"] == p.as_str())) } (None, Some(l)) => sqlx::query_scalar::<_, String>("SELECT name FROM projects WHERE name = ?") .bind(p) .fetch_optional(l.pool()) .await? .is_some(), (None, None) => false, }; if !known { bail!("no project named {p} (spectra project list)"); } } let mut events: Vec<_> = w.refresh_event(&home).await?.into_iter().collect(); let owner = KeyPair::load_home(&home)?; let mut e = signed_event(&owner, "human", "brief.set", serde_json::json!({ "project": project, "text": text })); e.project = project.clone(); events.push(e); w.append(events).await?; let verb = if text.trim().is_empty() { "Cleared" } else { "Set" }; println!("{} {}", ui::done(verb), ui::subject(label(project.as_deref()))); println!(" {} {}", ui::note(), ui::dim("agents are told on their next call that their brief changed")); Ok(0) } /// Read the text from an argument, a file, or `-` for standard input. pub fn text_of(text: Option, file: Option) -> Result { match (text, file) { (Some(t), None) => Ok(t), (None, Some(f)) if f.as_os_str() == "-" => { let mut s = String::new(); std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?; Ok(s) } (None, Some(f)) => std::fs::read_to_string(&f).with_context(|| format!("reading {}", f.display())), (Some(_), Some(_)) => bail!("give the text or --file, not both"), (None, None) => bail!("give the text, or --file FILE (- for standard input), or use spectra brief edit"), } } /// `spectra brief edit`: the current text in `$VISUAL` or `$EDITOR`. pub async fn edit(ctx: &Ctx, project: Option, owner_home_flag: Option) -> Result { let before = current(ctx, project.as_deref()).await?.unwrap_or_default(); let dir = tempfile::tempdir()?; let path = dir.path().join(format!("{}.md", project.as_deref().unwrap_or("overall"))); std::fs::write(&path, &before)?; let editor = std::env::var("VISUAL").or_else(|_| std::env::var("EDITOR")).unwrap_or_else(|_| "nano".into()); let mut parts = editor.split_whitespace(); let program = parts.next().context("no editor ($VISUAL or $EDITOR)")?; let status = std::process::Command::new(program).args(parts).arg(&path).status().with_context(|| format!("running {editor}"))?; if !status.success() { bail!("{editor} did not finish cleanly: nothing was changed"); } let after = std::fs::read_to_string(&path)?; if after == before { println!("{} {}", ui::note(), ui::dim("unchanged")); return Ok(0); } set(ctx, project, after, owner_home_flag).await }