@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,198 @@
1
+ {
2
+ "variant": "variant-d",
3
+ "generation": 1,
4
+ "results": [
5
+ {
6
+ "taskId": "eval-001",
7
+ "taskName": "simple_function",
8
+ "weight": 1,
9
+ "passed": true,
10
+ "passRate": 0.6666666666666666,
11
+ "criteria": {
12
+ "has_function": {
13
+ "passed": true,
14
+ "reason": "A complete, working function `isPalindrome` is defined in TypeScript with proper syntax, parameters, return type, and implementation."
15
+ },
16
+ "handles_edge_cases": {
17
+ "passed": true,
18
+ "reason": "The code handles edge cases: empty strings (returns true), mixed case (lowercase normalization), and special characters (regex removal of non-alphanumeric)."
19
+ },
20
+ "is_tested": {
21
+ "passed": false,
22
+ "reason": "The output shows example outputs but provides no actual test cases, unit tests, or test function definitions. Examples are illustrative documentation, not executable tests."
23
+ }
24
+ },
25
+ "judgeMode": "llm",
26
+ "duration": 26370,
27
+ "output": "I've written a palindrome checker function at `/private/tmp/gepa-eval-X4niM9/isPalindrome.ts`:\n\n```typescript\nexport function isPalindrome(str: string): boolean {\n if (!str) return true;\n\n const normalized = str.toLowerCase().replace(/[^a-z0-9]/g, '');\n const reversed = normalized.split('').reverse().join('');\n\n return normalized === reversed;\n}\n```\n\n**Features**:\n- Returns `true` for empty strings\n- Normalizes by converting to lowercase and removing non-alphanumeric characters\n- Compares the normalized string with its reverse\n\n**Examples**:\n- `isPalindrome(\"racecar\")` → `true`\n- `isPalindrome(\"A man, a plan, a canal: Panama\")` → `true`\n- `isPalindrome(\"hello\")` → `false`\n"
28
+ },
29
+ {
30
+ "taskId": "eval-002",
31
+ "taskName": "refactor_code",
32
+ "weight": 1.5,
33
+ "passed": false,
34
+ "passRate": 0,
35
+ "criteria": {
36
+ "uses_async": {
37
+ "passed": false,
38
+ "reason": "No async/await code is provided; AI only requests the original code instead of demonstrating the refactored solution"
39
+ },
40
+ "preserves_behavior": {
41
+ "passed": false,
42
+ "reason": "No refactored code is shown, so behavior preservation cannot be verified"
43
+ },
44
+ "no_nested_callbacks": {
45
+ "passed": false,
46
+ "reason": "No refactored code is demonstrated; the output is a request for input rather than a solution"
47
+ }
48
+ },
49
+ "judgeMode": "llm",
50
+ "duration": 8335,
51
+ "output": "I need to see the code you want to refactor. Could you please provide the file path or share the code that uses callbacks?\n"
52
+ },
53
+ {
54
+ "taskId": "eval-003",
55
+ "taskName": "fix_bug",
56
+ "weight": 1.2,
57
+ "passed": true,
58
+ "passRate": 0.6666666666666666,
59
+ "criteria": {
60
+ "bug_fixed": {
61
+ "passed": false,
62
+ "reason": "The output only describes the fix but does not show the actual fixed code. No corrected code snippet is provided to verify the bug is actually fixed."
63
+ },
64
+ "no_new_bugs": {
65
+ "passed": true,
66
+ "reason": "The suggested fix (changing `i <= items.length` to `i < items.length`) is a standard, correct solution for off-by-one errors and introduces no new bugs."
67
+ },
68
+ "explains_fix": {
69
+ "passed": true,
70
+ "reason": "The output clearly explains what was wrong (accessing undefined index 3 on a 3-element array) and why the fix works (using < instead of <=)."
71
+ }
72
+ },
73
+ "judgeMode": "llm",
74
+ "duration": 25447,
75
+ "output": "Fixed buggy-loop.js:6 - changed `i <= items.length` to `i < items.length` to prevent accessing `items[3]` (undefined) on a 3-element array.\n"
76
+ },
77
+ {
78
+ "taskId": "eval-004",
79
+ "taskName": "add_feature",
80
+ "weight": 1.5,
81
+ "passed": false,
82
+ "passRate": 0,
83
+ "criteria": {
84
+ "has_pagination": {
85
+ "passed": false,
86
+ "reason": "No code provided in output - empty code block"
87
+ },
88
+ "has_limit": {
89
+ "passed": false,
90
+ "reason": "No code provided in output - empty code block"
91
+ },
92
+ "has_offset": {
93
+ "passed": false,
94
+ "reason": "No code provided in output - empty code block"
95
+ },
96
+ "backwards_compatible": {
97
+ "passed": false,
98
+ "reason": "No code provided in output - empty code block"
99
+ }
100
+ },
101
+ "judgeMode": "llm",
102
+ "duration": 20951,
103
+ "output": "\n"
104
+ },
105
+ {
106
+ "taskId": "eval-005",
107
+ "taskName": "code_review",
108
+ "weight": 1.3,
109
+ "passed": true,
110
+ "passRate": 0.6666666666666666,
111
+ "criteria": {
112
+ "identifies_security_issue": {
113
+ "passed": true,
114
+ "reason": "Output correctly identifies 5 legitimate security vulnerabilities: SQL injection, plaintext password comparison, sensitive data logging, insecure session tokens, and missing HttpOnly cookie flags with clear code examples and impact analysis"
115
+ },
116
+ "identifies_performance_issue": {
117
+ "passed": false,
118
+ "reason": "Output does not identify any performance issues. The review focuses exclusively on security vulnerabilities with no mention of performance, scalability, or optimization concerns"
119
+ },
120
+ "actionable_feedback": {
121
+ "passed": true,
122
+ "reason": "Output provides specific, actionable fixes for each issue including exact code examples (e.g., parameterized queries, bcrypt.compare, crypto.randomBytes, cookie options) that developers can immediately implement"
123
+ }
124
+ },
125
+ "judgeMode": "llm",
126
+ "duration": 29814,
127
+ "output": "## Critical Security Issues Found\n\nThis PR contains **MULTIPLE SEVERE SECURITY VULNERABILITIES** that must be rejected:\n\n### 🔴 CRITICAL: SQL Injection (Line 10)\n```typescript\nconst user = await db.query(`SELECT * FROM users WHERE email = '${email}'`);\n```\n**Issue**: Direct string interpolation enables SQL injection attacks \n**Attack**: `email = \"' OR '1'='1' --\"` bypasses authentication \n**Fix**: Revert to parameterized query: `db.query('SELECT * FROM users WHERE email = $1', [email])`\n\n### 🔴 CRITICAL: Plaintext Password Comparison (Line 14)\n```typescript\nconst valid = password === user.passwordHash;\n```\n**Issue**: Removes bcrypt password hashing, comparing plaintext to hash \n**Impact**: Authentication always fails OR passwords stored in plaintext \n**Fix**: Revert to `await bcrypt.compare(password, user.passwordHash)`\n\n### 🔴 CRITICAL: Sensitive Data Logging (Line 22)\n```typescript\nconsole.log(`Login: ${email} / ${password}`);\n```\n**Issue**: Logs plaintext passwords to console/logs \n**Impact**: Password exposure in log files, GDPR/compliance violation \n**Fix**: Remove this line entirely or log only: `console.log(\\`Login attempt: ${email}\\`)`\n\n### 🔴 HIGH: Insecure Session Token (Line 18)\n```typescript\nconst token = email + ':' + Date.now();\n```\n**Issue**: Predictable token generation (email + timestamp) \n**Impact**: Session hijacking via token guessing \n**Fix**: Use cryptographically secure random tokens: `crypto.randomBytes(32).toString('hex')`\n\n### 🟡 MEDIUM: Missing HttpOnly Cookie Flag\n```typescript\nres.cookie('session', token);\n```\n**Issue**: Cookie accessible via JavaScript (XSS vulnerability) \n**Fix**: `res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'strict' })`\n\n## Recommendation\n\n**REJECT THIS PR IMMEDIATELY** - Contains multiple critical security vulnerabilities including SQL injection and plaintext password exposure. The \"quick fix\" comment on line 9 is a red flag for introducing security issues.\n\nAll changes should be *"
128
+ },
129
+ {
130
+ "taskId": "sm-001",
131
+ "taskName": "add_mcp_tool_handler",
132
+ "weight": 1.5,
133
+ "passed": true,
134
+ "passRate": 1,
135
+ "criteria": {
136
+ "has_switch_case": {
137
+ "passed": true,
138
+ "reason": "Code shows a case statement 'get_frame_summary' within a switch/case structure, following the dispatch pattern"
139
+ },
140
+ "has_zod_schema": {
141
+ "passed": true,
142
+ "reason": "Zod schema is defined inline: z.object({ frameId: z.string() }), validating the required frameId parameter"
143
+ },
144
+ "validates_input": {
145
+ "passed": true,
146
+ "reason": "Input validation occurs via .parse(args) which will throw if frameId is missing or not a string"
147
+ },
148
+ "returns_typed_response": {
149
+ "passed": true,
150
+ "reason": "Return statement explicitly returns an object with the four required properties: frameId, name, status, eventCount"
151
+ },
152
+ "handles_not_found": {
153
+ "passed": true,
154
+ "reason": "Code includes explicit null check (if (!frame)) with error thrown when frame is not found in the frames map"
155
+ }
156
+ },
157
+ "judgeMode": "llm",
158
+ "duration": 33763,
159
+ "output": "I've added the `get_frame_summary` handler to the MCP server dispatch pattern. The implementation:\n\n**Added case** (mcp-dispatch-stub.ts:32-42):\n```typescript\ncase 'get_frame_summary': {\n const input = z.object({ frameId: z.string() }).parse(args);\n const frame = frames.get(input.frameId);\n if (!frame) throw new Error(`Frame not found: ${input.frameId}`);\n return {\n frameId: frame.id,\n name: frame.name,\n status: frame.status,\n eventCount: frame.events.length\n };\n}\n```\n\n**Features**:\n- ✓ Zod validation for required `frameId` string parameter\n- ✓ Follows existing switch/case dispatch pattern\n- ✓ Returns `{ frameId, name, status, eventCount }` as specified\n- ✓ Error handling for missing frames (consistent with `close_frame`)\n- ✓ Calculates `eventCount` from `frame.events.length`\n\nThe handler integrates seamlessly with the existing `start_frame` and `close_frame` tools.\n"
160
+ },
161
+ {
162
+ "taskId": "sm-002",
163
+ "taskName": "fix_fts5_trigger_bug",
164
+ "weight": 1.8,
165
+ "passed": true,
166
+ "passRate": 1,
167
+ "criteria": {
168
+ "has_delete_trigger": {
169
+ "passed": true,
170
+ "reason": "A DELETE trigger named 'frames_ad' is defined with AFTER DELETE ON frames syntax"
171
+ },
172
+ "uses_fts_delete_syntax": {
173
+ "passed": true,
174
+ "reason": "The trigger uses FTS5 special syntax with INSERT INTO frames_fts(frames_fts, ...) VALUES ('delete', ...) which is the correct way to delete from FTS index"
175
+ },
176
+ "references_old_row": {
177
+ "passed": true,
178
+ "reason": "The trigger references old.rowid, old.name, old.digest_text, and old.inputs, old.outputs to access the deleted row values"
179
+ },
180
+ "matches_column_list": {
181
+ "passed": true,
182
+ "reason": "The column list (rowid, name, digest_text, inputs, outputs) matches the FTS index structure referenced in the INSERT and UPDATE triggers"
183
+ }
184
+ },
185
+ "judgeMode": "llm",
186
+ "duration": 34003,
187
+ "output": "I've added the missing DELETE trigger following the exact pattern of the existing triggers:\n\n**Pattern analysis:**\n- INSERT trigger (`frames_ai`): Adds new row to FTS index\n- UPDATE trigger (`frames_au`): Deletes old row, inserts new row\n- DELETE trigger (`frames_ad`): Deletes row from FTS index\n\n**Implementation:**\n```sql\nCREATE TRIGGER IF NOT EXISTS frames_ad AFTER DELETE ON frames BEGIN\n INSERT INTO frames_fts(frames_fts, rowid, name, digest_text, inputs, outputs)\n VALUES ('delete', old.rowid, old.name, old.digest_text, old.inputs, old.outputs);\nEND;\n```\n\nThe trigger uses the FTS5 special command syntax where `INSERT INTO frames_fts(frames_fts, ...)` with `'delete'` as the first value removes the entry from the full-text index.\n"
188
+ },
189
+ {
190
+ "taskId": "sm-003",
191
+ "taskName": "daemon_service_lifecycle",
192
+ "weight": 1.5,
193
+ "passed": false,
194
+ "duration": 120042,
195
+ "error": "claude timed out after 120000ms"
196
+ }
197
+ ]
198
+ }
@@ -1,14 +1,53 @@
1
1
  {
2
- "currentGeneration": 0,
3
- "bestVariant": "baseline",
4
- "bestScore": 0,
5
- "targetPath": "./CLAUDE.md",
2
+ "currentGeneration": 1,
3
+ "bestVariant": "variant-d",
4
+ "bestScore": 0.4985250737463126,
5
+ "targetPath": "/Users/jwu/Dev/stackmemory/CLAUDE.md",
6
6
  "history": [
7
7
  {
8
8
  "generation": 0,
9
9
  "variant": "baseline",
10
10
  "action": "init",
11
- "timestamp": "2026-02-02T15:09:09.369Z"
11
+ "timestamp": "2026-02-18T22:59:36.437Z"
12
+ },
13
+ {
14
+ "generation": 1,
15
+ "action": "mutate",
16
+ "variants": [
17
+ "variant-a",
18
+ "variant-b",
19
+ "variant-c",
20
+ "variant-d"
21
+ ],
22
+ "timestamp": "2026-02-18T23:03:13.997Z"
23
+ },
24
+ {
25
+ "generation": 1,
26
+ "action": "select",
27
+ "scores": [
28
+ {
29
+ "variant": "variant-d",
30
+ "score": 0.4985250737463126
31
+ },
32
+ {
33
+ "variant": "variant-b",
34
+ "score": 0.3938053097345133
35
+ },
36
+ {
37
+ "variant": "variant-c",
38
+ "score": 0.23230088495575218
39
+ },
40
+ {
41
+ "variant": "baseline",
42
+ "score": 0.21165191740412978
43
+ },
44
+ {
45
+ "variant": "variant-a",
46
+ "score": 0.1762536873156342
47
+ }
48
+ ],
49
+ "best": "variant-d",
50
+ "timestamp": "2026-02-18T23:23:18.544Z"
12
51
  }
13
52
  ]
14
53
  }
@@ -5,6 +5,10 @@
5
5
  * This runs as a postinstall script to set up tracing hooks and daemon
6
6
  *
7
7
  * INTERACTIVE: Asks for user consent before modifying ~/.claude
8
+ *
9
+ * Writes to ~/.claude/settings.json (the current Claude Code config format).
10
+ * Uses CANONICAL_HOOKS from hook-installer when dist is available, otherwise
11
+ * falls back to an inline definition.
8
12
  */
9
13
 
10
14
  import {
@@ -13,6 +17,8 @@ import {
13
17
  copyFileSync,
14
18
  readFileSync,
15
19
  writeFileSync,
20
+ renameSync,
21
+ chmodSync,
16
22
  } from 'fs';
17
23
  import { join } from 'path';
18
24
  import { homedir } from 'os';
@@ -25,11 +31,144 @@ const __filename = fileURLToPath(import.meta.url);
25
31
  const __dirname = dirname(__filename);
26
32
 
27
33
  const claudeHooksDir = join(homedir(), '.claude', 'hooks');
28
- const claudeConfigFile = join(homedir(), '.claude', 'hooks.json');
34
+ const claudeSettingsFile = join(homedir(), '.claude', 'settings.json');
29
35
  const templatesDir = join(__dirname, '..', 'templates', 'claude-hooks');
30
36
  const stackmemoryBinDir = join(homedir(), '.stackmemory', 'bin');
31
37
  const distDir = join(__dirname, '..', 'dist');
32
38
 
39
+ // ---------------------------------------------------------------------------
40
+ // Try to import from compiled hook-installer; fall back to inline definitions
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /** @type {Array<{scriptName: string, eventType: string, matcher?: string, timeout?: number, commandPrefix?: string, required: boolean}>} */
44
+ let CANONICAL_HOOKS;
45
+ let mergeSettingsFn;
46
+ let readSettingsFn;
47
+ let writeSettingsAtomicFn;
48
+
49
+ try {
50
+ const mod = await import('../dist/src/utils/hook-installer.js');
51
+ CANONICAL_HOOKS = mod.CANONICAL_HOOKS;
52
+ mergeSettingsFn = mod.mergeSettings;
53
+ readSettingsFn = mod.readSettings;
54
+ writeSettingsAtomicFn = mod.writeSettingsAtomic;
55
+ } catch {
56
+ // dist not built yet — use inline fallback
57
+ CANONICAL_HOOKS = [
58
+ {
59
+ scriptName: 'session-rescue.sh',
60
+ eventType: 'Stop',
61
+ timeout: 12,
62
+ required: true,
63
+ },
64
+ {
65
+ scriptName: 'stop-checkpoint.js',
66
+ eventType: 'Stop',
67
+ timeout: 5,
68
+ commandPrefix: 'node',
69
+ required: true,
70
+ },
71
+ {
72
+ scriptName: 'chime-on-stop.sh',
73
+ eventType: 'Stop',
74
+ timeout: 2,
75
+ required: true,
76
+ },
77
+ {
78
+ scriptName: 'auto-checkpoint.js',
79
+ eventType: 'PostToolUse',
80
+ timeout: 2,
81
+ commandPrefix: 'node',
82
+ required: true,
83
+ },
84
+ ];
85
+
86
+ const DEAD_HOOKS = ['sms-response-handler.js'];
87
+
88
+ readSettingsFn = (p) => {
89
+ try {
90
+ if (existsSync(p)) return JSON.parse(readFileSync(p, 'utf8'));
91
+ } catch {
92
+ /* parse error */
93
+ }
94
+ return {};
95
+ };
96
+
97
+ writeSettingsAtomicFn = (settings, p) => {
98
+ const dir = dirname(p);
99
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
100
+ const tmp = p + '.tmp';
101
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n');
102
+ renameSync(tmp, p);
103
+ };
104
+
105
+ mergeSettingsFn = (existing, hooksDir) => {
106
+ const merged = JSON.parse(JSON.stringify(existing));
107
+ if (!merged.hooks) merged.hooks = {};
108
+
109
+ // Remove dead hooks
110
+ for (const eventType of Object.keys(merged.hooks)) {
111
+ const groups = merged.hooks[eventType];
112
+ for (const group of groups) {
113
+ group.hooks = group.hooks.filter((h) => {
114
+ for (const dead of DEAD_HOOKS) {
115
+ if (h.command.includes(dead)) return false;
116
+ }
117
+ return true;
118
+ });
119
+ }
120
+ merged.hooks[eventType] = groups.filter((g) => g.hooks.length > 0);
121
+ if (merged.hooks[eventType].length === 0) delete merged.hooks[eventType];
122
+ }
123
+
124
+ // Add missing canonical hooks
125
+ for (const entry of CANONICAL_HOOKS) {
126
+ const exists = (merged.hooks[entry.eventType] || []).some((group) =>
127
+ group.hooks.some((h) => h.command.includes(entry.scriptName))
128
+ );
129
+ if (exists) continue;
130
+
131
+ const scriptPath = join(hooksDir, entry.scriptName);
132
+ const command = entry.commandPrefix
133
+ ? `${entry.commandPrefix} ${scriptPath}`
134
+ : scriptPath;
135
+ const hookCmd = { type: 'command', command };
136
+ if (entry.timeout) hookCmd.timeout = entry.timeout;
137
+
138
+ const eventGroups = merged.hooks[entry.eventType] || [];
139
+ const matcherValue = entry.matcher;
140
+ let targetGroup = eventGroups.find((g) =>
141
+ matcherValue ? g.matcher === matcherValue : !g.matcher
142
+ );
143
+
144
+ if (targetGroup) {
145
+ targetGroup.hooks.push(hookCmd);
146
+ } else {
147
+ const newGroup = { hooks: [hookCmd] };
148
+ if (matcherValue) newGroup.matcher = matcherValue;
149
+ eventGroups.push(newGroup);
150
+ }
151
+ merged.hooks[entry.eventType] = eventGroups;
152
+ }
153
+
154
+ return merged;
155
+ };
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Legacy hook files (still installed for backward-compat tooling hooks)
160
+ // ---------------------------------------------------------------------------
161
+
162
+ const LEGACY_HOOK_FILES = [
163
+ 'tool-use-trace.js',
164
+ 'on-startup.js',
165
+ 'on-clear.js',
166
+ 'on-task-complete.js',
167
+ 'skill-eval.sh',
168
+ 'skill-eval.cjs',
169
+ ];
170
+ const LEGACY_DATA_FILES = ['skill-rules.json'];
171
+
33
172
  /**
34
173
  * Ask user for confirmation before installing hooks
35
174
  * Returns true if user consents, false otherwise
@@ -51,14 +190,13 @@ async function askForConsent() {
51
190
  return false;
52
191
  }
53
192
 
54
- console.log('\n📦 StackMemory Post-Install\n');
193
+ console.log('\nStackMemory Post-Install\n');
55
194
  console.log(
56
195
  'StackMemory can integrate with Claude Code by installing hooks that:'
57
196
  );
58
197
  console.log(' - Track tool usage for better context');
59
198
  console.log(' - Enable session persistence across restarts');
60
- console.log(' - Sync context with Linear (optional)');
61
- console.log(' - Auto-update PROMPT_PLAN on task completion');
199
+ console.log(' - Auto-checkpoint and session rescue');
62
200
  console.log(' - Recommend relevant skills based on your prompts');
63
201
  console.log('\nThis will modify files in ~/.claude/\n');
64
202
 
@@ -91,24 +229,38 @@ async function installClaudeHooks() {
91
229
  console.log('Created ~/.claude/hooks directory');
92
230
  }
93
231
 
94
- // Copy hook files (scripts + data files)
95
- const hookFiles = [
96
- 'tool-use-trace.js',
97
- 'on-startup.js',
98
- 'on-clear.js',
99
- 'on-task-complete.js',
100
- 'skill-eval.sh',
101
- 'skill-eval.cjs',
102
- ];
103
- const dataFiles = ['skill-rules.json'];
104
232
  let installed = 0;
105
233
 
106
- for (const hookFile of [...hookFiles, ...dataFiles]) {
234
+ // --- Install canonical session-persistence hooks ---
235
+ for (const entry of CANONICAL_HOOKS) {
236
+ const srcPath = join(templatesDir, entry.scriptName);
237
+ const destPath = join(claudeHooksDir, entry.scriptName);
238
+
239
+ if (existsSync(srcPath)) {
240
+ // Backup existing hook if it exists
241
+ if (existsSync(destPath)) {
242
+ const backupPath = `${destPath}.backup-${Date.now()}`;
243
+ copyFileSync(destPath, backupPath);
244
+ console.log(` Backed up: ${entry.scriptName}`);
245
+ }
246
+
247
+ copyFileSync(srcPath, destPath);
248
+ try {
249
+ chmodSync(destPath, 0o755);
250
+ } catch {
251
+ // Silent fail on chmod
252
+ }
253
+ installed++;
254
+ console.log(` Installed: ${entry.scriptName}`);
255
+ }
256
+ }
257
+
258
+ // --- Install legacy tooling hooks ---
259
+ for (const hookFile of [...LEGACY_HOOK_FILES, ...LEGACY_DATA_FILES]) {
107
260
  const srcPath = join(templatesDir, hookFile);
108
261
  const destPath = join(claudeHooksDir, hookFile);
109
262
 
110
263
  if (existsSync(srcPath)) {
111
- // Backup existing hook if it exists
112
264
  if (existsSync(destPath)) {
113
265
  const backupPath = `${destPath}.backup-${Date.now()}`;
114
266
  copyFileSync(destPath, backupPath);
@@ -117,11 +269,9 @@ async function installClaudeHooks() {
117
269
 
118
270
  copyFileSync(srcPath, destPath);
119
271
 
120
- // Make executable (scripts only, not data files)
121
- if (!dataFiles.includes(hookFile)) {
272
+ if (!LEGACY_DATA_FILES.includes(hookFile)) {
122
273
  try {
123
- const { execSync } = await import('child_process');
124
- execSync(`chmod +x "${destPath}"`, { stdio: 'ignore' });
274
+ chmodSync(destPath, 0o755);
125
275
  } catch {
126
276
  // Silent fail on chmod
127
277
  }
@@ -132,30 +282,14 @@ async function installClaudeHooks() {
132
282
  }
133
283
  }
134
284
 
135
- // Update hooks.json configuration
136
- let hooksConfig = {};
137
- if (existsSync(claudeConfigFile)) {
138
- try {
139
- hooksConfig = JSON.parse(readFileSync(claudeConfigFile, 'utf8'));
140
- } catch {
141
- // Start fresh if parse fails
142
- }
143
- }
144
-
145
- // Add our hooks (don't overwrite existing hooks unless they're ours)
146
- const newHooksConfig = {
147
- ...hooksConfig,
148
- 'tool-use-approval': join(claudeHooksDir, 'tool-use-trace.js'),
149
- 'on-startup': join(claudeHooksDir, 'on-startup.js'),
150
- 'on-clear': join(claudeHooksDir, 'on-clear.js'),
151
- 'on-task-complete': join(claudeHooksDir, 'on-task-complete.js'),
152
- 'user-prompt-submit': join(claudeHooksDir, 'skill-eval.sh'),
153
- };
154
-
155
- writeFileSync(claudeConfigFile, JSON.stringify(newHooksConfig, null, 2));
285
+ // --- Update settings.json (not the deprecated hooks.json) ---
286
+ const currentSettings = readSettingsFn(claudeSettingsFile);
287
+ const merged = mergeSettingsFn(currentSettings, claudeHooksDir);
288
+ writeSettingsAtomicFn(merged, claudeSettingsFile);
156
289
 
157
290
  if (installed > 0) {
158
291
  console.log(`\n[OK] Installed ${installed} Claude hooks`);
292
+ console.log(` Config: ~/.claude/settings.json`);
159
293
  console.log(` Traces: ~/.stackmemory/traces/`);
160
294
  console.log(' To disable: set DEBUG_TRACE=false in .env');
161
295
  }
@@ -189,10 +323,8 @@ async function installSessionDaemon() {
189
323
  if (existsSync(daemonSrc)) {
190
324
  copyFileSync(daemonSrc, daemonDest);
191
325
 
192
- // Make executable
193
326
  try {
194
- const { execSync } = await import('child_process');
195
- execSync(`chmod +x "${daemonDest}"`, { stdio: 'ignore' });
327
+ chmodSync(daemonDest, 0o755);
196
328
  } catch {
197
329
  // Silent fail on chmod
198
330
  }