use crate::git_store::{CommitInfo, GitStore};
use crate::manifest::Manifest;
use crate::types::{Action, Actor, DocType};
use anyhow::Result;
use std::path::Path;
pub fn generate(_store_root: &Path, manifest: &Manifest) -> String {
let plans = manifest.list(Some(&DocType::Plan));
let contexts = manifest.list(Some(&DocType::Context));
let logs = manifest.list(Some(&DocType::Log));
let references = manifest.list(Some(&DocType::Reference));
let scratches = manifest.list(Some(&DocType::Scratch));
let total = manifest.len();
let mut out = String::from("# AGENT-TRACE.md — Agent Discovery Index\n\n");
out.push_str(
"> This file is auto-generated by `agent-trace`. Do not edit manually.\n\
> Read this file to understand the document store and its contents.\n\n",
);
out.push_str("## How to Use This Store\n\n");
out.push_str("- **Read** any document freely — all documents are readable by all actors.\n");
out.push_str(
"- **Write** only to `plan` and `scratch` documents — other types are protected.\n",
);
out.push_str(
"- **Context** (`context.md`) is system-synthesized — read it for project state.\n",
);
out.push_str(
"- **Running Summary** (`running_summary.md`) is incrementally updated — read it to resume work.\n",
);
out.push_str("- **Logs** are system-generated — do not modify them.\n");
out.push_str("- **Reference** documents are user-curated — agents cannot modify them.\n\n");
out.push_str("## Write Permission Rules\n\n");
out.push_str("| Type | Agent Can Write | User Can Write | System Can Write |\n");
out.push_str("|------|:-:|:-:|:-:|\n");
out.push_str("| plan | ✓ | ✓ | ✗ |\n");
out.push_str("| context | ✗ | ⚠ | ✓ |\n");
out.push_str("| log | ✗ | ⚠ | ✓ |\n");
out.push_str("| reference | ✗ | ✓ | ✗ |\n");
out.push_str("| scratch | ✓ | ✓ | ✓ |\n\n");
out.push_str("*⚠ = requires user confirmation. Unauthorized agent writes are automatically reverted.*\n\n");
out.push_str(&format!("## Documents ({total} total)\n\n"));
if !plans.is_empty() {
out.push_str("### Plans\n\n");
for p in &plans {
let desc = if p.description.is_empty() {
""
} else {
&p.description
};
out.push_str(&format!("- `{}` {}\n", p.path.display(), desc));
}
out.push('\n');
}
if !contexts.is_empty() {
out.push_str("### Context\n\n");
for c in &contexts {
out.push_str(&format!("- `{}`\n", c.path.display()));
}
out.push('\n');
}
if !references.is_empty() {
out.push_str("### Reference\n\n");
for r in &references {
let desc = if r.description.is_empty() {
""
} else {
&r.description
};
out.push_str(&format!("- `{}` {}\n", r.path.display(), desc));
}
out.push('\n');
}
if !logs.is_empty() {
out.push_str("### Logs\n\n");
for l in &logs {
out.push_str(&format!("- `{}`\n", l.path.display()));
}
out.push('\n');
}
if !scratches.is_empty() {
out.push_str("### Scratch\n\n");
for s in &scratches {
out.push_str(&format!("- `{}`\n", s.path.display()));
}
out.push('\n');
}
out.push_str("## Store Stats\n\n");
out.push_str(&format!("- Total documents: {total}\n"));
out.push_str(&format!("- Plans: {}\n", plans.len()));
out.push_str(&format!("- Reference: {}\n", references.len()));
out.push_str(&format!("- Scratch: {}\n", scratches.len()));
out.push_str(&format!("- Logs: {}\n", logs.len()));
out.push_str("\n## Resume on Reconnect\n\n");
out.push_str("1. Call MCP tool `get_resume_context` first\n");
out.push_str("2. Read `running_summary.md` for current state\n");
out.push_str("3. Read `plan.md` only if summary references new phases\n");
out
}
pub fn sync(store_root: &Path, manifest: &Manifest, git: &GitStore) -> Result<()> {
let new_content = generate(store_root, manifest);
let target = store_root.join("AGENT-TRACE.md");
if std::fs::read_to_string(&target).unwrap_or_default() == new_content {
return Ok(());
}
let tmp = store_root.join(".agent-trace").join("AGENT-TRACE.md.tmp");
std::fs::write(&tmp, &new_content)?;
std::fs::rename(&tmp, &target)?;
let info = CommitInfo {
action: Action::Modify,
files: vec![(
std::path::PathBuf::from("AGENT-TRACE.md"),
Action::Modify,
DocType::Reference,
)],
actor: Actor::System,
summary: "update AGENT-TRACE.md index".into(),
agent_name: None,
session_id: None,
};
git.commit(&info)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::StoreInfo;
use crate::manifest::Manifest;
use std::path::PathBuf;
use tempfile::TempDir;
fn setup(tmp: &TempDir) -> (PathBuf, Manifest, GitStore) {
let root = tmp.path().to_path_buf();
std::fs::create_dir_all(root.join(".agent-trace")).unwrap();
let info = StoreInfo::new("test".into());
let manifest = Manifest::create_empty(info, &root).unwrap();
let git = GitStore::init(&root).unwrap();
(root, manifest, git)
}
#[test]
fn test_generate_empty_store() {
let tmp = TempDir::new().unwrap();
let (root, manifest, _git) = setup(&tmp);
let content = generate(&root, &manifest);
assert!(content.contains("AGENT-TRACE.md"));
assert!(content.contains("0 total"));
assert!(content.contains("Write Permission Rules"));
}
#[test]
fn test_generate_with_documents() {
let tmp = TempDir::new().unwrap();
let (root, mut manifest, _git) = setup(&tmp);
manifest
.register(&PathBuf::from("prd.md"), DocType::Plan, "")
.unwrap();
manifest
.register(&PathBuf::from("schema.md"), DocType::Reference, "")
.unwrap();
manifest
.register(&PathBuf::from("notes.md"), DocType::Scratch, "")
.unwrap();
let content = generate(&root, &manifest);
assert!(content.contains("prd.md"));
assert!(content.contains("schema.md"));
assert!(content.contains("notes.md"));
assert!(content.contains("3 total"));
}
#[test]
fn test_sync_writes_index() {
let tmp = TempDir::new().unwrap();
let (root, mut manifest, git) = setup(&tmp);
manifest
.register(&PathBuf::from("prd.md"), DocType::Plan, "")
.unwrap();
sync(&root, &manifest, &git).unwrap();
assert!(root.join("AGENT-TRACE.md").exists());
}
#[test]
fn test_generate_rules_section() {
let tmp = TempDir::new().unwrap();
let (root, manifest, _) = setup(&tmp);
let content = generate(&root, &manifest);
assert!(content.contains("Write Permission Rules"));
assert!(content.contains("plan"));
assert!(content.contains("context"));
assert!(content.contains("automatically reverted"));
}
}