ito_core/ralph/
task_sources.rs1use crate::errors::{CoreError, CoreResult};
2use serde::Deserialize;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum RalphTaskSource {
9 Markdown {
11 path: PathBuf,
13 line_index: usize,
15 task: String,
17 },
18 Yaml {
20 path: PathBuf,
22 index: usize,
24 task: String,
26 parallel_group: u32,
28 },
29 Github {
31 repo: String,
33 issue_number: u64,
35 task: String,
37 },
38}
39
40impl RalphTaskSource {
41 pub fn build_prompt(&self, base_prompt: &str) -> String {
43 let task_block = match self {
44 Self::Markdown { path, task, .. } => format!(
45 "## External Task Source\n- Type: markdown\n- Path: {}\n\n## Pending Task\n{}",
46 path.display(),
47 task
48 ),
49 Self::Yaml { path, task, .. } => format!(
50 "## External Task Source\n- Type: yaml\n- Path: {}\n\n## Pending Task\n{}",
51 path.display(),
52 task
53 ),
54 Self::Github {
55 repo,
56 issue_number,
57 task,
58 } => format!(
59 "## External Task Source\n- Type: github\n- Repo: {}\n- Issue: #{}\n\n## Pending Task\n{}",
60 repo, issue_number, task
61 ),
62 };
63
64 if base_prompt.trim().is_empty() {
65 return task_block;
66 }
67
68 format!("{task_block}\n\n---\n\n{base_prompt}")
69 }
70}
71
72pub fn resolve_markdown_task_sources(path: &Path) -> CoreResult<Vec<RalphTaskSource>> {
74 let contents = std::fs::read_to_string(path).map_err(|err| {
75 CoreError::io(
76 format!("Failed to read markdown task file {}", path.display()),
77 err,
78 )
79 })?;
80 let mut tasks = Vec::new();
81
82 for (line_index, line) in contents.lines().enumerate() {
83 let Some(task) = pending_markdown_task(line) else {
84 continue;
85 };
86 tasks.push(RalphTaskSource::Markdown {
87 path: path.to_path_buf(),
88 line_index,
89 task,
90 });
91 }
92
93 Ok(tasks)
94}
95
96pub fn resolve_yaml_task_sources(path: &Path) -> CoreResult<Vec<RalphTaskSource>> {
98 let contents = std::fs::read_to_string(path).map_err(|err| {
99 CoreError::io(
100 format!("Failed to read YAML task file {}", path.display()),
101 err,
102 )
103 })?;
104 let parsed: RalphYamlTasks = serde_yaml::from_str(&contents).map_err(|err| {
105 CoreError::serde(
106 format!("Failed to parse YAML task file {}", path.display()),
107 err.to_string(),
108 )
109 })?;
110
111 let mut tasks = Vec::new();
112 for (index, task) in parsed.tasks.into_iter().enumerate() {
113 if task.completed.unwrap_or(false) {
114 continue;
115 }
116 tasks.push(RalphTaskSource::Yaml {
117 path: path.to_path_buf(),
118 index,
119 task: task.title,
120 parallel_group: task.parallel_group.unwrap_or(0),
121 });
122 }
123
124 Ok(tasks)
125}
126
127pub fn resolve_github_task_sources(
129 repo: &str,
130 label: Option<&str>,
131) -> CoreResult<Vec<RalphTaskSource>> {
132 let mut command = Command::new("gh");
133 command
134 .arg("issue")
135 .arg("list")
136 .arg("--repo")
137 .arg(repo)
138 .arg("--state")
139 .arg("open")
140 .arg("--json")
141 .arg("number,title");
142 if let Some(label) = label {
143 command.arg("--label").arg(label);
144 }
145
146 let output = command.output().map_err(|err| {
147 CoreError::process(format!(
148 "Failed to run `gh issue list` for repository {repo}: {err}"
149 ))
150 })?;
151 if !output.status.success() {
152 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
153 return Err(CoreError::process(format!(
154 "`gh issue list` failed for repository {repo}: {stderr}"
155 )));
156 }
157
158 let issues: Vec<GitHubIssue> = serde_json::from_slice(&output.stdout).map_err(|err| {
159 CoreError::serde(
160 format!("Failed to parse `gh issue list` output for repository {repo}"),
161 err.to_string(),
162 )
163 })?;
164
165 Ok(issues
166 .into_iter()
167 .map(|issue| RalphTaskSource::Github {
168 repo: repo.to_string(),
169 issue_number: issue.number,
170 task: issue.title,
171 })
172 .collect())
173}
174
175fn pending_markdown_task(line: &str) -> Option<String> {
176 let trimmed = line.trim_start();
177 let task = trimmed
178 .strip_prefix("- [ ] ")
179 .or_else(|| trimmed.strip_prefix("* [ ] "))?;
180 let task = task.trim();
181 if task.is_empty() {
182 return None;
183 }
184 Some(task.to_string())
185}
186
187#[derive(Debug, Deserialize)]
188struct RalphYamlTasks {
189 tasks: Vec<RalphYamlTask>,
190}
191
192#[derive(Debug, Deserialize)]
193struct RalphYamlTask {
194 title: String,
195 completed: Option<bool>,
196 parallel_group: Option<u32>,
197}
198
199#[derive(Debug, Deserialize)]
200struct GitHubIssue {
201 number: u64,
202 title: String,
203}