Skip to main content

agentstatedeveloper_cli/commands/
workflow.rs

1//! `asd workflow` — view task workflow session history.
2//!
3//! Reads `.asd/workflow-sessions.jsonl` (written by `asd task-close`) and
4//! presents the most recent sessions either as a summary table (default) or
5//! as raw JSON (`--json`).
6//!
7//! ## Usage
8//!
9//! ```text
10//! asd workflow [--last <n>] [--json]
11//! ```
12
13use anyhow::Result;
14use clap::Args;
15use serde_json::Value;
16
17use crate::config::Config;
18
19#[derive(Debug, Args)]
20pub struct WorkflowArgs {
21    /// Number of recent sessions to show (default: 20).
22    #[arg(long, default_value = "20")]
23    pub last: usize,
24
25    /// Emit raw JSON array instead of the summary table.
26    #[arg(long)]
27    pub json: bool,
28}
29
30pub fn run(cfg: &Config, args: WorkflowArgs) -> Result<()> {
31    let dot_asd = cfg
32        .db_path
33        .parent()
34        .filter(|p| !p.as_os_str().is_empty())
35        .map(|p| p.join(".asd"))
36        .unwrap_or_else(|| std::path::PathBuf::from(".asd"));
37
38    let sessions_path = dot_asd.join("workflow-sessions.jsonl");
39
40    let raw = std::fs::read_to_string(&sessions_path).unwrap_or_default();
41    let mut sessions: Vec<Value> = raw
42        .lines()
43        .filter(|l| !l.trim().is_empty())
44        .filter_map(|l| serde_json::from_str(l).ok())
45        .collect();
46
47    // Most-recent first.
48    sessions.reverse();
49    sessions.truncate(args.last);
50
51    if args.json {
52        let out = serde_json::json!({
53            "sessions": sessions,
54            "count": sessions.len(),
55        });
56        println!("{}", serde_json::to_string_pretty(&out)?);
57        return Ok(());
58    }
59
60    if sessions.is_empty() {
61        eprintln!("asd: no workflow sessions recorded yet — run `asd task-close` first");
62        return Ok(());
63    }
64
65    // Human-readable table.
66    println!(
67        "{:<28}  {:<22}  {:<8}  {:<6}  {}",
68        "closed_at", "workflow_type", "ev_score", "syms", "missing_steps"
69    );
70    println!("{}", "-".repeat(90));
71    for s in &sessions {
72        let closed_at = s.get("closed_at").and_then(Value::as_str).unwrap_or("—");
73        let wf_type = s
74            .get("workflow_type")
75            .and_then(Value::as_str)
76            .unwrap_or("—");
77        let ev_score = s
78            .get("evidence_score")
79            .and_then(Value::as_f64)
80            .map(|f| format!("{:.2}", f))
81            .unwrap_or_else(|| "—".to_string());
82        let syms = s
83            .get("symbols_annotated")
84            .and_then(Value::as_u64)
85            .map(|n| n.to_string())
86            .unwrap_or_else(|| "—".to_string());
87        let missing = s
88            .get("missing_steps")
89            .and_then(Value::as_array)
90            .map(|arr| {
91                arr.iter()
92                    .filter_map(Value::as_str)
93                    .collect::<Vec<_>>()
94                    .join(", ")
95            })
96            .unwrap_or_default();
97        let missing_display = if missing.is_empty() {
98            "—".to_string()
99        } else {
100            missing
101        };
102
103        // Trim closed_at to a readable length.
104        let ts = if closed_at.len() > 27 {
105            &closed_at[..27]
106        } else {
107            closed_at
108        };
109        println!(
110            "{:<28}  {:<22}  {:<8}  {:<6}  {}",
111            ts, wf_type, ev_score, syms, missing_display
112        );
113    }
114
115    Ok(())
116}