multimodel-dev-os 2.0.1 → 2.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 (45) hide show
  1. package/.ai/intelligence/README.md +14 -0
  2. package/.ai/intelligence/apply-log.schema.json +65 -0
  3. package/.ai/intelligence/feedback-log.example.jsonl +2 -0
  4. package/.ai/intelligence/feedback.schema.json +47 -0
  5. package/.ai/intelligence/improvement-proposal.schema.json +70 -0
  6. package/.ai/intelligence/learning-rules.example.md +18 -0
  7. package/.ai/intelligence/memory.schema.json +97 -0
  8. package/.ai/policies/approval-gates.md +35 -0
  9. package/.ai/policies/memory-policy.md +30 -0
  10. package/.ai/policies/self-improvement-policy.md +39 -0
  11. package/.ai/proposals/README.md +44 -0
  12. package/.ai/proposals/apply-operation.example.json +22 -0
  13. package/.ai/registries/capabilities.yaml +73 -0
  14. package/.ai/registries/tools.yaml +84 -0
  15. package/.ai/registries/workflows.yaml +217 -0
  16. package/README.md +218 -97
  17. package/bin/multimodel-dev-os.js +2871 -6
  18. package/docs/.vitepress/config.js +23 -1
  19. package/docs/CLI.md +89 -2
  20. package/docs/adapter-sync.md +27 -0
  21. package/docs/adapters.md +16 -0
  22. package/docs/agent-handoff.md +40 -0
  23. package/docs/approved-proposal-apply.md +156 -0
  24. package/docs/capability-registry.md +24 -0
  25. package/docs/feedback-learning.md +33 -0
  26. package/docs/future-proof-architecture.md +22 -0
  27. package/docs/hash-compressed-memory.md +72 -0
  28. package/docs/improvement-proposals.md +70 -0
  29. package/docs/learning-rules.md +36 -0
  30. package/docs/public/llms-full.txt +32 -2
  31. package/docs/public/llms.txt +43 -2
  32. package/docs/public/sitemap.xml +30 -0
  33. package/docs/quickstart.md +7 -1
  34. package/docs/real-repo-onboarding.md +27 -0
  35. package/docs/repository-command-center.md +52 -0
  36. package/docs/self-improving-codebase.md +46 -0
  37. package/docs/template-recommendation.md +22 -0
  38. package/docs/templates-guide.md +11 -0
  39. package/docs/tool-registry.md +21 -0
  40. package/docs/v2-roadmap.md +20 -0
  41. package/docs/workflow-orchestration.md +59 -0
  42. package/package.json +1 -1
  43. package/scripts/install.ps1 +1 -1
  44. package/scripts/install.sh +1 -1
  45. package/scripts/verify.js +107 -3
@@ -6,8 +6,9 @@
6
6
  */
7
7
 
8
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
9
- import { join, dirname, resolve } from 'path';
9
+ import { join, dirname, resolve, relative, isAbsolute, basename } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
+ import { createHash } from 'crypto';
11
12
 
12
13
  const __filename = fileURLToPath(import.meta.url);
13
14
  const __dirname = dirname(__filename);
@@ -42,7 +43,14 @@ function parseArgs(args) {
42
43
  threshold: null,
43
44
  registry: null,
44
45
  allRegistries: false,
45
- release: false
46
+ release: false,
47
+ type: 'unknown',
48
+ tags: '',
49
+ files: '',
50
+ title: null,
51
+ approved: false,
52
+ intelligence: false,
53
+ onboarding: false
46
54
  };
47
55
 
48
56
  for (let i = 0; i < args.length; i++) {
@@ -67,6 +75,10 @@ function parseArgs(args) {
67
75
  params.allRegistries = true;
68
76
  } else if (arg === '--release') {
69
77
  params.release = true;
78
+ } else if (arg === '--intelligence') {
79
+ params.intelligence = true;
80
+ } else if (arg === '--onboarding') {
81
+ params.onboarding = true;
70
82
  } else if (arg === '--json') {
71
83
  params.json = true;
72
84
  } else if (arg === '--threshold') {
@@ -81,8 +93,16 @@ function parseArgs(args) {
81
93
  params.stack = args[++i];
82
94
  } else if (arg === '--mobile') {
83
95
  params.mobile = args[++i];
84
- } else if (arg === '--ai-app') {
85
- params.aiApp = args[++i];
96
+ } else if (arg === '--type') {
97
+ params.type = args[++i];
98
+ } else if (arg === '--tags') {
99
+ params.tags = args[++i];
100
+ } else if (arg === '--files') {
101
+ params.files = args[++i];
102
+ } else if (arg === '--title') {
103
+ params.title = args[++i];
104
+ } else if (arg === '--approved') {
105
+ params.approved = true;
86
106
  } else if (!params.command && !arg.startsWith('-')) {
87
107
  params.command = arg;
88
108
  }
@@ -90,6 +110,24 @@ function parseArgs(args) {
90
110
  return params;
91
111
  }
92
112
 
113
+ function getPositionalArgs(args) {
114
+ const positionalArgs = [];
115
+ for (let i = 0; i < args.length; i++) {
116
+ const arg = args[i];
117
+ if (arg === '--target' || arg === '-t' || arg === '--template' || arg === '--adapter' || arg === '-a' ||
118
+ arg === '--threshold' || arg === '--registry' || arg === '--model-preset' || arg === '--agent' ||
119
+ arg === '--stack' || arg === '--mobile' || arg === '--type' || arg === '--tags' || arg === '--files' ||
120
+ arg === '--title') {
121
+ i++; // skip next arg (its value)
122
+ } else if (arg.startsWith('-')) {
123
+ // it's a flag, skip
124
+ } else {
125
+ positionalArgs.push(arg);
126
+ }
127
+ }
128
+ return positionalArgs;
129
+ }
130
+
93
131
  const params = parseArgs(ARGS);
94
132
  const COMMAND = params.command;
95
133
 
@@ -143,6 +181,74 @@ if (COMMAND === 'init') {
143
181
  handleInit(params);
144
182
  } else if (COMMAND === 'verify') {
145
183
  handleVerify(params);
184
+ } else if (COMMAND === 'scan') {
185
+ handleScan(params);
186
+ } else if (COMMAND === 'memory') {
187
+ const sub = ARGS[1];
188
+ if (sub === 'build') {
189
+ handleMemoryBuild(params);
190
+ } else if (sub === 'refresh') {
191
+ handleMemoryRefresh(params);
192
+ } else if (sub === 'diff') {
193
+ handleMemoryDiff(params);
194
+ } else {
195
+ console.error(`\x1b[31mError: Please specify a memory subcommand: build, refresh, or diff.\x1b[0m`);
196
+ console.error(`Example: node bin/multimodel-dev-os.js memory build`);
197
+ process.exit(1);
198
+ }
199
+ } else if (COMMAND === 'feedback') {
200
+ const sub = ARGS[1];
201
+ if (sub === 'add') {
202
+ handleFeedbackAdd(params);
203
+ } else if (sub === 'list') {
204
+ handleFeedbackList(params);
205
+ } else if (sub === 'summarize') {
206
+ handleFeedbackSummarize(params);
207
+ } else {
208
+ console.error(`\x1b[31mError: Please specify a feedback subcommand: add, list, or summarize.\x1b[0m`);
209
+ console.log(`Example: node bin/multimodel-dev-os.js feedback add "Prefer CSS Modules"`);
210
+ process.exit(1);
211
+ }
212
+ } else if (COMMAND === 'improve') {
213
+ const positional = getPositionalArgs(ARGS);
214
+ const sub = positional[1];
215
+ if (sub === 'propose') {
216
+ handleImprovePropose(params);
217
+ } else if (sub === 'review') {
218
+ handleImproveReview(params);
219
+ } else if (sub === 'status') {
220
+ handleImproveStatus(params);
221
+ } else if (sub === 'validate') {
222
+ const proposalFile = positional[2];
223
+ if (!proposalFile) {
224
+ console.error(`\x1b[31mError: Please specify a proposal file path.\x1b[0m`);
225
+ console.log(`Example: node bin/multimodel-dev-os.js improve validate .ai/proposals/proposal-xxxx.md`);
226
+ process.exit(1);
227
+ }
228
+ handleImproveValidate(proposalFile, params);
229
+ } else if (sub === 'diff') {
230
+ const proposalFile = positional[2];
231
+ if (!proposalFile) {
232
+ console.error(`\x1b[31mError: Please specify a proposal file path.\x1b[0m`);
233
+ console.log(`Example: node bin/multimodel-dev-os.js improve diff .ai/proposals/proposal-xxxx.md`);
234
+ process.exit(1);
235
+ }
236
+ handleImproveDiff(proposalFile, params);
237
+ } else if (sub === 'apply') {
238
+ const proposalFile = positional[2];
239
+ if (!proposalFile) {
240
+ console.error(`\x1b[31mError: Please specify a proposal file path.\x1b[0m`);
241
+ console.log(`Example: node bin/multimodel-dev-os.js improve apply .ai/proposals/proposal-xxxx.md --approved`);
242
+ process.exit(1);
243
+ }
244
+ handleImproveApply(proposalFile, params);
245
+ } else if (sub === 'log') {
246
+ handleImproveLog(params);
247
+ } else {
248
+ console.error(`\x1b[31mError: Please specify an improve subcommand: propose, review, status, validate, diff, apply, or log.\x1b[0m`);
249
+ console.log(`Example: node bin/multimodel-dev-os.js improve validate .ai/proposals/proposal-xxxx.md`);
250
+ process.exit(1);
251
+ }
146
252
  } else if (COMMAND === 'templates' || COMMAND === 'list-templates') {
147
253
  handleListTemplates(params);
148
254
  } else if (COMMAND === 'show-template') {
@@ -213,6 +319,96 @@ if (COMMAND === 'init') {
213
319
  process.exit(1);
214
320
  }
215
321
  handleShowSkill(sName, params);
322
+ } else if (COMMAND === 'status') {
323
+ handleStatus(params);
324
+ } else if (COMMAND === 'workflow') {
325
+ const positional = getPositionalArgs(ARGS);
326
+ const sub = positional[1];
327
+ if (sub === 'list') {
328
+ handleWorkflowList(params);
329
+ } else if (sub === 'show') {
330
+ const wName = positional[2];
331
+ if (!wName) {
332
+ console.error('\x1b[31mError: Please specify a workflow name.\x1b[0m');
333
+ console.log('Example: node bin/multimodel-dev-os.js workflow show repo-health');
334
+ process.exit(1);
335
+ }
336
+ handleWorkflowShow(wName, params);
337
+ } else if (sub === 'plan') {
338
+ const wName = positional[2];
339
+ if (!wName) {
340
+ console.error('\x1b[31mError: Please specify a workflow name.\x1b[0m');
341
+ console.log('Example: node bin/multimodel-dev-os.js workflow plan repo-health');
342
+ process.exit(1);
343
+ }
344
+ handleWorkflowPlan(wName, params);
345
+ } else if (sub === 'run') {
346
+ const wName = positional[2];
347
+ if (!wName) {
348
+ console.error('\x1b[31mError: Please specify a workflow name.\x1b[0m');
349
+ console.log('Example: node bin/multimodel-dev-os.js workflow run repo-health');
350
+ process.exit(1);
351
+ }
352
+ handleWorkflowRun(wName, params);
353
+ } else {
354
+ console.error('\x1b[31mError: Please specify a workflow subcommand: list, show, plan, or run.\x1b[0m');
355
+ console.log('Example: node bin/multimodel-dev-os.js workflow list');
356
+ process.exit(1);
357
+ }
358
+ } else if (COMMAND === 'handoff') {
359
+ const positional = getPositionalArgs(ARGS);
360
+ const sub = positional[1];
361
+ if (sub === 'build') {
362
+ handleHandoffBuild(params);
363
+ } else if (sub === 'show') {
364
+ handleHandoffShow(params);
365
+ } else {
366
+ console.error('\x1b[31mError: Please specify a handoff subcommand: build or show.\x1b[0m');
367
+ console.log('Example: node bin/multimodel-dev-os.js handoff build');
368
+ process.exit(1);
369
+ }
370
+ } else if (COMMAND === 'onboard') {
371
+ const positional = getPositionalArgs(ARGS);
372
+ const sub = positional[1];
373
+ if (sub === 'analyze') {
374
+ handleOnboardAnalyze(params);
375
+ } else if (sub === 'recommend') {
376
+ handleOnboardRecommend(params);
377
+ } else if (sub === 'plan') {
378
+ handleOnboardPlan(params);
379
+ } else if (sub === 'apply') {
380
+ handleOnboardApply(params);
381
+ } else if (sub === 'status') {
382
+ handleOnboardStatus(params);
383
+ } else {
384
+ console.error('\x1b[31mError: Please specify an onboard subcommand: analyze, recommend, plan, apply, or status.\x1b[0m');
385
+ console.log('Example: node bin/multimodel-dev-os.js onboard analyze');
386
+ process.exit(1);
387
+ }
388
+ } else if (COMMAND === 'adapter') {
389
+ const positional = getPositionalArgs(ARGS);
390
+ const sub = positional[1];
391
+ if (sub === 'status') {
392
+ handleAdapterStatus(params);
393
+ } else if (sub === 'diff') {
394
+ const aName = positional[2];
395
+ if (!aName) {
396
+ console.error('\x1b[31mError: Please specify an adapter name (e.g. cursor, claude) or "all".\x1b[0m');
397
+ process.exit(1);
398
+ }
399
+ handleAdapterDiff(aName, params);
400
+ } else if (sub === 'sync') {
401
+ const aName = positional[2];
402
+ if (!aName) {
403
+ console.error('\x1b[31mError: Please specify an adapter name or "all" to sync.\x1b[0m');
404
+ process.exit(1);
405
+ }
406
+ handleAdapterSync(aName, params);
407
+ } else {
408
+ console.error('\x1b[31mError: Please specify an adapter subcommand: status, diff, or sync.\x1b[0m');
409
+ console.log('Example: node bin/multimodel-dev-os.js adapter status');
410
+ process.exit(1);
411
+ }
216
412
  } else {
217
413
  console.error(`\x1b[31mUnknown command: ${COMMAND}\x1b[0m`);
218
414
  showHelp();
@@ -225,6 +421,15 @@ function showHelp() {
225
421
  console.log('Usage: node bin/multimodel-dev-os.js <command> [options]\n');
226
422
  console.log('Commands:');
227
423
  console.log(' init Initialize a project with configs and adapters');
424
+ console.log(' scan Scan project structure and framework signals');
425
+ console.log(' status Show compact dashboard summarizing repository intelligence state');
426
+ console.log(' memory <subcmd> Manage hash-compressed codebase memory (subcmd: build, refresh, diff)');
427
+ console.log(' feedback <subcmd> Manage developer feedback loops (subcmd: add, list, summarize)');
428
+ console.log(' improve <subcmd> Manage codebase self-improvement proposals (subcmd: propose, review, status, validate, diff, apply, log)');
429
+ console.log(' workflow <subcmd> Orchestrate read-only development workflow pipelines (subcmd: list, show, plan, run)');
430
+ console.log(' handoff <subcmd> Compile or print token-compressed agent session summaries (subcmd: build, show)');
431
+ console.log(' onboard <subcmd> Safely integrate MultiModel Dev OS into existing repo (subcmd: analyze, recommend, plan, apply, status)');
432
+ console.log(' adapter <subcmd> Manage and sync rule/settings files for IDE adapters (subcmd: status, diff, sync)');
228
433
  console.log(' verify Validate structural integrity of an existing project');
229
434
  console.log(' templates List all built-in template profiles with details');
230
435
  console.log(' list-templates Alias for templates command');
@@ -244,10 +449,17 @@ function showHelp() {
244
449
  console.log(' show-skill <s> View prompt contents of target workspace skill <s>\n');
245
450
  console.log('Options:');
246
451
  console.log(' -t, --target <path> Target folder destination (default: current working directory)');
452
+ console.log(' --type <type> Feedback classification (correction, preference, bug, etc.)');
453
+ console.log(' --tags <list> Comma-separated descriptor tags for feedback');
454
+ console.log(' --files <list> Comma-separated target files for feedback');
455
+ console.log(' --title <text> Specifies title for codebase improvement proposal');
456
+ console.log(' --approved Explicitly approve and execute proposal/onboarding/adapter sync writes');
247
457
  console.log(' --template <name> Template profile: nextjs-saas, expo-react-native-android, etc.');
248
458
  console.log(' -a, --adapter <name> Inject specific adapter: cursor, claude, vscode, gemini, etc.');
249
459
  console.log(' --caveman Use minimal-token templates (~79% fewer tokens)');
250
460
  console.log(' --tokens Run a deeper token-sink size analysis during doctor checkup');
461
+ console.log(' --intelligence Run diagnostic checkup of repository intelligence config');
462
+ console.log(' --onboarding Run diagnostic checkup of repository onboarding setup');
251
463
  console.log(' --json Output raw JSON data for listing commands (models, adapters, templates)');
252
464
  console.log(' --threshold <val> Set custom size threshold for doctor tokens checks (e.g. 50KB)');
253
465
  console.log(' --registry <path> Override default registry (for templates/adapters list or check)');
@@ -376,7 +588,7 @@ function handleInit(options) {
376
588
  }
377
589
 
378
590
  // Fallback to copy default global folders if files aren't already included by template
379
- const globalAiSubdirs = ['context', 'agents', 'skills', 'prompts', 'checks', 'templates', 'session-logs'];
591
+ const globalAiSubdirs = ['context', 'agents', 'skills', 'prompts', 'checks', 'templates', 'session-logs', 'registries', 'proposals', 'intelligence'];
380
592
  globalAiSubdirs.forEach(sub => {
381
593
  const globalPath = join(sourceRoot, '.ai', sub);
382
594
  if (existsSync(globalPath)) {
@@ -573,9 +785,11 @@ function handleVerify(options) {
573
785
  console.log('\n=====================================');
574
786
  if (failed > 0) {
575
787
  console.error(` \x1b[31mVerification FAILED. [Passed: ${passed}, Failed: ${failed}]\x1b[0m\n`);
788
+ if (options && options.noExit) return false;
576
789
  process.exit(1);
577
790
  } else {
578
791
  console.log(` \x1b[32mVerification PASSED. [All ${passed} files present]\x1b[0m\n`);
792
+ if (options && options.noExit) return true;
579
793
  process.exit(0);
580
794
  }
581
795
  }
@@ -589,6 +803,14 @@ function handleDoctor(options) {
589
803
  handleDoctorRelease(options);
590
804
  return;
591
805
  }
806
+ if (options.intelligence) {
807
+ handleDoctorIntelligence(options);
808
+ return;
809
+ }
810
+ if (options.onboarding) {
811
+ handleDoctorOnboarding(options);
812
+ return;
813
+ }
592
814
  console.log(`\n🩺 \x1b[36mRunning advisory doctor checkup in: ${options.target}\x1b[0m\n`);
593
815
 
594
816
  let warnings = 0;
@@ -669,7 +891,7 @@ function handleDoctor(options) {
669
891
  checkAdapter('vscode', '.vscode/settings.json');
670
892
  checkAdapter('antigravity', '.gemini/settings.json');
671
893
  } else {
672
- warn('.ai/config.yaml is missing from project. Active adapters could not be audited.');
894
+ warn('MultiModel Dev OS is not initialized (.ai/config.yaml is missing). Run "npx multimodel-dev-os init" to bootstrap configuration.');
673
895
  }
674
896
 
675
897
  // 6. Token sinks audit
@@ -1394,3 +1616,2646 @@ function handleDoctorRelease(options) {
1394
1616
  console.log(' \x1b[32m✔ Release hygiene checks PASSED successfully!\x1b[0m\n');
1395
1617
  }
1396
1618
  }
1619
+
1620
+ // ==========================================
1621
+ // --- v2.2.0 Intelligence Layer Helpers & Handlers ---
1622
+ // ==========================================
1623
+
1624
+ function hashFile(filePath) {
1625
+ try {
1626
+ const data = readFileSync(filePath);
1627
+ return createHash('sha256').update(data).digest('hex');
1628
+ } catch (e) {
1629
+ return '';
1630
+ }
1631
+ }
1632
+
1633
+ function shouldIgnorePath(relPath) {
1634
+ const normalized = relPath.replace(/\\/g, '/');
1635
+ const segments = normalized.split('/');
1636
+
1637
+ // Ignored folders
1638
+ const ignoredFolders = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage'];
1639
+ for (const seg of segments) {
1640
+ if (ignoredFolders.includes(seg)) return true;
1641
+ }
1642
+
1643
+ // Special check for docs/.vitepress/dist and docs/.vitepress/cache
1644
+ if (normalized.includes('docs/.vitepress/dist') || normalized.includes('docs/.vitepress/cache')) {
1645
+ return true;
1646
+ }
1647
+
1648
+ // Ignore generated memory and intelligence runtime files
1649
+ if (
1650
+ normalized.endsWith('memory.hash.json') ||
1651
+ normalized.endsWith('memory.summary.md') ||
1652
+ normalized.endsWith('feedback-log.jsonl') ||
1653
+ normalized.endsWith('learning-rules.md') ||
1654
+ normalized.endsWith('apply-log.jsonl') ||
1655
+ normalized.includes('.ai/proposals/')
1656
+ ) {
1657
+ return true;
1658
+ }
1659
+
1660
+ // Skip secret-like files/patterns
1661
+ const lower = normalized.toLowerCase();
1662
+ const filePart = segments[segments.length - 1];
1663
+ if (
1664
+ lower.endsWith('.env') ||
1665
+ lower.includes('.env.') ||
1666
+ lower.endsWith('.npmrc') ||
1667
+ lower.endsWith('.keystore') ||
1668
+ lower.endsWith('.jks') ||
1669
+ lower.endsWith('.key') ||
1670
+ lower.endsWith('.pem') ||
1671
+ lower.endsWith('credentials.json') ||
1672
+ filePart === 'id_rsa' ||
1673
+ filePart === 'id_dsa' ||
1674
+ filePart === 'id_ecdsa' ||
1675
+ filePart === 'id_ed25519'
1676
+ ) {
1677
+ return true;
1678
+ }
1679
+
1680
+ return false;
1681
+ }
1682
+
1683
+ function scanTarget(targetDir) {
1684
+ const files = [];
1685
+ let ignoredCount = 0;
1686
+
1687
+ function walk(dir) {
1688
+ if (!existsSync(dir)) return;
1689
+ const items = readdirSync(dir);
1690
+ for (const item of items) {
1691
+ const fullPath = join(dir, item);
1692
+ const relPath = relative(targetDir, fullPath).replace(/\\/g, '/');
1693
+
1694
+ if (shouldIgnorePath(relPath)) {
1695
+ ignoredCount++;
1696
+ continue;
1697
+ }
1698
+
1699
+ try {
1700
+ const stat = statSync(fullPath);
1701
+ if (stat.isDirectory()) {
1702
+ walk(fullPath);
1703
+ } else if (stat.isFile()) {
1704
+ files.push({
1705
+ relPath,
1706
+ fullPath,
1707
+ size: stat.size,
1708
+ mtime: stat.mtime.toISOString()
1709
+ });
1710
+ }
1711
+ } catch (e) {
1712
+ // Skip inaccessible files or broken links
1713
+ }
1714
+ }
1715
+ }
1716
+
1717
+ walk(targetDir);
1718
+ return { files, ignoredCount };
1719
+ }
1720
+
1721
+ function detectFrameworkSignals(files, targetDir) {
1722
+ const signals = [];
1723
+ const hasFile = (name) => files.some(f => f.relPath.toLowerCase() === name.toLowerCase());
1724
+
1725
+ if (hasFile('next.config.js') || hasFile('next.config.mjs')) signals.push('Next.js');
1726
+ if (hasFile('nuxt.config.js') || hasFile('nuxt.config.ts')) signals.push('Nuxt.js');
1727
+ if (hasFile('wp-config.php') || hasFile('index.php')) signals.push('WordPress/PHP');
1728
+ if (hasFile('tsconfig.json')) signals.push('TypeScript');
1729
+ if (hasFile('package.json')) {
1730
+ signals.push('Node.js');
1731
+ try {
1732
+ const pkg = JSON.parse(readFileSync(join(targetDir, 'package.json'), 'utf8'));
1733
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1734
+ if (deps['react']) signals.push('React');
1735
+ if (deps['vue']) signals.push('Vue');
1736
+ if (deps['svelte']) signals.push('Svelte');
1737
+ if (deps['expo']) signals.push('Expo');
1738
+ if (deps['react-native']) signals.push('React Native');
1739
+ if (deps['vite']) signals.push('Vite');
1740
+ if (deps['express']) signals.push('Express');
1741
+ if (deps['angular']) signals.push('Angular');
1742
+ } catch (e) {}
1743
+ }
1744
+ if (hasFile('requirements.txt') || hasFile('pyproject.toml')) signals.push('Python');
1745
+ if (hasFile('cargo.toml')) signals.push('Rust');
1746
+ if (hasFile('gemfile')) signals.push('Ruby');
1747
+ if (hasFile('go.mod')) signals.push('Go');
1748
+
1749
+ if (signals.length === 0) signals.push('Generic/Unknown');
1750
+ return [...new Set(signals)];
1751
+ }
1752
+
1753
+ function detectDependencySignals(files, targetDir) {
1754
+ const signals = [];
1755
+ const hasFile = (name) => files.some(f => f.relPath.toLowerCase() === name.toLowerCase());
1756
+
1757
+ if (hasFile('package-lock.json')) signals.push('npm');
1758
+ else if (hasFile('yarn.lock')) signals.push('Yarn');
1759
+ else if (hasFile('pnpm-lock.yaml')) signals.push('pnpm');
1760
+ else if (hasFile('bun.lockb')) signals.push('Bun');
1761
+
1762
+ if (hasFile('requirements.txt')) signals.push('pip');
1763
+ if (hasFile('poetry.lock')) signals.push('Poetry');
1764
+ if (hasFile('cargo.lock')) signals.push('Cargo');
1765
+
1766
+ return signals;
1767
+ }
1768
+
1769
+ function detectAiDevOsSignals(files) {
1770
+ const signals = [];
1771
+ const hasFile = (name) => files.some(f => f.relPath.toLowerCase() === name.toLowerCase());
1772
+
1773
+ if (hasFile('agents.md')) signals.push('AGENTS.md');
1774
+ if (hasFile('memory.md')) signals.push('MEMORY.md');
1775
+ if (hasFile('tasks.md')) signals.push('TASKS.md');
1776
+ if (hasFile('runbook.md')) signals.push('RUNBOOK.md');
1777
+ if (hasFile('.ai/config.yaml')) signals.push('.ai/config.yaml');
1778
+
1779
+ const hasPrefix = (prefix) => files.some(f => f.relPath.startsWith(prefix));
1780
+ if (hasPrefix('.ai/templates/')) signals.push('Templates Registry');
1781
+ if (hasPrefix('.ai/adapters/')) signals.push('Adapters Registry');
1782
+ if (hasPrefix('.ai/skills/')) signals.push('Skills Registry');
1783
+ if (hasPrefix('.ai/intelligence/')) signals.push('Intelligence Layer');
1784
+ if (hasPrefix('.ai/policies/')) signals.push('Policy Layer');
1785
+ if (hasPrefix('.ai/registries/')) signals.push('Registry Layer');
1786
+
1787
+ return signals;
1788
+ }
1789
+
1790
+ function detectRisks(files, targetDir) {
1791
+ const risks = [];
1792
+ const gitignorePath = join(targetDir, '.gitignore');
1793
+ const gitignoreContent = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
1794
+
1795
+ const hasFolder = (name) => files.some(f => f.relPath.split('/')[0] === name);
1796
+
1797
+ if (hasFolder('node_modules') && !gitignoreContent.includes('node_modules')) {
1798
+ risks.push({
1799
+ file_pattern: 'node_modules/',
1800
+ risk_description: 'Large token-sink directory node_modules/ is present but not ignored in .gitignore.',
1801
+ severity: 'high'
1802
+ });
1803
+ }
1804
+
1805
+ files.forEach(f => {
1806
+ if (f.relPath.endsWith('.json') && f.relPath.toLowerCase().includes('config') && f.size > 50000) {
1807
+ risks.push({
1808
+ file_pattern: f.relPath,
1809
+ risk_description: `Large config file (${(f.size / 1024).toFixed(1)} KB) might contain sensitive parameters or inflate prompt context.`,
1810
+ severity: 'medium'
1811
+ });
1812
+ }
1813
+ });
1814
+
1815
+ return risks;
1816
+ }
1817
+
1818
+ function buildMemoryIndex(targetDir) {
1819
+ const { files, ignoredCount } = scanTarget(targetDir);
1820
+ const framework_signals = detectFrameworkSignals(files, targetDir);
1821
+ const dependency_signals = detectDependencySignals(files, targetDir);
1822
+ const ai_dev_os_signals = detectAiDevOsSignals(files);
1823
+ const risks = detectRisks(files, targetDir);
1824
+
1825
+ const file_fingerprints = {};
1826
+ files.forEach(f => {
1827
+ file_fingerprints[f.relPath] = {
1828
+ hash: hashFile(f.fullPath),
1829
+ size: f.size,
1830
+ last_modified: f.mtime
1831
+ };
1832
+ });
1833
+
1834
+ const recommended_next_steps = [];
1835
+ if (ai_dev_os_signals.length === 0) {
1836
+ recommended_next_steps.push('Run init to bootstrap MultiModel Dev OS.');
1837
+ }
1838
+ if (risks.some(r => r.severity === 'high')) {
1839
+ recommended_next_steps.push('Address Gitignore configuration to exclude large directories (node_modules/ or build artifacts).');
1840
+ }
1841
+ recommended_next_steps.push('Use validate or doctor to check structural integrity.');
1842
+ recommended_next_steps.push('Commit the .ai/ intelligence policies to share constraints across AI agents.');
1843
+
1844
+ return {
1845
+ generated_at: new Date().toISOString(),
1846
+ project_root: targetDir.replace(/\\/g, '/'),
1847
+ file_count: files.length,
1848
+ ignored_count: ignoredCount,
1849
+ file_fingerprints,
1850
+ framework_signals,
1851
+ dependency_signals,
1852
+ ai_dev_os_signals,
1853
+ risks,
1854
+ recommended_next_steps
1855
+ };
1856
+ }
1857
+
1858
+ function writeMemoryFiles(targetDir, index) {
1859
+ const intelDir = join(targetDir, '.ai', 'intelligence');
1860
+ if (!existsSync(intelDir)) {
1861
+ mkdirSync(intelDir, { recursive: true });
1862
+ }
1863
+
1864
+ const hashJsonPath = join(intelDir, 'memory.hash.json');
1865
+ writeFileSync(hashJsonPath, JSON.stringify(index, null, 2), 'utf8');
1866
+
1867
+ const summaryMdPath = join(intelDir, 'memory.summary.md');
1868
+
1869
+ let md = `# MultiModel Dev OS Repository Memory Summary\n\n`;
1870
+ md += `**Generated At:** ${index.generated_at}\n`;
1871
+ md += `**Project Root:** ${index.project_root}\n`;
1872
+ md += `**Total Files:** ${index.file_count} (Ignored: ${index.ignored_count})\n\n`;
1873
+
1874
+ md += `## Framework & Environment Signals\n`;
1875
+ md += `- **Frameworks/Languages:** ${index.framework_signals.join(', ') || 'None'}\n`;
1876
+ md += `- **Package Manager/Build:** ${index.dependency_signals.join(', ') || 'None'}\n`;
1877
+ md += `- **AI Dev OS Integration:** ${index.ai_dev_os_signals.join(', ') || 'None'}\n\n`;
1878
+
1879
+ md += `## Codebase Fingerprints\n`;
1880
+ md += `| File Path | Size (Bytes) | Hash (SHA-256) |\n`;
1881
+ md += `|---|---|---|\n`;
1882
+
1883
+ const entries = Object.entries(index.file_fingerprints);
1884
+ entries.forEach(([filePath, fp]) => {
1885
+ md += `| ${filePath} | ${fp.size} | \`${fp.hash.substring(0, 12)}...\` |\n`;
1886
+ });
1887
+ md += `\n`;
1888
+
1889
+ if (index.risks.length > 0) {
1890
+ md += `## Detected Risks\n`;
1891
+ index.risks.forEach(r => {
1892
+ md += `- **[${r.severity.toUpperCase()}]** \`${r.file_pattern}\`: ${r.risk_description}\n`;
1893
+ });
1894
+ md += `\n`;
1895
+ }
1896
+
1897
+ md += `## Recommended Next Steps\n`;
1898
+ index.recommended_next_steps.forEach(step => {
1899
+ md += `- ${step}\n`;
1900
+ });
1901
+
1902
+ writeFileSync(summaryMdPath, md, 'utf8');
1903
+ }
1904
+
1905
+ function diffMemory(targetDir) {
1906
+ const hashJsonPath = join(targetDir, '.ai', 'intelligence', 'memory.hash.json');
1907
+ if (!existsSync(hashJsonPath)) {
1908
+ return null;
1909
+ }
1910
+
1911
+ let existing;
1912
+ try {
1913
+ existing = JSON.parse(readFileSync(hashJsonPath, 'utf8'));
1914
+ } catch (e) {
1915
+ return null;
1916
+ }
1917
+
1918
+ const currentScan = buildMemoryIndex(targetDir);
1919
+
1920
+ const added = [];
1921
+ const removed = [];
1922
+ const changed = [];
1923
+ let unchangedCount = 0;
1924
+
1925
+ const currentFp = currentScan.file_fingerprints;
1926
+ const existingFp = existing.file_fingerprints || {};
1927
+
1928
+ Object.keys(currentFp).forEach(file => {
1929
+ if (!existingFp[file]) {
1930
+ added.push(file);
1931
+ } else if (existingFp[file].hash !== currentFp[file].hash || existingFp[file].size !== currentFp[file].size) {
1932
+ changed.push(file);
1933
+ } else {
1934
+ unchangedCount++;
1935
+ }
1936
+ });
1937
+
1938
+ Object.keys(existingFp).forEach(file => {
1939
+ if (!currentFp[file]) {
1940
+ removed.push(file);
1941
+ }
1942
+ });
1943
+
1944
+ return { added, removed, changed, unchangedCount, currentScan };
1945
+ }
1946
+
1947
+ function handleScan(options) {
1948
+ console.log(`\n🔍 \x1b[36mCodebase Scan target: ${options.target}\x1b[0m`);
1949
+ console.log('==================================================');
1950
+
1951
+ const { files, ignoredCount } = scanTarget(options.target);
1952
+ const frameworkSignals = detectFrameworkSignals(files, options.target);
1953
+ const dependencySignals = detectDependencySignals(files, options.target);
1954
+ const aiDevOsSignals = detectAiDevOsSignals(files);
1955
+ const risks = detectRisks(files, options.target);
1956
+
1957
+ console.log(`\n\x1b[33mProject Stats:\x1b[0m`);
1958
+ console.log(` File Count: ${files.length}`);
1959
+ console.log(` Ignored Files: ${ignoredCount}`);
1960
+
1961
+ console.log(`\n\x1b[33mFramework & Language Signals:\x1b[0m`);
1962
+ frameworkSignals.forEach(sig => console.log(` - ${sig}`));
1963
+
1964
+ console.log(`\n\x1b[33mPackage Manager & Dependency Signals:\x1b[0m`);
1965
+ dependencySignals.forEach(sig => console.log(` - ${sig}`));
1966
+
1967
+ console.log(`\n\x1b[33mMultiModel Dev OS Files:\x1b[0m`);
1968
+ if (aiDevOsSignals.length > 0) {
1969
+ aiDevOsSignals.forEach(sig => console.log(` - ${sig}`));
1970
+ } else {
1971
+ console.log(` No MultiModel Dev OS files detected. Run \x1b[36mmit --template general-app\x1b[0m to initialize.`);
1972
+ }
1973
+
1974
+ if (risks.length > 0) {
1975
+ console.log(`\n\x1b[31mDetected Risks:\x1b[0m`);
1976
+ risks.forEach(r => console.log(` - [${r.severity.toUpperCase()}] ${r.file_pattern}: ${r.risk_description}`));
1977
+ } else {
1978
+ console.log(`\n\x1b[32m✔ No high/medium risks detected in repository structure.\x1b[0m`);
1979
+ }
1980
+
1981
+ console.log();
1982
+ }
1983
+
1984
+ function handleMemoryBuild(options) {
1985
+ console.log(`\n🧠 \x1b[36mBuilding Codebase Memory in: ${options.target}\x1b[0m`);
1986
+ console.log('==================================================');
1987
+
1988
+ const index = buildMemoryIndex(options.target);
1989
+ writeMemoryFiles(options.target, index);
1990
+
1991
+ console.log(` \x1b[32mCREATE:\x1b[0m .ai/intelligence/memory.hash.json`);
1992
+ console.log(` \x1b[32mCREATE:\x1b[0m .ai/intelligence/memory.summary.md`);
1993
+ console.log(`\n✔ Memory index built successfully! [Files indexed: ${index.file_count}]`);
1994
+
1995
+ console.log(`\n\x1b[33mRecommended Next Steps:\x1b[0m`);
1996
+ index.recommended_next_steps.forEach(step => console.log(` - ${step}`));
1997
+ console.log();
1998
+ }
1999
+
2000
+ function handleMemoryRefresh(options) {
2001
+ console.log(`\n🧠 \x1b[36mRefreshing Codebase Memory in: ${options.target}\x1b[0m`);
2002
+ console.log('==================================================');
2003
+
2004
+ const diff = diffMemory(options.target);
2005
+ if (!diff) {
2006
+ console.log(' No existing memory index found. Building fresh index...');
2007
+ handleMemoryBuild(options);
2008
+ return;
2009
+ }
2010
+
2011
+ writeMemoryFiles(options.target, diff.currentScan);
2012
+
2013
+ console.log(` \x1b[32mUPDATE:\x1b[0m .ai/intelligence/memory.hash.json`);
2014
+ console.log(` \x1b[32mUPDATE:\x1b[0m .ai/intelligence/memory.summary.md`);
2015
+
2016
+ console.log(`\n✔ Memory index refreshed successfully!`);
2017
+ console.log(` Added: ${diff.added.length}`);
2018
+ console.log(` Removed: ${diff.removed.length}`);
2019
+ console.log(` Changed: ${diff.changed.length}`);
2020
+ console.log(` Unchanged: ${diff.unchangedCount}`);
2021
+ console.log();
2022
+ }
2023
+
2024
+ function handleMemoryDiff(options) {
2025
+ console.log(`\n🧠 \x1b[36mDiffing Codebase State against Memory in: ${options.target}\x1b[0m`);
2026
+ console.log('==================================================');
2027
+
2028
+ const diff = diffMemory(options.target);
2029
+ if (!diff) {
2030
+ console.error(`\x1b[31mError: No existing memory index found. Run 'memory build' first.\x1b[0m\n`);
2031
+ if (options && options.noExit) return false;
2032
+ process.exit(1);
2033
+ }
2034
+
2035
+ console.log(`\n\x1b[33mMemory Diff Summary:\x1b[0m`);
2036
+ console.log(` Added Files: ${diff.added.length}`);
2037
+ console.log(` Removed Files: ${diff.removed.length}`);
2038
+ console.log(` Changed Files: ${diff.changed.length}`);
2039
+ console.log(` Unchanged: ${diff.unchangedCount}`);
2040
+
2041
+ if (diff.added.length > 0) {
2042
+ console.log(`\n\x1b[32mAdded Files:\x1b[0m`);
2043
+ diff.added.forEach(f => console.log(` + ${f}`));
2044
+ }
2045
+ if (diff.removed.length > 0) {
2046
+ console.log(`\n\x1b[31mRemoved Files:\x1b[0m`);
2047
+ diff.removed.forEach(f => console.log(` - ${f}`));
2048
+ }
2049
+ if (diff.changed.length > 0) {
2050
+ console.log(`\n\x1b[33mChanged Files:\x1b[0m`);
2051
+ diff.changed.forEach(f => console.log(` M ${f}`));
2052
+ }
2053
+
2054
+ console.log();
2055
+ }
2056
+
2057
+ function handleFeedbackAdd(options) {
2058
+ const intelDir = join(options.target, '.ai', 'intelligence');
2059
+ if (!options.dryRun && !existsSync(intelDir)) {
2060
+ mkdirSync(intelDir, { recursive: true });
2061
+ }
2062
+
2063
+ const addIdx = process.argv.indexOf('add');
2064
+ const text = (addIdx !== -1 && process.argv[addIdx + 1] && !process.argv[addIdx + 1].startsWith('-')) ? process.argv[addIdx + 1] : null;
2065
+
2066
+ if (!text) {
2067
+ console.error(`\x1b[31mError: Please provide feedback text.\x1b[0m`);
2068
+ console.log(`Example: node bin/multimodel-dev-os.js feedback add "Prefer CSS modules"`);
2069
+ process.exit(1);
2070
+ }
2071
+
2072
+ const uuid = createHash('md5').update(new Date().toISOString() + Math.random().toString()).digest('hex').substring(0, 16);
2073
+ const tagsStr = options.tags || '';
2074
+ const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()) : [];
2075
+ const filesStr = options.files || '';
2076
+ const related_files = filesStr ? filesStr.split(',').map(f => f.trim()) : [];
2077
+
2078
+ const rawRecord = {
2079
+ id: `fb-${uuid}`,
2080
+ created_at: new Date().toISOString(),
2081
+ source: 'user',
2082
+ type: options.type || 'unknown',
2083
+ text: text,
2084
+ tags: tags,
2085
+ related_files: related_files
2086
+ };
2087
+
2088
+ rawRecord.hash = createHash('sha256').update(JSON.stringify(rawRecord)).digest('hex');
2089
+
2090
+ const recordLine = JSON.stringify(rawRecord) + '\n';
2091
+ const feedbackLogPath = join(intelDir, 'feedback-log.jsonl');
2092
+
2093
+ if (options.dryRun) {
2094
+ console.log(`\x1b[36m[DRY-RUN] WOULD APPEND TO ${feedbackLogPath}:\x1b[0m`);
2095
+ console.log(recordLine.trim());
2096
+ } else {
2097
+ try {
2098
+ let isDuplicate = false;
2099
+ if (existsSync(feedbackLogPath)) {
2100
+ const lines = readFileSync(feedbackLogPath, 'utf8').split('\n');
2101
+ for (const line of lines) {
2102
+ if (!line.trim()) continue;
2103
+ try {
2104
+ const entry = JSON.parse(line);
2105
+ if (entry.text === text && JSON.stringify(entry.related_files) === JSON.stringify(related_files)) {
2106
+ isDuplicate = true;
2107
+ break;
2108
+ }
2109
+ } catch (e) {}
2110
+ }
2111
+ }
2112
+ if (isDuplicate) {
2113
+ console.log(`\x1b[33mFeedback already exists. Skipping duplicate entry.\x1b[0m`);
2114
+ return;
2115
+ }
2116
+
2117
+ writeFileSync(feedbackLogPath, recordLine, { flag: 'a', encoding: 'utf8' });
2118
+ console.log(`✔ Feedback successfully added (ID: ${rawRecord.id})`);
2119
+ } catch (e) {
2120
+ console.error(`\x1b[31mError: Failed to write to feedback-log.jsonl: ${e.message}\x1b[0m`);
2121
+ process.exit(1);
2122
+ }
2123
+ }
2124
+ }
2125
+
2126
+ function handleFeedbackList(options) {
2127
+ const feedbackLogPath = join(options.target, '.ai', 'intelligence', 'feedback-log.jsonl');
2128
+ if (!existsSync(feedbackLogPath)) {
2129
+ console.log('No feedback logged yet.');
2130
+ return;
2131
+ }
2132
+
2133
+ try {
2134
+ const content = readFileSync(feedbackLogPath, 'utf8');
2135
+ const lines = content.split('\n').filter(l => l.trim() !== '');
2136
+ if (lines.length === 0) {
2137
+ console.log('No feedback logged yet.');
2138
+ return;
2139
+ }
2140
+
2141
+ console.log(`\n🧠 \x1b[36mLogged Feedback Entries\x1b[0m`);
2142
+ console.log('==================================================');
2143
+ lines.forEach(line => {
2144
+ try {
2145
+ const entry = JSON.parse(line);
2146
+ console.log(`\n\x1b[32m* [${entry.type || 'unknown'}] (${entry.id})\x1b[0m`);
2147
+ console.log(` \x1b[37mText:\x1b[0m ${entry.text}`);
2148
+ if (entry.tags && entry.tags.length > 0) {
2149
+ console.log(` \x1b[33mTags:\x1b[0m ${entry.tags.join(', ')}`);
2150
+ }
2151
+ if (entry.related_files && entry.related_files.length > 0) {
2152
+ console.log(` \x1b[33mFiles:\x1b[0m ${entry.related_files.join(', ')}`);
2153
+ }
2154
+ console.log(` \x1b[33mLogged:\x1b[0m ${entry.created_at}`);
2155
+ } catch (e) {}
2156
+ });
2157
+ console.log();
2158
+ } catch (e) {
2159
+ console.error(`\x1b[31mError: Failed to read feedback log: ${e.message}\x1b[0m`);
2160
+ process.exit(1);
2161
+ }
2162
+ }
2163
+
2164
+ function handleFeedbackSummarize(options) {
2165
+ const intelDir = join(options.target, '.ai', 'intelligence');
2166
+ const feedbackLogPath = join(intelDir, 'feedback-log.jsonl');
2167
+ if (!existsSync(feedbackLogPath)) {
2168
+ console.log('No feedback logs found to compile.');
2169
+ return;
2170
+ }
2171
+
2172
+ try {
2173
+ const content = readFileSync(feedbackLogPath, 'utf8');
2174
+ const lines = content.split('\n').filter(l => l.trim() !== '');
2175
+ if (lines.length === 0) {
2176
+ console.log('No feedback logs found to compile.');
2177
+ return;
2178
+ }
2179
+
2180
+ const categories = {};
2181
+ lines.forEach(line => {
2182
+ try {
2183
+ const entry = JSON.parse(line);
2184
+ const cat = entry.type || 'general';
2185
+ if (!categories[cat]) categories[cat] = [];
2186
+ categories[cat].push(entry);
2187
+ } catch (e) {}
2188
+ });
2189
+
2190
+ let md = `# Compiled Learning Rules\n\n`;
2191
+ md += `*Generated automatically by MultiModel Dev OS. Do not modify manually.*\n\n`;
2192
+ md += `**Last compiled:** ${new Date().toISOString()}\n`;
2193
+ md += `**Total source feedback items:** ${lines.length}\n\n`;
2194
+ md += `## Active Instructions\n\n`;
2195
+
2196
+ Object.keys(categories).forEach(cat => {
2197
+ md += `### Category: ${cat}\n`;
2198
+ categories[cat].forEach(entry => {
2199
+ const pattern = entry.related_files && entry.related_files.length > 0 ? entry.related_files.join(', ') : '*';
2200
+ md += `* **Pattern:** \`${pattern}\`\n`;
2201
+ md += ` * **Rule:** ${entry.text}\n`;
2202
+ md += ` * **Source ID:** \`${entry.id}\`\n\n`;
2203
+ });
2204
+ });
2205
+
2206
+ const targetRulesPath = join(intelDir, 'learning-rules.md');
2207
+ if (options.dryRun) {
2208
+ console.log(`\x1b[36m[DRY-RUN] WOULD WRITE TO ${targetRulesPath}:\x1b[0m`);
2209
+ console.log(md);
2210
+ } else {
2211
+ writeFileSync(targetRulesPath, md, 'utf8');
2212
+ console.log(`✔ Compiled ${lines.length} feedback items into learning rules in .ai/intelligence/learning-rules.md`);
2213
+ }
2214
+ } catch (e) {
2215
+ console.error(`\x1b[31mError: Failed to compile learning rules: ${e.message}\x1b[0m`);
2216
+ process.exit(1);
2217
+ }
2218
+ }
2219
+
2220
+ function handleImprovePropose(options) {
2221
+ const proposalsDir = join(options.target, '.ai', 'proposals');
2222
+ if (!options.dryRun && !existsSync(proposalsDir)) {
2223
+ mkdirSync(proposalsDir, { recursive: true });
2224
+ }
2225
+
2226
+ const now = new Date();
2227
+ const pad = (n) => String(n).padStart(2, '0');
2228
+ const dateStr = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
2229
+ const timeStr = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
2230
+ const timestamp = `${dateStr}-${timeStr}`;
2231
+ const id = `proposal-${timestamp}`;
2232
+
2233
+ const title = options.title || 'Auto-detected codebase optimization';
2234
+ let problem = 'No specific problems detected.';
2235
+ let evidence = 'N/A';
2236
+ let riskLevel = 'low';
2237
+ let affectedFiles = [];
2238
+ let suggestedChange = 'No code suggestions compiled.';
2239
+ let verifyCommand = 'npm run verify';
2240
+ let rollbackPlan = 'git checkout -- .';
2241
+
2242
+ const gitignorePath = join(options.target, '.gitignore');
2243
+ const agentsPath = join(options.target, 'AGENTS.md');
2244
+
2245
+ if (!existsSync(gitignorePath)) {
2246
+ problem = 'Missing .gitignore file in target workspace. AI agents may scan large build directories and run out of token context.';
2247
+ evidence = `.gitignore file is not present at root directory: ${options.target}`;
2248
+ affectedFiles = ['.gitignore'];
2249
+ suggestedChange = 'Create a standard .gitignore file to exclude node_modules, build/ and dist/ directories.';
2250
+ rollbackPlan = 'git clean -fd .gitignore';
2251
+ } else if (!existsSync(agentsPath)) {
2252
+ problem = 'Missing AGENTS.md document in target workspace. Models will lack stack-specific implementation blueprints.';
2253
+ evidence = `AGENTS.md file is not present at root directory: ${options.target}`;
2254
+ affectedFiles = ['AGENTS.md'];
2255
+ suggestedChange = 'Create an AGENTS.md document specifying the codebase development guidelines and framework profiles.';
2256
+ rollbackPlan = 'git clean -fd AGENTS.md';
2257
+ } else {
2258
+ problem = 'Outdated codebase memory index. Memory files need to be refreshed to sync with recent local changes.';
2259
+ evidence = 'Current memory.hash.json represents a previous commit state.';
2260
+ affectedFiles = ['.ai/intelligence/memory.hash.json', '.ai/intelligence/memory.summary.md'];
2261
+ suggestedChange = 'Refresh codebase memory index using multimodel-dev-os memory refresh CLI command.';
2262
+ riskLevel = 'low';
2263
+ verifyCommand = 'node bin/multimodel-dev-os.js memory refresh';
2264
+ rollbackPlan = 'git checkout -- .ai/intelligence/';
2265
+ }
2266
+
2267
+ let md = `---
2268
+ id: ${id}
2269
+ created_at: ${now.toISOString()}
2270
+ title: ${title}
2271
+ problem: ${problem}
2272
+ evidence: ${evidence}
2273
+ risk_level: ${riskLevel}
2274
+ affected_files:
2275
+ `;
2276
+ affectedFiles.forEach(f => {
2277
+ md += ` - ${f}\n`;
2278
+ });
2279
+ md += `suggested_change: ${suggestedChange}
2280
+ verify_command: ${verifyCommand}
2281
+ rollback_plan: ${rollbackPlan}
2282
+ approval_status: pending
2283
+ ---
2284
+
2285
+ # Codebase Improvement Proposal: ${title}
2286
+
2287
+ > [!WARNING]
2288
+ > Manual approval is required before implementing this proposal. Edit the frontmatter metadata block to change \`approval_status\` to \`approved\` to authorize modifications.
2289
+
2290
+ ## 1. Problem Description
2291
+ ${problem}
2292
+
2293
+ ## 2. Evidence
2294
+ ${evidence}
2295
+
2296
+ ## 3. Suggested Modifications
2297
+ ${suggestedChange}
2298
+
2299
+ ## 4. Safety & Rollback Parameters
2300
+ * **Risk Level**: ${riskLevel.toUpperCase()}
2301
+ * **Verification Command**: \`${verifyCommand}\`
2302
+ * **Rollback Command**: \`${rollbackPlan}\`
2303
+ * **Approval Status**: PENDING (Manual approval required before implementation)
2304
+ `;
2305
+
2306
+ const proposalFile = join(proposalsDir, `${id}.md`);
2307
+ if (options.dryRun) {
2308
+ console.log(`\x1b[36m[DRY-RUN] WOULD WRITE PROPOSAL TO ${proposalFile}:\x1b[0m`);
2309
+ console.log(md);
2310
+ } else {
2311
+ writeFileSync(proposalFile, md, 'utf8');
2312
+ console.log(`✔ Created codebase improvement proposal: .ai/proposals/${id}.md`);
2313
+ }
2314
+ }
2315
+
2316
+ function handleImproveReview(options) {
2317
+ const proposalsDir = join(options.target, '.ai', 'proposals');
2318
+ if (!existsSync(proposalsDir)) {
2319
+ console.log('No improvement proposals found.');
2320
+ return;
2321
+ }
2322
+
2323
+ try {
2324
+ const files = readdirSync(proposalsDir).filter(f => f.startsWith('proposal-') && f.endsWith('.md'));
2325
+ if (files.length === 0) {
2326
+ console.log('No improvement proposals found.');
2327
+ return;
2328
+ }
2329
+
2330
+ console.log(`\n📋 \x1b[36mCodebase Improvement Proposals\x1b[0m`);
2331
+ console.log('==================================================');
2332
+
2333
+ files.forEach(file => {
2334
+ const fullPath = join(proposalsDir, file);
2335
+ const content = readFileSync(fullPath, 'utf8');
2336
+
2337
+ const fmMatch = content.match(/^---([\s\S]*?)---/);
2338
+ if (!fmMatch) return;
2339
+
2340
+ const fmContent = fmMatch[1];
2341
+ const metadata = parseYaml(fmContent) || {};
2342
+
2343
+ const statusColor = metadata.approval_status === 'approved' ? '\x1b[32m' : metadata.approval_status === 'rejected' ? '\x1b[31m' : '\x1b[33m';
2344
+ console.log(`\n\x1b[34m* [${metadata.id || file.replace('.md', '')}] ${metadata.title || 'Untitled'}\x1b[0m`);
2345
+ console.log(` \x1b[37mRisk Level:\x1b[0m ${metadata.risk_level || 'unknown'}`);
2346
+ console.log(` \x1b[37mStatus:\x1b[0m ${statusColor}${metadata.approval_status || 'pending'}\x1b[0m`);
2347
+ console.log(` \x1b[37mProblem:\x1b[0m ${metadata.problem || 'N/A'}`);
2348
+ if (metadata.affected_files && metadata.affected_files.length > 0) {
2349
+ console.log(` \x1b[37mAffected Files:\x1b[0m ${metadata.affected_files.join(', ')}`);
2350
+ }
2351
+ });
2352
+ console.log();
2353
+ } catch (e) {
2354
+ console.error(`\x1b[31mError: Failed to review proposals: ${e.message}\x1b[0m`);
2355
+ process.exit(1);
2356
+ }
2357
+ }
2358
+
2359
+ function handleImproveStatus(options) {
2360
+ const proposalsDir = join(options.target, '.ai', 'proposals');
2361
+ if (!existsSync(proposalsDir)) {
2362
+ console.log('Improvement Proposal Engine Status:');
2363
+ console.log(' Total Proposals: 0');
2364
+ console.log(' Pending Approval: 0');
2365
+ return;
2366
+ }
2367
+
2368
+ try {
2369
+ const files = readdirSync(proposalsDir).filter(f => f.startsWith('proposal-') && f.endsWith('.md'));
2370
+ let pending = 0;
2371
+ let approved = 0;
2372
+ let rejected = 0;
2373
+
2374
+ files.forEach(file => {
2375
+ const content = readFileSync(join(proposalsDir, file), 'utf8');
2376
+ const fmMatch = content.match(/^---([\s\S]*?)---/);
2377
+ if (fmMatch) {
2378
+ const metadata = parseYaml(fmMatch[1]) || {};
2379
+ const status = metadata.approval_status || 'pending';
2380
+ if (status === 'approved') approved++;
2381
+ else if (status === 'rejected') rejected++;
2382
+ else pending++;
2383
+ }
2384
+ });
2385
+
2386
+ console.log(`\n⚙ \x1b[36mImprovement Proposals Engine Status\x1b[0m`);
2387
+ console.log('==================================================');
2388
+ console.log(` Total Proposals: ${files.length}`);
2389
+ console.log(` Pending Approval: \x1b[33m${pending}\x1b[0m`);
2390
+ console.log(` Approved: \x1b[32m${approved}\x1b[0m`);
2391
+ console.log(` Rejected: \x1b[31m${rejected}\x1b[0m`);
2392
+ console.log();
2393
+ } catch (e) {
2394
+ console.error(`\x1b[31mError: Failed to fetch status: ${e.message}\x1b[0m`);
2395
+ process.exit(1);
2396
+ }
2397
+ }
2398
+
2399
+ function getSha256(content) {
2400
+ return createHash('sha256').update(content, 'utf8').digest('hex');
2401
+ }
2402
+
2403
+ function validatePath(targetRoot, relPath) {
2404
+ const normalizedRel = relPath.replace(/\\/g, '/');
2405
+
2406
+ if (normalizedRel.startsWith('/') || normalizedRel.includes('..')) {
2407
+ return { valid: false, reason: `Path '${relPath}' contains directory traversal or is absolute.`, type: 'outside' };
2408
+ }
2409
+
2410
+ const resolved = resolve(targetRoot, relPath);
2411
+ const relativeFromRoot = relative(targetRoot, resolved);
2412
+
2413
+ if (relativeFromRoot.startsWith('..') || isAbsolute(relativeFromRoot) || resolved === targetRoot) {
2414
+ return { valid: false, reason: `Path '${relPath}' resolves outside the target root.`, type: 'outside' };
2415
+ }
2416
+
2417
+ const parts = relativeFromRoot.replace(/\\/g, '/').split('/');
2418
+
2419
+ const protectedFolders = [
2420
+ '.git',
2421
+ 'node_modules',
2422
+ 'dist',
2423
+ 'build',
2424
+ '.next',
2425
+ 'coverage'
2426
+ ];
2427
+ for (const part of parts) {
2428
+ if (protectedFolders.includes(part)) {
2429
+ return { valid: false, reason: `Path '${relPath}' attempts to access protected directory '${part}/'.`, type: 'protected' };
2430
+ }
2431
+ }
2432
+
2433
+ const cleanRelativeFromRoot = relativeFromRoot.replace(/\\/g, '/');
2434
+ if (cleanRelativeFromRoot.startsWith('docs/.vitepress/dist') || cleanRelativeFromRoot.startsWith('docs/.vitepress/cache')) {
2435
+ return { valid: false, reason: `Path '${relPath}' attempts to access protected vitepress path.`, type: 'protected' };
2436
+ }
2437
+
2438
+ const filename = parts[parts.length - 1];
2439
+ if (filename === '.env' || filename.startsWith('.env.') || filename === '.npmrc' || filename === 'credentials.json' || filename === 'package-lock.json' || filename === 'apply-log.jsonl') {
2440
+ return { valid: false, reason: `Path '${relPath}' targets a protected config/secret file.`, type: 'protected' };
2441
+ }
2442
+ if (filename.endsWith('.pem') || filename.endsWith('.key') || filename.endsWith('.jks') || filename.endsWith('.keystore')) {
2443
+ return { valid: false, reason: `Path '${relPath}' targets a protected key/certificate file.`, type: 'protected' };
2444
+ }
2445
+
2446
+ return { valid: true, resolved };
2447
+ }
2448
+
2449
+ function validateProposal(proposalFile, targetRoot) {
2450
+ const gates = {
2451
+ frontmatter: { status: 'skip' },
2452
+ approval: { status: 'skip' },
2453
+ json: { status: 'skip' },
2454
+ types: { status: 'skip' },
2455
+ boundaries: { status: 'skip' },
2456
+ permissions: { status: 'skip' },
2457
+ constraints: { status: 'skip' }
2458
+ };
2459
+
2460
+ if (!existsSync(proposalFile)) {
2461
+ gates.frontmatter = { status: 'fail', reason: 'missing frontmatter' };
2462
+ return { valid: false, reason: 'missing frontmatter', gates };
2463
+ }
2464
+
2465
+ const content = readFileSync(proposalFile, 'utf8');
2466
+ const fmMatch = content.match(/^---([\s\S]*?)---/);
2467
+ if (!fmMatch) {
2468
+ gates.frontmatter = { status: 'fail', reason: 'missing frontmatter' };
2469
+ return { valid: false, reason: 'missing frontmatter', gates };
2470
+ }
2471
+ const fmContent = fmMatch[1];
2472
+ const metadata = parseYaml(fmContent);
2473
+ if (!metadata || typeof metadata !== 'object') {
2474
+ gates.frontmatter = { status: 'fail', reason: 'missing frontmatter' };
2475
+ return { valid: false, reason: 'missing frontmatter', gates };
2476
+ }
2477
+
2478
+ gates.frontmatter = { status: 'pass' };
2479
+ const proposalId = metadata.id || basename(proposalFile, '.md');
2480
+ const proposalTitle = metadata.title || 'Untitled Proposal';
2481
+ const proposalStatus = metadata.approval_status || 'pending';
2482
+
2483
+ const isApproved = (metadata.approval_status === 'approved');
2484
+ gates.approval = isApproved ? { status: 'pass' } : { status: 'fail', reason: 'approval_status not approved' };
2485
+
2486
+ const body = content.substring(fmMatch[0].length);
2487
+ const jsonBlockRegex = /```json\s*\n([\s\S]*?)\n\s*```/;
2488
+ const jsonMatch = body.match(jsonBlockRegex);
2489
+
2490
+ let operationsData = null;
2491
+ if (!jsonMatch) {
2492
+ gates.json = { status: 'fail', reason: 'no operations block' };
2493
+ } else {
2494
+ try {
2495
+ operationsData = JSON.parse(jsonMatch[1]);
2496
+ if (!operationsData || !Array.isArray(operationsData.operations) || operationsData.operations.length === 0) {
2497
+ gates.json = { status: 'fail', reason: 'no operations block' };
2498
+ } else {
2499
+ gates.json = { status: 'pass' };
2500
+ }
2501
+ } catch (e) {
2502
+ gates.json = { status: 'fail', reason: 'invalid JSON operations block' };
2503
+ }
2504
+ }
2505
+
2506
+ if (gates.json.status !== 'pass') {
2507
+ const gateOrder = ['frontmatter', 'approval', 'json', 'types', 'boundaries', 'permissions', 'constraints'];
2508
+ let firstFailReason = null;
2509
+ for (const g of gateOrder) {
2510
+ if (gates[g].status === 'fail') {
2511
+ firstFailReason = gates[g].reason;
2512
+ break;
2513
+ }
2514
+ }
2515
+ return {
2516
+ valid: false,
2517
+ reason: firstFailReason,
2518
+ gates,
2519
+ proposalId,
2520
+ proposalTitle,
2521
+ proposalStatus,
2522
+ operations: []
2523
+ };
2524
+ }
2525
+
2526
+ let typesStatus = 'pass';
2527
+ let typesReason = '';
2528
+ let boundariesStatus = 'pass';
2529
+ let boundariesReason = '';
2530
+ let permissionsStatus = 'pass';
2531
+ let permissionsReason = '';
2532
+ let constraintsStatus = 'pass';
2533
+ let constraintsReason = '';
2534
+
2535
+ const validatedOperations = [];
2536
+ const operations = operationsData.operations;
2537
+
2538
+ for (let idx = 0; idx < operations.length; idx++) {
2539
+ const op = operations[idx];
2540
+ if (!op || typeof op !== 'object' || !op.type) {
2541
+ if (typesStatus === 'pass') {
2542
+ typesStatus = 'fail';
2543
+ typesReason = `unsupported operation type`;
2544
+ }
2545
+ continue;
2546
+ }
2547
+
2548
+ const allowedTypes = ['create_file', 'append_line', 'replace_text'];
2549
+ if (!allowedTypes.includes(op.type)) {
2550
+ if (typesStatus === 'pass') {
2551
+ typesStatus = 'fail';
2552
+ typesReason = `unsupported operation type`;
2553
+ }
2554
+ continue;
2555
+ }
2556
+
2557
+ if (typeof op.path !== 'string' || !op.path.trim()) {
2558
+ if (boundariesStatus === 'pass') {
2559
+ boundariesStatus = 'fail';
2560
+ boundariesReason = `path outside target`;
2561
+ }
2562
+ continue;
2563
+ }
2564
+
2565
+ const pathVal = validatePath(targetRoot, op.path);
2566
+ if (!pathVal.valid) {
2567
+ if (pathVal.type === 'outside') {
2568
+ if (boundariesStatus === 'pass') {
2569
+ boundariesStatus = 'fail';
2570
+ boundariesReason = `path outside target`;
2571
+ }
2572
+ } else if (pathVal.type === 'protected') {
2573
+ if (permissionsStatus === 'pass') {
2574
+ permissionsStatus = 'fail';
2575
+ permissionsReason = `protected path`;
2576
+ }
2577
+ }
2578
+ continue;
2579
+ }
2580
+ const resolvedPath = pathVal.resolved;
2581
+
2582
+ if (op.type === 'create_file') {
2583
+ if (typeof op.content !== 'string') {
2584
+ if (constraintsStatus === 'pass') {
2585
+ constraintsStatus = 'fail';
2586
+ constraintsReason = `unsupported operation type`; // Treated as malformed/unsupported or type logic error
2587
+ }
2588
+ } else if (existsSync(resolvedPath) && !op.overwrite) {
2589
+ if (constraintsStatus === 'pass') {
2590
+ constraintsStatus = 'fail';
2591
+ constraintsReason = `create_file target exists without overwrite`;
2592
+ }
2593
+ }
2594
+ } else if (op.type === 'append_line') {
2595
+ if (typeof op.line !== 'string') {
2596
+ if (constraintsStatus === 'pass') {
2597
+ constraintsStatus = 'fail';
2598
+ constraintsReason = `unsupported operation type`;
2599
+ }
2600
+ }
2601
+ } else if (op.type === 'replace_text') {
2602
+ if (typeof op.find !== 'string' || typeof op.replace !== 'string') {
2603
+ if (constraintsStatus === 'pass') {
2604
+ constraintsStatus = 'fail';
2605
+ constraintsReason = `unsupported operation type`;
2606
+ }
2607
+ } else if (!existsSync(resolvedPath)) {
2608
+ if (constraintsStatus === 'pass') {
2609
+ constraintsStatus = 'fail';
2610
+ constraintsReason = `replace_text zero matches`; // file does not exist, so zero matches
2611
+ }
2612
+ } else {
2613
+ const fileContent = readFileSync(resolvedPath, 'utf8');
2614
+ let count = 0;
2615
+ let pos = fileContent.indexOf(op.find);
2616
+ while (pos !== -1) {
2617
+ count++;
2618
+ pos = fileContent.indexOf(op.find, pos + op.find.length);
2619
+ }
2620
+
2621
+ if (count === 0) {
2622
+ if (constraintsStatus === 'pass') {
2623
+ constraintsStatus = 'fail';
2624
+ constraintsReason = `replace_text zero matches`;
2625
+ }
2626
+ } else if (count > 1 && !op.allow_multiple) {
2627
+ if (constraintsStatus === 'pass') {
2628
+ constraintsStatus = 'fail';
2629
+ constraintsReason = `replace_text multiple matches without allow_multiple`;
2630
+ }
2631
+ }
2632
+ }
2633
+ }
2634
+
2635
+ validatedOperations.push({
2636
+ ...op,
2637
+ resolvedPath
2638
+ });
2639
+ }
2640
+
2641
+ gates.types = { status: typesStatus, reason: typesReason };
2642
+ gates.boundaries = { status: boundariesStatus, reason: boundariesReason };
2643
+ gates.permissions = { status: permissionsStatus, reason: permissionsReason };
2644
+ gates.constraints = { status: constraintsStatus, reason: constraintsReason };
2645
+
2646
+ const gateOrder = ['frontmatter', 'approval', 'json', 'types', 'boundaries', 'permissions', 'constraints'];
2647
+ let firstFailReason = null;
2648
+ for (const g of gateOrder) {
2649
+ if (gates[g].status === 'fail') {
2650
+ firstFailReason = gates[g].reason;
2651
+ break;
2652
+ }
2653
+ }
2654
+
2655
+ const valid = (firstFailReason === null);
2656
+ return {
2657
+ valid,
2658
+ reason: firstFailReason,
2659
+ gates,
2660
+ proposalId,
2661
+ proposalTitle,
2662
+ proposalStatus,
2663
+ operations: valid ? validatedOperations : []
2664
+ };
2665
+ }
2666
+
2667
+ function handleImproveValidate(proposalFile, options) {
2668
+ console.log(`🛡 \x1b[34mValidating improvement proposal: ${proposalFile}\x1b[0m\n`);
2669
+ const validation = validateProposal(proposalFile, options.target);
2670
+
2671
+ if (validation.proposalId) {
2672
+ console.log(`Proposal ID: \x1b[33m${validation.proposalId}\x1b[0m`);
2673
+ console.log(`Title: \x1b[37m${validation.proposalTitle}\x1b[0m`);
2674
+ console.log(`Status: ${validation.proposalStatus === 'approved' ? '\x1b[32m' : '\x1b[31m'}${validation.proposalStatus}\x1b[0m\n`);
2675
+ }
2676
+
2677
+ console.log(`Safety Gate Checklist:`);
2678
+
2679
+ const gateLabels = {
2680
+ frontmatter: 'Frontmatter Metadata',
2681
+ approval: 'Approval Status',
2682
+ json: 'Operations JSON Block',
2683
+ types: 'Operation Type Safety',
2684
+ boundaries: 'Path Boundaries (Within Target Root)',
2685
+ permissions: 'Path Permissions (No Protected Paths)',
2686
+ constraints: 'Operation Constraints (Overwrites & Replacements)'
2687
+ };
2688
+
2689
+ const gateOrder = ['frontmatter', 'approval', 'json', 'types', 'boundaries', 'permissions', 'constraints'];
2690
+
2691
+ gateOrder.forEach(g => {
2692
+ const gate = validation.gates[g];
2693
+ const label = gateLabels[g];
2694
+ if (gate.status === 'pass') {
2695
+ console.log(` \x1b[32m[✓]\x1b[0m ${label}`);
2696
+ } else if (gate.status === 'fail') {
2697
+ console.log(` \x1b[31m[✗]\x1b[0m ${label} - \x1b[31m${gate.reason}\x1b[0m`);
2698
+ } else {
2699
+ console.log(` \x1b[37m[-]\x1b[0m ${label}`);
2700
+ }
2701
+ });
2702
+ console.log();
2703
+
2704
+ if (!validation.valid) {
2705
+ console.error(`\x1b[31mValidation FAILED: ${validation.reason}\x1b[0m`);
2706
+ console.error(`\x1b[33mActionable Fix:\x1b[0m`);
2707
+ if (validation.reason === 'missing frontmatter') {
2708
+ console.error(` Please verify that the proposal file contains a valid YAML frontmatter block at the very top delimited by '---'.`);
2709
+ } else if (validation.reason === 'approval_status not approved') {
2710
+ console.error(` The proposal approval status is not set to 'approved'. Edit the frontmatter block and set 'approval_status: approved'.`);
2711
+ } else if (validation.reason === 'no operations block') {
2712
+ console.error(` No valid operations JSON block was found. Ensure a \`\`\`json block exists containing an "operations" array.`);
2713
+ } else if (validation.reason === 'invalid JSON operations block') {
2714
+ console.error(` The operations block inside \`\`\`json is not valid JSON. Run it through a JSON validator to fix syntax errors.`);
2715
+ } else if (validation.reason === 'unsupported operation type') {
2716
+ console.error(` An operation type is disallowed. Allowed types are: 'create_file', 'append_line', 'replace_text'.`);
2717
+ } else if (validation.reason === 'protected path') {
2718
+ console.error(` An operation targets a protected directory (like .git, node_modules) or configuration file (like .env, .npmrc, apply-log.jsonl).`);
2719
+ } else if (validation.reason === 'path outside target') {
2720
+ console.error(` An operation path tries to escape the target directory using directory traversal (..) or absolute paths.`);
2721
+ } else if (validation.reason === 'replace_text zero matches') {
2722
+ console.error(` The 'find' text specified in a replace_text operation was not found in the target file.`);
2723
+ } else if (validation.reason === 'replace_text multiple matches without allow_multiple') {
2724
+ console.error(` The 'find' text matched multiple times. Set 'allow_multiple: true' if you want to replace all occurrences.`);
2725
+ } else if (validation.reason === 'create_file target exists without overwrite') {
2726
+ console.error(` The target file already exists. Set 'overwrite: true' in the operation to allow overwriting.`);
2727
+ } else {
2728
+ console.error(` Check the proposal constraints and make sure all target files and fields are correct.`);
2729
+ }
2730
+ console.error();
2731
+ process.exit(1);
2732
+ }
2733
+
2734
+ console.log(`\x1b[32m✔ Proposal is VALID and ready to be applied. ${validation.operations.length} operations parsed successfully.\x1b[0m\n`);
2735
+ process.exit(0);
2736
+ }
2737
+
2738
+ function handleImproveDiff(proposalFile, options) {
2739
+ console.log(`🔍 \x1b[36mGenerating diff for proposal: ${proposalFile}\x1b[0m\n`);
2740
+ const validation = validateProposal(proposalFile, options.target);
2741
+ if (!validation.valid) {
2742
+ console.error(`\x1b[31mValidation FAILED: ${validation.reason}\x1b[0m`);
2743
+ process.exit(1);
2744
+ }
2745
+
2746
+ const operations = validation.operations;
2747
+
2748
+ let createCount = 0;
2749
+ let appendCount = 0;
2750
+ let replaceCount = 0;
2751
+ const affectedFilesSet = new Set();
2752
+
2753
+ operations.forEach(op => {
2754
+ affectedFilesSet.add(op.path);
2755
+ if (op.type === 'create_file') createCount++;
2756
+ else if (op.type === 'append_line') appendCount++;
2757
+ else if (op.type === 'replace_text') replaceCount++;
2758
+ });
2759
+
2760
+ console.log(`Summary of Planned Changes:`);
2761
+ console.log(`---------------------------`);
2762
+ console.log(`Total Operations: \x1b[33m${operations.length}\x1b[0m`);
2763
+ console.log(`Operations Count: \x1b[32m${createCount} Create\x1b[0m, \x1b[33m${appendCount} Append\x1b[0m, \x1b[35m${replaceCount} Replace\x1b[0m`);
2764
+ console.log(`Affected Files (${affectedFilesSet.size}):`);
2765
+ affectedFilesSet.forEach(f => console.log(` - ${f}`));
2766
+ console.log();
2767
+
2768
+ const printTruncatedLines = (content, prefix, colorCode) => {
2769
+ const lines = content.split(/\r?\n/);
2770
+ const maxLines = 5;
2771
+ for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
2772
+ console.log(`${colorCode}${prefix} ${lines[i]}\x1b[0m`);
2773
+ }
2774
+ if (lines.length > maxLines) {
2775
+ console.log(`${colorCode}${prefix} ... (${lines.length - maxLines} more lines)\x1b[0m`);
2776
+ }
2777
+ };
2778
+
2779
+ const types = ['create_file', 'append_line', 'replace_text'];
2780
+ const typeHeaders = {
2781
+ create_file: '--- CREATE_FILE OPERATIONS ---',
2782
+ append_line: '--- APPEND_LINE OPERATIONS ---',
2783
+ replace_text: '--- REPLACE_TEXT OPERATIONS ---'
2784
+ };
2785
+
2786
+ types.forEach(type => {
2787
+ const typeOps = operations.filter(op => op.type === type);
2788
+ if (typeOps.length === 0) return;
2789
+
2790
+ console.log(`\x1b[36m\x1b[1m${typeHeaders[type]}\x1b[0m`);
2791
+ typeOps.forEach(op => {
2792
+ const idx = operations.indexOf(op);
2793
+ console.log(`\n\x1b[33m[Operation #${idx + 1}] Target: ${op.path}\x1b[0m`);
2794
+
2795
+ if (type === 'create_file') {
2796
+ const exists = existsSync(op.resolvedPath);
2797
+ if (exists) {
2798
+ console.log(` \x1b[31m⚠️ [Overwriting existing file]\x1b[0m`);
2799
+ } else {
2800
+ console.log(` \x1b[32m+ [Creating new file]\x1b[0m`);
2801
+ }
2802
+ const linesCount = op.content.split(/\r?\n/).length;
2803
+ console.log(` + [File content: ${linesCount} line(s), overwrite: ${!!op.overwrite}]`);
2804
+ printTruncatedLines(op.content, ' +', '\x1b[32m');
2805
+ } else if (type === 'append_line') {
2806
+ const exists = existsSync(op.resolvedPath);
2807
+ let currentFileContent = '';
2808
+ if (exists) {
2809
+ currentFileContent = readFileSync(op.resolvedPath, 'utf8');
2810
+ }
2811
+ const fileLines = currentFileContent.split(/\r?\n/);
2812
+ const lineExists = fileLines.some(l => l.trim() === op.line.trim());
2813
+ if (lineExists) {
2814
+ console.log(` \x1b[33m[IDEMPOTENT] Line already exists in file. No changes will be made.\x1b[0m`);
2815
+ } else {
2816
+ console.log(` \x1b[32m+ Appending line:\x1b[0m`);
2817
+ console.log(` \x1b[32m+ ${op.line}\x1b[0m`);
2818
+ }
2819
+ } else if (type === 'replace_text') {
2820
+ console.log(` --- a/${op.path}`);
2821
+ console.log(` +++ b/${op.path}`);
2822
+ console.log(` \x1b[31m- Removing:\x1b[0m`);
2823
+ printTruncatedLines(op.find, ' -', '\x1b[31m');
2824
+ console.log(` \x1b[32m+ Inserting:\x1b[0m`);
2825
+ printTruncatedLines(op.replace, ' +', '\x1b[32m');
2826
+ }
2827
+ });
2828
+ console.log();
2829
+ });
2830
+ }
2831
+
2832
+ function handleImproveApply(proposalFile, options) {
2833
+ if (!options.approved) {
2834
+ console.error(`\x1b[31mError: Proposal cannot be applied without explicit user approval. Pass the --approved flag.\x1b[0m`);
2835
+ console.error(`Example: node bin/multimodel-dev-os.js improve apply ${proposalFile} --approved`);
2836
+ process.exit(1);
2837
+ }
2838
+
2839
+ console.log(`🚀 \x1b[34mApplying proposal: ${proposalFile}\x1b[0m`);
2840
+ const validation = validateProposal(proposalFile, options.target);
2841
+ if (!validation.valid) {
2842
+ console.error(`\x1b[31mValidation FAILED: ${validation.reason}\x1b[0m`);
2843
+
2844
+ // Log the refusal
2845
+ const applyId = `apply-${new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14)}`;
2846
+ const logDir = join(options.target, '.ai', 'proposals');
2847
+ if (!existsSync(logDir)) {
2848
+ try { mkdirSync(logDir, { recursive: true }); } catch (e) {}
2849
+ }
2850
+ const logFile = join(logDir, 'apply-log.jsonl');
2851
+ const record = {
2852
+ id: applyId,
2853
+ proposal_id: validation.proposalId || basename(proposalFile, '.md'),
2854
+ applied_at: new Date().toISOString(),
2855
+ target: options.target,
2856
+ operations_count: 0,
2857
+ files_changed: [],
2858
+ before_hashes: {},
2859
+ after_hashes: {},
2860
+ status: 'refused',
2861
+ refused_reason: validation.reason,
2862
+ notes: `Validation failed: ${validation.reason}`
2863
+ };
2864
+ try {
2865
+ writeFileSync(logFile, JSON.stringify(record) + '\n', { flag: 'a', encoding: 'utf8' });
2866
+ } catch (err) {}
2867
+ process.exit(1);
2868
+ }
2869
+
2870
+ const operations = validation.operations;
2871
+ const proposalId = validation.proposalId;
2872
+
2873
+ // Print compact operations summary
2874
+ const createCount = operations.filter(op => op.type === 'create_file').length;
2875
+ const appendCount = operations.filter(op => op.type === 'append_line').length;
2876
+ const replaceCount = operations.filter(op => op.type === 'replace_text').length;
2877
+ console.log(`Summary of Operations:`);
2878
+ console.log(` - ${createCount} file(s) to create`);
2879
+ console.log(` - ${appendCount} file(s) to append`);
2880
+ console.log(` - ${replaceCount} file(s) to modify (replace)`);
2881
+ console.log(`\nApplying changes...`);
2882
+
2883
+ const filesChanged = [];
2884
+ const beforeHashes = {};
2885
+ const afterHashes = {};
2886
+ let status = 'success';
2887
+ let notes = '';
2888
+
2889
+ const applyId = `apply-${new Date().toISOString().replace(/[-:T.Z]/g, '').slice(0, 14)}`;
2890
+
2891
+ try {
2892
+ operations.forEach(op => {
2893
+ const relPath = relative(options.target, op.resolvedPath).replace(/\\/g, '/');
2894
+ if (!filesChanged.includes(relPath)) {
2895
+ filesChanged.push(relPath);
2896
+ }
2897
+ if (existsSync(op.resolvedPath)) {
2898
+ const fileContent = readFileSync(op.resolvedPath, 'utf8');
2899
+ beforeHashes[relPath] = getSha256(fileContent);
2900
+ } else {
2901
+ beforeHashes[relPath] = null;
2902
+ }
2903
+ });
2904
+
2905
+ operations.forEach((op, idx) => {
2906
+ const relPath = relative(options.target, op.resolvedPath).replace(/\\/g, '/');
2907
+ console.log(` Executing Operation #${idx + 1} (${op.type}) on '${relPath}'...`);
2908
+
2909
+ if (op.type === 'create_file') {
2910
+ const dir = dirname(op.resolvedPath);
2911
+ if (!existsSync(dir)) {
2912
+ mkdirSync(dir, { recursive: true });
2913
+ }
2914
+ const exists = existsSync(op.resolvedPath);
2915
+ writeFileSync(op.resolvedPath, op.content, 'utf8');
2916
+ if (exists) {
2917
+ console.log(` [OVERWRITTEN] Overwrote existing file '${relPath}'.`);
2918
+ } else {
2919
+ console.log(` [CREATED] Created new file '${relPath}'.`);
2920
+ }
2921
+ } else if (op.type === 'append_line') {
2922
+ let content = '';
2923
+ if (existsSync(op.resolvedPath)) {
2924
+ content = readFileSync(op.resolvedPath, 'utf8');
2925
+ }
2926
+ const fileLines = content.split(/\r?\n/);
2927
+ const lineExists = fileLines.some(l => l.trim() === op.line.trim());
2928
+ if (!lineExists) {
2929
+ let newContent = content;
2930
+ if (content.length > 0 && !content.endsWith('\n') && !content.endsWith('\r')) {
2931
+ newContent += '\n';
2932
+ }
2933
+ newContent += op.line + '\n';
2934
+ const dir = dirname(op.resolvedPath);
2935
+ if (!existsSync(dir)) {
2936
+ mkdirSync(dir, { recursive: true });
2937
+ }
2938
+ writeFileSync(op.resolvedPath, newContent, 'utf8');
2939
+ console.log(` [APPENDED] Appended 1 line to '${relPath}'.`);
2940
+ } else {
2941
+ console.log(` [IDEMPOTENT] Line already exists in '${relPath}'. Skipping append.`);
2942
+ }
2943
+ } else if (op.type === 'replace_text') {
2944
+ const fileContent = readFileSync(op.resolvedPath, 'utf8');
2945
+ let count = 0;
2946
+ let pos = fileContent.indexOf(op.find);
2947
+ while (pos !== -1) {
2948
+ count++;
2949
+ pos = fileContent.indexOf(op.find, pos + op.find.length);
2950
+ }
2951
+
2952
+ let newContent;
2953
+ if (op.allow_multiple) {
2954
+ newContent = fileContent.split(op.find).join(op.replace);
2955
+ } else {
2956
+ newContent = fileContent.replace(op.find, op.replace);
2957
+ if (count > 0) count = 1;
2958
+ }
2959
+ writeFileSync(op.resolvedPath, newContent, 'utf8');
2960
+ console.log(` [REPLACED] Replaced ${count} occurrence(s) of find text in '${relPath}'.`);
2961
+ }
2962
+ });
2963
+
2964
+ filesChanged.forEach(relPath => {
2965
+ const fullPath = resolve(options.target, relPath);
2966
+ if (existsSync(fullPath)) {
2967
+ const fileContent = readFileSync(fullPath, 'utf8');
2968
+ afterHashes[relPath] = getSha256(fileContent);
2969
+ } else {
2970
+ afterHashes[relPath] = null;
2971
+ }
2972
+ });
2973
+
2974
+ notes = `Successfully applied ${operations.length} operations.`;
2975
+ } catch (e) {
2976
+ status = 'failed';
2977
+ notes = `Execution error: ${e.message}`;
2978
+ console.error(`\x1b[31mError applying proposal: ${e.message}\x1b[0m`);
2979
+ }
2980
+
2981
+ const logDir = join(options.target, '.ai', 'proposals');
2982
+ if (!existsSync(logDir)) {
2983
+ mkdirSync(logDir, { recursive: true });
2984
+ }
2985
+ const logFile = join(logDir, 'apply-log.jsonl');
2986
+
2987
+ const record = {
2988
+ id: applyId,
2989
+ proposal_id: proposalId,
2990
+ applied_at: new Date().toISOString(),
2991
+ target: options.target,
2992
+ operations_count: operations.length,
2993
+ files_changed: filesChanged,
2994
+ before_hashes: beforeHashes,
2995
+ after_hashes: afterHashes,
2996
+ status,
2997
+ refused_reason: status === 'failed' ? notes : undefined,
2998
+ notes
2999
+ };
3000
+
3001
+ try {
3002
+ writeFileSync(logFile, JSON.stringify(record) + '\n', { flag: 'a', encoding: 'utf8' });
3003
+ } catch (err) {
3004
+ console.error(`\x1b[31mFailed to write to audit log: ${err.message}\x1b[0m`);
3005
+ }
3006
+
3007
+ if (status === 'success') {
3008
+ console.log(`\n\x1b[32m✔ Proposal applied successfully!\x1b[0m`);
3009
+ console.log(`Files changed:`);
3010
+ filesChanged.forEach(f => console.log(` - ${f}`));
3011
+ console.log(`Audit log recorded to: ${logFile}`);
3012
+ } else {
3013
+ process.exit(1);
3014
+ }
3015
+ }
3016
+
3017
+ function handleImproveLog(options) {
3018
+ const logFile = join(options.target, '.ai', 'proposals', 'apply-log.jsonl');
3019
+ if (!existsSync(logFile)) {
3020
+ console.log('No apply log found.');
3021
+ return;
3022
+ }
3023
+
3024
+ try {
3025
+ const lines = readFileSync(logFile, 'utf8').trim().split(/\r?\n/);
3026
+ console.log(`\n📜 \x1b[36mApplied Proposals Audit Log\x1b[0m`);
3027
+ console.log('==================================================');
3028
+ lines.forEach(line => {
3029
+ if (!line.trim()) return;
3030
+ const record = JSON.parse(line);
3031
+ const statusColor = record.status === 'success' ? '\x1b[32m' : '\x1b[31m';
3032
+ console.log(`\n\x1b[34m* [${record.id}] Proposal: ${record.proposal_id}\x1b[0m`);
3033
+ console.log(` \x1b[37mApplied At:\x1b[0m ${record.applied_at}`);
3034
+ console.log(` \x1b[37mOperations:\x1b[0m ${record.operations_count}`);
3035
+ console.log(` \x1b[37mFiles Changed:\x1b[0m ${record.files_changed.join(', ')}`);
3036
+ console.log(` \x1b[37mStatus:\x1b[0m ${statusColor}${record.status}\x1b[0m`);
3037
+ console.log(` \x1b[37mNotes:\x1b[0m ${record.notes}`);
3038
+ });
3039
+ console.log();
3040
+ } catch (e) {
3041
+ console.error(`\x1b[31mError reading audit log: ${e.message}\x1b[0m`);
3042
+ process.exit(1);
3043
+ }
3044
+ }
3045
+
3046
+ // ==================================================
3047
+ // v2.5.0 Repository Intelligence Command Center
3048
+ // ==================================================
3049
+
3050
+ function handleStatus(options) {
3051
+ console.log(`\n📊 \x1b[36mRepository Intelligence Status: ${options.target}\x1b[0m`);
3052
+ console.log('==================================================');
3053
+
3054
+ // 1. Project Info
3055
+ let pkgName = 'unknown';
3056
+ let pkgVersion = 'unknown';
3057
+ try {
3058
+ const pkgPath = join(options.target, 'package.json');
3059
+ if (existsSync(pkgPath)) {
3060
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
3061
+ pkgName = pkg.name || pkgName;
3062
+ pkgVersion = pkg.version || pkgVersion;
3063
+ }
3064
+ } catch (e) {}
3065
+ console.log(` \x1b[33mProject Info:\x1b[0m`);
3066
+ console.log(` Package Name: ${pkgName}`);
3067
+ console.log(` Package Version: ${pkgVersion}`);
3068
+
3069
+ // 2. Framework signals
3070
+ const { files } = scanTarget(options.target);
3071
+ const frameworkSignals = detectFrameworkSignals(files, options.target);
3072
+ const dependencySignals = detectDependencySignals(files, options.target);
3073
+ console.log(` \x1b[33mFramework & Dependency Signals:\x1b[0m`);
3074
+ console.log(` Frameworks: ${frameworkSignals.join(', ') || 'None'}`);
3075
+ console.log(` Dependencies: ${dependencySignals.join(', ') || 'None'}`);
3076
+
3077
+ // 3. Memory status
3078
+ const memoryHashPath = join(options.target, '.ai', 'intelligence', 'memory.hash.json');
3079
+ let memoryStatus = '\x1b[31mMISSING\x1b[0m';
3080
+ let lastBuildTime = 'N/A';
3081
+ if (existsSync(memoryHashPath)) {
3082
+ try {
3083
+ const memObj = JSON.parse(readFileSync(memoryHashPath, 'utf8'));
3084
+ lastBuildTime = memObj.generated_at || 'N/A';
3085
+ const diff = diffMemory(options.target);
3086
+ if (diff) {
3087
+ if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) {
3088
+ memoryStatus = '\x1b[32mCURRENT\x1b[0m';
3089
+ } else {
3090
+ memoryStatus = `\x1b[33mSTALE\x1b[0m (changes: +${diff.added.length}, -${diff.removed.length}, ~${diff.changed.length})`;
3091
+ }
3092
+ }
3093
+ } catch (e) {
3094
+ memoryStatus = '\x1b[31mCORRUPT\x1b[0m';
3095
+ }
3096
+ }
3097
+ console.log(` \x1b[33mMemory State:\x1b[0m`);
3098
+ console.log(` Status: ${memoryStatus}`);
3099
+ console.log(` Last Built: ${lastBuildTime}`);
3100
+
3101
+ // 4. Feedback & Rules
3102
+ const feedbackPath = join(options.target, '.ai', 'intelligence', 'feedback-log.jsonl');
3103
+ let feedbackCount = 0;
3104
+ if (existsSync(feedbackPath)) {
3105
+ try {
3106
+ feedbackCount = readFileSync(feedbackPath, 'utf8').trim().split(/\r?\n/).filter(l => l.trim() !== '').length;
3107
+ } catch (e) {}
3108
+ }
3109
+ const rulesPath = join(options.target, '.ai', 'intelligence', 'learning-rules.md');
3110
+ const rulesStatus = existsSync(rulesPath) ? '\x1b[32mPRESENT\x1b[0m' : '\x1b[31mMISSING\x1b[0m';
3111
+ console.log(` \x1b[33mFeedback Loop & Rules:\x1b[0m`);
3112
+ console.log(` Feedback Count: ${feedbackCount}`);
3113
+ console.log(` Learning Rules: ${rulesStatus}`);
3114
+
3115
+ // 5. Proposals Engine
3116
+ const proposalsDir = join(options.target, '.ai', 'proposals');
3117
+ let pendingCount = 0;
3118
+ let approvedCount = 0;
3119
+ let rejectedCount = 0;
3120
+ let totalProposals = 0;
3121
+ if (existsSync(proposalsDir)) {
3122
+ try {
3123
+ const propFiles = readdirSync(proposalsDir).filter(f => f.startsWith('proposal-') && f.endsWith('.md'));
3124
+ totalProposals = propFiles.length;
3125
+ propFiles.forEach(file => {
3126
+ const content = readFileSync(join(proposalsDir, file), 'utf8');
3127
+ const fmMatch = content.match(/^---([\s\S]*?)---/);
3128
+ if (fmMatch) {
3129
+ const metadata = parseYaml(fmMatch[1]) || {};
3130
+ const status = metadata.approval_status || 'pending';
3131
+ if (status === 'approved') approvedCount++;
3132
+ else if (status === 'rejected') rejectedCount++;
3133
+ else pendingCount++;
3134
+ }
3135
+ });
3136
+ } catch (e) {}
3137
+ }
3138
+ console.log(` \x1b[33mImprovement Proposals:\x1b[0m`);
3139
+ console.log(` Total proposals: ${totalProposals}`);
3140
+ console.log(` Pending: \x1b[33m${pendingCount}\x1b[0m`);
3141
+ console.log(` Approved: \x1b[32m${approvedCount}\x1b[0m`);
3142
+ console.log(` Rejected: \x1b[31m${rejectedCount}\x1b[0m`);
3143
+
3144
+ // 6. Apply Log History
3145
+ const applyLogPath = join(options.target, '.ai', 'proposals', 'apply-log.jsonl');
3146
+ let applyLogCount = 0;
3147
+ if (existsSync(applyLogPath)) {
3148
+ try {
3149
+ applyLogCount = readFileSync(applyLogPath, 'utf8').trim().split(/\r?\n/).filter(l => l.trim() !== '').length;
3150
+ } catch (e) {}
3151
+ }
3152
+ console.log(` \x1b[33mApply Audit Log:\x1b[0m`);
3153
+ console.log(` Apply Count: ${applyLogCount}`);
3154
+
3155
+ // 7. Recommended Next Move
3156
+ let nextMove = 'mmdo status';
3157
+ if (!existsSync(join(options.target, '.ai', 'config.yaml'))) {
3158
+ nextMove = '\x1b[36mnpx multimodel-dev-os init\x1b[0m (initialize MultiModel Dev OS first)';
3159
+ } else if (!existsSync(memoryHashPath)) {
3160
+ nextMove = '\x1b[36mnpx multimodel-dev-os memory build\x1b[0m (initialize memory index)';
3161
+ } else {
3162
+ const diff = diffMemory(options.target);
3163
+ if (diff && (diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0)) {
3164
+ nextMove = '\x1b[36mnpx multimodel-dev-os memory refresh\x1b[0m (update memory with changes)';
3165
+ } else if (feedbackCount > 0 && !existsSync(rulesPath)) {
3166
+ nextMove = '\x1b[36mnpx multimodel-dev-os feedback summarize\x1b[0m (compile feedback into learning rules)';
3167
+ } else if (pendingCount > 0) {
3168
+ nextMove = '\x1b[36mnpx multimodel-dev-os improve review\x1b[0m (review pending proposals)';
3169
+ } else {
3170
+ nextMove = '\x1b[36mnpx multimodel-dev-os workflow run repo-health\x1b[0m (run standard codebase health checks)';
3171
+ }
3172
+ }
3173
+ console.log(`\n \x1b[35mRecommended Next Command:\x1b[0m`);
3174
+ console.log(` ${nextMove}\n`);
3175
+ }
3176
+
3177
+ function getWorkflowsPath(target) {
3178
+ let workflowsPath = join(target, '.ai', 'registries', 'workflows.yaml');
3179
+ let usingFallback = false;
3180
+ if (!existsSync(workflowsPath)) {
3181
+ const fallbackPath = join(sourceRoot, '.ai', 'registries', 'workflows.yaml');
3182
+ if (existsSync(fallbackPath)) {
3183
+ workflowsPath = fallbackPath;
3184
+ usingFallback = true;
3185
+ }
3186
+ }
3187
+ return { workflowsPath, usingFallback };
3188
+ }
3189
+
3190
+ function handleWorkflowList(options) {
3191
+ const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
3192
+ if (!existsSync(workflowsPath)) {
3193
+ console.log('No workflows registry found.');
3194
+ return;
3195
+ }
3196
+ if (usingFallback) {
3197
+ console.log('\x1b[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1b[0m');
3198
+ }
3199
+ try {
3200
+ const registry = parseYaml(readFileSync(workflowsPath, 'utf8')) || {};
3201
+ const workflows = registry.workflows || {};
3202
+ console.log(`\n⚙ \x1b[36mRegistered Workflows\x1b[0m`);
3203
+ console.log('==================================================');
3204
+ Object.keys(workflows).forEach(key => {
3205
+ const wf = workflows[key];
3206
+ const name = wf.name || key;
3207
+ const risk = wf.risk_level || 'unknown';
3208
+ const riskColor = risk === 'low' ? '\x1b[32m' : risk === 'medium' ? '\x1b[33m' : '\x1b[31m';
3209
+ console.log(`\n \x1b[34m* ${name}\x1b[0m (\x1b[35m${key}\x1b[0m)`);
3210
+ console.log(` Description: ${wf.description || 'No description'}`);
3211
+ console.log(` Risk Level: ${riskColor}${risk.toUpperCase()}\x1b[0m`);
3212
+ });
3213
+ console.log();
3214
+ } catch (e) {
3215
+ console.error(`\x1b[31mError loading workflows: ${e.message}\x1b[0m`);
3216
+ }
3217
+ }
3218
+
3219
+ function handleWorkflowShow(wName, options) {
3220
+ const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
3221
+ if (!existsSync(workflowsPath)) {
3222
+ console.log('No workflows registry found.');
3223
+ return;
3224
+ }
3225
+ if (usingFallback) {
3226
+ console.log('\x1b[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1b[0m');
3227
+ }
3228
+ try {
3229
+ const registry = parseYaml(readFileSync(workflowsPath, 'utf8')) || {};
3230
+ const workflows = registry.workflows || {};
3231
+ const wf = workflows[wName];
3232
+ if (!wf) {
3233
+ console.error(`\x1b[31mError: Workflow '${wName}' not found in registry.\x1b[0m`);
3234
+ process.exit(1);
3235
+ }
3236
+ const name = wf.name || wName;
3237
+ const risk = wf.risk_level || 'unknown';
3238
+ const riskColor = risk === 'low' ? '\x1b[32m' : risk === 'medium' ? '\x1b[33m' : '\x1b[31m';
3239
+ console.log(`\n⚙ \x1b[36mWorkflow Spec: ${name}\x1b[0m`);
3240
+ console.log('==================================================');
3241
+ console.log(` Description: ${wf.description || 'No description'}`);
3242
+ console.log(` Risk Level: ${riskColor}${risk.toUpperCase()}\x1b[0m`);
3243
+ console.log(` Allowed to write memory: ${wf.allowed_to_write_memory || false}`);
3244
+ console.log(` Allowed to modify code: ${wf.allowed_to_modify_source || false}`);
3245
+ console.log(`\n \x1b[33mSteps:\x1b[0m`);
3246
+
3247
+ const steps = wf.steps || [];
3248
+ steps.forEach((step, idx) => {
3249
+ console.log(` ${idx + 1}. [${step.name}]`);
3250
+ console.log(` Command: ${step.command}`);
3251
+ console.log(` Expected Output: ${step.expected_output || 'N/A'}`);
3252
+ console.log(` Next Action: ${step.next_action || 'N/A'}`);
3253
+ });
3254
+ console.log();
3255
+ } catch (e) {
3256
+ console.error(`\x1b[31mError loading workflow '${wName}': ${e.message}\x1b[0m`);
3257
+ }
3258
+ }
3259
+
3260
+ function handleWorkflowPlan(wName, options) {
3261
+ const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
3262
+ if (!existsSync(workflowsPath)) {
3263
+ console.log('No workflows registry found.');
3264
+ return;
3265
+ }
3266
+ if (usingFallback) {
3267
+ console.log('\x1b[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1b[0m');
3268
+ }
3269
+ try {
3270
+ const registry = parseYaml(readFileSync(workflowsPath, 'utf8')) || {};
3271
+ const workflows = registry.workflows || {};
3272
+ const wf = workflows[wName];
3273
+ if (!wf) {
3274
+ console.error(`\x1b[31mError: Workflow '${wName}' not found.\x1b[0m`);
3275
+ process.exit(1);
3276
+ }
3277
+ const name = wf.name || wName;
3278
+ console.log(`\n📝 \x1b[36mExecution Plan for Workflow: ${name}\x1b[0m`);
3279
+ console.log('==================================================');
3280
+ console.log(`\x1b[33m[DRY-RUN/PLAN ONLY] No commands will be run.\x1b[0m\n`);
3281
+ const steps = wf.steps || [];
3282
+ steps.forEach((step, idx) => {
3283
+ console.log(` Step ${idx + 1}: ${step.name}`);
3284
+ console.log(` Command: ${step.command}`);
3285
+ console.log(` Expected Output: ${step.expected_output || 'N/A'}`);
3286
+ console.log(` Next Action: ${step.next_action || 'N/A'}`);
3287
+ });
3288
+ console.log();
3289
+ } catch (e) {
3290
+ console.error(`\x1b[31mError loading workflow plan: ${e.message}\x1b[0m`);
3291
+ }
3292
+ }
3293
+
3294
+ function handleWorkflowRun(wName, options) {
3295
+ const { workflowsPath, usingFallback } = getWorkflowsPath(options.target);
3296
+ if (!existsSync(workflowsPath)) {
3297
+ console.log('No workflows registry found.');
3298
+ return;
3299
+ }
3300
+ if (usingFallback) {
3301
+ console.log('\x1b[33mNotice: Local workflows registry not found. Using bundled workflows registry fallback.\x1b[0m');
3302
+ }
3303
+ try {
3304
+ const registry = parseYaml(readFileSync(workflowsPath, 'utf8')) || {};
3305
+ const workflows = registry.workflows || {};
3306
+ const wf = workflows[wName];
3307
+ if (!wf) {
3308
+ console.error(`\x1b[31mError: Workflow '${wName}' not found.\x1b[0m`);
3309
+ process.exit(1);
3310
+ }
3311
+
3312
+ const name = wf.name || wName;
3313
+ console.log(`\n🚀 \x1b[36mRunning Workflow: ${name}\x1b[0m`);
3314
+ console.log('==================================================');
3315
+
3316
+ const steps = wf.steps || [];
3317
+ const safeCommands = {
3318
+ 'scan': () => handleScan(options),
3319
+ 'doctor': () => handleDoctor(options),
3320
+ 'verify': () => handleVerify({ ...options, noExit: true }),
3321
+ 'memory diff': () => handleMemoryDiff({ ...options, noExit: true }),
3322
+ 'memory refresh': () => handleMemoryRefresh(options),
3323
+ 'memory build': () => handleMemoryBuild(options),
3324
+ 'feedback list': () => handleFeedbackList(options),
3325
+ 'feedback summarize': () => handleFeedbackSummarize(options),
3326
+ 'improve review': () => handleImproveReview(options),
3327
+ 'improve status': () => handleImproveStatus(options),
3328
+ 'improve log': () => handleImproveLog(options),
3329
+ 'doctor --release': () => handleDoctor({ ...options, release: true })
3330
+ };
3331
+
3332
+ steps.forEach((step, idx) => {
3333
+ console.log(`\n\x1b[33m[Step ${idx + 1}/${steps.length}] Running: ${step.name} (${step.command})\x1b[0m`);
3334
+ const cmd = step.command;
3335
+ if (safeCommands[cmd]) {
3336
+ try {
3337
+ safeCommands[cmd]();
3338
+ } catch (e) {
3339
+ console.error(`\x1b[31mError executing step ${step.name}: ${e.message}\x1b[0m`);
3340
+ }
3341
+ } else {
3342
+ console.log(` \x1b[35m[MANUAL ACTION NEEDED]\x1b[0m This step requires manual execution.`);
3343
+ console.log(` Please run command: \x1b[36mnpx multimodel-dev-os ${cmd}\x1b[0m`);
3344
+ if (step.expected_output) {
3345
+ console.log(` Expected Output: ${step.expected_output}`);
3346
+ }
3347
+ }
3348
+ });
3349
+ console.log(`\n✔ Workflow '${name}' complete.\n`);
3350
+ } catch (e) {
3351
+ console.error(`\x1b[31mError running workflow '${wName}': ${e.message}\x1b[0m`);
3352
+ }
3353
+ }
3354
+
3355
+ function handleHandoffBuild(options) {
3356
+ const intelDir = join(options.target, '.ai', 'intelligence');
3357
+ if (!existsSync(intelDir)) {
3358
+ mkdirSync(intelDir, { recursive: true });
3359
+ }
3360
+ const handoffPath = join(intelDir, 'handoff.md');
3361
+
3362
+ // 1. Get package metadata
3363
+ let pkgName = 'unknown';
3364
+ let pkgVersion = 'unknown';
3365
+ try {
3366
+ const pkgPath = join(options.target, 'package.json');
3367
+ if (existsSync(pkgPath)) {
3368
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
3369
+ pkgName = pkg.name || pkgName;
3370
+ pkgVersion = pkg.version || pkgVersion;
3371
+ }
3372
+ } catch (e) {}
3373
+
3374
+ // 2. Scan targets
3375
+ const { files } = scanTarget(options.target);
3376
+ const frameworkSignals = detectFrameworkSignals(files, options.target);
3377
+ const dependencySignals = detectDependencySignals(files, options.target);
3378
+
3379
+ // 3. Memory
3380
+ const memoryHashPath = join(intelDir, 'memory.hash.json');
3381
+ let memoryStatus = 'MISSING';
3382
+ let memoryTime = 'N/A';
3383
+ if (existsSync(memoryHashPath)) {
3384
+ try {
3385
+ const memObj = JSON.parse(readFileSync(memoryHashPath, 'utf8'));
3386
+ memoryTime = memObj.generated_at || 'N/A';
3387
+ const diff = diffMemory(options.target);
3388
+ if (diff) {
3389
+ memoryStatus = (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) ? 'CURRENT' : 'STALE';
3390
+ }
3391
+ } catch (e) {
3392
+ memoryStatus = 'CORRUPT';
3393
+ }
3394
+ }
3395
+
3396
+ // 4. Feedback
3397
+ const feedbackPath = join(intelDir, 'feedback-log.jsonl');
3398
+ let feedbackCount = 0;
3399
+ if (existsSync(feedbackPath)) {
3400
+ try {
3401
+ feedbackCount = readFileSync(feedbackPath, 'utf8').trim().split(/\r?\n/).filter(l => l.trim() !== '').length;
3402
+ } catch (e) {}
3403
+ }
3404
+ const rulesPath = join(intelDir, 'learning-rules.md');
3405
+ const rulesStatus = existsSync(rulesPath) ? 'PRESENT' : 'MISSING';
3406
+
3407
+ // 5. Proposals
3408
+ const proposalsDir = join(options.target, '.ai', 'proposals');
3409
+ let pendingCount = 0;
3410
+ let approvedCount = 0;
3411
+ let rejectedCount = 0;
3412
+ if (existsSync(proposalsDir)) {
3413
+ try {
3414
+ const propFiles = readdirSync(proposalsDir).filter(f => f.startsWith('proposal-') && f.endsWith('.md'));
3415
+ propFiles.forEach(file => {
3416
+ const content = readFileSync(join(proposalsDir, file), 'utf8');
3417
+ const fmMatch = content.match(/^---([\s\S]*?)---/);
3418
+ if (fmMatch) {
3419
+ const metadata = parseYaml(fmMatch[1]) || {};
3420
+ const status = metadata.approval_status || 'pending';
3421
+ if (status === 'approved') approvedCount++;
3422
+ else if (status === 'rejected') rejectedCount++;
3423
+ else pendingCount++;
3424
+ }
3425
+ });
3426
+ } catch (e) {}
3427
+ }
3428
+
3429
+ // 6. Apply logs
3430
+ const applyLogPath = join(proposalsDir, 'apply-log.jsonl');
3431
+ let applyLogCount = 0;
3432
+ let lastApplyId = 'None';
3433
+ if (existsSync(applyLogPath)) {
3434
+ try {
3435
+ const lines = readFileSync(applyLogPath, 'utf8').trim().split(/\r?\n/).filter(l => l.trim() !== '');
3436
+ applyLogCount = lines.length;
3437
+ if (applyLogCount > 0) {
3438
+ const lastRecord = JSON.parse(lines[lines.length - 1]);
3439
+ lastApplyId = lastRecord.id || 'unknown';
3440
+ }
3441
+ } catch (e) {}
3442
+ }
3443
+
3444
+ // 7. Core Learning Summary
3445
+ let rulesSummary = 'No learning rules defined yet.';
3446
+ if (existsSync(rulesPath)) {
3447
+ try {
3448
+ const rulesContent = readFileSync(rulesPath, 'utf8');
3449
+ const lines = rulesContent.split(/\r?\n/);
3450
+ const summaryLines = [];
3451
+ for (const line of lines) {
3452
+ if (line.startsWith('* **Pattern:**') || line.startsWith(' * **Rule:**')) {
3453
+ summaryLines.push(line);
3454
+ }
3455
+ if (summaryLines.length >= 10) break;
3456
+ }
3457
+ if (summaryLines.length > 0) {
3458
+ rulesSummary = summaryLines.join('\n');
3459
+ }
3460
+ } catch (e) {}
3461
+ }
3462
+
3463
+ // Next steps recommended
3464
+ let recs = '1. Run `npx multimodel-dev-os workflow run repo-health` to check the directory hygiene.\n2. Review pending proposals if any exist.';
3465
+ if (!existsSync(join(options.target, '.ai', 'config.yaml'))) {
3466
+ recs = '1. Run `npx multimodel-dev-os init` to bootstrap MultiModel Dev OS.\n2. Run `npx multimodel-dev-os memory build` to initialize codebase memory.';
3467
+ } else if (memoryStatus === 'MISSING') {
3468
+ recs = '1. Run `npx multimodel-dev-os memory build` to initialize codebase index.\n2. Verify package safety boundaries.';
3469
+ } else if (memoryStatus === 'STALE') {
3470
+ recs = '1. Run `npx multimodel-dev-os memory refresh` to update memory files.\n2. Analyze modifications.';
3471
+ } else if (pendingCount > 0) {
3472
+ recs = `1. Run \`npx multimodel-dev-os improve review\` to inspect the ${pendingCount} pending proposals.\n2. Apply approved changes manually.`;
3473
+ }
3474
+
3475
+ const handoffContent = `# Agent Handoff Spec - ${new Date().toISOString()}
3476
+
3477
+ ## 1. Project Context
3478
+ - **Name**: ${pkgName}
3479
+ - **Version**: ${pkgVersion}
3480
+ - **Frameworks**: ${frameworkSignals.join(', ') || 'None'}
3481
+ - **Dependencies**: ${dependencySignals.join(', ') || 'None'}
3482
+
3483
+ ## 2. Intelligence Core State
3484
+ - **Memory**: ${memoryStatus} (Last build: ${memoryTime})
3485
+ - **Feedback Loop**: ${feedbackCount} items logged. \`learning-rules.md\` is ${rulesStatus}.
3486
+ - **Proposals**: ${pendingCount} Pending, ${approvedCount} Approved, ${rejectedCount} Rejected.
3487
+ - **Applied Modifications**: ${applyLogCount} runs recorded. Last run: ${lastApplyId}.
3488
+
3489
+ ## 3. Core Learning Summaries
3490
+ \`\`\`markdown
3491
+ ${rulesSummary}
3492
+ \`\`\`
3493
+
3494
+ ## 4. Safety Constraints
3495
+ - Workflow run is restricted to read-only actions.
3496
+ - Modifications must be applied explicitly via \`improve apply --approved\`.
3497
+ - No code modification permissions exist in this session context.
3498
+
3499
+ ## 5. Recommended Next Steps
3500
+ ${recs}
3501
+ `;
3502
+
3503
+ try {
3504
+ writeFileSync(handoffPath, handoffContent, 'utf8');
3505
+ console.log(`\n✔ Handoff context built successfully in: .ai/intelligence/handoff.md`);
3506
+ } catch (e) {
3507
+ console.error(`\x1b[31mError writing handoff: ${e.message}\x1b[0m`);
3508
+ }
3509
+ }
3510
+
3511
+ function handleHandoffShow(options) {
3512
+ const handoffPath = join(options.target, '.ai', 'intelligence', 'handoff.md');
3513
+ if (!existsSync(handoffPath)) {
3514
+ console.log('No compiled handoff file exists. Building first...');
3515
+ handleHandoffBuild(options);
3516
+ }
3517
+ try {
3518
+ const content = readFileSync(handoffPath, 'utf8');
3519
+ console.log('\n' + content);
3520
+ } catch (e) {
3521
+ console.error(`\x1b[31mError reading handoff: ${e.message}\x1b[0m`);
3522
+ }
3523
+ }
3524
+
3525
+ function handleDoctorIntelligence(options) {
3526
+ console.log(`\n🩺 \x1b[36mRunning advisory intelligence doctor checkup in: ${options.target}\x1b[0m\n`);
3527
+
3528
+ let warnings = 0;
3529
+ const warn = (msg) => {
3530
+ console.warn(` \x1b[33m[WARNING]\x1b[0m ${msg}`);
3531
+ warnings++;
3532
+ };
3533
+
3534
+ // 1. Memory checks
3535
+ const memoryHashPath = join(options.target, '.ai', 'intelligence', 'memory.hash.json');
3536
+ if (!existsSync(memoryHashPath)) {
3537
+ warn('Memory hash index (.ai/intelligence/memory.hash.json) is MISSING. Run `memory build` first.');
3538
+ } else {
3539
+ try {
3540
+ const diff = diffMemory(options.target);
3541
+ if (!diff) {
3542
+ warn('Memory hash index is present but corrupt.');
3543
+ } else if (diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0) {
3544
+ warn(`Memory hash index is STALE. Delts: +${diff.added.length}, -${diff.removed.length}, ~${diff.changed.length}. Run \`memory refresh\`.`);
3545
+ }
3546
+ } catch (e) {
3547
+ warn('Failed to diff memory index.');
3548
+ }
3549
+ }
3550
+
3551
+ // 2. Feedback checks
3552
+ const feedbackPath = join(options.target, '.ai', 'intelligence', 'feedback-log.jsonl');
3553
+ if (!existsSync(feedbackPath)) {
3554
+ warn('Feedback log (.ai/intelligence/feedback-log.jsonl) is MISSING.');
3555
+ }
3556
+ const rulesPath = join(options.target, '.ai', 'intelligence', 'learning-rules.md');
3557
+ if (!existsSync(rulesPath)) {
3558
+ warn('Learning rules (.ai/intelligence/learning-rules.md) are MISSING. Run `feedback summarize` to compile logs.');
3559
+ }
3560
+
3561
+ // 3. Proposals checks
3562
+ const proposalsDir = join(options.target, '.ai', 'proposals');
3563
+ if (!existsSync(proposalsDir)) {
3564
+ warn('Proposals directory (.ai/proposals) is MISSING.');
3565
+ } else {
3566
+ try {
3567
+ const files = readdirSync(proposalsDir).filter(f => f.startsWith('proposal-') && f.endsWith('.md'));
3568
+ let pending = 0;
3569
+ files.forEach(file => {
3570
+ const content = readFileSync(join(proposalsDir, file), 'utf8');
3571
+ const fmMatch = content.match(/^---([\s\S]*?)---/);
3572
+ if (fmMatch) {
3573
+ const metadata = parseYaml(fmMatch[1]) || {};
3574
+ if ((metadata.approval_status || 'pending') === 'pending') {
3575
+ pending++;
3576
+ }
3577
+ }
3578
+ });
3579
+ if (pending > 0) {
3580
+ warn(`Found ${pending} pending improvement proposals waiting for approval.`);
3581
+ }
3582
+ } catch (e) {}
3583
+ }
3584
+
3585
+ // 4. Apply log check
3586
+ const applyLogPath = join(options.target, '.ai', 'proposals', 'apply-log.jsonl');
3587
+ if (!existsSync(applyLogPath)) {
3588
+ warn('Apply audit log (.ai/proposals/apply-log.jsonl) is MISSING.');
3589
+ }
3590
+
3591
+ // 5. Gitignore ignores intelligence checks
3592
+ const gitignorePath = join(options.target, '.gitignore');
3593
+ if (existsSync(gitignorePath)) {
3594
+ const gitignoreContent = readFileSync(gitignorePath, 'utf8');
3595
+ const checkIgnore = (pattern) => {
3596
+ if (!gitignoreContent.includes(pattern)) {
3597
+ warn(`.gitignore is missing rules ignoring: ${pattern}`);
3598
+ }
3599
+ };
3600
+ checkIgnore('.ai/intelligence/handoff.md');
3601
+ checkIgnore('.ai/intelligence/status.snapshot.json');
3602
+ checkIgnore('.ai/intelligence/feedback-log.jsonl');
3603
+ checkIgnore('.ai/intelligence/learning-rules.md');
3604
+ checkIgnore('.ai/proposals/apply-log.jsonl');
3605
+ } else {
3606
+ warn('.gitignore file is missing in target root.');
3607
+ }
3608
+
3609
+ // 6. Danger files check inside memory index
3610
+ if (existsSync(memoryHashPath)) {
3611
+ try {
3612
+ const memObj = JSON.parse(readFileSync(memoryHashPath, 'utf8'));
3613
+ const fingerprints = memObj.file_fingerprints || {};
3614
+ Object.keys(fingerprints).forEach(file => {
3615
+ const name = file.toLowerCase();
3616
+ if (name.includes('.env') || name.includes('id_rsa') || name.includes('credential') || name.endsWith('.pem') || name.endsWith('.p12') || name.endsWith('.key') || name.endsWith('.keystore') || name.endsWith('.jks')) {
3617
+ warn(`Memory index contains potentially sensitive file: ${file}`);
3618
+ }
3619
+ });
3620
+ } catch (e) {}
3621
+ }
3622
+
3623
+ console.log('\n==================================================');
3624
+ if (warnings > 0) {
3625
+ console.log(`\x1b[33mDoctor intelligence check complete. Found ${warnings} warnings.\x1b[0m\n`);
3626
+ } else {
3627
+ console.log('\x1b[32m✔ Doctor intelligence check complete. Your intelligence setup is pristine!\x1b[0m\n');
3628
+ }
3629
+ }
3630
+
3631
+ function getAnalysis(target) {
3632
+ const { files, ignoredCount } = scanTarget(target);
3633
+ const frameworks = detectFrameworkSignals(files, target);
3634
+ const packageManagers = detectDependencySignals(files, target);
3635
+ const aiSignals = detectAiDevOsSignals(files);
3636
+
3637
+ let jsCount = 0, tsCount = 0, phpCount = 0, pyCount = 0, mdCount = 0;
3638
+ files.forEach(f => {
3639
+ const ext = f.relPath.substring(f.relPath.lastIndexOf('.')).toLowerCase();
3640
+ if (ext === '.js' || ext === '.mjs' || ext === '.cjs') jsCount++;
3641
+ else if (ext === '.ts' || ext === '.tsx') tsCount++;
3642
+ else if (ext === '.php') phpCount++;
3643
+ else if (ext === '.py') pyCount++;
3644
+ else if (ext === '.md') mdCount++;
3645
+ });
3646
+
3647
+ let language = 'mixed';
3648
+ if (tsCount > jsCount && tsCount > phpCount && tsCount > pyCount && tsCount > mdCount) language = 'TS';
3649
+ else if (jsCount > tsCount && jsCount > phpCount && jsCount > pyCount && jsCount > mdCount) language = 'JS';
3650
+ else if (phpCount > jsCount && phpCount > tsCount && phpCount > pyCount && phpCount > mdCount) language = 'PHP';
3651
+ else if (pyCount > jsCount && pyCount > tsCount && phpCount > pyCount && phpCount > mdCount) language = 'Python';
3652
+ else if (mdCount > jsCount && mdCount > tsCount && mdCount > phpCount && mdCount > pyCount) language = 'Markdown-heavy';
3653
+
3654
+ let repoType = 'app';
3655
+ if (files.some(f => f.relPath.includes('wp-content/themes') || f.relPath.includes('wp-content/plugins'))) {
3656
+ repoType = 'WordPress theme/plugin';
3657
+ } else if (files.some(f => f.relPath.includes('app.json') || f.relPath.includes('eas.json'))) {
3658
+ repoType = 'mobile app';
3659
+ } else if (files.some(f => f.relPath.includes('lerna.json') || f.relPath.includes('pnpm-workspace.yaml'))) {
3660
+ repoType = 'monorepo';
3661
+ } else if (files.some(f => f.relPath.includes('docs/')) && mdCount > (files.length * 0.4)) {
3662
+ repoType = 'docs';
3663
+ } else if (files.some(f => f.relPath === 'package.json')) {
3664
+ try {
3665
+ const pkg = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'));
3666
+ if (pkg.main && (pkg.main.includes('dist/') || pkg.main.includes('lib/'))) {
3667
+ repoType = 'library';
3668
+ }
3669
+ } catch (e) {}
3670
+ }
3671
+
3672
+ const existingTools = [];
3673
+ if (files.some(f => f.relPath === '.cursorrules')) existingTools.push('Cursor');
3674
+ if (files.some(f => f.relPath === 'CLAUDE.md')) existingTools.push('Claude');
3675
+ if (files.some(f => f.relPath === 'GEMINI.md')) existingTools.push('Gemini');
3676
+ if (files.some(f => f.relPath.startsWith('.vscode/'))) existingTools.push('VS Code');
3677
+ if (files.some(f => f.relPath.startsWith('.gemini/'))) existingTools.push('Antigravity');
3678
+
3679
+ const packageScripts = [];
3680
+ if (files.some(f => f.relPath === 'package.json')) {
3681
+ try {
3682
+ const pkg = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'));
3683
+ if (pkg.scripts) {
3684
+ Object.keys(pkg.scripts).forEach(k => packageScripts.push(k));
3685
+ }
3686
+ } catch (e) {}
3687
+ }
3688
+
3689
+ const githubWorkflows = [];
3690
+ const githubDir = join(target, '.github', 'workflows');
3691
+ if (existsSync(githubDir)) {
3692
+ try {
3693
+ readdirSync(githubDir).forEach(f => {
3694
+ if (f.endsWith('.yml') || f.endsWith('.yaml')) githubWorkflows.push(f);
3695
+ });
3696
+ } catch (e) {}
3697
+ }
3698
+
3699
+ const envRiskMarkers = [];
3700
+ files.forEach(f => {
3701
+ const name = f.relPath.toLowerCase();
3702
+ if (name.includes('.env') || name.includes('id_rsa') || name.includes('credential') || name.endsWith('.pem') || name.endsWith('.key') || name.endsWith('.keystore') || name.endsWith('.jks')) {
3703
+ envRiskMarkers.push(f.relPath);
3704
+ }
3705
+ });
3706
+
3707
+ return {
3708
+ packageManagers,
3709
+ frameworks,
3710
+ language,
3711
+ repoType,
3712
+ existingTools,
3713
+ packageScripts,
3714
+ githubWorkflows,
3715
+ envRiskMarkers,
3716
+ aiSignals,
3717
+ filesCount: files.length,
3718
+ ignoredCount
3719
+ };
3720
+ }
3721
+
3722
+ function getRecommendation(analysis) {
3723
+ const scores = {
3724
+ 'nextjs-saas': 0.0,
3725
+ 'expo-react-native-android': 0.0,
3726
+ 'wordpress-site': 0.0,
3727
+ 'ecommerce-store': 0.0,
3728
+ 'seo-landing-page': 0.0,
3729
+ 'general-app': 0.1
3730
+ };
3731
+
3732
+ if (analysis.frameworks.includes('Next.js')) scores['nextjs-saas'] += 0.6;
3733
+ if (analysis.frameworks.includes('React')) scores['nextjs-saas'] += 0.2;
3734
+ if (analysis.frameworks.includes('TypeScript')) scores['nextjs-saas'] += 0.1;
3735
+
3736
+ if (analysis.repoType === 'mobile app') scores['expo-react-native-android'] += 0.6;
3737
+ if (analysis.frameworks.includes('Expo') || analysis.frameworks.includes('React Native')) scores['expo-react-native-android'] += 0.3;
3738
+
3739
+ if (analysis.repoType === 'WordPress theme/plugin') scores['wordpress-site'] += 0.6;
3740
+ if (analysis.frameworks.includes('WordPress/PHP')) scores['wordpress-site'] += 0.3;
3741
+
3742
+ if (analysis.frameworks.includes('Vite') || analysis.frameworks.includes('React')) scores['seo-landing-page'] += 0.3;
3743
+
3744
+ let recommended = 'general-app';
3745
+ let maxScore = 0.0;
3746
+ Object.keys(scores).forEach(k => {
3747
+ if (scores[k] > maxScore) {
3748
+ maxScore = scores[k];
3749
+ recommended = k;
3750
+ }
3751
+ });
3752
+
3753
+ const suggestedAdapters = ['cursor', 'claude', 'gemini', 'vscode', 'antigravity'];
3754
+
3755
+ return {
3756
+ template: recommended,
3757
+ confidence: Math.min(1.0, maxScore === 0.1 ? 0.5 : maxScore),
3758
+ suggestedAdapters,
3759
+ riskNotes: analysis.envRiskMarkers.length > 0 ? 'Workspace contains unignored credentials or key files. Ensure .gitignore covers them.' : 'None'
3760
+ };
3761
+ }
3762
+
3763
+ function handleOnboardAnalyze(options) {
3764
+ console.log(`\n🔍 \x1b[36mAnalyzing Workspace for Onboarding: ${options.target}\x1b[0m`);
3765
+ console.log('==================================================');
3766
+ const analysis = getAnalysis(options.target);
3767
+
3768
+ console.log(` Package Manager: ${analysis.packageManagers.join(', ') || 'None'}`);
3769
+ console.log(` Detected Frameworks: ${analysis.frameworks.join(', ') || 'None'}`);
3770
+ console.log(` Dominant Language: ${analysis.language}`);
3771
+ console.log(` Repository Type: ${analysis.repoType}`);
3772
+ console.log(` Existing AI Tools: ${analysis.existingTools.join(', ') || 'None'}`);
3773
+ console.log(` GitHub Workflows: ${analysis.githubWorkflows.join(', ') || 'None'}`);
3774
+ console.log(` Security Risk Markers: ${analysis.envRiskMarkers.length} files found`);
3775
+ if (analysis.envRiskMarkers.length > 0) {
3776
+ analysis.envRiskMarkers.forEach(m => console.log(` └─> ${m} (potential secrets exposure risk)`));
3777
+ }
3778
+ console.log();
3779
+ }
3780
+
3781
+ function handleOnboardRecommend(options) {
3782
+ const analysis = getAnalysis(options.target);
3783
+ const rec = getRecommendation(analysis);
3784
+
3785
+ console.log(`\n💡 \x1b[36mOnboarding Recommendation for: ${options.target}\x1b[0m`);
3786
+ console.log('==================================================');
3787
+ console.log(` Recommended Template: \x1b[32m${rec.template}\x1b[0m`);
3788
+ console.log(` Confidence Score: ${(rec.confidence * 100).toFixed(0)}%`);
3789
+ console.log(` Suggested Adapters: ${rec.suggestedAdapters.join(', ')}`);
3790
+ console.log(` Risk Notes: ${rec.riskNotes}`);
3791
+ console.log(` Suggested Next Command:`);
3792
+ console.log(` npx multimodel-dev-os onboard plan --target .`);
3793
+ console.log();
3794
+ }
3795
+
3796
+ function handleOnboardPlan(options) {
3797
+ console.log(`\n📋 \x1b[36mGenerating Onboarding Plan: ${options.target}\x1b[0m`);
3798
+ console.log('==================================================');
3799
+ const analysis = getAnalysis(options.target);
3800
+ const rec = getRecommendation(analysis);
3801
+
3802
+ const planPath = join(options.target, '.ai', 'intelligence', 'onboarding.plan.json');
3803
+ const reportPath = join(options.target, '.ai', 'intelligence', 'onboarding.report.md');
3804
+
3805
+ const plannedFiles = [
3806
+ { action: 'CREATE', path: 'AGENTS.md', source_template: `examples/${rec.template}/AGENTS.md` },
3807
+ { action: 'CREATE', path: 'MEMORY.md', source_template: `examples/${rec.template}/MEMORY.md` },
3808
+ { action: 'CREATE', path: 'TASKS.md', source_template: `examples/${rec.template}/TASKS.md` },
3809
+ { action: 'CREATE', path: 'RUNBOOK.md', source_template: `RUNBOOK.md` },
3810
+ { action: 'CREATE', path: '.ai/config.yaml', source_template: `examples/${rec.template}/.ai/config.yaml` }
3811
+ ];
3812
+
3813
+ const planData = {
3814
+ generated_at: new Date().toISOString(),
3815
+ target_path: options.target,
3816
+ project_analysis: {
3817
+ package_manager: analysis.packageManagers.join(', ') || 'npm',
3818
+ framework: analysis.frameworks.join(', ') || 'Generic',
3819
+ language: analysis.language,
3820
+ repo_type: analysis.repoType,
3821
+ has_existing_ai_config: analysis.aiSignals.includes('.ai/config.yaml'),
3822
+ risk_markers: analysis.envRiskMarkers
3823
+ },
3824
+ recommendation: {
3825
+ template: rec.template,
3826
+ confidence: rec.confidence,
3827
+ suggested_adapters: rec.suggestedAdapters,
3828
+ reasons: [`Detected dominant language ${analysis.language}`, `Detected framework ${analysis.frameworks.join(', ')}`]
3829
+ },
3830
+ planned_files: plannedFiles
3831
+ };
3832
+
3833
+ let reportMd = `# MultiModel Dev OS Onboarding Report\n\n`;
3834
+ reportMd += `**Generated At:** ${planData.generated_at}\n`;
3835
+ reportMd += `**Target Path:** ${planData.target_path}\n\n`;
3836
+ reportMd += `## 1. Project Analysis Details\n`;
3837
+ reportMd += `- **Package Manager:** ${planData.project_analysis.package_manager}\n`;
3838
+ reportMd += `- **Frameworks:** ${planData.project_analysis.framework}\n`;
3839
+ reportMd += `- **Language:** ${planData.project_analysis.language}\n`;
3840
+ reportMd += `- **Repo Type:** ${planData.project_analysis.repo_type}\n\n`;
3841
+
3842
+ reportMd += `## 2. Onboarding Recommendation\n`;
3843
+ reportMd += `- **Recommended Profile:** **${planData.recommendation.template}** (Confidence: ${(planData.recommendation.confidence * 100).toFixed(0)}%)\n`;
3844
+ reportMd += `- **Suggested Adapters:** ${planData.recommendation.suggested_adapters.join(', ')}\n\n`;
3845
+
3846
+ reportMd += `## 3. Planned File Operations\n`;
3847
+ reportMd += `| Action | Target Path | Source Template |\n`;
3848
+ reportMd += `|---|---|---|\n`;
3849
+ plannedFiles.forEach(f => {
3850
+ reportMd += `| ${f.action} | ${f.path} | ${f.source_template} |\n`;
3851
+ });
3852
+ reportMd += `\n`;
3853
+
3854
+ reportMd += `## 4. Next Step\n`;
3855
+ reportMd += `To safely apply this plan, run:\n`;
3856
+ reportMd += `\`\`\`bash\n`;
3857
+ reportMd += `npx multimodel-dev-os onboard apply --target . --approved\n`;
3858
+ reportMd += `\`\`\`\n`;
3859
+
3860
+ try {
3861
+ const intelDir = join(options.target, '.ai', 'intelligence');
3862
+ if (!options.dryRun && !existsSync(intelDir)) {
3863
+ mkdirSync(intelDir, { recursive: true });
3864
+ }
3865
+ if (!options.dryRun) {
3866
+ writeFileSync(planPath, JSON.stringify(planData, null, 2), 'utf8');
3867
+ writeFileSync(reportPath, reportMd, 'utf8');
3868
+ }
3869
+
3870
+ console.log(` [SUCCESS] Onboarding plan generated:`);
3871
+ console.log(` - Plan JSON: .ai/intelligence/onboarding.plan.json`);
3872
+ console.log(` - Report MD: .ai/intelligence/onboarding.report.md`);
3873
+ console.log(`\nReview the plan and run "npx multimodel-dev-os onboard apply --target . --approved" to execute.\n`);
3874
+ } catch (e) {
3875
+ console.error(`\x1b[31mError writing plan: ${e.message}\x1b[0m`);
3876
+ }
3877
+ }
3878
+
3879
+ function handleOnboardApply(options) {
3880
+ if (!options.approved) {
3881
+ console.error('\x1b[31mError: Onboarding apply requires explicit approval flag: --approved\x1b[0m');
3882
+ console.log('Example: node bin/multimodel-dev-os.js onboard apply --approved');
3883
+ process.exit(1);
3884
+ }
3885
+
3886
+ const planPath = join(options.target, '.ai', 'intelligence', 'onboarding.plan.json');
3887
+ if (!existsSync(planPath)) {
3888
+ console.error('\x1b[31mError: Onboarding plan not found. Run "npx multimodel-dev-os onboard plan" first.\x1b[0m');
3889
+ process.exit(1);
3890
+ }
3891
+
3892
+ let plan;
3893
+ try {
3894
+ plan = JSON.parse(readFileSync(planPath, 'utf8'));
3895
+ } catch (e) {
3896
+ console.error(`\x1b[31mError reading plan JSON: ${e.message}\x1b[0m`);
3897
+ process.exit(1);
3898
+ }
3899
+
3900
+ console.log(`\n🚀 \x1b[36mApplying Onboarding Scaffolding: ${options.target}\x1b[0m`);
3901
+ console.log('==================================================');
3902
+
3903
+ const template = plan.recommendation.template;
3904
+ options.template = template;
3905
+
3906
+ const operations = [];
3907
+
3908
+ plan.planned_files.forEach(f => {
3909
+ let srcFile;
3910
+ if (f.source_template === 'RUNBOOK.md') {
3911
+ srcFile = join(sourceRoot, 'RUNBOOK.md');
3912
+ } else {
3913
+ srcFile = join(sourceRoot, f.source_template);
3914
+ }
3915
+ operations.push({ dest: f.path, src: srcFile });
3916
+ });
3917
+
3918
+ const templateDir = join(sourceRoot, 'examples', template);
3919
+ const templateAiDir = join(templateDir, '.ai');
3920
+ if (existsSync(templateAiDir) && !options.caveman) {
3921
+ const subdirs = ['context', 'skills'];
3922
+ subdirs.forEach(sub => {
3923
+ const subPath = join(templateAiDir, sub);
3924
+ if (existsSync(subPath)) {
3925
+ readdirSync(subPath).forEach(file => {
3926
+ operations.push({
3927
+ dest: join('.ai', sub, file),
3928
+ src: join(subPath, file)
3929
+ });
3930
+ });
3931
+ }
3932
+ });
3933
+ }
3934
+
3935
+ const globalAiSubdirs = ['context', 'agents', 'skills', 'prompts', 'checks', 'templates', 'session-logs', 'registries', 'proposals', 'intelligence'];
3936
+ globalAiSubdirs.forEach(sub => {
3937
+ const globalPath = join(sourceRoot, '.ai', sub);
3938
+ if (existsSync(globalPath)) {
3939
+ readdirSync(globalPath).forEach(file => {
3940
+ const destRel = join('.ai', sub, file);
3941
+ if (!operations.some(op => op.dest === destRel)) {
3942
+ if (options.caveman && (sub === 'context' || sub === 'skills' || sub === 'prompts' || sub === 'checks')) {
3943
+ return;
3944
+ }
3945
+ operations.push({
3946
+ dest: destRel,
3947
+ src: join(globalPath, file)
3948
+ });
3949
+ }
3950
+ });
3951
+ }
3952
+ });
3953
+
3954
+ let createdCount = 0;
3955
+ let skippedCount = 0;
3956
+ let updatedCount = 0;
3957
+
3958
+ operations.forEach(op => {
3959
+ const destPath = join(options.target, op.dest);
3960
+ const destDir = dirname(destPath);
3961
+
3962
+ if (existsSync(destPath)) {
3963
+ if (options.force) {
3964
+ if (!options.dryRun) {
3965
+ const backupPath = destPath + '.bak';
3966
+ writeFileSync(backupPath, readFileSync(destPath));
3967
+ if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
3968
+ writeFileSync(destPath, readFileSync(op.src));
3969
+ console.log(` \x1b[33mOVERWRITE (BACKUP CREATED):\x1b[0m ${op.dest} -> ${op.dest}.bak`);
3970
+ } else {
3971
+ console.log(` \x1b[36m[DRY-RUN] WOULD OVERWRITE & BACKUP:\x1b[0m ${op.dest}`);
3972
+ }
3973
+ updatedCount++;
3974
+ } else {
3975
+ console.log(` \x1b[37m[SKIP] Already exists:\x1b[0m ${op.dest}`);
3976
+ skippedCount++;
3977
+ }
3978
+ } else {
3979
+ if (!options.dryRun) {
3980
+ if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
3981
+ writeFileSync(destPath, readFileSync(op.src));
3982
+ console.log(` \x1b[32mCREATE:\x1b[0m ${op.dest}`);
3983
+ } else {
3984
+ console.log(` \x1b[36m[DRY-RUN] WOULD CREATE:\x1b[0m ${op.dest}`);
3985
+ }
3986
+ createdCount++;
3987
+ }
3988
+ });
3989
+
3990
+ console.log(`\n✔ Onboarding apply complete! Created: ${createdCount}, Skipped: ${skippedCount}, Overwritten (with backup): ${updatedCount}\n`);
3991
+ }
3992
+
3993
+ function handleOnboardStatus(options) {
3994
+ console.log(`\n📊 \x1b[36mOnboarding Status Dashboard: ${options.target}\x1b[0m`);
3995
+ console.log('==================================================');
3996
+
3997
+ const crucialFiles = [
3998
+ 'AGENTS.md',
3999
+ 'MEMORY.md',
4000
+ 'TASKS.md',
4001
+ 'RUNBOOK.md',
4002
+ '.ai/config.yaml'
4003
+ ];
4004
+
4005
+ let presentCount = 0;
4006
+ crucialFiles.forEach(f => {
4007
+ const fullPath = join(options.target, f);
4008
+ const exists = existsSync(fullPath);
4009
+ if (exists) presentCount++;
4010
+ console.log(` [${exists ? '✔' : ' '}] ${f}`);
4011
+ });
4012
+
4013
+ const percentage = (presentCount / crucialFiles.length) * 100;
4014
+ console.log(`\n Completeness Score: ${percentage.toFixed(0)}%`);
4015
+ if (percentage === 100) {
4016
+ console.log(' Status: \x1b[32mREADY (Onboarding complete)\x1b[0m\n');
4017
+ } else if (percentage > 0) {
4018
+ console.log(' Status: \x1b[33mIN_PROGRESS (Run "onboard apply --approved" to initialize remaining files)\x1b[0m\n');
4019
+ } else {
4020
+ console.log(' Status: \x1b[31mMISSING (Run "onboard plan" and "onboard apply" to onboard this repo)\x1b[0m\n');
4021
+ }
4022
+ }
4023
+
4024
+ function getEnabledAdapters(target) {
4025
+ const configPath = join(target, '.ai', 'config.yaml');
4026
+ if (existsSync(configPath)) {
4027
+ try {
4028
+ const config = parseYaml(readFileSync(configPath, 'utf8')) || {};
4029
+ return config.adapters || {};
4030
+ } catch (e) {}
4031
+ }
4032
+ return {};
4033
+ }
4034
+
4035
+ function handleAdapterStatus(options) {
4036
+ console.log(`\n🔌 \x1b[36mIDE & Agent Adapters Status: ${options.target}\x1b[0m`);
4037
+ console.log('==================================================');
4038
+
4039
+ const enabled = getEnabledAdapters(options.target);
4040
+
4041
+ Object.keys(ADAPTERS).forEach(name => {
4042
+ const a = ADAPTERS[name];
4043
+ const isEnabled = enabled[name] || false;
4044
+ const rulesFile = a.rules_file;
4045
+ const exists = existsSync(join(options.target, rulesFile));
4046
+
4047
+ let statusStr = '\x1b[31mMISSING\x1b[0m';
4048
+ if (exists) {
4049
+ statusStr = '\x1b[32mINSTALLED\x1b[0m';
4050
+ }
4051
+
4052
+ console.log(`\n\x1b[33m* ${a.name || name}\x1b[0m (${name})`);
4053
+ console.log(` Config Status: ${isEnabled ? '\x1b[32mENABLED\x1b[0m' : '\x1b[37mDISABLED\x1b[0m'}`);
4054
+ console.log(` File Status: ${statusStr} (${rulesFile})`);
4055
+ });
4056
+ console.log();
4057
+ }
4058
+
4059
+ function printDiff(srcContent, destContent, filename) {
4060
+ console.log(`\nDiff for ${filename}:`);
4061
+ console.log('--------------------------------------------------');
4062
+ if (srcContent === destContent) {
4063
+ console.log(' Pristine (No differences detected)');
4064
+ return;
4065
+ }
4066
+ const srcLines = srcContent.split(/\r?\n/);
4067
+ const destLines = destContent.split(/\r?\n/);
4068
+
4069
+ let i = 0;
4070
+ while (i < Math.max(srcLines.length, destLines.length)) {
4071
+ const sLine = srcLines[i];
4072
+ const dLine = destLines[i];
4073
+ if (sLine !== dLine) {
4074
+ if (dLine !== undefined) console.log(`\x1b[31m- ${dLine}\x1b[0m`);
4075
+ if (sLine !== undefined) console.log(`\x1b[32m+ ${sLine}\x1b[0m`);
4076
+ } else {
4077
+ if (sLine !== undefined) console.log(` ${sLine}`);
4078
+ }
4079
+ i++;
4080
+ }
4081
+ }
4082
+
4083
+ function handleAdapterDiff(aName, options) {
4084
+ const adaptersToDiff = [];
4085
+ if (aName === 'all') {
4086
+ const enabled = getEnabledAdapters(options.target);
4087
+ Object.keys(ADAPTERS).forEach(name => {
4088
+ if (enabled[name]) adaptersToDiff.push(name);
4089
+ });
4090
+ } else {
4091
+ if (!ADAPTERS[aName]) {
4092
+ console.error(`\x1b[31mError: Adapter '${aName}' not found in registry.\x1b[0m`);
4093
+ process.exit(1);
4094
+ }
4095
+ adaptersToDiff.push(aName);
4096
+ }
4097
+
4098
+ if (adaptersToDiff.length === 0) {
4099
+ console.log('No enabled adapters found to diff.');
4100
+ return;
4101
+ }
4102
+
4103
+ adaptersToDiff.forEach(name => {
4104
+ const a = ADAPTERS[name];
4105
+ const srcFile = join(sourceRoot, 'adapters', name, a.rules_file);
4106
+ const destFile = join(options.target, a.rules_file);
4107
+
4108
+ if (!existsSync(srcFile)) {
4109
+ console.warn(`Warning: Source file for adapter '${name}' is missing at: ${srcFile}`);
4110
+ return;
4111
+ }
4112
+
4113
+ const srcContent = readFileSync(srcFile, 'utf8');
4114
+ if (existsSync(destFile)) {
4115
+ const destContent = readFileSync(destFile, 'utf8');
4116
+ printDiff(srcContent, destContent, a.rules_file);
4117
+ } else {
4118
+ console.log(`\nFile: ${a.rules_file} \x1b[31m(MISSING)\x1b[0m`);
4119
+ console.log('--------------------------------------------------');
4120
+ srcContent.split(/\r?\n/).forEach(l => console.log(`\x1b[32m+ ${l}\x1b[0m`));
4121
+ }
4122
+ });
4123
+ }
4124
+
4125
+ function handleAdapterSync(aName, options) {
4126
+ if (!options.approved) {
4127
+ console.error('\x1b[31mError: Adapter sync requires explicit approval flag: --approved\x1b[0m');
4128
+ console.log('Example: node bin/multimodel-dev-os.js adapter sync cursor --approved');
4129
+ process.exit(1);
4130
+ }
4131
+
4132
+ const adaptersToSync = [];
4133
+ if (aName === 'all') {
4134
+ const enabled = getEnabledAdapters(options.target);
4135
+ Object.keys(ADAPTERS).forEach(name => {
4136
+ if (enabled[name]) adaptersToSync.push(name);
4137
+ });
4138
+ } else {
4139
+ if (!ADAPTERS[aName]) {
4140
+ console.error(`\x1b[31mError: Adapter '${aName}' not found in registry.\x1b[0m`);
4141
+ process.exit(1);
4142
+ }
4143
+ adaptersToSync.push(aName);
4144
+ }
4145
+
4146
+ if (adaptersToSync.length === 0) {
4147
+ console.log('No adapters found to sync.');
4148
+ return;
4149
+ }
4150
+
4151
+ console.log(`\n🔄 \x1b[36mSynchronizing IDE Adapters in: ${options.target}\x1b[0m`);
4152
+ console.log('==================================================');
4153
+
4154
+ adaptersToSync.forEach(name => {
4155
+ const a = ADAPTERS[name];
4156
+ const srcFile = join(sourceRoot, 'adapters', name, a.rules_file);
4157
+ const destFile = join(options.target, a.rules_file);
4158
+ const destDir = dirname(destFile);
4159
+
4160
+ if (!existsSync(srcFile)) {
4161
+ console.warn(`Warning: Source file for adapter '${name}' is missing at: ${srcFile}`);
4162
+ return;
4163
+ }
4164
+
4165
+ if (existsSync(destFile)) {
4166
+ if (options.force) {
4167
+ if (!options.dryRun) {
4168
+ const backupPath = destFile + '.bak';
4169
+ writeFileSync(backupPath, readFileSync(destFile));
4170
+ if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
4171
+ writeFileSync(destFile, readFileSync(srcFile));
4172
+ console.log(` \x1b[33mOVERWRITE (BACKUP CREATED):\x1b[0m ${a.rules_file} -> ${a.rules_file}.bak`);
4173
+ } else {
4174
+ console.log(` \x1b[36m[DRY-RUN] WOULD OVERWRITE & BACKUP:\x1b[0m ${a.rules_file}`);
4175
+ }
4176
+ } else {
4177
+ console.log(` \x1b[37m[SKIP] Already exists:\x1b[0m ${a.rules_file}`);
4178
+ }
4179
+ } else {
4180
+ if (!options.dryRun) {
4181
+ if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
4182
+ writeFileSync(destFile, readFileSync(srcFile));
4183
+ console.log(` \x1b[32mCREATE:\x1b[0m ${a.rules_file}`);
4184
+ } else {
4185
+ console.log(` \x1b[36m[DRY-RUN] WOULD CREATE:\x1b[0m ${a.rules_file}`);
4186
+ }
4187
+ }
4188
+ });
4189
+
4190
+ console.log();
4191
+ }
4192
+
4193
+ function handleDoctorOnboarding(options) {
4194
+ console.log(`\n🩺 \x1b[36mRunning advisory onboarding doctor checkup in: ${options.target}\x1b[0m\n`);
4195
+
4196
+ let warnings = 0;
4197
+ const warn = (msg) => {
4198
+ console.warn(` \x1b[33m[WARNING]\x1b[0m ${msg}`);
4199
+ warnings++;
4200
+ };
4201
+
4202
+ const crucialFiles = [
4203
+ 'AGENTS.md',
4204
+ 'MEMORY.md',
4205
+ 'TASKS.md',
4206
+ 'RUNBOOK.md'
4207
+ ];
4208
+
4209
+ crucialFiles.forEach(f => {
4210
+ if (!existsSync(join(options.target, f))) {
4211
+ warn(`Crucial onboarding file '${f}' is missing from project root.`);
4212
+ }
4213
+ });
4214
+
4215
+ const configPath = join(options.target, '.ai', 'config.yaml');
4216
+ if (!existsSync(configPath)) {
4217
+ warn('MultiModel Dev OS configuration file (.ai/config.yaml) is missing.');
4218
+ }
4219
+
4220
+ const registriesDir = join(options.target, '.ai', 'registries');
4221
+ if (!existsSync(registriesDir)) {
4222
+ warn('Registries directory (.ai/registries) is missing.');
4223
+ }
4224
+
4225
+ const proposalsDir = join(options.target, '.ai', 'proposals');
4226
+ if (!existsSync(proposalsDir)) {
4227
+ warn('Proposals directory (.ai/proposals) is missing.');
4228
+ }
4229
+
4230
+ const intelligenceDir = join(options.target, '.ai', 'intelligence');
4231
+ if (!existsSync(intelligenceDir)) {
4232
+ warn('Intelligence directory (.ai/intelligence) is missing.');
4233
+ }
4234
+
4235
+ const gitignorePath = join(options.target, '.gitignore');
4236
+ if (existsSync(gitignorePath)) {
4237
+ const gitignoreContent = readFileSync(gitignorePath, 'utf8');
4238
+ const checkIgnore = (pattern) => {
4239
+ if (!gitignoreContent.includes(pattern)) {
4240
+ warn(`Generated runtime file '${pattern}' is not ignored in .gitignore.`);
4241
+ }
4242
+ };
4243
+ checkIgnore('onboarding.plan.json');
4244
+ checkIgnore('onboarding.report.md');
4245
+ }
4246
+
4247
+ const { files } = scanTarget(options.target);
4248
+ const packageManagers = detectDependencySignals(files, options.target);
4249
+ if (packageManagers.length === 0) {
4250
+ warn('No package manager lockfile detected in project root.');
4251
+ }
4252
+
4253
+ console.log('\n==================================================');
4254
+ if (warnings > 0) {
4255
+ console.log(`\x1b[33mDoctor onboarding check complete. Found ${warnings} warnings.\x1b[0m\n`);
4256
+ } else {
4257
+ console.log('\x1b[32m✔ Doctor onboarding check complete. Your workspace onboarding setup is pristine!\x1b[0m\n');
4258
+ }
4259
+ }
4260
+
4261
+