diff --git a/crates/g3-core/src/tools/invariants.rs b/crates/g3-core/src/tools/invariants.rs index e5f277e..27baec6 100644 --- a/crates/g3-core/src/tools/invariants.rs +++ b/crates/g3-core/src/tools/invariants.rs @@ -974,6 +974,74 @@ fn format_predicate_markdown( } } +/// Format an action envelope as human-readable markdown. +/// +/// This produces a rich, readable format suitable for tool output, +/// showing the facts recorded about completed work. +pub fn format_envelope_markdown(envelope: &ActionEnvelope) -> String { + let mut output = String::new(); + + output.push_str("\n"); + output.push_str("### Action Envelope\n\n"); + + if envelope.facts.is_empty() { + output.push_str("_No facts recorded._\n"); + return output; + } + + // Sort facts by key for consistent output + let mut keys: Vec<_> = envelope.facts.keys().collect(); + keys.sort(); + + for key in keys { + if let Some(value) = envelope.facts.get(key) { + output.push_str(&format!("**{}**:\n", key)); + format_yaml_value_markdown(&mut output, value, 0); + output.push_str("\n"); + } + } + + output +} + +/// Format a YAML value as indented markdown. +fn format_yaml_value_markdown(output: &mut String, value: &YamlValue, indent: usize) { + let prefix = " ".repeat(indent); + match value { + YamlValue::Null => output.push_str(&format!("{} - _null_\n", prefix)), + YamlValue::Bool(b) => output.push_str(&format!("{} - `{}`\n", prefix, b)), + YamlValue::Number(n) => output.push_str(&format!("{} - `{}`\n", prefix, n)), + YamlValue::String(s) => output.push_str(&format!("{} - `{}`\n", prefix, s)), + YamlValue::Sequence(seq) => { + for item in seq { + match item { + YamlValue::String(s) => output.push_str(&format!("{} - `{}`\n", prefix, s)), + YamlValue::Number(n) => output.push_str(&format!("{} - `{}`\n", prefix, n)), + YamlValue::Bool(b) => output.push_str(&format!("{} - `{}`\n", prefix, b)), + _ => format_yaml_value_markdown(output, item, indent + 1), + } + } + } + YamlValue::Mapping(map) => { + for (k, v) in map { + let key_str = yaml_to_display(k); + match v { + YamlValue::String(s) => output.push_str(&format!("{} - {}: `{}`\n", prefix, key_str, s)), + YamlValue::Number(n) => output.push_str(&format!("{} - {}: `{}`\n", prefix, key_str, n)), + YamlValue::Bool(b) => output.push_str(&format!("{} - {}: `{}`\n", prefix, key_str, b)), + YamlValue::Null => output.push_str(&format!("{} - {}: _null_\n", prefix, key_str)), + YamlValue::Sequence(_) | YamlValue::Mapping(_) => { + output.push_str(&format!("{} - {}:\n", prefix, key_str)); + format_yaml_value_markdown(output, v, indent + 2); + } + YamlValue::Tagged(t) => output.push_str(&format!("{} - {}: !{} ...\n", prefix, key_str, t.tag)), + } + } + } + YamlValue::Tagged(t) => output.push_str(&format!("{} - !{} ...\n", prefix, t.tag)), + } +} + // ============================================================================ // Tests // ============================================================================ @@ -1397,4 +1465,51 @@ mod tests { assert!(output.contains("**From Task:**")); assert!(!output.contains("**From Memory:**")); } + + // ======================================================================== + // Format Envelope Markdown Tests + // ======================================================================== + + #[test] + fn test_format_envelope_markdown_empty() { + let envelope = ActionEnvelope::new(); + let output = format_envelope_markdown(&envelope); + + assert!(output.contains("### Action Envelope")); + assert!(output.contains("_No facts recorded._")); + } + + #[test] + fn test_format_envelope_markdown_with_facts() { + let mut envelope = ActionEnvelope::new(); + envelope.add_fact( + "csv_importer", + serde_yaml::from_str(r#" + capabilities: + - handle_headers + - handle_tsv + file: src/import/csv.rs + "#).unwrap(), + ); + + let output = format_envelope_markdown(&envelope); + + assert!(output.contains("### Action Envelope")); + assert!(output.contains("**csv_importer**:")); + assert!(output.contains("`handle_headers`")); + assert!(output.contains("`handle_tsv`")); + assert!(output.contains("`src/import/csv.rs`")); + } + + #[test] + fn test_format_envelope_markdown_with_null_value() { + let mut envelope = ActionEnvelope::new(); + envelope.add_fact("breaking_changes", YamlValue::Null); + + let output = format_envelope_markdown(&envelope); + + assert!(output.contains("### Action Envelope")); + assert!(output.contains("**breaking_changes**:")); + assert!(output.contains("_null_")); + } } diff --git a/crates/g3-core/src/tools/plan.rs b/crates/g3-core/src/tools/plan.rs index 0c0304a..02a10b0 100644 --- a/crates/g3-core/src/tools/plan.rs +++ b/crates/g3-core/src/tools/plan.rs @@ -20,7 +20,7 @@ use crate::ToolCall; use super::executor::ToolContext; -use super::invariants::{format_rulespec_markdown, get_envelope_path, get_rulespec_path, read_rulespec}; +use super::invariants::{format_envelope_markdown, format_rulespec_markdown, get_envelope_path, get_rulespec_path, read_envelope, read_rulespec}; // ============================================================================ // Plan Schema @@ -798,11 +798,27 @@ pub async fn execute_plan_read( Some(plan) => { let yaml = serde_yaml::to_string(&plan)?; ctx.ui_writer.print_plan_compact(Some(&yaml), Some(&plan_path_str), false); - Ok(format!( + + // Build output with plan + let mut output = format!( "📋 {}\n\n```yaml\n{}```", plan.status_summary(), yaml - )) + ); + + // Append rulespec if present + match read_rulespec(session_id) { + Ok(Some(rulespec)) => output.push_str(&format_rulespec_markdown(&rulespec)), + _ => output.push_str("\n\n_No rulespec generated._\n"), + } + + // Append envelope if present + match read_envelope(session_id) { + Ok(Some(envelope)) => output.push_str(&format_envelope_markdown(&envelope)), + _ => output.push_str("\n_No envelope generated._\n"), + } + + Ok(output) } None => { ctx.ui_writer.print_plan_compact(None, None, false); @@ -885,22 +901,31 @@ pub async fn execute_plan_write( Err(_) => "\n_No rulespec generated._\n".to_string(), }; + // Read and format envelope if it exists + let envelope_section = match read_envelope(session_id) { + Ok(Some(envelope)) => format_envelope_markdown(&envelope), + Ok(None) => "\n_No envelope generated._\n".to_string(), + Err(_) => "\n_No envelope generated._\n".to_string(), + }; + // Check if plan is now complete and trigger verification if plan.is_complete() && plan.is_approved() { let verification = plan_verify(&plan, ctx.working_dir); let verification_output = format_verification_results(&verification, ctx.session_id); return Ok(format!( - "✅ Plan updated: {}\n{}\n{}", + "✅ Plan updated: {}\n{}\n{}\n{}", plan.status_summary(), verification_output, - rulespec_section + rulespec_section, + envelope_section )); } Ok(format!( - "✅ Plan updated: {}\n{}", + "✅ Plan updated: {}\n{}\n{}", plan.status_summary(), - rulespec_section + rulespec_section, + envelope_section )) }