1use std::path::Path;
12
13use super::handoff::HandoffRequest;
14use super::harness_artifacts;
15
16#[derive(Debug, Clone)]
18pub struct OrientationContext {
19 pub progress_summary: Option<String>,
21 pub spec_summary: Option<String>,
23 pub contract_summary: Option<String>,
25 pub sprint_contract_summary: Option<String>,
27 pub evaluation_summary: Option<String>,
29 pub outcome_verification_summary: Option<String>,
31 pub recent_git_log: Option<String>,
33 pub loop_notes: Option<String>,
35 pub loop_decisions: Option<String>,
37 pub compaction_summary: Option<String>,
39 pub feature_list_summary: Option<String>,
42 pub context_reset_manifest: Option<String>,
46 pub handoff: Option<HandoffRequest>,
48}
49
50impl OrientationContext {
51 pub fn to_prompt_section(&self) -> Option<String> {
55 let mut parts = Vec::new();
56
57 if let Some(progress) = &self.progress_summary {
58 parts.push(format!("### Progress\n{progress}"));
59 }
60 if let Some(spec) = &self.spec_summary {
61 parts.push(format!("### Spec\n{spec}"));
62 }
63 if let Some(contract) = &self.contract_summary {
64 parts.push(format!("### Contract\n{contract}"));
65 }
66 if let Some(sprint) = &self.sprint_contract_summary {
67 parts.push(format!("### Sprint Contract\n{sprint}"));
68 }
69 if let Some(eval) = &self.evaluation_summary {
70 parts.push(format!("### Last Evaluation\n{eval}"));
71 }
72 if let Some(outcome) = &self.outcome_verification_summary {
73 parts.push(format!("### Outcome Verification\n{outcome}"));
74 }
75 if let Some(git) = &self.recent_git_log {
76 parts.push(format!("### Recent Git Log\n{git}"));
77 }
78 if let Some(notes) = &self.loop_notes {
79 parts.push(format!("### Loop Notes\n{notes}"));
80 }
81 if let Some(decisions) = &self.loop_decisions {
82 parts.push(format!("### Loop Decisions\n{decisions}"));
83 }
84 if let Some(compaction) = &self.compaction_summary {
85 parts.push(format!("### Previous Session Summary\n{compaction}"));
86 }
87 if let Some(feature_list) = &self.feature_list_summary {
88 parts.push(format!("### Feature List\n{feature_list}"));
89 }
90 if let Some(reset) = &self.context_reset_manifest {
91 parts.push(format!(
92 "### Context Reset\n\
93 This session starts from a clean context. Previous conversation \
94 history was deliberately discarded to clear noise and bad \
95 assumptions. Reorient from the artifacts below.\n\n{reset}"
96 ));
97 }
98 if let Some(handoff) = &self.handoff {
99 parts.push(handoff.to_handoff_prompt());
100 }
101
102 if parts.is_empty() {
103 return None;
104 }
105
106 Some(format!("[Orientation Context]\n\n{}", parts.join("\n\n")))
107 }
108}
109
110pub fn gather_orientation(workspace_root: &Path, session_id: &str) -> OrientationContext {
116 use crate::loop_memory::{LoopMemoryStore, MarkdownLoopMemory};
117
118 let progress_summary = vtcode_memory::progress::load_progress(workspace_root, session_id)
119 .ok()
120 .flatten()
121 .map(|ledger| {
122 format!(
123 "Goal: {} | Completion: {:.0}% | Confidence: {:.2} | {}",
124 ledger.goal,
125 ledger.completion_ratio() * 100.0,
126 ledger.confidence,
127 if ledger.is_stalled() { "STALLED" } else { "on track" },
128 )
129 });
130
131 let artifact_cutoff = harness_artifacts::session_artifact_cutoff(workspace_root, session_id);
134 let spec_summary = harness_artifacts::read_spec_summary_fresh(workspace_root, artifact_cutoff);
135 let contract_summary = harness_artifacts::read_contract_summary_fresh(workspace_root, artifact_cutoff);
136 let sprint_contract_summary =
137 harness_artifacts::read_sprint_contract_summary_fresh(workspace_root, artifact_cutoff);
138 let evaluation_summary = harness_artifacts::read_evaluation_summary_fresh(workspace_root, artifact_cutoff);
139 let outcome_verification_summary =
140 harness_artifacts::read_outcome_verification_summary_fresh(workspace_root, artifact_cutoff);
141 let feature_list_summary = harness_artifacts::read_feature_list_summary_fresh(workspace_root, artifact_cutoff);
142
143 let recent_git_log = gather_recent_git_log(workspace_root);
144
145 let memory = MarkdownLoopMemory::new(workspace_root);
146 let loop_notes = memory.read_notes().ok().filter(|s| !s.is_empty());
147 let loop_decisions = memory.read_decisions().ok().filter(|s| !s.is_empty());
148
149 let compaction_summary = read_compaction_summary(workspace_root);
154
155 let context_reset_manifest = read_context_reset_manifest(workspace_root);
159
160 OrientationContext {
161 progress_summary,
162 spec_summary,
163 contract_summary,
164 sprint_contract_summary,
165 evaluation_summary,
166 outcome_verification_summary,
167 recent_git_log,
168 loop_notes,
169 loop_decisions,
170 compaction_summary,
171 feature_list_summary,
172 context_reset_manifest,
173 handoff: None,
174 }
175}
176
177fn gather_recent_git_log(workspace_root: &Path) -> Option<String> {
179 let output = std::process::Command::new("git")
180 .args(["log", "--oneline", "-5", "--no-decorate"])
181 .current_dir(workspace_root)
182 .output()
183 .ok()?;
184
185 if !output.status.success() {
186 return None;
187 }
188
189 let log = String::from_utf8_lossy(&output.stdout).into_owned();
190 if log.trim().is_empty() { None } else { Some(log) }
191}
192
193fn read_compaction_summary(workspace_root: &Path) -> Option<String> {
198 let path = workspace_root.join("memories").join("compaction_summary.md");
199 let content = std::fs::read_to_string(&path).ok()?;
200 if content.trim().is_empty() { None } else { Some(content) }
201}
202
203fn read_context_reset_manifest(workspace_root: &Path) -> Option<String> {
207 crate::core::agent::context_reset::read_manifest_orientation(workspace_root)
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn empty_context_returns_none() {
216 let ctx = OrientationContext {
217 progress_summary: None,
218 spec_summary: None,
219 contract_summary: None,
220 sprint_contract_summary: None,
221 evaluation_summary: None,
222 outcome_verification_summary: None,
223 recent_git_log: None,
224 loop_notes: None,
225 loop_decisions: None,
226 compaction_summary: None,
227 feature_list_summary: None,
228 context_reset_manifest: None,
229 handoff: None,
230 };
231 assert!(ctx.to_prompt_section().is_none());
232 }
233
234 #[test]
235 fn single_field_produces_section() {
236 let ctx = OrientationContext {
237 progress_summary: Some("Goal: test | Completion: 50%".to_string()),
238 spec_summary: None,
239 contract_summary: None,
240 sprint_contract_summary: None,
241 evaluation_summary: None,
242 outcome_verification_summary: None,
243 recent_git_log: None,
244 loop_notes: None,
245 loop_decisions: None,
246 compaction_summary: None,
247 feature_list_summary: None,
248 context_reset_manifest: None,
249 handoff: None,
250 };
251 let section = ctx.to_prompt_section().expect("should have section");
252 assert!(section.contains("[Orientation Context]"));
253 assert!(section.contains("### Progress"));
254 assert!(section.contains("Goal: test"));
255 }
256
257 #[test]
258 fn multiple_fields_rendered() {
259 let ctx = OrientationContext {
260 progress_summary: Some("progress".to_string()),
261 spec_summary: Some("spec".to_string()),
262 contract_summary: None,
263 sprint_contract_summary: None,
264 evaluation_summary: None,
265 outcome_verification_summary: None,
266 recent_git_log: Some("abc123 commit".to_string()),
267 loop_notes: None,
268 loop_decisions: None,
269 compaction_summary: None,
270 feature_list_summary: Some("auth, api".to_string()),
271 context_reset_manifest: None,
272 handoff: None,
273 };
274 let section = ctx.to_prompt_section().expect("should have section");
275 assert!(section.contains("### Progress"));
276 assert!(section.contains("### Spec"));
277 assert!(section.contains("### Recent Git Log"));
278 assert!(section.contains("### Feature List"));
279 assert!(!section.contains("### Contract"));
281 }
282
283 #[test]
284 fn context_reset_manifest_rendered() {
285 let ctx = OrientationContext {
286 progress_summary: Some("Goal: test | Completion: 50%".to_string()),
287 spec_summary: None,
288 contract_summary: None,
289 sprint_contract_summary: None,
290 evaluation_summary: None,
291 outcome_verification_summary: None,
292 recent_git_log: None,
293 loop_notes: None,
294 loop_decisions: None,
295 compaction_summary: None,
296 feature_list_summary: None,
297 context_reset_manifest: Some("# Context Reset Manifest\n\n**Trigger:** stall".to_string()),
298 handoff: None,
299 };
300 let section = ctx.to_prompt_section().expect("should have section");
301 assert!(section.contains("### Context Reset"));
302 assert!(section.contains("clean context"));
303 assert!(section.contains("**Trigger:** stall"));
304 }
305}