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