@mainahq/core 1.0.0 → 1.0.2

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.
package/src/init/index.ts CHANGED
@@ -6,7 +6,13 @@
6
6
  * Never overwrites existing files unless `force: true`.
7
7
  */
8
8
 
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import {
10
+ existsSync,
11
+ mkdirSync,
12
+ readdirSync,
13
+ readFileSync,
14
+ writeFileSync,
15
+ } from "node:fs";
10
16
  import { join } from "node:path";
11
17
  import type { Result } from "../db/index";
12
18
  import type { DetectedTool } from "../verify/detect";
@@ -16,6 +22,7 @@ import { detectTools } from "../verify/detect";
16
22
 
17
23
  export interface InitOptions {
18
24
  force?: boolean;
25
+ aiGenerate?: boolean;
19
26
  }
20
27
 
21
28
  export interface InitReport {
@@ -24,14 +31,24 @@ export interface InitReport {
24
31
  directory: string;
25
32
  detectedStack: DetectedStack;
26
33
  detectedTools: DetectedTool[];
34
+ aiGenerated?: boolean;
27
35
  }
28
36
 
29
37
  export interface DetectedStack {
30
38
  runtime: "bun" | "node" | "deno" | "unknown";
31
39
  language: "typescript" | "javascript" | "unknown";
40
+ languages: string[];
32
41
  testRunner: string;
33
42
  linter: string;
34
43
  framework: string;
44
+ /** package.json scripts (e.g. { test: "vitest", build: "tsc" }) */
45
+ scripts: Record<string, string>;
46
+ /** Build tool detected (e.g. "vite", "webpack", "tsup", "bunup", "esbuild") */
47
+ buildTool: string;
48
+ /** Whether this is a monorepo (workspaces detected) */
49
+ monorepo: boolean;
50
+ /** Inferred conventions from project context */
51
+ conventions: string[];
35
52
  }
36
53
 
37
54
  // ── Project Detection ───────────────────────────────────────────────────────
@@ -40,89 +57,282 @@ function detectStack(repoRoot: string): DetectedStack {
40
57
  const stack: DetectedStack = {
41
58
  runtime: "unknown",
42
59
  language: "unknown",
60
+ languages: [],
43
61
  testRunner: "unknown",
44
62
  linter: "unknown",
45
63
  framework: "none",
64
+ scripts: {},
65
+ buildTool: "unknown",
66
+ monorepo: false,
67
+ conventions: [],
46
68
  };
47
69
 
48
- // Try reading package.json
49
- const pkgPath = join(repoRoot, "package.json");
50
- if (!existsSync(pkgPath)) return stack;
70
+ // ── Multi-language detection (file-marker based) ─────────────────────
71
+ const languages: string[] = [];
72
+
73
+ // Go
74
+ if (existsSync(join(repoRoot, "go.mod"))) {
75
+ languages.push("go");
76
+ }
77
+
78
+ // Rust
79
+ if (existsSync(join(repoRoot, "Cargo.toml"))) {
80
+ languages.push("rust");
81
+ }
82
+
83
+ // Python
84
+ if (
85
+ existsSync(join(repoRoot, "pyproject.toml")) ||
86
+ existsSync(join(repoRoot, "requirements.txt")) ||
87
+ existsSync(join(repoRoot, "setup.py"))
88
+ ) {
89
+ languages.push("python");
90
+ }
91
+
92
+ // Java
93
+ if (
94
+ existsSync(join(repoRoot, "pom.xml")) ||
95
+ existsSync(join(repoRoot, "build.gradle")) ||
96
+ existsSync(join(repoRoot, "build.gradle.kts"))
97
+ ) {
98
+ languages.push("java");
99
+ }
51
100
 
52
- let pkg: Record<string, unknown>;
101
+ // .NET (C#/F#) — check for .csproj, .fsproj, .sln files
53
102
  try {
54
- pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
103
+ const entries = readdirSync(repoRoot);
104
+ if (
105
+ entries.some(
106
+ (e: string) =>
107
+ e.endsWith(".csproj") || e.endsWith(".sln") || e.endsWith(".fsproj"),
108
+ )
109
+ ) {
110
+ languages.push("dotnet");
111
+ }
55
112
  } catch {
56
- return stack;
113
+ // Directory not readable — skip
57
114
  }
58
115
 
59
- const allDeps = {
60
- ...(pkg.dependencies as Record<string, string> | undefined),
61
- ...(pkg.devDependencies as Record<string, string> | undefined),
62
- ...(pkg.peerDependencies as Record<string, string> | undefined),
63
- };
116
+ // ── JS/TS detection from package.json ───────────────────────────────
117
+ const pkgPath = join(repoRoot, "package.json");
118
+ let hasPkgJson = false;
119
+ let allDeps: Record<string, string> = {};
120
+
121
+ if (existsSync(pkgPath)) {
122
+ try {
123
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as Record<
124
+ string,
125
+ unknown
126
+ >;
127
+ hasPkgJson = true;
128
+ allDeps = {
129
+ ...(pkg.dependencies as Record<string, string> | undefined),
130
+ ...(pkg.devDependencies as Record<string, string> | undefined),
131
+ ...(pkg.peerDependencies as Record<string, string> | undefined),
132
+ };
133
+
134
+ // Extract scripts
135
+ if (pkg.scripts && typeof pkg.scripts === "object") {
136
+ stack.scripts = pkg.scripts as Record<string, string>;
137
+ }
138
+
139
+ // Detect monorepo (workspaces)
140
+ if (pkg.workspaces) {
141
+ stack.monorepo = true;
142
+ }
143
+ } catch {
144
+ // Malformed package.json — skip
145
+ }
146
+ }
147
+
148
+ if (hasPkgJson) {
149
+ // Runtime detection
150
+ if (
151
+ allDeps["@types/bun"] ||
152
+ allDeps["bun-types"] ||
153
+ existsSync(join(repoRoot, "bun.lock"))
154
+ ) {
155
+ stack.runtime = "bun";
156
+ } else if (
157
+ existsSync(join(repoRoot, "deno.json")) ||
158
+ existsSync(join(repoRoot, "deno.jsonc"))
159
+ ) {
160
+ stack.runtime = "deno";
161
+ } else {
162
+ stack.runtime = "node";
163
+ }
164
+
165
+ // Language detection (single primary)
166
+ if (existsSync(join(repoRoot, "tsconfig.json")) || allDeps.typescript) {
167
+ stack.language = "typescript";
168
+ if (!languages.includes("typescript")) {
169
+ languages.push("typescript");
170
+ }
171
+ } else {
172
+ stack.language = "javascript";
173
+ if (!languages.includes("javascript")) {
174
+ languages.push("javascript");
175
+ }
176
+ }
177
+
178
+ // Test runner detection
179
+ if (stack.runtime === "bun") {
180
+ stack.testRunner = "bun:test";
181
+ } else if (allDeps.vitest) {
182
+ stack.testRunner = "vitest";
183
+ } else if (allDeps.jest || allDeps["@jest/core"]) {
184
+ stack.testRunner = "jest";
185
+ } else if (allDeps.mocha) {
186
+ stack.testRunner = "mocha";
187
+ }
188
+
189
+ // Linter detection
190
+ if (allDeps["@biomejs/biome"]) {
191
+ stack.linter = "biome";
192
+ } else if (allDeps.eslint) {
193
+ stack.linter = "eslint";
194
+ } else if (allDeps.prettier) {
195
+ stack.linter = "prettier";
196
+ }
197
+
198
+ // Framework detection
199
+ if (allDeps.next) {
200
+ stack.framework = "next.js";
201
+ } else if (allDeps.express) {
202
+ stack.framework = "express";
203
+ } else if (allDeps.hono) {
204
+ stack.framework = "hono";
205
+ } else if (allDeps.react && !allDeps.next) {
206
+ stack.framework = "react";
207
+ } else if (allDeps.vue) {
208
+ stack.framework = "vue";
209
+ } else if (allDeps.svelte) {
210
+ stack.framework = "svelte";
211
+ }
212
+
213
+ // Build tool detection
214
+ if (allDeps.bunup) {
215
+ stack.buildTool = "bunup";
216
+ } else if (allDeps.tsup) {
217
+ stack.buildTool = "tsup";
218
+ } else if (allDeps.vite) {
219
+ stack.buildTool = "vite";
220
+ } else if (allDeps.webpack) {
221
+ stack.buildTool = "webpack";
222
+ } else if (allDeps.esbuild) {
223
+ stack.buildTool = "esbuild";
224
+ } else if (allDeps.rollup) {
225
+ stack.buildTool = "rollup";
226
+ } else if (allDeps.turbo) {
227
+ stack.buildTool = "turborepo";
228
+ }
229
+
230
+ // Also check for monorepo tools
231
+ if (
232
+ allDeps.turbo ||
233
+ allDeps.nx ||
234
+ allDeps.lerna ||
235
+ existsSync(join(repoRoot, "pnpm-workspace.yaml"))
236
+ ) {
237
+ stack.monorepo = true;
238
+ }
239
+ }
64
240
 
65
- // Runtime detection
241
+ // ── Infer conventions from project context ───────────────────────────
242
+ const conventions: string[] = [];
243
+
244
+ // Check for conventional commits
66
245
  if (
67
- allDeps["@types/bun"] ||
68
- allDeps["bun-types"] ||
69
- existsSync(join(repoRoot, "bun.lock"))
70
- ) {
71
- stack.runtime = "bun";
72
- } else if (
73
- existsSync(join(repoRoot, "deno.json")) ||
74
- existsSync(join(repoRoot, "deno.jsonc"))
246
+ existsSync(join(repoRoot, "commitlint.config.js")) ||
247
+ existsSync(join(repoRoot, "commitlint.config.ts")) ||
248
+ existsSync(join(repoRoot, ".commitlintrc.json")) ||
249
+ existsSync(join(repoRoot, ".commitlintrc.yml"))
75
250
  ) {
76
- stack.runtime = "deno";
77
- } else {
78
- stack.runtime = "node";
251
+ conventions.push("Conventional commits enforced via commitlint");
252
+ }
253
+
254
+ // Check for git hooks
255
+ if (existsSync(join(repoRoot, "lefthook.yml"))) {
256
+ conventions.push("Git hooks via lefthook");
257
+ } else if (existsSync(join(repoRoot, ".husky"))) {
258
+ conventions.push("Git hooks via husky");
79
259
  }
80
260
 
81
- // Language detection
82
- if (existsSync(join(repoRoot, "tsconfig.json")) || allDeps.typescript) {
83
- stack.language = "typescript";
84
- } else {
85
- stack.language = "javascript";
261
+ // Check for strict TypeScript
262
+ if (existsSync(join(repoRoot, "tsconfig.json"))) {
263
+ try {
264
+ const tsconfig = readFileSync(join(repoRoot, "tsconfig.json"), "utf-8");
265
+ if (tsconfig.includes('"strict"') && tsconfig.includes("true")) {
266
+ conventions.push("TypeScript strict mode enabled");
267
+ }
268
+ } catch {
269
+ // ignore
270
+ }
86
271
  }
87
272
 
88
- // Test runner detection
89
- if (stack.runtime === "bun") {
90
- stack.testRunner = "bun:test";
91
- } else if (allDeps.vitest) {
92
- stack.testRunner = "vitest";
93
- } else if (allDeps.jest || allDeps["@jest/core"]) {
94
- stack.testRunner = "jest";
95
- } else if (allDeps.mocha) {
96
- stack.testRunner = "mocha";
273
+ // Check for Docker
274
+ if (
275
+ existsSync(join(repoRoot, "Dockerfile")) ||
276
+ existsSync(join(repoRoot, "docker-compose.yml")) ||
277
+ existsSync(join(repoRoot, "docker-compose.yaml"))
278
+ ) {
279
+ conventions.push("Docker containerization");
97
280
  }
98
281
 
99
- // Linter detection
100
- if (allDeps["@biomejs/biome"]) {
101
- stack.linter = "biome";
102
- } else if (allDeps.eslint) {
103
- stack.linter = "eslint";
104
- } else if (allDeps.prettier) {
105
- stack.linter = "prettier";
282
+ // Check for CI
283
+ if (existsSync(join(repoRoot, ".github/workflows"))) {
284
+ conventions.push("GitHub Actions CI/CD");
285
+ } else if (existsSync(join(repoRoot, ".gitlab-ci.yml"))) {
286
+ conventions.push("GitLab CI/CD");
287
+ } else if (existsSync(join(repoRoot, ".circleci"))) {
288
+ conventions.push("CircleCI");
106
289
  }
107
290
 
108
- // Framework detection
109
- if (allDeps.next) {
110
- stack.framework = "next.js";
111
- } else if (allDeps.express) {
112
- stack.framework = "express";
113
- } else if (allDeps.hono) {
114
- stack.framework = "hono";
115
- } else if (allDeps.react && !allDeps.next) {
116
- stack.framework = "react";
117
- } else if (allDeps.vue) {
118
- stack.framework = "vue";
119
- } else if (allDeps.svelte) {
120
- stack.framework = "svelte";
291
+ // Check for env management
292
+ if (existsSync(join(repoRoot, ".env.example"))) {
293
+ conventions.push("Environment variables documented in .env.example");
121
294
  }
122
295
 
296
+ // Infer from package.json scripts
297
+ if (stack.scripts.lint || stack.scripts["lint:fix"]) {
298
+ conventions.push(
299
+ `Lint command: \`${stack.runtime === "bun" ? "bun" : "npm"} run lint\``,
300
+ );
301
+ }
302
+ if (stack.scripts.test) {
303
+ conventions.push(`Test command: \`${stack.scripts.test}\``);
304
+ }
305
+ if (stack.scripts.build) {
306
+ conventions.push(`Build command: \`${stack.scripts.build}\``);
307
+ }
308
+ if (stack.scripts.typecheck || stack.scripts["type-check"]) {
309
+ conventions.push("Type checking enforced");
310
+ }
311
+
312
+ stack.conventions = conventions;
313
+
314
+ // If no languages detected, mark as unknown
315
+ stack.languages = languages.length > 0 ? languages : ["unknown"];
316
+
123
317
  return stack;
124
318
  }
125
319
 
320
+ // ── Constants (shared across agent files) ───────────────────────────────────
321
+
322
+ const WORKFLOW_ORDER =
323
+ "brainstorm -> ticket -> plan -> design -> spec -> implement -> verify -> review -> fix -> commit -> review -> pr";
324
+
325
+ const MCP_TOOLS_TABLE = `| Tool | When to use |
326
+ |------|-------------|
327
+ | \`getContext\` | Before starting — understand branch state and verification status |
328
+ | \`verify\` | After changes — run the full verification pipeline |
329
+ | \`checkSlop\` | On changed files — detect AI-generated slop patterns |
330
+ | \`reviewCode\` | On your diff — two-stage review (spec compliance + code quality) |
331
+ | \`suggestTests\` | When implementing — generate TDD test stubs |
332
+ | \`getConventions\` | Understand project coding conventions |
333
+ | \`explainModule\` | Understand a module's purpose and dependencies |
334
+ | \`analyzeFeature\` | Analyze a feature directory for consistency |`;
335
+
126
336
  // ── Templates ────────────────────────────────────────────────────────────────
127
337
 
128
338
  function buildConstitution(stack: DetectedStack): string {
@@ -149,6 +359,53 @@ function buildConstitution(stack: DetectedStack): string {
149
359
  const frameworkLine =
150
360
  stack.framework !== "none" ? `- Framework: ${stack.framework}\n` : "";
151
361
 
362
+ const buildLine =
363
+ stack.buildTool !== "unknown" ? `- Build: ${stack.buildTool}\n` : "";
364
+
365
+ const monorepoLine = stack.monorepo ? "- Monorepo: yes (workspaces)\n" : "";
366
+
367
+ // Build architecture section from context
368
+ const archLines: string[] = [];
369
+ if (stack.monorepo) {
370
+ archLines.push("- Monorepo with shared packages");
371
+ }
372
+ if (stack.framework !== "none") {
373
+ archLines.push(`- ${stack.framework} application`);
374
+ }
375
+ if (stack.languages.length > 1) {
376
+ archLines.push(`- Multi-language: ${stack.languages.join(", ")}`);
377
+ }
378
+ const archSection =
379
+ archLines.length > 0
380
+ ? archLines.join("\n")
381
+ : "- [NEEDS CLARIFICATION] Define architectural constraints.";
382
+
383
+ // Build verification section from scripts
384
+ const verifyLines: string[] = [];
385
+ const runCmd = stack.runtime === "bun" ? "bun" : "npm";
386
+ if (stack.scripts.lint || stack.linter !== "unknown") {
387
+ verifyLines.push(
388
+ `- Lint: \`${stack.scripts.lint ?? `${runCmd} run lint`}\``,
389
+ );
390
+ }
391
+ if (stack.language === "typescript") {
392
+ verifyLines.push(
393
+ `- Typecheck: \`${stack.scripts.typecheck ?? stack.scripts["type-check"] ?? `${runCmd} run typecheck`}\``,
394
+ );
395
+ }
396
+ if (stack.scripts.test) {
397
+ verifyLines.push(`- Test: \`${stack.scripts.test}\``);
398
+ } else if (stack.testRunner !== "unknown") {
399
+ verifyLines.push(`- Test: \`${runCmd} test\``);
400
+ }
401
+ verifyLines.push("- Diff-only: only report findings on changed lines");
402
+
403
+ // Build conventions section from detected conventions
404
+ const conventionLines =
405
+ stack.conventions.length > 0
406
+ ? stack.conventions.map((c) => `- ${c}`).join("\n")
407
+ : "- [NEEDS CLARIFICATION] Add project-specific conventions.";
408
+
152
409
  return `# Project Constitution
153
410
 
154
411
  Non-negotiable rules. Injected into every AI call.
@@ -158,16 +415,15 @@ ${runtimeLine}
158
415
  ${langLine}
159
416
  ${lintLine}
160
417
  ${testLine}
161
- ${frameworkLine}
418
+ ${frameworkLine}${buildLine}${monorepoLine}
162
419
  ## Architecture
163
- - [NEEDS CLARIFICATION] Define architectural constraints.
420
+ ${archSection}
164
421
 
165
422
  ## Verification
166
- - All commits pass: lint + typecheck + test
167
- - Diff-only: only report findings on changed lines
423
+ ${verifyLines.join("\n")}
168
424
 
169
425
  ## Conventions
170
- - [NEEDS CLARIFICATION] Add project-specific conventions.
426
+ ${conventionLines}
171
427
  `;
172
428
  }
173
429
 
@@ -179,6 +435,11 @@ function buildAgentsMd(stack: DetectedStack): string {
179
435
 
180
436
  This repo uses [Maina](https://github.com/mainahq/maina) for verification-first development.
181
437
 
438
+ ## Workflow Order
439
+
440
+ Follow this order for every feature:
441
+ \`${WORKFLOW_ORDER}\`
442
+
182
443
  ## Quick Start
183
444
  \`\`\`bash
184
445
  ${installCmd}
@@ -199,6 +460,10 @@ maina commit # verify + commit
199
460
  | \`maina stats\` | Show verification metrics |
200
461
  | \`maina doctor\` | Check tool health |
201
462
 
463
+ ## MCP Tools
464
+
465
+ ${MCP_TOOLS_TABLE}
466
+
202
467
  ## Config Files
203
468
  | File | Purpose | Who Edits |
204
469
  |------|---------|-----------|
@@ -206,6 +471,9 @@ maina commit # verify + commit
206
471
  | \`AGENTS.md\` | Agent instructions — commands, conventions | Team |
207
472
  | \`.github/copilot-instructions.md\` | Copilot agent instructions + MCP tools | Team |
208
473
  | \`CLAUDE.md\` | Claude Code specific instructions | Optional, Claude Code users |
474
+ | \`GEMINI.md\` | Gemini CLI specific instructions | Optional, Gemini CLI users |
475
+ | \`.cursorrules\` | Cursor specific instructions | Optional, Cursor users |
476
+ | \`.mcp.json\` | MCP server configuration | Team |
209
477
  | \`.maina/prompts/*.md\` | Prompt overrides for review/commit/etc | Maina (via \`maina learn\`) |
210
478
 
211
479
  ## Runtime
@@ -220,7 +488,12 @@ function buildCopilotInstructions(stack: DetectedStack): string {
220
488
 
221
489
  You are working on a codebase verified by [Maina](https://mainahq.com), the verification-first developer OS. Maina MCP tools are available — use them.
222
490
 
223
- ## Workflow
491
+ ## Workflow Order
492
+
493
+ Follow this order for every feature:
494
+ \`${WORKFLOW_ORDER}\`
495
+
496
+ ## Step-by-step
224
497
 
225
498
  1. **Get context** — call \`maina getContext\` to understand codebase state
226
499
  2. **Write tests first** — TDD always. Write failing tests, then implement
@@ -230,14 +503,7 @@ You are working on a codebase verified by [Maina](https://mainahq.com), the veri
230
503
 
231
504
  ## Available MCP Tools
232
505
 
233
- | Tool | When to use |
234
- |------|-------------|
235
- | \`getContext\` | Before starting — understand branch state and verification status |
236
- | \`verify\` | After changes — run the full verification pipeline |
237
- | \`checkSlop\` | On changed files — detect AI-generated slop patterns |
238
- | \`reviewCode\` | On your diff — two-stage review (spec compliance + code quality) |
239
- | \`suggestTests\` | When implementing — generate TDD test stubs |
240
- | \`getConventions\` | Understand project coding conventions |
506
+ ${MCP_TOOLS_TABLE}
241
507
 
242
508
  ## Conventions
243
509
 
@@ -253,6 +519,242 @@ Issues labeled \`audit\` come from maina's daily verification. Fix the specific
253
519
  `;
254
520
  }
255
521
 
522
+ // ── .mcp.json ───────────────────────────────────────────────────────────────
523
+
524
+ function buildMcpJson(): string {
525
+ return JSON.stringify(
526
+ {
527
+ mcpServers: {
528
+ maina: {
529
+ command: "maina",
530
+ args: ["--mcp"],
531
+ },
532
+ },
533
+ },
534
+ null,
535
+ 2,
536
+ );
537
+ }
538
+
539
+ // ── Agent Instruction Files ─────────────────────────────────────────────────
540
+
541
+ function buildClaudeMd(stack: DetectedStack): string {
542
+ const runCmd = stack.runtime === "bun" ? "bun" : "npm";
543
+ return `# CLAUDE.md
544
+
545
+ This repo uses [Maina](https://mainahq.com) for verification-first development.
546
+ Read \`.maina/constitution.md\` for project DNA — stack rules, conventions, and gates.
547
+
548
+ ## Maina Workflow
549
+
550
+ Follow this order for every feature:
551
+ \`${WORKFLOW_ORDER}\`
552
+
553
+ ## MCP Tools
554
+
555
+ Maina exposes MCP tools — use them in every session:
556
+
557
+ ${MCP_TOOLS_TABLE}
558
+
559
+ ## Commands
560
+
561
+ \`\`\`bash
562
+ maina verify # run full verification pipeline
563
+ maina commit # verify + commit
564
+ maina review # two-stage code review
565
+ maina context # generate focused codebase context
566
+ maina doctor # check tool health
567
+ maina plan # create feature with spec/plan/tasks
568
+ maina stats # show verification metrics
569
+ \`\`\`
570
+
571
+ ## Conventions
572
+
573
+ - Runtime: ${stack.runtime}
574
+ - Test: \`${runCmd} test\`
575
+ - Conventional commits (feat, fix, refactor, test, docs, chore)
576
+ - No \`console.log\` in production code
577
+ - Diff-only: only fix issues on changed lines
578
+ - TDD always — write tests first
579
+ `;
580
+ }
581
+
582
+ function buildGeminiMd(stack: DetectedStack): string {
583
+ const runCmd = stack.runtime === "bun" ? "bun" : "npm";
584
+ return `# GEMINI.md
585
+
586
+ Instructions for Gemini CLI when working in this repository.
587
+
588
+ This repo uses [Maina](https://mainahq.com) for verification-first development.
589
+ Read \`.maina/constitution.md\` for project DNA — stack rules, conventions, and gates.
590
+
591
+ ## Maina Workflow
592
+
593
+ Follow this order for every feature:
594
+ \`${WORKFLOW_ORDER}\`
595
+
596
+ ## MCP Tools
597
+
598
+ Maina exposes MCP tools via \`.mcp.json\`. Use them:
599
+
600
+ ${MCP_TOOLS_TABLE}
601
+
602
+ ## Key Commands
603
+
604
+ - \`maina verify\` — run full verification pipeline
605
+ - \`maina commit\` — verify + commit
606
+ - \`maina review\` — two-stage code review
607
+ - \`maina context\` — generate focused codebase context
608
+ - \`maina doctor\` — check tool health
609
+
610
+ ## Rules
611
+
612
+ - Runtime: ${stack.runtime}
613
+ - Test: \`${runCmd} test\`
614
+ - Conventional commits (feat, fix, refactor, test, docs, chore)
615
+ - No \`console.log\` in production code
616
+ - Diff-only: only report findings on changed lines
617
+ - TDD always — write tests first
618
+ `;
619
+ }
620
+
621
+ function buildCursorRules(stack: DetectedStack): string {
622
+ const runCmd = stack.runtime === "bun" ? "bun" : "npm";
623
+ return `# Cursor Rules
624
+
625
+ This repo uses Maina for verification-first development.
626
+ Read \`.maina/constitution.md\` for project DNA.
627
+
628
+ ## Workflow Order
629
+ ${WORKFLOW_ORDER}
630
+
631
+ ## MCP Tools (via .mcp.json)
632
+ ${MCP_TOOLS_TABLE}
633
+
634
+ ## Commands
635
+ - maina verify — run full verification pipeline
636
+ - maina commit — verify + commit
637
+ - maina review — two-stage code review
638
+ - maina context — generate focused codebase context
639
+
640
+ ## Conventions
641
+ - Runtime: ${stack.runtime}
642
+ - Test: ${runCmd} test
643
+ - Conventional commits
644
+ - No console.log in production
645
+ - Diff-only: only report findings on changed lines
646
+ - TDD: write tests first, then implement
647
+ `;
648
+ }
649
+
650
+ // ── AI-Generated Constitution ───────────────────────────────────────────────
651
+
652
+ function buildProjectSummary(repoRoot: string, stack: DetectedStack): string {
653
+ const parts: string[] = [];
654
+ parts.push("## Detected Project Stack");
655
+ parts.push(`- Runtime: ${stack.runtime}`);
656
+ parts.push(`- Primary language: ${stack.language}`);
657
+ parts.push(`- All languages: ${stack.languages.join(", ")}`);
658
+ parts.push(`- Test runner: ${stack.testRunner}`);
659
+ parts.push(`- Linter: ${stack.linter}`);
660
+ parts.push(`- Framework: ${stack.framework}`);
661
+
662
+ // Read package.json for extra context
663
+ const pkgPath = join(repoRoot, "package.json");
664
+ if (existsSync(pkgPath)) {
665
+ try {
666
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as Record<
667
+ string,
668
+ unknown
669
+ >;
670
+ const deps = Object.keys(
671
+ (pkg.dependencies as Record<string, string>) ?? {},
672
+ );
673
+ const devDeps = Object.keys(
674
+ (pkg.devDependencies as Record<string, string>) ?? {},
675
+ );
676
+ if (deps.length > 0) {
677
+ parts.push(`\n## Dependencies\n${deps.join(", ")}`);
678
+ }
679
+ if (devDeps.length > 0) {
680
+ parts.push(`\n## Dev Dependencies\n${devDeps.join(", ")}`);
681
+ }
682
+ if (pkg.description) {
683
+ parts.push(`\n## Project Description\n${pkg.description}`);
684
+ }
685
+ } catch {
686
+ // Ignore parse errors
687
+ }
688
+ }
689
+
690
+ // Check for common config files
691
+ const configFiles: string[] = [];
692
+ const checks = [
693
+ "tsconfig.json",
694
+ "biome.json",
695
+ ".eslintrc.json",
696
+ "jest.config.ts",
697
+ "vitest.config.ts",
698
+ "Dockerfile",
699
+ "docker-compose.yml",
700
+ ".env.example",
701
+ "Makefile",
702
+ ];
703
+ for (const f of checks) {
704
+ if (existsSync(join(repoRoot, f))) {
705
+ configFiles.push(f);
706
+ }
707
+ }
708
+ if (configFiles.length > 0) {
709
+ parts.push(`\n## Config Files Found\n${configFiles.join(", ")}`);
710
+ }
711
+
712
+ return parts.join("\n");
713
+ }
714
+
715
+ async function tryGenerateConstitution(
716
+ repoRoot: string,
717
+ stack: DetectedStack,
718
+ ): Promise<string | null> {
719
+ try {
720
+ const { tryAIGenerate } = await import("../ai/try-generate");
721
+ const mainaDir = join(repoRoot, ".maina");
722
+ const summary = buildProjectSummary(repoRoot, stack);
723
+
724
+ const result = await tryAIGenerate(
725
+ "init-constitution",
726
+ mainaDir,
727
+ {
728
+ stack_runtime: stack.runtime,
729
+ stack_language: stack.language,
730
+ stack_languages: stack.languages.join(", "),
731
+ stack_testRunner: stack.testRunner,
732
+ stack_linter: stack.linter,
733
+ stack_framework: stack.framework,
734
+ },
735
+ `Generate a project constitution for this software project based on the detected stack information below.
736
+
737
+ A constitution defines non-negotiable rules injected into every AI call. It should include:
738
+ 1. Stack section — runtime, language, linter, test runner, framework
739
+ 2. Architecture section — key architectural constraints (infer from the stack)
740
+ 3. Verification section — what must pass before code merges
741
+ 4. Conventions section — coding conventions (infer from the stack)
742
+
743
+ Replace [NEEDS CLARIFICATION] placeholders with reasonable defaults based on the stack.
744
+ Keep it concise (under 50 lines). Use markdown format starting with "# Project Constitution".
745
+
746
+ ${summary}`,
747
+ );
748
+
749
+ if (result.fromAI && result.text) {
750
+ return result.text;
751
+ }
752
+ } catch {
753
+ // AI unavailable — fall back to static template
754
+ }
755
+ return null;
756
+ }
757
+
256
758
  const REVIEW_PROMPT_TEMPLATE = `# Review Prompt
257
759
 
258
760
  Review the following code changes for:
@@ -306,11 +808,14 @@ interface FileEntry {
306
808
  content: string;
307
809
  }
308
810
 
309
- function getFileManifest(stack: DetectedStack): FileEntry[] {
811
+ function getFileManifest(
812
+ stack: DetectedStack,
813
+ constitutionOverride?: string,
814
+ ): FileEntry[] {
310
815
  return [
311
816
  {
312
817
  relativePath: ".maina/constitution.md",
313
- content: buildConstitution(stack),
818
+ content: constitutionOverride ?? buildConstitution(stack),
314
819
  },
315
820
  {
316
821
  relativePath: ".maina/prompts/review.md",
@@ -332,6 +837,22 @@ function getFileManifest(stack: DetectedStack): FileEntry[] {
332
837
  relativePath: ".github/copilot-instructions.md",
333
838
  content: buildCopilotInstructions(stack),
334
839
  },
840
+ {
841
+ relativePath: ".mcp.json",
842
+ content: buildMcpJson(),
843
+ },
844
+ {
845
+ relativePath: "CLAUDE.md",
846
+ content: buildClaudeMd(stack),
847
+ },
848
+ {
849
+ relativePath: "GEMINI.md",
850
+ content: buildGeminiMd(stack),
851
+ },
852
+ {
853
+ relativePath: ".cursorrules",
854
+ content: buildCursorRules(stack),
855
+ },
335
856
  ];
336
857
  }
337
858
 
@@ -385,6 +906,7 @@ export async function bootstrap(
385
906
  options?: InitOptions,
386
907
  ): Promise<Result<InitReport>> {
387
908
  const force = options?.force ?? false;
909
+ const aiGenerate = options?.aiGenerate ?? false;
388
910
  const mainaDir = join(repoRoot, ".maina");
389
911
  const created: string[] = [];
390
912
  const skipped: string[] = [];
@@ -393,8 +915,8 @@ export async function bootstrap(
393
915
  // Detect project stack from package.json
394
916
  const detectedStack = detectStack(repoRoot);
395
917
 
396
- // Detect available verification tools on PATH
397
- const detectedToolsList = await detectTools();
918
+ // Detect available verification tools on PATH (filtered by project languages)
919
+ const detectedToolsList = await detectTools(detectedStack.languages);
398
920
 
399
921
  // Ensure .maina/ exists
400
922
  mkdirSync(mainaDir, { recursive: true });
@@ -404,8 +926,22 @@ export async function bootstrap(
404
926
  mkdirSync(join(repoRoot, dir), { recursive: true });
405
927
  }
406
928
 
929
+ // Try AI-generated constitution when requested
930
+ let constitutionOverride: string | undefined;
931
+ let aiGenerated = false;
932
+ if (aiGenerate) {
933
+ const aiConstitution = await tryGenerateConstitution(
934
+ repoRoot,
935
+ detectedStack,
936
+ );
937
+ if (aiConstitution) {
938
+ constitutionOverride = aiConstitution;
939
+ aiGenerated = true;
940
+ }
941
+ }
942
+
407
943
  // Scaffold each file with stack-aware templates
408
- const manifest = getFileManifest(detectedStack);
944
+ const manifest = getFileManifest(detectedStack, constitutionOverride);
409
945
  for (const entry of manifest) {
410
946
  const fullPath = join(repoRoot, entry.relativePath);
411
947
  const dirPath = join(fullPath, "..");
@@ -440,6 +976,7 @@ export async function bootstrap(
440
976
  directory: mainaDir,
441
977
  detectedStack,
442
978
  detectedTools: detectedToolsList,
979
+ aiGenerated,
443
980
  },
444
981
  };
445
982
  } catch (e) {