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
|
// 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_"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user