use std::path::Path;
use super::handoff::HandoffRequest;
use super::harness_artifacts;
#[derive(Debug, Clone)]
pub struct OrientationContext {
pub progress_summary: Option<String>,
pub spec_summary: Option<String>,
pub contract_summary: Option<String>,
pub sprint_contract_summary: Option<String>,
pub evaluation_summary: Option<String>,
pub outcome_verification_summary: Option<String>,
pub recent_git_log: Option<String>,
pub loop_notes: Option<String>,
pub loop_decisions: Option<String>,
pub compaction_summary: Option<String>,
pub feature_list_summary: Option<String>,
pub context_reset_manifest: Option<String>,
pub handoff: Option<HandoffRequest>,
}
impl OrientationContext {
pub fn to_prompt_section(&self) -> Option<String> {
let mut parts = Vec::new();
if let Some(progress) = &self.progress_summary {
parts.push(format!("### Progress\n{progress}"));
}
if let Some(spec) = &self.spec_summary {
parts.push(format!("### Spec\n{spec}"));
}
if let Some(contract) = &self.contract_summary {
parts.push(format!("### Contract\n{contract}"));
}
if let Some(sprint) = &self.sprint_contract_summary {
parts.push(format!("### Sprint Contract\n{sprint}"));
}
if let Some(eval) = &self.evaluation_summary {
parts.push(format!("### Last Evaluation\n{eval}"));
}
if let Some(outcome) = &self.outcome_verification_summary {
parts.push(format!("### Outcome Verification\n{outcome}"));
}
if let Some(git) = &self.recent_git_log {
parts.push(format!("### Recent Git Log\n{git}"));
}
if let Some(notes) = &self.loop_notes {
parts.push(format!("### Loop Notes\n{notes}"));
}
if let Some(decisions) = &self.loop_decisions {
parts.push(format!("### Loop Decisions\n{decisions}"));
}
if let Some(compaction) = &self.compaction_summary {
parts.push(format!("### Previous Session Summary\n{compaction}"));
}
if let Some(feature_list) = &self.feature_list_summary {
parts.push(format!("### Feature List\n{feature_list}"));
}
if let Some(reset) = &self.context_reset_manifest {
parts.push(format!(
"### Context Reset\n\
This session starts from a clean context. Previous conversation \
history was deliberately discarded to clear noise and bad \
assumptions. Reorient from the artifacts below.\n\n{reset}"
));
}
if let Some(handoff) = &self.handoff {
parts.push(handoff.to_handoff_prompt());
}
if parts.is_empty() {
return None;
}
Some(format!("[Orientation Context]\n\n{}", parts.join("\n\n")))
}
}
pub fn gather_orientation(workspace_root: &Path, session_id: &str) -> OrientationContext {
use crate::loop_memory::{LoopMemoryStore, MarkdownLoopMemory};
let progress_summary = vtcode_memory::progress::load_progress(workspace_root, session_id)
.ok()
.flatten()
.map(|ledger| {
format!(
"Goal: {} | Completion: {:.0}% | Confidence: {:.2} | {}",
ledger.goal,
ledger.completion_ratio() * 100.0,
ledger.confidence,
if ledger.is_stalled() { "STALLED" } else { "on track" },
)
});
let spec_summary = harness_artifacts::read_spec_summary(workspace_root);
let contract_summary = harness_artifacts::read_contract_summary(workspace_root);
let sprint_contract_summary = harness_artifacts::read_sprint_contract_summary(workspace_root);
let evaluation_summary = harness_artifacts::read_evaluation_summary(workspace_root);
let outcome_verification_summary = harness_artifacts::read_outcome_verification_summary(workspace_root);
let feature_list_summary = harness_artifacts::read_feature_list_summary(workspace_root);
let recent_git_log = gather_recent_git_log(workspace_root);
let memory = MarkdownLoopMemory::new(workspace_root);
let loop_notes = memory.read_notes().ok().filter(|s| !s.is_empty());
let loop_decisions = memory.read_decisions().ok().filter(|s| !s.is_empty());
let compaction_summary = read_compaction_summary(workspace_root);
let context_reset_manifest = read_context_reset_manifest(workspace_root);
OrientationContext {
progress_summary,
spec_summary,
contract_summary,
sprint_contract_summary,
evaluation_summary,
outcome_verification_summary,
recent_git_log,
loop_notes,
loop_decisions,
compaction_summary,
feature_list_summary,
context_reset_manifest,
handoff: None,
}
}
fn gather_recent_git_log(workspace_root: &Path) -> Option<String> {
let output = std::process::Command::new("git")
.args(["log", "--oneline", "-5", "--no-decorate"])
.current_dir(workspace_root)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let log = String::from_utf8_lossy(&output.stdout).to_string();
if log.trim().is_empty() { None } else { Some(log) }
}
fn read_compaction_summary(workspace_root: &Path) -> Option<String> {
let path = workspace_root.join("memories").join("compaction_summary.md");
let content = std::fs::read_to_string(&path).ok()?;
if content.trim().is_empty() { None } else { Some(content) }
}
fn read_context_reset_manifest(workspace_root: &Path) -> Option<String> {
let path = harness_artifacts::current_context_reset_path(workspace_root);
let content = std::fs::read_to_string(&path).ok()?;
if content.trim().is_empty() { None } else { Some(content) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_context_returns_none() {
let ctx = OrientationContext {
progress_summary: None,
spec_summary: None,
contract_summary: None,
sprint_contract_summary: None,
evaluation_summary: None,
outcome_verification_summary: None,
recent_git_log: None,
loop_notes: None,
loop_decisions: None,
compaction_summary: None,
feature_list_summary: None,
context_reset_manifest: None,
handoff: None,
};
assert!(ctx.to_prompt_section().is_none());
}
#[test]
fn single_field_produces_section() {
let ctx = OrientationContext {
progress_summary: Some("Goal: test | Completion: 50%".to_string()),
spec_summary: None,
contract_summary: None,
sprint_contract_summary: None,
evaluation_summary: None,
outcome_verification_summary: None,
recent_git_log: None,
loop_notes: None,
loop_decisions: None,
compaction_summary: None,
feature_list_summary: None,
context_reset_manifest: None,
handoff: None,
};
let section = ctx.to_prompt_section().expect("should have section");
assert!(section.contains("[Orientation Context]"));
assert!(section.contains("### Progress"));
assert!(section.contains("Goal: test"));
}
#[test]
fn multiple_fields_rendered() {
let ctx = OrientationContext {
progress_summary: Some("progress".to_string()),
spec_summary: Some("spec".to_string()),
contract_summary: None,
sprint_contract_summary: None,
evaluation_summary: None,
outcome_verification_summary: None,
recent_git_log: Some("abc123 commit".to_string()),
loop_notes: None,
loop_decisions: None,
compaction_summary: None,
feature_list_summary: Some("auth, api".to_string()),
context_reset_manifest: None,
handoff: None,
};
let section = ctx.to_prompt_section().expect("should have section");
assert!(section.contains("### Progress"));
assert!(section.contains("### Spec"));
assert!(section.contains("### Recent Git Log"));
assert!(section.contains("### Feature List"));
assert!(!section.contains("### Contract"));
}
#[test]
fn context_reset_manifest_rendered() {
let ctx = OrientationContext {
progress_summary: Some("Goal: test | Completion: 50%".to_string()),
spec_summary: None,
contract_summary: None,
sprint_contract_summary: None,
evaluation_summary: None,
outcome_verification_summary: None,
recent_git_log: None,
loop_notes: None,
loop_decisions: None,
compaction_summary: None,
feature_list_summary: None,
context_reset_manifest: Some("# Context Reset Manifest\n\n**Trigger:** stall".to_string()),
handoff: None,
};
let section = ctx.to_prompt_section().expect("should have section");
assert!(section.contains("### Context Reset"));
assert!(section.contains("clean context"));
assert!(section.contains("**Trigger:** stall"));
}
}