From 0a6698695505736a0d408e3e2769d09b6ecbc22c Mon Sep 17 00:00:00 2001 From: wren-nutthouse Date: Tue, 17 Mar 2026 06:13:02 +1100 Subject: [PATCH 1/3] feat: surface SDLC ledger summary during resume Refs #30 --- src/cli/run.rs | 4 ++++ src/state/mod.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/cli/run.rs b/src/cli/run.rs index 74a76f6..b2af709 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -5,6 +5,7 @@ use crate::automation::{ }; use crate::config::{GlobalConfig, TuttiConfig}; use crate::error::{Result, TuttiError}; +use crate::state::{load_sdlc_run_ledger, sdlc_pr_comment_summary}; use crate::{budget, budget::BudgetGuardOutcome}; use comfy_table::{Table, presets::UTF8_BORDERS_ONLY}; use serde::Serialize; @@ -89,6 +90,9 @@ pub fn run( if !plan.is_empty() { print_resume_plan(&ctx.run_id, &plan); } + if let Some(ledger) = load_sdlc_run_ledger(project_root, &ctx.run_id)? { + eprintln!("{}", sdlc_pr_comment_summary(&ledger)); + } } if dry_run { diff --git a/src/state/mod.rs b/src/state/mod.rs index 0cf5e1e..e18201d 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -204,6 +204,39 @@ pub struct SdlcRunLedgerRecord { pub transitions: Vec, } +pub fn sdlc_pr_comment_summary(ledger: &SdlcRunLedgerRecord) -> String { + let mut out = String::new(); + out.push_str(&format!( + "SDLC run `{}` for #{} is currently `{:?}` (updated {} by {}).\n", + ledger.run_id, + ledger.issue_number, + ledger.state, + ledger.updated_at.to_rfc3339(), + ledger.actor + )); + if ledger.transitions.is_empty() { + out.push_str("No transitions recorded yet."); + return out; + } + + out.push_str("\nTransitions:\n"); + for transition in &ledger.transitions { + out.push_str(&format!( + "- {:?} → {:?} @ {} by {}{}\n", + transition.from, + transition.to, + transition.timestamp.to_rfc3339(), + transition.actor, + transition + .reason + .as_ref() + .map(|r| format!(" ({r})")) + .unwrap_or_default() + )); + } + out.trim_end().to_string() +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ActivityState { @@ -1191,4 +1224,30 @@ mod tests { let bad = validate_step_id("step*1").unwrap_err(); assert!(bad.to_string().contains("only [A-Za-z0-9_-] allowed")); } + + #[test] + fn sdlc_pr_comment_summary_renders_transitions() { + let now = Utc::now(); + let ledger = SdlcRunLedgerRecord { + run_id: "run-ledger-summary".to_string(), + issue_number: 30, + repository: "nutthouse/tutti".to_string(), + workflow_name: "readiness".to_string(), + state: SdlcRunState::Tested, + updated_at: now, + actor: "wren".to_string(), + transitions: vec![SdlcTransitionRecord { + from: SdlcRunState::Implemented, + to: SdlcRunState::Tested, + timestamp: now, + actor: "wren".to_string(), + reason: Some("tests passed".to_string()), + }], + }; + + let summary = sdlc_pr_comment_summary(&ledger); + assert!(summary.contains("run-ledger-summary")); + assert!(summary.contains("Transitions:")); + assert!(summary.contains("tests passed")); + } } From acf7735540ff2fa27b1e82cffa6696d2808515ac Mon Sep 17 00:00:00 2001 From: wren-nutthouse Date: Tue, 17 Mar 2026 06:20:19 +1100 Subject: [PATCH 2/3] fix: make resume ledger loading best-effort --- src/cli/run.rs | 16 ++++++++++++++-- src/state/mod.rs | 8 ++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/cli/run.rs b/src/cli/run.rs index b2af709..f757733 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -90,8 +90,20 @@ pub fn run( if !plan.is_empty() { print_resume_plan(&ctx.run_id, &plan); } - if let Some(ledger) = load_sdlc_run_ledger(project_root, &ctx.run_id)? { - eprintln!("{}", sdlc_pr_comment_summary(&ledger)); + match load_sdlc_run_ledger(project_root, &ctx.run_id) { + Ok(Some(ledger)) => match sdlc_pr_comment_summary(&ledger) { + Ok(summary) => eprintln!("{summary}"), + Err(err) => { + eprintln!("warn: failed to format SDLC run ledger summary: {err}"); + } + }, + Ok(None) => {} + Err(err) => { + eprintln!( + "warn: failed to load SDLC run ledger for '{}': {err}", + ctx.run_id + ); + } } } diff --git a/src/state/mod.rs b/src/state/mod.rs index e18201d..30e4e7b 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -204,7 +204,7 @@ pub struct SdlcRunLedgerRecord { pub transitions: Vec, } -pub fn sdlc_pr_comment_summary(ledger: &SdlcRunLedgerRecord) -> String { +pub fn sdlc_pr_comment_summary(ledger: &SdlcRunLedgerRecord) -> Result { let mut out = String::new(); out.push_str(&format!( "SDLC run `{}` for #{} is currently `{:?}` (updated {} by {}).\n", @@ -216,7 +216,7 @@ pub fn sdlc_pr_comment_summary(ledger: &SdlcRunLedgerRecord) -> String { )); if ledger.transitions.is_empty() { out.push_str("No transitions recorded yet."); - return out; + return Ok(out); } out.push_str("\nTransitions:\n"); @@ -234,7 +234,7 @@ pub fn sdlc_pr_comment_summary(ledger: &SdlcRunLedgerRecord) -> String { .unwrap_or_default() )); } - out.trim_end().to_string() + Ok(out.trim_end().to_string()) } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -1245,7 +1245,7 @@ mod tests { }], }; - let summary = sdlc_pr_comment_summary(&ledger); + let summary = sdlc_pr_comment_summary(&ledger).unwrap(); assert!(summary.contains("run-ledger-summary")); assert!(summary.contains("Transitions:")); assert!(summary.contains("tests passed")); From b90afb218c8b2de4f962c799a2bc926ae29118f4 Mon Sep 17 00:00:00 2001 From: wren-nutthouse Date: Tue, 17 Mar 2026 08:05:21 +1100 Subject: [PATCH 3/3] docs: add rustdoc for SDLC ledger summary APIs --- src/state/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/state/mod.rs b/src/state/mod.rs index 30e4e7b..9e70f18 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -179,6 +179,7 @@ fn with_run_ledger_lock(project_root: &Path, op: impl FnOnce() -> Result) op() } +/// A single state transition recorded for an SDLC run. #[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SdlcTransitionRecord { @@ -190,6 +191,7 @@ pub struct SdlcTransitionRecord { pub reason: Option, } +/// Persisted state for an SDLC-tracked run, including transition history. #[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SdlcRunLedgerRecord { @@ -204,6 +206,10 @@ pub struct SdlcRunLedgerRecord { pub transitions: Vec, } +/// Render a reusable PR comment summary for the provided SDLC run ledger. +/// +/// The output includes the current run state and a chronological transition list +/// suitable for posting in PR status updates. pub fn sdlc_pr_comment_summary(ledger: &SdlcRunLedgerRecord) -> Result { let mut out = String::new(); out.push_str(&format!(