Skip to main content

intent_engine/
tasks.rs

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