@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
JavaScript
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