use super::disk::{display, git, mkdir, stamp, write};
use super::places::Places;
use super::recipe::{Conv, Recipe, Step, Wsp};
use crate::registry;
use crate::wire::material::{LEAVES, Role};
use std::path::{Path, PathBuf};
const MODELS_YAML: &str = "models:\n default:\n provider: anthropic\n";
const SETTLED: &str = "{\"type\":\"finish\"}\n{\"type\":\"end\"}\n";
const FAILED: &str = "{\"type\":\"error\",\"message\":\"the model refused\"}\n{\"type\":\"end\"}\n";
const LIMITED: &str = "{\"type\":\"finish\",\"reason\":\"length\"}\n{\"type\":\"end\"}\n";
const STREAMING: &str = "{\"type\":\"content_delta\",\"delta\":{\"text\":\"Reading the \"}}\n\
{\"type\":\"content_delta\",\"delta\":{\"text\":\"module map\"}}\n";
const SEQ: &str = "000";
const TRUNK_EPOCH: i64 = 1_767_225_600;
pub fn lay(root: &Path, recipe: &Recipe, origin: i64) -> Result<Vec<PathBuf>, String> {
let places = Places::under(root);
mkdir(&places.litany)?;
write(&places.litany.join("models.yaml"), MODELS_YAML)?;
mkdir(&places.state)?;
if let Some(body) = recipe.cadence {
write(&places.state.join(crate::app::cadence::CADENCE_YAML), body)?;
}
let mut hold = Vec::new();
for wsp in recipe.workspaces {
lay_workspace(&places, wsp, recipe.brazen, origin, &mut hold)?;
}
Ok(hold)
}
fn lay_workspace(
places: &Places,
wsp: &Wsp,
brazen: Option<&str>,
origin: i64,
hold: &mut Vec<PathBuf>,
) -> Result<(), String> {
let ws = places.workspace(wsp.name);
found(&ws)?;
for conv in wsp.convs {
lay_conv(&ws, conv, origin, hold)?;
}
if let Some(body) = brazen {
let wall = places.walls.join(wsp.name).join("brazen");
mkdir(&wall)?;
write(&wall.join("config.toml"), body)?;
}
for client in LEAVES
.iter()
.filter(|role| **role != Role::Server)
.filter_map(|role| registry::Client::parse(&role.common_name()).ok())
{
registry::register(&places.state, &client, wsp.name)
.map_err(|e| format!("register {}: {e}", wsp.name))?;
}
Ok(())
}
fn found(ws: &Path) -> Result<(), String> {
let repo = ws.join("repo.git");
mkdir(&repo)?;
git(
&repo,
&["init", "-q", "--bare", "-b", "config/default"],
None,
)?;
let author = ws.join(".author");
let author_s = display(&author);
git(
&repo,
&[
"worktree",
"add",
"-q",
"--orphan",
"-b",
"config/default",
&author_s,
],
None,
)?;
write(&author.join("version"), "1\n")?;
git(&author, &["add", "version"], None)?;
git(
&author,
&["commit", "-q", "-m", "config: init"],
Some(TRUNK_EPOCH),
)?;
git(&repo, &["worktree", "remove", &author_s], None)
}
fn lay_conv(ws: &Path, conv: &Conv, origin: i64, hold: &mut Vec<PathBuf>) -> Result<(), String> {
let repo = ws.join("repo.git");
let at = origin - conv.age_secs;
let wt = ws.join("agents").join(conv.id);
let branch = format!("agents/{}", conv.id);
git(
&repo,
&[
"worktree",
"add",
"-q",
"-b",
&branch,
&display(&wt),
"config/default",
],
None,
)?;
write(&wt.join("goal.md"), conv.goal)?;
git(&wt, &["add", "goal.md"], None)?;
git(&wt, &["commit", "-q", "-m", "dispatch"], Some(at))?;
for (name, body) in conv.messages {
let path = wt.join("messages").join(name);
write(&path, body)?;
stamp(&path, at)?;
}
for (name, body) in conv.summaries {
write(&wt.join("summary").join(name), body)?;
}
for (name, body) in conv.deposits {
write(
&ws.join("inbox").join(conv.id).join(format!("{name}.md")),
body,
)?;
}
for mark in conv.marks {
let refname = format!("refs/litany/{mark}/{}", conv.id);
git(&repo, &["update-ref", &refname, &branch], None)?;
}
lay_step(ws, conv, at, hold)
}
fn lay_step(ws: &Path, conv: &Conv, at: i64, hold: &mut Vec<PathBuf>) -> Result<(), String> {
if conv.step == Step::Absent {
return Ok(());
}
let steps = ws.join("steps").join(conv.id);
let step = steps.join(SEQ);
let request = step.join("request.json");
write(&request, "{\"model\":\"fixture-1\"}\n")?;
stamp(&request, at)?;
if !conv.driver_log.is_empty() {
write(&steps.join("driver.log"), conv.driver_log)?;
}
let tail = match conv.step {
Step::Settled => SETTLED,
Step::Failed => FAILED,
Step::OutputLimit => LIMITED,
Step::Streaming => STREAMING,
Step::Absent | Step::Wound(_) => "",
};
if let Step::Wound(words) = conv.step {
write(&step.join("stderr.log"), words)?;
return Ok(());
}
let response = step.join("response.json");
write(&response, tail)?;
stamp(&response, at)?;
if conv.step == Step::Streaming {
hold.push(ws.join("inbox").join(conv.id));
hold.push(response);
return mkdir(&ws.join("inbox").join(conv.id));
}
write(&step.join("meta.json"), &meta(at))
}
fn meta(at: i64) -> String {
let stamp = crate::ui_state::iso8601_extended(at);
format!("{{\"started_at\":\"{stamp}\",\"ended_at\":\"{stamp}\"}}\n")
}
#[cfg(test)]
mod tests;