agent_trace/trace/
agent_trace_md.rs1use crate::git_store::{CommitInfo, GitStore};
2use crate::manifest::Manifest;
3use crate::types::{Action, Actor, DocType};
4use anyhow::Result;
5use std::path::Path;
6
7pub 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
114pub 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 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}