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 repo_root: &resolved.repo_root,
145 runner_kind: settings.runner,
146 bins,
147 model: settings.model,
148 reasoning_effort: settings.reasoning_effort,
149 runner_cli: settings.runner_cli,
150 prompt: &prompt,
151 timeout: None,
152 permission_mode: settings.permission_mode,
153 revert_on_error: false,
154 git_revert_mode: resolved
155 .config
156 .agent
157 .git_revert_mode
158 .unwrap_or(crate::contracts::GitRevertMode::Ask),
159 output_handler: None,
160 output_stream: runner::OutputStream::Terminal,
161 revert_prompt: None,
162 phase_type: PhaseType::SinglePhase,
163 session_id: None,
164 retry_policy,
165 },
166 runutil::RunnerErrorMessages {
167 log_label: "task builder",
168 interrupted_msg: "Task builder interrupted: the agent run was canceled.",
169 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.",
170 terminated_msg: "Task builder terminated: the agent was stopped by a signal. Review uncommitted changes before rerunning.",
171 non_zero_msg: |code| {
172 format!(
173 "Task builder failed: the agent exited with a non-zero code ({}). Review uncommitted changes before rerunning.",
174 code
175 )
176 },
177 other_msg: |err| {
178 format!(
179 "Task builder failed: the agent could not be started or encountered an error. Error: {:#}",
180 err
181 )
182 },
183 },
184 )?;
185
186 let mut after = match queue::load_queue(&resolved.queue_path)
187 .with_context(|| format!("read queue {}", resolved.queue_path.display()))
188 {
189 Ok(queue) => queue,
190 Err(err) => {
191 return Err(err);
192 }
193 };
194
195 let done_after = queue::load_queue_or_default(&resolved.done_path)
196 .with_context(|| format!("read done {}", resolved.done_path.display()))?;
197 let done_after_ref = if done_after.tasks.is_empty() && !resolved.done_path.exists() {
198 None
199 } else {
200 Some(&done_after)
201 };
202 queue::validate_queue_set(
203 &after,
204 done_after_ref,
205 &resolved.id_prefix,
206 resolved.id_width,
207 max_depth,
208 )
209 .context("validate queue set after task")?;
210
211 let added = queue::added_tasks(&before_ids, &after);
212 if !added.is_empty() {
213 let added_ids: Vec<String> = added.iter().map(|(id, _)| id.clone()).collect();
214
215 queue::reposition_new_tasks(&mut after, &added_ids, insert_index);
217
218 let now = timeutil::now_utc_rfc3339_or_fallback();
219 let default_request = opts.request.clone();
220 queue::backfill_missing_fields(&mut after, &added_ids, &default_request, &now);
221
222 if let Some(estimated) = opts.estimated_minutes {
224 for task in &mut after.tasks {
225 if added_ids.contains(&task.id) {
226 task.estimated_minutes = Some(estimated);
227 }
228 }
229 }
230
231 queue::save_queue(&resolved.queue_path, &after)
232 .context("save queue with backfilled fields")?;
233 }
234 if added.is_empty() {
235 log::info!("Task builder completed. No new tasks detected.");
236 } else {
237 log::info!("Task builder added {} task(s):", added.len());
238 for (id, title) in added.iter().take(10) {
239 log::info!("- {}: {}", id, title);
240 }
241 if added.len() > 10 {
242 log::info!("...and {} more.", added.len() - 10);
243 }
244 }
245 Ok(())
246}