1use super::{TaskBuildOptions, resolve_task_build_settings};
23use crate::commands::run::PhaseType;
24use crate::contracts::ProjectType;
25use crate::{config, prompts, queue, runner, runutil, timeutil};
26use anyhow::{Context, Result, bail};
27
28pub fn build_task(resolved: &config::Resolved, opts: TaskBuildOptions) -> Result<()> {
29 build_task_impl(resolved, opts, true)
30}
31
32pub fn build_task_without_lock(resolved: &config::Resolved, opts: TaskBuildOptions) -> Result<()> {
33 build_task_impl(resolved, opts, false)
34}
35
36fn build_task_impl(
37 resolved: &config::Resolved,
38 mut opts: TaskBuildOptions,
39 acquire_lock: bool,
40) -> Result<()> {
41 let _queue_lock = if acquire_lock {
42 Some(queue::acquire_queue_lock(
43 &resolved.repo_root,
44 "task",
45 opts.force,
46 )?)
47 } else {
48 None
49 };
50
51 if opts.request.trim().is_empty() {
52 bail!("Missing request: task requires a request description. Provide a non-empty request.");
53 }
54
55 let mut template_context = String::new();
57 if let Some(template_name) = opts.template_hint.clone() {
58 let load_result = crate::template::load_template_with_context(
60 &template_name,
61 &resolved.repo_root,
62 opts.template_target.as_deref(),
63 opts.strict_templates,
64 );
65
66 match load_result {
67 Ok(loaded) => {
68 for warning in &loaded.warnings {
70 log::warn!("Template '{}': {}", template_name, warning);
71 }
72
73 crate::template::merge_template_with_options(&loaded.task, &mut opts);
74 template_context = crate::template::format_template_context(&loaded.task);
75 log::info!("Using template '{}' for task creation", template_name);
76 }
77 Err(e) => {
78 if opts.strict_templates {
79 bail!(
80 "Template '{}' failed strict validation: {}",
81 template_name,
82 e
83 );
84 } else {
85 log::warn!("Failed to load template '{}': {}", template_name, e);
86 }
87 }
88 }
89 }
90
91 let before = queue::load_queue(&resolved.queue_path)
92 .with_context(|| format!("read queue {}", resolved.queue_path.display()))?;
93
94 let insert_index = queue::suggest_new_task_insert_index(&before);
96
97 let done = queue::load_queue_or_default(&resolved.done_path)
98 .with_context(|| format!("read done {}", resolved.done_path.display()))?;
99 let done_ref = if done.tasks.is_empty() && !resolved.done_path.exists() {
100 None
101 } else {
102 Some(&done)
103 };
104 let max_depth = resolved.config.queue.max_dependency_depth.unwrap_or(10);
105 queue::validate_queue_set(
106 &before,
107 done_ref,
108 &resolved.id_prefix,
109 resolved.id_width,
110 max_depth,
111 )
112 .context("validate queue set before task")?;
113 let before_ids = queue::task_id_set(&before);
114
115 let template = prompts::load_task_builder_prompt(&resolved.repo_root)?;
116 let project_type = resolved.config.project_type.unwrap_or(ProjectType::Code);
117 let mut prompt = prompts::render_task_builder_prompt(
118 &template,
119 &opts.request,
120 &opts.hint_tags,
121 &opts.hint_scope,
122 project_type,
123 &resolved.config,
124 )?;
125
126 if !template_context.is_empty() {
128 prompt.push_str("\n\n--- Template Suggestions ---\n");
129 prompt.push_str(&template_context);
130 }
131
132 prompt = prompts::wrap_with_repoprompt_requirement(&prompt, opts.repoprompt_tool_injection);
133 prompt = prompts::wrap_with_instruction_files(&resolved.repo_root, &prompt, &resolved.config)?;
134
135 let settings = resolve_task_build_settings(resolved, &opts)?;
136 let bins = runner::resolve_binaries(&resolved.config.agent);
137 let retry_policy = runutil::RunnerRetryPolicy::from_config(&resolved.config.agent.runner_retry)
140 .unwrap_or_default();
141
142 let _output = runutil::run_prompt_with_handling(
143 runutil::RunnerInvocation {
144 settings: runutil::RunnerSettings {
145 repo_root: &resolved.repo_root,
146 runner_kind: settings.runner,
147 bins,
148 model: settings.model,
149 reasoning_effort: settings.reasoning_effort,
150 runner_cli: settings.runner_cli,
151 timeout: None,
152 permission_mode: settings.permission_mode,
153 output_handler: None,
154 output_stream: runner::OutputStream::Terminal,
155 },
156 execution: runutil::RunnerExecutionContext {
157 prompt: &prompt,
158 phase_type: PhaseType::SinglePhase,
159 session_id: None,
160 },
161 failure: runutil::RunnerFailureHandling {
162 revert_on_error: false,
163 git_revert_mode: resolved
164 .config
165 .agent
166 .git_revert_mode
167 .unwrap_or(crate::contracts::GitRevertMode::Ask),
168 revert_prompt: None,
169 },
170 retry: runutil::RunnerRetryState {
171 policy: retry_policy,
172 },
173 },
174 runutil::RunnerErrorMessages {
175 log_label: "task builder",
176 interrupted_msg: "Task builder interrupted: the agent run was canceled.",
177 timeout_msg: "Task builder timed out: the agent run exceeded the time limit. Changes in the working tree were NOT reverted; review the repo state manually.",
178 terminated_msg: "Task builder terminated: the agent was stopped by a signal. Review uncommitted changes before rerunning.",
179 non_zero_msg: |code| {
180 format!(
181 "Task builder failed: the agent exited with a non-zero code ({}). Review uncommitted changes before rerunning.",
182 code
183 )
184 },
185 other_msg: |err| {
186 format!(
187 "Task builder failed: the agent could not be started or encountered an error. Error: {:#}",
188 err
189 )
190 },
191 },
192 )?;
193
194 let mut after = match queue::load_queue(&resolved.queue_path)
195 .with_context(|| format!("read queue {}", resolved.queue_path.display()))
196 {
197 Ok(queue) => queue,
198 Err(err) => {
199 return Err(err);
200 }
201 };
202
203 let done_after = queue::load_queue_or_default(&resolved.done_path)
204 .with_context(|| format!("read done {}", resolved.done_path.display()))?;
205 let done_after_ref = if done_after.tasks.is_empty() && !resolved.done_path.exists() {
206 None
207 } else {
208 Some(&done_after)
209 };
210 queue::validate_queue_set(
211 &after,
212 done_after_ref,
213 &resolved.id_prefix,
214 resolved.id_width,
215 max_depth,
216 )
217 .context("validate queue set after task")?;
218
219 let added = queue::added_tasks(&before_ids, &after);
220 if !added.is_empty() {
221 let added_ids: Vec<String> = added.iter().map(|(id, _)| id.clone()).collect();
222
223 queue::reposition_new_tasks(&mut after, &added_ids, insert_index);
225
226 let now = timeutil::now_utc_rfc3339_or_fallback();
227 let default_request = opts.request.clone();
228 queue::backfill_missing_fields(&mut after, &added_ids, &default_request, &now);
229
230 if let Some(estimated) = opts.estimated_minutes {
232 for task in &mut after.tasks {
233 if added_ids.contains(&task.id) {
234 task.estimated_minutes = Some(estimated);
235 }
236 }
237 }
238
239 queue::save_queue(&resolved.queue_path, &after)
240 .context("save queue with backfilled fields")?;
241 }
242 if added.is_empty() {
243 log::info!("Task builder completed. No new tasks detected.");
244 } else {
245 log::info!("Task builder added {} task(s):", added.len());
246 for (id, title) in added.iter().take(10) {
247 log::info!("- {}: {}", id, title);
248 }
249 if added.len() > 10 {
250 log::info!("...and {} more.", added.len() - 10);
251 }
252 }
253 Ok(())
254}