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:
Dhanji R. Prasanna
2026-02-05 19:08:55 +11:00
parent bc5c1bdf61
commit 06d75f613c
2 changed files with 147 additions and 7 deletions

View File

@@ -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 // Tests
// ============================================================================ // ============================================================================
@@ -1397,4 +1465,51 @@ mod tests {
assert!(output.contains("**From Task:**")); assert!(output.contains("**From Task:**"));
assert!(!output.contains("**From Memory:**")); 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_"));
}
} }

View File

@@ -20,7 +20,7 @@ use crate::ToolCall;
use super::executor::ToolContext; 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 // Plan Schema
@@ -798,11 +798,27 @@ pub async fn execute_plan_read<W: UiWriter>(
Some(plan) => { Some(plan) => {
let yaml = serde_yaml::to_string(&plan)?; let yaml = serde_yaml::to_string(&plan)?;
ctx.ui_writer.print_plan_compact(Some(&yaml), Some(&plan_path_str), false); 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{}```", "📋 {}\n\n```yaml\n{}```",
plan.status_summary(), plan.status_summary(),
yaml 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 => { None => {
ctx.ui_writer.print_plan_compact(None, None, false); 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(), 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 // Check if plan is now complete and trigger verification
if plan.is_complete() && plan.is_approved() { if plan.is_complete() && plan.is_approved() {
let verification = plan_verify(&plan, ctx.working_dir); let verification = plan_verify(&plan, ctx.working_dir);
let verification_output = format_verification_results(&verification, ctx.session_id); let verification_output = format_verification_results(&verification, ctx.session_id);
return Ok(format!( return Ok(format!(
"✅ Plan updated: {}\n{}\n{}", "✅ Plan updated: {}\n{}\n{}\n{}",
plan.status_summary(), plan.status_summary(),
verification_output, verification_output,
rulespec_section rulespec_section,
envelope_section
)); ));
} }
Ok(format!( Ok(format!(
"✅ Plan updated: {}\n{}", "✅ Plan updated: {}\n{}\n{}",
plan.status_summary(), plan.status_summary(),
rulespec_section rulespec_section,
envelope_section
)) ))
} }