@stackmemoryai/stackmemory 1.2.0 → 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 (47) hide show
  1. package/dist/src/cli/claude-sm.js +65 -0
  2. package/dist/src/cli/commands/skills.js +123 -1
  3. package/dist/src/hooks/graphiti-hooks.js +149 -0
  4. package/dist/src/hooks/session-summary.js +30 -0
  5. package/dist/src/integrations/graphiti/linear-graphiti-bridge.js +115 -0
  6. package/dist/src/integrations/greptile/client.js +101 -0
  7. package/dist/src/integrations/greptile/config.js +14 -0
  8. package/dist/src/integrations/greptile/index.js +11 -0
  9. package/dist/src/integrations/greptile/types.js +4 -0
  10. package/dist/src/integrations/linear/webhook.js +16 -0
  11. package/dist/src/integrations/mcp/handlers/greptile-handlers.js +456 -0
  12. package/dist/src/integrations/mcp/server.js +136 -0
  13. package/dist/src/integrations/mcp/tool-definitions.js +53 -1
  14. package/dist/src/skills/claude-skills.js +46 -1
  15. package/dist/src/skills/parallel-agent-skill.js +514 -0
  16. package/dist/src/utils/hook-installer.js +155 -0
  17. package/package.json +2 -2
  18. package/scripts/gepa/.before-optimize.md +140 -0
  19. package/scripts/gepa/config.json +7 -1
  20. package/scripts/gepa/evals/fixtures/api-endpoint.ts +31 -0
  21. package/scripts/gepa/evals/fixtures/brittle-integration.ts +38 -0
  22. package/scripts/gepa/evals/fixtures/fts5-triggers.sql +23 -0
  23. package/scripts/gepa/evals/fixtures/leaky-service.ts +70 -0
  24. package/scripts/gepa/evals/fixtures/mcp-dispatch-stub.ts +39 -0
  25. package/scripts/gepa/evals/fixtures/pr-diff.txt +24 -0
  26. package/scripts/gepa/evals/fixtures/unsafe-webhook.ts +34 -0
  27. package/scripts/gepa/evals/fixtures/unwrapped-db-op.ts +42 -0
  28. package/scripts/gepa/evals/stackmemory-tasks.jsonl +8 -0
  29. package/scripts/gepa/generations/gen-000/baseline.md +48 -0
  30. package/scripts/gepa/generations/gen-001/baseline.md +172 -0
  31. package/scripts/gepa/generations/gen-001/variant-a.md +176 -0
  32. package/scripts/gepa/generations/gen-001/variant-b.md +266 -0
  33. package/scripts/gepa/generations/gen-001/variant-c.md +142 -0
  34. package/scripts/gepa/generations/gen-001/variant-d.md +172 -0
  35. package/scripts/gepa/hooks/reflect.js +44 -5
  36. package/scripts/gepa/optimize.js +281 -39
  37. package/scripts/gepa/results/eval-1-baseline.json +218 -0
  38. package/scripts/gepa/results/eval-1-variant-a.json +218 -0
  39. package/scripts/gepa/results/eval-1-variant-b.json +218 -0
  40. package/scripts/gepa/results/eval-1-variant-c.json +198 -0
  41. package/scripts/gepa/results/eval-1-variant-d.json +198 -0
  42. package/scripts/gepa/state.json +44 -5
  43. package/scripts/install-claude-hooks-auto.js +176 -44
  44. package/templates/claude-hooks/auto-checkpoint.js +174 -0
  45. package/templates/claude-hooks/chime-on-stop.sh +22 -0
  46. package/templates/claude-hooks/session-rescue.sh +15 -0
  47. package/templates/claude-hooks/stop-checkpoint.js +120 -0
@@ -0,0 +1,155 @@
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 * as fs from "fs";
6
+ import * as path from "path";
7
+ import * as os from "os";
8
+ const CANONICAL_HOOKS = [
9
+ {
10
+ scriptName: "session-rescue.sh",
11
+ eventType: "Stop",
12
+ timeout: 12,
13
+ required: true
14
+ },
15
+ {
16
+ scriptName: "stop-checkpoint.js",
17
+ eventType: "Stop",
18
+ timeout: 5,
19
+ commandPrefix: "node",
20
+ required: true
21
+ },
22
+ {
23
+ scriptName: "chime-on-stop.sh",
24
+ eventType: "Stop",
25
+ timeout: 2,
26
+ required: true
27
+ },
28
+ {
29
+ scriptName: "auto-checkpoint.js",
30
+ eventType: "PostToolUse",
31
+ timeout: 2,
32
+ commandPrefix: "node",
33
+ required: true
34
+ }
35
+ ];
36
+ const DEAD_HOOKS = ["sms-response-handler.js"];
37
+ function buildCommand(entry, hooksDir) {
38
+ const scriptPath = path.join(hooksDir, entry.scriptName);
39
+ if (entry.commandPrefix) {
40
+ return `${entry.commandPrefix} ${scriptPath}`;
41
+ }
42
+ return scriptPath;
43
+ }
44
+ function hookExists(settings, entry) {
45
+ const groups = settings.hooks?.[entry.eventType];
46
+ if (!groups) return false;
47
+ for (const group of groups) {
48
+ for (const hook of group.hooks) {
49
+ if (hook.command.includes(entry.scriptName)) {
50
+ return true;
51
+ }
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+ function hasDeadHooks(settings) {
57
+ if (!settings.hooks) return false;
58
+ for (const groups of Object.values(settings.hooks)) {
59
+ for (const group of groups) {
60
+ for (const hook of group.hooks) {
61
+ for (const dead of DEAD_HOOKS) {
62
+ if (hook.command.includes(dead)) return true;
63
+ }
64
+ }
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+ function removeDeadHooks(settings) {
70
+ if (!settings.hooks) return false;
71
+ let removed = false;
72
+ for (const eventType of Object.keys(settings.hooks)) {
73
+ const groups = settings.hooks[eventType];
74
+ for (const group of groups) {
75
+ const before = group.hooks.length;
76
+ group.hooks = group.hooks.filter((hook) => {
77
+ for (const dead of DEAD_HOOKS) {
78
+ if (hook.command.includes(dead)) return false;
79
+ }
80
+ return true;
81
+ });
82
+ if (group.hooks.length < before) removed = true;
83
+ }
84
+ settings.hooks[eventType] = groups.filter((g) => g.hooks.length > 0);
85
+ if (settings.hooks[eventType].length === 0) {
86
+ delete settings.hooks[eventType];
87
+ }
88
+ }
89
+ return removed;
90
+ }
91
+ function addHook(settings, entry, hooksDir) {
92
+ if (!settings.hooks) settings.hooks = {};
93
+ const eventGroups = settings.hooks[entry.eventType] || [];
94
+ const command = buildCommand(entry, hooksDir);
95
+ const hookCmd = { type: "command", command };
96
+ if (entry.timeout) hookCmd.timeout = entry.timeout;
97
+ const matcherValue = entry.matcher ?? void 0;
98
+ const targetGroup = eventGroups.find((g) => {
99
+ if (matcherValue) return g.matcher === matcherValue;
100
+ return !g.matcher;
101
+ });
102
+ if (targetGroup) {
103
+ targetGroup.hooks.push(hookCmd);
104
+ } else {
105
+ const newGroup = { hooks: [hookCmd] };
106
+ if (matcherValue) newGroup.matcher = matcherValue;
107
+ eventGroups.push(newGroup);
108
+ }
109
+ settings.hooks[entry.eventType] = eventGroups;
110
+ }
111
+ function mergeSettings(existing, hooksDir) {
112
+ const merged = JSON.parse(JSON.stringify(existing));
113
+ removeDeadHooks(merged);
114
+ for (const entry of CANONICAL_HOOKS) {
115
+ if (!hookExists(merged, entry)) {
116
+ addHook(merged, entry, hooksDir);
117
+ }
118
+ }
119
+ return merged;
120
+ }
121
+ function getSettingsPath() {
122
+ return path.join(os.homedir(), ".claude", "settings.json");
123
+ }
124
+ function readSettings(settingsPath) {
125
+ const p = settingsPath ?? getSettingsPath();
126
+ try {
127
+ if (fs.existsSync(p)) {
128
+ return JSON.parse(fs.readFileSync(p, "utf8"));
129
+ }
130
+ } catch {
131
+ }
132
+ return {};
133
+ }
134
+ function writeSettingsAtomic(settings, settingsPath) {
135
+ const p = settingsPath ?? getSettingsPath();
136
+ const dir = path.dirname(p);
137
+ if (!fs.existsSync(dir)) {
138
+ fs.mkdirSync(dir, { recursive: true });
139
+ }
140
+ const tmp = p + ".tmp";
141
+ fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
142
+ fs.renameSync(tmp, p);
143
+ }
144
+ export {
145
+ CANONICAL_HOOKS,
146
+ DEAD_HOOKS,
147
+ buildCommand,
148
+ getSettingsPath,
149
+ hasDeadHooks,
150
+ hookExists,
151
+ mergeSettings,
152
+ readSettings,
153
+ removeDeadHooks,
154
+ writeSettingsAtomic
155
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackmemoryai/stackmemory",
3
- "version": "1.2.0",
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",
@@ -171,7 +171,7 @@
171
171
  "@types/better-sqlite3": "^7.6.8",
172
172
  "@types/express": "^4.17.25",
173
173
  "@types/js-yaml": "^4.0.9",
174
- "@types/node": "^20.10.6",
174
+ "@types/node": "^22.0.0",
175
175
  "@types/uuid": "^10.0.0",
176
176
  "@types/ws": "^8.5.10",
177
177
  "@typescript-eslint/eslint-plugin": "^8.50.1",
@@ -0,0 +1,140 @@
1
+ # StackMemory - Project Configuration
2
+
3
+ ## Project Structure
4
+
5
+ ```
6
+ src/
7
+ cli/ # CLI commands and entry point
8
+ core/ # Core business logic
9
+ context/ # Frame and context management
10
+ database/ # Database adapters (SQLite, ParadeDB)
11
+ digest/ # Digest generation
12
+ query/ # Query parsing and routing
13
+ integrations/ # External integrations (Linear, MCP)
14
+ services/ # Business services
15
+ skills/ # Claude Code skills
16
+ utils/ # Shared utilities
17
+ scripts/ # Build and utility scripts
18
+ config/ # Configuration files
19
+ docs/ # Documentation
20
+ ```
21
+
22
+ ## Key Files
23
+
24
+ - Entry: src/cli/index.ts
25
+ - MCP Server: src/integrations/mcp/server.ts
26
+ - Frame Manager: src/core/context/frame-manager.ts
27
+ - Database: src/core/database/sqlite-adapter.ts
28
+
29
+ ## Detailed Guides
30
+
31
+ Quick reference (agent_docs/):
32
+ - linear_integration.md - Linear sync
33
+ - mcp_server.md - MCP tools
34
+ - database_storage.md - Storage
35
+ - claude_hooks.md - Hooks
36
+
37
+ Full documentation (docs/):
38
+ - principles.md - Agent programming paradigm
39
+ - architecture.md - Extension model and browser sandbox
40
+ - SPEC.md - Technical specification
41
+ - API_REFERENCE.md - API docs
42
+ - DEVELOPMENT.md - Dev guide
43
+ - SETUP.md - Installation
44
+
45
+ ## Commands
46
+
47
+ ```bash
48
+ npm run build # Compile TypeScript (esbuild)
49
+ npm run lint # ESLint check
50
+ npm run lint:fix # Auto-fix lint issues
51
+ npm test # Run Vitest (watch)
52
+ npm run test:run # Run tests once
53
+ npm run linear:sync # Sync with Linear
54
+
55
+ # StackMemory CLI
56
+ stackmemory capture # Save session state for handoff
57
+ stackmemory restore # Restore from captured state
58
+ ```
59
+
60
+ ## Working Directory
61
+
62
+ - PRIMARY: /Users/jwu/Dev/stackmemory
63
+ - ALLOWED: All subdirectories
64
+ - TEMP: /tmp for temporary operations
65
+
66
+ ## Validation (MUST DO)
67
+
68
+ After code changes:
69
+ 1. `npm run lint` - fix any errors AND warnings
70
+ 2. `npm run test:run` - verify no regressions
71
+ 3. `npm run build` - ensure compilation
72
+ 4. Run code to verify it works
73
+
74
+ Test coverage:
75
+ - New features require tests in `src/**/__tests__/`
76
+ - Maintain or improve coverage (no untested code paths)
77
+ - Critical paths: context management, handoff, Linear sync
78
+
79
+ Never: Assume success | Skip testing | Use mock data as fallback
80
+
81
+ ## Git Rules (CRITICAL)
82
+
83
+ - NEVER use `--no-verify` on git push or commit
84
+ - ALWAYS fix lint/test errors before pushing
85
+ - If pre-push hooks fail, fix the underlying issue
86
+ - Run `npm run lint && npm run test:run` before pushing
87
+ - Commit message format: `type(scope): message`
88
+ - Branch naming: `feature/STA-XXX-description` | `fix/STA-XXX-description` | `chore/description`
89
+
90
+ ## Task Management
91
+
92
+ - Use TodoWrite for 3+ steps or multiple requests
93
+ - Keep one task in_progress at a time
94
+ - Update task status immediately on completion
95
+
96
+ ## Security
97
+
98
+ NEVER hardcode secrets - use process.env with dotenv/config
99
+
100
+ ```javascript
101
+ import 'dotenv/config';
102
+ const API_KEY = process.env.LINEAR_API_KEY;
103
+ if (!API_KEY) {
104
+ console.error('LINEAR_API_KEY not set');
105
+ process.exit(1);
106
+ }
107
+ ```
108
+
109
+ Environment sources (check in order):
110
+ 1. .env file
111
+ 2. .env.local
112
+ 3. ~/.zshrc
113
+ 4. Process environment
114
+
115
+ Secret patterns to block: lin_api_* | lin_oauth_* | sk-* | npm_*
116
+
117
+ ## Deploy
118
+
119
+ ```bash
120
+ # npm publish (uses NPM_TOKEN from .env, no OTP needed)
121
+ git stash -- scripts/gepa/ # stash GEPA state (dirties working tree)
122
+ NPM_TOKEN=$(grep '^NPM_TOKEN=' .env | cut -d= -f2) \
123
+ npm publish --registry https://registry.npmjs.org/ \
124
+ --//registry.npmjs.org/:_authToken="$NPM_TOKEN"
125
+ git stash pop # restore GEPA state
126
+
127
+ # Railway
128
+ railway up
129
+
130
+ # Pre-publish checks require clean git status — stash GEPA files first
131
+ ```
132
+
133
+ ## Workflow
134
+
135
+ - Check .env for API keys before asking
136
+ - Run npm run linear:sync after task completion
137
+ - Use browser MCP for visual testing
138
+ - Review recent commits and stackmemory.json on session start
139
+ - Use subagents for multi-step tasks
140
+ - Ask 1-3 clarifying questions for complex commands (one at a time)
@@ -24,7 +24,7 @@
24
24
 
25
25
  "evals": {
26
26
  "directory": "./evals",
27
- "minSamplesPerVariant": 5,
27
+ "minSamplesPerVariant": 8,
28
28
  "timeout": 120000,
29
29
  "metrics": [
30
30
  "task_completion",
@@ -34,6 +34,12 @@
34
34
  ]
35
35
  },
36
36
 
37
+ "judge": {
38
+ "model": "claude-haiku-4-5-20251001",
39
+ "maxOutputTokens": 2000,
40
+ "timeoutMs": 30000
41
+ },
42
+
37
43
  "scoring": {
38
44
  "weights": {
39
45
  "task_completion": 0.4,
@@ -0,0 +1,31 @@
1
+ // Simple API endpoint that needs pagination added
2
+ import express from 'express';
3
+
4
+ const router = express.Router();
5
+
6
+ interface User {
7
+ id: number;
8
+ name: string;
9
+ email: string;
10
+ }
11
+
12
+ // In-memory store
13
+ const users: User[] = Array.from({ length: 100 }, (_, i) => ({
14
+ id: i + 1,
15
+ name: `User ${i + 1}`,
16
+ email: `user${i + 1}@example.com`,
17
+ }));
18
+
19
+ // GET /users - returns ALL users (no pagination)
20
+ router.get('/users', (req, res) => {
21
+ res.json(users);
22
+ });
23
+
24
+ // GET /users/:id
25
+ router.get('/users/:id', (req, res) => {
26
+ const user = users.find((u) => u.id === parseInt(req.params.id));
27
+ if (!user) return res.status(404).json({ error: 'Not found' });
28
+ res.json(user);
29
+ });
30
+
31
+ export default router;
@@ -0,0 +1,38 @@
1
+ // Integration handler that crashes on external API failure
2
+ // Needs graceful degradation following the DiffMem/Greptile pattern
3
+
4
+ interface MCPResponse {
5
+ content: Array<{ type: string; text: string }>;
6
+ metadata?: Record<string, unknown>;
7
+ }
8
+
9
+ // This handler crashes the entire MCP server when the API is unreachable.
10
+ // Refactor to degrade gracefully.
11
+ async function handleExternalLookup(args: {
12
+ query: string;
13
+ limit?: number;
14
+ }): Promise<MCPResponse> {
15
+ const response = await fetch('https://api.external-service.com/search', {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json' },
18
+ body: JSON.stringify({ query: args.query, limit: args.limit || 10 }),
19
+ });
20
+
21
+ // BUG: No error handling — fetch failures (ECONNREFUSED, DNS, timeout)
22
+ // will throw and crash the MCP server process.
23
+ // BUG: 4xx errors (bad request, auth failed) should not be retried.
24
+ // BUG: Logs at error level, flooding logs when service is down.
25
+
26
+ if (!response.ok) {
27
+ throw new Error(`API error: ${response.status}`);
28
+ }
29
+
30
+ const data = await response.json();
31
+
32
+ return {
33
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
34
+ metadata: { tool: 'external_lookup', resultCount: data.results?.length },
35
+ };
36
+ }
37
+
38
+ export { handleExternalLookup };
@@ -0,0 +1,23 @@
1
+ -- FTS5 external content table for full-text search on frames
2
+ CREATE VIRTUAL TABLE IF NOT EXISTS frames_fts USING fts5(
3
+ name, digest_text, inputs, outputs,
4
+ content='frames', content_rowid='rowid'
5
+ );
6
+
7
+ -- Sync trigger: INSERT
8
+ CREATE TRIGGER IF NOT EXISTS frames_ai AFTER INSERT ON frames BEGIN
9
+ INSERT INTO frames_fts(rowid, name, digest_text, inputs, outputs)
10
+ VALUES (new.rowid, new.name, new.digest_text, new.inputs, new.outputs);
11
+ END;
12
+
13
+ -- Sync trigger: UPDATE
14
+ CREATE TRIGGER IF NOT EXISTS frames_au AFTER UPDATE ON frames BEGIN
15
+ INSERT INTO frames_fts(frames_fts, rowid, name, digest_text, inputs, outputs)
16
+ VALUES ('delete', old.rowid, old.name, old.digest_text, old.inputs, old.outputs);
17
+ INSERT INTO frames_fts(rowid, name, digest_text, inputs, outputs)
18
+ VALUES (new.rowid, new.name, new.digest_text, new.inputs, new.outputs);
19
+ END;
20
+
21
+ -- BUG: Missing DELETE trigger!
22
+ -- When a frame is deleted, the FTS index still contains stale data.
23
+ -- Add the missing AFTER DELETE trigger below.
@@ -0,0 +1,70 @@
1
+ // Daemon service with timer leak bug
2
+ interface ServiceConfig {
3
+ enabled: boolean;
4
+ interval: number; // minutes
5
+ }
6
+
7
+ interface ServiceState {
8
+ isRunning: boolean;
9
+ intervalMs: number;
10
+ lastRunTime: number;
11
+ errorCount: number;
12
+ }
13
+
14
+ class MonitorService {
15
+ private config: ServiceConfig;
16
+ private intervalId?: NodeJS.Timeout;
17
+ private isRunning = false;
18
+ private lastRunTime = 0;
19
+ private errorCount = 0;
20
+
21
+ constructor(config: ServiceConfig) {
22
+ this.config = config;
23
+ }
24
+
25
+ // BUG: No guard against double-start — calling start() twice
26
+ // creates two intervals but only stores the second one.
27
+ // The first interval leaks and keeps running forever.
28
+ start(): void {
29
+ this.isRunning = true;
30
+ const intervalMs = this.config.interval * 60 * 1000;
31
+
32
+ this.doWork(); // initial run
33
+
34
+ this.intervalId = setInterval(() => {
35
+ this.doWork();
36
+ }, intervalMs);
37
+ }
38
+
39
+ stop(): void {
40
+ if (this.intervalId) {
41
+ clearInterval(this.intervalId);
42
+ this.intervalId = undefined;
43
+ }
44
+ this.isRunning = false;
45
+ }
46
+
47
+ // BUG: Calls stop() then start() but start() doesn't check
48
+ // if already running, so if stop() fails to clear the interval
49
+ // (e.g., intervalId is undefined), we leak.
50
+ updateConfig(config: Partial<ServiceConfig>): void {
51
+ const wasRunning = this.isRunning;
52
+ if (wasRunning) this.stop();
53
+ this.config = { ...this.config, ...config };
54
+ if (wasRunning && this.config.enabled) this.start();
55
+ }
56
+
57
+ // TODO: Add getState() method returning ServiceState
58
+
59
+ private doWork(): void {
60
+ try {
61
+ // Simulate work
62
+ console.log('Running monitor check...');
63
+ this.lastRunTime = Date.now();
64
+ } catch (err) {
65
+ this.errorCount++;
66
+ }
67
+ }
68
+ }
69
+
70
+ export { MonitorService, ServiceConfig, ServiceState };
@@ -0,0 +1,39 @@
1
+ // Existing MCP server dispatch pattern — add get_frame_summary handler
2
+ import { z } from 'zod';
3
+
4
+ interface Frame {
5
+ id: string;
6
+ name: string;
7
+ status: 'open' | 'closed';
8
+ events: Array<{ id: string; type: string }>;
9
+ }
10
+
11
+ // Simulated frame store
12
+ const frames = new Map<string, Frame>();
13
+
14
+ // Existing tool dispatch (add your handler here)
15
+ async function handleToolCall(name: string, args: unknown) {
16
+ switch (name) {
17
+ case 'start_frame': {
18
+ const input = z.object({ name: z.string().min(1) }).parse(args);
19
+ const id = `frame-${Date.now()}`;
20
+ frames.set(id, { id, name: input.name, status: 'open', events: [] });
21
+ return { frameId: id, status: 'opened' };
22
+ }
23
+
24
+ case 'close_frame': {
25
+ const input = z.object({ frameId: z.string() }).parse(args);
26
+ const frame = frames.get(input.frameId);
27
+ if (!frame) throw new Error(`Frame not found: ${input.frameId}`);
28
+ frame.status = 'closed';
29
+ return { frameId: frame.id, status: 'closed' };
30
+ }
31
+
32
+ // TODO: Add get_frame_summary handler here
33
+
34
+ default:
35
+ throw new Error(`Unknown tool: ${name}`);
36
+ }
37
+ }
38
+
39
+ export { handleToolCall };
@@ -0,0 +1,24 @@
1
+ diff --git a/src/auth/login.ts b/src/auth/login.ts
2
+ index 1a2b3c4..5d6e7f8 100644
3
+ --- a/src/auth/login.ts
4
+ +++ b/src/auth/login.ts
5
+ @@ -12,8 +12,15 @@ export async function handleLogin(req: Request, res: Response) {
6
+ const { email, password } = req.body;
7
+
8
+ - const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);
9
+ + // Quick fix: direct string interpolation for faster queries
10
+ + const user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);
11
+ if (!user) return res.status(401).json({ error: 'Invalid credentials' });
12
+
13
+ - const valid = await bcrypt.compare(password, user.passwordHash);
14
+ + const valid = password === user.passwordHash;
15
+ if (!valid) return res.status(401).json({ error: 'Invalid credentials' });
16
+
17
+ + // Store session
18
+ + const token = email + ':' + Date.now();
19
+ + res.cookie('session', token);
20
+ +
21
+ + // Log for debugging
22
+ + console.log(`Login: ${email} / ${password}`);
23
+ +
24
+ return res.json({ success: true, user: { id: user.id, email: user.email } });
@@ -0,0 +1,34 @@
1
+ // Webhook handler with validation vulnerabilities
2
+ interface WebhookPayload {
3
+ action: string;
4
+ type: string;
5
+ data: {
6
+ id: string;
7
+ title?: string;
8
+ description?: string;
9
+ [key: string]: unknown;
10
+ };
11
+ }
12
+
13
+ // VULNERABLE: No prototype pollution protection, no length limits,
14
+ // no action validation. Fix this function.
15
+ function validateWebhookPayload(payload: unknown): WebhookPayload | null {
16
+ if (!payload || typeof payload !== 'object') return null;
17
+
18
+ const p = payload as Record<string, unknown>;
19
+
20
+ if (!p.action || typeof p.action !== 'string') return null;
21
+ if (!p.type || typeof p.type !== 'string') return null;
22
+ if (!p.data || typeof p.data !== 'object') return null;
23
+
24
+ const data = p.data as Record<string, unknown>;
25
+ if (!data.id || typeof data.id !== 'string') return null;
26
+
27
+ // No sanitization — title and description can be any length
28
+ // No action validation — accepts any string
29
+ // No prototype pollution check — __proto__ passes through
30
+
31
+ return p as unknown as WebhookPayload;
32
+ }
33
+
34
+ export { validateWebhookPayload, WebhookPayload };
@@ -0,0 +1,42 @@
1
+ // Database operation with no error handling — needs StackMemory error wrapping
2
+
3
+ interface Frame {
4
+ id: string;
5
+ name: string;
6
+ status: string;
7
+ }
8
+
9
+ // Simulated database
10
+ const db = {
11
+ prepare(sql: string) {
12
+ return {
13
+ get(...params: unknown[]): Frame | undefined {
14
+ // May throw: SQLITE_BUSY, SQLITE_CONSTRAINT, SQLITE_CORRUPT
15
+ throw new Error('SQLITE_BUSY: database is locked');
16
+ },
17
+ run(...params: unknown[]) {
18
+ // May throw various SQLite errors
19
+ },
20
+ };
21
+ },
22
+ };
23
+
24
+ // This function has NO error handling. Wrap it properly using:
25
+ // - DatabaseError from core/errors
26
+ // - Appropriate ErrorCode (DB_QUERY_FAILED, DB_CONNECTION_FAILED)
27
+ // - Preserve the original error as cause
28
+ // - Set isRetryable = true for connection/busy errors, false for constraint errors
29
+ // - Log with structured context (operation name, frameId)
30
+ async function getFrameById(frameId: string): Promise<Frame | null> {
31
+ const row = db.prepare('SELECT * FROM frames WHERE id = ?').get(frameId);
32
+ return row || null;
33
+ }
34
+
35
+ async function updateFrameStatus(
36
+ frameId: string,
37
+ status: string
38
+ ): Promise<void> {
39
+ db.prepare('UPDATE frames SET status = ? WHERE id = ?').run(status, frameId);
40
+ }
41
+
42
+ export { getFrameById, updateFrameStatus };
@@ -0,0 +1,8 @@
1
+ {"id": "sm-001", "name": "add_mcp_tool_handler", "prompt": "Add a new MCP tool handler called 'get_frame_summary' that takes a frameId (required string) and returns { frameId, name, status, eventCount }. Follow the existing switch/case dispatch pattern in server.ts. Include Zod input validation.", "input_file": "fixtures/mcp-dispatch-stub.ts", "expected": {"has_switch_case": true, "has_zod_schema": true, "validates_input": true, "returns_typed_response": true, "handles_not_found": true}, "weight": 1.5}
2
+ {"id": "sm-002", "name": "fix_fts5_trigger_bug", "prompt": "The FTS5 DELETE trigger is missing — when a frame is deleted from the frames table, the frames_fts index is not updated. Add the missing AFTER DELETE trigger following the pattern of the existing INSERT and UPDATE triggers.", "input_file": "fixtures/fts5-triggers.sql", "expected": {"has_delete_trigger": true, "uses_fts_delete_syntax": true, "references_old_row": true, "matches_column_list": true}, "weight": 1.8}
3
+ {"id": "sm-003", "name": "daemon_service_lifecycle", "prompt": "This daemon service has a timer leak — when updateConfig() restarts the service, the old interval is not cleared before creating a new one if start() is called while already running. Fix the bug and add a getState() method that returns { isRunning, intervalMs, lastRunTime, errorCount }.", "input_file": "fixtures/leaky-service.ts", "expected": {"clears_old_interval": true, "prevents_double_start": true, "has_getstate_method": true, "returns_correct_state_shape": true}, "weight": 1.5}
4
+ {"id": "sm-004", "name": "webhook_payload_validation", "prompt": "This webhook handler is vulnerable to prototype pollution and has no input length limits. Fix the validation to: 1) reject __proto__ and constructor keys, 2) limit title to 500 chars, 3) limit description to 5000 chars, 4) validate that action is one of create/update/remove.", "input_file": "fixtures/unsafe-webhook.ts", "expected": {"blocks_proto_pollution": true, "limits_title_length": true, "limits_description_length": true, "validates_action_enum": true, "returns_null_on_invalid": true}, "weight": 2.0}
5
+ {"id": "sm-005", "name": "error_handling_chain", "prompt": "Wrap this database operation in proper StackMemory error handling: use DatabaseError with appropriate ErrorCode, preserve the cause chain, set isRetryable based on error type (connection errors are retryable, constraint violations are not), and log with structured context.", "input_file": "fixtures/unwrapped-db-op.ts", "expected": {"uses_database_error": true, "preserves_cause": true, "sets_retryable_correctly": true, "has_structured_logging": true, "catches_unknown_type": true}, "weight": 1.3}
6
+ {"id": "sm-006", "name": "integration_graceful_degradation", "prompt": "This integration handler crashes the MCP server when the external API is down. Refactor it to degrade gracefully: catch connection errors, return a user-friendly MCPResponse with metadata.unavailable=true, log at debug level (not error), and don't retry on 4xx errors.", "input_file": "fixtures/brittle-integration.ts", "expected": {"catches_connection_errors": true, "returns_mcp_response": true, "sets_unavailable_metadata": true, "logs_at_debug": true, "no_retry_on_4xx": true}, "weight": 1.5}
7
+ {"id": "sm-007", "name": "sqlite_migration_safety", "prompt": "Write a safe SQLite migration that adds a 'tags' TEXT column to the frames table. The migration must: 1) check if column already exists first (idempotent), 2) wrap in a transaction, 3) update the schema_version table, 4) handle the case where schema_version table doesn't exist yet.", "expected": {"checks_column_exists": true, "uses_transaction": true, "updates_schema_version": true, "is_idempotent": true, "handles_missing_version_table": true}, "weight": 1.5}
8
+ {"id": "sm-008", "name": "review_pr_security", "prompt": "Review this PR diff and identify all security issues, performance problems, and code quality concerns. Provide actionable feedback for each issue found.", "input_file": "fixtures/pr-diff.txt", "expected": {"identifies_sql_injection": true, "identifies_plaintext_password": true, "identifies_credential_logging": true, "identifies_weak_session": true, "provides_fix_suggestions": true}, "weight": 2.0}