mindsystem-cc 3.3.3 → 3.6.0

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 (31) hide show
  1. package/README.md +65 -1
  2. package/agents/ms-code-simplifier.md +1 -0
  3. package/agents/ms-flutter-code-quality.md +168 -0
  4. package/agents/ms-flutter-reviewer.md +210 -0
  5. package/agents/ms-flutter-simplifier.md +12 -118
  6. package/bin/install.js +444 -85
  7. package/commands/ms/audit-milestone.md +209 -0
  8. package/commands/ms/do-work.md +7 -7
  9. package/commands/ms/execute-phase.md +5 -5
  10. package/commands/ms/research-project.md +10 -4
  11. package/mindsystem/templates/config.json +5 -1
  12. package/mindsystem/workflows/do-work.md +23 -23
  13. package/mindsystem/workflows/execute-phase.md +20 -20
  14. package/mindsystem/workflows/map-codebase.md +12 -6
  15. package/package.json +3 -2
  16. package/skills/flutter-code-quality/SKILL.md +142 -0
  17. package/skills/flutter-code-simplification/SKILL.md +102 -0
  18. package/skills/flutter-senior-review/AGENTS.md +869 -0
  19. package/skills/flutter-senior-review/SKILL.md +205 -0
  20. package/skills/flutter-senior-review/principles/dependencies-data-not-callbacks.md +75 -0
  21. package/skills/flutter-senior-review/principles/dependencies-provider-tree.md +85 -0
  22. package/skills/flutter-senior-review/principles/dependencies-temporal-coupling.md +97 -0
  23. package/skills/flutter-senior-review/principles/pragmatism-consistent-error-handling.md +130 -0
  24. package/skills/flutter-senior-review/principles/pragmatism-speculative-generality.md +91 -0
  25. package/skills/flutter-senior-review/principles/state-data-clumps.md +64 -0
  26. package/skills/flutter-senior-review/principles/state-invalid-states.md +53 -0
  27. package/skills/flutter-senior-review/principles/state-single-source-of-truth.md +68 -0
  28. package/skills/flutter-senior-review/principles/state-type-hierarchies.md +75 -0
  29. package/skills/flutter-senior-review/principles/structure-composition-over-config.md +105 -0
  30. package/skills/flutter-senior-review/principles/structure-shared-visual-patterns.md +107 -0
  31. package/skills/flutter-senior-review/principles/structure-wrapper-pattern.md +90 -0
package/bin/install.js CHANGED
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
  const readline = require('readline');
7
+ const crypto = require('crypto');
7
8
 
8
9
  // Colors (using 256-color mode for better terminal compatibility)
9
10
  const cyan = '\x1b[38;5;37m'; // Closest to #2FA7A0 in 256-color palette
@@ -54,6 +55,7 @@ function parseConfigDirArg() {
54
55
  return null;
55
56
  }
56
57
  const explicitConfigDir = parseConfigDirArg();
58
+ const hasForce = args.includes('--force') || args.includes('-f');
57
59
  const hasHelp = args.includes('--help') || args.includes('-h');
58
60
 
59
61
  console.log(banner);
@@ -66,6 +68,7 @@ if (hasHelp) {
66
68
  ${cyan}-g, --global${reset} Install globally (to Claude config directory)
67
69
  ${cyan}-l, --local${reset} Install locally (to ./.claude in current directory)
68
70
  ${cyan}-c, --config-dir <path>${reset} Specify custom Claude config directory
71
+ ${cyan}-f, --force${reset} Overwrite modified files without prompting
69
72
  ${cyan}-h, --help${reset} Show this help message
70
73
 
71
74
  ${yellow}Examples:${reset}
@@ -99,6 +102,308 @@ function expandTilde(filePath) {
99
102
  return filePath;
100
103
  }
101
104
 
105
+ /**
106
+ * Compute SHA-256 checksum truncated to 16 chars
107
+ */
108
+ function computeChecksum(content) {
109
+ return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
110
+ }
111
+
112
+ /**
113
+ * Read and parse manifest file, return null if missing/corrupted
114
+ */
115
+ function readManifest(claudeDir) {
116
+ const manifestPath = path.join(claudeDir, 'mindsystem', '.manifest.json');
117
+ try {
118
+ if (!fs.existsSync(manifestPath)) {
119
+ return null;
120
+ }
121
+ const content = fs.readFileSync(manifestPath, 'utf8');
122
+ return JSON.parse(content);
123
+ } catch (e) {
124
+ console.log(` ${yellow}⚠${reset} Manifest corrupted, treating as fresh install`);
125
+ return null;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Write manifest JSON file
131
+ */
132
+ function writeManifest(claudeDir, manifest) {
133
+ const manifestDir = path.join(claudeDir, 'mindsystem');
134
+ fs.mkdirSync(manifestDir, { recursive: true });
135
+ const manifestPath = path.join(manifestDir, '.manifest.json');
136
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
137
+ }
138
+
139
+ /**
140
+ * Check if running in interactive TTY
141
+ */
142
+ function isInteractive() {
143
+ return process.stdin.isTTY && process.stdout.isTTY;
144
+ }
145
+
146
+ /**
147
+ * Recursively collect files with relative paths
148
+ * @param {string} baseDir - The base directory for relative path calculation
149
+ * @param {string} currentDir - The current directory being scanned
150
+ * @param {string} destPrefix - The destination prefix (e.g., 'commands/ms', 'agents')
151
+ * @returns {Array<{relativePath: string, absolutePath: string}>}
152
+ */
153
+ function collectFiles(baseDir, currentDir, destPrefix) {
154
+ const files = [];
155
+ if (!fs.existsSync(currentDir)) {
156
+ return files;
157
+ }
158
+
159
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
160
+ for (const entry of entries) {
161
+ const absolutePath = path.join(currentDir, entry.name);
162
+ const relativeToCurrent = path.relative(baseDir, absolutePath);
163
+ const relativePath = path.join(destPrefix, relativeToCurrent);
164
+
165
+ if (entry.isDirectory()) {
166
+ files.push(...collectFiles(baseDir, absolutePath, destPrefix));
167
+ } else {
168
+ files.push({ relativePath, absolutePath });
169
+ }
170
+ }
171
+ return files;
172
+ }
173
+
174
+ /**
175
+ * Build complete list of files to install from all source directories
176
+ * @param {string} src - The source directory (mindsystem package root)
177
+ * @returns {Array<{relativePath: string, absolutePath: string}>}
178
+ */
179
+ function buildInstallManifest(src) {
180
+ const files = [];
181
+
182
+ // commands/ms
183
+ const commandsSrc = path.join(src, 'commands', 'ms');
184
+ files.push(...collectFiles(commandsSrc, commandsSrc, 'commands/ms'));
185
+
186
+ // mindsystem
187
+ const mindsystemSrc = path.join(src, 'mindsystem');
188
+ files.push(...collectFiles(mindsystemSrc, mindsystemSrc, 'mindsystem'));
189
+
190
+ // agents
191
+ const agentsSrc = path.join(src, 'agents');
192
+ files.push(...collectFiles(agentsSrc, agentsSrc, 'agents'));
193
+
194
+ // scripts -> mindsystem/scripts
195
+ const scriptsSrc = path.join(src, 'scripts');
196
+ files.push(...collectFiles(scriptsSrc, scriptsSrc, 'mindsystem/scripts'));
197
+
198
+ // skills
199
+ const skillsSrc = path.join(src, 'skills');
200
+ files.push(...collectFiles(skillsSrc, skillsSrc, 'skills'));
201
+
202
+ // CHANGELOG.md -> mindsystem/CHANGELOG.md
203
+ const changelogSrc = path.join(src, 'CHANGELOG.md');
204
+ if (fs.existsSync(changelogSrc)) {
205
+ files.push({ relativePath: 'mindsystem/CHANGELOG.md', absolutePath: changelogSrc });
206
+ }
207
+
208
+ return files;
209
+ }
210
+
211
+ /**
212
+ * Compare manifests to detect orphans and conflicts
213
+ * @param {Object|null} oldManifest - Previous manifest or null for fresh install
214
+ * @param {string} claudeDir - Target directory
215
+ * @param {Array} newFiles - Files to install
216
+ * @param {string} pathPrefix - Path prefix for content replacement
217
+ * @returns {{orphans: string[], conflicts: Array<{relativePath: string, reason: string}>}}
218
+ */
219
+ function compareManifests(oldManifest, claudeDir, newFiles, pathPrefix) {
220
+ const orphans = [];
221
+ const conflicts = [];
222
+
223
+ // Build set of new file paths
224
+ const newFilePaths = new Set(newFiles.map(f => f.relativePath));
225
+
226
+ // Find orphans (files in old manifest but not in new)
227
+ if (oldManifest && oldManifest.files) {
228
+ for (const oldPath of Object.keys(oldManifest.files)) {
229
+ if (!newFilePaths.has(oldPath)) {
230
+ const fullPath = path.join(claudeDir, oldPath);
231
+ if (fs.existsSync(fullPath)) {
232
+ orphans.push(oldPath);
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ // Find conflicts (installed files modified by user)
239
+ if (oldManifest && oldManifest.files) {
240
+ for (const fileInfo of newFiles) {
241
+ const destPath = path.join(claudeDir, fileInfo.relativePath);
242
+ if (!fs.existsSync(destPath)) {
243
+ continue; // New file, no conflict
244
+ }
245
+
246
+ const oldChecksum = oldManifest.files[fileInfo.relativePath];
247
+ if (!oldChecksum) {
248
+ continue; // File not in old manifest, treat as new
249
+ }
250
+
251
+ // Read current installed content
252
+ const installedContent = fs.readFileSync(destPath, 'utf8');
253
+ const installedChecksum = computeChecksum(installedContent);
254
+
255
+ // If installed file differs from what we last installed, it's been modified
256
+ if (installedChecksum !== oldChecksum) {
257
+ // Read source content with path replacement to compare
258
+ let sourceContent = fs.readFileSync(fileInfo.absolutePath, 'utf8');
259
+ if (fileInfo.absolutePath.endsWith('.md')) {
260
+ sourceContent = sourceContent.replace(/~\/\.claude\//g, pathPrefix);
261
+ }
262
+ const sourceChecksum = computeChecksum(sourceContent);
263
+
264
+ // Only conflict if source is also different (user modified AND we have changes)
265
+ if (sourceChecksum !== installedChecksum) {
266
+ conflicts.push({
267
+ relativePath: fileInfo.relativePath,
268
+ reason: 'locally modified'
269
+ });
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ return { orphans, conflicts };
276
+ }
277
+
278
+ /**
279
+ * Interactive conflict resolution
280
+ * @param {Array} conflicts - List of conflicting files
281
+ * @param {boolean} forceOverwrite - Skip prompts and overwrite all
282
+ * @returns {Promise<{overwrite: Set<string>, keep: Set<string>}>}
283
+ */
284
+ async function resolveConflicts(conflicts, forceOverwrite) {
285
+ const overwrite = new Set();
286
+ const keep = new Set();
287
+
288
+ if (conflicts.length === 0) {
289
+ return { overwrite, keep };
290
+ }
291
+
292
+ if (forceOverwrite) {
293
+ for (const c of conflicts) {
294
+ overwrite.add(c.relativePath);
295
+ }
296
+ console.log(` ${yellow}⚠${reset} Force overwriting ${conflicts.length} modified file(s)`);
297
+ return { overwrite, keep };
298
+ }
299
+
300
+ if (!isInteractive()) {
301
+ for (const c of conflicts) {
302
+ overwrite.add(c.relativePath);
303
+ }
304
+ console.log(` ${yellow}⚠${reset} Non-interactive mode: overwriting ${conflicts.length} modified file(s)`);
305
+ return { overwrite, keep };
306
+ }
307
+
308
+ const rl = readline.createInterface({
309
+ input: process.stdin,
310
+ output: process.stdout
311
+ });
312
+
313
+ const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
314
+
315
+ console.log(`\n ${yellow}${conflicts.length} file(s) have local modifications:${reset}\n`);
316
+
317
+ let overwriteAll = false;
318
+ let keepAll = false;
319
+
320
+ for (const conflict of conflicts) {
321
+ if (overwriteAll) {
322
+ overwrite.add(conflict.relativePath);
323
+ continue;
324
+ }
325
+ if (keepAll) {
326
+ keep.add(conflict.relativePath);
327
+ continue;
328
+ }
329
+
330
+ console.log(` ${dim}${conflict.relativePath}${reset}`);
331
+ const answer = await question(` [O]verwrite, [K]eep, [A]ll overwrite, [N]one keep? `);
332
+
333
+ switch (answer.toLowerCase().trim()) {
334
+ case 'o':
335
+ case 'overwrite':
336
+ overwrite.add(conflict.relativePath);
337
+ break;
338
+ case 'k':
339
+ case 'keep':
340
+ keep.add(conflict.relativePath);
341
+ break;
342
+ case 'a':
343
+ case 'all':
344
+ overwriteAll = true;
345
+ overwrite.add(conflict.relativePath);
346
+ break;
347
+ case 'n':
348
+ case 'none':
349
+ keepAll = true;
350
+ keep.add(conflict.relativePath);
351
+ break;
352
+ default:
353
+ // Default to overwrite
354
+ overwrite.add(conflict.relativePath);
355
+ }
356
+ }
357
+
358
+ rl.close();
359
+ console.log('');
360
+ return { overwrite, keep };
361
+ }
362
+
363
+ /**
364
+ * Remove orphaned files and empty directories
365
+ * @param {string} claudeDir - Target directory
366
+ * @param {string[]} filesToRemove - Relative paths of files to remove
367
+ */
368
+ function cleanupOrphanedFiles(claudeDir, filesToRemove) {
369
+ if (filesToRemove.length === 0) {
370
+ return;
371
+ }
372
+
373
+ const dirsToCheck = new Set();
374
+
375
+ for (const relativePath of filesToRemove) {
376
+ const fullPath = path.join(claudeDir, relativePath);
377
+ try {
378
+ if (fs.existsSync(fullPath)) {
379
+ fs.unlinkSync(fullPath);
380
+ console.log(` ${yellow}✗${reset} Removed ${relativePath}`);
381
+ // Track parent directories for cleanup
382
+ dirsToCheck.add(path.dirname(fullPath));
383
+ }
384
+ } catch (e) {
385
+ console.log(` ${yellow}⚠${reset} Failed to remove ${relativePath}: ${e.message}`);
386
+ }
387
+ }
388
+
389
+ // Remove empty directories (deepest first)
390
+ const sortedDirs = Array.from(dirsToCheck).sort((a, b) => b.length - a.length);
391
+ for (const dir of sortedDirs) {
392
+ try {
393
+ // Don't remove the claudeDir itself or its immediate children (commands, agents, etc.)
394
+ if (dir === claudeDir || path.dirname(dir) === claudeDir) {
395
+ continue;
396
+ }
397
+ const entries = fs.readdirSync(dir);
398
+ if (entries.length === 0) {
399
+ fs.rmdirSync(dir);
400
+ }
401
+ } catch (e) {
402
+ // Ignore errors (directory not empty or doesn't exist)
403
+ }
404
+ }
405
+ }
406
+
102
407
  /**
103
408
  * Recursively copy directory, replacing paths in .md files
104
409
  */
@@ -144,10 +449,33 @@ function copyDir(srcDir, destDir) {
144
449
  }
145
450
  }
146
451
 
452
+ /**
453
+ * Install a single file with path replacement
454
+ * @param {string} srcPath - Source file path
455
+ * @param {string} destPath - Destination file path
456
+ * @param {string} pathPrefix - Path prefix for .md file replacement
457
+ */
458
+ function installFile(srcPath, destPath, pathPrefix) {
459
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
460
+
461
+ if (srcPath.endsWith('.md')) {
462
+ let content = fs.readFileSync(srcPath, 'utf8');
463
+ content = content.replace(/~\/\.claude\//g, pathPrefix);
464
+ fs.writeFileSync(destPath, content);
465
+ } else {
466
+ fs.copyFileSync(srcPath, destPath);
467
+ }
468
+
469
+ // Make shell scripts executable
470
+ if (srcPath.endsWith('.sh')) {
471
+ fs.chmodSync(destPath, '755');
472
+ }
473
+ }
474
+
147
475
  /**
148
476
  * Install to the specified directory
149
477
  */
150
- function install(isGlobal) {
478
+ async function install(isGlobal) {
151
479
  const src = path.join(__dirname, '..');
152
480
  // Priority: explicit --config-dir arg > CLAUDE_CONFIG_DIR env var > default ~/.claude
153
481
  const configDir = expandTilde(explicitConfigDir) || expandTilde(process.env.CLAUDE_CONFIG_DIR);
@@ -168,85 +496,106 @@ function install(isGlobal) {
168
496
 
169
497
  console.log(` Installing to ${cyan}${locationLabel}${reset}\n`);
170
498
 
171
- // Create commands directory
172
- const commandsDir = path.join(claudeDir, 'commands');
173
- fs.mkdirSync(commandsDir, { recursive: true });
499
+ // Phase 1: Read old manifest
500
+ const oldManifest = readManifest(claudeDir);
501
+
502
+ // Phase 2: Build list of files to install
503
+ const filesToInstall = buildInstallManifest(src);
504
+
505
+ // Phase 3: Compare and detect conflicts
506
+ const { orphans, conflicts } = compareManifests(oldManifest, claudeDir, filesToInstall, pathPrefix);
507
+
508
+ // Phase 4: Resolve conflicts interactively
509
+ const { overwrite, keep } = await resolveConflicts(conflicts, hasForce);
510
+
511
+ // Phase 5: Install files (skipping kept files)
512
+ const newManifestFiles = {};
513
+ const categories = {
514
+ 'commands/ms': { count: 0, label: 'commands/ms' },
515
+ 'mindsystem': { count: 0, label: 'mindsystem' },
516
+ 'agents': { count: 0, label: 'agents' },
517
+ 'mindsystem/scripts': { count: 0, label: 'scripts' },
518
+ 'skills': { count: 0, label: 'skills' }
519
+ };
520
+
521
+ for (const fileInfo of filesToInstall) {
522
+ const destPath = path.join(claudeDir, fileInfo.relativePath);
523
+
524
+ // Skip files user wants to keep
525
+ if (keep.has(fileInfo.relativePath)) {
526
+ // Still need to track in manifest with current checksum
527
+ const installedContent = fs.readFileSync(destPath, 'utf8');
528
+ newManifestFiles[fileInfo.relativePath] = computeChecksum(installedContent);
529
+ continue;
530
+ }
174
531
 
175
- // Copy commands/ms with path replacement
176
- const msSrc = path.join(src, 'commands', 'ms');
177
- const msDest = path.join(commandsDir, 'ms');
178
- copyWithPathReplacement(msSrc, msDest, pathPrefix);
179
- console.log(` ${green}✓${reset} Installed commands/ms`);
532
+ // Install the file
533
+ installFile(fileInfo.absolutePath, destPath, pathPrefix);
180
534
 
181
- // Copy mindsystem skill with path replacement
182
- const skillSrc = path.join(src, 'mindsystem');
183
- const skillDest = path.join(claudeDir, 'mindsystem');
184
- copyWithPathReplacement(skillSrc, skillDest, pathPrefix);
185
- console.log(` ${green}✓${reset} Installed mindsystem`);
535
+ // Compute checksum of installed content (after path replacement)
536
+ let installedContent;
537
+ if (fileInfo.absolutePath.endsWith('.md')) {
538
+ installedContent = fs.readFileSync(fileInfo.absolutePath, 'utf8');
539
+ installedContent = installedContent.replace(/~\/\.claude\//g, pathPrefix);
540
+ } else {
541
+ installedContent = fs.readFileSync(destPath, 'utf8');
542
+ }
543
+ newManifestFiles[fileInfo.relativePath] = computeChecksum(installedContent);
186
544
 
187
- // Copy agents to ~/.claude/agents (subagents must be at root level)
188
- const agentsSrc = path.join(src, 'agents');
189
- if (fs.existsSync(agentsSrc)) {
190
- const agentsDest = path.join(claudeDir, 'agents');
191
- copyWithPathReplacement(agentsSrc, agentsDest, pathPrefix);
192
- console.log(` ${green}✓${reset} Installed agents`);
545
+ // Track category counts
546
+ for (const prefix of Object.keys(categories)) {
547
+ if (fileInfo.relativePath.startsWith(prefix)) {
548
+ categories[prefix].count++;
549
+ break;
550
+ }
551
+ }
193
552
  }
194
553
 
195
- // Copy scripts to ~/.claude/mindsystem/scripts/
196
- const scriptsSrc = path.join(src, 'scripts');
197
- if (fs.existsSync(scriptsSrc)) {
198
- const scriptsDest = path.join(claudeDir, 'mindsystem', 'scripts');
199
- fs.mkdirSync(scriptsDest, { recursive: true });
200
- const scriptEntries = fs.readdirSync(scriptsSrc, { withFileTypes: true });
201
- for (const entry of scriptEntries) {
202
- const srcPath = path.join(scriptsSrc, entry.name);
203
- const destPath = path.join(scriptsDest, entry.name);
204
- if (entry.isDirectory()) {
205
- // Recursively copy directories (like ms-lookup/)
206
- copyDir(srcPath, destPath);
207
- } else {
208
- fs.copyFileSync(srcPath, destPath);
209
- // Make shell scripts executable
210
- if (entry.name.endsWith('.sh')) {
211
- fs.chmodSync(destPath, '755');
212
- }
213
- }
554
+ // Print install summaries
555
+ for (const [prefix, info] of Object.entries(categories)) {
556
+ if (info.count > 0) {
557
+ console.log(` ${green}✓${reset} Installed ${info.label}`);
214
558
  }
215
- console.log(` ${green}✓${reset} Installed scripts`);
216
-
217
- // Check Python availability for ms-lookup
218
- const msLookupPath = path.join(scriptsDest, 'ms-lookup');
219
- if (fs.existsSync(msLookupPath)) {
220
- try {
221
- const { execSync } = require('child_process');
222
- const pyVersion = execSync('python3 --version 2>&1', { encoding: 'utf8' });
223
- const versionMatch = pyVersion.match(/(\d+)\.(\d+)/);
224
- if (versionMatch) {
225
- const [, major, minor] = versionMatch;
226
- if (parseInt(major) < 3 || (parseInt(major) === 3 && parseInt(minor) < 9)) {
227
- console.log(` ${yellow}⚠${reset} Python 3.9+ required for ms-lookup (found ${major}.${minor})`);
228
- } else {
229
- console.log(` ${green}✓${reset} Installed ms-lookup CLI (Python ${major}.${minor})`);
230
- }
559
+ }
560
+
561
+ // Phase 6: Write VERSION file
562
+ const versionDest = path.join(claudeDir, 'mindsystem', 'VERSION');
563
+ fs.writeFileSync(versionDest, pkg.version);
564
+ console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
565
+
566
+ // Phase 7: Check Python for ms-lookup
567
+ const msLookupPath = path.join(claudeDir, 'mindsystem', 'scripts', 'ms-lookup');
568
+ if (fs.existsSync(msLookupPath)) {
569
+ try {
570
+ const { execSync } = require('child_process');
571
+ const pyVersion = execSync('python3 --version 2>&1', { encoding: 'utf8' });
572
+ const versionMatch = pyVersion.match(/(\d+)\.(\d+)/);
573
+ if (versionMatch) {
574
+ const [, major, minor] = versionMatch;
575
+ if (parseInt(major) < 3 || (parseInt(major) === 3 && parseInt(minor) < 9)) {
576
+ console.log(` ${yellow}⚠${reset} Python 3.9+ required for ms-lookup (found ${major}.${minor})`);
577
+ } else {
578
+ console.log(` ${green}✓${reset} Installed ms-lookup CLI (Python ${major}.${minor})`);
231
579
  }
232
- } catch (e) {
233
- console.log(` ${yellow}⚠${reset} Python not found - ms-lookup CLI requires Python 3.9+`);
234
580
  }
581
+ } catch (e) {
582
+ console.log(` ${yellow}⚠${reset} Python not found - ms-lookup CLI requires Python 3.9+`);
235
583
  }
236
584
  }
237
585
 
238
- // Copy CHANGELOG.md
239
- const changelogSrc = path.join(src, 'CHANGELOG.md');
240
- const changelogDest = path.join(claudeDir, 'mindsystem', 'CHANGELOG.md');
241
- if (fs.existsSync(changelogSrc)) {
242
- fs.copyFileSync(changelogSrc, changelogDest);
243
- console.log(` ${green}✓${reset} Installed CHANGELOG.md`);
586
+ // Phase 8: Cleanup orphaned files
587
+ if (orphans.length > 0) {
588
+ console.log('');
589
+ cleanupOrphanedFiles(claudeDir, orphans);
244
590
  }
245
591
 
246
- // Write VERSION file for whats-new command
247
- const versionDest = path.join(claudeDir, 'mindsystem', 'VERSION');
248
- fs.writeFileSync(versionDest, pkg.version);
249
- console.log(` ${green}✓${reset} Wrote VERSION (${pkg.version})`);
592
+ // Phase 9: Write new manifest
593
+ const newManifest = {
594
+ version: pkg.version,
595
+ installedAt: new Date().toISOString(),
596
+ files: newManifestFiles
597
+ };
598
+ writeManifest(claudeDir, newManifest);
250
599
 
251
600
  console.log(`
252
601
  ${green}Done!${reset} Launch Claude Code and run ${cyan}/ms:help${reset}.
@@ -256,7 +605,7 @@ function install(isGlobal) {
256
605
  /**
257
606
  * Prompt for install location
258
607
  */
259
- function promptLocation() {
608
+ async function promptLocation() {
260
609
  const rl = readline.createInterface({
261
610
  input: process.stdin,
262
611
  output: process.stdout
@@ -272,25 +621,35 @@ function promptLocation() {
272
621
  ${cyan}2${reset}) Local ${dim}(./.claude)${reset} - this project only
273
622
  `);
274
623
 
275
- rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
276
- rl.close();
277
- const choice = answer.trim() || '1';
278
- const isGlobal = choice !== '2';
279
- install(isGlobal);
624
+ return new Promise((resolve) => {
625
+ rl.question(` Choice ${dim}[1]${reset}: `, async (answer) => {
626
+ rl.close();
627
+ const choice = answer.trim() || '1';
628
+ const isGlobal = choice !== '2';
629
+ await install(isGlobal);
630
+ resolve();
631
+ });
280
632
  });
281
633
  }
282
634
 
283
635
  // Main
284
- if (hasGlobal && hasLocal) {
285
- console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
286
- process.exit(1);
287
- } else if (explicitConfigDir && hasLocal) {
288
- console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
289
- process.exit(1);
290
- } else if (hasGlobal) {
291
- install(true);
292
- } else if (hasLocal) {
293
- install(false);
294
- } else {
295
- promptLocation();
636
+ async function main() {
637
+ if (hasGlobal && hasLocal) {
638
+ console.error(` ${yellow}Cannot specify both --global and --local${reset}`);
639
+ process.exit(1);
640
+ } else if (explicitConfigDir && hasLocal) {
641
+ console.error(` ${yellow}Cannot use --config-dir with --local${reset}`);
642
+ process.exit(1);
643
+ } else if (hasGlobal) {
644
+ await install(true);
645
+ } else if (hasLocal) {
646
+ await install(false);
647
+ } else {
648
+ await promptLocation();
649
+ }
296
650
  }
651
+
652
+ main().catch((err) => {
653
+ console.error(` ${yellow}Error: ${err.message}${reset}`);
654
+ process.exit(1);
655
+ });