@stackmemoryai/stackmemory 1.2.1 → 1.2.2

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.
Files changed (39) hide show
  1. package/dist/src/cli/commands/skills.js +123 -1
  2. package/dist/src/hooks/graphiti-hooks.js +149 -0
  3. package/dist/src/hooks/session-summary.js +30 -0
  4. package/dist/src/integrations/graphiti/linear-graphiti-bridge.js +115 -0
  5. package/dist/src/integrations/greptile/client.js +101 -0
  6. package/dist/src/integrations/greptile/config.js +14 -0
  7. package/dist/src/integrations/greptile/index.js +11 -0
  8. package/dist/src/integrations/greptile/types.js +4 -0
  9. package/dist/src/integrations/linear/webhook.js +16 -0
  10. package/dist/src/integrations/mcp/handlers/greptile-handlers.js +456 -0
  11. package/dist/src/integrations/mcp/server.js +27 -0
  12. package/dist/src/skills/claude-skills.js +46 -1
  13. package/dist/src/skills/parallel-agent-skill.js +514 -0
  14. package/package.json +1 -1
  15. package/scripts/gepa/.before-optimize.md +140 -159
  16. package/scripts/gepa/config.json +7 -1
  17. package/scripts/gepa/evals/fixtures/api-endpoint.ts +31 -0
  18. package/scripts/gepa/evals/fixtures/brittle-integration.ts +38 -0
  19. package/scripts/gepa/evals/fixtures/fts5-triggers.sql +23 -0
  20. package/scripts/gepa/evals/fixtures/leaky-service.ts +70 -0
  21. package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +39 -0
  22. package/scripts/gepa/evals/fixtures/pr-diff.txt +24 -0
  23. package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +34 -0
  24. package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +42 -0
  25. package/scripts/gepa/evals/stackmemory-tasks.jsonl +8 -0
  26. package/scripts/gepa/generations/gen-000/baseline.md +172 -159
  27. package/scripts/gepa/generations/gen-001/baseline.md +172 -159
  28. package/scripts/gepa/generations/gen-001/variant-a.md +156 -146
  29. package/scripts/gepa/generations/gen-001/variant-b.md +199 -170
  30. package/scripts/gepa/generations/gen-001/variant-c.md +127 -46
  31. package/scripts/gepa/generations/gen-001/variant-d.md +160 -107
  32. package/scripts/gepa/hooks/reflect.js +44 -5
  33. package/scripts/gepa/optimize.js +281 -39
  34. package/scripts/gepa/results/eval-1-baseline.json +187 -10
  35. package/scripts/gepa/results/eval-1-variant-a.json +188 -11
  36. package/scripts/gepa/results/eval-1-variant-b.json +188 -11
  37. package/scripts/gepa/results/eval-1-variant-c.json +168 -11
  38. package/scripts/gepa/results/eval-1-variant-d.json +169 -12
  39. package/scripts/gepa/state.json +18 -18
@@ -0,0 +1,514 @@
1
+ import { fileURLToPath as __fileURLToPath } from 'url';
2
+ import { dirname as __pathDirname } from 'path';
3
+ const __filename = __fileURLToPath(import.meta.url);
4
+ const __dirname = __pathDirname(__filename);
5
+ import { spawn } from "child_process";
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import { execSync } from "child_process";
9
+ import { logger } from "../core/monitoring/logger.js";
10
+ import { hookEmitter } from "../hooks/events.js";
11
+ const DEFAULT_TIMEOUT = 5 * 60 * 1e3;
12
+ function spawnAgent(prompt, opts) {
13
+ return new Promise((resolve, reject) => {
14
+ let workDir;
15
+ try {
16
+ workDir = fs.mkdtempSync(path.join("/tmp", `${opts.prefix}-`));
17
+ } catch (err) {
18
+ return reject(
19
+ new Error(`Failed to create temp dir: ${err.message}`)
20
+ );
21
+ }
22
+ try {
23
+ execSync(`git clone --depth=1 "${opts.repoRoot}" "${workDir}"`, {
24
+ stdio: "pipe",
25
+ timeout: 3e4
26
+ });
27
+ } catch (err) {
28
+ cleanupDir(workDir);
29
+ return reject(new Error(`git clone failed: ${err.message}`));
30
+ }
31
+ const child = spawn("claude", ["--print"], {
32
+ cwd: workDir,
33
+ stdio: ["pipe", "pipe", "pipe"],
34
+ env: { ...process.env }
35
+ });
36
+ let stdout = "";
37
+ let stderr = "";
38
+ let killed = false;
39
+ const timer = setTimeout(() => {
40
+ killed = true;
41
+ child.kill("SIGTERM");
42
+ }, opts.timeout);
43
+ if (child.stdout) {
44
+ child.stdout.on("data", (d) => stdout += d);
45
+ }
46
+ if (child.stderr) {
47
+ child.stderr.on("data", (d) => stderr += d);
48
+ }
49
+ child.on("close", (code) => {
50
+ clearTimeout(timer);
51
+ resolve({ stdout, stderr, exitCode: code, workDir, timedOut: killed });
52
+ });
53
+ child.on("error", (err) => {
54
+ clearTimeout(timer);
55
+ resolve({
56
+ stdout,
57
+ stderr: err.message,
58
+ exitCode: 1,
59
+ workDir,
60
+ timedOut: false
61
+ });
62
+ });
63
+ if (child.stdin) {
64
+ child.stdin.write(prompt);
65
+ child.stdin.end();
66
+ }
67
+ });
68
+ }
69
+ function cleanupDir(dir) {
70
+ try {
71
+ fs.rmSync(dir, { recursive: true, force: true });
72
+ } catch {
73
+ }
74
+ }
75
+ function slugify(text) {
76
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
77
+ }
78
+ class ParallelAgentSkill {
79
+ constructor(context) {
80
+ this.context = context;
81
+ this.repoRoot = process.cwd();
82
+ }
83
+ repoRoot;
84
+ /**
85
+ * Research — explore codebase, save findings as a frame.
86
+ */
87
+ async research(question, options) {
88
+ if (!question?.trim()) {
89
+ return { success: false, message: "Question is required" };
90
+ }
91
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
92
+ const prompt = [
93
+ "Explore this codebase and answer the following question.",
94
+ "Do NOT modify any files. Output your findings as structured markdown.",
95
+ "",
96
+ `Question: ${question}`
97
+ ].join("\n");
98
+ let result;
99
+ try {
100
+ hookEmitter.emitHook({
101
+ type: "agent_start",
102
+ timestamp: Date.now(),
103
+ data: { agentType: "research", workDir: "", task: question }
104
+ }).catch(() => {
105
+ });
106
+ result = await spawnAgent(prompt, {
107
+ prefix: "sm-research",
108
+ timeout,
109
+ repoRoot: this.repoRoot
110
+ });
111
+ } catch (err) {
112
+ const msg = err.message;
113
+ hookEmitter.emitHook({
114
+ type: "agent_error",
115
+ timestamp: Date.now(),
116
+ data: { agentType: "research", error: msg }
117
+ }).catch(() => {
118
+ });
119
+ return {
120
+ success: false,
121
+ message: `Agent failed to start: ${msg}`
122
+ };
123
+ }
124
+ try {
125
+ if (result.timedOut) {
126
+ hookEmitter.emitHook({
127
+ type: "agent_complete",
128
+ timestamp: Date.now(),
129
+ data: {
130
+ agentType: "research",
131
+ workDir: result.workDir,
132
+ exitCode: result.exitCode,
133
+ timedOut: true
134
+ }
135
+ }).catch(() => {
136
+ });
137
+ return {
138
+ success: false,
139
+ message: `Research agent timed out after ${timeout / 1e3}s`,
140
+ data: { partial: result.stdout.slice(0, 2e3) }
141
+ };
142
+ }
143
+ const findings = result.stdout || "(no output)";
144
+ let frameId;
145
+ try {
146
+ frameId = await this.saveFrame("research", question, findings);
147
+ } catch (err) {
148
+ logger.warn("Failed to save research frame:", err);
149
+ }
150
+ hookEmitter.emitHook({
151
+ type: "agent_complete",
152
+ timestamp: Date.now(),
153
+ data: {
154
+ agentType: "research",
155
+ workDir: result.workDir,
156
+ exitCode: result.exitCode,
157
+ timedOut: false,
158
+ frameId
159
+ }
160
+ }).catch(() => {
161
+ });
162
+ return {
163
+ success: true,
164
+ message: `Research complete${frameId ? ` (frame: ${frameId})` : ""}`,
165
+ data: { findings, frameId, exitCode: result.exitCode }
166
+ };
167
+ } finally {
168
+ cleanupDir(result.workDir);
169
+ }
170
+ }
171
+ /**
172
+ * Maintain — low-stakes fix, produces a .patch file.
173
+ */
174
+ async maintain(task, options) {
175
+ if (!task?.trim()) {
176
+ return { success: false, message: "Task description is required" };
177
+ }
178
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
179
+ const prompt = [
180
+ task,
181
+ "",
182
+ "After making changes, verify by running: npm run lint && npm run test:run"
183
+ ].join("\n");
184
+ let result;
185
+ try {
186
+ hookEmitter.emitHook({
187
+ type: "agent_start",
188
+ timestamp: Date.now(),
189
+ data: { agentType: "maintain", workDir: "", task }
190
+ }).catch(() => {
191
+ });
192
+ result = await spawnAgent(prompt, {
193
+ prefix: "sm-maint",
194
+ timeout,
195
+ repoRoot: this.repoRoot
196
+ });
197
+ } catch (err) {
198
+ const msg = err.message;
199
+ hookEmitter.emitHook({
200
+ type: "agent_error",
201
+ timestamp: Date.now(),
202
+ data: { agentType: "maintain", error: msg }
203
+ }).catch(() => {
204
+ });
205
+ return {
206
+ success: false,
207
+ message: `Agent failed to start: ${msg}`
208
+ };
209
+ }
210
+ try {
211
+ if (result.timedOut) {
212
+ hookEmitter.emitHook({
213
+ type: "agent_complete",
214
+ timestamp: Date.now(),
215
+ data: {
216
+ agentType: "maintain",
217
+ workDir: result.workDir,
218
+ exitCode: result.exitCode,
219
+ timedOut: true
220
+ }
221
+ }).catch(() => {
222
+ });
223
+ return {
224
+ success: false,
225
+ message: `Maintenance agent timed out after ${timeout / 1e3}s`,
226
+ data: { partial: result.stdout.slice(0, 2e3) }
227
+ };
228
+ }
229
+ let diff = "";
230
+ try {
231
+ diff = execSync("git diff HEAD", {
232
+ cwd: result.workDir,
233
+ encoding: "utf-8",
234
+ timeout: 1e4
235
+ });
236
+ } catch {
237
+ }
238
+ if (!diff.trim()) {
239
+ hookEmitter.emitHook({
240
+ type: "agent_complete",
241
+ timestamp: Date.now(),
242
+ data: {
243
+ agentType: "maintain",
244
+ workDir: result.workDir,
245
+ exitCode: result.exitCode,
246
+ timedOut: false
247
+ }
248
+ }).catch(() => {
249
+ });
250
+ return {
251
+ success: false,
252
+ message: "Agent made no changes (empty diff)",
253
+ data: { output: result.stdout.slice(0, 2e3) }
254
+ };
255
+ }
256
+ let validated = false;
257
+ try {
258
+ execSync("npm run lint", {
259
+ cwd: result.workDir,
260
+ stdio: "pipe",
261
+ timeout: 6e4
262
+ });
263
+ validated = true;
264
+ } catch {
265
+ }
266
+ const patchDir = path.join(this.repoRoot, ".stackmemory", "patches");
267
+ fs.mkdirSync(patchDir, { recursive: true });
268
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
269
+ const slug = slugify(task);
270
+ const patchFile = `${timestamp}-${slug}.patch`;
271
+ const patchPath = path.join(patchDir, patchFile);
272
+ fs.writeFileSync(patchPath, diff);
273
+ const diffLines = diff.split("\n");
274
+ const filesChanged = diffLines.filter(
275
+ (l) => l.startsWith("diff --git")
276
+ ).length;
277
+ const additions = diffLines.filter((l) => /^\+[^+]/.test(l)).length;
278
+ const deletions = diffLines.filter((l) => /^-[^-]/.test(l)).length;
279
+ hookEmitter.emitHook({
280
+ type: "agent_complete",
281
+ timestamp: Date.now(),
282
+ data: {
283
+ agentType: "maintain",
284
+ workDir: result.workDir,
285
+ exitCode: result.exitCode,
286
+ timedOut: false,
287
+ patchPath,
288
+ validated
289
+ }
290
+ }).catch(() => {
291
+ });
292
+ return {
293
+ success: true,
294
+ message: `Patch saved: ${patchFile}${validated ? "" : " (lint failed)"}`,
295
+ data: {
296
+ patchPath,
297
+ validated,
298
+ filesChanged,
299
+ additions,
300
+ deletions,
301
+ output: result.stdout.slice(0, 2e3)
302
+ },
303
+ action: `Apply with: git apply ${patchPath}`
304
+ };
305
+ } finally {
306
+ cleanupDir(result.workDir);
307
+ }
308
+ }
309
+ /**
310
+ * SpecRun — implement a spec file on a branch, validate.
311
+ */
312
+ async specRun(specPath, options) {
313
+ if (!specPath?.trim()) {
314
+ return { success: false, message: "Spec file path is required" };
315
+ }
316
+ const absSpec = path.isAbsolute(specPath) ? specPath : path.join(this.repoRoot, specPath);
317
+ if (!fs.existsSync(absSpec)) {
318
+ return { success: false, message: `Spec file not found: ${specPath}` };
319
+ }
320
+ const specContent = fs.readFileSync(absSpec, "utf-8");
321
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
322
+ const prompt = [
323
+ "Implement the following specification. Follow it precisely.",
324
+ "",
325
+ "--- SPEC START ---",
326
+ specContent,
327
+ "--- SPEC END ---"
328
+ ].join("\n");
329
+ let workDir;
330
+ try {
331
+ workDir = fs.mkdtempSync(path.join("/tmp", "sm-spec-"));
332
+ } catch (err) {
333
+ return {
334
+ success: false,
335
+ message: `Failed to create temp dir: ${err.message}`
336
+ };
337
+ }
338
+ try {
339
+ execSync(`git clone --depth=1 "${this.repoRoot}" "${workDir}"`, {
340
+ stdio: "pipe",
341
+ timeout: 3e4
342
+ });
343
+ } catch (err) {
344
+ const msg = err.message;
345
+ cleanupDir(workDir);
346
+ hookEmitter.emitHook({
347
+ type: "agent_error",
348
+ timestamp: Date.now(),
349
+ data: { agentType: "spec-run", error: msg, workDir }
350
+ }).catch(() => {
351
+ });
352
+ return {
353
+ success: false,
354
+ message: `git clone failed: ${msg}`
355
+ };
356
+ }
357
+ const branchName = `agent/spec-${Date.now()}`;
358
+ try {
359
+ execSync(`git checkout -b "${branchName}"`, {
360
+ cwd: workDir,
361
+ stdio: "pipe",
362
+ timeout: 5e3
363
+ });
364
+ } catch (err) {
365
+ const msg = err.message;
366
+ cleanupDir(workDir);
367
+ hookEmitter.emitHook({
368
+ type: "agent_error",
369
+ timestamp: Date.now(),
370
+ data: { agentType: "spec-run", error: msg, workDir }
371
+ }).catch(() => {
372
+ });
373
+ return {
374
+ success: false,
375
+ message: `Failed to create branch: ${msg}`
376
+ };
377
+ }
378
+ hookEmitter.emitHook({
379
+ type: "agent_start",
380
+ timestamp: Date.now(),
381
+ data: { agentType: "spec-run", workDir, task: specPath }
382
+ }).catch(() => {
383
+ });
384
+ const child = spawn("claude", ["--print"], {
385
+ cwd: workDir,
386
+ stdio: ["pipe", "pipe", "pipe"],
387
+ env: { ...process.env }
388
+ });
389
+ let stdout = "";
390
+ let stderr = "";
391
+ let killed = false;
392
+ const agentDone = new Promise((resolve) => {
393
+ const timer = setTimeout(() => {
394
+ killed = true;
395
+ child.kill("SIGTERM");
396
+ }, timeout);
397
+ if (child.stdout) {
398
+ child.stdout.on("data", (d) => stdout += d);
399
+ }
400
+ if (child.stderr) {
401
+ child.stderr.on("data", (d) => stderr += d);
402
+ }
403
+ child.on("close", (code) => {
404
+ clearTimeout(timer);
405
+ resolve({ stdout, stderr, exitCode: code, timedOut: killed });
406
+ });
407
+ child.on("error", (err) => {
408
+ clearTimeout(timer);
409
+ resolve({
410
+ stdout,
411
+ stderr: err.message,
412
+ exitCode: 1,
413
+ timedOut: false
414
+ });
415
+ });
416
+ });
417
+ if (child.stdin) {
418
+ child.stdin.write(prompt);
419
+ child.stdin.end();
420
+ }
421
+ const agentResult = await agentDone;
422
+ if (agentResult.timedOut) {
423
+ return {
424
+ success: false,
425
+ message: `Spec agent timed out after ${timeout / 1e3}s`,
426
+ data: {
427
+ workDir,
428
+ branch: branchName,
429
+ partial: agentResult.stdout.slice(0, 2e3)
430
+ }
431
+ };
432
+ }
433
+ const validation = { lint: false, test: false, build: false };
434
+ try {
435
+ execSync("npm run lint", {
436
+ cwd: workDir,
437
+ stdio: "pipe",
438
+ timeout: 6e4
439
+ });
440
+ validation.lint = true;
441
+ } catch {
442
+ }
443
+ try {
444
+ execSync("npm run test:run", {
445
+ cwd: workDir,
446
+ stdio: "pipe",
447
+ timeout: 12e4
448
+ });
449
+ validation.test = true;
450
+ } catch {
451
+ }
452
+ try {
453
+ execSync("npm run build", {
454
+ cwd: workDir,
455
+ stdio: "pipe",
456
+ timeout: 6e4
457
+ });
458
+ validation.build = true;
459
+ } catch {
460
+ }
461
+ let diffStat = "";
462
+ try {
463
+ diffStat = execSync("git diff --stat HEAD~1", {
464
+ cwd: workDir,
465
+ encoding: "utf-8",
466
+ timeout: 5e3
467
+ });
468
+ } catch {
469
+ }
470
+ const allPassed = validation.lint && validation.test && validation.build;
471
+ hookEmitter.emitHook({
472
+ type: "agent_complete",
473
+ timestamp: Date.now(),
474
+ data: {
475
+ agentType: "spec-run",
476
+ workDir,
477
+ exitCode: agentResult.exitCode,
478
+ timedOut: false,
479
+ branch: branchName,
480
+ validation
481
+ }
482
+ }).catch(() => {
483
+ });
484
+ return {
485
+ success: allPassed,
486
+ message: allPassed ? `Spec implemented on branch ${branchName}` : `Spec implemented but validation failed on branch ${branchName}`,
487
+ data: {
488
+ workDir,
489
+ branch: branchName,
490
+ validation,
491
+ diffStat: diffStat.trim(),
492
+ output: agentResult.stdout.slice(0, 2e3)
493
+ },
494
+ action: allPassed ? `Review: cd ${workDir} && git log --oneline` : `Inspect: cd ${workDir} && git diff`
495
+ };
496
+ }
497
+ async saveFrame(type, query, content) {
498
+ const db = this.context.database;
499
+ if (!db) return void 0;
500
+ const frameId = await db.createFrame({
501
+ run_id: `agent-${Date.now()}`,
502
+ project_id: this.context.projectId,
503
+ type,
504
+ name: `Agent ${type}: ${query.slice(0, 60)}`,
505
+ state: "completed",
506
+ inputs: { query, agentType: type },
507
+ digest_text: content
508
+ });
509
+ return frameId;
510
+ }
511
+ }
512
+ export {
513
+ ParallelAgentSkill
514
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0",