warp-os 1.1.2 → 1.2.1

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 (45) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +6 -4
  3. package/VERSION +1 -1
  4. package/agents/warp-annotate.md +394 -0
  5. package/agents/warp-browse.md +9 -1
  6. package/agents/warp-build-code.md +9 -1
  7. package/agents/warp-orchestrator.md +10 -1
  8. package/agents/warp-plan-architect.md +120 -1
  9. package/agents/warp-plan-brainstorm.md +93 -2
  10. package/agents/warp-plan-design.md +97 -4
  11. package/agents/warp-plan-onboarding.md +9 -1
  12. package/agents/warp-plan-optimize.md +9 -1
  13. package/agents/warp-plan-scope.md +67 -1
  14. package/agents/warp-plan-security.md +576 -35
  15. package/agents/warp-plan-testdesign.md +9 -1
  16. package/agents/warp-qa-debug.md +117 -1
  17. package/agents/warp-qa-test.md +167 -1
  18. package/agents/warp-release-update.md +290 -4
  19. package/agents/warp-setup.md +9 -1
  20. package/agents/warp-upgrade.md +21 -4
  21. package/bin/hooks/CLAUDE.md +24 -0
  22. package/bin/hooks/_warp_json.sh +4 -2
  23. package/bin/hooks/identity-briefing.sh +20 -13
  24. package/bin/hooks/validate-askuser.sh +41 -0
  25. package/bin/migrate-sessions.js +284 -173
  26. package/dist/warp-annotate/SKILL.md +404 -0
  27. package/dist/warp-browse/SKILL.md +9 -1
  28. package/dist/warp-build-code/SKILL.md +9 -1
  29. package/dist/warp-orchestrator/SKILL.md +10 -1
  30. package/dist/warp-plan-architect/SKILL.md +120 -1
  31. package/dist/warp-plan-brainstorm/SKILL.md +93 -2
  32. package/dist/warp-plan-design/SKILL.md +97 -4
  33. package/dist/warp-plan-onboarding/SKILL.md +9 -1
  34. package/dist/warp-plan-optimize/SKILL.md +9 -1
  35. package/dist/warp-plan-scope/SKILL.md +67 -1
  36. package/dist/warp-plan-security/SKILL.md +578 -35
  37. package/dist/warp-plan-testdesign/SKILL.md +9 -1
  38. package/dist/warp-qa-debug/SKILL.md +117 -1
  39. package/dist/warp-qa-test/SKILL.md +167 -1
  40. package/dist/warp-release-update/SKILL.md +290 -4
  41. package/dist/warp-setup/SKILL.md +9 -1
  42. package/dist/warp-upgrade/SKILL.md +21 -4
  43. package/package.json +2 -2
  44. package/shared/project-hooks.json +7 -0
  45. package/shared/tier1-engineering-constitution.md +9 -1
@@ -1,41 +1,84 @@
1
1
  #!/usr/bin/env node
2
2
  // migrate-sessions.js — Import old .warp/sessions/*.md into claude-mem
3
- // Run once during upgrade. Reads markdown session handoffs, posts them
4
- // to claude-mem's worker API as observations, then archives originals.
3
+ //
4
+ // Uses bun + direct SQLite inserts (not HTTP API — claude-mem has no public write API).
5
+ // The HTTP API only queues raw tool data through LLM reprocessing, which is wrong for
6
+ // pre-formed historical knowledge. Direct DB insert is the proven approach.
7
+ //
8
+ // Requires: bun (auto-installed by claude-mem)
9
+ // Database: ~/.claude-mem/claude-mem.db
10
+ //
11
+ // Usage:
12
+ // node bin/migrate-sessions.js [project-dir] Run migration
13
+ // node bin/migrate-sessions.js --clean [dir] Remove all warp-import-* data
14
+ // node bin/migrate-sessions.js --dry-run [dir] Preview without touching DB
15
+ //
16
+ // Idempotent: uses content hashes to avoid duplicates. Safe to re-run.
17
+ // All imported sessions use content_session_id = "warp-import-{id}" for identification.
5
18
 
6
19
  const fs = require('fs');
7
20
  const path = require('path');
8
- const http = require('http');
21
+ const os = require('os');
22
+ const crypto = require('crypto');
23
+ const { execSync } = require('child_process');
9
24
 
10
25
  const GREEN = '\x1b[32m';
11
26
  const YELLOW = '\x1b[33m';
12
27
  const CYAN = '\x1b[36m';
28
+ const RED = '\x1b[31m';
13
29
  const NC = '\x1b[0m';
14
30
 
15
31
  function ok(msg) { console.log(` ${GREEN}OK${NC} ${msg}`); }
16
32
  function warn(msg) { console.log(` ${YELLOW}WARN${NC} ${msg}`); }
17
33
  function info(msg) { console.log(` ${CYAN}INFO${NC} ${msg}`); }
34
+ function fail(msg) { console.log(` ${RED}FAIL${NC} ${msg}`); }
18
35
 
19
- // ── Find session files ───────────────────────────────────────────────────────
36
+ // ── Args ─────────────────────────────────────────────────────────────────────
37
+
38
+ const args = process.argv.slice(2);
39
+ const cleanMode = args.includes('--clean');
40
+ const dryRun = args.includes('--dry-run');
41
+ const cwd = args.find(a => !a.startsWith('--')) || process.cwd();
42
+ const DB_PATH = path.join(os.homedir(), '.claude-mem', 'claude-mem.db');
43
+
44
+ // ── Check prerequisites ─────────────────────────────────────────────────────
45
+
46
+ if (!fs.existsSync(DB_PATH)) {
47
+ warn('claude-mem database not found at ~/.claude-mem/claude-mem.db');
48
+ info('Install claude-mem first: npx claude-mem install');
49
+ process.exit(0);
50
+ }
51
+
52
+ let bunPath;
53
+ try {
54
+ bunPath = execSync('which bun 2>/dev/null || where bun 2>nul', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0];
55
+ } catch {
56
+ warn('bun not found. Required for SQLite access.');
57
+ info('Install bun: curl -fsSL https://bun.sh/install | bash');
58
+ process.exit(0);
59
+ }
60
+
61
+ // ── Generate the bun script and execute it ──────────────────────────────────
62
+ // We write a temporary .ts file and run it with bun because bun:sqlite
63
+ // is only available in the bun runtime, not Node.js.
20
64
 
21
- const cwd = process.argv[2] || process.cwd();
22
65
  const sessionsDir = path.join(cwd, '.warp', 'sessions');
23
66
  const archiveDir = path.join(sessionsDir, 'archive');
24
67
 
25
- if (!fs.existsSync(sessionsDir)) {
68
+ if (!cleanMode && !fs.existsSync(sessionsDir)) {
26
69
  console.log('No .warp/sessions/ directory found. Nothing to migrate.');
27
70
  process.exit(0);
28
71
  }
29
72
 
30
73
  // Gather all session files (active + archived)
31
74
  const sessionFiles = [];
32
-
33
- for (const file of fs.readdirSync(sessionsDir)) {
34
- if (file.endsWith('.md') && file.startsWith('SESSION_')) {
35
- sessionFiles.push(path.join(sessionsDir, file));
75
+ if (fs.existsSync(sessionsDir)) {
76
+ for (const file of fs.readdirSync(sessionsDir)) {
77
+ if (file.endsWith('.md') && file.startsWith('SESSION_')) {
78
+ sessionFiles.push(path.join(sessionsDir, file));
79
+ }
36
80
  }
37
81
  }
38
-
39
82
  if (fs.existsSync(archiveDir)) {
40
83
  for (const file of fs.readdirSync(archiveDir)) {
41
84
  if (file.endsWith('.md') && file.startsWith('SESSION_')) {
@@ -44,75 +87,30 @@ if (fs.existsSync(archiveDir)) {
44
87
  }
45
88
  }
46
89
 
47
- if (sessionFiles.length === 0) {
90
+ if (!cleanMode && sessionFiles.length === 0) {
48
91
  console.log('No session files found to migrate.');
49
92
  process.exit(0);
50
93
  }
51
94
 
52
- console.log(`Found ${sessionFiles.length} session file(s) to migrate.`);
53
-
54
- // ── Check if claude-mem worker is running ────────────────────────────────────
55
-
56
- function httpPost(urlPath, data) {
57
- return new Promise((resolve, reject) => {
58
- const body = JSON.stringify(data);
59
- const req = http.request({
60
- hostname: '127.0.0.1',
61
- port: 37777,
62
- path: urlPath,
63
- method: 'POST',
64
- headers: {
65
- 'Content-Type': 'application/json',
66
- 'Content-Length': Buffer.byteLength(body)
67
- },
68
- timeout: 10000
69
- }, (res) => {
70
- let data = '';
71
- res.on('data', chunk => data += chunk);
72
- res.on('end', () => resolve({ status: res.statusCode, body: data }));
73
- });
74
- req.on('error', reject);
75
- req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
76
- req.write(body);
77
- req.end();
78
- });
79
- }
80
-
81
- function httpGet(urlPath) {
82
- return new Promise((resolve, reject) => {
83
- const req = http.request({
84
- hostname: '127.0.0.1',
85
- port: 37777,
86
- path: urlPath,
87
- method: 'GET',
88
- timeout: 5000
89
- }, (res) => {
90
- let data = '';
91
- res.on('data', chunk => data += chunk);
92
- res.on('end', () => resolve({ status: res.statusCode, body: data }));
93
- });
94
- req.on('error', reject);
95
- req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
96
- req.end();
97
- });
98
- }
99
-
100
- // ── Parse session file ───────────────────────────────────────────────────────
101
-
95
+ // Parse session files in Node, pass structured data to bun
102
96
  function parseSessionFile(filePath) {
103
97
  const content = fs.readFileSync(filePath, 'utf8');
104
98
  const filename = path.basename(filePath, '.md');
105
99
 
106
- // Extract session metadata from filename: SESSION_CC7_20260404
107
100
  const match = filename.match(/SESSION_CC(\d+)_(\d{8})/);
108
- const sessionNum = match ? `CC${match[1]}` : filename;
109
- const dateStr = match ? `${match[2].slice(0,4)}-${match[2].slice(4,6)}-${match[2].slice(6,8)}` : 'unknown';
101
+ const sessionNum = match ? parseInt(match[1]) : 0;
102
+ const sessionId = match ? `CC${match[1]}` : filename;
103
+ const dateStr = match
104
+ ? `${match[2].slice(0,4)}-${match[2].slice(4,6)}-${match[2].slice(6,8)}`
105
+ : new Date().toISOString().slice(0,10);
106
+
107
+ // Parse epoch from date string
108
+ const epoch = new Date(dateStr + 'T12:00:00Z').getTime();
110
109
 
111
110
  // Extract sections
112
111
  const sections = {};
113
112
  let currentSection = 'header';
114
113
  sections[currentSection] = [];
115
-
116
114
  for (const line of content.split('\n')) {
117
115
  if (line.startsWith('## ')) {
118
116
  currentSection = line.replace('## ', '').trim();
@@ -123,132 +121,245 @@ function parseSessionFile(filePath) {
123
121
  }
124
122
  }
125
123
 
126
- // Extract goal from header
127
124
  const goalLine = content.match(/Goal:\s+(.+)/);
128
- const goal = goalLine ? goalLine[1].trim() : `Session ${sessionNum}`;
125
+ const goal = goalLine ? goalLine[1].trim() : `Session ${sessionId}`;
126
+
127
+ const extractBullets = (key) =>
128
+ (sections[key] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, ''));
129
+
130
+ const keyDecisions = extractBullets('Key Decisions');
131
+ const itemsAchieved = extractBullets('Items Achieved');
132
+ const itemsNotDone = extractBullets('Items Not Done');
133
+ const nextPickup = (sections['Next Pickup Point'] || []).join('\n').trim();
134
+ const filesProduced = extractBullets('Files Produced');
135
+ const filesAmended = extractBullets('Files Amended');
136
+
137
+ // Content hash for idempotency
138
+ const contentHash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
129
139
 
130
140
  return {
131
- filename,
132
- sessionNum,
133
- dateStr,
134
- goal,
135
- content,
136
- sections,
137
- keyDecisions: (sections['Key Decisions'] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, '')),
138
- itemsAchieved: (sections['Items Achieved'] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, '')),
139
- itemsNotDone: (sections['Items Not Done'] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, '')),
140
- nextPickup: (sections['Next Pickup Point'] || []).join('\n').trim(),
141
- filesProduced: (sections['Files Produced'] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, '')),
142
- filesAmended: (sections['Files Amended'] || []).filter(l => l.trim().startsWith('-')).map(l => l.trim().replace(/^- /, '')),
141
+ filename, sessionId, sessionNum, dateStr, epoch, goal, contentHash,
142
+ keyDecisions, itemsAchieved, itemsNotDone, nextPickup, filesProduced, filesAmended
143
143
  };
144
144
  }
145
145
 
146
- // ── Main ─────────────────────────────────────────────────────────────────────
146
+ const projectName = path.basename(cwd);
147
+ const sessions = sessionFiles.map(f => parseSessionFile(f));
148
+
149
+ if (!cleanMode) {
150
+ console.log(`Found ${sessions.length} session file(s) to migrate.`);
151
+ }
152
+
153
+ // Write the bun migration script to a temp file
154
+ const bunScript = `
155
+ import { Database } from "bun:sqlite";
156
+
157
+ const DB_PATH = ${JSON.stringify(DB_PATH)};
158
+ const projectName = ${JSON.stringify(projectName)};
159
+ const cleanMode = ${cleanMode};
160
+ const dryRun = ${dryRun};
161
+ const sessions = ${JSON.stringify(sessions)};
162
+
163
+ const db = new Database(DB_PATH);
164
+ db.exec("PRAGMA journal_mode=WAL");
165
+
166
+ // Bun-compatible crypto helpers
167
+ function bunHash(input: string): string {
168
+ const hasher = new Bun.CryptoHasher("sha256");
169
+ hasher.update(input);
170
+ return hasher.digest("hex").slice(0, 16);
171
+ }
172
+ function bunUUID(): string {
173
+ return crypto.randomUUID();
174
+ }
175
+
176
+ if (cleanMode) {
177
+ if (dryRun) {
178
+ const count = db.prepare("SELECT COUNT(*) as c FROM sdk_sessions WHERE content_session_id LIKE 'warp-import-%'").get();
179
+ const obsCount = db.prepare(\`
180
+ SELECT COUNT(*) as c FROM observations o
181
+ JOIN sdk_sessions s ON o.memory_session_id = s.memory_session_id
182
+ WHERE s.content_session_id LIKE 'warp-import-%'
183
+ \`).get();
184
+ console.log(\` DRY RUN: Would remove \${count.c} sessions, \${obsCount.c} observations\`);
185
+ } else {
186
+ // Delete observations first (FK constraint)
187
+ const deleted = db.prepare(\`
188
+ DELETE FROM observations WHERE memory_session_id IN (
189
+ SELECT memory_session_id FROM sdk_sessions WHERE content_session_id LIKE 'warp-import-%'
190
+ )
191
+ \`).run();
192
+ // Delete summaries
193
+ db.prepare(\`
194
+ DELETE FROM session_summaries WHERE memory_session_id IN (
195
+ SELECT memory_session_id FROM sdk_sessions WHERE content_session_id LIKE 'warp-import-%'
196
+ )
197
+ \`).run();
198
+ // Delete sessions
199
+ const sessDeleted = db.prepare("DELETE FROM sdk_sessions WHERE content_session_id LIKE 'warp-import-%'").run();
200
+ console.log(\` Cleaned: \${sessDeleted.changes} sessions, \${deleted.changes} observations\`);
201
+ }
202
+ db.close();
203
+ process.exit(0);
204
+ }
205
+
206
+ // Check for existing imports (idempotency)
207
+ const existingImports = db.prepare(
208
+ "SELECT content_session_id FROM sdk_sessions WHERE content_session_id LIKE 'warp-import-%'"
209
+ ).all().map(r => r.content_session_id);
210
+
211
+ let migrated = 0;
212
+ let skipped = 0;
213
+ let failed = 0;
214
+
215
+ for (const session of sessions) {
216
+ const contentSessionId = \`warp-import-\${session.sessionId}\`;
217
+
218
+ // Skip if already imported
219
+ if (existingImports.includes(contentSessionId)) {
220
+ console.log(\` SKIP \${session.filename} (already imported)\`);
221
+ skipped++;
222
+ continue;
223
+ }
224
+
225
+ if (dryRun) {
226
+ const obsCount = session.keyDecisions.length + (session.itemsAchieved.length > 0 ? 1 : 0) +
227
+ (session.itemsNotDone.length > 0 ? 1 : 0) + (session.nextPickup ? 1 : 0);
228
+ console.log(\` DRY \${session.filename} → \${obsCount} observations\`);
229
+ migrated++;
230
+ continue;
231
+ }
147
232
 
148
- async function main() {
149
- // Check worker health
150
233
  try {
151
- const health = await httpGet('/health');
152
- if (health.status !== 200) {
153
- warn('claude-mem worker not healthy. Start it with: npx claude-mem start');
154
- process.exit(1);
234
+ // Create session
235
+ const memorySessionId = bunUUID();
236
+ const dateIso = new Date(session.epoch).toISOString();
237
+ db.prepare(\`
238
+ INSERT INTO sdk_sessions (content_session_id, memory_session_id, project, status, started_at, started_at_epoch, completed_at, completed_at_epoch)
239
+ VALUES (?, ?, ?, 'completed', ?, ?, ?, ?)
240
+ \`).run(contentSessionId, memorySessionId, projectName, dateIso, session.epoch, dateIso, session.epoch);
241
+
242
+ let obsCount = 0;
243
+
244
+ // Import key decisions as individual "decision" observations
245
+ for (const decision of session.keyDecisions) {
246
+ const hash = bunHash(memorySessionId + decision);
247
+ db.prepare(\`
248
+ INSERT INTO observations (memory_session_id, project, type, title, narrative, content_hash, created_at, created_at_epoch)
249
+ VALUES (?, ?, 'decision', ?, ?, ?, ?, ?)
250
+ \`).run(memorySessionId, projectName,
251
+ decision.length > 80 ? decision.slice(0, 77) + '...' : decision,
252
+ decision,
253
+ hash, dateIso, session.epoch);
254
+ obsCount++;
155
255
  }
156
- } catch {
157
- warn('claude-mem worker not running. Start it with: npx claude-mem start');
158
- info('Migration will run on next upgrade after worker is available.');
159
- process.exit(0);
160
- }
161
256
 
162
- const projectName = path.basename(cwd);
163
- let migrated = 0;
164
- let failed = 0;
257
+ // Import items achieved as a "change" observation (grouped)
258
+ if (session.itemsAchieved.length > 0) {
259
+ const narrative = session.itemsAchieved.map(i => \`- \${i}\`).join('\\n');
260
+ const hash = bunHash(memorySessionId + 'achieved' + narrative);
261
+ db.prepare(\`
262
+ INSERT INTO observations (memory_session_id, project, type, title, narrative, content_hash, created_at, created_at_epoch)
263
+ VALUES (?, ?, 'change', ?, ?, ?, ?, ?)
264
+ \`).run(memorySessionId, projectName,
265
+ \`\${session.sessionId}: \${session.itemsAchieved.length} items completed\`,
266
+ \`Session \${session.sessionId} (\${session.dateStr}): \${session.goal}\\n\\nCompleted:\\n\${narrative}\`,
267
+ hash, dateIso, session.epoch);
268
+ obsCount++;
269
+ }
165
270
 
166
- for (const file of sessionFiles) {
167
- const session = parseSessionFile(file);
168
-
169
- // Build a narrative for claude-mem from the session handoff
170
- const narrative = [
171
- `Session ${session.sessionNum} (${session.dateStr}): ${session.goal}`,
172
- '',
173
- session.keyDecisions.length > 0 ? `Key Decisions:\n${session.keyDecisions.map(d => ` - ${d}`).join('\n')}` : '',
174
- session.itemsAchieved.length > 0 ? `Completed:\n${session.itemsAchieved.map(d => ` - ${d}`).join('\n')}` : '',
175
- session.itemsNotDone.length > 0 ? `Not Done:\n${session.itemsNotDone.map(d => ` - ${d}`).join('\n')}` : '',
176
- session.nextPickup ? `Next Pickup: ${session.nextPickup}` : '',
177
- session.filesProduced.length > 0 ? `Files Produced:\n${session.filesProduced.map(d => ` - ${d}`).join('\n')}` : '',
178
- session.filesAmended.length > 0 ? `Files Amended:\n${session.filesAmended.map(d => ` - ${d}`).join('\n')}` : '',
179
- ].filter(Boolean).join('\n\n');
271
+ // Import items not done as a "discovery" observation
272
+ if (session.itemsNotDone.length > 0) {
273
+ const narrative = session.itemsNotDone.map(i => \`- \${i}\`).join('\\n');
274
+ const hash = bunHash(memorySessionId + 'notdone' + narrative);
275
+ db.prepare(\`
276
+ INSERT INTO observations (memory_session_id, project, type, title, narrative, content_hash, created_at, created_at_epoch)
277
+ VALUES (?, ?, 'discovery', ?, ?, ?, ?, ?)
278
+ \`).run(memorySessionId, projectName,
279
+ \`\${session.sessionId}: \${session.itemsNotDone.length} items deferred\`,
280
+ \`Not completed in \${session.sessionId}:\\n\${narrative}\`,
281
+ hash, dateIso, session.epoch);
282
+ obsCount++;
283
+ }
180
284
 
181
- try {
182
- // Post as an observation to claude-mem
183
- const result = await httpPost('/api/sessions/observations', {
184
- contentSessionId: `warp-migration-${session.sessionNum}`,
185
- project: projectName,
186
- tool_name: 'warp-session-migration',
187
- tool_input: {
188
- session: session.sessionNum,
189
- date: session.dateStr,
190
- goal: session.goal
191
- },
192
- tool_output: {
193
- narrative: narrative,
194
- keyDecisions: session.keyDecisions,
195
- itemsAchieved: session.itemsAchieved,
196
- filesAmended: session.filesAmended,
197
- nextPickup: session.nextPickup
198
- }
199
- });
200
-
201
- if (result.status >= 200 && result.status < 300) {
202
- ok(`${session.filename} → claude-mem (${session.keyDecisions.length} decisions, ${session.itemsAchieved.length} items)`);
203
- migrated++;
204
- } else {
205
- // If the observation API doesn't accept this format, try the raw session init
206
- const initResult = await httpPost('/api/sessions/init', {
207
- contentSessionId: `warp-migration-${session.sessionNum}`,
208
- project: projectName,
209
- userPrompt: `[Migrated from ${session.filename}] ${session.goal}\n\n${narrative}`
210
- });
211
-
212
- if (initResult.status >= 200 && initResult.status < 300) {
213
- ok(`${session.filename} → claude-mem (via session init)`);
214
- migrated++;
215
- } else {
216
- warn(`${session.filename} — API returned ${result.status}`);
217
- failed++;
218
- }
219
- }
220
- } catch (err) {
221
- warn(`${session.filename} — ${err.message}`);
222
- failed++;
285
+ // Import next pickup as a "discovery" observation
286
+ if (session.nextPickup) {
287
+ const hash = bunHash(memorySessionId + 'pickup' + session.nextPickup);
288
+ db.prepare(\`
289
+ INSERT INTO observations (memory_session_id, project, type, title, narrative, content_hash, created_at, created_at_epoch)
290
+ VALUES (?, ?, 'discovery', ?, ?, ?, ?, ?)
291
+ \`).run(memorySessionId, projectName,
292
+ \`\${session.sessionId}: Next pickup point\`,
293
+ session.nextPickup,
294
+ hash, dateIso, session.epoch);
295
+ obsCount++;
223
296
  }
297
+
298
+ // Create session summary
299
+ const summaryRequest = session.goal;
300
+ const summaryLearned = session.keyDecisions.join('; ') || 'No key decisions recorded';
301
+ const summaryCompleted = session.itemsAchieved.join('; ') || 'No items recorded';
302
+ const summaryNextSteps = session.nextPickup || 'No next steps recorded';
303
+ const filesRead = session.filesProduced.join(', ');
304
+ const filesModified = session.filesAmended.join(', ');
305
+
306
+ const summaryDateIso = new Date(session.epoch).toISOString();
307
+ db.prepare(\`
308
+ INSERT INTO session_summaries (memory_session_id, project, request, learned, completed, next_steps, files_read, files_edited, created_at, created_at_epoch)
309
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
310
+ \`).run(memorySessionId, projectName, summaryRequest, summaryLearned, summaryCompleted, summaryNextSteps, filesRead, filesModified, summaryDateIso, session.epoch);
311
+
312
+ console.log(\` OK \${session.filename} → \${obsCount} observations, 1 summary\`);
313
+ migrated++;
314
+ } catch (err) {
315
+ console.log(\` FAIL \${session.filename} — \${err.message}\`);
316
+ failed++;
224
317
  }
318
+ }
319
+
320
+ db.close();
321
+
322
+ console.log('');
323
+ console.log(\`Migration: \${migrated} imported, \${skipped} skipped, \${failed} failed\`);
324
+ `;
325
+
326
+ // Write temp script and execute with bun
327
+ const tmpScript = path.join(os.tmpdir(), `warp-migrate-${Date.now()}.ts`);
328
+ fs.writeFileSync(tmpScript, bunScript);
329
+
330
+ try {
331
+ execSync(`"${bunPath}" run "${tmpScript}"`, {
332
+ stdio: 'inherit',
333
+ timeout: 120000,
334
+ env: { ...process.env }
335
+ });
336
+ } catch (err) {
337
+ fail(`Migration script failed: ${err.message}`);
338
+ process.exit(1);
339
+ } finally {
340
+ try { fs.unlinkSync(tmpScript); } catch {}
341
+ }
225
342
 
226
- // Archive migrated files
227
- if (migrated > 0) {
228
- const migratedDir = path.join(sessionsDir, 'migrated-to-claude-mem');
229
- fs.mkdirSync(migratedDir, { recursive: true });
343
+ // Archive migrated files (only if not dry-run and not clean mode)
344
+ if (!dryRun && !cleanMode && sessions.length > 0) {
345
+ const migratedDir = path.join(sessionsDir, 'migrated-to-claude-mem');
346
+ fs.mkdirSync(migratedDir, { recursive: true });
230
347
 
231
- for (const file of sessionFiles) {
232
- const basename = path.basename(file);
233
- const dest = path.join(migratedDir, basename);
234
- try {
348
+ let archived = 0;
349
+ for (const file of sessionFiles) {
350
+ const basename = path.basename(file);
351
+ const dest = path.join(migratedDir, basename);
352
+ try {
353
+ if (!fs.existsSync(dest)) {
235
354
  fs.copyFileSync(file, dest);
236
- fs.unlinkSync(file);
237
- } catch {
238
- // File might already be in archive subdir, just leave it
239
355
  }
356
+ fs.unlinkSync(file);
357
+ archived++;
358
+ } catch {
359
+ // File might already be archived or in a subdir
240
360
  }
241
- ok(`Archived ${migrated} session file(s) → .warp/sessions/migrated-to-claude-mem/`);
242
361
  }
243
-
244
- console.log('');
245
- console.log(`Migration: ${GREEN}${migrated}${NC} imported, ${YELLOW}${failed}${NC} failed`);
246
- if (failed > 0) {
247
- info('Failed files left in place. Re-run migration after fixing claude-mem worker.');
362
+ if (archived > 0) {
363
+ ok(`Archived ${archived} session file(s) → .warp/sessions/migrated-to-claude-mem/`);
248
364
  }
249
365
  }
250
-
251
- main().catch(err => {
252
- warn(`Migration error: ${err.message}`);
253
- process.exit(1);
254
- });