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