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
22pub struct TaskManager<'a> {
23    pool: &'a SqlitePool,
24    notifier: crate::notifications::NotificationSender,
25    cli_notifier: Option<crate::dashboard::cli_notifier::CliNotifier>,
26    project_path: Option<String>,
27}
28
29impl<'a> TaskManager<'a> {
30    pub fn new(pool: &'a SqlitePool) -> Self {
31        Self {
32            pool,
33            notifier: crate::notifications::NotificationSender::new(None),
34            cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
35            project_path: None,
36        }
37    }
38
39    /// Create a TaskManager with project path for CLI notifications
40    pub fn with_project_path(pool: &'a SqlitePool, project_path: String) -> Self {
41        Self {
42            pool,
43            notifier: crate::notifications::NotificationSender::new(None),
44            cli_notifier: Some(crate::dashboard::cli_notifier::CliNotifier::new()),
45            project_path: Some(project_path),
46        }
47    }
48
49    /// Create a TaskManager with WebSocket notification support
50    pub fn with_websocket(
51        pool: &'a SqlitePool,
52        ws_state: Arc<crate::dashboard::websocket::WebSocketState>,
53        project_path: String,
54    ) -> Self {
55        Self {
56            pool,
57            notifier: crate::notifications::NotificationSender::new(Some(ws_state)),
58            cli_notifier: None, // Dashboard context doesn't need CLI notifier
59            project_path: Some(project_path),
60        }
61    }
62
63    /// Internal helper: Notify UI about task creation
64    async fn notify_task_created(&self, task: &Task) {
65        use crate::dashboard::websocket::DatabaseOperationPayload;
66
67        // WebSocket notification (Dashboard context)
68        if let Some(project_path) = &self.project_path {
69            let task_json = match serde_json::to_value(task) {
70                Ok(json) => json,
71                Err(e) => {
72                    tracing::warn!(error = %e, "Failed to serialize task for notification");
73                    return;
74                },
75            };
76
77            let payload =
78                DatabaseOperationPayload::task_created(task.id, task_json, project_path.clone());
79            self.notifier.send(payload).await;
80        }
81
82        // CLI → Dashboard HTTP notification (CLI context)
83        if let Some(cli_notifier) = &self.cli_notifier {
84            cli_notifier
85                .notify_task_changed(Some(task.id), "created", self.project_path.clone())
86                .await;
87        }
88    }
89
90    /// Internal helper: Notify UI about task update
91    async fn notify_task_updated(&self, task: &Task) {
92        use crate::dashboard::websocket::DatabaseOperationPayload;
93
94        // WebSocket notification (Dashboard context)
95        if let Some(project_path) = &self.project_path {
96            let task_json = match serde_json::to_value(task) {
97                Ok(json) => json,
98                Err(e) => {
99                    tracing::warn!(error = %e, "Failed to serialize task for notification");
100                    return;
101                },
102            };
103
104            let payload =
105                DatabaseOperationPayload::task_updated(task.id, task_json, project_path.clone());
106            self.notifier.send(payload).await;
107        }
108
109        // CLI → Dashboard HTTP notification (CLI context)
110        if let Some(cli_notifier) = &self.cli_notifier {
111            cli_notifier
112                .notify_task_changed(Some(task.id), "updated", self.project_path.clone())
113                .await;
114        }
115    }
116
117    /// Internal helper: Notify UI about task deletion
118    async fn notify_task_deleted(&self, task_id: i64) {
119        use crate::dashboard::websocket::DatabaseOperationPayload;
120
121        // WebSocket notification (Dashboard context)
122        if let Some(project_path) = &self.project_path {
123            let payload = DatabaseOperationPayload::task_deleted(task_id, project_path.clone());
124            self.notifier.send(payload).await;
125        }
126
127        // CLI → Dashboard HTTP notification (CLI context)
128        if let Some(cli_notifier) = &self.cli_notifier {
129            cli_notifier
130                .notify_task_changed(Some(task_id), "deleted", self.project_path.clone())
131                .await;
132        }
133    }
134
135    /// Add a new task
136    /// owner: 'human' (created via CLI/Dashboard) or 'ai' (created via MCP)
137    #[tracing::instrument(skip(self), fields(task_name = %name))]
138    pub async fn add_task(
139        &self,
140        name: &str,
141        spec: Option<&str>,
142        parent_id: Option<i64>,
143        owner: Option<&str>,
144    ) -> Result<Task> {
145        // Check for circular dependency if parent_id is provided
146        if let Some(pid) = parent_id {
147            self.check_task_exists(pid).await?;
148        }
149
150        let now = Utc::now();
151        let owner = owner.unwrap_or("human");
152
153        let result = sqlx::query(
154            r#"
155            INSERT INTO tasks (name, spec, parent_id, status, first_todo_at, owner)
156            VALUES (?, ?, ?, 'todo', ?, ?)
157            "#,
158        )
159        .bind(name)
160        .bind(spec)
161        .bind(parent_id)
162        .bind(now)
163        .bind(owner)
164        .execute(self.pool)
165        .await?;
166
167        let id = result.last_insert_rowid();
168        let task = self.get_task(id).await?;
169
170        // Notify WebSocket clients about the new task
171        self.notify_task_created(&task).await;
172
173        Ok(task)
174    }
175
176    // =========================================================================
177    // Transaction-aware methods (for batch operations like PlanExecutor)
178    // These methods do NOT notify - caller is responsible for notifications
179    // =========================================================================
180
181    /// Create a task within a transaction (no notification)
182    ///
183    /// This is used by PlanExecutor for batch operations where:
184    /// - Multiple tasks need atomic creation
185    /// - Notification should happen after all tasks are committed
186    ///
187    /// # Arguments
188    /// * `tx` - The active transaction
189    /// * `name` - Task name
190    /// * `spec` - Optional task specification
191    /// * `priority` - Optional priority (1=critical, 2=high, 3=medium, 4=low)
192    /// * `status` - Optional status string ("todo", "doing", "done")
193    /// * `active_form` - Optional active form description
194    /// * `owner` - Task owner ("human" or "ai")
195    ///
196    /// # Returns
197    /// The ID of the created task
198    #[allow(clippy::too_many_arguments)]
199    pub async fn create_task_in_tx(
200        &self,
201        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
202        name: &str,
203        spec: Option<&str>,
204        priority: Option<i32>,
205        status: Option<&str>,
206        active_form: Option<&str>,
207        owner: &str,
208    ) -> Result<i64> {
209        let now = Utc::now();
210        let status = status.unwrap_or("todo");
211        let priority = priority.unwrap_or(3); // Default: medium
212
213        let result = sqlx::query(
214            r#"
215            INSERT INTO tasks (name, spec, priority, status, active_form, first_todo_at, owner)
216            VALUES (?, ?, ?, ?, ?, ?, ?)
217            "#,
218        )
219        .bind(name)
220        .bind(spec)
221        .bind(priority)
222        .bind(status)
223        .bind(active_form)
224        .bind(now)
225        .bind(owner)
226        .execute(&mut **tx)
227        .await?;
228
229        Ok(result.last_insert_rowid())
230    }
231
232    /// Update a task within a transaction (no notification)
233    ///
234    /// Only updates fields that are Some - supports partial updates.
235    /// Does NOT update name (used for identity) or timestamps.
236    ///
237    /// # Arguments
238    /// * `tx` - The active transaction
239    /// * `task_id` - ID of the task to update
240    /// * `spec` - New spec (if Some)
241    /// * `priority` - New priority (if Some)
242    /// * `status` - New status (if Some)
243    /// * `active_form` - New active form (if Some)
244    pub async fn update_task_in_tx(
245        &self,
246        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
247        task_id: i64,
248        spec: Option<&str>,
249        priority: Option<i32>,
250        status: Option<&str>,
251        active_form: Option<&str>,
252    ) -> Result<()> {
253        // Update spec if provided
254        if let Some(spec) = spec {
255            sqlx::query("UPDATE tasks SET spec = ? WHERE id = ?")
256                .bind(spec)
257                .bind(task_id)
258                .execute(&mut **tx)
259                .await?;
260        }
261
262        // Update priority if provided
263        if let Some(priority) = priority {
264            sqlx::query("UPDATE tasks SET priority = ? WHERE id = ?")
265                .bind(priority)
266                .bind(task_id)
267                .execute(&mut **tx)
268                .await?;
269        }
270
271        // Update status if provided
272        if let Some(status) = status {
273            sqlx::query("UPDATE tasks SET status = ? WHERE id = ?")
274                .bind(status)
275                .bind(task_id)
276                .execute(&mut **tx)
277                .await?;
278        }
279
280        // Update active_form if provided
281        if let Some(active_form) = active_form {
282            sqlx::query("UPDATE tasks SET active_form = ? WHERE id = ?")
283                .bind(active_form)
284                .bind(task_id)
285                .execute(&mut **tx)
286                .await?;
287        }
288
289        Ok(())
290    }
291
292    /// Set parent_id for a task within a transaction (no notification)
293    ///
294    /// Used to establish parent-child relationships after tasks are created.
295    pub async fn set_parent_in_tx(
296        &self,
297        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
298        task_id: i64,
299        parent_id: i64,
300    ) -> Result<()> {
301        sqlx::query("UPDATE tasks SET parent_id = ? WHERE id = ?")
302            .bind(parent_id)
303            .bind(task_id)
304            .execute(&mut **tx)
305            .await?;
306
307        Ok(())
308    }
309
310    /// Clear parent_id for a task in a transaction (make it a root task)
311    ///
312    /// Used when explicitly setting parent_id to null in JSON.
313    pub async fn clear_parent_in_tx(
314        &self,
315        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
316        task_id: i64,
317    ) -> Result<()> {
318        sqlx::query("UPDATE tasks SET parent_id = NULL WHERE id = ?")
319            .bind(task_id)
320            .execute(&mut **tx)
321            .await?;
322
323        Ok(())
324    }
325
326    /// Delete a task within a transaction (no notification)
327    ///
328    /// Used by PlanExecutor for batch delete operations.
329    /// WebSocket notification is sent after transaction commit via notify_batch_changed().
330    ///
331    /// **Warning**: Due to `ON DELETE CASCADE` on `parent_id`, deleting a parent task
332    /// will also delete all descendant tasks. The returned `DeleteTaskResult` includes
333    /// the count of descendants that will be cascade-deleted.
334    ///
335    /// Returns `DeleteTaskResult` with:
336    /// - `found`: whether the task existed
337    /// - `descendant_count`: number of descendants that will be cascade-deleted
338    ///
339    /// Note: Focus protection is handled by the caller (PlanExecutor) BEFORE
340    /// calling this function, using `find_focused_in_subtree_in_tx`.
341    pub async fn delete_task_in_tx(
342        &self,
343        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
344        task_id: i64,
345    ) -> Result<DeleteTaskResult> {
346        // Check if task exists and count descendants before deletion
347        let task_info: Option<(i64,)> = sqlx::query_as("SELECT id FROM tasks WHERE id = ?")
348            .bind(task_id)
349            .fetch_optional(&mut **tx)
350            .await?;
351
352        if task_info.is_none() {
353            return Ok(DeleteTaskResult {
354                found: false,
355                descendant_count: 0,
356            });
357        }
358
359        // Count descendants that will be cascade-deleted
360        let descendant_count = self.count_descendants_in_tx(tx, task_id).await?;
361
362        // Perform the delete (CASCADE will handle children)
363        sqlx::query("DELETE FROM tasks WHERE id = ?")
364            .bind(task_id)
365            .execute(&mut **tx)
366            .await?;
367
368        Ok(DeleteTaskResult {
369            found: true,
370            descendant_count,
371        })
372    }
373
374    /// Count all descendants of a task (children, grandchildren, etc.)
375    async fn count_descendants_in_tx(
376        &self,
377        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
378        task_id: i64,
379    ) -> Result<i64> {
380        // Use recursive CTE to count all descendants
381        let count: (i64,) = sqlx::query_as(
382            r#"
383            WITH RECURSIVE descendants AS (
384                SELECT id FROM tasks WHERE parent_id = ?
385                UNION ALL
386                SELECT t.id FROM tasks t
387                INNER JOIN descendants d ON t.parent_id = d.id
388            )
389            SELECT COUNT(*) FROM descendants
390            "#,
391        )
392        .bind(task_id)
393        .fetch_one(&mut **tx)
394        .await?;
395
396        Ok(count.0)
397    }
398
399    /// Find if a task or any of its descendants is ANY session's focus
400    ///
401    /// This is critical for delete protection: deleting a parent task cascades
402    /// to all descendants, so we must check the entire subtree for focus.
403    ///
404    /// Focus protection is GLOBAL - a task focused by any session cannot be deleted.
405    /// This prevents one session from accidentally breaking another session's work.
406    ///
407    /// Returns `Some((task_id, session_id))` if any task in the subtree is focused,
408    /// `None` if no focus found in the subtree.
409    pub async fn find_focused_in_subtree_in_tx(
410        &self,
411        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
412        task_id: i64,
413    ) -> Result<Option<(i64, String)>> {
414        // Use recursive CTE to get all task IDs in the subtree (including the root)
415        // Then check if any of them is focused by ANY session
416        let row: Option<(i64, String)> = sqlx::query_as(
417            r#"
418            WITH RECURSIVE subtree AS (
419                SELECT id FROM tasks WHERE id = ?
420                UNION ALL
421                SELECT t.id FROM tasks t
422                INNER JOIN subtree s ON t.parent_id = s.id
423            )
424            SELECT s.current_task_id, s.session_id FROM sessions s
425            WHERE s.current_task_id IN (SELECT id FROM subtree)
426            LIMIT 1
427            "#,
428        )
429        .bind(task_id)
430        .fetch_optional(&mut **tx)
431        .await?;
432
433        Ok(row)
434    }
435
436    /// Count incomplete children of a task within a transaction
437    ///
438    /// Returns the number of child tasks that are not in 'done' status.
439    /// Used to validate that all children are complete before marking parent as done.
440    pub async fn count_incomplete_children_in_tx(
441        &self,
442        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
443        task_id: i64,
444    ) -> Result<i64> {
445        let count: (i64,) = sqlx::query_as(crate::sql_constants::COUNT_INCOMPLETE_CHILDREN)
446            .bind(task_id)
447            .fetch_one(&mut **tx)
448            .await?;
449
450        Ok(count.0)
451    }
452
453    /// Complete a task within a transaction (core business logic)
454    ///
455    /// This is the single source of truth for task completion logic:
456    /// - Validates all children are complete
457    /// - Updates status to 'done'
458    /// - Sets first_done_at timestamp
459    ///
460    /// Called by both `done_task()` and `PlanExecutor`.
461    pub async fn complete_task_in_tx(
462        &self,
463        tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
464        task_id: i64,
465    ) -> Result<()> {
466        // Check if all children are done
467        let incomplete_count = self.count_incomplete_children_in_tx(tx, task_id).await?;
468        if incomplete_count > 0 {
469            return Err(IntentError::UncompletedChildren);
470        }
471
472        // Update task status to done
473        let now = chrono::Utc::now();
474        sqlx::query(
475            r#"
476            UPDATE tasks
477            SET status = 'done', first_done_at = COALESCE(first_done_at, ?)
478            WHERE id = ?
479            "#,
480        )
481        .bind(now)
482        .bind(task_id)
483        .execute(&mut **tx)
484        .await?;
485
486        Ok(())
487    }
488
489    /// Notify Dashboard about a batch operation
490    ///
491    /// Call this after committing a transaction that created/updated multiple tasks.
492    /// Sends a single "batch_update" notification instead of per-task notifications.
493    pub async fn notify_batch_changed(&self) {
494        if let Some(cli_notifier) = &self.cli_notifier {
495            cli_notifier
496                .notify_task_changed(None, "batch_update", self.project_path.clone())
497                .await;
498        }
499    }
500
501    // =========================================================================
502    // End of transaction-aware methods
503    // =========================================================================
504
505    /// Get a task by ID
506    #[tracing::instrument(skip(self))]
507    pub async fn get_task(&self, id: i64) -> Result<Task> {
508        let task = sqlx::query_as::<_, Task>(
509            r#"
510            SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
511            FROM tasks
512            WHERE id = ?
513            "#,
514        )
515        .bind(id)
516        .fetch_optional(self.pool)
517        .await?
518        .ok_or(IntentError::TaskNotFound(id))?;
519
520        Ok(task)
521    }
522
523    /// Get a task with events summary
524    pub async fn get_task_with_events(&self, id: i64) -> Result<TaskWithEvents> {
525        let task = self.get_task(id).await?;
526        let events_summary = self.get_events_summary(id).await?;
527
528        Ok(TaskWithEvents {
529            task,
530            events_summary: Some(events_summary),
531        })
532    }
533
534    /// Get full ancestry chain for a task
535    ///
536    /// Returns a vector of tasks from the given task up to the root:
537    /// [task itself, parent, grandparent, ..., root]
538    ///
539    /// Example:
540    /// - Task 42 (parent_id: 55) → [Task 42, Task 55, ...]
541    /// - Task 100 (parent_id: null) → [Task 100]
542    pub async fn get_task_ancestry(&self, task_id: i64) -> Result<Vec<Task>> {
543        let mut chain = Vec::new();
544        let mut current_id = Some(task_id);
545
546        while let Some(id) = current_id {
547            let task = self.get_task(id).await?;
548            current_id = task.parent_id;
549            chain.push(task);
550        }
551
552        Ok(chain)
553    }
554
555    /// Get task context - the complete family tree of a task
556    ///
557    /// Returns:
558    /// - task: The requested task
559    /// - ancestors: Parent chain up to root (ordered from immediate parent to root)
560    /// - siblings: Other tasks at the same level (same parent_id)
561    /// - children: Direct subtasks of this task
562    pub async fn get_task_context(&self, id: i64) -> Result<TaskContext> {
563        // Get the main task
564        let task = self.get_task(id).await?;
565
566        // Get ancestors (walk up parent chain)
567        let mut ancestors = Vec::new();
568        let mut current_parent_id = task.parent_id;
569
570        while let Some(parent_id) = current_parent_id {
571            let parent = self.get_task(parent_id).await?;
572            current_parent_id = parent.parent_id;
573            ancestors.push(parent);
574        }
575
576        // Get siblings (tasks with same parent_id)
577        let siblings = if let Some(parent_id) = task.parent_id {
578            sqlx::query_as::<_, Task>(
579                r#"
580                SELECT id, parent_id, name, spec, status, complexity, priority,
581                       first_todo_at, first_doing_at, first_done_at, active_form, owner
582                FROM tasks
583                WHERE parent_id = ? AND id != ?
584                ORDER BY priority ASC NULLS LAST, id ASC
585                "#,
586            )
587            .bind(parent_id)
588            .bind(id)
589            .fetch_all(self.pool)
590            .await?
591        } else {
592            // For root tasks, get other root tasks as siblings
593            sqlx::query_as::<_, Task>(
594                r#"
595                SELECT id, parent_id, name, spec, status, complexity, priority,
596                       first_todo_at, first_doing_at, first_done_at, active_form, owner
597                FROM tasks
598                WHERE parent_id IS NULL AND id != ?
599                ORDER BY priority ASC NULLS LAST, id ASC
600                "#,
601            )
602            .bind(id)
603            .fetch_all(self.pool)
604            .await?
605        };
606
607        // Get children (direct subtasks)
608        let children = sqlx::query_as::<_, Task>(
609            r#"
610            SELECT id, parent_id, name, spec, status, complexity, priority,
611                   first_todo_at, first_doing_at, first_done_at, active_form, owner
612            FROM tasks
613            WHERE parent_id = ?
614            ORDER BY priority ASC NULLS LAST, id ASC
615            "#,
616        )
617        .bind(id)
618        .fetch_all(self.pool)
619        .await?;
620
621        // Get blocking tasks (tasks that this task depends on)
622        let blocking_tasks = sqlx::query_as::<_, Task>(
623            r#"
624            SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
625                   t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
626            FROM tasks t
627            JOIN dependencies d ON t.id = d.blocking_task_id
628            WHERE d.blocked_task_id = ?
629            ORDER BY t.priority ASC NULLS LAST, t.id ASC
630            "#,
631        )
632        .bind(id)
633        .fetch_all(self.pool)
634        .await?;
635
636        // Get blocked_by tasks (tasks that depend on this task)
637        let blocked_by_tasks = sqlx::query_as::<_, Task>(
638            r#"
639            SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
640                   t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
641            FROM tasks t
642            JOIN dependencies d ON t.id = d.blocked_task_id
643            WHERE d.blocking_task_id = ?
644            ORDER BY t.priority ASC NULLS LAST, t.id ASC
645            "#,
646        )
647        .bind(id)
648        .fetch_all(self.pool)
649        .await?;
650
651        Ok(TaskContext {
652            task,
653            ancestors,
654            siblings,
655            children,
656            dependencies: crate::db::models::TaskDependencies {
657                blocking_tasks,
658                blocked_by_tasks,
659            },
660        })
661    }
662
663    /// Get all descendants of a task recursively (children, grandchildren, etc.)
664    /// Uses recursive CTE for efficient querying
665    pub async fn get_descendants(&self, task_id: i64) -> Result<Vec<Task>> {
666        let descendants = sqlx::query_as::<_, Task>(
667            r#"
668            WITH RECURSIVE descendants AS (
669                SELECT id, parent_id, name, spec, status, complexity, priority,
670                       first_todo_at, first_doing_at, first_done_at, active_form, owner
671                FROM tasks
672                WHERE parent_id = ?
673
674                UNION ALL
675
676                SELECT t.id, t.parent_id, t.name, t.spec, t.status, t.complexity, t.priority,
677                       t.first_todo_at, t.first_doing_at, t.first_done_at, t.active_form, t.owner
678                FROM tasks t
679                INNER JOIN descendants d ON t.parent_id = d.id
680            )
681            SELECT * FROM descendants
682            ORDER BY parent_id NULLS FIRST, priority ASC NULLS LAST, id ASC
683            "#,
684        )
685        .bind(task_id)
686        .fetch_all(self.pool)
687        .await?;
688
689        Ok(descendants)
690    }
691
692    /// Get status response for a task (the "spotlight" view)
693    /// This is the main method for `ie status` command
694    pub async fn get_status(
695        &self,
696        task_id: i64,
697        with_events: bool,
698    ) -> Result<crate::db::models::StatusResponse> {
699        use crate::db::models::{StatusResponse, TaskBrief};
700
701        // Get task context (reuse existing method)
702        let context = self.get_task_context(task_id).await?;
703
704        // Get all descendants recursively
705        let descendants_full = self.get_descendants(task_id).await?;
706
707        // Convert siblings and descendants to brief format
708        let siblings: Vec<TaskBrief> = context.siblings.iter().map(TaskBrief::from).collect();
709        let descendants: Vec<TaskBrief> = descendants_full.iter().map(TaskBrief::from).collect();
710
711        // Get events if requested
712        let events = if with_events {
713            let event_mgr = crate::events::EventManager::new(self.pool);
714            Some(
715                event_mgr
716                    .list_events(Some(task_id), Some(50), None, None)
717                    .await?,
718            )
719        } else {
720            None
721        };
722
723        Ok(StatusResponse {
724            focused_task: context.task,
725            ancestors: context.ancestors,
726            siblings,
727            descendants,
728            events,
729        })
730    }
731
732    /// Get root tasks (tasks with no parent) for NoFocusResponse
733    pub async fn get_root_tasks(&self) -> Result<Vec<Task>> {
734        let tasks = sqlx::query_as::<_, Task>(
735            r#"
736            SELECT id, parent_id, name, spec, status, complexity, priority,
737                   first_todo_at, first_doing_at, first_done_at, active_form, owner
738            FROM tasks
739            WHERE parent_id IS NULL
740            ORDER BY
741                CASE status
742                    WHEN 'doing' THEN 0
743                    WHEN 'todo' THEN 1
744                    WHEN 'done' THEN 2
745                END,
746                priority ASC NULLS LAST,
747                id ASC
748            "#,
749        )
750        .fetch_all(self.pool)
751        .await?;
752
753        Ok(tasks)
754    }
755
756    /// Get events summary for a task
757    async fn get_events_summary(&self, task_id: i64) -> Result<EventsSummary> {
758        let total_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM events WHERE task_id = ?")
759            .bind(task_id)
760            .fetch_one(self.pool)
761            .await?;
762
763        let recent_events = sqlx::query_as::<_, Event>(
764            r#"
765            SELECT id, task_id, timestamp, log_type, discussion_data
766            FROM events
767            WHERE task_id = ?
768            ORDER BY timestamp DESC
769            LIMIT 10
770            "#,
771        )
772        .bind(task_id)
773        .fetch_all(self.pool)
774        .await?;
775
776        Ok(EventsSummary {
777            total_count,
778            recent_events,
779        })
780    }
781
782    /// Update a task
783    #[allow(clippy::too_many_arguments)]
784    pub async fn update_task(
785        &self,
786        id: i64,
787        name: Option<&str>,
788        spec: Option<&str>,
789        parent_id: Option<Option<i64>>,
790        status: Option<&str>,
791        complexity: Option<i32>,
792        priority: Option<i32>,
793    ) -> Result<Task> {
794        // Check task exists
795        let task = self.get_task(id).await?;
796
797        // Validate status if provided
798        if let Some(s) = status {
799            if !["todo", "doing", "done"].contains(&s) {
800                return Err(IntentError::InvalidInput(format!("Invalid status: {}", s)));
801            }
802        }
803
804        // Check for circular dependency if parent_id is being changed
805        if let Some(Some(pid)) = parent_id {
806            if pid == id {
807                return Err(IntentError::CircularDependency {
808                    blocking_task_id: pid,
809                    blocked_task_id: id,
810                });
811            }
812            self.check_task_exists(pid).await?;
813            self.check_circular_dependency(id, pid).await?;
814        }
815
816        // Build dynamic update query using QueryBuilder for SQL injection safety
817        let mut builder: sqlx::QueryBuilder<sqlx::Sqlite> =
818            sqlx::QueryBuilder::new("UPDATE tasks SET ");
819        let mut has_updates = false;
820
821        if let Some(n) = name {
822            if has_updates {
823                builder.push(", ");
824            }
825            builder.push("name = ").push_bind(n);
826            has_updates = true;
827        }
828
829        if let Some(s) = spec {
830            if has_updates {
831                builder.push(", ");
832            }
833            builder.push("spec = ").push_bind(s);
834            has_updates = true;
835        }
836
837        if let Some(pid) = parent_id {
838            if has_updates {
839                builder.push(", ");
840            }
841            match pid {
842                Some(p) => {
843                    builder.push("parent_id = ").push_bind(p);
844                },
845                None => {
846                    builder.push("parent_id = NULL");
847                },
848            }
849            has_updates = true;
850        }
851
852        if let Some(c) = complexity {
853            if has_updates {
854                builder.push(", ");
855            }
856            builder.push("complexity = ").push_bind(c);
857            has_updates = true;
858        }
859
860        if let Some(p) = priority {
861            if has_updates {
862                builder.push(", ");
863            }
864            builder.push("priority = ").push_bind(p);
865            has_updates = true;
866        }
867
868        if let Some(s) = status {
869            if has_updates {
870                builder.push(", ");
871            }
872            builder.push("status = ").push_bind(s);
873            has_updates = true;
874
875            // Update timestamp fields based on status
876            let now = Utc::now();
877            let timestamp = now.to_rfc3339();
878            match s {
879                "todo" if task.first_todo_at.is_none() => {
880                    builder.push(", first_todo_at = ").push_bind(timestamp);
881                },
882                "doing" if task.first_doing_at.is_none() => {
883                    builder.push(", first_doing_at = ").push_bind(timestamp);
884                },
885                "done" if task.first_done_at.is_none() => {
886                    builder.push(", first_done_at = ").push_bind(timestamp);
887                },
888                _ => {},
889            }
890        }
891
892        if !has_updates {
893            return Ok(task);
894        }
895
896        builder.push(" WHERE id = ").push_bind(id);
897
898        builder.build().execute(self.pool).await?;
899
900        let task = self.get_task(id).await?;
901
902        // Notify WebSocket clients about the task update
903        self.notify_task_updated(&task).await;
904
905        Ok(task)
906    }
907
908    /// Delete a task
909    pub async fn delete_task(&self, id: i64) -> Result<()> {
910        self.check_task_exists(id).await?;
911
912        sqlx::query("DELETE FROM tasks WHERE id = ?")
913            .bind(id)
914            .execute(self.pool)
915            .await?;
916
917        // Notify WebSocket clients about the task deletion
918        self.notify_task_deleted(id).await;
919
920        Ok(())
921    }
922
923    /// Find tasks with optional filters, sorting, and pagination
924    pub async fn find_tasks(
925        &self,
926        status: Option<&str>,
927        parent_id: Option<Option<i64>>,
928        sort_by: Option<TaskSortBy>,
929        limit: Option<i64>,
930        offset: Option<i64>,
931    ) -> Result<PaginatedTasks> {
932        // Apply defaults
933        let sort_by = sort_by.unwrap_or_default(); // Default: FocusAware
934        let limit = limit.unwrap_or(100);
935        let offset = offset.unwrap_or(0);
936
937        // Resolve session_id for FocusAware sorting
938        let session_id = crate::workspace::resolve_session_id(None);
939
940        // Build WHERE clause
941        let mut where_clause = String::from("WHERE 1=1");
942        let mut conditions = Vec::new();
943
944        if let Some(s) = status {
945            where_clause.push_str(" AND status = ?");
946            conditions.push(s.to_string());
947        }
948
949        if let Some(pid) = parent_id {
950            if let Some(p) = pid {
951                where_clause.push_str(" AND parent_id = ?");
952                conditions.push(p.to_string());
953            } else {
954                where_clause.push_str(" AND parent_id IS NULL");
955            }
956        }
957
958        // Track if FocusAware mode needs session_id bind
959        let uses_session_bind = matches!(sort_by, TaskSortBy::FocusAware);
960
961        // Build ORDER BY clause based on sort mode
962        let order_clause = match sort_by {
963            TaskSortBy::Id => {
964                // Legacy: simple ORDER BY id ASC
965                "ORDER BY id ASC".to_string()
966            },
967            TaskSortBy::Priority => {
968                // ORDER BY priority ASC, complexity ASC, id ASC
969                "ORDER BY COALESCE(priority, 999) ASC, COALESCE(complexity, 5) ASC, id ASC"
970                    .to_string()
971            },
972            TaskSortBy::Time => {
973                // ORDER BY timestamp based on status
974                r#"ORDER BY
975                    CASE status
976                        WHEN 'doing' THEN first_doing_at
977                        WHEN 'todo' THEN first_todo_at
978                        WHEN 'done' THEN first_done_at
979                    END ASC NULLS LAST,
980                    id ASC"#
981                    .to_string()
982            },
983            TaskSortBy::FocusAware => {
984                // Focus-aware: current focused task → doing tasks → todo tasks
985                r#"ORDER BY
986                    CASE
987                        WHEN t.id = (SELECT current_task_id FROM sessions WHERE session_id = ?) THEN 0
988                        WHEN t.status = 'doing' THEN 1
989                        WHEN t.status = 'todo' THEN 2
990                        ELSE 3
991                    END ASC,
992                    COALESCE(t.priority, 999) ASC,
993                    t.id ASC"#
994                    .to_string()
995            },
996        };
997
998        // Get total count
999        let count_query = format!("SELECT COUNT(*) FROM tasks {}", where_clause);
1000        let mut count_q = sqlx::query_scalar::<_, i64>(&count_query);
1001        for cond in &conditions {
1002            count_q = count_q.bind(cond);
1003        }
1004        let total_count = count_q.fetch_one(self.pool).await?;
1005
1006        // Build main query with pagination
1007        let main_query = format!(
1008            "SELECT id, parent_id, name, NULL as spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner FROM tasks t {} {} LIMIT ? OFFSET ?",
1009            where_clause, order_clause
1010        );
1011
1012        let mut q = sqlx::query_as::<_, Task>(&main_query);
1013        for cond in conditions {
1014            q = q.bind(cond);
1015        }
1016        // Bind session_id for FocusAware ORDER BY clause
1017        if uses_session_bind {
1018            q = q.bind(&session_id);
1019        }
1020        q = q.bind(limit);
1021        q = q.bind(offset);
1022
1023        let tasks = q.fetch_all(self.pool).await?;
1024
1025        // Calculate has_more
1026        let has_more = offset + (tasks.len() as i64) < total_count;
1027
1028        Ok(PaginatedTasks {
1029            tasks,
1030            total_count,
1031            has_more,
1032            limit,
1033            offset,
1034        })
1035    }
1036
1037    /// Get workspace statistics using SQL aggregation (no data loading)
1038    ///
1039    /// This is much more efficient than loading all tasks just to count them.
1040    /// Used by session restore when there's no focused task.
1041    pub async fn get_stats(&self) -> Result<WorkspaceStats> {
1042        let row = sqlx::query_as::<_, (i64, i64, i64, i64)>(
1043            r#"SELECT
1044                COUNT(*) as total,
1045                COALESCE(SUM(CASE WHEN status = 'todo' THEN 1 ELSE 0 END), 0),
1046                COALESCE(SUM(CASE WHEN status = 'doing' THEN 1 ELSE 0 END), 0),
1047                COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0)
1048            FROM tasks"#,
1049        )
1050        .fetch_one(self.pool)
1051        .await?;
1052
1053        Ok(WorkspaceStats {
1054            total_tasks: row.0,
1055            todo: row.1,
1056            doing: row.2,
1057            done: row.3,
1058        })
1059    }
1060
1061    /// Start a task (atomic: update status + set current)
1062    #[tracing::instrument(skip(self))]
1063    pub async fn start_task(&self, id: i64, with_events: bool) -> Result<TaskWithEvents> {
1064        // Check if task exists first
1065        let task_exists: bool =
1066            sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1067                .bind(id)
1068                .fetch_one(self.pool)
1069                .await?;
1070
1071        if !task_exists {
1072            return Err(IntentError::TaskNotFound(id));
1073        }
1074
1075        // Check if task is blocked by incomplete dependencies
1076        use crate::dependencies::get_incomplete_blocking_tasks;
1077        if let Some(blocking_tasks) = get_incomplete_blocking_tasks(self.pool, id).await? {
1078            return Err(IntentError::TaskBlocked {
1079                task_id: id,
1080                blocking_task_ids: blocking_tasks,
1081            });
1082        }
1083
1084        let mut tx = self.pool.begin().await?;
1085
1086        let now = Utc::now();
1087
1088        // Update task status to doing
1089        sqlx::query(
1090            r#"
1091            UPDATE tasks
1092            SET status = 'doing', first_doing_at = COALESCE(first_doing_at, ?)
1093            WHERE id = ?
1094            "#,
1095        )
1096        .bind(now)
1097        .bind(id)
1098        .execute(&mut *tx)
1099        .await?;
1100
1101        // Set as current task in sessions table
1102        // Use session_id from environment if available
1103        let session_id = crate::workspace::resolve_session_id(None);
1104        sqlx::query(
1105            r#"
1106            INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
1107            VALUES (?, ?, datetime('now'), datetime('now'))
1108            ON CONFLICT(session_id) DO UPDATE SET
1109                current_task_id = excluded.current_task_id,
1110                last_active_at = datetime('now')
1111            "#,
1112        )
1113        .bind(&session_id)
1114        .bind(id)
1115        .execute(&mut *tx)
1116        .await?;
1117
1118        tx.commit().await?;
1119
1120        if with_events {
1121            let result = self.get_task_with_events(id).await?;
1122            self.notify_task_updated(&result.task).await;
1123            Ok(result)
1124        } else {
1125            let task = self.get_task(id).await?;
1126            self.notify_task_updated(&task).await;
1127            Ok(TaskWithEvents {
1128                task,
1129                events_summary: None,
1130            })
1131        }
1132    }
1133
1134    /// Complete the current focused task (atomic: check children + update status + clear current)
1135    /// This command only operates on the current_task_id.
1136    /// Prerequisites: A task must be set as current
1137    ///
1138    /// # Arguments
1139    /// * `is_ai_caller` - Whether this is called from AI (MCP) or human (CLI/Dashboard).
1140    ///   When true and task is human-owned, the operation will fail.
1141    ///   Human tasks can only be completed via CLI or Dashboard.
1142    #[tracing::instrument(skip(self))]
1143    pub async fn done_task(&self, is_ai_caller: bool) -> Result<DoneTaskResponse> {
1144        let session_id = crate::workspace::resolve_session_id(None);
1145        let mut tx = self.pool.begin().await?;
1146
1147        // Get the current task ID from sessions table
1148        let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1149            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1150        )
1151        .bind(&session_id)
1152        .fetch_optional(&mut *tx)
1153        .await?
1154        .flatten();
1155
1156        let id = current_task_id.ok_or(IntentError::InvalidInput(
1157            "No current task is set. Use 'current --set <ID>' to set a task first.".to_string(),
1158        ))?;
1159
1160        // Get the task info before completing it (including owner)
1161        let task_info: (String, Option<i64>, String) =
1162            sqlx::query_as("SELECT name, parent_id, owner FROM tasks WHERE id = ?")
1163                .bind(id)
1164                .fetch_one(&mut *tx)
1165                .await?;
1166        let (task_name, parent_id, owner) = task_info;
1167
1168        // Human Task Protection: AI cannot complete human-owned tasks
1169        // Human must complete their own tasks via CLI or Dashboard
1170        if owner == "human" && is_ai_caller {
1171            return Err(IntentError::HumanTaskCannotBeCompletedByAI {
1172                task_id: id,
1173                task_name: task_name.clone(),
1174            });
1175        }
1176
1177        // Complete the task (validates children + updates status)
1178        self.complete_task_in_tx(&mut tx, id).await?;
1179
1180        // Clear the current task in sessions table for this session
1181        sqlx::query("UPDATE sessions SET current_task_id = NULL, last_active_at = datetime('now') WHERE session_id = ?")
1182            .bind(&session_id)
1183            .execute(&mut *tx)
1184            .await?;
1185
1186        // Determine next step suggestion based on context
1187        let next_step_suggestion = if let Some(parent_task_id) = parent_id {
1188            // Task has a parent - check sibling status
1189            let remaining_siblings: i64 = sqlx::query_scalar::<_, i64>(
1190                "SELECT COUNT(*) FROM tasks WHERE parent_id = ? AND status != 'done' AND id != ?",
1191            )
1192            .bind(parent_task_id)
1193            .bind(id)
1194            .fetch_one(&mut *tx)
1195            .await?;
1196
1197            if remaining_siblings == 0 {
1198                // All siblings are done - parent is ready
1199                let parent_name: String =
1200                    sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1201                        .bind(parent_task_id)
1202                        .fetch_one(&mut *tx)
1203                        .await?;
1204
1205                NextStepSuggestion::ParentIsReady {
1206                    message: format!(
1207                        "All sub-tasks of parent #{} '{}' are now complete. The parent task is ready for your attention.",
1208                        parent_task_id, parent_name
1209                    ),
1210                    parent_task_id,
1211                    parent_task_name: parent_name,
1212                }
1213            } else {
1214                // Siblings remain
1215                let parent_name: String =
1216                    sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1217                        .bind(parent_task_id)
1218                        .fetch_one(&mut *tx)
1219                        .await?;
1220
1221                NextStepSuggestion::SiblingTasksRemain {
1222                    message: format!(
1223                        "Task #{} completed. Parent task #{} '{}' has other sub-tasks remaining.",
1224                        id, parent_task_id, parent_name
1225                    ),
1226                    parent_task_id,
1227                    parent_task_name: parent_name,
1228                    remaining_siblings_count: remaining_siblings,
1229                }
1230            }
1231        } else {
1232            // No parent - check if this was a top-level task with children or standalone
1233            let child_count: i64 =
1234                sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_CHILDREN_TOTAL)
1235                    .bind(id)
1236                    .fetch_one(&mut *tx)
1237                    .await?;
1238
1239            if child_count > 0 {
1240                // Top-level task with children completed
1241                NextStepSuggestion::TopLevelTaskCompleted {
1242                    message: format!(
1243                        "Top-level task #{} '{}' has been completed. Well done!",
1244                        id, task_name
1245                    ),
1246                    completed_task_id: id,
1247                    completed_task_name: task_name.clone(),
1248                }
1249            } else {
1250                // Check if workspace is clear
1251                let remaining_tasks: i64 = sqlx::query_scalar::<_, i64>(
1252                    "SELECT COUNT(*) FROM tasks WHERE status != 'done' AND id != ?",
1253                )
1254                .bind(id)
1255                .fetch_one(&mut *tx)
1256                .await?;
1257
1258                if remaining_tasks == 0 {
1259                    NextStepSuggestion::WorkspaceIsClear {
1260                        message: format!(
1261                            "Project complete! Task #{} was the last remaining task. There are no more 'todo' or 'doing' tasks.",
1262                            id
1263                        ),
1264                        completed_task_id: id,
1265                    }
1266                } else {
1267                    NextStepSuggestion::NoParentContext {
1268                        message: format!("Task #{} '{}' has been completed.", id, task_name),
1269                        completed_task_id: id,
1270                        completed_task_name: task_name.clone(),
1271                    }
1272                }
1273            }
1274        };
1275
1276        tx.commit().await?;
1277
1278        // Fetch the completed task to notify UI
1279        let completed_task = self.get_task(id).await?;
1280        self.notify_task_updated(&completed_task).await;
1281
1282        Ok(DoneTaskResponse {
1283            completed_task,
1284            workspace_status: WorkspaceStatus {
1285                current_task_id: None,
1286            },
1287            next_step_suggestion,
1288        })
1289    }
1290
1291    /// Check if a task exists
1292    async fn check_task_exists(&self, id: i64) -> Result<()> {
1293        let exists: bool = sqlx::query_scalar::<_, bool>(crate::sql_constants::CHECK_TASK_EXISTS)
1294            .bind(id)
1295            .fetch_one(self.pool)
1296            .await?;
1297
1298        if !exists {
1299            return Err(IntentError::TaskNotFound(id));
1300        }
1301
1302        Ok(())
1303    }
1304
1305    /// Check for circular dependencies
1306    async fn check_circular_dependency(&self, task_id: i64, new_parent_id: i64) -> Result<()> {
1307        let mut current_id = new_parent_id;
1308
1309        loop {
1310            if current_id == task_id {
1311                return Err(IntentError::CircularDependency {
1312                    blocking_task_id: new_parent_id,
1313                    blocked_task_id: task_id,
1314                });
1315            }
1316
1317            let parent: Option<i64> =
1318                sqlx::query_scalar::<_, Option<i64>>(crate::sql_constants::SELECT_TASK_PARENT_ID)
1319                    .bind(current_id)
1320                    .fetch_optional(self.pool)
1321                    .await?
1322                    .flatten();
1323
1324            match parent {
1325                Some(pid) => current_id = pid,
1326                None => break,
1327            }
1328        }
1329
1330        Ok(())
1331    }
1332    /// Create a subtask under the current task and switch to it (atomic operation)
1333    /// Returns error if there is no current task
1334    /// Returns response with subtask info and parent task info
1335    pub async fn spawn_subtask(
1336        &self,
1337        name: &str,
1338        spec: Option<&str>,
1339    ) -> Result<SpawnSubtaskResponse> {
1340        // Get current task from sessions table for this session
1341        let session_id = crate::workspace::resolve_session_id(None);
1342        let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1343            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1344        )
1345        .bind(&session_id)
1346        .fetch_optional(self.pool)
1347        .await?
1348        .flatten();
1349
1350        let parent_id = current_task_id.ok_or(IntentError::InvalidInput(
1351            "No current task to create subtask under".to_string(),
1352        ))?;
1353
1354        // Get parent task info
1355        let parent_name: String =
1356            sqlx::query_scalar::<_, String>(crate::sql_constants::SELECT_TASK_NAME)
1357                .bind(parent_id)
1358                .fetch_one(self.pool)
1359                .await?;
1360
1361        // Create the subtask with AI ownership (CLI operation)
1362        let subtask = self
1363            .add_task(name, spec, Some(parent_id), Some("ai"))
1364            .await?;
1365
1366        // Start the new subtask (sets status to doing and updates current_task_id)
1367        // This keeps the parent task in 'doing' status (multi-doing design)
1368        self.start_task(subtask.id, false).await?;
1369
1370        Ok(SpawnSubtaskResponse {
1371            subtask: SubtaskInfo {
1372                id: subtask.id,
1373                name: subtask.name,
1374                parent_id,
1375                status: "doing".to_string(),
1376            },
1377            parent_task: ParentTaskInfo {
1378                id: parent_id,
1379                name: parent_name,
1380            },
1381        })
1382    }
1383
1384    /// Intelligently pick tasks from 'todo' and transition them to 'doing'
1385    /// Returns tasks that were successfully transitioned
1386    ///
1387    /// # Arguments
1388    /// * `max_count` - Maximum number of tasks to pick
1389    /// * `capacity_limit` - Maximum total number of tasks allowed in 'doing' status
1390    ///
1391    /// # Logic
1392    /// 1. Check current 'doing' task count
1393    /// 2. Calculate available capacity
1394    /// 3. Select tasks from 'todo' (prioritized by: priority DESC, complexity ASC)
1395    /// 4. Transition selected tasks to 'doing'
1396    pub async fn pick_next_tasks(
1397        &self,
1398        max_count: usize,
1399        capacity_limit: usize,
1400    ) -> Result<Vec<Task>> {
1401        let mut tx = self.pool.begin().await?;
1402
1403        // Get current doing count
1404        let doing_count: i64 =
1405            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
1406                .fetch_one(&mut *tx)
1407                .await?;
1408
1409        // Calculate available capacity
1410        let available = capacity_limit.saturating_sub(doing_count as usize);
1411        if available == 0 {
1412            return Ok(vec![]);
1413        }
1414
1415        let limit = std::cmp::min(max_count, available);
1416
1417        // Select tasks from todo, prioritizing by priority DESC, complexity ASC
1418        let todo_tasks = sqlx::query_as::<_, Task>(
1419            r#"
1420                        SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
1421                        FROM tasks
1422                        WHERE status = 'todo'
1423                        ORDER BY
1424                            COALESCE(priority, 0) ASC,
1425                            COALESCE(complexity, 5) ASC,
1426                            id ASC
1427                        LIMIT ?
1428                        "#,
1429        )
1430        .bind(limit as i64)
1431        .fetch_all(&mut *tx)
1432        .await?;
1433
1434        if todo_tasks.is_empty() {
1435            return Ok(vec![]);
1436        }
1437
1438        let now = Utc::now();
1439
1440        // Transition selected tasks to 'doing'
1441        for task in &todo_tasks {
1442            sqlx::query(
1443                r#"
1444                UPDATE tasks
1445                SET status = 'doing',
1446                    first_doing_at = COALESCE(first_doing_at, ?)
1447                WHERE id = ?
1448                "#,
1449            )
1450            .bind(now)
1451            .bind(task.id)
1452            .execute(&mut *tx)
1453            .await?;
1454        }
1455
1456        tx.commit().await?;
1457
1458        // Fetch and return updated tasks in the same order
1459        let task_ids: Vec<i64> = todo_tasks.iter().map(|t| t.id).collect();
1460        let placeholders = vec!["?"; task_ids.len()].join(",");
1461        let query = format!(
1462            "SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
1463                         FROM tasks WHERE id IN ({})
1464                         ORDER BY
1465                             COALESCE(priority, 0) ASC,
1466                             COALESCE(complexity, 5) ASC,
1467                             id ASC",
1468            placeholders
1469        );
1470
1471        let mut q = sqlx::query_as::<_, Task>(&query);
1472        for id in task_ids {
1473            q = q.bind(id);
1474        }
1475
1476        let updated_tasks = q.fetch_all(self.pool).await?;
1477        Ok(updated_tasks)
1478    }
1479
1480    /// Intelligently recommend the next task to work on based on context-aware priority model.
1481    ///
1482    /// Priority logic:
1483    /// 1. First priority: Subtasks of the current focused task (depth-first)
1484    /// 2. Second priority: Top-level tasks (breadth-first)
1485    /// 3. No recommendation: Return appropriate empty state
1486    ///
1487    /// This command does NOT modify task status.
1488    pub async fn pick_next(&self) -> Result<PickNextResponse> {
1489        // Step 1: Check if there's a current focused task for this session
1490        let session_id = crate::workspace::resolve_session_id(None);
1491        let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1492            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1493        )
1494        .bind(&session_id)
1495        .fetch_optional(self.pool)
1496        .await?
1497        .flatten();
1498
1499        if let Some(current_id) = current_task_id {
1500            // Step 1a: First priority - Get **doing** subtasks of current focused task
1501            // Exclude tasks blocked by incomplete dependencies
1502            let doing_subtasks = sqlx::query_as::<_, Task>(
1503                r#"
1504                        SELECT id, parent_id, name, spec, status, complexity, priority,
1505                               first_todo_at, first_doing_at, first_done_at, active_form, owner
1506                        FROM tasks
1507                        WHERE parent_id = ? AND status = 'doing'
1508                          AND NOT EXISTS (
1509                            SELECT 1 FROM dependencies d
1510                            JOIN tasks bt ON d.blocking_task_id = bt.id
1511                            WHERE d.blocked_task_id = tasks.id
1512                              AND bt.status != 'done'
1513                          )
1514                        ORDER BY COALESCE(priority, 999999) ASC, id ASC
1515                        LIMIT 1
1516                        "#,
1517            )
1518            .bind(current_id)
1519            .fetch_optional(self.pool)
1520            .await?;
1521
1522            if let Some(task) = doing_subtasks {
1523                return Ok(PickNextResponse::focused_subtask(task));
1524            }
1525
1526            // Step 1b: Second priority - Get **todo** subtasks if no doing subtasks
1527            let todo_subtasks = sqlx::query_as::<_, Task>(
1528                r#"
1529                            SELECT id, parent_id, name, spec, status, complexity, priority,
1530                                   first_todo_at, first_doing_at, first_done_at, active_form, owner
1531                            FROM tasks
1532                            WHERE parent_id = ? AND status = 'todo'
1533                              AND NOT EXISTS (
1534                                SELECT 1 FROM dependencies d
1535                                JOIN tasks bt ON d.blocking_task_id = bt.id
1536                                WHERE d.blocked_task_id = tasks.id
1537                                  AND bt.status != 'done'
1538                              )
1539                            ORDER BY COALESCE(priority, 999999) ASC, id ASC
1540                            LIMIT 1
1541                            "#,
1542            )
1543            .bind(current_id)
1544            .fetch_optional(self.pool)
1545            .await?;
1546
1547            if let Some(task) = todo_subtasks {
1548                return Ok(PickNextResponse::focused_subtask(task));
1549            }
1550        }
1551
1552        // Step 2a: Third priority - Get top-level **doing** tasks (excluding current task)
1553        // Exclude tasks blocked by incomplete dependencies
1554        let doing_top_level = if let Some(current_id) = current_task_id {
1555            sqlx::query_as::<_, Task>(
1556                r#"
1557                SELECT id, parent_id, name, spec, status, complexity, priority,
1558                       first_todo_at, first_doing_at, first_done_at, active_form, owner
1559                FROM tasks
1560                WHERE parent_id IS NULL AND status = 'doing' AND id != ?
1561                  AND NOT EXISTS (
1562                    SELECT 1 FROM dependencies d
1563                    JOIN tasks bt ON d.blocking_task_id = bt.id
1564                    WHERE d.blocked_task_id = tasks.id
1565                      AND bt.status != 'done'
1566                  )
1567                ORDER BY COALESCE(priority, 999999) ASC, id ASC
1568                LIMIT 1
1569                "#,
1570            )
1571            .bind(current_id)
1572            .fetch_optional(self.pool)
1573            .await?
1574        } else {
1575            sqlx::query_as::<_, Task>(
1576                r#"
1577                SELECT id, parent_id, name, spec, status, complexity, priority,
1578                       first_todo_at, first_doing_at, first_done_at, active_form, owner
1579                FROM tasks
1580                WHERE parent_id IS NULL AND status = 'doing'
1581                  AND NOT EXISTS (
1582                    SELECT 1 FROM dependencies d
1583                    JOIN tasks bt ON d.blocking_task_id = bt.id
1584                    WHERE d.blocked_task_id = tasks.id
1585                      AND bt.status != 'done'
1586                  )
1587                ORDER BY COALESCE(priority, 999999) ASC, id ASC
1588                LIMIT 1
1589                "#,
1590            )
1591            .fetch_optional(self.pool)
1592            .await?
1593        };
1594
1595        if let Some(task) = doing_top_level {
1596            return Ok(PickNextResponse::top_level_task(task));
1597        }
1598
1599        // Step 2b: Fourth priority - Get top-level **todo** tasks
1600        // Exclude tasks blocked by incomplete dependencies
1601        let todo_top_level = sqlx::query_as::<_, Task>(
1602            r#"
1603            SELECT id, parent_id, name, spec, status, complexity, priority,
1604                   first_todo_at, first_doing_at, first_done_at, active_form, owner
1605            FROM tasks
1606            WHERE parent_id IS NULL AND status = 'todo'
1607              AND NOT EXISTS (
1608                SELECT 1 FROM dependencies d
1609                JOIN tasks bt ON d.blocking_task_id = bt.id
1610                WHERE d.blocked_task_id = tasks.id
1611                  AND bt.status != 'done'
1612              )
1613            ORDER BY COALESCE(priority, 999999) ASC, id ASC
1614            LIMIT 1
1615            "#,
1616        )
1617        .fetch_optional(self.pool)
1618        .await?;
1619
1620        if let Some(task) = todo_top_level {
1621            return Ok(PickNextResponse::top_level_task(task));
1622        }
1623
1624        // Step 3: No recommendation - determine why
1625        // Check if there are any tasks at all
1626        let total_tasks: i64 =
1627            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_TOTAL)
1628                .fetch_one(self.pool)
1629                .await?;
1630
1631        if total_tasks == 0 {
1632            return Ok(PickNextResponse::no_tasks_in_project());
1633        }
1634
1635        // Check if all tasks are completed
1636        let todo_or_doing_count: i64 = sqlx::query_scalar::<_, i64>(
1637            "SELECT COUNT(*) FROM tasks WHERE status IN ('todo', 'doing')",
1638        )
1639        .fetch_one(self.pool)
1640        .await?;
1641
1642        if todo_or_doing_count == 0 {
1643            return Ok(PickNextResponse::all_tasks_completed());
1644        }
1645
1646        // Otherwise, there are tasks but none available based on current context
1647        Ok(PickNextResponse::no_available_todos())
1648    }
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653    use super::*;
1654    use crate::events::EventManager;
1655    use crate::test_utils::test_helpers::TestContext;
1656    use crate::workspace::WorkspaceManager;
1657
1658    #[tokio::test]
1659    async fn test_get_stats_empty() {
1660        let ctx = TestContext::new().await;
1661        let manager = TaskManager::new(ctx.pool());
1662
1663        let stats = manager.get_stats().await.unwrap();
1664
1665        assert_eq!(stats.total_tasks, 0);
1666        assert_eq!(stats.todo, 0);
1667        assert_eq!(stats.doing, 0);
1668        assert_eq!(stats.done, 0);
1669    }
1670
1671    #[tokio::test]
1672    async fn test_get_stats_with_tasks() {
1673        let ctx = TestContext::new().await;
1674        let manager = TaskManager::new(ctx.pool());
1675
1676        // Create tasks with different statuses
1677        let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
1678        let task2 = manager.add_task("Task 2", None, None, None).await.unwrap();
1679        let _task3 = manager.add_task("Task 3", None, None, None).await.unwrap();
1680
1681        // Update statuses
1682        manager
1683            .update_task(task1.id, None, None, None, Some("doing"), None, None)
1684            .await
1685            .unwrap();
1686        manager
1687            .update_task(task2.id, None, None, None, Some("done"), None, None)
1688            .await
1689            .unwrap();
1690        // task3 stays as todo
1691
1692        let stats = manager.get_stats().await.unwrap();
1693
1694        assert_eq!(stats.total_tasks, 3);
1695        assert_eq!(stats.todo, 1);
1696        assert_eq!(stats.doing, 1);
1697        assert_eq!(stats.done, 1);
1698    }
1699
1700    #[tokio::test]
1701    async fn test_add_task() {
1702        let ctx = TestContext::new().await;
1703        let manager = TaskManager::new(ctx.pool());
1704
1705        let task = manager
1706            .add_task("Test task", None, None, None)
1707            .await
1708            .unwrap();
1709
1710        assert_eq!(task.name, "Test task");
1711        assert_eq!(task.status, "todo");
1712        assert!(task.first_todo_at.is_some());
1713        assert!(task.first_doing_at.is_none());
1714        assert!(task.first_done_at.is_none());
1715    }
1716
1717    #[tokio::test]
1718    async fn test_add_task_with_spec() {
1719        let ctx = TestContext::new().await;
1720        let manager = TaskManager::new(ctx.pool());
1721
1722        let spec = "This is a task specification";
1723        let task = manager
1724            .add_task("Test task", Some(spec), None, None)
1725            .await
1726            .unwrap();
1727
1728        assert_eq!(task.name, "Test task");
1729        assert_eq!(task.spec.as_deref(), Some(spec));
1730    }
1731
1732    #[tokio::test]
1733    async fn test_add_task_with_parent() {
1734        let ctx = TestContext::new().await;
1735        let manager = TaskManager::new(ctx.pool());
1736
1737        let parent = manager
1738            .add_task("Parent task", None, None, None)
1739            .await
1740            .unwrap();
1741        let child = manager
1742            .add_task("Child task", None, Some(parent.id), None)
1743            .await
1744            .unwrap();
1745
1746        assert_eq!(child.parent_id, Some(parent.id));
1747    }
1748
1749    #[tokio::test]
1750    async fn test_get_task() {
1751        let ctx = TestContext::new().await;
1752        let manager = TaskManager::new(ctx.pool());
1753
1754        let created = manager
1755            .add_task("Test task", None, None, None)
1756            .await
1757            .unwrap();
1758        let retrieved = manager.get_task(created.id).await.unwrap();
1759
1760        assert_eq!(created.id, retrieved.id);
1761        assert_eq!(created.name, retrieved.name);
1762    }
1763
1764    #[tokio::test]
1765    async fn test_get_task_not_found() {
1766        let ctx = TestContext::new().await;
1767        let manager = TaskManager::new(ctx.pool());
1768
1769        let result = manager.get_task(999).await;
1770        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
1771    }
1772
1773    #[tokio::test]
1774    async fn test_update_task_name() {
1775        let ctx = TestContext::new().await;
1776        let manager = TaskManager::new(ctx.pool());
1777
1778        let task = manager
1779            .add_task("Original name", None, None, None)
1780            .await
1781            .unwrap();
1782        let updated = manager
1783            .update_task(task.id, Some("New name"), None, None, None, None, None)
1784            .await
1785            .unwrap();
1786
1787        assert_eq!(updated.name, "New name");
1788    }
1789
1790    #[tokio::test]
1791    async fn test_update_task_status() {
1792        let ctx = TestContext::new().await;
1793        let manager = TaskManager::new(ctx.pool());
1794
1795        let task = manager
1796            .add_task("Test task", None, None, None)
1797            .await
1798            .unwrap();
1799        let updated = manager
1800            .update_task(task.id, None, None, None, Some("doing"), None, None)
1801            .await
1802            .unwrap();
1803
1804        assert_eq!(updated.status, "doing");
1805        assert!(updated.first_doing_at.is_some());
1806    }
1807
1808    #[tokio::test]
1809    async fn test_delete_task() {
1810        let ctx = TestContext::new().await;
1811        let manager = TaskManager::new(ctx.pool());
1812
1813        let task = manager
1814            .add_task("Test task", None, None, None)
1815            .await
1816            .unwrap();
1817        manager.delete_task(task.id).await.unwrap();
1818
1819        let result = manager.get_task(task.id).await;
1820        assert!(result.is_err());
1821    }
1822
1823    #[tokio::test]
1824    async fn test_find_tasks_by_status() {
1825        let ctx = TestContext::new().await;
1826        let manager = TaskManager::new(ctx.pool());
1827
1828        manager
1829            .add_task("Todo task", None, None, None)
1830            .await
1831            .unwrap();
1832        let doing_task = manager
1833            .add_task("Doing task", None, None, None)
1834            .await
1835            .unwrap();
1836        manager
1837            .update_task(doing_task.id, None, None, None, Some("doing"), None, None)
1838            .await
1839            .unwrap();
1840
1841        let todo_result = manager
1842            .find_tasks(Some("todo"), None, None, None, None)
1843            .await
1844            .unwrap();
1845        let doing_result = manager
1846            .find_tasks(Some("doing"), None, None, None, None)
1847            .await
1848            .unwrap();
1849
1850        assert_eq!(todo_result.tasks.len(), 1);
1851        assert_eq!(doing_result.tasks.len(), 1);
1852        assert_eq!(doing_result.tasks[0].status, "doing");
1853    }
1854
1855    #[tokio::test]
1856    async fn test_find_tasks_by_parent() {
1857        let ctx = TestContext::new().await;
1858        let manager = TaskManager::new(ctx.pool());
1859
1860        let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1861        manager
1862            .add_task("Child 1", None, Some(parent.id), None)
1863            .await
1864            .unwrap();
1865        manager
1866            .add_task("Child 2", None, Some(parent.id), None)
1867            .await
1868            .unwrap();
1869
1870        let result = manager
1871            .find_tasks(None, Some(Some(parent.id)), None, None, None)
1872            .await
1873            .unwrap();
1874
1875        assert_eq!(result.tasks.len(), 2);
1876    }
1877
1878    #[tokio::test]
1879    async fn test_start_task() {
1880        let ctx = TestContext::new().await;
1881        let manager = TaskManager::new(ctx.pool());
1882
1883        let task = manager
1884            .add_task("Test task", None, None, None)
1885            .await
1886            .unwrap();
1887        let started = manager.start_task(task.id, false).await.unwrap();
1888
1889        assert_eq!(started.task.status, "doing");
1890        assert!(started.task.first_doing_at.is_some());
1891
1892        // Verify it's set as current task
1893        let session_id = crate::workspace::resolve_session_id(None);
1894        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1895            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1896        )
1897        .bind(&session_id)
1898        .fetch_optional(ctx.pool())
1899        .await
1900        .unwrap()
1901        .flatten();
1902
1903        assert_eq!(current, Some(task.id));
1904    }
1905
1906    #[tokio::test]
1907    async fn test_start_task_with_events() {
1908        let ctx = TestContext::new().await;
1909        let manager = TaskManager::new(ctx.pool());
1910
1911        let task = manager
1912            .add_task("Test task", None, None, None)
1913            .await
1914            .unwrap();
1915
1916        // Add an event
1917        sqlx::query("INSERT INTO events (task_id, log_type, discussion_data) VALUES (?, ?, ?)")
1918            .bind(task.id)
1919            .bind("test")
1920            .bind("test event")
1921            .execute(ctx.pool())
1922            .await
1923            .unwrap();
1924
1925        let started = manager.start_task(task.id, true).await.unwrap();
1926
1927        assert!(started.events_summary.is_some());
1928        let summary = started.events_summary.unwrap();
1929        assert_eq!(summary.total_count, 1);
1930    }
1931
1932    #[tokio::test]
1933    async fn test_done_task() {
1934        let ctx = TestContext::new().await;
1935        let manager = TaskManager::new(ctx.pool());
1936
1937        let task = manager
1938            .add_task("Test task", None, None, None)
1939            .await
1940            .unwrap();
1941        manager.start_task(task.id, false).await.unwrap();
1942        let response = manager.done_task(false).await.unwrap();
1943
1944        assert_eq!(response.completed_task.status, "done");
1945        assert!(response.completed_task.first_done_at.is_some());
1946        assert_eq!(response.workspace_status.current_task_id, None);
1947
1948        // Should be WORKSPACE_IS_CLEAR since it's the only task
1949        match response.next_step_suggestion {
1950            NextStepSuggestion::WorkspaceIsClear { .. } => {},
1951            _ => panic!("Expected WorkspaceIsClear suggestion"),
1952        }
1953
1954        // Verify current task is cleared
1955        let session_id = crate::workspace::resolve_session_id(None);
1956        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
1957            "SELECT current_task_id FROM sessions WHERE session_id = ?",
1958        )
1959        .bind(&session_id)
1960        .fetch_optional(ctx.pool())
1961        .await
1962        .unwrap()
1963        .flatten();
1964
1965        assert!(current.is_none());
1966    }
1967
1968    #[tokio::test]
1969    async fn test_done_task_with_uncompleted_children() {
1970        let ctx = TestContext::new().await;
1971        let manager = TaskManager::new(ctx.pool());
1972
1973        let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1974        manager
1975            .add_task("Child", None, Some(parent.id), None)
1976            .await
1977            .unwrap();
1978
1979        // Set parent as current task
1980        manager.start_task(parent.id, false).await.unwrap();
1981
1982        let result = manager.done_task(false).await;
1983        assert!(matches!(result, Err(IntentError::UncompletedChildren)));
1984    }
1985
1986    #[tokio::test]
1987    async fn test_done_task_with_completed_children() {
1988        let ctx = TestContext::new().await;
1989        let manager = TaskManager::new(ctx.pool());
1990
1991        let parent = manager.add_task("Parent", None, None, None).await.unwrap();
1992        let child = manager
1993            .add_task("Child", None, Some(parent.id), None)
1994            .await
1995            .unwrap();
1996
1997        // Complete child first
1998        manager.start_task(child.id, false).await.unwrap();
1999        let child_response = manager.done_task(false).await.unwrap();
2000
2001        // Child completion should suggest parent is ready
2002        match child_response.next_step_suggestion {
2003            NextStepSuggestion::ParentIsReady { parent_task_id, .. } => {
2004                assert_eq!(parent_task_id, parent.id);
2005            },
2006            _ => panic!("Expected ParentIsReady suggestion"),
2007        }
2008
2009        // Now parent can be completed
2010        manager.start_task(parent.id, false).await.unwrap();
2011        let parent_response = manager.done_task(false).await.unwrap();
2012        assert_eq!(parent_response.completed_task.status, "done");
2013
2014        // Parent completion should indicate top-level task completed (since it had children)
2015        match parent_response.next_step_suggestion {
2016            NextStepSuggestion::TopLevelTaskCompleted { .. } => {},
2017            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
2018        }
2019    }
2020
2021    #[tokio::test]
2022    async fn test_circular_dependency() {
2023        let ctx = TestContext::new().await;
2024        let manager = TaskManager::new(ctx.pool());
2025
2026        let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
2027        let task2 = manager
2028            .add_task("Task 2", None, Some(task1.id), None)
2029            .await
2030            .unwrap();
2031
2032        // Try to make task1 a child of task2 (circular)
2033        let result = manager
2034            .update_task(task1.id, None, None, Some(Some(task2.id)), None, None, None)
2035            .await;
2036
2037        assert!(matches!(
2038            result,
2039            Err(IntentError::CircularDependency { .. })
2040        ));
2041    }
2042
2043    #[tokio::test]
2044    async fn test_invalid_parent_id() {
2045        let ctx = TestContext::new().await;
2046        let manager = TaskManager::new(ctx.pool());
2047
2048        let result = manager.add_task("Test", None, Some(999), None).await;
2049        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2050    }
2051
2052    #[tokio::test]
2053    async fn test_update_task_complexity_and_priority() {
2054        let ctx = TestContext::new().await;
2055        let manager = TaskManager::new(ctx.pool());
2056
2057        let task = manager
2058            .add_task("Test task", None, None, None)
2059            .await
2060            .unwrap();
2061        let updated = manager
2062            .update_task(task.id, None, None, None, None, Some(8), Some(10))
2063            .await
2064            .unwrap();
2065
2066        assert_eq!(updated.complexity, Some(8));
2067        assert_eq!(updated.priority, Some(10));
2068    }
2069
2070    #[tokio::test]
2071    async fn test_spawn_subtask() {
2072        let ctx = TestContext::new().await;
2073        let manager = TaskManager::new(ctx.pool());
2074
2075        // Create and start a parent task
2076        let parent = manager
2077            .add_task("Parent task", None, None, None)
2078            .await
2079            .unwrap();
2080        manager.start_task(parent.id, false).await.unwrap();
2081
2082        // Spawn a subtask
2083        let response = manager
2084            .spawn_subtask("Child task", Some("Details"))
2085            .await
2086            .unwrap();
2087
2088        assert_eq!(response.subtask.parent_id, parent.id);
2089        assert_eq!(response.subtask.name, "Child task");
2090        assert_eq!(response.subtask.status, "doing");
2091        assert_eq!(response.parent_task.id, parent.id);
2092        assert_eq!(response.parent_task.name, "Parent task");
2093
2094        // Verify subtask is now the current task
2095        let session_id = crate::workspace::resolve_session_id(None);
2096        let current: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
2097            "SELECT current_task_id FROM sessions WHERE session_id = ?",
2098        )
2099        .bind(&session_id)
2100        .fetch_optional(ctx.pool())
2101        .await
2102        .unwrap()
2103        .flatten();
2104
2105        assert_eq!(current, Some(response.subtask.id));
2106
2107        // Verify subtask is in doing status
2108        let retrieved = manager.get_task(response.subtask.id).await.unwrap();
2109        assert_eq!(retrieved.status, "doing");
2110    }
2111
2112    #[tokio::test]
2113    async fn test_spawn_subtask_no_current_task() {
2114        let ctx = TestContext::new().await;
2115        let manager = TaskManager::new(ctx.pool());
2116
2117        // Try to spawn subtask without a current task
2118        let result = manager.spawn_subtask("Child", None).await;
2119        assert!(result.is_err());
2120    }
2121
2122    #[tokio::test]
2123    async fn test_pick_next_tasks_basic() {
2124        let ctx = TestContext::new().await;
2125        let manager = TaskManager::new(ctx.pool());
2126
2127        // Create 10 todo tasks
2128        for i in 1..=10 {
2129            manager
2130                .add_task(&format!("Task {}", i), None, None, None)
2131                .await
2132                .unwrap();
2133        }
2134
2135        // Pick 5 tasks with capacity limit of 5
2136        let picked = manager.pick_next_tasks(5, 5).await.unwrap();
2137
2138        assert_eq!(picked.len(), 5);
2139        for task in &picked {
2140            assert_eq!(task.status, "doing");
2141            assert!(task.first_doing_at.is_some());
2142        }
2143
2144        // Verify total doing count
2145        let doing_count: i64 =
2146            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2147                .fetch_one(ctx.pool())
2148                .await
2149                .unwrap();
2150
2151        assert_eq!(doing_count, 5);
2152    }
2153
2154    #[tokio::test]
2155    async fn test_pick_next_tasks_with_existing_doing() {
2156        let ctx = TestContext::new().await;
2157        let manager = TaskManager::new(ctx.pool());
2158
2159        // Create 10 todo tasks
2160        for i in 1..=10 {
2161            manager
2162                .add_task(&format!("Task {}", i), None, None, None)
2163                .await
2164                .unwrap();
2165        }
2166
2167        // Start 2 tasks
2168        let result = manager
2169            .find_tasks(Some("todo"), None, None, None, None)
2170            .await
2171            .unwrap();
2172        manager.start_task(result.tasks[0].id, false).await.unwrap();
2173        manager.start_task(result.tasks[1].id, false).await.unwrap();
2174
2175        // Pick more tasks with capacity limit of 5
2176        let picked = manager.pick_next_tasks(10, 5).await.unwrap();
2177
2178        // Should only pick 3 more (5 - 2 = 3)
2179        assert_eq!(picked.len(), 3);
2180
2181        // Verify total doing count
2182        let doing_count: i64 =
2183            sqlx::query_scalar::<_, i64>(crate::sql_constants::COUNT_TASKS_DOING)
2184                .fetch_one(ctx.pool())
2185                .await
2186                .unwrap();
2187
2188        assert_eq!(doing_count, 5);
2189    }
2190
2191    #[tokio::test]
2192    async fn test_pick_next_tasks_at_capacity() {
2193        let ctx = TestContext::new().await;
2194        let manager = TaskManager::new(ctx.pool());
2195
2196        // Create 10 tasks
2197        for i in 1..=10 {
2198            manager
2199                .add_task(&format!("Task {}", i), None, None, None)
2200                .await
2201                .unwrap();
2202        }
2203
2204        // Fill capacity
2205        let first_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2206        assert_eq!(first_batch.len(), 5);
2207
2208        // Try to pick more (should return empty)
2209        let second_batch = manager.pick_next_tasks(5, 5).await.unwrap();
2210        assert_eq!(second_batch.len(), 0);
2211    }
2212
2213    #[tokio::test]
2214    async fn test_pick_next_tasks_priority_ordering() {
2215        let ctx = TestContext::new().await;
2216        let manager = TaskManager::new(ctx.pool());
2217
2218        // Create tasks with different priorities
2219        let low = manager
2220            .add_task("Low priority", None, None, None)
2221            .await
2222            .unwrap();
2223        manager
2224            .update_task(low.id, None, None, None, None, None, Some(1))
2225            .await
2226            .unwrap();
2227
2228        let high = manager
2229            .add_task("High priority", None, None, None)
2230            .await
2231            .unwrap();
2232        manager
2233            .update_task(high.id, None, None, None, None, None, Some(10))
2234            .await
2235            .unwrap();
2236
2237        let medium = manager
2238            .add_task("Medium priority", None, None, None)
2239            .await
2240            .unwrap();
2241        manager
2242            .update_task(medium.id, None, None, None, None, None, Some(5))
2243            .await
2244            .unwrap();
2245
2246        // Pick tasks
2247        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2248
2249        // Should be ordered by priority ASC (lower number = higher priority)
2250        assert_eq!(picked.len(), 3);
2251        assert_eq!(picked[0].priority, Some(1)); // lowest number = highest priority
2252        assert_eq!(picked[1].priority, Some(5)); // medium
2253        assert_eq!(picked[2].priority, Some(10)); // highest number = lowest priority
2254    }
2255
2256    #[tokio::test]
2257    async fn test_pick_next_tasks_complexity_ordering() {
2258        let ctx = TestContext::new().await;
2259        let manager = TaskManager::new(ctx.pool());
2260
2261        // Create tasks with different complexities (same priority)
2262        let complex = manager.add_task("Complex", None, None, None).await.unwrap();
2263        manager
2264            .update_task(complex.id, None, None, None, None, Some(9), Some(5))
2265            .await
2266            .unwrap();
2267
2268        let simple = manager.add_task("Simple", None, None, None).await.unwrap();
2269        manager
2270            .update_task(simple.id, None, None, None, None, Some(1), Some(5))
2271            .await
2272            .unwrap();
2273
2274        let medium = manager.add_task("Medium", None, None, None).await.unwrap();
2275        manager
2276            .update_task(medium.id, None, None, None, None, Some(5), Some(5))
2277            .await
2278            .unwrap();
2279
2280        // Pick tasks
2281        let picked = manager.pick_next_tasks(3, 5).await.unwrap();
2282
2283        // Should be ordered by complexity ASC (simple first)
2284        assert_eq!(picked.len(), 3);
2285        assert_eq!(picked[0].complexity, Some(1)); // simple
2286        assert_eq!(picked[1].complexity, Some(5)); // medium
2287        assert_eq!(picked[2].complexity, Some(9)); // complex
2288    }
2289
2290    #[tokio::test]
2291    async fn test_done_task_sibling_tasks_remain() {
2292        let ctx = TestContext::new().await;
2293        let manager = TaskManager::new(ctx.pool());
2294
2295        // Create parent with multiple children
2296        let parent = manager
2297            .add_task("Parent Task", None, None, None)
2298            .await
2299            .unwrap();
2300        let child1 = manager
2301            .add_task("Child 1", None, Some(parent.id), None)
2302            .await
2303            .unwrap();
2304        let child2 = manager
2305            .add_task("Child 2", None, Some(parent.id), None)
2306            .await
2307            .unwrap();
2308        let _child3 = manager
2309            .add_task("Child 3", None, Some(parent.id), None)
2310            .await
2311            .unwrap();
2312
2313        // Complete first child
2314        manager.start_task(child1.id, false).await.unwrap();
2315        let response = manager.done_task(false).await.unwrap();
2316
2317        // Should indicate siblings remain
2318        match response.next_step_suggestion {
2319            NextStepSuggestion::SiblingTasksRemain {
2320                parent_task_id,
2321                remaining_siblings_count,
2322                ..
2323            } => {
2324                assert_eq!(parent_task_id, parent.id);
2325                assert_eq!(remaining_siblings_count, 2); // child2 and child3
2326            },
2327            _ => panic!("Expected SiblingTasksRemain suggestion"),
2328        }
2329
2330        // Complete second child
2331        manager.start_task(child2.id, false).await.unwrap();
2332        let response2 = manager.done_task(false).await.unwrap();
2333
2334        // Should still indicate siblings remain
2335        match response2.next_step_suggestion {
2336            NextStepSuggestion::SiblingTasksRemain {
2337                remaining_siblings_count,
2338                ..
2339            } => {
2340                assert_eq!(remaining_siblings_count, 1); // only child3
2341            },
2342            _ => panic!("Expected SiblingTasksRemain suggestion"),
2343        }
2344    }
2345
2346    #[tokio::test]
2347    async fn test_done_task_top_level_with_children() {
2348        let ctx = TestContext::new().await;
2349        let manager = TaskManager::new(ctx.pool());
2350
2351        // Create top-level task with children
2352        let parent = manager
2353            .add_task("Epic Task", None, None, None)
2354            .await
2355            .unwrap();
2356        let child = manager
2357            .add_task("Sub Task", None, Some(parent.id), None)
2358            .await
2359            .unwrap();
2360
2361        // Complete child first
2362        manager.start_task(child.id, false).await.unwrap();
2363        manager.done_task(false).await.unwrap();
2364
2365        // Complete parent
2366        manager.start_task(parent.id, false).await.unwrap();
2367        let response = manager.done_task(false).await.unwrap();
2368
2369        // Should be TOP_LEVEL_TASK_COMPLETED
2370        match response.next_step_suggestion {
2371            NextStepSuggestion::TopLevelTaskCompleted {
2372                completed_task_id,
2373                completed_task_name,
2374                ..
2375            } => {
2376                assert_eq!(completed_task_id, parent.id);
2377                assert_eq!(completed_task_name, "Epic Task");
2378            },
2379            _ => panic!("Expected TopLevelTaskCompleted suggestion"),
2380        }
2381    }
2382
2383    #[tokio::test]
2384    async fn test_done_task_no_parent_context() {
2385        let ctx = TestContext::new().await;
2386        let manager = TaskManager::new(ctx.pool());
2387
2388        // Create multiple standalone tasks
2389        let task1 = manager
2390            .add_task("Standalone Task 1", None, None, None)
2391            .await
2392            .unwrap();
2393        let _task2 = manager
2394            .add_task("Standalone Task 2", None, None, None)
2395            .await
2396            .unwrap();
2397
2398        // Complete first task
2399        manager.start_task(task1.id, false).await.unwrap();
2400        let response = manager.done_task(false).await.unwrap();
2401
2402        // Should be NO_PARENT_CONTEXT since task2 is still pending
2403        match response.next_step_suggestion {
2404            NextStepSuggestion::NoParentContext {
2405                completed_task_id,
2406                completed_task_name,
2407                ..
2408            } => {
2409                assert_eq!(completed_task_id, task1.id);
2410                assert_eq!(completed_task_name, "Standalone Task 1");
2411            },
2412            _ => panic!("Expected NoParentContext suggestion"),
2413        }
2414    }
2415
2416    #[tokio::test]
2417    async fn test_pick_next_focused_subtask() {
2418        let ctx = TestContext::new().await;
2419        let manager = TaskManager::new(ctx.pool());
2420
2421        // Create parent task and set as current
2422        let parent = manager
2423            .add_task("Parent task", None, None, None)
2424            .await
2425            .unwrap();
2426        manager.start_task(parent.id, false).await.unwrap();
2427
2428        // Create subtasks with different priorities
2429        let subtask1 = manager
2430            .add_task("Subtask 1", None, Some(parent.id), None)
2431            .await
2432            .unwrap();
2433        let subtask2 = manager
2434            .add_task("Subtask 2", None, Some(parent.id), None)
2435            .await
2436            .unwrap();
2437
2438        // Set priorities: subtask1 = 2, subtask2 = 1 (lower number = higher priority)
2439        manager
2440            .update_task(subtask1.id, None, None, None, None, None, Some(2))
2441            .await
2442            .unwrap();
2443        manager
2444            .update_task(subtask2.id, None, None, None, None, None, Some(1))
2445            .await
2446            .unwrap();
2447
2448        // Pick next should recommend subtask2 (priority 1)
2449        let response = manager.pick_next().await.unwrap();
2450
2451        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2452        assert!(response.task.is_some());
2453        assert_eq!(response.task.as_ref().unwrap().id, subtask2.id);
2454        assert_eq!(response.task.as_ref().unwrap().name, "Subtask 2");
2455    }
2456
2457    #[tokio::test]
2458    async fn test_pick_next_top_level_task() {
2459        let ctx = TestContext::new().await;
2460        let manager = TaskManager::new(ctx.pool());
2461
2462        // Create top-level tasks with different priorities
2463        let task1 = manager.add_task("Task 1", None, None, None).await.unwrap();
2464        let task2 = manager.add_task("Task 2", None, None, None).await.unwrap();
2465
2466        // Set priorities: task1 = 5, task2 = 3 (lower number = higher priority)
2467        manager
2468            .update_task(task1.id, None, None, None, None, None, Some(5))
2469            .await
2470            .unwrap();
2471        manager
2472            .update_task(task2.id, None, None, None, None, None, Some(3))
2473            .await
2474            .unwrap();
2475
2476        // Pick next should recommend task2 (priority 3)
2477        let response = manager.pick_next().await.unwrap();
2478
2479        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2480        assert!(response.task.is_some());
2481        assert_eq!(response.task.as_ref().unwrap().id, task2.id);
2482        assert_eq!(response.task.as_ref().unwrap().name, "Task 2");
2483    }
2484
2485    #[tokio::test]
2486    async fn test_pick_next_no_tasks() {
2487        let ctx = TestContext::new().await;
2488        let manager = TaskManager::new(ctx.pool());
2489
2490        // No tasks created
2491        let response = manager.pick_next().await.unwrap();
2492
2493        assert_eq!(response.suggestion_type, "NONE");
2494        assert_eq!(response.reason_code.as_deref(), Some("NO_TASKS_IN_PROJECT"));
2495        assert!(response.message.is_some());
2496    }
2497
2498    #[tokio::test]
2499    async fn test_pick_next_all_completed() {
2500        let ctx = TestContext::new().await;
2501        let manager = TaskManager::new(ctx.pool());
2502
2503        // Create task and mark as done
2504        let task = manager.add_task("Task 1", None, None, None).await.unwrap();
2505        manager.start_task(task.id, false).await.unwrap();
2506        manager.done_task(false).await.unwrap();
2507
2508        // Pick next should indicate all tasks completed
2509        let response = manager.pick_next().await.unwrap();
2510
2511        assert_eq!(response.suggestion_type, "NONE");
2512        assert_eq!(response.reason_code.as_deref(), Some("ALL_TASKS_COMPLETED"));
2513        assert!(response.message.is_some());
2514    }
2515
2516    #[tokio::test]
2517    async fn test_pick_next_no_available_todos() {
2518        let ctx = TestContext::new().await;
2519        let manager = TaskManager::new(ctx.pool());
2520
2521        // Create a parent task that's in "doing" status
2522        let parent = manager
2523            .add_task("Parent task", None, None, None)
2524            .await
2525            .unwrap();
2526        manager.start_task(parent.id, false).await.unwrap();
2527
2528        // Create a subtask also in "doing" status (no "todo" subtasks)
2529        let subtask = manager
2530            .add_task("Subtask", None, Some(parent.id), None)
2531            .await
2532            .unwrap();
2533        // Switch to subtask (this will set parent back to todo, so we need to manually set subtask to doing)
2534        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2535            .bind(subtask.id)
2536            .execute(ctx.pool())
2537            .await
2538            .unwrap();
2539
2540        // Set subtask as current
2541        let session_id = crate::workspace::resolve_session_id(None);
2542        sqlx::query(
2543            r#"
2544            INSERT INTO sessions (session_id, current_task_id, created_at, last_active_at)
2545            VALUES (?, ?, datetime('now'), datetime('now'))
2546            ON CONFLICT(session_id) DO UPDATE SET
2547                current_task_id = excluded.current_task_id,
2548                last_active_at = datetime('now')
2549            "#,
2550        )
2551        .bind(&session_id)
2552        .bind(subtask.id)
2553        .execute(ctx.pool())
2554        .await
2555        .unwrap();
2556
2557        // Set parent to doing (not todo)
2558        sqlx::query("UPDATE tasks SET status = 'doing' WHERE id = ?")
2559            .bind(parent.id)
2560            .execute(ctx.pool())
2561            .await
2562            .unwrap();
2563
2564        // With multi-doing semantics, pick next should recommend the doing parent
2565        // (it's a valid top-level doing task that's not current)
2566        let response = manager.pick_next().await.unwrap();
2567
2568        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2569        assert_eq!(response.task.as_ref().unwrap().id, parent.id);
2570        assert_eq!(response.task.as_ref().unwrap().status, "doing");
2571    }
2572
2573    #[tokio::test]
2574    async fn test_pick_next_priority_ordering() {
2575        let ctx = TestContext::new().await;
2576        let manager = TaskManager::new(ctx.pool());
2577
2578        // Create parent and set as current
2579        let parent = manager.add_task("Parent", None, None, None).await.unwrap();
2580        manager.start_task(parent.id, false).await.unwrap();
2581
2582        // Create multiple subtasks with various priorities
2583        let sub1 = manager
2584            .add_task("Priority 10", None, Some(parent.id), None)
2585            .await
2586            .unwrap();
2587        manager
2588            .update_task(sub1.id, None, None, None, None, None, Some(10))
2589            .await
2590            .unwrap();
2591
2592        let sub2 = manager
2593            .add_task("Priority 1", None, Some(parent.id), None)
2594            .await
2595            .unwrap();
2596        manager
2597            .update_task(sub2.id, None, None, None, None, None, Some(1))
2598            .await
2599            .unwrap();
2600
2601        let sub3 = manager
2602            .add_task("Priority 5", None, Some(parent.id), None)
2603            .await
2604            .unwrap();
2605        manager
2606            .update_task(sub3.id, None, None, None, None, None, Some(5))
2607            .await
2608            .unwrap();
2609
2610        // Pick next should recommend the task with priority 1 (lowest number)
2611        let response = manager.pick_next().await.unwrap();
2612
2613        assert_eq!(response.suggestion_type, "FOCUSED_SUB_TASK");
2614        assert_eq!(response.task.as_ref().unwrap().id, sub2.id);
2615        assert_eq!(response.task.as_ref().unwrap().name, "Priority 1");
2616    }
2617
2618    #[tokio::test]
2619    async fn test_pick_next_falls_back_to_top_level_when_no_subtasks() {
2620        let ctx = TestContext::new().await;
2621        let manager = TaskManager::new(ctx.pool());
2622
2623        // Create parent without subtasks and set as current
2624        let parent = manager.add_task("Parent", None, None, None).await.unwrap();
2625        manager.start_task(parent.id, false).await.unwrap();
2626
2627        // Create another top-level task
2628        let top_level = manager
2629            .add_task("Top level task", None, None, None)
2630            .await
2631            .unwrap();
2632
2633        // Pick next should fall back to top-level task since parent has no todo subtasks
2634        let response = manager.pick_next().await.unwrap();
2635
2636        assert_eq!(response.suggestion_type, "TOP_LEVEL_TASK");
2637        assert_eq!(response.task.as_ref().unwrap().id, top_level.id);
2638    }
2639
2640    // ===== Missing coverage tests =====
2641
2642    #[tokio::test]
2643    async fn test_get_task_with_events() {
2644        let ctx = TestContext::new().await;
2645        let task_mgr = TaskManager::new(ctx.pool());
2646        let event_mgr = EventManager::new(ctx.pool());
2647
2648        let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2649
2650        // Add some events
2651        event_mgr
2652            .add_event(task.id, "progress", "Event 1")
2653            .await
2654            .unwrap();
2655        event_mgr
2656            .add_event(task.id, "decision", "Event 2")
2657            .await
2658            .unwrap();
2659
2660        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2661
2662        assert_eq!(result.task.id, task.id);
2663        assert!(result.events_summary.is_some());
2664
2665        let summary = result.events_summary.unwrap();
2666        assert_eq!(summary.total_count, 2);
2667        assert_eq!(summary.recent_events.len(), 2);
2668        assert_eq!(summary.recent_events[0].log_type, "decision"); // Most recent first
2669        assert_eq!(summary.recent_events[1].log_type, "progress");
2670    }
2671
2672    #[tokio::test]
2673    async fn test_get_task_with_events_nonexistent() {
2674        let ctx = TestContext::new().await;
2675        let task_mgr = TaskManager::new(ctx.pool());
2676
2677        let result = task_mgr.get_task_with_events(999).await;
2678        assert!(matches!(result, Err(IntentError::TaskNotFound(999))));
2679    }
2680
2681    #[tokio::test]
2682    async fn test_get_task_with_many_events() {
2683        let ctx = TestContext::new().await;
2684        let task_mgr = TaskManager::new(ctx.pool());
2685        let event_mgr = EventManager::new(ctx.pool());
2686
2687        let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2688
2689        // Add 20 events
2690        for i in 0..20 {
2691            event_mgr
2692                .add_event(task.id, "test", &format!("Event {}", i))
2693                .await
2694                .unwrap();
2695        }
2696
2697        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2698        let summary = result.events_summary.unwrap();
2699
2700        assert_eq!(summary.total_count, 20);
2701        assert_eq!(summary.recent_events.len(), 10); // Limited to 10
2702    }
2703
2704    #[tokio::test]
2705    async fn test_get_task_with_no_events() {
2706        let ctx = TestContext::new().await;
2707        let task_mgr = TaskManager::new(ctx.pool());
2708
2709        let task = task_mgr.add_task("Test", None, None, None).await.unwrap();
2710
2711        let result = task_mgr.get_task_with_events(task.id).await.unwrap();
2712        let summary = result.events_summary.unwrap();
2713
2714        assert_eq!(summary.total_count, 0);
2715        assert_eq!(summary.recent_events.len(), 0);
2716    }
2717
2718    #[tokio::test]
2719    async fn test_pick_next_tasks_zero_capacity() {
2720        let ctx = TestContext::new().await;
2721        let task_mgr = TaskManager::new(ctx.pool());
2722
2723        task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2724
2725        // capacity_limit = 0 means no capacity available
2726        let results = task_mgr.pick_next_tasks(10, 0).await.unwrap();
2727        assert_eq!(results.len(), 0);
2728    }
2729
2730    #[tokio::test]
2731    async fn test_pick_next_tasks_capacity_exceeds_available() {
2732        let ctx = TestContext::new().await;
2733        let task_mgr = TaskManager::new(ctx.pool());
2734
2735        task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2736        task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2737
2738        // Request 10 tasks but only 2 available, capacity = 100
2739        let results = task_mgr.pick_next_tasks(10, 100).await.unwrap();
2740        assert_eq!(results.len(), 2); // Only returns available tasks
2741    }
2742
2743    // ========== task_context tests ==========
2744
2745    #[tokio::test]
2746    async fn test_get_task_context_root_task_no_relations() {
2747        let ctx = TestContext::new().await;
2748        let task_mgr = TaskManager::new(ctx.pool());
2749
2750        // Create a single root task with no relations
2751        let task = task_mgr
2752            .add_task("Root task", None, None, None)
2753            .await
2754            .unwrap();
2755
2756        let context = task_mgr.get_task_context(task.id).await.unwrap();
2757
2758        // Verify task itself
2759        assert_eq!(context.task.id, task.id);
2760        assert_eq!(context.task.name, "Root task");
2761
2762        // No ancestors (root task)
2763        assert_eq!(context.ancestors.len(), 0);
2764
2765        // No siblings
2766        assert_eq!(context.siblings.len(), 0);
2767
2768        // No children
2769        assert_eq!(context.children.len(), 0);
2770    }
2771
2772    #[tokio::test]
2773    async fn test_get_task_context_with_siblings() {
2774        let ctx = TestContext::new().await;
2775        let task_mgr = TaskManager::new(ctx.pool());
2776
2777        // Create multiple root tasks (siblings)
2778        let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
2779        let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
2780        let task3 = task_mgr.add_task("Task 3", None, None, None).await.unwrap();
2781
2782        let context = task_mgr.get_task_context(task2.id).await.unwrap();
2783
2784        // Verify task itself
2785        assert_eq!(context.task.id, task2.id);
2786
2787        // No ancestors (root task)
2788        assert_eq!(context.ancestors.len(), 0);
2789
2790        // Should have 2 siblings
2791        assert_eq!(context.siblings.len(), 2);
2792        let sibling_ids: Vec<i64> = context.siblings.iter().map(|t| t.id).collect();
2793        assert!(sibling_ids.contains(&task1.id));
2794        assert!(sibling_ids.contains(&task3.id));
2795        assert!(!sibling_ids.contains(&task2.id)); // Should not include itself
2796
2797        // No children
2798        assert_eq!(context.children.len(), 0);
2799    }
2800
2801    #[tokio::test]
2802    async fn test_get_task_context_with_parent() {
2803        let ctx = TestContext::new().await;
2804        let task_mgr = TaskManager::new(ctx.pool());
2805
2806        // Create parent-child relationship
2807        let parent = task_mgr
2808            .add_task("Parent task", None, None, None)
2809            .await
2810            .unwrap();
2811        let child = task_mgr
2812            .add_task("Child task", None, Some(parent.id), None)
2813            .await
2814            .unwrap();
2815
2816        let context = task_mgr.get_task_context(child.id).await.unwrap();
2817
2818        // Verify task itself
2819        assert_eq!(context.task.id, child.id);
2820        assert_eq!(context.task.parent_id, Some(parent.id));
2821
2822        // Should have 1 ancestor (the parent)
2823        assert_eq!(context.ancestors.len(), 1);
2824        assert_eq!(context.ancestors[0].id, parent.id);
2825        assert_eq!(context.ancestors[0].name, "Parent task");
2826
2827        // No siblings
2828        assert_eq!(context.siblings.len(), 0);
2829
2830        // No children
2831        assert_eq!(context.children.len(), 0);
2832    }
2833
2834    #[tokio::test]
2835    async fn test_get_task_context_with_children() {
2836        let ctx = TestContext::new().await;
2837        let task_mgr = TaskManager::new(ctx.pool());
2838
2839        // Create parent with multiple children
2840        let parent = task_mgr
2841            .add_task("Parent task", None, None, None)
2842            .await
2843            .unwrap();
2844        let child1 = task_mgr
2845            .add_task("Child 1", None, Some(parent.id), None)
2846            .await
2847            .unwrap();
2848        let child2 = task_mgr
2849            .add_task("Child 2", None, Some(parent.id), None)
2850            .await
2851            .unwrap();
2852        let child3 = task_mgr
2853            .add_task("Child 3", None, Some(parent.id), None)
2854            .await
2855            .unwrap();
2856
2857        let context = task_mgr.get_task_context(parent.id).await.unwrap();
2858
2859        // Verify task itself
2860        assert_eq!(context.task.id, parent.id);
2861
2862        // No ancestors (root task)
2863        assert_eq!(context.ancestors.len(), 0);
2864
2865        // No siblings
2866        assert_eq!(context.siblings.len(), 0);
2867
2868        // Should have 3 children
2869        assert_eq!(context.children.len(), 3);
2870        let child_ids: Vec<i64> = context.children.iter().map(|t| t.id).collect();
2871        assert!(child_ids.contains(&child1.id));
2872        assert!(child_ids.contains(&child2.id));
2873        assert!(child_ids.contains(&child3.id));
2874    }
2875
2876    #[tokio::test]
2877    async fn test_get_task_context_multi_level_hierarchy() {
2878        let ctx = TestContext::new().await;
2879        let task_mgr = TaskManager::new(ctx.pool());
2880
2881        // Create 3-level hierarchy: grandparent -> parent -> child
2882        let grandparent = task_mgr
2883            .add_task("Grandparent", None, None, None)
2884            .await
2885            .unwrap();
2886        let parent = task_mgr
2887            .add_task("Parent", None, Some(grandparent.id), None)
2888            .await
2889            .unwrap();
2890        let child = task_mgr
2891            .add_task("Child", None, Some(parent.id), None)
2892            .await
2893            .unwrap();
2894
2895        let context = task_mgr.get_task_context(child.id).await.unwrap();
2896
2897        // Verify task itself
2898        assert_eq!(context.task.id, child.id);
2899
2900        // Should have 2 ancestors (parent and grandparent, ordered from immediate to root)
2901        assert_eq!(context.ancestors.len(), 2);
2902        assert_eq!(context.ancestors[0].id, parent.id);
2903        assert_eq!(context.ancestors[0].name, "Parent");
2904        assert_eq!(context.ancestors[1].id, grandparent.id);
2905        assert_eq!(context.ancestors[1].name, "Grandparent");
2906
2907        // No siblings
2908        assert_eq!(context.siblings.len(), 0);
2909
2910        // No children
2911        assert_eq!(context.children.len(), 0);
2912    }
2913
2914    #[tokio::test]
2915    async fn test_get_task_context_complex_family_tree() {
2916        let ctx = TestContext::new().await;
2917        let task_mgr = TaskManager::new(ctx.pool());
2918
2919        // Create complex structure:
2920        // Root
2921        //  ├─ Child1
2922        //  │   ├─ Grandchild1
2923        //  │   └─ Grandchild2 (target)
2924        //  └─ Child2
2925
2926        let root = task_mgr.add_task("Root", None, None, None).await.unwrap();
2927        let child1 = task_mgr
2928            .add_task("Child1", None, Some(root.id), None)
2929            .await
2930            .unwrap();
2931        let child2 = task_mgr
2932            .add_task("Child2", None, Some(root.id), None)
2933            .await
2934            .unwrap();
2935        let grandchild1 = task_mgr
2936            .add_task("Grandchild1", None, Some(child1.id), None)
2937            .await
2938            .unwrap();
2939        let grandchild2 = task_mgr
2940            .add_task("Grandchild2", None, Some(child1.id), None)
2941            .await
2942            .unwrap();
2943
2944        // Get context for grandchild2
2945        let context = task_mgr.get_task_context(grandchild2.id).await.unwrap();
2946
2947        // Verify task itself
2948        assert_eq!(context.task.id, grandchild2.id);
2949
2950        // Should have 2 ancestors: child1 and root
2951        assert_eq!(context.ancestors.len(), 2);
2952        assert_eq!(context.ancestors[0].id, child1.id);
2953        assert_eq!(context.ancestors[1].id, root.id);
2954
2955        // Should have 1 sibling: grandchild1
2956        assert_eq!(context.siblings.len(), 1);
2957        assert_eq!(context.siblings[0].id, grandchild1.id);
2958
2959        // No children
2960        assert_eq!(context.children.len(), 0);
2961
2962        // Now get context for child1 to verify it sees both grandchildren
2963        let context_child1 = task_mgr.get_task_context(child1.id).await.unwrap();
2964        assert_eq!(context_child1.ancestors.len(), 1);
2965        assert_eq!(context_child1.ancestors[0].id, root.id);
2966        assert_eq!(context_child1.siblings.len(), 1);
2967        assert_eq!(context_child1.siblings[0].id, child2.id);
2968        assert_eq!(context_child1.children.len(), 2);
2969    }
2970
2971    #[tokio::test]
2972    async fn test_get_task_context_respects_priority_ordering() {
2973        let ctx = TestContext::new().await;
2974        let task_mgr = TaskManager::new(ctx.pool());
2975
2976        // Create parent with children having different priorities
2977        let parent = task_mgr.add_task("Parent", None, None, None).await.unwrap();
2978
2979        // Add children with priorities (lower number = higher priority)
2980        let child_low = task_mgr
2981            .add_task("Low priority", None, Some(parent.id), None)
2982            .await
2983            .unwrap();
2984        let _ = task_mgr
2985            .update_task(child_low.id, None, None, None, None, None, Some(10))
2986            .await
2987            .unwrap();
2988
2989        let child_high = task_mgr
2990            .add_task("High priority", None, Some(parent.id), None)
2991            .await
2992            .unwrap();
2993        let _ = task_mgr
2994            .update_task(child_high.id, None, None, None, None, None, Some(1))
2995            .await
2996            .unwrap();
2997
2998        let child_medium = task_mgr
2999            .add_task("Medium priority", None, Some(parent.id), None)
3000            .await
3001            .unwrap();
3002        let _ = task_mgr
3003            .update_task(child_medium.id, None, None, None, None, None, Some(5))
3004            .await
3005            .unwrap();
3006
3007        let context = task_mgr.get_task_context(parent.id).await.unwrap();
3008
3009        // Children should be ordered by priority (1, 5, 10)
3010        assert_eq!(context.children.len(), 3);
3011        assert_eq!(context.children[0].priority, Some(1));
3012        assert_eq!(context.children[1].priority, Some(5));
3013        assert_eq!(context.children[2].priority, Some(10));
3014    }
3015
3016    #[tokio::test]
3017    async fn test_get_task_context_nonexistent_task() {
3018        let ctx = TestContext::new().await;
3019        let task_mgr = TaskManager::new(ctx.pool());
3020
3021        let result = task_mgr.get_task_context(99999).await;
3022        assert!(result.is_err());
3023        assert!(matches!(result, Err(IntentError::TaskNotFound(99999))));
3024    }
3025
3026    #[tokio::test]
3027    async fn test_get_task_context_handles_null_priority() {
3028        let ctx = TestContext::new().await;
3029        let task_mgr = TaskManager::new(ctx.pool());
3030
3031        // Create siblings with mixed null and set priorities
3032        let task1 = task_mgr.add_task("Task 1", None, None, None).await.unwrap();
3033        let _ = task_mgr
3034            .update_task(task1.id, None, None, None, None, None, Some(1))
3035            .await
3036            .unwrap();
3037
3038        let task2 = task_mgr.add_task("Task 2", None, None, None).await.unwrap();
3039        // task2 has NULL priority
3040
3041        let task3 = task_mgr.add_task("Task 3", None, None, None).await.unwrap();
3042        let _ = task_mgr
3043            .update_task(task3.id, None, None, None, None, None, Some(5))
3044            .await
3045            .unwrap();
3046
3047        let context = task_mgr.get_task_context(task2.id).await.unwrap();
3048
3049        // Should have 2 siblings, ordered by priority (non-null first, then null)
3050        assert_eq!(context.siblings.len(), 2);
3051        // Task with priority 1 should come first
3052        assert_eq!(context.siblings[0].id, task1.id);
3053        assert_eq!(context.siblings[0].priority, Some(1));
3054        // Task with priority 5 should come second
3055        assert_eq!(context.siblings[1].id, task3.id);
3056        assert_eq!(context.siblings[1].priority, Some(5));
3057    }
3058
3059    #[tokio::test]
3060    async fn test_pick_next_tasks_priority_order() {
3061        let ctx = TestContext::new().await;
3062        let task_mgr = TaskManager::new(ctx.pool());
3063
3064        // Create 4 tasks with different priorities
3065        let critical = task_mgr
3066            .add_task("Critical Task", None, None, None)
3067            .await
3068            .unwrap();
3069        task_mgr
3070            .update_task(critical.id, None, None, None, None, None, Some(1))
3071            .await
3072            .unwrap();
3073
3074        let low = task_mgr
3075            .add_task("Low Task", None, None, None)
3076            .await
3077            .unwrap();
3078        task_mgr
3079            .update_task(low.id, None, None, None, None, None, Some(4))
3080            .await
3081            .unwrap();
3082
3083        let high = task_mgr
3084            .add_task("High Task", None, None, None)
3085            .await
3086            .unwrap();
3087        task_mgr
3088            .update_task(high.id, None, None, None, None, None, Some(2))
3089            .await
3090            .unwrap();
3091
3092        let medium = task_mgr
3093            .add_task("Medium Task", None, None, None)
3094            .await
3095            .unwrap();
3096        task_mgr
3097            .update_task(medium.id, None, None, None, None, None, Some(3))
3098            .await
3099            .unwrap();
3100
3101        // Pick next tasks should return them in priority order: critical > high > medium > low
3102        let tasks = task_mgr.pick_next_tasks(10, 10).await.unwrap();
3103
3104        assert_eq!(tasks.len(), 4);
3105        assert_eq!(tasks[0].id, critical.id); // Priority 1
3106        assert_eq!(tasks[1].id, high.id); // Priority 2
3107        assert_eq!(tasks[2].id, medium.id); // Priority 3
3108        assert_eq!(tasks[3].id, low.id); // Priority 4
3109    }
3110
3111    #[tokio::test]
3112    async fn test_pick_next_prefers_doing_over_todo() {
3113        let ctx = TestContext::new().await;
3114        let task_mgr = TaskManager::new(ctx.pool());
3115        let workspace_mgr = WorkspaceManager::new(ctx.pool());
3116
3117        // Create a parent task and set it as current
3118        let parent = task_mgr.add_task("Parent", None, None, None).await.unwrap();
3119        let parent_started = task_mgr.start_task(parent.id, false).await.unwrap();
3120        workspace_mgr
3121            .set_current_task(parent_started.task.id, None)
3122            .await
3123            .unwrap();
3124
3125        // Create two subtasks with same priority: one doing, one todo
3126        let doing_subtask = task_mgr
3127            .add_task("Doing Subtask", None, Some(parent.id), None)
3128            .await
3129            .unwrap();
3130        task_mgr.start_task(doing_subtask.id, false).await.unwrap();
3131        // Switch back to parent so doing_subtask is "pending" (doing but not current)
3132        workspace_mgr
3133            .set_current_task(parent.id, None)
3134            .await
3135            .unwrap();
3136
3137        let _todo_subtask = task_mgr
3138            .add_task("Todo Subtask", None, Some(parent.id), None)
3139            .await
3140            .unwrap();
3141
3142        // Both have same priority (default), but doing should be picked first
3143        let result = task_mgr.pick_next().await.unwrap();
3144
3145        if let Some(task) = result.task {
3146            assert_eq!(
3147                task.id, doing_subtask.id,
3148                "Should recommend doing subtask over todo subtask"
3149            );
3150            assert_eq!(task.status, "doing");
3151        } else {
3152            panic!("Expected a task recommendation");
3153        }
3154    }
3155
3156    #[tokio::test]
3157    async fn test_multiple_doing_tasks_allowed() {
3158        let ctx = TestContext::new().await;
3159        let task_mgr = TaskManager::new(ctx.pool());
3160        let workspace_mgr = WorkspaceManager::new(ctx.pool());
3161
3162        // Create and start task A
3163        let task_a = task_mgr.add_task("Task A", None, None, None).await.unwrap();
3164        let task_a_started = task_mgr.start_task(task_a.id, false).await.unwrap();
3165        assert_eq!(task_a_started.task.status, "doing");
3166
3167        // Verify task A is current
3168        let current = workspace_mgr.get_current_task(None).await.unwrap();
3169        assert_eq!(current.current_task_id, Some(task_a.id));
3170
3171        // Create and start task B
3172        let task_b = task_mgr.add_task("Task B", None, None, None).await.unwrap();
3173        let task_b_started = task_mgr.start_task(task_b.id, false).await.unwrap();
3174        assert_eq!(task_b_started.task.status, "doing");
3175
3176        // Verify task B is now current
3177        let current = workspace_mgr.get_current_task(None).await.unwrap();
3178        assert_eq!(current.current_task_id, Some(task_b.id));
3179
3180        // Verify task A is still doing (not reverted to todo)
3181        let task_a_after = task_mgr.get_task(task_a.id).await.unwrap();
3182        assert_eq!(
3183            task_a_after.status, "doing",
3184            "Task A should remain doing even though it is not current"
3185        );
3186
3187        // Verify both tasks are in doing status
3188        let doing_tasks: Vec<Task> = sqlx::query_as(
3189            r#"SELECT id, parent_id, name, spec, status, complexity, priority, first_todo_at, first_doing_at, first_done_at, active_form, owner
3190             FROM tasks WHERE status = 'doing' ORDER BY id"#
3191        )
3192        .fetch_all(ctx.pool())
3193        .await
3194        .unwrap();
3195
3196        assert_eq!(doing_tasks.len(), 2, "Should have 2 doing tasks");
3197        assert_eq!(doing_tasks[0].id, task_a.id);
3198        assert_eq!(doing_tasks[1].id, task_b.id);
3199    }
3200    #[tokio::test]
3201    async fn test_find_tasks_pagination() {
3202        let ctx = TestContext::new().await;
3203        let task_mgr = TaskManager::new(ctx.pool());
3204
3205        // Create 15 tasks
3206        for i in 0..15 {
3207            task_mgr
3208                .add_task(&format!("Task {}", i), None, None, None)
3209                .await
3210                .unwrap();
3211        }
3212
3213        // Page 1: Limit 10, Offset 0
3214        let page1 = task_mgr
3215            .find_tasks(None, None, None, Some(10), Some(0))
3216            .await
3217            .unwrap();
3218        assert_eq!(page1.tasks.len(), 10);
3219        assert_eq!(page1.total_count, 15);
3220        assert!(page1.has_more);
3221        assert_eq!(page1.offset, 0);
3222
3223        // Page 2: Limit 10, Offset 10
3224        let page2 = task_mgr
3225            .find_tasks(None, None, None, Some(10), Some(10))
3226            .await
3227            .unwrap();
3228        assert_eq!(page2.tasks.len(), 5);
3229        assert_eq!(page2.total_count, 15);
3230        assert!(!page2.has_more);
3231        assert_eq!(page2.offset, 10);
3232    }
3233}
3234
3235// Re-export TaskContext for cli_handlers