UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

263 lines (262 loc) 7.96 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../core/monitoring/logger.js"; import * as fs from "fs"; import * as path from "path"; const PROMPT_PLAN_PATH = "docs/specs/PROMPT_PLAN.md"; class LinearTaskRunner { constructor(taskManager, rlmOrchestrator, context, specSkill) { this.taskManager = taskManager; this.rlmOrchestrator = rlmOrchestrator; this.context = context; this.specSkill = specSkill; } /** Pull next task from Linear, execute via RLM, update status */ async runNext(opts) { const tasks = this.getFilteredTasks(opts); if (tasks.length === 0) { return { success: true, message: "No pending tasks found", data: { tasksAvailable: 0 } }; } const task = tasks[0]; return this.runTask(task.id, opts); } /** Run all active tasks iteratively */ async runAll(opts) { const startTime = Date.now(); const tasks = this.getFilteredTasks(opts); if (tasks.length === 0) { return { success: true, message: "No pending tasks to execute", data: { tasksAvailable: 0 } }; } if (opts?.dryRun) { return this.preview(); } const summary = { completed: [], failed: [], skipped: [], totalTokens: 0, totalCost: 0, duration: 0 }; for (const task of tasks) { try { const result = await this.executeTask(task); if (result.success) { summary.completed.push(task.id); const data = result.data; summary.totalTokens += data?.totalTokens || 0; summary.totalCost += data?.totalCost || 0; await this.autoUpdatePromptPlan(task); } else { summary.failed.push({ taskId: task.id, error: result.message }); } await this.syncSafe(); } catch (error) { const msg = error instanceof Error ? error.message : String(error); summary.failed.push({ taskId: task.id, error: msg }); logger.error("Task execution failed", { taskId: task.id, error: msg }); } } summary.duration = Date.now() - startTime; return { success: summary.failed.length === 0, message: `Completed ${summary.completed.length}/${tasks.length} tasks`, data: summary, action: `Executed ${summary.completed.length} tasks, ${summary.failed.length} failures` }; } /** Execute a specific Linear task by ID */ async runTask(taskId, opts) { const task = this.taskManager.getTask(taskId); if (!task) { return { success: false, message: `Task not found: ${taskId}` }; } if (opts?.dryRun) { return this.previewTask(task); } const result = await this.executeTask(task); if (result.success) { await this.autoUpdatePromptPlan(task); await this.syncSafe(); } return result; } /** Show execution plan without running */ async preview(taskId) { if (taskId) { const task = this.taskManager.getTask(taskId); if (!task) { return { success: false, message: `Task not found: ${taskId}` }; } return this.previewTask(task); } const tasks = this.getFilteredTasks(); const plan = tasks.map((t, i) => ({ order: i + 1, id: t.id, identifier: t.externalIdentifier || t.id, title: t.title, priority: t.priority || "medium", status: t.status, tags: t.tags })); return { success: true, message: `${plan.length} tasks in execution queue`, data: { plan, totalTasks: plan.length } }; } // --- Private helpers --- async executeTask(task) { const taskLabel = task.externalIdentifier || task.id; logger.info("Starting task execution", { taskId: task.id, title: task.title }); try { this.taskManager.updateTaskStatus( task.id, "in_progress", "LinearTaskRunner: starting execution" ); } catch { } try { const result = await this.rlmOrchestrator.execute( task.description || task.title, { linearTaskId: task.id, linearIdentifier: task.externalIdentifier, title: task.title, tags: task.tags } ); if (result.success) { this.taskManager.updateTaskStatus( task.id, "done", `Completed via RLM: ${result.improvements.length} improvements, ${result.testsGenerated} tests` ); return { success: true, message: `${taskLabel}: completed`, data: { taskId: task.id, duration: result.duration, totalTokens: result.totalTokens, totalCost: result.totalCost, testsGenerated: result.testsGenerated, improvements: result.improvements.length, issuesFound: result.issuesFound, issuesFixed: result.issuesFixed }, action: `Executed ${taskLabel} via RLM` }; } else { logger.warn("Task execution failed", { taskId: task.id, rootNode: result.rootNode }); return { success: false, message: `${taskLabel}: execution failed`, data: { taskId: task.id, duration: result.duration, totalTokens: result.totalTokens } }; } } catch (error) { const msg = error instanceof Error ? error.message : String(error); logger.error("Task execution threw", { taskId: task.id, error: msg }); return { success: false, message: `${taskLabel}: ${msg}`, data: { taskId: task.id } }; } } getFilteredTasks(opts) { const tasks = this.taskManager.getTasksByStatus("todo"); let filtered = tasks; if (opts?.priority) { filtered = filtered.filter((t) => t.priority === opts.priority); } if (opts?.tag) { const tag = opts.tag; filtered = filtered.filter((t) => t.tags.includes(tag)); } const priorityOrder = { urgent: 0, high: 1, medium: 2, low: 3 }; return filtered.sort( (a, b) => (priorityOrder[a.priority || "medium"] || 2) - (priorityOrder[b.priority || "medium"] || 2) ); } previewTask(task) { return { success: true, message: `Preview: ${task.externalIdentifier || task.id}`, data: { id: task.id, identifier: task.externalIdentifier, title: task.title, description: task.description?.slice(0, 200), priority: task.priority, status: task.status, tags: task.tags, willExecuteVia: "RLM Orchestrator", estimatedSteps: [ "Planning agent decomposes task", "Code/Test/Review subagents execute", "Multi-stage review", "Update Linear status to done" ] } }; } /** Auto-update PROMPT_PLAN checkboxes when a task completes */ async autoUpdatePromptPlan(task) { if (!this.specSkill) return; const promptPlanPath = path.join(process.cwd(), PROMPT_PLAN_PATH); if (!fs.existsSync(promptPlanPath)) return; try { await this.specSkill.update(PROMPT_PLAN_PATH, task.title); logger.info("Auto-updated PROMPT_PLAN checkbox", { taskId: task.id, title: task.title }); } catch { } } /** Safe Linear sync — log errors but don't throw */ async syncSafe() { try { await this.taskManager.syncWithLinear(); } catch (error) { const msg = error instanceof Error ? error.message : String(error); logger.warn("Linear sync failed (non-fatal)", { error: msg }); } } } export { LinearTaskRunner }; //# sourceMappingURL=linear-task-runner.js.map