Skip to main content

agent_trace/trace/
agent_trace_md.rs

1use crate::git_store::{CommitInfo, GitStore};
2use crate::manifest::Manifest;
3use crate::types::{Action, Actor, DocType};
4use anyhow::Result;
5use std::path::Path;
6
7/// Generate the AGENT-TRACE.md content without writing to disk.
8pub fn generate(_store_root: &Path, manifest: &Manifest) -> String {
9    let plans = manifest.list(Some(&DocType::Plan));
10    let contexts = manifest.list(Some(&DocType::Context));
11    let logs = manifest.list(Some(&DocType::Log));
12    let references = manifest.list(Some(&DocType::Reference));
13    let scratches = manifest.list(Some(&DocType::Scratch));
14
15    let total = manifest.len();
16
17    let mut out = String::from("# AGENT-TRACE.md — Agent Discovery Index\n\n");
18    out.push_str(
19        "> This file is auto-generated by `agent-trace`. Do not edit manually.\n\
20         > Read this file to understand the document store and its contents.\n\n",
21    );
22
23    out.push_str("## How to Use This Store\n\n");
24    out.push_str("- **Read** any document freely — all documents are readable by all actors.\n");
25    out.push_str(
26        "- **Write** only to `plan` and `scratch` documents — other types are protected.\n",
27    );
28    out.push_str(
29        "- **Context** (`context.md`) is system-synthesized — read it for project state.\n",
30    );
31    out.push_str(
32        "- **Running Summary** (`running_summary.md`) is incrementally updated — read it to resume work.\n",
33    );
34    out.push_str("- **Logs** are system-generated — do not modify them.\n");
35    out.push_str("- **Reference** documents are user-curated — agents cannot modify them.\n\n");
36
37    out.push_str("## Write Permission Rules\n\n");
38    out.push_str("| Type | Agent Can Write | User Can Write | System Can Write |\n");
39    out.push_str("|------|:-:|:-:|:-:|\n");
40    out.push_str("| plan | ✓ | ✓ | ✗ |\n");
41    out.push_str("| context | ✗ | ⚠ | ✓ |\n");
42    out.push_str("| log | ✗ | ⚠ | ✓ |\n");
43    out.push_str("| reference | ✗ | ✓ | ✗ |\n");
44    out.push_str("| scratch | ✓ | ✓ | ✓ |\n\n");
45    out.push_str("*⚠ = requires user confirmation. Unauthorized agent writes are automatically reverted.*\n\n");
46
47    out.push_str(&format!("## Documents ({total} total)\n\n"));
48
49    if !plans.is_empty() {
50        out.push_str("### Plans\n\n");
51        for p in &plans {
52            let desc = if p.description.is_empty() {
53                ""
54            } else {
55                &p.description
56            };
57            out.push_str(&format!("- `{}` {}\n", p.path.display(), desc));
58        }
59        out.push('\n');
60    }
61
62    if !contexts.is_empty() {
63        out.push_str("### Context\n\n");
64        for c in &contexts {
65            out.push_str(&format!("- `{}`\n", c.path.display()));
66        }
67        out.push('\n');
68    }
69
70    if !references.is_empty() {
71        out.push_str("### Reference\n\n");
72        for r in &references {
73            let desc = if r.description.is_empty() {
74                ""
75            } else {
76                &r.description
77            };
78            out.push_str(&format!("- `{}` {}\n", r.path.display(), desc));
79        }
80        out.push('\n');
81    }
82
83    if !logs.is_empty() {
84        out.push_str("### Logs\n\n");
85        for l in &logs {
86            out.push_str(&format!("- `{}`\n", l.path.display()));
87        }
88        out.push('\n');
89    }
90
91    if !scratches.is_empty() {
92        out.push_str("### Scratch\n\n");
93        for s in &scratches {
94            out.push_str(&format!("- `{}`\n", s.path.display()));
95        }
96        out.push('\n');
97    }
98
99    out.push_str("## Store Stats\n\n");
100    out.push_str(&format!("- Total documents: {total}\n"));
101    out.push_str(&format!("- Plans: {}\n", plans.len()));
102    out.push_str(&format!("- Reference: {}\n", references.len()));
103    out.push_str(&format!("- Scratch: {}\n", scratches.len()));
104    out.push_str(&format!("- Logs: {}\n", logs.len()));
105
106    out.push_str("\n## Resume on Reconnect\n\n");
107    out.push_str("1. Call MCP tool `get_resume_context` first\n");
108    out.push_str("2. Read `running_summary.md` for current state\n");
109    out.push_str("3. Read `plan.md` only if summary references new phases\n");
110
111    out
112}
113
114/// Write AGENT-TRACE.md when content changed, using an atomic tmp rename.
115pub fn sync(store_root: &Path, manifest: &Manifest, git: &GitStore) -> Result<()> {
116    let new_content = generate(store_root, manifest);
117    let target = store_root.join("AGENT-TRACE.md");
118    if std::fs::read_to_string(&target).unwrap_or_default() == new_content {
119        return Ok(());
120    }
121
122    let tmp = store_root.join(".agent-trace").join("AGENT-TRACE.md.tmp");
123    std::fs::write(&tmp, &new_content)?;
124    std::fs::rename(&tmp, &target)?;
125
126    let info = CommitInfo {
127        action: Action::Modify,
128        files: vec![(
129            std::path::PathBuf::from("AGENT-TRACE.md"),
130            Action::Modify,
131            DocType::Reference,
132        )],
133        actor: Actor::System,
134        summary: "update AGENT-TRACE.md index".into(),
135        agent_name: None,
136        session_id: None,
137    };
138    git.commit(&info)?;
139    Ok(())
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::config::StoreInfo;
146    use crate::manifest::Manifest;
147    use std::path::PathBuf;
148    use tempfile::TempDir;
149
150    fn setup(tmp: &TempDir) -> (PathBuf, Manifest, GitStore) {
151        let root = tmp.path().to_path_buf();
152        std::fs::create_dir_all(root.join(".agent-trace")).unwrap();
153        let info = StoreInfo::new("test".into());
154        let manifest = Manifest::create_empty(info, &root).unwrap();
155        let git = GitStore::init(&root).unwrap();
156        (root, manifest, git)
157    }
158
159    #[test]
160    fn test_generate_empty_store() {
161        let tmp = TempDir::new().unwrap();
162        let (root, manifest, _git) = setup(&tmp);
163        let content = generate(&root, &manifest);
164        assert!(content.contains("AGENT-TRACE.md"));
165        assert!(content.contains("0 total"));
166        assert!(content.contains("Write Permission Rules"));
167    }
168
169    #[test]
170    fn test_generate_with_documents() {
171        let tmp = TempDir::new().unwrap();
172        let (root, mut manifest, _git) = setup(&tmp);
173        manifest
174            .register(&PathBuf::from("prd.md"), DocType::Plan, "")
175            .unwrap();
176        manifest
177            .register(&PathBuf::from("schema.md"), DocType::Reference, "")
178            .unwrap();
179        manifest
180            .register(&PathBuf::from("notes.md"), DocType::Scratch, "")
181            .unwrap();
182
183        let content = generate(&root, &manifest);
184        assert!(content.contains("prd.md"));
185        assert!(content.contains("schema.md"));
186        assert!(content.contains("notes.md"));
187        assert!(content.contains("3 total"));
188    }
189
190    #[test]
191    fn test_sync_writes_index() {
192        let tmp = TempDir::new().unwrap();
193        let (root, mut manifest, git) = setup(&tmp);
194        manifest
195            .register(&PathBuf::from("prd.md"), DocType::Plan, "")
196            .unwrap();
197
198        // Need prd.md to exist to stage it (already in AGENT-TRACE generation we only write AGENT-TRACE.md)
199        sync(&root, &manifest, &git).unwrap();
200        assert!(root.join("AGENT-TRACE.md").exists());
201    }
202
203    #[test]
204    fn test_generate_rules_section() {
205        let tmp = TempDir::new().unwrap();
206        let (root, manifest, _) = setup(&tmp);
207        let content = generate(&root, &manifest);
208        assert!(content.contains("Write Permission Rules"));
209        assert!(content.contains("plan"));
210        assert!(content.contains("context"));
211        assert!(content.contains("automatically reverted"));
212    }
213}