@stackmemoryai/stackmemory 0.5.22 → 0.5.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/claude-sm.js +2 -0
- package/dist/cli/claude-sm.js.map +2 -2
- package/dist/cli/commands/discovery.js +279 -0
- package/dist/cli/commands/discovery.js.map +7 -0
- package/dist/cli/commands/retrieval.js +248 -0
- package/dist/cli/commands/retrieval.js.map +7 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +2 -2
- package/dist/core/retrieval/llm-context-retrieval.js +33 -1
- package/dist/core/retrieval/llm-context-retrieval.js.map +2 -2
- package/dist/core/retrieval/llm-provider.js +128 -0
- package/dist/core/retrieval/llm-provider.js.map +7 -0
- package/dist/core/retrieval/retrieval-audit.js +236 -0
- package/dist/core/retrieval/retrieval-audit.js.map +7 -0
- package/dist/integrations/linear/client.js +125 -0
- package/dist/integrations/linear/client.js.map +2 -2
- package/dist/integrations/mcp/handlers/discovery-handlers.js +497 -0
- package/dist/integrations/mcp/handlers/discovery-handlers.js.map +7 -0
- package/dist/integrations/mcp/handlers/index.js +40 -12
- package/dist/integrations/mcp/handlers/index.js.map +2 -2
- package/dist/integrations/mcp/server.js +270 -0
- package/dist/integrations/mcp/server.js.map +2 -2
- package/dist/integrations/mcp/tool-definitions.js +141 -5
- package/dist/integrations/mcp/tool-definitions.js.map +2 -2
- package/package.json +1 -1
- package/dist/cli/commands/agent.js +0 -286
- package/dist/cli/commands/agent.js.map +0 -7
- package/dist/cli/commands/chromadb.js +0 -482
- package/dist/cli/commands/chromadb.js.map +0 -7
- package/dist/cli/commands/gc.js +0 -251
- package/dist/cli/commands/gc.js.map +0 -7
- package/dist/cli/commands/infinite-storage.js +0 -292
- package/dist/cli/commands/infinite-storage.js.map +0 -7
- package/dist/cli/commands/linear-create.js +0 -171
- package/dist/cli/commands/linear-create.js.map +0 -7
- package/dist/cli/commands/linear-list.js +0 -103
- package/dist/cli/commands/linear-list.js.map +0 -7
- package/dist/cli/commands/linear-migrate.js +0 -64
- package/dist/cli/commands/linear-migrate.js.map +0 -7
- package/dist/cli/commands/linear-test.js +0 -134
- package/dist/cli/commands/linear-test.js.map +0 -7
- package/dist/cli/commands/tui.js +0 -77
- package/dist/cli/commands/tui.js.map +0 -7
- package/dist/cli/commands/webhook.js +0 -181
- package/dist/cli/commands/webhook.js.map +0 -7
- package/dist/cli/streamlined-cli.js +0 -144
- package/dist/cli/streamlined-cli.js.map +0 -7
- package/dist/core/events/event-bus.js +0 -110
- package/dist/core/events/event-bus.js.map +0 -7
- package/dist/core/frame/workflow-templates-stub.js +0 -42
- package/dist/core/frame/workflow-templates-stub.js.map +0 -7
- package/dist/core/plugins/plugin-interface.js +0 -87
- package/dist/core/plugins/plugin-interface.js.map +0 -7
- package/dist/core/session/clear-survival-stub.js +0 -53
- package/dist/core/session/clear-survival-stub.js.map +0 -7
- package/dist/core/storage/chromadb-simple.js +0 -172
- package/dist/core/storage/chromadb-simple.js.map +0 -7
- package/dist/core/storage/simplified-storage.js +0 -328
- package/dist/core/storage/simplified-storage.js.map +0 -7
- package/dist/features/tasks/pebbles-task-store.js +0 -647
- package/dist/features/tasks/pebbles-task-store.js.map +0 -7
- package/dist/integrations/linear/sync-enhanced.js +0 -202
- package/dist/integrations/linear/sync-enhanced.js.map +0 -7
- package/dist/plugins/linear/index.js +0 -166
- package/dist/plugins/linear/index.js.map +0 -7
- package/dist/plugins/loader.js +0 -57
- package/dist/plugins/loader.js.map +0 -7
- package/dist/plugins/plugin-interface.js +0 -67
- package/dist/plugins/plugin-interface.js.map +0 -7
- package/dist/plugins/ralph/simple-ralph-plugin.js +0 -305
- package/dist/plugins/ralph/simple-ralph-plugin.js.map +0 -7
- package/dist/plugins/ralph/use-cases/code-generator.js +0 -151
- package/dist/plugins/ralph/use-cases/code-generator.js.map +0 -7
- package/dist/plugins/ralph/use-cases/test-generator.js +0 -201
- package/dist/plugins/ralph/use-cases/test-generator.js.map +0 -7
- package/dist/utils/logger.js +0 -52
- package/dist/utils/logger.js.map +0 -7
|
@@ -1,647 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "events";
|
|
2
|
-
import { createHash } from "crypto";
|
|
3
|
-
import { appendFile, existsSync, mkdirSync, readFileSync } from "fs";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import { logger } from "../../core/monitoring/logger.js";
|
|
6
|
-
import {
|
|
7
|
-
DatabaseError,
|
|
8
|
-
TaskError,
|
|
9
|
-
SystemError,
|
|
10
|
-
ErrorCode,
|
|
11
|
-
createErrorHandler
|
|
12
|
-
} from "../../core/errors/index.js";
|
|
13
|
-
import { retry } from "../../core/errors/recovery.js";
|
|
14
|
-
import { StreamingJSONLParser } from "../../core/performance/streaming-jsonl-parser.js";
|
|
15
|
-
import { ContextCache } from "../../core/performance/context-cache.js";
|
|
16
|
-
class PebblesTaskStore extends EventEmitter {
|
|
17
|
-
db;
|
|
18
|
-
projectRoot;
|
|
19
|
-
tasksFile;
|
|
20
|
-
cacheFile;
|
|
21
|
-
jsonlParser;
|
|
22
|
-
taskCache;
|
|
23
|
-
constructor(projectRoot, db) {
|
|
24
|
-
super();
|
|
25
|
-
this.projectRoot = projectRoot;
|
|
26
|
-
this.db = db;
|
|
27
|
-
const stackmemoryDir = join(projectRoot, ".stackmemory");
|
|
28
|
-
if (!existsSync(stackmemoryDir)) {
|
|
29
|
-
mkdirSync(stackmemoryDir, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
this.tasksFile = join(stackmemoryDir, "tasks.jsonl");
|
|
32
|
-
this.cacheFile = join(stackmemoryDir, "cache.db");
|
|
33
|
-
this.jsonlParser = new StreamingJSONLParser();
|
|
34
|
-
this.taskCache = new ContextCache({
|
|
35
|
-
maxSize: 10 * 1024 * 1024,
|
|
36
|
-
// 10MB for tasks
|
|
37
|
-
maxItems: 1e3,
|
|
38
|
-
defaultTTL: 36e5
|
|
39
|
-
// 1 hour
|
|
40
|
-
});
|
|
41
|
-
this.initializeCache();
|
|
42
|
-
this.loadFromJSONLSync();
|
|
43
|
-
}
|
|
44
|
-
initializeCache() {
|
|
45
|
-
const errorHandler = createErrorHandler({
|
|
46
|
-
operation: "initializeCache",
|
|
47
|
-
projectRoot: this.projectRoot
|
|
48
|
-
});
|
|
49
|
-
try {
|
|
50
|
-
this.db.exec(`
|
|
51
|
-
CREATE TABLE IF NOT EXISTS task_cache (
|
|
52
|
-
id TEXT PRIMARY KEY,
|
|
53
|
-
type TEXT NOT NULL,
|
|
54
|
-
timestamp INTEGER NOT NULL,
|
|
55
|
-
parent_id TEXT,
|
|
56
|
-
frame_id TEXT NOT NULL,
|
|
57
|
-
title TEXT NOT NULL,
|
|
58
|
-
description TEXT,
|
|
59
|
-
status TEXT NOT NULL,
|
|
60
|
-
priority TEXT NOT NULL,
|
|
61
|
-
assignee TEXT,
|
|
62
|
-
created_at INTEGER NOT NULL,
|
|
63
|
-
started_at INTEGER,
|
|
64
|
-
completed_at INTEGER,
|
|
65
|
-
estimated_effort INTEGER,
|
|
66
|
-
actual_effort INTEGER,
|
|
67
|
-
depends_on TEXT DEFAULT '[]',
|
|
68
|
-
blocks TEXT DEFAULT '[]',
|
|
69
|
-
tags TEXT DEFAULT '[]',
|
|
70
|
-
external_refs TEXT DEFAULT '{}',
|
|
71
|
-
context_score REAL DEFAULT 0.5,
|
|
72
|
-
last_accessed INTEGER
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
CREATE INDEX IF NOT EXISTS idx_task_status ON task_cache(status);
|
|
76
|
-
CREATE INDEX IF NOT EXISTS idx_task_priority ON task_cache(priority);
|
|
77
|
-
CREATE INDEX IF NOT EXISTS idx_task_frame ON task_cache(frame_id);
|
|
78
|
-
CREATE INDEX IF NOT EXISTS idx_task_timestamp ON task_cache(timestamp);
|
|
79
|
-
CREATE INDEX IF NOT EXISTS idx_task_parent ON task_cache(parent_id);
|
|
80
|
-
`);
|
|
81
|
-
} catch (error) {
|
|
82
|
-
const dbError = errorHandler(error, {
|
|
83
|
-
operation: "initializeCache",
|
|
84
|
-
schema: "task_cache"
|
|
85
|
-
});
|
|
86
|
-
throw new DatabaseError(
|
|
87
|
-
"Failed to initialize task cache schema",
|
|
88
|
-
ErrorCode.DB_MIGRATION_FAILED,
|
|
89
|
-
{
|
|
90
|
-
projectRoot: this.projectRoot,
|
|
91
|
-
cacheFile: this.cacheFile,
|
|
92
|
-
operation: "initializeCache"
|
|
93
|
-
},
|
|
94
|
-
error instanceof Error ? error : void 0
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Load existing tasks from JSONL into SQLite cache (optimized)
|
|
100
|
-
*/
|
|
101
|
-
async loadFromJSONL() {
|
|
102
|
-
if (!existsSync(this.tasksFile)) return;
|
|
103
|
-
const errorHandler = createErrorHandler({
|
|
104
|
-
operation: "loadFromJSONL",
|
|
105
|
-
tasksFile: this.tasksFile
|
|
106
|
-
});
|
|
107
|
-
try {
|
|
108
|
-
let loaded = 0;
|
|
109
|
-
let errors = 0;
|
|
110
|
-
for await (const batch of this.jsonlParser.parseStream(
|
|
111
|
-
this.tasksFile,
|
|
112
|
-
{
|
|
113
|
-
batchSize: 100,
|
|
114
|
-
filter: (obj) => obj.type && obj.id && obj.title,
|
|
115
|
-
// Basic validation
|
|
116
|
-
onProgress: (count) => {
|
|
117
|
-
if (count % 500 === 0) {
|
|
118
|
-
logger.debug("Loading tasks progress", { loaded: count });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
)) {
|
|
123
|
-
for (const task of batch) {
|
|
124
|
-
try {
|
|
125
|
-
this.upsertToCache(task);
|
|
126
|
-
this.taskCache.set(task.id, task, {
|
|
127
|
-
ttl: 36e5
|
|
128
|
-
// 1 hour cache
|
|
129
|
-
});
|
|
130
|
-
loaded++;
|
|
131
|
-
} catch (error) {
|
|
132
|
-
errors++;
|
|
133
|
-
logger.warn("Failed to cache task", {
|
|
134
|
-
taskId: task.id,
|
|
135
|
-
error: error instanceof Error ? error.message : String(error)
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
logger.info("Loaded tasks from JSONL", {
|
|
141
|
-
loaded,
|
|
142
|
-
errors,
|
|
143
|
-
file: this.tasksFile,
|
|
144
|
-
cacheStats: this.taskCache.getStats()
|
|
145
|
-
});
|
|
146
|
-
} catch (error) {
|
|
147
|
-
const systemError = errorHandler(error, {
|
|
148
|
-
operation: "loadFromJSONL",
|
|
149
|
-
file: this.tasksFile
|
|
150
|
-
});
|
|
151
|
-
throw new SystemError(
|
|
152
|
-
"Failed to load tasks from JSONL file",
|
|
153
|
-
ErrorCode.INTERNAL_ERROR,
|
|
154
|
-
{
|
|
155
|
-
tasksFile: this.tasksFile,
|
|
156
|
-
operation: "loadFromJSONL"
|
|
157
|
-
},
|
|
158
|
-
error instanceof Error ? error : void 0
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Load existing tasks from JSONL synchronously (for constructor)
|
|
164
|
-
*/
|
|
165
|
-
loadFromJSONLSync() {
|
|
166
|
-
if (!existsSync(this.tasksFile)) return;
|
|
167
|
-
try {
|
|
168
|
-
const content = readFileSync(this.tasksFile, "utf-8");
|
|
169
|
-
const lines = content.split("\n").filter((line) => line.trim());
|
|
170
|
-
let loaded = 0;
|
|
171
|
-
let errors = 0;
|
|
172
|
-
for (const line of lines) {
|
|
173
|
-
try {
|
|
174
|
-
const task = JSON.parse(line);
|
|
175
|
-
if (task.type && task.id && task.title) {
|
|
176
|
-
this.upsertToCache(task);
|
|
177
|
-
this.taskCache.set(task.id, task, {
|
|
178
|
-
ttl: 36e5
|
|
179
|
-
// 1 hour cache
|
|
180
|
-
});
|
|
181
|
-
loaded++;
|
|
182
|
-
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
errors++;
|
|
185
|
-
logger.warn("Failed to parse JSONL line", {
|
|
186
|
-
error: error instanceof Error ? error.message : String(error)
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
logger.info("Loaded tasks from JSONL", {
|
|
191
|
-
loaded,
|
|
192
|
-
errors,
|
|
193
|
-
file: this.tasksFile
|
|
194
|
-
});
|
|
195
|
-
} catch (error) {
|
|
196
|
-
logger.error("Failed to load tasks from JSONL", error);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Create a new task with content-hash ID
|
|
201
|
-
*/
|
|
202
|
-
createTask(options) {
|
|
203
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
204
|
-
const content = `${options.title}:${options.frameId}:${now}:${Math.random()}`;
|
|
205
|
-
const id = this.generateTaskId(content);
|
|
206
|
-
const task = {
|
|
207
|
-
id,
|
|
208
|
-
type: "task_create",
|
|
209
|
-
timestamp: now,
|
|
210
|
-
parent_id: options.parentId,
|
|
211
|
-
frame_id: options.frameId,
|
|
212
|
-
title: options.title,
|
|
213
|
-
description: options.description,
|
|
214
|
-
status: "pending",
|
|
215
|
-
priority: options.priority || "medium",
|
|
216
|
-
assignee: options.assignee,
|
|
217
|
-
created_at: now,
|
|
218
|
-
estimated_effort: options.estimatedEffort,
|
|
219
|
-
depends_on: options.dependsOn || [],
|
|
220
|
-
blocks: [],
|
|
221
|
-
tags: options.tags || [],
|
|
222
|
-
context_score: 0.5
|
|
223
|
-
};
|
|
224
|
-
this.appendTask(task);
|
|
225
|
-
this.emit("task:created", task);
|
|
226
|
-
this.emit("sync:needed", "task:created");
|
|
227
|
-
return id;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Update task status with new event
|
|
231
|
-
*/
|
|
232
|
-
updateTaskStatus(taskId, newStatus, _reason) {
|
|
233
|
-
const existing = this.getTask(taskId);
|
|
234
|
-
if (!existing) {
|
|
235
|
-
throw new TaskError(
|
|
236
|
-
`Task not found: ${taskId}`,
|
|
237
|
-
ErrorCode.TASK_NOT_FOUND,
|
|
238
|
-
{
|
|
239
|
-
taskId,
|
|
240
|
-
newStatus,
|
|
241
|
-
operation: "updateTaskStatus"
|
|
242
|
-
}
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
if (existing.status === "completed" && newStatus !== "cancelled") {
|
|
246
|
-
throw new TaskError(
|
|
247
|
-
`Cannot change completed task status from ${existing.status} to ${newStatus}`,
|
|
248
|
-
ErrorCode.TASK_INVALID_STATE,
|
|
249
|
-
{
|
|
250
|
-
taskId,
|
|
251
|
-
currentStatus: existing.status,
|
|
252
|
-
newStatus,
|
|
253
|
-
operation: "updateTaskStatus"
|
|
254
|
-
}
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
258
|
-
const updates = {
|
|
259
|
-
status: newStatus,
|
|
260
|
-
timestamp: now
|
|
261
|
-
};
|
|
262
|
-
if (newStatus === "in_progress" && existing.status === "pending") {
|
|
263
|
-
updates.started_at = now;
|
|
264
|
-
updates.type = "task_update";
|
|
265
|
-
} else if (newStatus === "completed" && existing.status === "in_progress") {
|
|
266
|
-
updates.completed_at = now;
|
|
267
|
-
updates.type = "task_complete";
|
|
268
|
-
if (existing.started_at) {
|
|
269
|
-
updates.actual_effort = Math.floor((now - existing.started_at) / 60);
|
|
270
|
-
}
|
|
271
|
-
} else if (newStatus === "blocked") {
|
|
272
|
-
updates.type = "task_block";
|
|
273
|
-
}
|
|
274
|
-
const updatedTask = { ...existing, ...updates };
|
|
275
|
-
this.appendTask(updatedTask);
|
|
276
|
-
if (newStatus === "completed") {
|
|
277
|
-
this.emit("task:completed", updatedTask);
|
|
278
|
-
}
|
|
279
|
-
this.emit("sync:needed", "task:updated");
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Add dependency relationship
|
|
283
|
-
*/
|
|
284
|
-
addDependency(taskId, dependsOnId) {
|
|
285
|
-
const task = this.getTask(taskId);
|
|
286
|
-
const dependsOnTask = this.getTask(dependsOnId);
|
|
287
|
-
if (!task) {
|
|
288
|
-
throw new TaskError(
|
|
289
|
-
`Task not found: ${taskId}`,
|
|
290
|
-
ErrorCode.TASK_NOT_FOUND,
|
|
291
|
-
{
|
|
292
|
-
taskId,
|
|
293
|
-
operation: "addDependency"
|
|
294
|
-
}
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
if (!dependsOnTask) {
|
|
298
|
-
throw new TaskError(
|
|
299
|
-
`Dependency task not found: ${dependsOnId}`,
|
|
300
|
-
ErrorCode.TASK_NOT_FOUND,
|
|
301
|
-
{
|
|
302
|
-
dependsOnId,
|
|
303
|
-
taskId,
|
|
304
|
-
operation: "addDependency"
|
|
305
|
-
}
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
if (this.wouldCreateCircularDependency(taskId, dependsOnId)) {
|
|
309
|
-
throw new TaskError(
|
|
310
|
-
`Adding dependency would create circular dependency: ${taskId} -> ${dependsOnId}`,
|
|
311
|
-
ErrorCode.TASK_CIRCULAR_DEPENDENCY,
|
|
312
|
-
{
|
|
313
|
-
taskId,
|
|
314
|
-
dependsOnId,
|
|
315
|
-
operation: "addDependency"
|
|
316
|
-
}
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
const updatedTask = {
|
|
320
|
-
...task,
|
|
321
|
-
depends_on: [.../* @__PURE__ */ new Set([...task.depends_on, dependsOnId])],
|
|
322
|
-
timestamp: Math.floor(Date.now() / 1e3),
|
|
323
|
-
type: "task_update"
|
|
324
|
-
};
|
|
325
|
-
const updatedBlockingTask = {
|
|
326
|
-
...dependsOnTask,
|
|
327
|
-
blocks: [.../* @__PURE__ */ new Set([...dependsOnTask.blocks, taskId])],
|
|
328
|
-
timestamp: Math.floor(Date.now() / 1e3),
|
|
329
|
-
type: "task_update"
|
|
330
|
-
};
|
|
331
|
-
this.appendTask(updatedTask);
|
|
332
|
-
this.appendTask(updatedBlockingTask);
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Get current active tasks
|
|
336
|
-
*/
|
|
337
|
-
getActiveTasks(frameId) {
|
|
338
|
-
try {
|
|
339
|
-
let query = `
|
|
340
|
-
SELECT * FROM task_cache
|
|
341
|
-
WHERE status IN ('pending', 'in_progress')
|
|
342
|
-
`;
|
|
343
|
-
const params = [];
|
|
344
|
-
if (frameId) {
|
|
345
|
-
query += ` AND frame_id = ?`;
|
|
346
|
-
params.push(frameId);
|
|
347
|
-
}
|
|
348
|
-
query += ` ORDER BY priority DESC, created_at ASC`;
|
|
349
|
-
const rows = this.db.prepare(query).all(...params);
|
|
350
|
-
return this.hydrateTasks(rows);
|
|
351
|
-
} catch (error) {
|
|
352
|
-
throw new DatabaseError(
|
|
353
|
-
"Failed to get active tasks",
|
|
354
|
-
ErrorCode.DB_QUERY_FAILED,
|
|
355
|
-
{
|
|
356
|
-
frameId,
|
|
357
|
-
operation: "getActiveTasks"
|
|
358
|
-
},
|
|
359
|
-
error instanceof Error ? error : void 0
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Get task by ID (latest version)
|
|
365
|
-
*/
|
|
366
|
-
getTask(taskId) {
|
|
367
|
-
try {
|
|
368
|
-
const row = this.db.prepare(
|
|
369
|
-
`
|
|
370
|
-
SELECT * FROM task_cache WHERE id = ?
|
|
371
|
-
`
|
|
372
|
-
).get(taskId);
|
|
373
|
-
return row ? this.hydrateTask(row) : void 0;
|
|
374
|
-
} catch (error) {
|
|
375
|
-
throw new DatabaseError(
|
|
376
|
-
`Failed to get task: ${taskId}`,
|
|
377
|
-
ErrorCode.DB_QUERY_FAILED,
|
|
378
|
-
{
|
|
379
|
-
taskId,
|
|
380
|
-
operation: "getTask"
|
|
381
|
-
},
|
|
382
|
-
error instanceof Error ? error : void 0
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Get tasks that are blocking other tasks
|
|
388
|
-
*/
|
|
389
|
-
getBlockingTasks() {
|
|
390
|
-
try {
|
|
391
|
-
const rows = this.db.prepare(
|
|
392
|
-
`
|
|
393
|
-
SELECT * FROM task_cache
|
|
394
|
-
WHERE JSON_ARRAY_LENGTH(blocks) > 0
|
|
395
|
-
AND status NOT IN ('completed', 'cancelled')
|
|
396
|
-
ORDER BY priority DESC
|
|
397
|
-
`
|
|
398
|
-
).all();
|
|
399
|
-
return this.hydrateTasks(rows);
|
|
400
|
-
} catch (error) {
|
|
401
|
-
throw new DatabaseError(
|
|
402
|
-
"Failed to get blocking tasks",
|
|
403
|
-
ErrorCode.DB_QUERY_FAILED,
|
|
404
|
-
{
|
|
405
|
-
operation: "getBlockingTasks"
|
|
406
|
-
},
|
|
407
|
-
error instanceof Error ? error : void 0
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Get metrics for current project
|
|
413
|
-
*/
|
|
414
|
-
getMetrics() {
|
|
415
|
-
try {
|
|
416
|
-
const statusCounts = this.db.prepare(
|
|
417
|
-
`
|
|
418
|
-
SELECT status, COUNT(*) as count
|
|
419
|
-
FROM task_cache
|
|
420
|
-
GROUP BY status
|
|
421
|
-
`
|
|
422
|
-
).all();
|
|
423
|
-
const priorityCounts = this.db.prepare(
|
|
424
|
-
`
|
|
425
|
-
SELECT priority, COUNT(*) as count
|
|
426
|
-
FROM task_cache
|
|
427
|
-
GROUP BY priority
|
|
428
|
-
`
|
|
429
|
-
).all();
|
|
430
|
-
const totalTasks = statusCounts.reduce((sum, s) => sum + s.count, 0);
|
|
431
|
-
const completedTasks = statusCounts.find((s) => s.status === "completed")?.count || 0;
|
|
432
|
-
const blockedTasks = statusCounts.find((s) => s.status === "blocked")?.count || 0;
|
|
433
|
-
const effortRows = this.db.prepare(
|
|
434
|
-
`
|
|
435
|
-
SELECT estimated_effort, actual_effort
|
|
436
|
-
FROM task_cache
|
|
437
|
-
WHERE estimated_effort IS NOT NULL
|
|
438
|
-
AND actual_effort IS NOT NULL
|
|
439
|
-
`
|
|
440
|
-
).all();
|
|
441
|
-
let avgEffortAccuracy = 0;
|
|
442
|
-
if (effortRows.length > 0) {
|
|
443
|
-
const accuracies = effortRows.map(
|
|
444
|
-
(r) => 1 - Math.abs(r.estimated_effort - r.actual_effort) / Math.max(r.estimated_effort, 1)
|
|
445
|
-
);
|
|
446
|
-
avgEffortAccuracy = accuracies.reduce((sum, acc) => sum + acc, 0) / accuracies.length;
|
|
447
|
-
}
|
|
448
|
-
return {
|
|
449
|
-
total_tasks: totalTasks,
|
|
450
|
-
by_status: Object.fromEntries(
|
|
451
|
-
statusCounts.map((s) => [s.status, s.count])
|
|
452
|
-
),
|
|
453
|
-
by_priority: Object.fromEntries(
|
|
454
|
-
priorityCounts.map((p) => [p.priority, p.count])
|
|
455
|
-
),
|
|
456
|
-
completion_rate: totalTasks > 0 ? completedTasks / totalTasks : 0,
|
|
457
|
-
avg_effort_accuracy: avgEffortAccuracy,
|
|
458
|
-
blocked_tasks: blockedTasks,
|
|
459
|
-
overdue_tasks: 0
|
|
460
|
-
// TODO: implement due dates
|
|
461
|
-
};
|
|
462
|
-
} catch (error) {
|
|
463
|
-
throw new DatabaseError(
|
|
464
|
-
"Failed to get task metrics",
|
|
465
|
-
ErrorCode.DB_QUERY_FAILED,
|
|
466
|
-
{
|
|
467
|
-
operation: "getMetrics"
|
|
468
|
-
},
|
|
469
|
-
error instanceof Error ? error : void 0
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* Export tasks for Linear integration (Phase 2)
|
|
475
|
-
*/
|
|
476
|
-
exportForLinear() {
|
|
477
|
-
const tasks = this.db.prepare(
|
|
478
|
-
`
|
|
479
|
-
SELECT * FROM task_cache
|
|
480
|
-
WHERE external_refs IS NULL OR JSON_EXTRACT(external_refs, '$.linear') IS NULL
|
|
481
|
-
ORDER BY created_at ASC
|
|
482
|
-
`
|
|
483
|
-
).all();
|
|
484
|
-
return tasks.map((task) => ({
|
|
485
|
-
title: task.title,
|
|
486
|
-
description: task.description,
|
|
487
|
-
priority: this.mapPriorityToLinear(task.priority),
|
|
488
|
-
status: this.mapStatusToLinear(task.status),
|
|
489
|
-
estimate: task.estimated_effort,
|
|
490
|
-
dependencies: JSON.parse(task.depends_on || "[]")
|
|
491
|
-
}));
|
|
492
|
-
}
|
|
493
|
-
// Private methods
|
|
494
|
-
appendTask(task) {
|
|
495
|
-
try {
|
|
496
|
-
const jsonLine = JSON.stringify(task) + "\n";
|
|
497
|
-
appendFile(this.tasksFile, jsonLine, (err) => {
|
|
498
|
-
if (err) {
|
|
499
|
-
logger.error(
|
|
500
|
-
`Failed to append task ${task.id} to JSONL: ${err.message}`,
|
|
501
|
-
err,
|
|
502
|
-
{
|
|
503
|
-
taskId: task.id,
|
|
504
|
-
tasksFile: this.tasksFile
|
|
505
|
-
}
|
|
506
|
-
);
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
retry(() => Promise.resolve(this.upsertToCache(task)), {
|
|
510
|
-
maxAttempts: 3,
|
|
511
|
-
initialDelay: 100,
|
|
512
|
-
onRetry: (attempt, error) => {
|
|
513
|
-
logger.warn(`Retrying task cache upsert (attempt ${attempt})`, {
|
|
514
|
-
taskId: task.id,
|
|
515
|
-
errorMessage: error instanceof Error ? error.message : String(error)
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
}).catch((error) => {
|
|
519
|
-
logger.error(
|
|
520
|
-
"Failed to upsert task to cache after retries",
|
|
521
|
-
error instanceof Error ? error : new Error(String(error)),
|
|
522
|
-
{
|
|
523
|
-
taskId: task.id
|
|
524
|
-
}
|
|
525
|
-
);
|
|
526
|
-
throw error;
|
|
527
|
-
});
|
|
528
|
-
logger.info("Appended task", {
|
|
529
|
-
id: task.id,
|
|
530
|
-
type: task.type,
|
|
531
|
-
title: task.title,
|
|
532
|
-
status: task.status
|
|
533
|
-
});
|
|
534
|
-
} catch (error) {
|
|
535
|
-
throw new SystemError(
|
|
536
|
-
`Failed to append task: ${task.id}`,
|
|
537
|
-
ErrorCode.INTERNAL_ERROR,
|
|
538
|
-
{
|
|
539
|
-
taskId: task.id,
|
|
540
|
-
operation: "appendTask"
|
|
541
|
-
},
|
|
542
|
-
error instanceof Error ? error : void 0
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
upsertToCache(task) {
|
|
547
|
-
try {
|
|
548
|
-
this.db.prepare(
|
|
549
|
-
`
|
|
550
|
-
INSERT OR REPLACE INTO task_cache (
|
|
551
|
-
id, type, timestamp, parent_id, frame_id, title, description,
|
|
552
|
-
status, priority, assignee, created_at, started_at, completed_at,
|
|
553
|
-
estimated_effort, actual_effort, depends_on, blocks, tags,
|
|
554
|
-
external_refs, context_score, last_accessed
|
|
555
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
556
|
-
`
|
|
557
|
-
).run(
|
|
558
|
-
task.id,
|
|
559
|
-
task.type,
|
|
560
|
-
task.timestamp,
|
|
561
|
-
task.parent_id,
|
|
562
|
-
task.frame_id,
|
|
563
|
-
task.title,
|
|
564
|
-
task.description,
|
|
565
|
-
task.status,
|
|
566
|
-
task.priority,
|
|
567
|
-
task.assignee,
|
|
568
|
-
task.created_at,
|
|
569
|
-
task.started_at,
|
|
570
|
-
task.completed_at,
|
|
571
|
-
task.estimated_effort,
|
|
572
|
-
task.actual_effort,
|
|
573
|
-
JSON.stringify(task.depends_on),
|
|
574
|
-
JSON.stringify(task.blocks),
|
|
575
|
-
JSON.stringify(task.tags),
|
|
576
|
-
JSON.stringify(task.external_refs || {}),
|
|
577
|
-
task.context_score,
|
|
578
|
-
task.last_accessed
|
|
579
|
-
);
|
|
580
|
-
} catch (error) {
|
|
581
|
-
throw new DatabaseError(
|
|
582
|
-
`Failed to upsert task to cache: ${task.id}`,
|
|
583
|
-
ErrorCode.DB_QUERY_FAILED,
|
|
584
|
-
{
|
|
585
|
-
taskId: task.id,
|
|
586
|
-
taskTitle: task.title,
|
|
587
|
-
operation: "upsertToCache"
|
|
588
|
-
},
|
|
589
|
-
error instanceof Error ? error : void 0
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
generateTaskId(content) {
|
|
594
|
-
const hash = createHash("sha256").update(content).digest("hex");
|
|
595
|
-
return `tsk-${hash.substring(0, 8)}`;
|
|
596
|
-
}
|
|
597
|
-
hydrateTask = (row) => ({
|
|
598
|
-
...row,
|
|
599
|
-
depends_on: JSON.parse(row.depends_on || "[]"),
|
|
600
|
-
blocks: JSON.parse(row.blocks || "[]"),
|
|
601
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
602
|
-
external_refs: JSON.parse(row.external_refs || "{}")
|
|
603
|
-
});
|
|
604
|
-
hydrateTasks(rows) {
|
|
605
|
-
return rows.map(this.hydrateTask);
|
|
606
|
-
}
|
|
607
|
-
mapPriorityToLinear(priority) {
|
|
608
|
-
const map = { low: 1, medium: 2, high: 3, urgent: 4 };
|
|
609
|
-
return map[priority] || 2;
|
|
610
|
-
}
|
|
611
|
-
mapStatusToLinear(status) {
|
|
612
|
-
const map = {
|
|
613
|
-
pending: "Backlog",
|
|
614
|
-
in_progress: "In Progress",
|
|
615
|
-
completed: "Done",
|
|
616
|
-
blocked: "Blocked",
|
|
617
|
-
cancelled: "Cancelled"
|
|
618
|
-
};
|
|
619
|
-
return map[status] || "Backlog";
|
|
620
|
-
}
|
|
621
|
-
/**
|
|
622
|
-
* Check if adding a dependency would create a circular dependency
|
|
623
|
-
*/
|
|
624
|
-
wouldCreateCircularDependency(taskId, dependsOnId) {
|
|
625
|
-
const visited = /* @__PURE__ */ new Set();
|
|
626
|
-
const stack = [dependsOnId];
|
|
627
|
-
while (stack.length > 0) {
|
|
628
|
-
const currentId = stack.pop();
|
|
629
|
-
if (currentId === taskId) {
|
|
630
|
-
return true;
|
|
631
|
-
}
|
|
632
|
-
if (visited.has(currentId)) {
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
visited.add(currentId);
|
|
636
|
-
const currentTask = this.getTask(currentId);
|
|
637
|
-
if (currentTask) {
|
|
638
|
-
stack.push(...currentTask.depends_on);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
return false;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
export {
|
|
645
|
-
PebblesTaskStore
|
|
646
|
-
};
|
|
647
|
-
//# sourceMappingURL=pebbles-task-store.js.map
|