Skip to main content

intent_engine/
tasks.rs

1use crate::db::models::{
2    DoneTaskResponse, Event, EventsSummary, NextStepSuggestion, PaginatedTasks, ParentTaskInfo,
3    PickNextResponse, SpawnSubtaskResponse, SubtaskInfo, Task, TaskSortBy, TaskWithEvents,
4    WorkspaceStats, WorkspaceStatus,
5};
6use crate::error::{IntentError, Result};
7use chrono::Utc;
8use sqlx::SqlitePool;
9use std::sync::Arc;
10
11pub use crate::db::models::TaskContext;
12
13/// Result of a delete operation within a transaction
14#[derive(Debug, Clone)]
15pub struct DeleteTaskResult {
16    /// Whether the task was found (false if ID didn't exist)
17    pub found: bool,
18    /// Number of descendant tasks that were cascade-deleted
19    pub descendant_count: i64,
20}
21
22/// Parameter struct for `TaskManager::update_task`.
23/// Only set the fields you want to change; the rest default to `None` (no change).
24#[derive(Debug, Default)]
25pub struct TaskUpdate<'a> {
26    pub name: Option<&'a str>,
27    pub spec: Option<&'a str>,
28    pub parent_id: Option<Option<i64>>,
29    pub status: Option<&'a str>,
30    pub complexity: Option<i32>,
31    pub priority: Option<i32>,
32    pub active_form: Option<&'a str>,
33    pub owner: Option<&'a str>,
34    pub metadata: Option<&'a str>,
35}
36
37pub struct TaskManager<'a> {
38    pool: &'a SqlitePool,
39    notifier: crate::notifications::NotificationSender,
40    cli_notifier: Option<crate::dashboard::cli_notifier::CliNotifier>,
41    project_path: Option<String>,
42}
43
44impl<'a> TaskManager<'a> {
45    pub fn new(pool: &'a SqlitePool) -> Self {
46        Self {
47            pool,
48            notifier: crate::notifications::NotificationSender::new(None),
49            cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
50            project_path: None,
51        }
52    }
53
54    /// Create a TaskManager with project path for CLI notifications
55    pub fn with_project_path(pool: &'a SqlitePool, project_path: String) -> Self {
56        Self {
57            pool,
58            notifier: crate::notifications::NotificationSender::new(None),
59            cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
60            project_path: Some(project_path),
61        }
62    }
63
64    /// Create a TaskManager with WebSocket notification support
65    pub fn with_websocket(
66        pool: &'a SqlitePool,
67        ws_state: Arc<crate::dashboard::websocket::WebSocketState>,
68        project_path: String,
69    ) -> Self {
70        Self {
71            pool,
72            notifier: crate::notifications::NotificationSender::new(Some(ws_state)),
73            cli_notifier: None, // Dashboard context doesn't need CLI notifier
74            project_path: Some(project_path),
75        }
76    }
77
78    /// Internal helper: Notify UI about task creation
79    async fn notify_task_created(&self, task: &Task) {
80        use crate::dashboard::websocket::DatabaseOperationPayload;
81
82        // WebSocket notification (Dashboard context)
83        if let Some(project_path) = &self.project_path {
84            let task_json = match serde_json::to_value(task) {
85                Ok(json) => json,
86                Err(e) => {
87                    tracing::warn!(error = %e, "Failed to serialize task for notification");
88                    return;
89                },
90            };
91
92            let payload =
93                DatabaseOperationPayload::task_created(task.id, task_json, project_path.clone());
94            self.notifier.send(payload).await;
95        }
96
97        // CLI → Dashboard HTTP notification (CLI context)
98        if let Some(cli_notifier) = &self.cli_notifier {
99            cli_notifier
100                .notify_task_changed(Some(task.id), "created", self.project_path.clone())
101                .await;
102        }
103    }
104
105    /// Internal helper: Notify UI about task update
106    async fn notify_task_updated(&self, task: &Task) {
107        use crate::dashboard::websocket::DatabaseOperationPayload;
108
109        // WebSocket notification (Dashboard context)
110        if let Some(project_path) = &self.project_path {
111            let task_json = match serde_json::to_value(task) {
112                Ok(json) => json,
113                Err(e) => {
114                    tracing::warn!(error = %e, "Failed to serialize task for notification");
115                    return;
116                },
117            };
118
119            let payload =
120                DatabaseOperationPayload::task_updated(task.id, task_json, project_path.clone());
121            self.notifier.send(payload).await;
122        }
123
124        // CLI → Dashboard HTTP notification (CLI context)
125        if let Some(cli_notifier) = &self.cli_notifier {
126            cli_notifier
127                .notify_task_changed(Some(task.id), "updated", self.project_path.clone())
128                .await;
129        }
130    }
131
132    /// Internal helper: Notify UI about task deletion
133    async fn notify_task_deleted(&self, task_id: i64) {
134        use crate::dashboard::websocket::DatabaseOperationPayload;
135
136        // WebSocket notification (Dashboard context)
137        if let Some(project_path) = &self.project_path {
138            let payload = DatabaseOperationPayload::task_deleted(task_id, project_path.clone());
139            self.notifier.send(payload).await;
140        }
141
142        // CLI → Dashboard HTTP notification (CLI context)
143        if let Some(cli_notifier) = &self.cli_notifier {
144            cli_notifier
145                .notify_task_changed(Some(task_id), "deleted", self.project_path.clone())
146                .await;
147        }
148    }
149
150    /// Add a new task
151    /// owner: identifies who created the task (e.g. 'human', 'ai', or any custom string)
152    #[tracing::instrument(skip(self), fields(task_name = %name))]
153    pub async fn add_task(
154        &self,
155        name: String,
156        spec: Option<String>,
157        parent_id: Option<i64>,
158        owner: Option<String>,
159        priority: Option<i32>,
160        metadata: Option<String>,
161    ) -> Result<Task> {
162        // Check for circular dependency if parent_id is provided
163        if let Some(pid) = parent_id {
164            self.check_task_exists(pid).await?;
165        }
166
167        let now = Utc::now();
168        let owner = owner.as_deref().unwrap_or("human").to_string();
169
170        let result = sqlx::query(
171            r#"
172            INSERT INTO tasks (name, spec, parent_id, status, first_todo_at, owner, priority, metadata)
173            VALUES (?, ?, ?, 'todo', ?, ?, ?, ?)
174            "#,
175        )
176        .bind(name)
177        .bind(spec)
178        .bind(parent_id)
179        .bind(now)
180        .bind(owner)
181        .bind(priority)
182        .bind(metadata)
183        .execute(self.pool)
184        .await?;
185
186        let id = result.last_insert_rowid();
187        let task = self.get_task(id).await?;
188
189        // Notify WebSocket clients about the new task
190        self.notify_task_created(&task).await;
191
192        Ok(task)
193    }
194
195    // =========================================================================
196    // Transaction-aware methods (for batch operations like PlanExecutor)
197    // These methods do NOT notify - caller is responsible for notifications
198    // =========================================================================
199
200    /// Create a task within a transaction (no notification)
201    ///
202    /// This is used by PlanExecutor for batch operations where:
203    /// - Multiple tasks need atomic creation
204    /// - Notification should happen after all tasks are committed
205    ///
206    /// # Arguments
207    /// * `tx` - The active transaction
208    /// * `name` - Task name
209    /// * `spec` - Optional task specification
210    /// * `priority` - Optional priority (1=critical, 2=high, 3=medium, 4=low)
211    /// * `status` - Optional status string ("todo", "doing", "done")
212    /// * `active_form` - Optional active form description
213    /// * `owner` - Task owner (e.g. "human", "ai", or any custom string)
214    ///
215    /// # Returns
216    /// The ID of the created task
217    #[allow(clippy::too_many_arguments)]
218    pub async fn create_task_in_tx(
219        &self,
220        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
221        name: &str,
222        spec: Option<&str>,
223        priority: Option<i32>,
224        status: Option<&str>,
225        active_form: Option<&str>,
226        owner: &str,
227    ) -> Result<i64> {
228        let now = Utc::now();
229        let status = status.unwrap_or("todo");
230        let priority = priority.unwrap_or(3); // Default: medium
231
232        let result = sqlx::query(
233            r#"
234            INSERT INTO tasks (name, spec, priority, status, active_form, first_todo_at, owner)
235            VALUES (?, ?, ?, ?, ?, ?, ?)
236            "#,
237        )
238        .bind(name)
239        .bind(spec)
240        .bind(priority)
241        .bind(status)
242        .bind(active_form)
243        .bind(now)
244        .bind(owner)
245        .execute(&mut **tx)
246        .await?;
247
248        Ok(result.last_insert_rowid())
249    }
250
251    /// Update a task within a transaction (no notification)
252    ///
253    /// Only updates fields that are Some - supports partial updates.
254    /// Does NOT update name (used for identity) or timestamps.
255    ///
256    /// # Arguments
257    /// * `tx` - The active transaction
258    /// * `task_id` - ID of the task to update
259    /// * `spec` - New spec (if Some)
260    /// * `priority` - New priority (if Some)
261    /// * `status` - New status (if Some)
262    /// * `active_form` - New active form (if Some)
263    pub async fn update_task_in_tx(
264        &self,
265        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
266        task_id: i64,
267        spec: Option<&str>,
268        priority: Option<i32>,
269        status: Option<&str>,
270        active_form: Option<&str>,
271    ) -> Result<()> {
272        // Update spec if provided
273        if let Some(spec) = spec {
274            sqlx::query("UPDATE tasks SET spec = ? WHERE id = ?")
275                .bind(spec)
276                .bind(task_id)
277                .execute(&mut **tx)
278                .await?;
279        }
280
281        // Update priority if provided
282        if let Some(priority) = priority {
283            sqlx::query("UPDATE tasks SET priority = ? WHERE id = ?")
284                .bind(priority)
285                .bind(task_id)
286                .execute(&mut **tx)
287                .await?;
288        }
289
290        // Update status if provided
291        if let Some(status) = status {
292            sqlx::query("UPDATE tasks SET status = ? WHERE id = ?")
293                .bind(status)
294                .bind(task_id)
295                .execute(&mut **tx)
296                .await?;
297        }
298
299        // Update active_form if provided
300        if let Some(active_form) = active_form {
301            sqlx::query("UPDATE tasks SET active_form = ? WHERE id = ?")
302                .bind(active_form)
303                .bind(task_id)
304                .execute(&mut **tx)
305                .await?;
306        }
307
308        Ok(())
309    }
310
311    /// Set parent_id for a task within a transaction (no notification)
312    ///
313    /// Used to establish parent-child relationships after tasks are created.
314    pub async fn set_parent_in_tx(
315        &self,
316        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
317        task_id: i64,
318        parent_id: i64,
319    ) -> Result<()> {
320        sqlx::query("UPDATE tasks SET parent_id = ? WHERE id = ?")
321            .bind(parent_id)
322            .bind(task_id)
323            .execute(&mut **tx)
324            .await?;
325
326        Ok(())
327    }
328
329    /// Clear parent_id for a task in a transaction (make it a root task)
330    ///
331    /// Used when explicitly setting parent_id to null in JSON.
332    pub async fn clear_parent_in_tx(
333        &self,
334        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
335        task_id: i64,
336    ) -> Result<()> {
337        sqlx::query("UPDATE tasks SET parent_id = NULL WHERE id = ?")
338            .bind(task_id)
339            .execute(&mut **tx)
340            .await?;
341
342        Ok(())
343    }
344
345    /// Soft-delete a task and all its descendants within a transaction (no notification).
346    ///
347    /// Used by PlanExecutor for batch delete operations.
348    /// WebSocket notification is sent after transaction commit via notify_batch_changed().
349    ///
350    /// Returns `DeleteTaskResult` with:
351    /// - `found`: whether the task existed and was active
352    /// - `descendant_count`: number of active descendants soft-deleted
353    ///
354    /// Note: Focus protection is handled by the caller (PlanExecutor) BEFORE
355    /// calling this function, using `find_focused_in_subtree_in_tx`.
356    pub async fn delete_task_in_tx(
357        &self,
358        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
359        task_id: i64,
360    ) -> Result<DeleteTaskResult> {
361        // Check if active task exists
362        let task_info: Option<(i64,)> =
363            sqlx::query_as("SELECT id FROM tasks WHERE id = ? AND deleted_at IS NULL")
364                .bind(task_id)
365                .fetch_optional(&mut **tx)
366                .await?;
367
368        if task_info.is_none() {
369            return Ok(DeleteTaskResult {
370                found: false,
371                descendant_count: 0,
372            });
373        }
374
375        // Count active descendants before soft-deleting
376        let descendant_count = self.count_descendants_in_tx(tx, task_id).await?;
377
378        // Soft-delete the entire subtree in one statement via recursive CTE
379        let now = chrono::Utc::now();
380        sqlx::query(
381            r#"
382            WITH RECURSIVE subtree(id) AS (
383                SELECT id FROM tasks WHERE id = ? AND deleted_at IS NULL
384                UNION ALL
385                SELECT t.id FROM tasks t JOIN subtree s ON t.parent_id = s.id
386                WHERE t.deleted_at IS NULL
387            )
388            UPDATE tasks SET deleted_at = ? WHERE id IN (SELECT id FROM subtree)
389            "#,
390        )
391        .bind(task_id)
392        .bind(now)
393        .execute(&mut **tx)
394        .await?;
395
396        Ok(DeleteTaskResult {
397            found: true,
398            descendant_count,
399        })
400    }
401
402    /// Count all active descendants of a task (children, grandchildren, etc.)
403    async fn count_descendants_in_tx(
404        &self,
405        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
406        task_id: i64,
407    ) -> Result<i64> {
408        // Use recursive CTE to count all active descendants
409        let count: (i64,) = sqlx::query_as(
410            r#"
411            WITH RECURSIVE descendants AS (
412                SELECT id FROM tasks WHERE parent_id = ? AND deleted_at IS NULL
413                UNION ALL
414                SELECT t.id FROM tasks t
415                INNER JOIN descendants d ON t.parent_id = d.id
416                WHERE t.deleted_at IS NULL
417            )
418            SELECT COUNT(*) FROM descendants
419            "#,
420        )
421        .bind(task_id)
422        .fetch_one(&mut **tx)
423        .await?;
424
425        Ok(count.0)
426    }
427
428    /// Find if a task or any of its descendants is ANY session's focus
429    ///
430    /// This is critical for delete protection: deleting a parent task cascades
431    /// to all descendants, so we must check the entire subtree for focus.
432    ///
433    /// Focus protection is GLOBAL - a task focused by any session cannot be deleted.
434    /// This prevents one session from accidentally breaking another session's work.
435    ///
436    /// Returns `Some((task_id, session_id))` if any task in the subtree is focused,
437    /// `None` if no focus found in the subtree.
438    pub async fn find_focused_in_subtree_in_tx(
439        &self,
440        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
441        task_id: i64,
442    ) -> Result<Option<(i64, String)>> {
443        // Use recursive CTE to get all active task IDs in the subtree (including the root)
444        // Then check if any of them is focused by ANY session
445        let row: Option<(i64, String)> = sqlx::query_as(
446            r#"
447            WITH RECURSIVE subtree AS (
448                SELECT id FROM tasks WHERE id = ? AND deleted_at IS NULL
449                UNION ALL
450                SELECT t.id FROM tasks t
451                INNER JOIN subtree s ON t.parent_id = s.id
452                WHERE t.deleted_at IS NULL
453            )
454            SELECT s.current_task_id, s.session_id FROM sessions s
455            WHERE s.current_task_id IN (SELECT id FROM subtree)
456            LIMIT 1
457            "#,
458        )
459        .bind(task_id)
460        .fetch_optional(&mut **tx)
461        .await?;
462
463        Ok(row)
464    }
465
466    /// Count incomplete children of a task within a transaction
467    ///
468    /// Returns the number of child tasks that are not in 'done' status.
469    /// Used to validate that all children are complete before marking parent as done.
470    pub async fn count_incomplete_children_in_tx(
471        &self,
472        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
473        task_id: i64,
474    ) -> Result<i64> {
475        let count: (i64,) = sqlx::query_as(crate::sql_constants::COUNT_INCOMPLETE_CHILDREN)
476            .bind(task_id)
477            .fetch_one(&mut **tx)
478            .await?;
479
480        Ok(count.0)
481    }
482
483    /// Complete a task within a transaction (core business logic)
484    ///
485    /// This is the single source of truth for task completion logic:
486    /// - Validates all children are complete
487    /// - Updates status to 'done'
488    /// - Sets first_done_at timestamp
489    ///
490    /// Called by both `done_task()` and `PlanExecutor`.
491    pub async fn complete_task_in_tx(
492        &self,
493        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
494        task_id: i64,
495    ) -> Result<()> {
496        // Check if all children are done
497        let incomplete_count = self.count_incomplete_children_in_tx(tx, task_id).await?;
498        if incomplete_count > 0 {
499            return Err(IntentError::UncompletedChildren);
500        }
501
502        // Update task status to done
503        let now = chrono::Utc::now();
504        sqlx::query(
505            r#"
506            UPDATE tasks
507            SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
508            WHERE id = ?
509            "#,
510        )
511        .bind(now)
512        .bind(task_id)
513        .execute(&mut **tx)
514        .await?;
515
516        Ok(())
517    }
518
519    /// Notify Dashboard about a batch operation
520    ///
521    /// Call this after committing a transaction that created/updated multiple tasks.
522    /// Sends a single "batch_update" notification instead of per-task notifications.
523    pub async fn notify_batch_changed(&self) {
524        if let Some(cli_notifier) = &self.cli_notifier {
525            cli_notifier
526                .notify_task_changed(None, "batch_update", self.project_path.clone())
527                .await;
528        }
529    }
530
531    // =========================================================================
532    // End of transaction-aware methods
533    // =========================================================================
534
535    /// Get a task by ID
536    #[tracing::instrument(skip(self))]
537    pub async fn get_task(&self, id: i64) -> Result<Task> {
538        let task = sqlx::query_as::<_, Task>(
539            r#"
540            SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
541            FROM tasks
542            WHERE id = ? AND deleted_at IS NULL
543            "#,
544        )
545        .bind(id)
546        .fetch_optional(self.pool)
547        .await?
548        .ok_or(IntentError::TaskNotFound(id))?;
549
550        Ok(task)
551    }
552
553    /// Get a task with events summary
554    pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
555        let task = self.get_task(id).await?;
556        let events_summary = self.get_events_summary(id).await?;
557
558        Ok(TaskWithEvents {
559            task,
560            events_summary: Some(events_summary),
561        })
562    }
563
564    /// Get full ancestry chain for a task
565    ///
566    /// Returns a vector of tasks from the given task up to the root:
567    /// [task itself, parent, grandparent, ..., root]
568    ///
569    /// Example:
570    /// - Task 42 (parent_id: 55) → [Task 42, Task 55, ...]
571    /// - Task 100 (parent_id: null) → [Task 100]
572    pub async fn get_task_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
573        let mut chain = Vec::new();
574        let mut current_id = Some(task_id);
575
576        while let Some(id) = current_id {
577            let task = self.get_task(id).await?;
578            current_id = task.parent_id;
579            chain.push(task);
580        }
581
582        Ok(chain)
583    }
584
585    /// Get task context - the complete family tree of a task
586    ///
587    /// Returns:
588    /// - task: The requested task
589    /// - ancestors: Parent chain up to root (ordered from immediate parent to root)
590    /// - siblings: Other tasks at the same level (same parent_id)
591    /// - children: Direct subtasks of this task
592    pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
593        let task = self.get_task(id).await?;
594
595        // Get ancestors (walk up parent chain)
596        let mut ancestors = Vec::new();
597        let mut current_parent_id = task.parent_id;
598        while let Some(parent_id) = current_parent_id {
599            let parent = self.get_task(parent_id).await?;
600            current_parent_id = parent.parent_id;
601            ancestors.push(parent);
602        }
603
604        let siblings = self.get_siblings(id, task.parent_id).await?;
605        let children = self.get_children(id).await?;
606        let blocking_tasks = self.get_blocking_tasks(id).await?;
607        let blocked_by_tasks = self.get_blocked_by_tasks(id).await?;
608
609        Ok(TaskContext {
610            task,
611            ancestors,
612            siblings,
613            children,
614            dependencies: crate::db::models::TaskDependencies {
615                blocking_tasks,
616                blocked_by_tasks,
617            },
618        })
619    }
620
621    /// Get sibling tasks (same parent_id, excluding self).
622    pub async fn get_siblings(&self, id: i64, parent_id: Option<i64>) -> Result<Vec<Task>> {
623        if let Some(parent_id) = parent_id {
624            sqlx::query_as::<_, Task>(&format!(
625                "SELECT {} FROM tasks WHERE parent_id = ? AND id != ? AND deleted_at IS NULL ORDER BY priority ASC NULLS LAST, id ASC",
626                crate::sql_constants::TASK_COLUMNS
627            ))
628            .bind(parent_id)
629            .bind(id)
630            .fetch_all(self.pool)
631            .await
632            .map_err(Into::into)
633        } else {
634            sqlx::query_as::<_, Task>(&format!(
635                "SELECT {} FROM tasks WHERE parent_id IS NULL AND id != ? AND deleted_at IS NULL ORDER BY priority ASC NULLS LAST, id ASC",
636                crate::sql_constants::TASK_COLUMNS
637            ))
638            .bind(id)
639            .fetch_all(self.pool)
640            .await
641            .map_err(Into::into)
642        }
643    }
644
645    /// Get direct children of a task.
646    pub async fn get_children(&self, id: i64) -> Result<Vec<Task>> {
647        sqlx::query_as::<_, Task>(&format!(
648            "SELECT {} FROM tasks WHERE parent_id = ? AND deleted_at IS NULL ORDER BY priority ASC NULLS LAST, id ASC",
649            crate::sql_constants::TASK_COLUMNS
650        ))
651        .bind(id)
652        .fetch_all(self.pool)
653        .await
654        .map_err(Into::into)
655    }
656
657    /// Get tasks that this task depends on (blocking tasks).
658    pub async fn get_blocking_tasks(&self, id: i64) -> Result<Vec<Task>> {
659        sqlx::query_as::<_, Task>(&format!(
660            "SELECT {} FROM tasks t \
661             JOIN dependencies d ON t.id = d.blocking_task_id \
662             WHERE d.blocked_task_id = ? AND t.deleted_at IS NULL \
663             ORDER BY t.priority ASC NULLS LAST, t.id ASC",
664            crate::sql_constants::TASK_COLUMNS_PREFIXED
665        ))
666        .bind(id)
667        .fetch_all(self.pool)
668        .await
669        .map_err(Into::into)
670    }
671
672    /// Get tasks that depend on this task (blocked by this task).
673    pub async fn get_blocked_by_tasks(&self, id: i64) -> Result<Vec<Task>> {
674        sqlx::query_as::<_, Task>(&format!(
675            "SELECT {} FROM tasks t \
676             JOIN dependencies d ON t.id = d.blocked_task_id \
677             WHERE d.blocking_task_id = ? AND t.deleted_at IS NULL \
678             ORDER BY t.priority ASC NULLS LAST, t.id ASC",
679            crate::sql_constants::TASK_COLUMNS_PREFIXED
680        ))
681        .bind(id)
682        .fetch_all(self.pool)
683        .await
684        .map_err(Into::into)
685    }
686
687    /// Get all descendants of a task recursively (children, grandchildren, etc.)
688    /// Uses recursive CTE for efficient querying
689    pub async fn get_descendants(&self, task_id: i64) -> Result<Vec<Task>> {
690        let descendants = sqlx::query_as::<_, Task>(
691            r#"
692            WITH RECURSIVE descendants AS (
693                SELECT id, parent_id, name, spec, status, complexity, priority,
694                       first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
695                FROM tasks
696                WHERE parent_id = ? AND deleted_at IS NULL
697
698                UNION ALL
699
700                SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
701                       t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner, t.metadata
702                FROM tasks t
703                INNER JOIN descendants d ON t.parent_id = d.id
704                WHERE t.deleted_at IS NULL
705            )
706            SELECT * FROM descendants
707            ORDER BY parent_id NULLS FIRST, priority ASC NULLS LAST, id ASC
708            "#,
709        )
710        .bind(task_id)
711        .fetch_all(self.pool)
712        .await?;
713
714        Ok(descendants)
715    }
716
717    /// Get status response for a task (the "spotlight" view)
718    /// This is the main method for `ie status` command
719    pub async fn get_status(
720        &self,
721        task_id: i64,
722        with_events: bool,
723    ) -> Result<crate::db::models::StatusResponse> {
724        use crate::db::models::{StatusResponse, TaskBrief};
725
726        // Get task context (reuse existing method)
727        let context = self.get_task_context(task_id).await?;
728
729        // Get all descendants recursively
730        let descendants_full = self.get_descendants(task_id).await?;
731
732        // Convert siblings and descendants to brief format
733        let siblings: Vec<TaskBrief> = context.siblings.iter().map(TaskBrief::from).collect();
734        let descendants: Vec<TaskBrief> = descendants_full.iter().map(TaskBrief::from).collect();
735
736        // Get events if requested
737        let events = if with_events {
738            let event_mgr = crate::events::EventManager::new(self.pool);
739            Some(
740                event_mgr
741                    .list_events(Some(task_id), Some(50), None, None)
742                    .await?,
743            )
744        } else {
745            None
746        };
747
748        Ok(StatusResponse {
749            focused_task: context.task,
750            ancestors: context.ancestors,
751            siblings,
752            descendants,
753            events,
754        })
755    }
756
757    /// Get root tasks (tasks with no parent) for NoFocusResponse
758    pub async fn get_root_tasks(&self) -> Result<Vec<Task>> {
759        let tasks = sqlx::query_as::<_, Task>(
760            r#"
761            SELECT id, parent_id, name, spec, status, complexity, priority,
762                   first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
763            FROM tasks
764            WHERE parent_id IS NULL AND deleted_at IS NULL
765            ORDER BY
766                CASE status
767                    WHEN 'doing' THEN 0
768                    WHEN 'todo' THEN 1
769                    WHEN 'done' THEN 2
770                END,
771                priority ASC NULLS LAST,
772                id ASC
773            "#,
774        )
775        .fetch_all(self.pool)
776        .await?;
777
778        Ok(tasks)
779    }
780
781    /// Get events summary for a task
782    async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
783        let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
784            .bind(task_id)
785            .fetch_one(self.pool)
786            .await?;
787
788        let recent_events = sqlx::query_as::<_, Event>(
789            r#"
790            SELECT id, task_id, timestamp, log_type, discussion_data
791            FROM events
792            WHERE task_id = ?
793            ORDER BY timestamp DESC
794            LIMIT 10
795            "#,
796        )
797        .bind(task_id)
798        .fetch_all(self.pool)
799        .await?;
800
801        Ok(EventsSummary {
802            total_count,
803            recent_events,
804        })
805    }
806
807    /// Update a task
808    pub async fn update_task(&self, id: i64, update: TaskUpdate<'_>) -> Result<Task> {
809        let TaskUpdate {
810            name,
811            spec,
812            parent_id,
813            status,
814            complexity,
815            priority,
816            active_form,
817            owner,
818            metadata,
819        } = update;
820
821        // Check task exists
822        let task = self.get_task(id).await?;
823
824        // Normalize and validate status. Accepts canonical values (todo/doing/done) and
825        // aliases (pending/in_progress/completed). Always stores the canonical form so that
826        // no alias ever reaches the database.
827        let status = if let Some(s) = status {
828            match crate::plan::TaskStatus::from_db_str(s) {
829                Some(ts) => Some(ts.as_db_str()),
830                None => return Err(IntentError::InvalidInput(format!("Invalid status: {}", s))),
831            }
832        } else {
833            None
834        };
835
836        // Check for circular dependency if parent_id is being changed
837        if let Some(Some(pid)) = parent_id {
838            if pid == id {
839                return Err(IntentError::CircularDependency {
840                    blocking_task_id: pid,
841                    blocked_task_id: id,
842                });
843            }
844            self.check_task_exists(pid).await?;
845            self.check_circular_dependency(id, pid).await?;
846        }
847
848        // Build dynamic update query using QueryBuilder for SQL injection safety
849        let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
850            sqlx::QueryBuilder::new("UPDATE tasks SET ");
851        let mut has_updates = false;
852
853        if let Some(n) = name {
854            if has_updates {
855                builder.push(", ");
856            }
857            builder.push("name = ").push_bind(n);
858            has_updates = true;
859        }
860
861        if let Some(s) = spec {
862            if has_updates {
863                builder.push(", ");
864            }
865            builder.push("spec = ").push_bind(s);
866            has_updates = true;
867        }
868
869        if let Some(pid) = parent_id {
870            if has_updates {
871                builder.push(", ");
872            }
873            match pid {
874                Some(p) => {
875                    builder.push("parent_id = ").push_bind(p);
876                },
877                None => {
878                    builder.push("parent_id = NULL");
879                },
880            }
881            has_updates = true;
882        }
883
884        if let Some(c) = complexity {
885            if has_updates {
886                builder.push(", ");
887            }
888            builder.push("complexity = ").push_bind(c);
889            has_updates = true;
890        }
891
892        if let Some(p) = priority {
893            if has_updates {
894                builder.push(", ");
895            }
896            builder.push("priority = ").push_bind(p);
897            has_updates = true;
898        }
899
900        if let Some(af) = active_form {
901            if has_updates {
902                builder.push(", ");
903            }
904            builder.push("active_form = ").push_bind(af);
905            has_updates = true;
906        }
907
908        if let Some(o) = owner {
909            if o.is_empty() {
910                return Err(IntentError::InvalidInput(
911                    "owner cannot be empty".to_string(),
912                ));
913            }
914            if has_updates {
915                builder.push(", ");
916            }
917            builder.push("owner = ").push_bind(o);
918            has_updates = true;
919        }
920
921        if let Some(m) = metadata {
922            if has_updates {
923                builder.push(", ");
924            }
925            builder.push("metadata = ").push_bind(m);
926            has_updates = true;
927        }
928
929        if let Some(s) = status {
930            if has_updates {
931                builder.push(", ");
932            }
933            builder.push("status = ").push_bind(s);
934            has_updates = true;
935
936            // Update timestamp fields based on status
937            let now = Utc::now();
938            let timestamp = now.to_rfc3339();
939            match s {
940                "todo" if task.first_todo_at.is_none() => {
941                    builder.push(", first_todo_at = ").push_bind(timestamp);
942                },
943                "doing" if task.first_doing_at.is_none() => {
944                    builder.push(", first_doing_at = ").push_bind(timestamp);
945                },
946                "done" if task.first_done_at.is_none() => {
947                    builder.push(", first_done_at = ").push_bind(timestamp);
948                },
949                _ => {},
950            }
951        }
952
953        if !has_updates {
954            return Ok(task);
955        }
956
957        builder.push(" WHERE id = ").push_bind(id);
958
959        builder.build().execute(self.pool).await?;
960
961        let task = self.get_task(id).await?;
962
963        // Notify WebSocket clients about the task update
964        self.notify_task_updated(&task).await;
965
966        Ok(task)
967    }
968
969    /// Soft-delete a task. Refuses if the task is focused by any session.
970    ///
971    /// # FTS note
972    ///
973    /// The `tasks_au_softdelete` trigger removes the task from the FTS index on
974    /// the active→deleted transition.  There is intentionally **no restore path**
975    /// in the application layer.  If a `restore_task` function is ever added, it
976    /// must manually re-insert the row into the FTS index:
977    ///
978    /// ```sql
979    /// INSERT INTO tasks_fts(rowid, name, spec) VALUES (?, ?, ?);
980    /// ```
981    ///
982    /// The `tasks_au_active` trigger does NOT fire on a deleted→active transition
983    /// (WHEN clause requires `old.deleted_at IS NULL`), so the application is
984    /// solely responsible for FTS repair on restore.
985    pub async fn delete_task(&self, id: i64) -> Result<()> {
986        self.check_task_exists(id).await?;
987
988        // Focus protection
989        if let Some((_, sid)) = self.find_focused_in_set(&[id]).await? {
990            return Err(IntentError::ActionNotAllowed(format!(
991                "Task #{} is focused by session '{}'. Unfocus it first.",
992                id, sid
993            )));
994        }
995
996        let now = chrono::Utc::now();
997        sqlx::query("UPDATE tasks SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL")
998            .bind(now)
999            .bind(id)
1000            .execute(self.pool)
1001            .await?;
1002
1003        // Notify WebSocket clients about the task deletion
1004        self.notify_task_deleted(id).await;
1005
1006        Ok(())
1007    }
1008
1009    /// Soft-delete a task and all its descendants (cascade).
1010    ///
1011    /// Refuses if any task in the subtree is focused by any session.
1012    /// Returns the number of descendants soft-deleted.
1013    pub async fn delete_task_cascade(&self, id: i64) -> Result<usize> {
1014        let descendants = self.get_descendants(id).await?;
1015
1016        // Focus protection: check the task itself + all descendants
1017        let mut subtree_ids: Vec<i64> = descendants.iter().map(|t| t.id).collect();
1018        subtree_ids.push(id);
1019
1020        if let Some((tid, sid)) = self.find_focused_in_set(&subtree_ids).await? {
1021            return Err(IntentError::ActionNotAllowed(format!(
1022                "Cannot cascade delete: task #{} is focused by session '{}'. Unfocus it first.",
1023                tid, sid
1024            )));
1025        }
1026
1027        // TODO: The focus check above and the UPDATE below are not atomic.
1028        // Another session could focus a subtask in the window between them,
1029        // and we would soft-delete it anyway.  Fix: wrap both operations in
1030        // a single SQLite transaction with serializable isolation before
1031        // enabling multi-user concurrent access.
1032
1033        // Soft-delete the entire subtree in one statement via recursive CTE
1034        let now = chrono::Utc::now();
1035        sqlx::query(
1036            r#"
1037            WITH RECURSIVE subtree(id) AS (
1038                SELECT id FROM tasks WHERE id = ? AND deleted_at IS NULL
1039                UNION ALL
1040                SELECT t.id FROM tasks t JOIN subtree s ON t.parent_id = s.id
1041                WHERE t.deleted_at IS NULL
1042            )
1043            UPDATE tasks SET deleted_at = ? WHERE id IN (SELECT id FROM subtree)
1044            "#,
1045        )
1046        .bind(id)
1047        .bind(now)
1048        .execute(self.pool)
1049        .await?;
1050
1051        // Notify WebSocket clients for every deleted node, not just the root.
1052        // Dashboard subscribers track individual task IDs; cascade-deleted
1053        // descendants must each receive a deletion event or they become stale.
1054        // Notification order: descendants first, then root.  This is intentional:
1055        // a client receiving a child deletion may query the parent; delivering
1056        // the parent deletion last ensures the parent is already marked deleted
1057        // by the time clients re-query it.
1058        for &deleted_id in &subtree_ids {
1059            self.notify_task_deleted(deleted_id).await;
1060        }
1061
1062        // subtree_ids = descendants + root, so this is the true total deleted count.
1063        Ok(subtree_ids.len())
1064    }
1065
1066    /// Check if any task in the given set of IDs is focused by any session.
1067    /// Returns `Some((task_id, session_id))` if found, `None` otherwise.
1068    async fn find_focused_in_set(&self, task_ids: &[i64]) -> Result<Option<(i64, String)>> {
1069        if task_ids.is_empty() {
1070            return Ok(None);
1071        }
1072
1073        // Build parameterized IN clause
1074        let placeholders: Vec<&str> = task_ids.iter().map(|_| "?").collect();
1075        let sql = format!(
1076            "SELECT current_task_id, session_id FROM sessions WHERE current_task_id IN ({}) LIMIT 1",
1077            placeholders.join(", ")
1078        );
1079
1080        let mut query = sqlx::query_as::<_, (i64, String)>(&sql);
1081        for id in task_ids {
1082            query = query.bind(id);
1083        }
1084
1085        Ok(query.fetch_optional(self.pool).await?)
1086    }
1087
1088    /// Add a dependency: blocking_id blocks blocked_id.
1089    pub async fn add_dependency(&self, blocking_id: i64, blocked_id: i64) -> Result<()> {
1090        crate::dependencies::add_dependency(self.pool, blocking_id, blocked_id)
1091            .await
1092            .map(|_| ())
1093    }
1094
1095    /// Remove a dependency: blocking_id no longer blocks blocked_id.
1096    pub async fn remove_dependency(&self, blocking_id: i64, blocked_id: i64) -> Result<()> {
1097        sqlx::query("DELETE FROM dependencies WHERE blocking_task_id = ? AND blocked_task_id = ?")
1098            .bind(blocking_id)
1099            .bind(blocked_id)
1100            .execute(self.pool)
1101            .await?;
1102        Ok(())
1103    }
1104
1105    /// Find tasks with optional filters, sorting, and pagination
1106    pub async fn find_tasks(
1107        &self,
1108        status: Option<String>,
1109        parent_id: Option<Option<i64>>,
1110        sort_by: Option<TaskSortBy>,
1111        limit: Option<i64>,
1112        offset: Option<i64>,
1113    ) -> Result<PaginatedTasks> {
1114        // Apply defaults
1115        let sort_by = sort_by.unwrap_or_default(); // Default: FocusAware
1116        let limit = limit.unwrap_or(100);
1117        let offset = offset.unwrap_or(0);
1118
1119        // Resolve session_id for FocusAware sorting
1120        let session_id = crate::workspace::resolve_session_id(None);
1121
1122        // Build WHERE clause (always exclude soft-deleted tasks)
1123        let mut where_clause = String::from("WHERE deleted_at IS NULL");
1124        let mut conditions = Vec::new();
1125
1126        if let Some(s) = status {
1127            let canonical = match crate::plan::TaskStatus::from_db_str(&s) {
1128                Some(ts) => ts.as_db_str(),
1129                None => return Err(IntentError::InvalidInput(format!("Invalid status: {}", s))),
1130            };
1131            where_clause.push_str(" AND status = ?");
1132            conditions.push(canonical.to_string());
1133        }
1134
1135        if let Some(pid) = parent_id {
1136            if let Some(p) = pid {
1137                where_clause.push_str(" AND parent_id = ?");
1138                conditions.push(p.to_string());
1139            } else {
1140                where_clause.push_str(" AND parent_id IS NULL");
1141            }
1142        }
1143
1144        // Track if FocusAware mode needs session_id bind
1145        let uses_session_bind = matches!(sort_by, TaskSortBy::FocusAware);
1146
1147        // Build ORDER BY clause based on sort mode
1148        let order_clause = match sort_by {
1149            TaskSortBy::Id => {
1150                // Legacy: simple ORDER BY id ASC
1151                "ORDER BY id ASC".to_string()
1152            },
1153            TaskSortBy::Priority => {
1154                // ORDER BY priority ASC, complexity ASC, id ASC
1155                "ORDER BY COALESCE(priority, 999) ASC, COALESCE(complexity, 5) ASC, id ASC"
1156                    .to_string()
1157            },
1158            TaskSortBy::Time => {
1159                // ORDER BY timestamp based on status
1160                r#"ORDER BY
1161                    CASE status
1162                        WHEN 'doing' THEN first_doing_at
1163                        WHEN 'todo' THEN first_todo_at
1164                        WHEN 'done' THEN first_done_at
1165                    END ASC NULLS LAST,
1166                    id ASC"#
1167                    .to_string()
1168            },
1169            TaskSortBy::FocusAware => {
1170                // Focus-aware: current focused task → doing tasks → todo tasks
1171                r#"ORDER BY
1172                    CASE
1173                        WHEN t.id = (SELECT current_task_id FROM sessions WHERE session_id = ?) THEN 0
1174                        WHEN t.status = 'doing' THEN 1
1175                        WHEN t.status = 'todo' THEN 2
1176                        ELSE 3
1177                    END ASC,
1178                    COALESCE(t.priority, 999) ASC,
1179                    t.id ASC"#
1180                    .to_string()
1181            },
1182        };
1183
1184        // Get total count
1185        let count_query = format!("SELECT COUNT(*) FROM tasks {}", where_clause);
1186        let mut count_q = sqlx::query_scalar::<_, i64>(&count_query);
1187        for cond in &conditions {
1188            count_q = count_q.bind(cond);
1189        }
1190        let total_count = count_q.fetch_one(self.pool).await?;
1191
1192        // Build main query with pagination
1193        let main_query = format!(
1194            "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata FROM tasks t {} {} LIMIT ? OFFSET ?",
1195            where_clause, order_clause
1196        );
1197
1198        let mut q = sqlx::query_as::<_, Task>(&main_query);
1199        for cond in conditions {
1200            q = q.bind(cond);
1201        }
1202        // Bind session_id for FocusAware ORDER BY clause
1203        if uses_session_bind {
1204            q = q.bind(&session_id);
1205        }
1206        q = q.bind(limit);
1207        q = q.bind(offset);
1208
1209        let tasks = q.fetch_all(self.pool).await?;
1210
1211        // Calculate has_more
1212        let has_more = offset + (tasks.len() as i64) < total_count;
1213
1214        Ok(PaginatedTasks {
1215            tasks,
1216            total_count,
1217            has_more,
1218            limit,
1219            offset,
1220        })
1221    }
1222
1223    /// Get workspace statistics using SQL aggregation (no data loading)
1224    ///
1225    /// This is much more efficient than loading all tasks just to count them.
1226    /// Used by session restore when there's no focused task.
1227    pub async fn get_stats(&self) -> Result<WorkspaceStats> {
1228        let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
1229            r#"SELECT
1230                COUNT(*) as total,
1231                COALESCE(SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END), 0),
1232                COALESCE(SUM(CASE WHEN status = 'doing' THEN 1 ELSE 0 END), 0),
1233                COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0)
1234            FROM tasks WHERE deleted_at IS NULL"#,
1235        )
1236        .fetch_one(self.pool)
1237        .await?;
1238
1239        Ok(WorkspaceStats {
1240            total_tasks: row.0,
1241            todo: row.1,
1242            doing: row.2,
1243            done: row.3,
1244        })
1245    }
1246
1247    /// Start a task (atomic: update status + set current)
1248    #[tracing::instrument(skip(self))]
1249    pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
1250        // Check if task exists first
1251        let task_exists: bool =
1252            sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1253                .bind(id)
1254                .fetch_one(self.pool)
1255                .await?;
1256
1257        if !task_exists {
1258            return Err(IntentError::TaskNotFound(id));
1259        }
1260
1261        // Check if task is blocked by incomplete dependencies
1262        use crate::dependencies::get_incomplete_blocking_tasks;
1263        if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
1264            return Err(IntentError::TaskBlocked {
1265                task_id: id,
1266                blocking_task_ids: blocking_tasks,
1267            });
1268        }
1269
1270        let mut tx = self.pool.begin().await?;
1271
1272        let now = Utc::now();
1273
1274        // Update task status to doing
1275        sqlx::query(
1276            r#"
1277            UPDATE tasks
1278            SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
1279            WHERE id = ?
1280            "#,
1281        )
1282        .bind(now)
1283        .bind(id)
1284        .execute(&mut *tx)
1285        .await?;
1286
1287        // Set as current task in sessions table
1288        // Use session_id from environment if available
1289        let session_id = crate::workspace::resolve_session_id(None);
1290        sqlx::query(
1291            r#"
1292            INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
1293            VALUES (?, ?, datetime('now'), datetime('now'))
1294            ON CONFLICT(session_id) DO UPDATE SET
1295                current_task_id = excluded.current_task_id,
1296                last_active_at = datetime('now')
1297            "#,
1298        )
1299        .bind(&session_id)
1300        .bind(id)
1301        .execute(&mut *tx)
1302        .await?;
1303
1304        tx.commit().await?;
1305
1306        if with_events {
1307            let result = self.get_task_with_events(id).await?;
1308            self.notify_task_updated(&result.task).await;
1309            Ok(result)
1310        } else {
1311            let task = self.get_task(id).await?;
1312            self.notify_task_updated(&task).await;
1313            Ok(TaskWithEvents {
1314                task,
1315                events_summary: None,
1316            })
1317        }
1318    }
1319
1320    /// Build a next-step suggestion after completing a task.
1321    ///
1322    /// Shared by `done_task` and `done_task_by_id` to avoid duplication.
1323    async fn build_next_step_suggestion(
1324        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
1325        id: i64,
1326        task_name: &str,
1327        parent_id: Option<i64>,
1328    ) -> Result<NextStepSuggestion> {
1329        if let Some(parent_task_id) = parent_id {
1330            let remaining_siblings: i64 = sqlx::query_scalar::<_, i64>(
1331                "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ? AND deleted_at IS NULL",
1332            )
1333            .bind(parent_task_id)
1334            .bind(id)
1335            .fetch_one(&mut **tx)
1336            .await?;
1337
1338            let parent_name: String =
1339                sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1340                    .bind(parent_task_id)
1341                    .fetch_one(&mut **tx)
1342                    .await?;
1343
1344            if remaining_siblings == 0 {
1345                Ok(NextStepSuggestion::ParentIsReady {
1346                    message: format!(
1347                        "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
1348                        parent_task_id, parent_name
1349                    ),
1350                    parent_task_id,
1351                    parent_task_name: parent_name,
1352                })
1353            } else {
1354                Ok(NextStepSuggestion::SiblingTasksRemain {
1355                    message: format!(
1356                        "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
1357                        id, parent_task_id, parent_name
1358                    ),
1359                    parent_task_id,
1360                    parent_task_name: parent_name,
1361                    remaining_siblings_count: remaining_siblings,
1362                })
1363            }
1364        } else {
1365            let child_count: i64 =
1366                sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_CHILDREN_TOTAL)
1367                    .bind(id)
1368                    .fetch_one(&mut **tx)
1369                    .await?;
1370
1371            if child_count > 0 {
1372                Ok(NextStepSuggestion::TopLevelTaskCompleted {
1373                    message: format!(
1374                        "Top-level task #{} '{}' has been completed. Well done!",
1375                        id, task_name
1376                    ),
1377                    completed_task_id: id,
1378                    completed_task_name: task_name.to_string(),
1379                })
1380            } else {
1381                let remaining_tasks: i64 = sqlx::query_scalar::<_, i64>(
1382                    "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ? AND deleted_at IS NULL",
1383                )
1384                .bind(id)
1385                .fetch_one(&mut **tx)
1386                .await?;
1387
1388                if remaining_tasks == 0 {
1389                    Ok(NextStepSuggestion::WorkspaceIsClear {
1390                        message: format!(
1391                            "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
1392                            id
1393                        ),
1394                        completed_task_id: id,
1395                    })
1396                } else {
1397                    Ok(NextStepSuggestion::NoParentContext {
1398                        message: format!("Task #{} '{}' has been completed.", id, task_name),
1399                        completed_task_id: id,
1400                        completed_task_name: task_name.to_string(),
1401                    })
1402                }
1403            }
1404        }
1405    }
1406
1407    /// Complete the current focused task (atomic: check children + update status + clear current)
1408    /// This command only operates on the current_task_id.
1409    /// Prerequisites: A task must be set as current
1410    ///
1411    /// # Arguments
1412    /// * `is_ai_caller` - Whether this is called from AI (MCP) or human (CLI/Dashboard).
1413    ///   When true and task is human-owned, the operation will fail.
1414    ///   Human tasks can only be completed via CLI or Dashboard.
1415    #[tracing::instrument(skip(self))]
1416    pub async fn done_task(&self, is_ai_caller: bool) -> Result<DoneTaskResponse> {
1417        let session_id = crate::workspace::resolve_session_id(None);
1418        let mut tx = self.pool.begin().await?;
1419
1420        // Get the current task ID from sessions table
1421        let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1422            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1423        )
1424        .bind(&session_id)
1425        .fetch_optional(&mut *tx)
1426        .await?
1427        .flatten();
1428
1429        let id = current_task_id.ok_or(IntentError::InvalidInput(
1430            "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
1431        ))?;
1432
1433        // Get the task info before completing it (including owner)
1434        let task_info: (String, Option<i64>, String) =
1435            sqlx::query_as("SELECT name, parent_id, owner FROM tasks WHERE id = ?")
1436                .bind(id)
1437                .fetch_one(&mut *tx)
1438                .await?;
1439        let (task_name, parent_id, owner) = task_info;
1440
1441        // Human Task Protection: AI cannot complete human-owned tasks
1442        // Human must complete their own tasks via CLI or Dashboard
1443        if owner == "human" && is_ai_caller {
1444            return Err(IntentError::HumanTaskCannotBeCompletedByAI {
1445                task_id: id,
1446                task_name: task_name.clone(),
1447            });
1448        }
1449
1450        // Complete the task (validates children + updates status)
1451        self.complete_task_in_tx(&mut tx, id).await?;
1452
1453        // Clear the current task in sessions table for this session
1454        sqlx::query("UPDATE sessions SET current_task_id = NULL, last_active_at = datetime('now') WHERE session_id = ?")
1455            .bind(&session_id)
1456            .execute(&mut *tx)
1457            .await?;
1458
1459        let next_step_suggestion =
1460            Self::build_next_step_suggestion(&mut tx, id, &task_name, parent_id).await?;
1461
1462        tx.commit().await?;
1463
1464        // Fetch the completed task to notify UI
1465        let completed_task = self.get_task(id).await?;
1466        self.notify_task_updated(&completed_task).await;
1467
1468        Ok(DoneTaskResponse {
1469            completed_task,
1470            workspace_status: WorkspaceStatus {
1471                current_task_id: None,
1472            },
1473            next_step_suggestion,
1474        })
1475    }
1476
1477    /// Complete a task by its ID directly (without requiring it to be the current focus).
1478    ///
1479    /// Unlike `done_task` which only completes the currently focused task, this method
1480    /// completes a task by ID. If the task happens to be the current session's focus,
1481    /// the focus is cleared. Otherwise, the current focus is left unchanged.
1482    ///
1483    /// # Arguments
1484    /// * `id` - The task ID to complete
1485    /// * `is_ai_caller` - Whether this is called from AI. When true and task is human-owned, fails.
1486    #[tracing::instrument(skip(self))]
1487    pub async fn done_task_by_id(&self, id: i64, is_ai_caller: bool) -> Result<DoneTaskResponse> {
1488        let session_id = crate::workspace::resolve_session_id(None);
1489        let mut tx = self.pool.begin().await?;
1490
1491        // Get the task info (name, parent_id, owner) by ID — exclude soft-deleted tasks
1492        let task_info: (String, Option<i64>, String) = sqlx::query_as(
1493            "SELECT name, parent_id, owner FROM tasks WHERE id = ? AND deleted_at IS NULL",
1494        )
1495        .bind(id)
1496        .fetch_optional(&mut *tx)
1497        .await?
1498        .ok_or(IntentError::TaskNotFound(id))?;
1499        let (task_name, parent_id, owner) = task_info;
1500
1501        // Human Task Protection: AI cannot complete human-owned tasks
1502        if owner == "human" && is_ai_caller {
1503            return Err(IntentError::HumanTaskCannotBeCompletedByAI {
1504                task_id: id,
1505                task_name: task_name.clone(),
1506            });
1507        }
1508
1509        // Complete the task (validates children + updates status)
1510        self.complete_task_in_tx(&mut tx, id).await?;
1511
1512        // If this task is the current session's focus, clear it (otherwise leave focus untouched)
1513        sqlx::query(
1514            "UPDATE sessions SET current_task_id = NULL, last_active_at = datetime('now') WHERE session_id = ? AND current_task_id = ?",
1515        )
1516        .bind(&session_id)
1517        .bind(id)
1518        .execute(&mut *tx)
1519        .await?;
1520
1521        // Read back the actual current_task_id (may still be set if we completed a non-focused task)
1522        let actual_current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1523            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1524        )
1525        .bind(&session_id)
1526        .fetch_optional(&mut *tx)
1527        .await?
1528        .flatten();
1529
1530        let next_step_suggestion =
1531            Self::build_next_step_suggestion(&mut tx, id, &task_name, parent_id).await?;
1532
1533        // LLM Synthesis: Generate updated task description from events (if configured)
1534        let synthesis_result = self.try_synthesize_task_description(id, &task_name).await;
1535
1536        tx.commit().await?;
1537
1538        // Fetch the completed task to notify UI
1539        let mut completed_task = self.get_task(id).await?;
1540
1541        // Apply synthesis if available and appropriate (respects owner field)
1542        if let Ok(Some(new_spec)) = synthesis_result {
1543            completed_task = self
1544                .apply_synthesis_if_appropriate(completed_task, &new_spec, &owner)
1545                .await?;
1546        }
1547
1548        // Trigger background task structure analysis (async, non-blocking)
1549        crate::llm::analyze_task_structure_background(self.pool.clone());
1550
1551        self.notify_task_updated(&completed_task).await;
1552
1553        Ok(DoneTaskResponse {
1554            completed_task,
1555            workspace_status: WorkspaceStatus {
1556                current_task_id: actual_current_task_id,
1557            },
1558            next_step_suggestion,
1559        })
1560    }
1561
1562    /// Check if a task exists
1563    async fn check_task_exists(&self, id: i64) -> Result<()> {
1564        let exists: bool = sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1565            .bind(id)
1566            .fetch_one(self.pool)
1567            .await?;
1568
1569        if !exists {
1570            return Err(IntentError::TaskNotFound(id));
1571        }
1572
1573        Ok(())
1574    }
1575
1576    /// Check for circular dependencies
1577    async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
1578        let mut current_id = new_parent_id;
1579
1580        loop {
1581            if current_id == task_id {
1582                return Err(IntentError::CircularDependency {
1583                    blocking_task_id: new_parent_id,
1584                    blocked_task_id: task_id,
1585                });
1586            }
1587
1588            let parent: Option<i64> =
1589                sqlx::query_scalar::<_, Option<i64>>(crate::sql_constants::SELECT_TASK_PARENT_ID)
1590                    .bind(current_id)
1591                    .fetch_optional(self.pool)
1592                    .await?
1593                    .flatten();
1594
1595            match parent {
1596                Some(pid) => current_id = pid,
1597                None => break,
1598            }
1599        }
1600
1601        Ok(())
1602    }
1603    /// Create a subtask under the current task and switch to it (atomic operation)
1604    /// Returns error if there is no current task
1605    /// Returns response with subtask info and parent task info
1606    pub async fn spawn_subtask(
1607        &self,
1608        name: &str,
1609        spec: Option<&str>,
1610    ) -> Result<SpawnSubtaskResponse> {
1611        // Get current task from sessions table for this session
1612        let session_id = crate::workspace::resolve_session_id(None);
1613        let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1614            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1615        )
1616        .bind(&session_id)
1617        .fetch_optional(self.pool)
1618        .await?
1619        .flatten();
1620
1621        let parent_id = current_task_id.ok_or(IntentError::InvalidInput(
1622            "No current task to create subtask under".to_string(),
1623        ))?;
1624
1625        // Get parent task info
1626        let parent_name: String =
1627            sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1628                .bind(parent_id)
1629                .fetch_one(self.pool)
1630                .await?;
1631
1632        // Create the subtask with AI ownership (CLI operation)
1633        let subtask = self
1634            .add_task(
1635                name.to_string(),
1636                spec.map(|s| s.to_string()),
1637                Some(parent_id),
1638                Some("ai".to_string()),
1639                None,
1640                None,
1641            )
1642            .await?;
1643
1644        // Start the new subtask (sets status to doing and updates current_task_id)
1645        // This keeps the parent task in 'doing' status (multi-doing design)
1646        self.start_task(subtask.id, false).await?;
1647
1648        Ok(SpawnSubtaskResponse {
1649            subtask: SubtaskInfo {
1650                id: subtask.id,
1651                name: subtask.name,
1652                parent_id,
1653                status: "doing".to_string(),
1654            },
1655            parent_task: ParentTaskInfo {
1656                id: parent_id,
1657                name: parent_name,
1658            },
1659        })
1660    }
1661
1662    /// Intelligently pick tasks from 'todo' and transition them to 'doing'
1663    /// Returns tasks that were successfully transitioned
1664    ///
1665    /// # Arguments
1666    /// * `max_count` - Maximum number of tasks to pick
1667    /// * `capacity_limit` - Maximum total number of tasks allowed in 'doing' status
1668    ///
1669    /// # Logic
1670    /// 1. Check current 'doing' task count
1671    /// 2. Calculate available capacity
1672    /// 3. Select tasks from 'todo' (prioritized by: priority DESC, complexity ASC)
1673    /// 4. Transition selected tasks to 'doing'
1674    pub async fn pick_next_tasks(
1675        &self,
1676        max_count: usize,
1677        capacity_limit: usize,
1678    ) -> Result<Vec<Task>> {
1679        let mut tx = self.pool.begin().await?;
1680
1681        // Get current doing count
1682        let doing_count: i64 =
1683            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
1684                .fetch_one(&mut *tx)
1685                .await?;
1686
1687        // Calculate available capacity
1688        let available = capacity_limit.saturating_sub(doing_count as usize);
1689        if available == 0 {
1690            return Ok(vec![]);
1691        }
1692
1693        let limit = std::cmp::min(max_count, available);
1694
1695        // Select tasks from todo, prioritizing by priority DESC, complexity ASC
1696        let todo_tasks = sqlx::query_as::<_, Task>(
1697            r#"
1698                        SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1699                        FROM tasks
1700                        WHERE status = 'todo' AND deleted_at IS NULL
1701                        ORDER BY
1702                            COALESCE(priority, 0) ASC,
1703                            COALESCE(complexity, 5) ASC,
1704                            id ASC
1705                        LIMIT ?
1706                        "#,
1707        )
1708        .bind(limit as i64)
1709        .fetch_all(&mut *tx)
1710        .await?;
1711
1712        if todo_tasks.is_empty() {
1713            return Ok(vec![]);
1714        }
1715
1716        let now = Utc::now();
1717
1718        // Transition selected tasks to 'doing'
1719        for task in &todo_tasks {
1720            sqlx::query(
1721                r#"
1722                UPDATE tasks
1723                SET status = 'doing',
1724                    first_doing_at = COALESCE(first_doing_at, ?)
1725                WHERE id = ?
1726                "#,
1727            )
1728            .bind(now)
1729            .bind(task.id)
1730            .execute(&mut *tx)
1731            .await?;
1732        }
1733
1734        tx.commit().await?;
1735
1736        // Fetch and return updated tasks in the same order
1737        let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
1738        let placeholders = vec!["?"; task_ids.len()].join(",");
1739        let query = format!(
1740            "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1741                         FROM tasks WHERE id IN ({})
1742                         ORDER BY
1743                             COALESCE(priority, 0) ASC,
1744                             COALESCE(complexity, 5) ASC,
1745                             id ASC",
1746            placeholders
1747        );
1748
1749        let mut q = sqlx::query_as::<_, Task>(&query);
1750        for id in task_ids {
1751            q = q.bind(id);
1752        }
1753
1754        let updated_tasks = q.fetch_all(self.pool).await?;
1755        Ok(updated_tasks)
1756    }
1757
1758    /// Intelligently recommend the next task to work on based on context-aware priority model.
1759    ///
1760    /// Priority logic:
1761    /// 1. First priority: Subtasks of the current focused task (depth-first)
1762    /// 2. Second priority: Top-level tasks (breadth-first)
1763    /// 3. No recommendation: Return appropriate empty state
1764    ///
1765    /// This command does NOT modify task status.
1766    pub async fn pick_next(&self) -> Result<PickNextResponse> {
1767        // Step 1: Check if there's a current focused task for this session
1768        let session_id = crate::workspace::resolve_session_id(None);
1769        let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1770            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1771        )
1772        .bind(&session_id)
1773        .fetch_optional(self.pool)
1774        .await?
1775        .flatten();
1776
1777        if let Some(current_id) = current_task_id {
1778            // Step 1a: First priority - Get **doing** subtasks of current focused task
1779            // Exclude tasks blocked by incomplete dependencies
1780            let doing_subtasks = sqlx::query_as::<_, Task>(
1781                r#"
1782                        SELECT id, parent_id, name, spec, status, complexity, priority,
1783                               first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1784                        FROM tasks
1785                        WHERE parent_id = ? AND status = 'doing' AND deleted_at IS NULL
1786                          AND NOT EXISTS (
1787                            SELECT 1 FROM dependencies d
1788                            JOIN tasks bt ON d.blocking_task_id = bt.id
1789                            WHERE d.blocked_task_id = tasks.id
1790                              AND bt.status != 'done' AND bt.deleted_at IS NULL
1791                          )
1792                        ORDER BY COALESCE(priority, 999999) ASC, id ASC
1793                        LIMIT 1
1794                        "#,
1795            )
1796            .bind(current_id)
1797            .fetch_optional(self.pool)
1798            .await?;
1799
1800            if let Some(task) = doing_subtasks {
1801                return Ok(PickNextResponse::focused_subtask(task));
1802            }
1803
1804            // Step 1b: Second priority - Get **todo** subtasks if no doing subtasks
1805            let todo_subtasks = sqlx::query_as::<_, Task>(
1806                r#"
1807                            SELECT id, parent_id, name, spec, status, complexity, priority,
1808                                   first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1809                            FROM tasks
1810                            WHERE parent_id = ? AND status = 'todo' AND deleted_at IS NULL
1811                              AND NOT EXISTS (
1812                                SELECT 1 FROM dependencies d
1813                                JOIN tasks bt ON d.blocking_task_id = bt.id
1814                                WHERE d.blocked_task_id = tasks.id
1815                                  AND bt.status != 'done' AND bt.deleted_at IS NULL
1816                              )
1817                            ORDER BY COALESCE(priority, 999999) ASC, id ASC
1818                            LIMIT 1
1819                            "#,
1820            )
1821            .bind(current_id)
1822            .fetch_optional(self.pool)
1823            .await?;
1824
1825            if let Some(task) = todo_subtasks {
1826                return Ok(PickNextResponse::focused_subtask(task));
1827            }
1828        }
1829
1830        // Step 2a: Third priority - Get top-level **doing** tasks (excluding current task)
1831        // Exclude tasks blocked by incomplete dependencies
1832        let doing_top_level = if let Some(current_id) = current_task_id {
1833            sqlx::query_as::<_, Task>(
1834                r#"
1835                SELECT id, parent_id, name, spec, status, complexity, priority,
1836                       first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1837                FROM tasks
1838                WHERE parent_id IS NULL AND status = 'doing' AND id != ? AND deleted_at IS NULL
1839                  AND NOT EXISTS (
1840                    SELECT 1 FROM dependencies d
1841                    JOIN tasks bt ON d.blocking_task_id = bt.id
1842                    WHERE d.blocked_task_id = tasks.id
1843                      AND bt.status != 'done' AND bt.deleted_at IS NULL
1844                  )
1845                ORDER BY COALESCE(priority, 999999) ASC, id ASC
1846                LIMIT 1
1847                "#,
1848            )
1849            .bind(current_id)
1850            .fetch_optional(self.pool)
1851            .await?
1852        } else {
1853            sqlx::query_as::<_, Task>(
1854                r#"
1855                SELECT id, parent_id, name, spec, status, complexity, priority,
1856                       first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1857                FROM tasks
1858                WHERE parent_id IS NULL AND status = 'doing' AND deleted_at IS NULL
1859                  AND NOT EXISTS (
1860                    SELECT 1 FROM dependencies d
1861                    JOIN tasks bt ON d.blocking_task_id = bt.id
1862                    WHERE d.blocked_task_id = tasks.id
1863                      AND bt.status != 'done' AND bt.deleted_at IS NULL
1864                  )
1865                ORDER BY COALESCE(priority, 999999) ASC, id ASC
1866                LIMIT 1
1867                "#,
1868            )
1869            .fetch_optional(self.pool)
1870            .await?
1871        };
1872
1873        if let Some(task) = doing_top_level {
1874            return Ok(PickNextResponse::top_level_task(task));
1875        }
1876
1877        // Step 2b: Fourth priority - Get top-level **todo** tasks
1878        // Exclude tasks blocked by incomplete dependencies
1879        let todo_top_level = sqlx::query_as::<_, Task>(
1880            r#"
1881            SELECT id, parent_id, name, spec, status, complexity, priority,
1882                   first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
1883            FROM tasks
1884            WHERE parent_id IS NULL AND status = 'todo' AND deleted_at IS NULL
1885              AND NOT EXISTS (
1886                SELECT 1 FROM dependencies d
1887                JOIN tasks bt ON d.blocking_task_id = bt.id
1888                WHERE d.blocked_task_id = tasks.id
1889                  AND bt.status != 'done' AND bt.deleted_at IS NULL
1890              )
1891            ORDER BY COALESCE(priority, 999999) ASC, id ASC
1892            LIMIT 1
1893            "#,
1894        )
1895        .fetch_optional(self.pool)
1896        .await?;
1897
1898        if let Some(task) = todo_top_level {
1899            return Ok(PickNextResponse::top_level_task(task));
1900        }
1901
1902        // Step 3: No recommendation - determine why
1903        // Check if there are any tasks at all
1904        let total_tasks: i64 =
1905            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_TOTAL)
1906                .fetch_one(self.pool)
1907                .await?;
1908
1909        if total_tasks == 0 {
1910            return Ok(PickNextResponse::no_tasks_in_project());
1911        }
1912
1913        // Check if all tasks are completed
1914        let todo_or_doing_count: i64 = sqlx::query_scalar::<_, i64>(
1915            "SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing') AND deleted_at IS NULL",
1916        )
1917        .fetch_one(self.pool)
1918        .await?;
1919
1920        if todo_or_doing_count == 0 {
1921            return Ok(PickNextResponse::all_tasks_completed());
1922        }
1923
1924        // Otherwise, there are tasks but none available based on current context
1925        Ok(PickNextResponse::no_available_todos())
1926    }
1927
1928    /// Try to synthesize task description using LLM
1929    ///
1930    /// Returns Ok(None) if LLM is not configured (graceful degradation)
1931    /// Returns Ok(Some(synthesis)) if successful
1932    /// Returns Err only on critical failures
1933    async fn try_synthesize_task_description(
1934        &self,
1935        task_id: i64,
1936        task_name: &str,
1937    ) -> Result<Option<String>> {
1938        // Get task spec and events
1939        let task = self.get_task(task_id).await?;
1940        let events = crate::events::EventManager::new(self.pool)
1941            .list_events(Some(task_id), None, None, None)
1942            .await?;
1943
1944        // Call LLM synthesis (returns None if not configured)
1945        match crate::llm::synthesize_task_description(
1946            self.pool,
1947            task_name,
1948            task.spec.as_deref(),
1949            &events,
1950        )
1951        .await
1952        {
1953            Ok(synthesis) => Ok(synthesis),
1954            Err(e) => {
1955                // Log error but don't fail the task completion
1956                tracing::warn!("LLM synthesis failed: {}", e);
1957                Ok(None)
1958            },
1959        }
1960    }
1961
1962    /// Apply LLM synthesis to task based on owner field
1963    ///
1964    /// - AI-owned tasks: auto-apply
1965    /// - Human-owned tasks: prompt for approval (currently just logs and skips)
1966    async fn apply_synthesis_if_appropriate(
1967        &self,
1968        task: Task,
1969        new_spec: &str,
1970        owner: &str,
1971    ) -> Result<Task> {
1972        if owner == "ai" {
1973            // AI-owned task: auto-apply synthesis
1974            tracing::info!("Auto-applying LLM synthesis for AI-owned task #{}", task.id);
1975
1976            let updated = self
1977                .update_task(
1978                    task.id,
1979                    TaskUpdate {
1980                        spec: Some(new_spec),
1981                        ..Default::default()
1982                    },
1983                )
1984                .await?;
1985
1986            Ok(updated)
1987        } else {
1988            // Human-owned task: would prompt user, but for CLI we just log
1989            // TODO: Implement interactive prompt for human tasks
1990            tracing::info!(
1991                "LLM synthesis available for human-owned task #{}, but auto-apply disabled. \
1992                 User would be prompted in interactive mode.",
1993                task.id
1994            );
1995            eprintln!("\n💡 LLM generated a task summary:");
1996            eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
1997            eprintln!("{}", new_spec);
1998            eprintln!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
1999            eprintln!("(Auto-apply disabled for human-owned tasks)");
2000            eprintln!(
2001                "To apply manually: ie task update {} --description \"<new spec>\"",
2002                task.id
2003            );
2004
2005            Ok(task) // Return unchanged
2006        }
2007    }
2008}
2009
2010impl crate::backend::TaskBackend for TaskManager<'_> {
2011    fn get_task(&self, id: i64) -> impl std::future::Future<Output = Result<Task>> + Send {
2012        self.get_task(id)
2013    }
2014
2015    fn get_task_with_events(
2016        &self,
2017        id: i64,
2018    ) -> impl std::future::Future<Output = Result<TaskWithEvents>> + Send {
2019        self.get_task_with_events(id)
2020    }
2021
2022    fn get_task_ancestry(
2023        &self,
2024        task_id: i64,
2025    ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2026        self.get_task_ancestry(task_id)
2027    }
2028
2029    fn get_task_context(
2030        &self,
2031        id: i64,
2032    ) -> impl std::future::Future<Output = Result<TaskContext>> + Send {
2033        self.get_task_context(id)
2034    }
2035
2036    fn get_siblings(
2037        &self,
2038        id: i64,
2039        parent_id: Option<i64>,
2040    ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2041        self.get_siblings(id, parent_id)
2042    }
2043
2044    fn get_children(&self, id: i64) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2045        self.get_children(id)
2046    }
2047
2048    fn get_blocking_tasks(
2049        &self,
2050        id: i64,
2051    ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2052        self.get_blocking_tasks(id)
2053    }
2054
2055    fn get_blocked_by_tasks(
2056        &self,
2057        id: i64,
2058    ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2059        self.get_blocked_by_tasks(id)
2060    }
2061
2062    fn get_descendants(
2063        &self,
2064        task_id: i64,
2065    ) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2066        self.get_descendants(task_id)
2067    }
2068
2069    fn get_status(
2070        &self,
2071        task_id: i64,
2072        with_events: bool,
2073    ) -> impl std::future::Future<Output = Result<crate::db::models::StatusResponse>> + Send {
2074        self.get_status(task_id, with_events)
2075    }
2076
2077    fn get_root_tasks(&self) -> impl std::future::Future<Output = Result<Vec<Task>>> + Send {
2078        self.get_root_tasks()
2079    }
2080
2081    fn find_tasks(
2082        &self,
2083        status: Option<String>,
2084        parent_id: Option<Option<i64>>,
2085        sort_by: Option<TaskSortBy>,
2086        limit: Option<i64>,
2087        offset: Option<i64>,
2088    ) -> impl std::future::Future<Output = Result<PaginatedTasks>> + Send {
2089        self.find_tasks(status, parent_id, sort_by, limit, offset)
2090    }
2091
2092    fn add_task(
2093        &self,
2094        name: String,
2095        spec: Option<String>,
2096        parent_id: Option<i64>,
2097        owner: Option<String>,
2098        priority: Option<i32>,
2099        metadata: Option<String>,
2100    ) -> impl std::future::Future<Output = Result<Task>> + Send {
2101        self.add_task(name, spec, parent_id, owner, priority, metadata)
2102    }
2103
2104    fn update_task(
2105        &self,
2106        id: i64,
2107        update: TaskUpdate<'_>,
2108    ) -> impl std::future::Future<Output = Result<Task>> + Send {
2109        self.update_task(id, update)
2110    }
2111
2112    fn delete_task(&self, id: i64) -> impl std::future::Future<Output = Result<()>> + Send {
2113        self.delete_task(id)
2114    }
2115
2116    fn delete_task_cascade(
2117        &self,
2118        id: i64,
2119    ) -> impl std::future::Future<Output = Result<usize>> + Send {
2120        self.delete_task_cascade(id)
2121    }
2122
2123    fn add_dependency(
2124        &self,
2125        blocking_id: i64,
2126        blocked_id: i64,
2127    ) -> impl std::future::Future<Output = Result<()>> + Send {
2128        self.add_dependency(blocking_id, blocked_id)
2129    }
2130
2131    fn remove_dependency(
2132        &self,
2133        blocking_id: i64,
2134        blocked_id: i64,
2135    ) -> impl std::future::Future<Output = Result<()>> + Send {
2136        self.remove_dependency(blocking_id, blocked_id)
2137    }
2138
2139    fn start_task(
2140        &self,
2141        id: i64,
2142        with_events: bool,
2143    ) -> impl std::future::Future<Output = Result<TaskWithEvents>> + Send {
2144        self.start_task(id, with_events)
2145    }
2146
2147    fn done_task(
2148        &self,
2149        is_ai_caller: bool,
2150    ) -> impl std::future::Future<Output = Result<DoneTaskResponse>> + Send {
2151        self.done_task(is_ai_caller)
2152    }
2153
2154    fn done_task_by_id(
2155        &self,
2156        id: i64,
2157        is_ai_caller: bool,
2158    ) -> impl std::future::Future<Output = Result<DoneTaskResponse>> + Send {
2159        self.done_task_by_id(id, is_ai_caller)
2160    }
2161
2162    fn pick_next(&self) -> impl std::future::Future<Output = Result<PickNextResponse>> + Send {
2163        self.pick_next()
2164    }
2165}
2166
2167impl crate::backend::SearchBackend for TaskManager<'_> {
2168    fn search(
2169        &self,
2170        query: String,
2171        include_tasks: bool,
2172        include_events: bool,
2173        limit: Option<i64>,
2174        offset: Option<i64>,
2175    ) -> impl std::future::Future<Output = Result<crate::db::models::PaginatedSearchResults>> + Send
2176    {
2177        use crate::search::SearchManager;
2178        let mgr = SearchManager::new(self.pool);
2179        async move {
2180            mgr.search(&query, include_tasks, include_events, limit, offset, false)
2181                .await
2182        }
2183    }
2184}
2185
2186#[cfg(test)]
2187mod tests {
2188    use super::*;
2189    use crate::events::EventManager;
2190    use crate::test_utils::test_helpers::TestContext;
2191    use crate::workspace::WorkspaceManager;
2192
2193    #[tokio::test]
2194    async fn test_get_stats_empty() {
2195        let ctx = TestContext::new().await;
2196        let manager = TaskManager::new(ctx.pool());
2197
2198        let stats = manager.get_stats().await.unwrap();
2199
2200        assert_eq!(stats.total_tasks, 0);
2201        assert_eq!(stats.todo, 0);
2202        assert_eq!(stats.doing, 0);
2203        assert_eq!(stats.done, 0);
2204    }
2205
2206    #[tokio::test]
2207    async fn test_get_stats_with_tasks() {
2208        let ctx = TestContext::new().await;
2209        let manager = TaskManager::new(ctx.pool());
2210
2211        // Create tasks with different statuses
2212        let task1 = manager
2213            .add_task("Task 1".to_string(), None, None, None, None, None)
2214            .await
2215            .unwrap();
2216        let task2 = manager
2217            .add_task("Task 2".to_string(), None, None, None, None, None)
2218            .await
2219            .unwrap();
2220        let _task3 = manager
2221            .add_task("Task 3".to_string(), None, None, None, None, None)
2222            .await
2223            .unwrap();
2224
2225        // Update statuses
2226        manager
2227            .update_task(
2228                task1.id,
2229                TaskUpdate {
2230                    status: Some("doing"),
2231                    ..Default::default()
2232                },
2233            )
2234            .await
2235            .unwrap();
2236        manager
2237            .update_task(
2238                task2.id,
2239                TaskUpdate {
2240                    status: Some("done"),
2241                    ..Default::default()
2242                },
2243            )
2244            .await
2245            .unwrap();
2246        // task3 stays as todo
2247
2248        let stats = manager.get_stats().await.unwrap();
2249
2250        assert_eq!(stats.total_tasks, 3);
2251        assert_eq!(stats.todo, 1);
2252        assert_eq!(stats.doing, 1);
2253        assert_eq!(stats.done, 1);
2254    }
2255
2256    #[tokio::test]
2257    async fn test_add_task() {
2258        let ctx = TestContext::new().await;
2259        let manager = TaskManager::new(ctx.pool());
2260
2261        let task = manager
2262            .add_task("Test task".to_string(), None, None, None, None, None)
2263            .await
2264            .unwrap();
2265
2266        assert_eq!(task.name, "Test task");
2267        assert_eq!(task.status, "todo");
2268        assert!(task.first_todo_at.is_some());
2269        assert!(task.first_doing_at.is_none());
2270        assert!(task.first_done_at.is_none());
2271    }
2272
2273    #[tokio::test]
2274    async fn test_add_task_with_spec() {
2275        let ctx = TestContext::new().await;
2276        let manager = TaskManager::new(ctx.pool());
2277
2278        let spec = "This is a task specification";
2279        let task = manager
2280            .add_task(
2281                "Test task".to_string(),
2282                Some(spec.to_string()),
2283                None,
2284                None,
2285                None,
2286                None,
2287            )
2288            .await
2289            .unwrap();
2290
2291        assert_eq!(task.name, "Test task");
2292        assert_eq!(task.spec.as_deref(), Some(spec));
2293    }
2294
2295    #[tokio::test]
2296    async fn test_add_task_with_parent() {
2297        let ctx = TestContext::new().await;
2298        let manager = TaskManager::new(ctx.pool());
2299
2300        let parent = manager
2301            .add_task("Parent task".to_string(), None, None, None, None, None)
2302            .await
2303            .unwrap();
2304        let child = manager
2305            .add_task(
2306                "Child task".to_string(),
2307                None,
2308                Some(parent.id),
2309                None,
2310                None,
2311                None,
2312            )
2313            .await
2314            .unwrap();
2315
2316        assert_eq!(child.parent_id, Some(parent.id));
2317    }
2318
2319    #[tokio::test]
2320    async fn test_get_task() {
2321        let ctx = TestContext::new().await;
2322        let manager = TaskManager::new(ctx.pool());
2323
2324        let created = manager
2325            .add_task("Test task".to_string(), None, None, None, None, None)
2326            .await
2327            .unwrap();
2328        let retrieved = manager.get_task(created.id).await.unwrap();
2329
2330        assert_eq!(created.id, retrieved.id);
2331        assert_eq!(created.name, retrieved.name);
2332    }
2333
2334    #[tokio::test]
2335    async fn test_get_task_not_found() {
2336        let ctx = TestContext::new().await;
2337        let manager = TaskManager::new(ctx.pool());
2338
2339        let result = manager.get_task(999).await;
2340        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2341    }
2342
2343    #[tokio::test]
2344    async fn test_update_task_name() {
2345        let ctx = TestContext::new().await;
2346        let manager = TaskManager::new(ctx.pool());
2347
2348        let task = manager
2349            .add_task("Original name".to_string(), None, None, None, None, None)
2350            .await
2351            .unwrap();
2352        let updated = manager
2353            .update_task(
2354                task.id,
2355                TaskUpdate {
2356                    name: Some("New name"),
2357                    ..Default::default()
2358                },
2359            )
2360            .await
2361            .unwrap();
2362
2363        assert_eq!(updated.name, "New name");
2364    }
2365
2366    #[tokio::test]
2367    async fn test_update_task_status() {
2368        let ctx = TestContext::new().await;
2369        let manager = TaskManager::new(ctx.pool());
2370
2371        let task = manager
2372            .add_task("Test task".to_string(), None, None, None, None, None)
2373            .await
2374            .unwrap();
2375        let updated = manager
2376            .update_task(
2377                task.id,
2378                TaskUpdate {
2379                    status: Some("doing"),
2380                    ..Default::default()
2381                },
2382            )
2383            .await
2384            .unwrap();
2385
2386        assert_eq!(updated.status, "doing");
2387        assert!(updated.first_doing_at.is_some());
2388    }
2389
2390    #[tokio::test]
2391    async fn test_delete_task() {
2392        let ctx = TestContext::new().await;
2393        let manager = TaskManager::new(ctx.pool());
2394
2395        let task = manager
2396            .add_task("Test task".to_string(), None, None, None, None, None)
2397            .await
2398            .unwrap();
2399        manager.delete_task(task.id).await.unwrap();
2400
2401        let result = manager.get_task(task.id).await;
2402        assert!(result.is_err());
2403    }
2404
2405    #[tokio::test]
2406    async fn test_find_tasks_by_status() {
2407        let ctx = TestContext::new().await;
2408        let manager = TaskManager::new(ctx.pool());
2409
2410        manager
2411            .add_task("Todo task".to_string(), None, None, None, None, None)
2412            .await
2413            .unwrap();
2414        let doing_task = manager
2415            .add_task("Doing task".to_string(), None, None, None, None, None)
2416            .await
2417            .unwrap();
2418        manager
2419            .update_task(
2420                doing_task.id,
2421                TaskUpdate {
2422                    status: Some("doing"),
2423                    ..Default::default()
2424                },
2425            )
2426            .await
2427            .unwrap();
2428
2429        let todo_result = manager
2430            .find_tasks(Some("todo".to_string()), None, None, None, None)
2431            .await
2432            .unwrap();
2433        let doing_result = manager
2434            .find_tasks(Some("doing".to_string()), None, None, None, None)
2435            .await
2436            .unwrap();
2437
2438        assert_eq!(todo_result.tasks.len(), 1);
2439        assert_eq!(doing_result.tasks.len(), 1);
2440        assert_eq!(doing_result.tasks[0].status, "doing");
2441    }
2442
2443    #[tokio::test]
2444    async fn test_find_tasks_by_parent() {
2445        let ctx = TestContext::new().await;
2446        let manager = TaskManager::new(ctx.pool());
2447
2448        let parent = manager
2449            .add_task("Parent".to_string(), None, None, None, None, None)
2450            .await
2451            .unwrap();
2452        manager
2453            .add_task(
2454                "Child 1".to_string(),
2455                None,
2456                Some(parent.id),
2457                None,
2458                None,
2459                None,
2460            )
2461            .await
2462            .unwrap();
2463        manager
2464            .add_task(
2465                "Child 2".to_string(),
2466                None,
2467                Some(parent.id),
2468                None,
2469                None,
2470                None,
2471            )
2472            .await
2473            .unwrap();
2474
2475        let result = manager
2476            .find_tasks(None, Some(Some(parent.id)), None, None, None)
2477            .await
2478            .unwrap();
2479
2480        assert_eq!(result.tasks.len(), 2);
2481    }
2482
2483    #[tokio::test]
2484    async fn test_start_task() {
2485        let ctx = TestContext::new().await;
2486        let manager = TaskManager::new(ctx.pool());
2487
2488        let task = manager
2489            .add_task("Test task".to_string(), None, None, None, None, None)
2490            .await
2491            .unwrap();
2492        let started = manager.start_task(task.id, false).await.unwrap();
2493
2494        assert_eq!(started.task.status, "doing");
2495        assert!(started.task.first_doing_at.is_some());
2496
2497        // Verify it's set as current task
2498        let session_id = crate::workspace::resolve_session_id(None);
2499        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2500            "SELECT current_task_id FROM sessions WHERE session_id = ?",
2501        )
2502        .bind(&session_id)
2503        .fetch_optional(ctx.pool())
2504        .await
2505        .unwrap()
2506        .flatten();
2507
2508        assert_eq!(current, Some(task.id));
2509    }
2510
2511    #[tokio::test]
2512    async fn test_start_task_with_events() {
2513        let ctx = TestContext::new().await;
2514        let manager = TaskManager::new(ctx.pool());
2515
2516        let task = manager
2517            .add_task("Test task".to_string(), None, None, None, None, None)
2518            .await
2519            .unwrap();
2520
2521        // Add an event
2522        sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
2523            .bind(task.id)
2524            .bind("test")
2525            .bind("test event")
2526            .execute(ctx.pool())
2527            .await
2528            .unwrap();
2529
2530        let started = manager.start_task(task.id, true).await.unwrap();
2531
2532        assert!(started.events_summary.is_some());
2533        let summary = started.events_summary.unwrap();
2534        assert_eq!(summary.total_count, 1);
2535    }
2536
2537    #[tokio::test]
2538    async fn test_done_task() {
2539        let ctx = TestContext::new().await;
2540        let manager = TaskManager::new(ctx.pool());
2541
2542        let task = manager
2543            .add_task("Test task".to_string(), None, None, None, None, None)
2544            .await
2545            .unwrap();
2546        manager.start_task(task.id, false).await.unwrap();
2547        let response = manager.done_task(false).await.unwrap();
2548
2549        assert_eq!(response.completed_task.status, "done");
2550        assert!(response.completed_task.first_done_at.is_some());
2551        assert_eq!(response.workspace_status.current_task_id, None);
2552
2553        // Should be WORKSPACE_IS_CLEAR since it's the only task
2554        match response.next_step_suggestion {
2555            NextStepSuggestion::WorkspaceIsClear { .. } => {},
2556            _ => panic!("Expected WorkspaceIsClear suggestion"),
2557        }
2558
2559        // Verify current task is cleared
2560        let session_id = crate::workspace::resolve_session_id(None);
2561        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2562            "SELECT current_task_id FROM sessions WHERE session_id = ?",
2563        )
2564        .bind(&session_id)
2565        .fetch_optional(ctx.pool())
2566        .await
2567        .unwrap()
2568        .flatten();
2569
2570        assert!(current.is_none());
2571    }
2572
2573    #[tokio::test]
2574    async fn test_done_task_with_uncompleted_children() {
2575        let ctx = TestContext::new().await;
2576        let manager = TaskManager::new(ctx.pool());
2577
2578        let parent = manager
2579            .add_task("Parent".to_string(), None, None, None, None, None)
2580            .await
2581            .unwrap();
2582        manager
2583            .add_task("Child".to_string(), None, Some(parent.id), None, None, None)
2584            .await
2585            .unwrap();
2586
2587        // Set parent as current task
2588        manager.start_task(parent.id, false).await.unwrap();
2589
2590        let result = manager.done_task(false).await;
2591        assert!(matches!(result, Err(IntentError::UncompletedChildren)));
2592    }
2593
2594    #[tokio::test]
2595    async fn test_done_task_with_completed_children() {
2596        let ctx = TestContext::new().await;
2597        let manager = TaskManager::new(ctx.pool());
2598
2599        let parent = manager
2600            .add_task("Parent".to_string(), None, None, None, None, None)
2601            .await
2602            .unwrap();
2603        let child = manager
2604            .add_task("Child".to_string(), None, Some(parent.id), None, None, None)
2605            .await
2606            .unwrap();
2607
2608        // Complete child first
2609        manager.start_task(child.id, false).await.unwrap();
2610        let child_response = manager.done_task(false).await.unwrap();
2611
2612        // Child completion should suggest parent is ready
2613        match child_response.next_step_suggestion {
2614            NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
2615                assert_eq!(parent_task_id, parent.id);
2616            },
2617            _ => panic!("Expected ParentIsReady suggestion"),
2618        }
2619
2620        // Now parent can be completed
2621        manager.start_task(parent.id, false).await.unwrap();
2622        let parent_response = manager.done_task(false).await.unwrap();
2623        assert_eq!(parent_response.completed_task.status, "done");
2624
2625        // Parent completion should indicate top-level task completed (since it had children)
2626        match parent_response.next_step_suggestion {
2627            NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
2628            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
2629        }
2630    }
2631
2632    #[tokio::test]
2633    async fn test_circular_dependency() {
2634        let ctx = TestContext::new().await;
2635        let manager = TaskManager::new(ctx.pool());
2636
2637        let task1 = manager
2638            .add_task("Task 1".to_string(), None, None, None, None, None)
2639            .await
2640            .unwrap();
2641        let task2 = manager
2642            .add_task("Task 2".to_string(), None, Some(task1.id), None, None, None)
2643            .await
2644            .unwrap();
2645
2646        // Try to make task1 a child of task2 (circular)
2647        let result = manager
2648            .update_task(
2649                task1.id,
2650                TaskUpdate {
2651                    parent_id: Some(Some(task2.id)),
2652                    ..Default::default()
2653                },
2654            )
2655            .await;
2656
2657        assert!(matches!(
2658            result,
2659            Err(IntentError::CircularDependency { .. })
2660        ));
2661    }
2662
2663    #[tokio::test]
2664    async fn test_invalid_parent_id() {
2665        let ctx = TestContext::new().await;
2666        let manager = TaskManager::new(ctx.pool());
2667
2668        let result = manager
2669            .add_task("Test".to_string(), None, Some(999), None, None, None)
2670            .await;
2671        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2672    }
2673
2674    #[tokio::test]
2675    async fn test_update_task_complexity_and_priority() {
2676        let ctx = TestContext::new().await;
2677        let manager = TaskManager::new(ctx.pool());
2678
2679        let task = manager
2680            .add_task("Test task".to_string(), None, None, None, None, None)
2681            .await
2682            .unwrap();
2683        let updated = manager
2684            .update_task(
2685                task.id,
2686                TaskUpdate {
2687                    complexity: Some(8),
2688                    priority: Some(10),
2689                    ..Default::default()
2690                },
2691            )
2692            .await
2693            .unwrap();
2694
2695        assert_eq!(updated.complexity, Some(8));
2696        assert_eq!(updated.priority, Some(10));
2697    }
2698
2699    #[tokio::test]
2700    async fn test_spawn_subtask() {
2701        let ctx = TestContext::new().await;
2702        let manager = TaskManager::new(ctx.pool());
2703
2704        // Create and start a parent task
2705        let parent = manager
2706            .add_task("Parent task".to_string(), None, None, None, None, None)
2707            .await
2708            .unwrap();
2709        manager.start_task(parent.id, false).await.unwrap();
2710
2711        // Spawn a subtask
2712        let response = manager
2713            .spawn_subtask("Child task", Some("Details"))
2714            .await
2715            .unwrap();
2716
2717        assert_eq!(response.subtask.parent_id, parent.id);
2718        assert_eq!(response.subtask.name, "Child task");
2719        assert_eq!(response.subtask.status, "doing");
2720        assert_eq!(response.parent_task.id, parent.id);
2721        assert_eq!(response.parent_task.name, "Parent task");
2722
2723        // Verify subtask is now the current task
2724        let session_id = crate::workspace::resolve_session_id(None);
2725        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2726            "SELECT current_task_id FROM sessions WHERE session_id = ?",
2727        )
2728        .bind(&session_id)
2729        .fetch_optional(ctx.pool())
2730        .await
2731        .unwrap()
2732        .flatten();
2733
2734        assert_eq!(current, Some(response.subtask.id));
2735
2736        // Verify subtask is in doing status
2737        let retrieved = manager.get_task(response.subtask.id).await.unwrap();
2738        assert_eq!(retrieved.status, "doing");
2739    }
2740
2741    #[tokio::test]
2742    async fn test_spawn_subtask_no_current_task() {
2743        let ctx = TestContext::new().await;
2744        let manager = TaskManager::new(ctx.pool());
2745
2746        // Try to spawn subtask without a current task
2747        let result = manager.spawn_subtask("Child", None).await;
2748        assert!(result.is_err());
2749    }
2750
2751    #[tokio::test]
2752    async fn test_pick_next_tasks_basic() {
2753        let ctx = TestContext::new().await;
2754        let manager = TaskManager::new(ctx.pool());
2755
2756        // Create 10 todo tasks
2757        for i in 1..=10 {
2758            manager
2759                .add_task(format!("Task {}", i), None, None, None, None, None)
2760                .await
2761                .unwrap();
2762        }
2763
2764        // Pick 5 tasks with capacity limit of 5
2765        let picked = manager.pick_next_tasks(5, 5).await.unwrap();
2766
2767        assert_eq!(picked.len(), 5);
2768        for task in &picked {
2769            assert_eq!(task.status, "doing");
2770            assert!(task.first_doing_at.is_some());
2771        }
2772
2773        // Verify total doing count
2774        let doing_count: i64 =
2775            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2776                .fetch_one(ctx.pool())
2777                .await
2778                .unwrap();
2779
2780        assert_eq!(doing_count, 5);
2781    }
2782
2783    #[tokio::test]
2784    async fn test_pick_next_tasks_with_existing_doing() {
2785        let ctx = TestContext::new().await;
2786        let manager = TaskManager::new(ctx.pool());
2787
2788        // Create 10 todo tasks
2789        for i in 1..=10 {
2790            manager
2791                .add_task(format!("Task {}", i), None, None, None, None, None)
2792                .await
2793                .unwrap();
2794        }
2795
2796        // Start 2 tasks
2797        let result = manager
2798            .find_tasks(Some("todo".to_string()), None, None, None, None)
2799            .await
2800            .unwrap();
2801        manager.start_task(result.tasks[0].id, false).await.unwrap();
2802        manager.start_task(result.tasks[1].id, false).await.unwrap();
2803
2804        // Pick more tasks with capacity limit of 5
2805        let picked = manager.pick_next_tasks(10, 5).await.unwrap();
2806
2807        // Should only pick 3 more (5 - 2 = 3)
2808        assert_eq!(picked.len(), 3);
2809
2810        // Verify total doing count
2811        let doing_count: i64 =
2812            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2813                .fetch_one(ctx.pool())
2814                .await
2815                .unwrap();
2816
2817        assert_eq!(doing_count, 5);
2818    }
2819
2820    #[tokio::test]
2821    async fn test_pick_next_tasks_at_capacity() {
2822        let ctx = TestContext::new().await;
2823        let manager = TaskManager::new(ctx.pool());
2824
2825        // Create 10 tasks
2826        for i in 1..=10 {
2827            manager
2828                .add_task(format!("Task {}", i), None, None, None, None, None)
2829                .await
2830                .unwrap();
2831        }
2832
2833        // Fill capacity
2834        let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2835        assert_eq!(first_batch.len(), 5);
2836
2837        // Try to pick more (should return empty)
2838        let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2839        assert_eq!(second_batch.len(), 0);
2840    }
2841
2842    #[tokio::test]
2843    async fn test_pick_next_tasks_priority_ordering() {
2844        let ctx = TestContext::new().await;
2845        let manager = TaskManager::new(ctx.pool());
2846
2847        // Create tasks with different priorities
2848        let low = manager
2849            .add_task("Low priority".to_string(), None, None, None, None, None)
2850            .await
2851            .unwrap();
2852        manager
2853            .update_task(
2854                low.id,
2855                TaskUpdate {
2856                    priority: Some(1),
2857                    ..Default::default()
2858                },
2859            )
2860            .await
2861            .unwrap();
2862
2863        let high = manager
2864            .add_task("High priority".to_string(), None, None, None, None, None)
2865            .await
2866            .unwrap();
2867        manager
2868            .update_task(
2869                high.id,
2870                TaskUpdate {
2871                    priority: Some(10),
2872                    ..Default::default()
2873                },
2874            )
2875            .await
2876            .unwrap();
2877
2878        let medium = manager
2879            .add_task("Medium priority".to_string(), None, None, None, None, None)
2880            .await
2881            .unwrap();
2882        manager
2883            .update_task(
2884                medium.id,
2885                TaskUpdate {
2886                    priority: Some(5),
2887                    ..Default::default()
2888                },
2889            )
2890            .await
2891            .unwrap();
2892
2893        // Pick tasks
2894        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2895
2896        // Should be ordered by priority ASC (lower number = higher priority)
2897        assert_eq!(picked.len(), 3);
2898        assert_eq!(picked[0].priority, Some(1)); // lowest number = highest priority
2899        assert_eq!(picked[1].priority, Some(5)); // medium
2900        assert_eq!(picked[2].priority, Some(10)); // highest number = lowest priority
2901    }
2902
2903    #[tokio::test]
2904    async fn test_pick_next_tasks_complexity_ordering() {
2905        let ctx = TestContext::new().await;
2906        let manager = TaskManager::new(ctx.pool());
2907
2908        // Create tasks with different complexities (same priority)
2909        let complex = manager
2910            .add_task("Complex".to_string(), None, None, None, None, None)
2911            .await
2912            .unwrap();
2913        manager
2914            .update_task(
2915                complex.id,
2916                TaskUpdate {
2917                    complexity: Some(9),
2918                    priority: Some(5),
2919                    ..Default::default()
2920                },
2921            )
2922            .await
2923            .unwrap();
2924
2925        let simple = manager
2926            .add_task("Simple".to_string(), None, None, None, None, None)
2927            .await
2928            .unwrap();
2929        manager
2930            .update_task(
2931                simple.id,
2932                TaskUpdate {
2933                    complexity: Some(1),
2934                    priority: Some(5),
2935                    ..Default::default()
2936                },
2937            )
2938            .await
2939            .unwrap();
2940
2941        let medium = manager
2942            .add_task("Medium".to_string(), None, None, None, None, None)
2943            .await
2944            .unwrap();
2945        manager
2946            .update_task(
2947                medium.id,
2948                TaskUpdate {
2949                    complexity: Some(5),
2950                    priority: Some(5),
2951                    ..Default::default()
2952                },
2953            )
2954            .await
2955            .unwrap();
2956
2957        // Pick tasks
2958        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2959
2960        // Should be ordered by complexity ASC (simple first)
2961        assert_eq!(picked.len(), 3);
2962        assert_eq!(picked[0].complexity, Some(1)); // simple
2963        assert_eq!(picked[1].complexity, Some(5)); // medium
2964        assert_eq!(picked[2].complexity, Some(9)); // complex
2965    }
2966
2967    #[tokio::test]
2968    async fn test_done_task_sibling_tasks_remain() {
2969        let ctx = TestContext::new().await;
2970        let manager = TaskManager::new(ctx.pool());
2971
2972        // Create parent with multiple children
2973        let parent = manager
2974            .add_task("Parent Task".to_string(), None, None, None, None, None)
2975            .await
2976            .unwrap();
2977        let child1 = manager
2978            .add_task(
2979                "Child 1".to_string(),
2980                None,
2981                Some(parent.id),
2982                None,
2983                None,
2984                None,
2985            )
2986            .await
2987            .unwrap();
2988        let child2 = manager
2989            .add_task(
2990                "Child 2".to_string(),
2991                None,
2992                Some(parent.id),
2993                None,
2994                None,
2995                None,
2996            )
2997            .await
2998            .unwrap();
2999        let _child3 = manager
3000            .add_task(
3001                "Child 3".to_string(),
3002                None,
3003                Some(parent.id),
3004                None,
3005                None,
3006                None,
3007            )
3008            .await
3009            .unwrap();
3010
3011        // Complete first child
3012        manager.start_task(child1.id, false).await.unwrap();
3013        let response = manager.done_task(false).await.unwrap();
3014
3015        // Should indicate siblings remain
3016        match response.next_step_suggestion {
3017            NextStepSuggestion::SiblingTasksRemain {
3018                parent_task_id,
3019                remaining_siblings_count,
3020                ..
3021            } => {
3022                assert_eq!(parent_task_id, parent.id);
3023                assert_eq!(remaining_siblings_count, 2); // child2 and child3
3024            },
3025            _ => panic!("Expected SiblingTasksRemain suggestion"),
3026        }
3027
3028        // Complete second child
3029        manager.start_task(child2.id, false).await.unwrap();
3030        let response2 = manager.done_task(false).await.unwrap();
3031
3032        // Should still indicate siblings remain
3033        match response2.next_step_suggestion {
3034            NextStepSuggestion::SiblingTasksRemain {
3035                remaining_siblings_count,
3036                ..
3037            } => {
3038                assert_eq!(remaining_siblings_count, 1); // only child3
3039            },
3040            _ => panic!("Expected SiblingTasksRemain suggestion"),
3041        }
3042    }
3043
3044    #[tokio::test]
3045    async fn test_done_task_top_level_with_children() {
3046        let ctx = TestContext::new().await;
3047        let manager = TaskManager::new(ctx.pool());
3048
3049        // Create top-level task with children
3050        let parent = manager
3051            .add_task("Epic Task".to_string(), None, None, None, None, None)
3052            .await
3053            .unwrap();
3054        let child = manager
3055            .add_task(
3056                "Sub Task".to_string(),
3057                None,
3058                Some(parent.id),
3059                None,
3060                None,
3061                None,
3062            )
3063            .await
3064            .unwrap();
3065
3066        // Complete child first
3067        manager.start_task(child.id, false).await.unwrap();
3068        manager.done_task(false).await.unwrap();
3069
3070        // Complete parent
3071        manager.start_task(parent.id, false).await.unwrap();
3072        let response = manager.done_task(false).await.unwrap();
3073
3074        // Should be TOP_LEVEL_TASK_COMPLETED
3075        match response.next_step_suggestion {
3076            NextStepSuggestion::TopLevelTaskCompleted {
3077                completed_task_id,
3078                completed_task_name,
3079                ..
3080            } => {
3081                assert_eq!(completed_task_id, parent.id);
3082                assert_eq!(completed_task_name, "Epic Task");
3083            },
3084            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
3085        }
3086    }
3087
3088    #[tokio::test]
3089    async fn test_done_task_no_parent_context() {
3090        let ctx = TestContext::new().await;
3091        let manager = TaskManager::new(ctx.pool());
3092
3093        // Create multiple standalone tasks
3094        let task1 = manager
3095            .add_task(
3096                "Standalone Task 1".to_string(),
3097                None,
3098                None,
3099                None,
3100                None,
3101                None,
3102            )
3103            .await
3104            .unwrap();
3105        let _task2 = manager
3106            .add_task(
3107                "Standalone Task 2".to_string(),
3108                None,
3109                None,
3110                None,
3111                None,
3112                None,
3113            )
3114            .await
3115            .unwrap();
3116
3117        // Complete first task
3118        manager.start_task(task1.id, false).await.unwrap();
3119        let response = manager.done_task(false).await.unwrap();
3120
3121        // Should be NO_PARENT_CONTEXT since task2 is still pending
3122        match response.next_step_suggestion {
3123            NextStepSuggestion::NoParentContext {
3124                completed_task_id,
3125                completed_task_name,
3126                ..
3127            } => {
3128                assert_eq!(completed_task_id, task1.id);
3129                assert_eq!(completed_task_name, "Standalone Task 1");
3130            },
3131            _ => panic!("Expected NoParentContext suggestion"),
3132        }
3133    }
3134
3135    // =========================================================================
3136    // done_task_by_id tests
3137    // =========================================================================
3138
3139    #[tokio::test]
3140    async fn test_done_task_by_id_non_focused_task_preserves_focus() {
3141        let ctx = TestContext::new().await;
3142        let manager = TaskManager::new(ctx.pool());
3143
3144        // Create two tasks, focus on task_a
3145        let task_a = manager
3146            .add_task("Task A".to_string(), None, None, None, None, None)
3147            .await
3148            .unwrap();
3149        let task_b = manager
3150            .add_task("Task B".to_string(), None, None, None, None, None)
3151            .await
3152            .unwrap();
3153        manager.start_task(task_a.id, false).await.unwrap();
3154
3155        // Complete task_b by ID (not the focused task)
3156        let response = manager.done_task_by_id(task_b.id, false).await.unwrap();
3157
3158        // task_b should be done
3159        assert_eq!(response.completed_task.status, "done");
3160        assert_eq!(response.completed_task.id, task_b.id);
3161
3162        // Focus should still be on task_a
3163        assert_eq!(response.workspace_status.current_task_id, Some(task_a.id));
3164
3165        // Verify via direct DB query
3166        let session_id = crate::workspace::resolve_session_id(None);
3167        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
3168            "SELECT current_task_id FROM sessions WHERE session_id = ?",
3169        )
3170        .bind(&session_id)
3171        .fetch_optional(ctx.pool())
3172        .await
3173        .unwrap()
3174        .flatten();
3175        assert_eq!(current, Some(task_a.id));
3176    }
3177
3178    #[tokio::test]
3179    async fn test_done_task_by_id_focused_task_clears_focus() {
3180        let ctx = TestContext::new().await;
3181        let manager = TaskManager::new(ctx.pool());
3182
3183        let task = manager
3184            .add_task("Focused task".to_string(), None, None, None, None, None)
3185            .await
3186            .unwrap();
3187        manager.start_task(task.id, false).await.unwrap();
3188
3189        // Complete the focused task by ID
3190        let response = manager.done_task_by_id(task.id, false).await.unwrap();
3191
3192        assert_eq!(response.completed_task.status, "done");
3193        assert_eq!(response.workspace_status.current_task_id, None);
3194
3195        // Verify via direct DB query
3196        let session_id = crate::workspace::resolve_session_id(None);
3197        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
3198            "SELECT current_task_id FROM sessions WHERE session_id = ?",
3199        )
3200        .bind(&session_id)
3201        .fetch_optional(ctx.pool())
3202        .await
3203        .unwrap()
3204        .flatten();
3205        assert!(current.is_none());
3206    }
3207
3208    #[tokio::test]
3209    async fn test_done_task_by_id_human_task_rejected_for_ai_caller() {
3210        let ctx = TestContext::new().await;
3211        let manager = TaskManager::new(ctx.pool());
3212
3213        // Create a human-owned task and set it to doing
3214        let task = manager
3215            .add_task(
3216                "Human task".to_string(),
3217                None,
3218                None,
3219                Some("human".to_string()),
3220                None,
3221                None,
3222            )
3223            .await
3224            .unwrap();
3225        manager
3226            .update_task(
3227                task.id,
3228                TaskUpdate {
3229                    status: Some("doing"),
3230                    ..Default::default()
3231                },
3232            )
3233            .await
3234            .unwrap();
3235
3236        // AI caller should be rejected
3237        let result = manager.done_task_by_id(task.id, true).await;
3238        assert!(matches!(
3239            result,
3240            Err(IntentError::HumanTaskCannotBeCompletedByAI { .. })
3241        ));
3242
3243        // Human caller should succeed
3244        let response = manager.done_task_by_id(task.id, false).await.unwrap();
3245        assert_eq!(response.completed_task.status, "done");
3246    }
3247
3248    #[tokio::test]
3249    async fn test_done_task_by_id_with_uncompleted_children() {
3250        let ctx = TestContext::new().await;
3251        let manager = TaskManager::new(ctx.pool());
3252
3253        let parent = manager
3254            .add_task("Parent".to_string(), None, None, None, None, None)
3255            .await
3256            .unwrap();
3257        manager
3258            .add_task(
3259                "Incomplete child".to_string(),
3260                None,
3261                Some(parent.id),
3262                None,
3263                None,
3264                None,
3265            )
3266            .await
3267            .unwrap();
3268
3269        let result = manager.done_task_by_id(parent.id, false).await;
3270        assert!(matches!(result, Err(IntentError::UncompletedChildren)));
3271    }
3272
3273    #[tokio::test]
3274    async fn test_done_task_by_id_nonexistent_task() {
3275        let ctx = TestContext::new().await;
3276        let manager = TaskManager::new(ctx.pool());
3277
3278        let result = manager.done_task_by_id(99999, false).await;
3279        assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
3280    }
3281
3282    #[tokio::test]
3283    async fn test_done_task_synthesis_graceful_when_llm_unconfigured() {
3284        // Verify that task completion works even when LLM is not configured
3285        let ctx = TestContext::new().await;
3286        let manager = TaskManager::new(ctx.pool());
3287        let event_mgr = EventManager::new(ctx.pool());
3288
3289        // Create and complete a task
3290        let task = manager
3291            .add_task(
3292                "Test Task".to_string(),
3293                Some("Original spec".to_string()),
3294                None,
3295                Some("ai".to_string()),
3296                None,
3297                None,
3298            )
3299            .await
3300            .unwrap();
3301
3302        // Add some events
3303        event_mgr
3304            .add_event(task.id, "decision".to_string(), "Test decision".to_string())
3305            .await
3306            .unwrap();
3307
3308        manager.start_task(task.id, false).await.unwrap();
3309
3310        // Should complete successfully even without LLM
3311        let result = manager.done_task_by_id(task.id, false).await;
3312        assert!(result.is_ok(), "Task completion should succeed without LLM");
3313
3314        // Verify task is actually done
3315        let completed_task = manager.get_task(task.id).await.unwrap();
3316        assert_eq!(completed_task.status, "done");
3317
3318        // Original spec should be unchanged (no synthesis happened)
3319        assert_eq!(completed_task.spec, Some("Original spec".to_string()));
3320    }
3321
3322    #[tokio::test]
3323    async fn test_done_task_synthesis_respects_owner_field() {
3324        // This test verifies the owner field logic without actual LLM
3325        let ctx = TestContext::new().await;
3326        let manager = TaskManager::new(ctx.pool());
3327
3328        // Create AI-owned task
3329        let ai_task = manager
3330            .add_task(
3331                "AI Task".to_string(),
3332                Some("AI spec".to_string()),
3333                None,
3334                Some("ai".to_string()),
3335                None,
3336                None,
3337            )
3338            .await
3339            .unwrap();
3340        assert_eq!(ai_task.owner, "ai");
3341
3342        // Create human-owned task
3343        let human_task = manager
3344            .add_task(
3345                "Human Task".to_string(),
3346                Some("Human spec".to_string()),
3347                None,
3348                Some("human".to_string()),
3349                None,
3350                None,
3351            )
3352            .await
3353            .unwrap();
3354        assert_eq!(human_task.owner, "human");
3355
3356        // Both should complete successfully
3357        manager.start_task(ai_task.id, false).await.unwrap();
3358        let result = manager.done_task_by_id(ai_task.id, false).await;
3359        assert!(result.is_ok());
3360
3361        manager.start_task(human_task.id, false).await.unwrap();
3362        let result = manager.done_task_by_id(human_task.id, false).await;
3363        assert!(result.is_ok());
3364    }
3365
3366    #[tokio::test]
3367    async fn test_try_synthesize_task_description_basic() {
3368        let ctx = TestContext::new().await;
3369        let manager = TaskManager::new(ctx.pool());
3370
3371        let task = manager
3372            .add_task(
3373                "Synthesis Test".to_string(),
3374                Some("Original".to_string()),
3375                None,
3376                None,
3377                None,
3378                None,
3379            )
3380            .await
3381            .unwrap();
3382
3383        // Should return None when LLM not configured (graceful degradation)
3384        let result = manager
3385            .try_synthesize_task_description(task.id, &task.name)
3386            .await;
3387
3388        assert!(result.is_ok(), "Should not error when LLM unconfigured");
3389        assert_eq!(
3390            result.unwrap(),
3391            None,
3392            "Should return None when LLM unconfigured"
3393        );
3394    }
3395
3396    #[tokio::test]
3397    async fn test_pick_next_focused_subtask() {
3398        let ctx = TestContext::new().await;
3399        let manager = TaskManager::new(ctx.pool());
3400
3401        // Create parent task and set as current
3402        let parent = manager
3403            .add_task("Parent task".to_string(), None, None, None, None, None)
3404            .await
3405            .unwrap();
3406        manager.start_task(parent.id, false).await.unwrap();
3407
3408        // Create subtasks with different priorities
3409        let subtask1 = manager
3410            .add_task(
3411                "Subtask 1".to_string(),
3412                None,
3413                Some(parent.id),
3414                None,
3415                None,
3416                None,
3417            )
3418            .await
3419            .unwrap();
3420        let subtask2 = manager
3421            .add_task(
3422                "Subtask 2".to_string(),
3423                None,
3424                Some(parent.id),
3425                None,
3426                None,
3427                None,
3428            )
3429            .await
3430            .unwrap();
3431
3432        // Set priorities: subtask1 = 2, subtask2 = 1 (lower number = higher priority)
3433        manager
3434            .update_task(
3435                subtask1.id,
3436                TaskUpdate {
3437                    priority: Some(2),
3438                    ..Default::default()
3439                },
3440            )
3441            .await
3442            .unwrap();
3443        manager
3444            .update_task(
3445                subtask2.id,
3446                TaskUpdate {
3447                    priority: Some(1),
3448                    ..Default::default()
3449                },
3450            )
3451            .await
3452            .unwrap();
3453
3454        // Pick next should recommend subtask2 (priority 1)
3455        let response = manager.pick_next().await.unwrap();
3456
3457        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
3458        assert!(response.task.is_some());
3459        assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
3460        assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
3461    }
3462
3463    #[tokio::test]
3464    async fn test_pick_next_top_level_task() {
3465        let ctx = TestContext::new().await;
3466        let manager = TaskManager::new(ctx.pool());
3467
3468        // Create top-level tasks with different priorities
3469        let task1 = manager
3470            .add_task("Task 1".to_string(), None, None, None, None, None)
3471            .await
3472            .unwrap();
3473        let task2 = manager
3474            .add_task("Task 2".to_string(), None, None, None, None, None)
3475            .await
3476            .unwrap();
3477
3478        // Set priorities: task1 = 5, task2 = 3 (lower number = higher priority)
3479        manager
3480            .update_task(
3481                task1.id,
3482                TaskUpdate {
3483                    priority: Some(5),
3484                    ..Default::default()
3485                },
3486            )
3487            .await
3488            .unwrap();
3489        manager
3490            .update_task(
3491                task2.id,
3492                TaskUpdate {
3493                    priority: Some(3),
3494                    ..Default::default()
3495                },
3496            )
3497            .await
3498            .unwrap();
3499
3500        // Pick next should recommend task2 (priority 3)
3501        let response = manager.pick_next().await.unwrap();
3502
3503        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
3504        assert!(response.task.is_some());
3505        assert_eq!(response.task.as_ref().unwrap().id, task2.id);
3506        assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
3507    }
3508
3509    #[tokio::test]
3510    async fn test_pick_next_no_tasks() {
3511        let ctx = TestContext::new().await;
3512        let manager = TaskManager::new(ctx.pool());
3513
3514        // No tasks created
3515        let response = manager.pick_next().await.unwrap();
3516
3517        assert_eq!(response.suggestion_type, "NONE");
3518        assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
3519        assert!(response.message.is_some());
3520    }
3521
3522    #[tokio::test]
3523    async fn test_pick_next_all_completed() {
3524        let ctx = TestContext::new().await;
3525        let manager = TaskManager::new(ctx.pool());
3526
3527        // Create task and mark as done
3528        let task = manager
3529            .add_task("Task 1".to_string(), None, None, None, None, None)
3530            .await
3531            .unwrap();
3532        manager.start_task(task.id, false).await.unwrap();
3533        manager.done_task(false).await.unwrap();
3534
3535        // Pick next should indicate all tasks completed
3536        let response = manager.pick_next().await.unwrap();
3537
3538        assert_eq!(response.suggestion_type, "NONE");
3539        assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
3540        assert!(response.message.is_some());
3541    }
3542
3543    #[tokio::test]
3544    async fn test_pick_next_no_available_todos() {
3545        let ctx = TestContext::new().await;
3546        let manager = TaskManager::new(ctx.pool());
3547
3548        // Create a parent task that's in "doing" status
3549        let parent = manager
3550            .add_task("Parent task".to_string(), None, None, None, None, None)
3551            .await
3552            .unwrap();
3553        manager.start_task(parent.id, false).await.unwrap();
3554
3555        // Create a subtask also in "doing" status (no "todo" subtasks)
3556        let subtask = manager
3557            .add_task(
3558                "Subtask".to_string(),
3559                None,
3560                Some(parent.id),
3561                None,
3562                None,
3563                None,
3564            )
3565            .await
3566            .unwrap();
3567        // Switch to subtask (this will set parent back to todo, so we need to manually set subtask to doing)
3568        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
3569            .bind(subtask.id)
3570            .execute(ctx.pool())
3571            .await
3572            .unwrap();
3573
3574        // Set subtask as current
3575        let session_id = crate::workspace::resolve_session_id(None);
3576        sqlx::query(
3577            r#"
3578            INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
3579            VALUES (?, ?, datetime('now'), datetime('now'))
3580            ON CONFLICT(session_id) DO UPDATE SET
3581                current_task_id = excluded.current_task_id,
3582                last_active_at = datetime('now')
3583            "#,
3584        )
3585        .bind(&session_id)
3586        .bind(subtask.id)
3587        .execute(ctx.pool())
3588        .await
3589        .unwrap();
3590
3591        // Set parent to doing (not todo)
3592        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
3593            .bind(parent.id)
3594            .execute(ctx.pool())
3595            .await
3596            .unwrap();
3597
3598        // With multi-doing semantics, pick next should recommend the doing parent
3599        // (it's a valid top-level doing task that's not current)
3600        let response = manager.pick_next().await.unwrap();
3601
3602        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
3603        assert_eq!(response.task.as_ref().unwrap().id, parent.id);
3604        assert_eq!(response.task.as_ref().unwrap().status, "doing");
3605    }
3606
3607    #[tokio::test]
3608    async fn test_pick_next_priority_ordering() {
3609        let ctx = TestContext::new().await;
3610        let manager = TaskManager::new(ctx.pool());
3611
3612        // Create parent and set as current
3613        let parent = manager
3614            .add_task("Parent".to_string(), None, None, None, None, None)
3615            .await
3616            .unwrap();
3617        manager.start_task(parent.id, false).await.unwrap();
3618
3619        // Create multiple subtasks with various priorities
3620        let sub1 = manager
3621            .add_task(
3622                "Priority 10".to_string(),
3623                None,
3624                Some(parent.id),
3625                None,
3626                None,
3627                None,
3628            )
3629            .await
3630            .unwrap();
3631        manager
3632            .update_task(
3633                sub1.id,
3634                TaskUpdate {
3635                    priority: Some(10),
3636                    ..Default::default()
3637                },
3638            )
3639            .await
3640            .unwrap();
3641
3642        let sub2 = manager
3643            .add_task(
3644                "Priority 1".to_string(),
3645                None,
3646                Some(parent.id),
3647                None,
3648                None,
3649                None,
3650            )
3651            .await
3652            .unwrap();
3653        manager
3654            .update_task(
3655                sub2.id,
3656                TaskUpdate {
3657                    priority: Some(1),
3658                    ..Default::default()
3659                },
3660            )
3661            .await
3662            .unwrap();
3663
3664        let sub3 = manager
3665            .add_task(
3666                "Priority 5".to_string(),
3667                None,
3668                Some(parent.id),
3669                None,
3670                None,
3671                None,
3672            )
3673            .await
3674            .unwrap();
3675        manager
3676            .update_task(
3677                sub3.id,
3678                TaskUpdate {
3679                    priority: Some(5),
3680                    ..Default::default()
3681                },
3682            )
3683            .await
3684            .unwrap();
3685
3686        // Pick next should recommend the task with priority 1 (lowest number)
3687        let response = manager.pick_next().await.unwrap();
3688
3689        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
3690        assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
3691        assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
3692    }
3693
3694    #[tokio::test]
3695    async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
3696        let ctx = TestContext::new().await;
3697        let manager = TaskManager::new(ctx.pool());
3698
3699        // Create parent without subtasks and set as current
3700        let parent = manager
3701            .add_task("Parent".to_string(), None, None, None, None, None)
3702            .await
3703            .unwrap();
3704        manager.start_task(parent.id, false).await.unwrap();
3705
3706        // Create another top-level task
3707        let top_level = manager
3708            .add_task("Top level task".to_string(), None, None, None, None, None)
3709            .await
3710            .unwrap();
3711
3712        // Pick next should fall back to top-level task since parent has no todo subtasks
3713        let response = manager.pick_next().await.unwrap();
3714
3715        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
3716        assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
3717    }
3718
3719    // ===== Missing coverage tests =====
3720
3721    #[tokio::test]
3722    async fn test_get_task_with_events() {
3723        let ctx = TestContext::new().await;
3724        let task_mgr = TaskManager::new(ctx.pool());
3725        let event_mgr = EventManager::new(ctx.pool());
3726
3727        let task = task_mgr
3728            .add_task("Test".to_string(), None, None, None, None, None)
3729            .await
3730            .unwrap();
3731
3732        // Add some events
3733        event_mgr
3734            .add_event(task.id, "progress".to_string(), "Event 1".to_string())
3735            .await
3736            .unwrap();
3737        event_mgr
3738            .add_event(task.id, "decision".to_string(), "Event 2".to_string())
3739            .await
3740            .unwrap();
3741
3742        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
3743
3744        assert_eq!(result.task.id, task.id);
3745        assert!(result.events_summary.is_some());
3746
3747        let summary = result.events_summary.unwrap();
3748        assert_eq!(summary.total_count, 2);
3749        assert_eq!(summary.recent_events.len(), 2);
3750        assert_eq!(summary.recent_events[0].log_type, "decision"); // Most recent first
3751        assert_eq!(summary.recent_events[1].log_type, "progress");
3752    }
3753
3754    #[tokio::test]
3755    async fn test_get_task_with_events_nonexistent() {
3756        let ctx = TestContext::new().await;
3757        let task_mgr = TaskManager::new(ctx.pool());
3758
3759        let result = task_mgr.get_task_with_events(999).await;
3760        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
3761    }
3762
3763    #[tokio::test]
3764    async fn test_get_task_with_many_events() {
3765        let ctx = TestContext::new().await;
3766        let task_mgr = TaskManager::new(ctx.pool());
3767        let event_mgr = EventManager::new(ctx.pool());
3768
3769        let task = task_mgr
3770            .add_task("Test".to_string(), None, None, None, None, None)
3771            .await
3772            .unwrap();
3773
3774        // Add 20 events
3775        for i in 0..20 {
3776            event_mgr
3777                .add_event(task.id, "test".to_string(), format!("Event {}", i))
3778                .await
3779                .unwrap();
3780        }
3781
3782        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
3783        let summary = result.events_summary.unwrap();
3784
3785        assert_eq!(summary.total_count, 20);
3786        assert_eq!(summary.recent_events.len(), 10); // Limited to 10
3787    }
3788
3789    #[tokio::test]
3790    async fn test_get_task_with_no_events() {
3791        let ctx = TestContext::new().await;
3792        let task_mgr = TaskManager::new(ctx.pool());
3793
3794        let task = task_mgr
3795            .add_task("Test".to_string(), None, None, None, None, None)
3796            .await
3797            .unwrap();
3798
3799        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
3800        let summary = result.events_summary.unwrap();
3801
3802        assert_eq!(summary.total_count, 0);
3803        assert_eq!(summary.recent_events.len(), 0);
3804    }
3805
3806    #[tokio::test]
3807    async fn test_pick_next_tasks_zero_capacity() {
3808        let ctx = TestContext::new().await;
3809        let task_mgr = TaskManager::new(ctx.pool());
3810
3811        task_mgr
3812            .add_task("Task 1".to_string(), None, None, None, None, None)
3813            .await
3814            .unwrap();
3815
3816        // capacity_limit = 0 means no capacity available
3817        let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
3818        assert_eq!(results.len(), 0);
3819    }
3820
3821    #[tokio::test]
3822    async fn test_pick_next_tasks_capacity_exceeds_available() {
3823        let ctx = TestContext::new().await;
3824        let task_mgr = TaskManager::new(ctx.pool());
3825
3826        task_mgr
3827            .add_task("Task 1".to_string(), None, None, None, None, None)
3828            .await
3829            .unwrap();
3830        task_mgr
3831            .add_task("Task 2".to_string(), None, None, None, None, None)
3832            .await
3833            .unwrap();
3834
3835        // Request 10 tasks but only 2 available, capacity = 100
3836        let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
3837        assert_eq!(results.len(), 2); // Only returns available tasks
3838    }
3839
3840    // ========== task_context tests ==========
3841
3842    #[tokio::test]
3843    async fn test_get_task_context_root_task_no_relations() {
3844        let ctx = TestContext::new().await;
3845        let task_mgr = TaskManager::new(ctx.pool());
3846
3847        // Create a single root task with no relations
3848        let task = task_mgr
3849            .add_task("Root task".to_string(), None, None, None, None, None)
3850            .await
3851            .unwrap();
3852
3853        let context = task_mgr.get_task_context(task.id).await.unwrap();
3854
3855        // Verify task itself
3856        assert_eq!(context.task.id, task.id);
3857        assert_eq!(context.task.name, "Root task");
3858
3859        // No ancestors (root task)
3860        assert_eq!(context.ancestors.len(), 0);
3861
3862        // No siblings
3863        assert_eq!(context.siblings.len(), 0);
3864
3865        // No children
3866        assert_eq!(context.children.len(), 0);
3867    }
3868
3869    #[tokio::test]
3870    async fn test_get_task_context_with_siblings() {
3871        let ctx = TestContext::new().await;
3872        let task_mgr = TaskManager::new(ctx.pool());
3873
3874        // Create multiple root tasks (siblings)
3875        let task1 = task_mgr
3876            .add_task("Task 1".to_string(), None, None, None, None, None)
3877            .await
3878            .unwrap();
3879        let task2 = task_mgr
3880            .add_task("Task 2".to_string(), None, None, None, None, None)
3881            .await
3882            .unwrap();
3883        let task3 = task_mgr
3884            .add_task("Task 3".to_string(), None, None, None, None, None)
3885            .await
3886            .unwrap();
3887
3888        let context = task_mgr.get_task_context(task2.id).await.unwrap();
3889
3890        // Verify task itself
3891        assert_eq!(context.task.id, task2.id);
3892
3893        // No ancestors (root task)
3894        assert_eq!(context.ancestors.len(), 0);
3895
3896        // Should have 2 siblings
3897        assert_eq!(context.siblings.len(), 2);
3898        let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
3899        assert!(sibling_ids.contains(&task1.id));
3900        assert!(sibling_ids.contains(&task3.id));
3901        assert!(!sibling_ids.contains(&task2.id)); // Should not include itself
3902
3903        // No children
3904        assert_eq!(context.children.len(), 0);
3905    }
3906
3907    #[tokio::test]
3908    async fn test_get_task_context_with_parent() {
3909        let ctx = TestContext::new().await;
3910        let task_mgr = TaskManager::new(ctx.pool());
3911
3912        // Create parent-child relationship
3913        let parent = task_mgr
3914            .add_task("Parent task".to_string(), None, None, None, None, None)
3915            .await
3916            .unwrap();
3917        let child = task_mgr
3918            .add_task(
3919                "Child task".to_string(),
3920                None,
3921                Some(parent.id),
3922                None,
3923                None,
3924                None,
3925            )
3926            .await
3927            .unwrap();
3928
3929        let context = task_mgr.get_task_context(child.id).await.unwrap();
3930
3931        // Verify task itself
3932        assert_eq!(context.task.id, child.id);
3933        assert_eq!(context.task.parent_id, Some(parent.id));
3934
3935        // Should have 1 ancestor (the parent)
3936        assert_eq!(context.ancestors.len(), 1);
3937        assert_eq!(context.ancestors[0].id, parent.id);
3938        assert_eq!(context.ancestors[0].name, "Parent task");
3939
3940        // No siblings
3941        assert_eq!(context.siblings.len(), 0);
3942
3943        // No children
3944        assert_eq!(context.children.len(), 0);
3945    }
3946
3947    #[tokio::test]
3948    async fn test_get_task_context_with_children() {
3949        let ctx = TestContext::new().await;
3950        let task_mgr = TaskManager::new(ctx.pool());
3951
3952        // Create parent with multiple children
3953        let parent = task_mgr
3954            .add_task("Parent task".to_string(), None, None, None, None, None)
3955            .await
3956            .unwrap();
3957        let child1 = task_mgr
3958            .add_task(
3959                "Child 1".to_string(),
3960                None,
3961                Some(parent.id),
3962                None,
3963                None,
3964                None,
3965            )
3966            .await
3967            .unwrap();
3968        let child2 = task_mgr
3969            .add_task(
3970                "Child 2".to_string(),
3971                None,
3972                Some(parent.id),
3973                None,
3974                None,
3975                None,
3976            )
3977            .await
3978            .unwrap();
3979        let child3 = task_mgr
3980            .add_task(
3981                "Child 3".to_string(),
3982                None,
3983                Some(parent.id),
3984                None,
3985                None,
3986                None,
3987            )
3988            .await
3989            .unwrap();
3990
3991        let context = task_mgr.get_task_context(parent.id).await.unwrap();
3992
3993        // Verify task itself
3994        assert_eq!(context.task.id, parent.id);
3995
3996        // No ancestors (root task)
3997        assert_eq!(context.ancestors.len(), 0);
3998
3999        // No siblings
4000        assert_eq!(context.siblings.len(), 0);
4001
4002        // Should have 3 children
4003        assert_eq!(context.children.len(), 3);
4004        let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
4005        assert!(child_ids.contains(&child1.id));
4006        assert!(child_ids.contains(&child2.id));
4007        assert!(child_ids.contains(&child3.id));
4008    }
4009
4010    #[tokio::test]
4011    async fn test_get_task_context_multi_level_hierarchy() {
4012        let ctx = TestContext::new().await;
4013        let task_mgr = TaskManager::new(ctx.pool());
4014
4015        // Create 3-level hierarchy: grandparent -> parent -> child
4016        let grandparent = task_mgr
4017            .add_task("Grandparent".to_string(), None, None, None, None, None)
4018            .await
4019            .unwrap();
4020        let parent = task_mgr
4021            .add_task(
4022                "Parent".to_string(),
4023                None,
4024                Some(grandparent.id),
4025                None,
4026                None,
4027                None,
4028            )
4029            .await
4030            .unwrap();
4031        let child = task_mgr
4032            .add_task("Child".to_string(), None, Some(parent.id), None, None, None)
4033            .await
4034            .unwrap();
4035
4036        let context = task_mgr.get_task_context(child.id).await.unwrap();
4037
4038        // Verify task itself
4039        assert_eq!(context.task.id, child.id);
4040
4041        // Should have 2 ancestors (parent and grandparent, ordered from immediate to root)
4042        assert_eq!(context.ancestors.len(), 2);
4043        assert_eq!(context.ancestors[0].id, parent.id);
4044        assert_eq!(context.ancestors[0].name, "Parent");
4045        assert_eq!(context.ancestors[1].id, grandparent.id);
4046        assert_eq!(context.ancestors[1].name, "Grandparent");
4047
4048        // No siblings
4049        assert_eq!(context.siblings.len(), 0);
4050
4051        // No children
4052        assert_eq!(context.children.len(), 0);
4053    }
4054
4055    #[tokio::test]
4056    async fn test_get_task_context_complex_family_tree() {
4057        let ctx = TestContext::new().await;
4058        let task_mgr = TaskManager::new(ctx.pool());
4059
4060        // Create complex structure:
4061        // Root
4062        //  ├─ Child1
4063        //  │   ├─ Grandchild1
4064        //  │   └─ Grandchild2 (target)
4065        //  └─ Child2
4066
4067        let root = task_mgr
4068            .add_task("Root".to_string(), None, None, None, None, None)
4069            .await
4070            .unwrap();
4071        let child1 = task_mgr
4072            .add_task("Child1".to_string(), None, Some(root.id), None, None, None)
4073            .await
4074            .unwrap();
4075        let child2 = task_mgr
4076            .add_task("Child2".to_string(), None, Some(root.id), None, None, None)
4077            .await
4078            .unwrap();
4079        let grandchild1 = task_mgr
4080            .add_task(
4081                "Grandchild1".to_string(),
4082                None,
4083                Some(child1.id),
4084                None,
4085                None,
4086                None,
4087            )
4088            .await
4089            .unwrap();
4090        let grandchild2 = task_mgr
4091            .add_task(
4092                "Grandchild2".to_string(),
4093                None,
4094                Some(child1.id),
4095                None,
4096                None,
4097                None,
4098            )
4099            .await
4100            .unwrap();
4101
4102        // Get context for grandchild2
4103        let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
4104
4105        // Verify task itself
4106        assert_eq!(context.task.id, grandchild2.id);
4107
4108        // Should have 2 ancestors: child1 and root
4109        assert_eq!(context.ancestors.len(), 2);
4110        assert_eq!(context.ancestors[0].id, child1.id);
4111        assert_eq!(context.ancestors[1].id, root.id);
4112
4113        // Should have 1 sibling: grandchild1
4114        assert_eq!(context.siblings.len(), 1);
4115        assert_eq!(context.siblings[0].id, grandchild1.id);
4116
4117        // No children
4118        assert_eq!(context.children.len(), 0);
4119
4120        // Now get context for child1 to verify it sees both grandchildren
4121        let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
4122        assert_eq!(context_child1.ancestors.len(), 1);
4123        assert_eq!(context_child1.ancestors[0].id, root.id);
4124        assert_eq!(context_child1.siblings.len(), 1);
4125        assert_eq!(context_child1.siblings[0].id, child2.id);
4126        assert_eq!(context_child1.children.len(), 2);
4127    }
4128
4129    #[tokio::test]
4130    async fn test_get_task_context_respects_priority_ordering() {
4131        let ctx = TestContext::new().await;
4132        let task_mgr = TaskManager::new(ctx.pool());
4133
4134        // Create parent with children having different priorities
4135        let parent = task_mgr
4136            .add_task("Parent".to_string(), None, None, None, None, None)
4137            .await
4138            .unwrap();
4139
4140        // Add children with priorities (lower number = higher priority)
4141        let child_low = task_mgr
4142            .add_task(
4143                "Low priority".to_string(),
4144                None,
4145                Some(parent.id),
4146                None,
4147                None,
4148                None,
4149            )
4150            .await
4151            .unwrap();
4152        let _ = task_mgr
4153            .update_task(
4154                child_low.id,
4155                TaskUpdate {
4156                    priority: Some(10),
4157                    ..Default::default()
4158                },
4159            )
4160            .await
4161            .unwrap();
4162
4163        let child_high = task_mgr
4164            .add_task(
4165                "High priority".to_string(),
4166                None,
4167                Some(parent.id),
4168                None,
4169                None,
4170                None,
4171            )
4172            .await
4173            .unwrap();
4174        let _ = task_mgr
4175            .update_task(
4176                child_high.id,
4177                TaskUpdate {
4178                    priority: Some(1),
4179                    ..Default::default()
4180                },
4181            )
4182            .await
4183            .unwrap();
4184
4185        let child_medium = task_mgr
4186            .add_task(
4187                "Medium priority".to_string(),
4188                None,
4189                Some(parent.id),
4190                None,
4191                None,
4192                None,
4193            )
4194            .await
4195            .unwrap();
4196        let _ = task_mgr
4197            .update_task(
4198                child_medium.id,
4199                TaskUpdate {
4200                    priority: Some(5),
4201                    ..Default::default()
4202                },
4203            )
4204            .await
4205            .unwrap();
4206
4207        let context = task_mgr.get_task_context(parent.id).await.unwrap();
4208
4209        // Children should be ordered by priority (1, 5, 10)
4210        assert_eq!(context.children.len(), 3);
4211        assert_eq!(context.children[0].priority, Some(1));
4212        assert_eq!(context.children[1].priority, Some(5));
4213        assert_eq!(context.children[2].priority, Some(10));
4214    }
4215
4216    #[tokio::test]
4217    async fn test_get_task_context_nonexistent_task() {
4218        let ctx = TestContext::new().await;
4219        let task_mgr = TaskManager::new(ctx.pool());
4220
4221        let result = task_mgr.get_task_context(99999).await;
4222        assert!(result.is_err());
4223        assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
4224    }
4225
4226    #[tokio::test]
4227    async fn test_get_task_context_handles_null_priority() {
4228        let ctx = TestContext::new().await;
4229        let task_mgr = TaskManager::new(ctx.pool());
4230
4231        // Create siblings with mixed null and set priorities
4232        let task1 = task_mgr
4233            .add_task("Task 1".to_string(), None, None, None, None, None)
4234            .await
4235            .unwrap();
4236        let _ = task_mgr
4237            .update_task(
4238                task1.id,
4239                TaskUpdate {
4240                    priority: Some(1),
4241                    ..Default::default()
4242                },
4243            )
4244            .await
4245            .unwrap();
4246
4247        let task2 = task_mgr
4248            .add_task("Task 2".to_string(), None, None, None, None, None)
4249            .await
4250            .unwrap();
4251        // task2 has NULL priority
4252
4253        let task3 = task_mgr
4254            .add_task("Task 3".to_string(), None, None, None, None, None)
4255            .await
4256            .unwrap();
4257        let _ = task_mgr
4258            .update_task(
4259                task3.id,
4260                TaskUpdate {
4261                    priority: Some(5),
4262                    ..Default::default()
4263                },
4264            )
4265            .await
4266            .unwrap();
4267
4268        let context = task_mgr.get_task_context(task2.id).await.unwrap();
4269
4270        // Should have 2 siblings, ordered by priority (non-null first, then null)
4271        assert_eq!(context.siblings.len(), 2);
4272        // Task with priority 1 should come first
4273        assert_eq!(context.siblings[0].id, task1.id);
4274        assert_eq!(context.siblings[0].priority, Some(1));
4275        // Task with priority 5 should come second
4276        assert_eq!(context.siblings[1].id, task3.id);
4277        assert_eq!(context.siblings[1].priority, Some(5));
4278    }
4279
4280    #[tokio::test]
4281    async fn test_pick_next_tasks_priority_order() {
4282        let ctx = TestContext::new().await;
4283        let task_mgr = TaskManager::new(ctx.pool());
4284
4285        // Create 4 tasks with different priorities
4286        let critical = task_mgr
4287            .add_task("Critical Task".to_string(), None, None, None, None, None)
4288            .await
4289            .unwrap();
4290        task_mgr
4291            .update_task(
4292                critical.id,
4293                TaskUpdate {
4294                    priority: Some(1),
4295                    ..Default::default()
4296                },
4297            )
4298            .await
4299            .unwrap();
4300
4301        let low = task_mgr
4302            .add_task("Low Task".to_string(), None, None, None, None, None)
4303            .await
4304            .unwrap();
4305        task_mgr
4306            .update_task(
4307                low.id,
4308                TaskUpdate {
4309                    priority: Some(4),
4310                    ..Default::default()
4311                },
4312            )
4313            .await
4314            .unwrap();
4315
4316        let high = task_mgr
4317            .add_task("High Task".to_string(), None, None, None, None, None)
4318            .await
4319            .unwrap();
4320        task_mgr
4321            .update_task(
4322                high.id,
4323                TaskUpdate {
4324                    priority: Some(2),
4325                    ..Default::default()
4326                },
4327            )
4328            .await
4329            .unwrap();
4330
4331        let medium = task_mgr
4332            .add_task("Medium Task".to_string(), None, None, None, None, None)
4333            .await
4334            .unwrap();
4335        task_mgr
4336            .update_task(
4337                medium.id,
4338                TaskUpdate {
4339                    priority: Some(3),
4340                    ..Default::default()
4341                },
4342            )
4343            .await
4344            .unwrap();
4345
4346        // Pick next tasks should return them in priority order: critical > high > medium > low
4347        let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
4348
4349        assert_eq!(tasks.len(), 4);
4350        assert_eq!(tasks[0].id, critical.id); // Priority 1
4351        assert_eq!(tasks[1].id, high.id); // Priority 2
4352        assert_eq!(tasks[2].id, medium.id); // Priority 3
4353        assert_eq!(tasks[3].id, low.id); // Priority 4
4354    }
4355
4356    #[tokio::test]
4357    async fn test_pick_next_prefers_doing_over_todo() {
4358        let ctx = TestContext::new().await;
4359        let task_mgr = TaskManager::new(ctx.pool());
4360        let workspace_mgr = WorkspaceManager::new(ctx.pool());
4361
4362        // Create a parent task and set it as current
4363        let parent = task_mgr
4364            .add_task("Parent".to_string(), None, None, None, None, None)
4365            .await
4366            .unwrap();
4367        let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
4368        workspace_mgr
4369            .set_current_task(parent_started.task.id, None)
4370            .await
4371            .unwrap();
4372
4373        // Create two subtasks with same priority: one doing, one todo
4374        let doing_subtask = task_mgr
4375            .add_task(
4376                "Doing Subtask".to_string(),
4377                None,
4378                Some(parent.id),
4379                None,
4380                None,
4381                None,
4382            )
4383            .await
4384            .unwrap();
4385        task_mgr.start_task(doing_subtask.id, false).await.unwrap();
4386        // Switch back to parent so doing_subtask is "pending" (doing but not current)
4387        workspace_mgr
4388            .set_current_task(parent.id, None)
4389            .await
4390            .unwrap();
4391
4392        let _todo_subtask = task_mgr
4393            .add_task(
4394                "Todo Subtask".to_string(),
4395                None,
4396                Some(parent.id),
4397                None,
4398                None,
4399                None,
4400            )
4401            .await
4402            .unwrap();
4403
4404        // Both have same priority (default), but doing should be picked first
4405        let result = task_mgr.pick_next().await.unwrap();
4406
4407        if let Some(task) = result.task {
4408            assert_eq!(
4409                task.id, doing_subtask.id,
4410                "Should recommend doing subtask over todo subtask"
4411            );
4412            assert_eq!(task.status, "doing");
4413        } else {
4414            panic!("Expected a task recommendation");
4415        }
4416    }
4417
4418    #[tokio::test]
4419    async fn test_multiple_doing_tasks_allowed() {
4420        let ctx = TestContext::new().await;
4421        let task_mgr = TaskManager::new(ctx.pool());
4422        let workspace_mgr = WorkspaceManager::new(ctx.pool());
4423
4424        // Create and start task A
4425        let task_a = task_mgr
4426            .add_task("Task A".to_string(), None, None, None, None, None)
4427            .await
4428            .unwrap();
4429        let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
4430        assert_eq!(task_a_started.task.status, "doing");
4431
4432        // Verify task A is current
4433        let current = workspace_mgr.get_current_task(None).await.unwrap();
4434        assert_eq!(current.current_task_id, Some(task_a.id));
4435
4436        // Create and start task B
4437        let task_b = task_mgr
4438            .add_task("Task B".to_string(), None, None, None, None, None)
4439            .await
4440            .unwrap();
4441        let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
4442        assert_eq!(task_b_started.task.status, "doing");
4443
4444        // Verify task B is now current
4445        let current = workspace_mgr.get_current_task(None).await.unwrap();
4446        assert_eq!(current.current_task_id, Some(task_b.id));
4447
4448        // Verify task A is still doing (not reverted to todo)
4449        let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
4450        assert_eq!(
4451            task_a_after.status, "doing",
4452            "Task A should remain doing even though it is not current"
4453        );
4454
4455        // Verify both tasks are in doing status
4456        let doing_tasks: Vec<Task> = sqlx::query_as(
4457            r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner, metadata
4458             FROM tasks WHERE status = 'doing' AND deleted_at IS NULL ORDER BY id"#
4459        )
4460        .fetch_all(ctx.pool())
4461        .await
4462        .unwrap();
4463
4464        assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
4465        assert_eq!(doing_tasks[0].id, task_a.id);
4466        assert_eq!(doing_tasks[1].id, task_b.id);
4467    }
4468    #[tokio::test]
4469    async fn test_find_tasks_pagination() {
4470        let ctx = TestContext::new().await;
4471        let task_mgr = TaskManager::new(ctx.pool());
4472
4473        // Create 15 tasks
4474        for i in 0..15 {
4475            task_mgr
4476                .add_task(format!("Task {}", i), None, None, None, None, None)
4477                .await
4478                .unwrap();
4479        }
4480
4481        // Page 1: Limit 10, Offset 0
4482        let page1 = task_mgr
4483            .find_tasks(None, None, None, Some(10), Some(0))
4484            .await
4485            .unwrap();
4486        assert_eq!(page1.tasks.len(), 10);
4487        assert_eq!(page1.total_count, 15);
4488        assert!(page1.has_more);
4489        assert_eq!(page1.offset, 0);
4490
4491        // Page 2: Limit 10, Offset 10
4492        let page2 = task_mgr
4493            .find_tasks(None, None, None, Some(10), Some(10))
4494            .await
4495            .unwrap();
4496        assert_eq!(page2.tasks.len(), 5);
4497        assert_eq!(page2.total_count, 15);
4498        assert!(!page2.has_more);
4499        assert_eq!(page2.offset, 10);
4500    }
4501}
4502
4503// Re-export TaskContext for cli_handlers