Skip to main content

ralph/cli/queue/
shared.rs

1//! Shared queue CLI enums and conversions.
2//!
3//! Responsibilities:
4//! - Define shared clap enums used by queue/task commands.
5//! - Provide lightweight conversions for report/status types.
6//! - Provide shared ETA computation helpers for queue commands.
7//!
8//! Not handled here:
9//! - Command handlers or IO.
10//! - Business logic for queue mutations or reporting.
11//! - Actual ETA calculation logic (see `crate::eta_calculator`).
12//!
13//! Invariants/assumptions:
14//! - Enum variants map 1:1 with CLI strings.
15//! - Conversions are lossless and do not validate data.
16//! - ETA display uses execution history only (no heuristics).
17
18use clap::ValueEnum;
19
20use crate::{contracts, reports};
21
22#[derive(Clone, Copy, Debug, ValueEnum)]
23#[clap(rename_all = "snake_case")]
24pub enum StatusArg {
25    /// Task is a draft and not ready to run.
26    Draft,
27    /// Task is waiting to be started.
28    Todo,
29    /// Task is currently being worked on.
30    Doing,
31    /// Task is complete.
32    Done,
33    /// Task was rejected (dependents can proceed).
34    Rejected,
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
38#[clap(rename_all = "snake_case")]
39pub enum QueueShowFormat {
40    /// Full JSON representation of the task.
41    Json,
42    /// Compact tab-separated summary (ID, status, title).
43    Compact,
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
47#[clap(rename_all = "snake_case")]
48pub enum QueueListFormat {
49    /// Compact tab-separated summary (ID, status, title).
50    Compact,
51    /// Detailed tab-separated format including tags, scope, and timestamps.
52    Long,
53    /// JSON array of task objects (same shape as queue export).
54    Json,
55}
56
57#[derive(Clone, Copy, Debug, ValueEnum)]
58#[clap(rename_all = "snake_case")]
59pub enum QueueReportFormat {
60    /// Human-readable report output.
61    Text,
62    /// JSON output for scripting.
63    Json,
64}
65
66#[derive(Clone, Copy, Debug, ValueEnum)]
67#[clap(rename_all = "snake_case")]
68pub enum QueueExportFormat {
69    /// Comma-separated values (CSV) format.
70    Csv,
71    /// Tab-separated values (TSV) format.
72    Tsv,
73    /// JSON format (array of task objects).
74    Json,
75    /// Markdown table format for human-readable output.
76    Md,
77    /// GitHub issue format optimized for issue bodies.
78    Gh,
79}
80
81/// Import format for `ralph queue import`.
82#[derive(Clone, Copy, Debug, ValueEnum)]
83#[clap(rename_all = "snake_case")]
84pub enum QueueImportFormat {
85    /// Comma-separated values (CSV) format.
86    Csv,
87    /// Tab-separated values (TSV) format.
88    Tsv,
89    /// JSON format (array of task objects).
90    Json,
91}
92
93/// Sort-by field for `ralph queue sort` (reorders queue file).
94///
95/// Intentionally conservative: only supports priority to avoid dangerous
96/// "reorder by arbitrary field" footguns that mutate queue.json.
97#[derive(Clone, Copy, Debug, ValueEnum)]
98#[clap(rename_all = "snake_case")]
99pub enum QueueSortBy {
100    /// Sort by priority.
101    Priority,
102}
103
104impl std::fmt::Display for QueueSortBy {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        match self {
107            QueueSortBy::Priority => f.write_str("priority"),
108        }
109    }
110}
111
112/// Sort-by field for `ralph queue list` (sorts output only).
113///
114/// Supports comprehensive time-based and metadata sorting for triage
115/// without the risks of mutating queue.json ordering.
116#[derive(Clone, Copy, Debug, ValueEnum)]
117#[clap(rename_all = "snake_case")]
118pub enum QueueListSortBy {
119    /// Sort by priority.
120    Priority,
121    /// Sort by created_at timestamp.
122    CreatedAt,
123    /// Sort by updated_at timestamp.
124    UpdatedAt,
125    /// Sort by started_at timestamp.
126    StartedAt,
127    /// Sort by scheduled_start timestamp.
128    ScheduledStart,
129    /// Sort by status lifecycle ordering.
130    Status,
131    /// Sort by title (case-insensitive).
132    Title,
133}
134
135impl std::fmt::Display for QueueListSortBy {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        match self {
138            QueueListSortBy::Priority => f.write_str("priority"),
139            QueueListSortBy::CreatedAt => f.write_str("created_at"),
140            QueueListSortBy::UpdatedAt => f.write_str("updated_at"),
141            QueueListSortBy::StartedAt => f.write_str("started_at"),
142            QueueListSortBy::ScheduledStart => f.write_str("scheduled_start"),
143            QueueListSortBy::Status => f.write_str("status"),
144            QueueListSortBy::Title => f.write_str("title"),
145        }
146    }
147}
148
149#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
150pub enum QueueSortOrder {
151    Ascending,
152    Descending,
153}
154
155impl QueueSortOrder {
156    pub(crate) fn is_descending(self) -> bool {
157        matches!(self, QueueSortOrder::Descending)
158    }
159}
160
161impl std::fmt::Display for QueueSortOrder {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        match self {
164            QueueSortOrder::Ascending => f.write_str("ascending"),
165            QueueSortOrder::Descending => f.write_str("descending"),
166        }
167    }
168}
169
170impl From<StatusArg> for contracts::TaskStatus {
171    fn from(value: StatusArg) -> Self {
172        match value {
173            StatusArg::Draft => contracts::TaskStatus::Draft,
174            StatusArg::Todo => contracts::TaskStatus::Todo,
175            StatusArg::Doing => contracts::TaskStatus::Doing,
176            StatusArg::Done => contracts::TaskStatus::Done,
177            StatusArg::Rejected => contracts::TaskStatus::Rejected,
178        }
179    }
180}
181
182impl From<QueueReportFormat> for reports::ReportFormat {
183    fn from(value: QueueReportFormat) -> Self {
184        match value {
185            QueueReportFormat::Text => reports::ReportFormat::Text,
186            QueueReportFormat::Json => reports::ReportFormat::Json,
187        }
188    }
189}
190
191/// Compute the ETA display string for a task using execution history.
192///
193/// Returns "n/a" when:
194/// - Task status is not `draft` or `todo` (terminal/in-progress tasks don't need ETA)
195/// - No execution history exists for the resolved (runner, model, phase_count) key
196/// - Runner/model resolution fails
197///
198/// Uses `EtaCalculator::estimate_new_task_total` which only returns estimates
199/// when actual history samples exist (no heuristic fallbacks).
200pub(crate) fn task_eta_display(
201    resolved: &crate::config::Resolved,
202    calculator: &crate::eta_calculator::EtaCalculator,
203    task: &crate::contracts::Task,
204) -> String {
205    use crate::contracts::TaskStatus;
206    use crate::eta_calculator::format_eta;
207    use crate::runner::resolve_agent_settings;
208
209    // Only estimate for non-terminal, not-started tasks
210    if !matches!(task.status, TaskStatus::Draft | TaskStatus::Todo) {
211        return "n/a".to_string();
212    }
213
214    // Resolve runner/model using same precedence as runtime (no CLI overrides)
215    let empty_cli_patch = crate::contracts::RunnerCliOptionsPatch::default();
216    let settings = match resolve_agent_settings(
217        None, // runner_override
218        None, // model_override
219        None, // effort_override
220        &empty_cli_patch,
221        task.agent.as_ref(),
222        &resolved.config.agent,
223    ) {
224        Ok(s) => s,
225        Err(_) => return "n/a".to_string(),
226    };
227
228    let phase_count = resolved.config.agent.phases.unwrap_or(3);
229
230    // Get estimate from calculator (returns None if no history)
231    match calculator.estimate_new_task_total(
232        settings.runner.as_str(),
233        settings.model.as_str(),
234        phase_count,
235    ) {
236        Some(estimate) => format_eta(estimate.remaining),
237        None => "n/a".to_string(),
238    }
239}