@mainahq/core 1.0.2 → 1.1.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 (75) hide show
  1. package/package.json +1 -1
  2. package/src/ai/__tests__/availability.test.ts +131 -0
  3. package/src/ai/__tests__/delegation.test.ts +55 -1
  4. package/src/ai/availability.ts +23 -0
  5. package/src/ai/delegation.ts +5 -3
  6. package/src/context/__tests__/budget.test.ts +29 -6
  7. package/src/context/__tests__/engine.test.ts +1 -0
  8. package/src/context/__tests__/selector.test.ts +23 -3
  9. package/src/context/__tests__/wiki.test.ts +349 -0
  10. package/src/context/budget.ts +12 -8
  11. package/src/context/engine.ts +37 -0
  12. package/src/context/selector.ts +30 -4
  13. package/src/context/wiki.ts +296 -0
  14. package/src/db/index.ts +12 -0
  15. package/src/feedback/__tests__/capture.test.ts +166 -0
  16. package/src/feedback/__tests__/signals.test.ts +144 -0
  17. package/src/feedback/__tests__/tmp-capture-1775575256633-lah0etnzlj/feedback.db +0 -0
  18. package/src/feedback/__tests__/tmp-capture-1775575256640-2xmjme4qraa/feedback.db +0 -0
  19. package/src/feedback/capture.ts +102 -0
  20. package/src/feedback/signals.ts +68 -0
  21. package/src/index.ts +108 -1
  22. package/src/init/__tests__/init.test.ts +477 -18
  23. package/src/init/index.ts +419 -13
  24. package/src/language/__tests__/__fixtures__/detect/composer.lock +1 -0
  25. package/src/prompts/defaults/index.ts +3 -1
  26. package/src/prompts/defaults/wiki-compile.md +20 -0
  27. package/src/prompts/defaults/wiki-query.md +18 -0
  28. package/src/stats/__tests__/tool-usage.test.ts +133 -0
  29. package/src/stats/tracker.ts +92 -0
  30. package/src/verify/__tests__/builtin.test.ts +270 -0
  31. package/src/verify/__tests__/pipeline.test.ts +11 -8
  32. package/src/verify/builtin.ts +350 -0
  33. package/src/verify/pipeline.ts +32 -2
  34. package/src/verify/tools/__tests__/wiki-lint.test.ts +784 -0
  35. package/src/verify/tools/wiki-lint-runner.ts +38 -0
  36. package/src/verify/tools/wiki-lint.ts +898 -0
  37. package/src/wiki/__tests__/compiler.test.ts +389 -0
  38. package/src/wiki/__tests__/extractors/code.test.ts +99 -0
  39. package/src/wiki/__tests__/extractors/decision.test.ts +323 -0
  40. package/src/wiki/__tests__/extractors/feature.test.ts +186 -0
  41. package/src/wiki/__tests__/extractors/workflow.test.ts +131 -0
  42. package/src/wiki/__tests__/graph.test.ts +344 -0
  43. package/src/wiki/__tests__/hooks.test.ts +119 -0
  44. package/src/wiki/__tests__/indexer.test.ts +285 -0
  45. package/src/wiki/__tests__/linker.test.ts +230 -0
  46. package/src/wiki/__tests__/louvain.test.ts +229 -0
  47. package/src/wiki/__tests__/query.test.ts +316 -0
  48. package/src/wiki/__tests__/schema.test.ts +114 -0
  49. package/src/wiki/__tests__/signals.test.ts +474 -0
  50. package/src/wiki/__tests__/state.test.ts +168 -0
  51. package/src/wiki/__tests__/tracking.test.ts +118 -0
  52. package/src/wiki/__tests__/types.test.ts +387 -0
  53. package/src/wiki/compiler.ts +1075 -0
  54. package/src/wiki/extractors/code.ts +90 -0
  55. package/src/wiki/extractors/decision.ts +217 -0
  56. package/src/wiki/extractors/feature.ts +206 -0
  57. package/src/wiki/extractors/workflow.ts +112 -0
  58. package/src/wiki/graph.ts +445 -0
  59. package/src/wiki/hooks.ts +49 -0
  60. package/src/wiki/indexer.ts +105 -0
  61. package/src/wiki/linker.ts +117 -0
  62. package/src/wiki/louvain.ts +190 -0
  63. package/src/wiki/prompts/compile-architecture.md +59 -0
  64. package/src/wiki/prompts/compile-decision.md +66 -0
  65. package/src/wiki/prompts/compile-entity.md +56 -0
  66. package/src/wiki/prompts/compile-feature.md +60 -0
  67. package/src/wiki/prompts/compile-module.md +42 -0
  68. package/src/wiki/prompts/wiki-query.md +25 -0
  69. package/src/wiki/query.ts +338 -0
  70. package/src/wiki/schema.ts +111 -0
  71. package/src/wiki/signals.ts +368 -0
  72. package/src/wiki/state.ts +89 -0
  73. package/src/wiki/tracking.ts +30 -0
  74. package/src/wiki/types.ts +169 -0
  75. package/src/workflow/context.ts +26 -0
@@ -131,29 +131,21 @@ describe("bootstrap", () => {
131
131
  expect(existsSync(hooksDir)).toBe(true);
132
132
  });
133
133
 
134
- test("skips existing files (no overwrite)", async () => {
134
+ test("skips existing non-agent files (no overwrite)", async () => {
135
135
  // Pre-create constitution.md with custom content
136
136
  const mainaDir = join(tmpDir, ".maina");
137
137
  mkdirSync(mainaDir, { recursive: true });
138
138
  const constitutionPath = join(mainaDir, "constitution.md");
139
139
  writeFileSync(constitutionPath, "# My Custom Constitution\n");
140
140
 
141
- // Pre-create AGENTS.md with custom content
142
- const agentsPath = join(tmpDir, "AGENTS.md");
143
- writeFileSync(agentsPath, "# My Custom Agents\n");
144
-
145
141
  const result = await bootstrap(tmpDir);
146
142
  expect(result.ok).toBe(true);
147
143
  if (result.ok) {
148
144
  expect(result.value.skipped).toContain(".maina/constitution.md");
149
- expect(result.value.skipped).toContain("AGENTS.md");
150
145
 
151
146
  // Content should NOT have been overwritten
152
147
  const content = readFileSync(constitutionPath, "utf-8");
153
148
  expect(content).toBe("# My Custom Constitution\n");
154
-
155
- const agentsContent = readFileSync(agentsPath, "utf-8");
156
- expect(agentsContent).toBe("# My Custom Agents\n");
157
149
  }
158
150
  });
159
151
 
@@ -195,8 +187,11 @@ describe("bootstrap", () => {
195
187
  expect(result.value.created).toContain(".maina/prompts/review.md");
196
188
  expect(result.value.created).toContain(".maina/prompts/commit.md");
197
189
 
198
- // Total files should add up
199
- const total = result.value.created.length + result.value.skipped.length;
190
+ // Total files should add up (created + skipped + updated)
191
+ const total =
192
+ result.value.created.length +
193
+ result.value.skipped.length +
194
+ result.value.updated.length;
200
195
  expect(total).toBeGreaterThanOrEqual(5);
201
196
  }
202
197
  });
@@ -279,7 +274,12 @@ describe("bootstrap", () => {
279
274
 
280
275
  // ── .mcp.json generation ──────────────────────────────────────────────
281
276
 
282
- test("creates .mcp.json at repo root", async () => {
277
+ test("creates .mcp.json at repo root with npx for node runtime", async () => {
278
+ writeFileSync(
279
+ join(tmpDir, "package.json"),
280
+ JSON.stringify({ dependencies: { express: "^4" } }),
281
+ );
282
+
283
283
  const result = await bootstrap(tmpDir);
284
284
  expect(result.ok).toBe(true);
285
285
 
@@ -289,8 +289,25 @@ describe("bootstrap", () => {
289
289
  const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
290
290
  expect(content.mcpServers).toBeDefined();
291
291
  expect(content.mcpServers.maina).toBeDefined();
292
- expect(content.mcpServers.maina.command).toBe("maina");
293
- expect(content.mcpServers.maina.args).toEqual(["--mcp"]);
292
+ expect(content.mcpServers.maina.command).toBe("npx");
293
+ expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
294
+ });
295
+
296
+ test("creates .mcp.json with bunx for bun runtime", async () => {
297
+ writeFileSync(
298
+ join(tmpDir, "package.json"),
299
+ JSON.stringify({ devDependencies: { "@types/bun": "latest" } }),
300
+ );
301
+
302
+ const result = await bootstrap(tmpDir);
303
+ expect(result.ok).toBe(true);
304
+
305
+ const mcpPath = join(tmpDir, ".mcp.json");
306
+ expect(existsSync(mcpPath)).toBe(true);
307
+
308
+ const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
309
+ expect(content.mcpServers.maina.command).toBe("bunx");
310
+ expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
294
311
  });
295
312
 
296
313
  test("does not overwrite existing .mcp.json", async () => {
@@ -351,24 +368,91 @@ describe("bootstrap", () => {
351
368
  expect(content).toContain("maina verify");
352
369
  });
353
370
 
354
- test("does not overwrite existing agent files", async () => {
371
+ test("merges maina section into existing agent files without ## Maina", async () => {
355
372
  writeFileSync(join(tmpDir, "CLAUDE.md"), "# My Custom CLAUDE.md\n");
356
373
  writeFileSync(join(tmpDir, "GEMINI.md"), "# My Custom GEMINI.md\n");
357
374
  writeFileSync(join(tmpDir, ".cursorrules"), "# My Custom Rules\n");
358
375
 
376
+ const result = await bootstrap(tmpDir);
377
+ expect(result.ok).toBe(true);
378
+ if (result.ok) {
379
+ expect(result.value.updated).toContain("CLAUDE.md");
380
+ expect(result.value.updated).toContain("GEMINI.md");
381
+ expect(result.value.updated).toContain(".cursorrules");
382
+ expect(result.value.skipped).not.toContain("CLAUDE.md");
383
+ expect(result.value.skipped).not.toContain("GEMINI.md");
384
+ expect(result.value.skipped).not.toContain(".cursorrules");
385
+ }
386
+
387
+ const claudeContent = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8");
388
+ expect(claudeContent).toContain("# My Custom CLAUDE.md");
389
+ expect(claudeContent).toContain("## Maina");
390
+ expect(claudeContent).toContain("constitution.md");
391
+ expect(claudeContent).toContain("getContext");
392
+
393
+ const geminiContent = readFileSync(join(tmpDir, "GEMINI.md"), "utf-8");
394
+ expect(geminiContent).toContain("# My Custom GEMINI.md");
395
+ expect(geminiContent).toContain("## Maina");
396
+
397
+ const cursorContent = readFileSync(join(tmpDir, ".cursorrules"), "utf-8");
398
+ expect(cursorContent).toContain("# My Custom Rules");
399
+ expect(cursorContent).toContain("## Maina");
400
+ });
401
+
402
+ test("skips agent files that already have ## Maina section", async () => {
403
+ writeFileSync(
404
+ join(tmpDir, "CLAUDE.md"),
405
+ "# My CLAUDE.md\n\n## Maina\n\nAlready configured.\n",
406
+ );
407
+ writeFileSync(
408
+ join(tmpDir, "GEMINI.md"),
409
+ "# My GEMINI.md\n\n## Maina\n\nAlready configured.\n",
410
+ );
411
+
359
412
  const result = await bootstrap(tmpDir);
360
413
  expect(result.ok).toBe(true);
361
414
  if (result.ok) {
362
415
  expect(result.value.skipped).toContain("CLAUDE.md");
363
416
  expect(result.value.skipped).toContain("GEMINI.md");
364
- expect(result.value.skipped).toContain(".cursorrules");
417
+ expect(result.value.updated).not.toContain("CLAUDE.md");
418
+ expect(result.value.updated).not.toContain("GEMINI.md");
365
419
  }
366
420
 
367
- expect(readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8")).toBe(
368
- "# My Custom CLAUDE.md\n",
421
+ // Content should not be modified
422
+ const claudeContent = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8");
423
+ expect(claudeContent).toBe(
424
+ "# My CLAUDE.md\n\n## Maina\n\nAlready configured.\n",
369
425
  );
370
426
  });
371
427
 
428
+ test("merges AGENTS.md with maina section", async () => {
429
+ writeFileSync(
430
+ join(tmpDir, "AGENTS.md"),
431
+ "# My Agents File\n\nCustom content here.\n",
432
+ );
433
+
434
+ const result = await bootstrap(tmpDir);
435
+ expect(result.ok).toBe(true);
436
+ if (result.ok) {
437
+ expect(result.value.updated).toContain("AGENTS.md");
438
+ }
439
+
440
+ const content = readFileSync(join(tmpDir, "AGENTS.md"), "utf-8");
441
+ expect(content).toContain("# My Agents File");
442
+ expect(content).toContain("Custom content here.");
443
+ expect(content).toContain("## Maina");
444
+ expect(content).toContain("brainstorm");
445
+ expect(content).toContain("MCP Tools");
446
+ });
447
+
448
+ test("updated array is empty for fresh directory", async () => {
449
+ const result = await bootstrap(tmpDir);
450
+ expect(result.ok).toBe(true);
451
+ if (result.ok) {
452
+ expect(result.value.updated).toEqual([]);
453
+ }
454
+ });
455
+
372
456
  // ── Workflow order in agent files ─────────────────────────────────────
373
457
 
374
458
  test("AGENTS.md includes workflow order", async () => {
@@ -451,13 +535,388 @@ describe("bootstrap", () => {
451
535
  ".github/workflows/maina-ci.yml",
452
536
  ".github/copilot-instructions.md",
453
537
  ".mcp.json",
538
+ ".claude/settings.json",
454
539
  "CLAUDE.md",
455
540
  "GEMINI.md",
456
541
  ".cursorrules",
542
+ ".windsurfrules",
543
+ ".clinerules",
544
+ ".continue/config.yaml",
545
+ ".continue/mcpServers/maina.json",
546
+ ".roo/mcp.json",
547
+ ".roo/rules/maina.md",
548
+ ".amazonq/mcp.json",
549
+ ".aider.conf.yml",
550
+ "CONVENTIONS.md",
457
551
  ];
458
552
  for (const f of expectedFiles) {
459
553
  expect(result.value.created).toContain(f);
460
554
  }
461
555
  }
462
556
  });
557
+
558
+ // ── .claude/settings.json generation ──────────────────────────────────
559
+
560
+ test("creates .claude/settings.json for Claude Code MCP config", async () => {
561
+ writeFileSync(
562
+ join(tmpDir, "package.json"),
563
+ JSON.stringify({ devDependencies: { "@types/bun": "latest" } }),
564
+ );
565
+
566
+ const result = await bootstrap(tmpDir);
567
+ expect(result.ok).toBe(true);
568
+
569
+ const settingsPath = join(tmpDir, ".claude", "settings.json");
570
+ expect(existsSync(settingsPath)).toBe(true);
571
+
572
+ const content = JSON.parse(readFileSync(settingsPath, "utf-8"));
573
+ expect(content.mcpServers).toBeDefined();
574
+ expect(content.mcpServers.maina).toBeDefined();
575
+ expect(content.mcpServers.maina.command).toBe("bunx");
576
+ expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
577
+ });
578
+
579
+ test(".claude/settings.json uses npx for node runtime", async () => {
580
+ writeFileSync(
581
+ join(tmpDir, "package.json"),
582
+ JSON.stringify({ dependencies: { express: "^4" } }),
583
+ );
584
+
585
+ const result = await bootstrap(tmpDir);
586
+ expect(result.ok).toBe(true);
587
+
588
+ const settingsPath = join(tmpDir, ".claude", "settings.json");
589
+ expect(existsSync(settingsPath)).toBe(true);
590
+
591
+ const content = JSON.parse(readFileSync(settingsPath, "utf-8"));
592
+ expect(content.mcpServers.maina.command).toBe("npx");
593
+ expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
594
+ });
595
+
596
+ // ── CLAUDE.md includes wiki commands ─────────────────────────────────
597
+
598
+ test("CLAUDE.md includes wiki commands", async () => {
599
+ const result = await bootstrap(tmpDir);
600
+ expect(result.ok).toBe(true);
601
+
602
+ const content = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8");
603
+ expect(content).toContain("maina wiki init");
604
+ expect(content).toContain("maina wiki query");
605
+ expect(content).toContain("maina wiki compile");
606
+ expect(content).toContain("maina wiki status");
607
+ expect(content).toContain("maina wiki lint");
608
+ expect(content).toContain("maina brainstorm");
609
+ expect(content).toContain("maina ticket");
610
+ expect(content).toContain("maina design");
611
+ expect(content).toContain("maina spec");
612
+ expect(content).toContain("maina slop");
613
+ expect(content).toContain("maina explain");
614
+ expect(content).toContain("maina status");
615
+ });
616
+
617
+ // ── MCP_TOOLS_TABLE includes wiki tools ──────────────────────────────
618
+
619
+ test("MCP tools table includes wikiQuery and wikiStatus", async () => {
620
+ const result = await bootstrap(tmpDir);
621
+ expect(result.ok).toBe(true);
622
+
623
+ // Check multiple agent files that embed the MCP tools table
624
+ const claudeContent = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8");
625
+ expect(claudeContent).toContain("wikiQuery");
626
+ expect(claudeContent).toContain("wikiStatus");
627
+
628
+ const agentsContent = readFileSync(join(tmpDir, "AGENTS.md"), "utf-8");
629
+ expect(agentsContent).toContain("wikiQuery");
630
+ expect(agentsContent).toContain("wikiStatus");
631
+ });
632
+
633
+ // ── Wiki section in agent files ──────────────────────────────────────
634
+
635
+ test("agent files include wiki section", async () => {
636
+ const result = await bootstrap(tmpDir);
637
+ expect(result.ok).toBe(true);
638
+
639
+ const claudeContent = readFileSync(join(tmpDir, "CLAUDE.md"), "utf-8");
640
+ expect(claudeContent).toContain("## Wiki");
641
+ expect(claudeContent).toContain("wikiQuery");
642
+
643
+ const agentsContent = readFileSync(join(tmpDir, "AGENTS.md"), "utf-8");
644
+ expect(agentsContent).toContain("## Wiki");
645
+
646
+ const geminiContent = readFileSync(join(tmpDir, "GEMINI.md"), "utf-8");
647
+ expect(geminiContent).toContain("## Wiki");
648
+
649
+ const cursorContent = readFileSync(join(tmpDir, ".cursorrules"), "utf-8");
650
+ expect(cursorContent).toContain("## Wiki");
651
+
652
+ const copilotContent = readFileSync(
653
+ join(tmpDir, ".github", "copilot-instructions.md"),
654
+ "utf-8",
655
+ );
656
+ expect(copilotContent).toContain("## Wiki");
657
+ });
658
+
659
+ // ── Windsurf ──────────────────────────────────────────────────────────
660
+
661
+ test("creates .windsurfrules at repo root", async () => {
662
+ const result = await bootstrap(tmpDir);
663
+ expect(result.ok).toBe(true);
664
+
665
+ const filePath = join(tmpDir, ".windsurfrules");
666
+ expect(existsSync(filePath)).toBe(true);
667
+
668
+ const content = readFileSync(filePath, "utf-8");
669
+ expect(content).toContain("Windsurf Rules");
670
+ expect(content).toContain("constitution.md");
671
+ expect(content).toContain("brainstorm");
672
+ expect(content).toContain("maina verify");
673
+ expect(content).toContain("getContext");
674
+ expect(content).toContain("checkSlop");
675
+ });
676
+
677
+ test(".windsurfrules includes MCP tools table", async () => {
678
+ const result = await bootstrap(tmpDir);
679
+ expect(result.ok).toBe(true);
680
+
681
+ const content = readFileSync(join(tmpDir, ".windsurfrules"), "utf-8");
682
+ expect(content).toContain("MCP Tools");
683
+ expect(content).toContain("getContext");
684
+ expect(content).toContain("reviewCode");
685
+ expect(content).toContain("wikiQuery");
686
+ });
687
+
688
+ test("merges maina section into existing .windsurfrules", async () => {
689
+ writeFileSync(join(tmpDir, ".windsurfrules"), "# My Windsurf Rules\n");
690
+
691
+ const result = await bootstrap(tmpDir);
692
+ expect(result.ok).toBe(true);
693
+ if (result.ok) {
694
+ expect(result.value.updated).toContain(".windsurfrules");
695
+ }
696
+
697
+ const content = readFileSync(join(tmpDir, ".windsurfrules"), "utf-8");
698
+ expect(content).toContain("# My Windsurf Rules");
699
+ expect(content).toContain("## Maina");
700
+ });
701
+
702
+ // ── Cline ─────────────────────────────────────────────────────────────
703
+
704
+ test("creates .clinerules at repo root", async () => {
705
+ const result = await bootstrap(tmpDir);
706
+ expect(result.ok).toBe(true);
707
+
708
+ const filePath = join(tmpDir, ".clinerules");
709
+ expect(existsSync(filePath)).toBe(true);
710
+
711
+ const content = readFileSync(filePath, "utf-8");
712
+ expect(content).toContain("Cline Rules");
713
+ expect(content).toContain("constitution.md");
714
+ expect(content).toContain("brainstorm");
715
+ expect(content).toContain("maina verify");
716
+ expect(content).toContain("getContext");
717
+ expect(content).toContain("checkSlop");
718
+ });
719
+
720
+ test("merges maina section into existing .clinerules", async () => {
721
+ writeFileSync(join(tmpDir, ".clinerules"), "# My Cline Rules\n");
722
+
723
+ const result = await bootstrap(tmpDir);
724
+ expect(result.ok).toBe(true);
725
+ if (result.ok) {
726
+ expect(result.value.updated).toContain(".clinerules");
727
+ }
728
+
729
+ const content = readFileSync(join(tmpDir, ".clinerules"), "utf-8");
730
+ expect(content).toContain("# My Cline Rules");
731
+ expect(content).toContain("## Maina");
732
+ });
733
+
734
+ // ── Continue.dev ──────────────────────────────────────────────────────
735
+
736
+ test("creates .continue/mcpServers/maina.json", async () => {
737
+ const result = await bootstrap(tmpDir);
738
+ expect(result.ok).toBe(true);
739
+
740
+ const mcpPath = join(tmpDir, ".continue", "mcpServers", "maina.json");
741
+ expect(existsSync(mcpPath)).toBe(true);
742
+
743
+ const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
744
+ expect(content.maina).toBeDefined();
745
+ expect(content.maina.command).toBe("npx");
746
+ expect(content.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
747
+ });
748
+
749
+ test("creates .continue/config.yaml", async () => {
750
+ const result = await bootstrap(tmpDir);
751
+ expect(result.ok).toBe(true);
752
+
753
+ const configPath = join(tmpDir, ".continue", "config.yaml");
754
+ expect(existsSync(configPath)).toBe(true);
755
+
756
+ const content = readFileSync(configPath, "utf-8");
757
+ expect(content).toContain("customInstructions");
758
+ expect(content).toContain("Maina");
759
+ expect(content).toContain("constitution.md");
760
+ });
761
+
762
+ test(".continue/mcpServers/maina.json uses bunx for bun runtime", async () => {
763
+ writeFileSync(
764
+ join(tmpDir, "package.json"),
765
+ JSON.stringify({ devDependencies: { "@types/bun": "latest" } }),
766
+ );
767
+
768
+ const result = await bootstrap(tmpDir);
769
+ expect(result.ok).toBe(true);
770
+
771
+ const mcpPath = join(tmpDir, ".continue", "mcpServers", "maina.json");
772
+ const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
773
+ expect(content.maina.command).toBe("bunx");
774
+ });
775
+
776
+ // ── Roo Code ──────────────────────────────────────────────────────────
777
+
778
+ test("creates .roo/mcp.json", async () => {
779
+ const result = await bootstrap(tmpDir);
780
+ expect(result.ok).toBe(true);
781
+
782
+ const mcpPath = join(tmpDir, ".roo", "mcp.json");
783
+ expect(existsSync(mcpPath)).toBe(true);
784
+
785
+ const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
786
+ expect(content.mcpServers).toBeDefined();
787
+ expect(content.mcpServers.maina).toBeDefined();
788
+ expect(content.mcpServers.maina.command).toBe("npx");
789
+ expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
790
+ });
791
+
792
+ test("creates .roo/rules/maina.md with MCP tools", async () => {
793
+ const result = await bootstrap(tmpDir);
794
+ expect(result.ok).toBe(true);
795
+
796
+ const rulesPath = join(tmpDir, ".roo", "rules", "maina.md");
797
+ expect(existsSync(rulesPath)).toBe(true);
798
+
799
+ const content = readFileSync(rulesPath, "utf-8");
800
+ expect(content).toContain("# Maina");
801
+ expect(content).toContain("constitution.md");
802
+ expect(content).toContain("getContext");
803
+ expect(content).toContain("checkSlop");
804
+ expect(content).toContain("reviewCode");
805
+ expect(content).toContain("wikiQuery");
806
+ });
807
+
808
+ test(".roo/mcp.json uses bunx for bun runtime", async () => {
809
+ writeFileSync(
810
+ join(tmpDir, "package.json"),
811
+ JSON.stringify({ devDependencies: { "@types/bun": "latest" } }),
812
+ );
813
+
814
+ const result = await bootstrap(tmpDir);
815
+ expect(result.ok).toBe(true);
816
+
817
+ const mcpPath = join(tmpDir, ".roo", "mcp.json");
818
+ const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
819
+ expect(content.mcpServers.maina.command).toBe("bunx");
820
+ });
821
+
822
+ // ── Amazon Q ──────────────────────────────────────────────────────────
823
+
824
+ test("creates .amazonq/mcp.json", async () => {
825
+ const result = await bootstrap(tmpDir);
826
+ expect(result.ok).toBe(true);
827
+
828
+ const mcpPath = join(tmpDir, ".amazonq", "mcp.json");
829
+ expect(existsSync(mcpPath)).toBe(true);
830
+
831
+ const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
832
+ expect(content.mcpServers).toBeDefined();
833
+ expect(content.mcpServers.maina).toBeDefined();
834
+ expect(content.mcpServers.maina.command).toBe("npx");
835
+ expect(content.mcpServers.maina.args).toEqual(["@mainahq/cli", "--mcp"]);
836
+ });
837
+
838
+ test(".amazonq/mcp.json uses bunx for bun runtime", async () => {
839
+ writeFileSync(
840
+ join(tmpDir, "package.json"),
841
+ JSON.stringify({ devDependencies: { "@types/bun": "latest" } }),
842
+ );
843
+
844
+ const result = await bootstrap(tmpDir);
845
+ expect(result.ok).toBe(true);
846
+
847
+ const mcpPath = join(tmpDir, ".amazonq", "mcp.json");
848
+ const content = JSON.parse(readFileSync(mcpPath, "utf-8"));
849
+ expect(content.mcpServers.maina.command).toBe("bunx");
850
+ });
851
+
852
+ // ── Aider ─────────────────────────────────────────────────────────────
853
+
854
+ test("creates .aider.conf.yml", async () => {
855
+ const result = await bootstrap(tmpDir);
856
+ expect(result.ok).toBe(true);
857
+
858
+ const configPath = join(tmpDir, ".aider.conf.yml");
859
+ expect(existsSync(configPath)).toBe(true);
860
+
861
+ const content = readFileSync(configPath, "utf-8");
862
+ expect(content).toContain("CONVENTIONS.md");
863
+ expect(content).toContain("constitution.md");
864
+ expect(content).toContain("auto-commits: false");
865
+ });
866
+
867
+ test("creates CONVENTIONS.md", async () => {
868
+ const result = await bootstrap(tmpDir);
869
+ expect(result.ok).toBe(true);
870
+
871
+ const convPath = join(tmpDir, "CONVENTIONS.md");
872
+ expect(existsSync(convPath)).toBe(true);
873
+
874
+ const content = readFileSync(convPath, "utf-8");
875
+ expect(content).toContain("# Conventions");
876
+ expect(content).toContain("constitution.md");
877
+ expect(content).toContain("brainstorm");
878
+ expect(content).toContain("getContext");
879
+ expect(content).toContain("checkSlop");
880
+ expect(content).toContain("reviewCode");
881
+ expect(content).toContain("maina verify");
882
+ expect(content).toContain("wikiQuery");
883
+ });
884
+
885
+ test("merges maina section into existing CONVENTIONS.md", async () => {
886
+ writeFileSync(join(tmpDir, "CONVENTIONS.md"), "# My Conventions\n");
887
+
888
+ const result = await bootstrap(tmpDir);
889
+ expect(result.ok).toBe(true);
890
+ if (result.ok) {
891
+ expect(result.value.updated).toContain("CONVENTIONS.md");
892
+ }
893
+
894
+ const content = readFileSync(join(tmpDir, "CONVENTIONS.md"), "utf-8");
895
+ expect(content).toContain("# My Conventions");
896
+ expect(content).toContain("## Maina");
897
+ });
898
+
899
+ // ── All rules files consistent ───────────────────────────────────────
900
+
901
+ test("all rules files contain MCP tools table", async () => {
902
+ const result = await bootstrap(tmpDir);
903
+ expect(result.ok).toBe(true);
904
+
905
+ const rulesFiles = [
906
+ ".cursorrules",
907
+ ".windsurfrules",
908
+ ".clinerules",
909
+ ".roo/rules/maina.md",
910
+ "CONVENTIONS.md",
911
+ ];
912
+
913
+ for (const f of rulesFiles) {
914
+ const content = readFileSync(join(tmpDir, f), "utf-8");
915
+ expect(content).toContain("getContext");
916
+ expect(content).toContain("checkSlop");
917
+ expect(content).toContain("reviewCode");
918
+ expect(content).toContain("suggestTests");
919
+ expect(content).toContain("wikiQuery");
920
+ }
921
+ });
463
922
  });