feat(plan): display rulespec.yaml and envelope.yaml in plan_read/plan_write output
- Add format_envelope_markdown() function in invariants.rs for rich markdown formatting of ActionEnvelope facts - Add format_yaml_value_markdown() helper for recursive YAML value display - Update execute_plan_read() to append rulespec and envelope sections - Update execute_plan_write() to append envelope section alongside rulespec - Add 3 tests for format_envelope_markdown (empty, with facts, null values) When plan_read or plan_write is called, the output now includes: - Plan YAML (as before) - Rulespec section (if rulespec.yaml exists) with invariants grouped by source - Envelope section (if envelope.yaml exists) with facts in readable format Missing files show placeholder text rather than errors.
This commit is contained in:
@@ -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_"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<W: UiWriter>(
|
||||
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<W: UiWriter>(
|
||||
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
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user