diff --git a/pi-worker/src/scheduler/task-loop.ts b/pi-worker/src/scheduler/task-loop.ts index 775bcae..d616820 100644 --- a/pi-worker/src/scheduler/task-loop.ts +++ b/pi-worker/src/scheduler/task-loop.ts @@ -89,17 +89,13 @@ export class TaskLoop { } try { - logger.info("task-loop", "Polling for Asana tasks..."); + // First: sweep all sections for tasks that are already done + await this.sweepDoneTasks(); - // Search TODO sections in priority order - let tasks: AsanaTask[] = []; - for (const sectionName of SECTIONS.TODO) { - tasks = await this.asana.getIncompleteTasks(this.config.asanaProjectGid, sectionName); - if (tasks.length > 0) { - logger.info("task-loop", `Found ${tasks.length} task(s) in "${sectionName}"`); - break; - } - } + // Then: pick up new tasks to execute + logger.info("task-loop", "Polling for new Asana tasks..."); + + let tasks = await this.getTasksFromSections(this.config.asanaProjectGid, SECTIONS.TODO); if (tasks.length === 0) { logger.info("task-loop", "No pending tasks found"); @@ -260,6 +256,69 @@ EVIDENCE: one line why`; } } + private async sweepDoneTasks() { + try { + // Get all incomplete tasks from all active sections (not just TODO) + const sweepSections = [...SECTIONS.TODO, ...SECTIONS.IN_PROGRESS]; + const allTasks: AsanaTask[] = []; + + for (const sectionName of sweepSections) { + const section = await this.asana.findSection(this.config.asanaProjectGid, sectionName); + if (!section) continue; + const tasks = await this.asana.getSectionTasks(section.gid); + const incomplete = tasks.filter((t) => !t.completed); + allTasks.push(...incomplete); + } + + if (allTasks.length === 0) return; + + // Deduplicate by GID + const seen = new Set(); + const uniqueTasks = allTasks.filter((t) => { + if (seen.has(t.gid)) return false; + seen.add(t.gid); + return true; + }); + + logger.info("task-loop", `Sweeping ${uniqueTasks.length} incomplete tasks for already-done work`); + + for (const task of uniqueTasks) { + const check = await this.verifyTaskAlreadyDone(task); + + if (check.done) { + logger.info("task-loop", `Sweep: "${task.name}" is already done`, { evidence: check.evidence }); + + await this.asana.addComment( + task.gid, + `✅ Pi Worker sweep: This task is **already completed** on the server.\n\nEvidence:\n${check.evidence}` + ); + + const doneSection = await this.findFirstSection(SECTIONS.DONE); + if (doneSection) { + await this.asana.moveTaskToSection(task.gid, doneSection.gid); + } + await this.asana.updateTask(task.gid, { completed: true }); + logger.info("task-loop", `Sweep: Marked done: ${task.name}`); + } else { + logger.debug("task-loop", `Sweep: "${task.name}" not yet done`, { evidence: check.evidence }); + } + } + } catch (error: any) { + logger.error("task-loop", `Sweep error: ${error.message}`); + } + } + + private async getTasksFromSections(projectGid: string, sectionNames: string[]): Promise { + for (const name of sectionNames) { + const section = await this.asana.findSection(projectGid, name); + if (!section) continue; + const tasks = await this.asana.getSectionTasks(section.gid); + const incomplete = tasks.filter((t) => !t.completed); + if (incomplete.length > 0) return incomplete; + } + return []; + } + private buildTaskContext(task: AsanaTask): string { const parts = [ `Asana Task: ${task.name}`,