@mainahq/core 0.7.0 → 1.0.1
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/package.json +3 -3
- package/src/cloud/__tests__/client.test.ts +171 -0
- package/src/cloud/auth.ts +1 -0
- package/src/cloud/client.ts +94 -0
- package/src/cloud/types.ts +48 -0
- package/src/context/engine.ts +72 -5
- package/src/feedback/__tests__/sync.test.ts +63 -1
- package/src/feedback/collector.ts +54 -0
- package/src/feedback/sync.ts +92 -3
- package/src/git/__tests__/git.test.ts +15 -0
- package/src/git/index.ts +25 -0
- package/src/index.ts +12 -1
- package/src/init/__tests__/detect-stack.test.ts +237 -0
- package/src/init/__tests__/init.test.ts +184 -0
- package/src/init/index.ts +479 -66
- package/src/verify/__tests__/detect-filter.test.ts +303 -0
- package/src/verify/detect.ts +162 -25
- package/src/language/__tests__/__fixtures__/detect/composer.lock +0 -1
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 {
|
|
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,11 +31,13 @@ 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;
|
|
@@ -40,89 +49,168 @@ function detectStack(repoRoot: string): DetectedStack {
|
|
|
40
49
|
const stack: DetectedStack = {
|
|
41
50
|
runtime: "unknown",
|
|
42
51
|
language: "unknown",
|
|
52
|
+
languages: [],
|
|
43
53
|
testRunner: "unknown",
|
|
44
54
|
linter: "unknown",
|
|
45
55
|
framework: "none",
|
|
46
56
|
};
|
|
47
57
|
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
if (!existsSync(pkgPath)) return stack;
|
|
58
|
+
// ── Multi-language detection (file-marker based) ─────────────────────
|
|
59
|
+
const languages: string[] = [];
|
|
51
60
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} catch {
|
|
56
|
-
return stack;
|
|
61
|
+
// Go
|
|
62
|
+
if (existsSync(join(repoRoot, "go.mod"))) {
|
|
63
|
+
languages.push("go");
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
};
|
|
66
|
+
// Rust
|
|
67
|
+
if (existsSync(join(repoRoot, "Cargo.toml"))) {
|
|
68
|
+
languages.push("rust");
|
|
69
|
+
}
|
|
64
70
|
|
|
65
|
-
//
|
|
71
|
+
// Python
|
|
66
72
|
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
existsSync(join(repoRoot, "
|
|
73
|
+
existsSync(join(repoRoot, "pyproject.toml")) ||
|
|
74
|
+
existsSync(join(repoRoot, "requirements.txt")) ||
|
|
75
|
+
existsSync(join(repoRoot, "setup.py"))
|
|
70
76
|
) {
|
|
71
|
-
|
|
72
|
-
} else if (
|
|
73
|
-
existsSync(join(repoRoot, "deno.json")) ||
|
|
74
|
-
existsSync(join(repoRoot, "deno.jsonc"))
|
|
75
|
-
) {
|
|
76
|
-
stack.runtime = "deno";
|
|
77
|
-
} else {
|
|
78
|
-
stack.runtime = "node";
|
|
77
|
+
languages.push("python");
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
//
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
// Java
|
|
81
|
+
if (
|
|
82
|
+
existsSync(join(repoRoot, "pom.xml")) ||
|
|
83
|
+
existsSync(join(repoRoot, "build.gradle")) ||
|
|
84
|
+
existsSync(join(repoRoot, "build.gradle.kts"))
|
|
85
|
+
) {
|
|
86
|
+
languages.push("java");
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
// .NET (C#/F#) — check for .csproj, .fsproj, .sln files
|
|
90
|
+
try {
|
|
91
|
+
const entries = readdirSync(repoRoot);
|
|
92
|
+
if (
|
|
93
|
+
entries.some(
|
|
94
|
+
(e: string) =>
|
|
95
|
+
e.endsWith(".csproj") || e.endsWith(".sln") || e.endsWith(".fsproj"),
|
|
96
|
+
)
|
|
97
|
+
) {
|
|
98
|
+
languages.push("dotnet");
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Directory not readable — skip
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
// ── JS/TS detection from package.json ───────────────────────────────
|
|
105
|
+
const pkgPath = join(repoRoot, "package.json");
|
|
106
|
+
let hasPkgJson = false;
|
|
107
|
+
let allDeps: Record<string, string> = {};
|
|
108
|
+
|
|
109
|
+
if (existsSync(pkgPath)) {
|
|
110
|
+
try {
|
|
111
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as Record<
|
|
112
|
+
string,
|
|
113
|
+
unknown
|
|
114
|
+
>;
|
|
115
|
+
hasPkgJson = true;
|
|
116
|
+
allDeps = {
|
|
117
|
+
...(pkg.dependencies as Record<string, string> | undefined),
|
|
118
|
+
...(pkg.devDependencies as Record<string, string> | undefined),
|
|
119
|
+
...(pkg.peerDependencies as Record<string, string> | undefined),
|
|
120
|
+
};
|
|
121
|
+
} catch {
|
|
122
|
+
// Malformed package.json — skip
|
|
123
|
+
}
|
|
106
124
|
}
|
|
107
125
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
if (hasPkgJson) {
|
|
127
|
+
// Runtime detection
|
|
128
|
+
if (
|
|
129
|
+
allDeps["@types/bun"] ||
|
|
130
|
+
allDeps["bun-types"] ||
|
|
131
|
+
existsSync(join(repoRoot, "bun.lock"))
|
|
132
|
+
) {
|
|
133
|
+
stack.runtime = "bun";
|
|
134
|
+
} else if (
|
|
135
|
+
existsSync(join(repoRoot, "deno.json")) ||
|
|
136
|
+
existsSync(join(repoRoot, "deno.jsonc"))
|
|
137
|
+
) {
|
|
138
|
+
stack.runtime = "deno";
|
|
139
|
+
} else {
|
|
140
|
+
stack.runtime = "node";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Language detection (single primary)
|
|
144
|
+
if (existsSync(join(repoRoot, "tsconfig.json")) || allDeps.typescript) {
|
|
145
|
+
stack.language = "typescript";
|
|
146
|
+
if (!languages.includes("typescript")) {
|
|
147
|
+
languages.push("typescript");
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
stack.language = "javascript";
|
|
151
|
+
if (!languages.includes("javascript")) {
|
|
152
|
+
languages.push("javascript");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Test runner detection
|
|
157
|
+
if (stack.runtime === "bun") {
|
|
158
|
+
stack.testRunner = "bun:test";
|
|
159
|
+
} else if (allDeps.vitest) {
|
|
160
|
+
stack.testRunner = "vitest";
|
|
161
|
+
} else if (allDeps.jest || allDeps["@jest/core"]) {
|
|
162
|
+
stack.testRunner = "jest";
|
|
163
|
+
} else if (allDeps.mocha) {
|
|
164
|
+
stack.testRunner = "mocha";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Linter detection
|
|
168
|
+
if (allDeps["@biomejs/biome"]) {
|
|
169
|
+
stack.linter = "biome";
|
|
170
|
+
} else if (allDeps.eslint) {
|
|
171
|
+
stack.linter = "eslint";
|
|
172
|
+
} else if (allDeps.prettier) {
|
|
173
|
+
stack.linter = "prettier";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Framework detection
|
|
177
|
+
if (allDeps.next) {
|
|
178
|
+
stack.framework = "next.js";
|
|
179
|
+
} else if (allDeps.express) {
|
|
180
|
+
stack.framework = "express";
|
|
181
|
+
} else if (allDeps.hono) {
|
|
182
|
+
stack.framework = "hono";
|
|
183
|
+
} else if (allDeps.react && !allDeps.next) {
|
|
184
|
+
stack.framework = "react";
|
|
185
|
+
} else if (allDeps.vue) {
|
|
186
|
+
stack.framework = "vue";
|
|
187
|
+
} else if (allDeps.svelte) {
|
|
188
|
+
stack.framework = "svelte";
|
|
189
|
+
}
|
|
121
190
|
}
|
|
122
191
|
|
|
192
|
+
// If no languages detected, mark as unknown
|
|
193
|
+
stack.languages = languages.length > 0 ? languages : ["unknown"];
|
|
194
|
+
|
|
123
195
|
return stack;
|
|
124
196
|
}
|
|
125
197
|
|
|
198
|
+
// ── Constants (shared across agent files) ───────────────────────────────────
|
|
199
|
+
|
|
200
|
+
const WORKFLOW_ORDER =
|
|
201
|
+
"brainstorm -> ticket -> plan -> design -> spec -> implement -> verify -> review -> fix -> commit -> review -> pr";
|
|
202
|
+
|
|
203
|
+
const MCP_TOOLS_TABLE = `| Tool | When to use |
|
|
204
|
+
|------|-------------|
|
|
205
|
+
| \`getContext\` | Before starting — understand branch state and verification status |
|
|
206
|
+
| \`verify\` | After changes — run the full verification pipeline |
|
|
207
|
+
| \`checkSlop\` | On changed files — detect AI-generated slop patterns |
|
|
208
|
+
| \`reviewCode\` | On your diff — two-stage review (spec compliance + code quality) |
|
|
209
|
+
| \`suggestTests\` | When implementing — generate TDD test stubs |
|
|
210
|
+
| \`getConventions\` | Understand project coding conventions |
|
|
211
|
+
| \`explainModule\` | Understand a module's purpose and dependencies |
|
|
212
|
+
| \`analyzeFeature\` | Analyze a feature directory for consistency |`;
|
|
213
|
+
|
|
126
214
|
// ── Templates ────────────────────────────────────────────────────────────────
|
|
127
215
|
|
|
128
216
|
function buildConstitution(stack: DetectedStack): string {
|
|
@@ -177,7 +265,12 @@ function buildAgentsMd(stack: DetectedStack): string {
|
|
|
177
265
|
|
|
178
266
|
return `# AGENTS.md
|
|
179
267
|
|
|
180
|
-
This repo uses [Maina](https://github.com/
|
|
268
|
+
This repo uses [Maina](https://github.com/mainahq/maina) for verification-first development.
|
|
269
|
+
|
|
270
|
+
## Workflow Order
|
|
271
|
+
|
|
272
|
+
Follow this order for every feature:
|
|
273
|
+
\`${WORKFLOW_ORDER}\`
|
|
181
274
|
|
|
182
275
|
## Quick Start
|
|
183
276
|
\`\`\`bash
|
|
@@ -199,12 +292,20 @@ maina commit # verify + commit
|
|
|
199
292
|
| \`maina stats\` | Show verification metrics |
|
|
200
293
|
| \`maina doctor\` | Check tool health |
|
|
201
294
|
|
|
295
|
+
## MCP Tools
|
|
296
|
+
|
|
297
|
+
${MCP_TOOLS_TABLE}
|
|
298
|
+
|
|
202
299
|
## Config Files
|
|
203
300
|
| File | Purpose | Who Edits |
|
|
204
301
|
|------|---------|-----------|
|
|
205
302
|
| \`.maina/constitution.md\` | Project DNA — stack, rules, gates | Team (stable, rarely changes) |
|
|
206
303
|
| \`AGENTS.md\` | Agent instructions — commands, conventions | Team |
|
|
304
|
+
| \`.github/copilot-instructions.md\` | Copilot agent instructions + MCP tools | Team |
|
|
207
305
|
| \`CLAUDE.md\` | Claude Code specific instructions | Optional, Claude Code users |
|
|
306
|
+
| \`GEMINI.md\` | Gemini CLI specific instructions | Optional, Gemini CLI users |
|
|
307
|
+
| \`.cursorrules\` | Cursor specific instructions | Optional, Cursor users |
|
|
308
|
+
| \`.mcp.json\` | MCP server configuration | Team |
|
|
208
309
|
| \`.maina/prompts/*.md\` | Prompt overrides for review/commit/etc | Maina (via \`maina learn\`) |
|
|
209
310
|
|
|
210
311
|
## Runtime
|
|
@@ -213,6 +314,279 @@ maina commit # verify + commit
|
|
|
213
314
|
`;
|
|
214
315
|
}
|
|
215
316
|
|
|
317
|
+
function buildCopilotInstructions(stack: DetectedStack): string {
|
|
318
|
+
const runCmd = stack.runtime === "bun" ? "bun" : "npm";
|
|
319
|
+
return `# Copilot Instructions
|
|
320
|
+
|
|
321
|
+
You are working on a codebase verified by [Maina](https://mainahq.com), the verification-first developer OS. Maina MCP tools are available — use them.
|
|
322
|
+
|
|
323
|
+
## Workflow Order
|
|
324
|
+
|
|
325
|
+
Follow this order for every feature:
|
|
326
|
+
\`${WORKFLOW_ORDER}\`
|
|
327
|
+
|
|
328
|
+
## Step-by-step
|
|
329
|
+
|
|
330
|
+
1. **Get context** — call \`maina getContext\` to understand codebase state
|
|
331
|
+
2. **Write tests first** — TDD always. Write failing tests, then implement
|
|
332
|
+
3. **Verify your work** — call \`maina verify\` before requesting review
|
|
333
|
+
4. **Check for slop** — call \`maina checkSlop\` on changed files
|
|
334
|
+
5. **Review your code** — call \`maina reviewCode\` with your diff
|
|
335
|
+
|
|
336
|
+
## Available MCP Tools
|
|
337
|
+
|
|
338
|
+
${MCP_TOOLS_TABLE}
|
|
339
|
+
|
|
340
|
+
## Conventions
|
|
341
|
+
|
|
342
|
+
- Runtime: ${stack.runtime}
|
|
343
|
+
- Test: \`${runCmd} test\`
|
|
344
|
+
- Commits: conventional commits (feat, fix, refactor, test, docs, chore)
|
|
345
|
+
- No \`console.log\` in production code
|
|
346
|
+
- Diff-only: only fix issues on changed lines
|
|
347
|
+
|
|
348
|
+
## When Working on Audit Issues
|
|
349
|
+
|
|
350
|
+
Issues labeled \`audit\` come from maina's daily verification. Fix the specific findings listed — don't refactor unrelated code.
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── .mcp.json ───────────────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
function buildMcpJson(): string {
|
|
357
|
+
return JSON.stringify(
|
|
358
|
+
{
|
|
359
|
+
mcpServers: {
|
|
360
|
+
maina: {
|
|
361
|
+
command: "maina",
|
|
362
|
+
args: ["--mcp"],
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
null,
|
|
367
|
+
2,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Agent Instruction Files ─────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
function buildClaudeMd(stack: DetectedStack): string {
|
|
374
|
+
const runCmd = stack.runtime === "bun" ? "bun" : "npm";
|
|
375
|
+
return `# CLAUDE.md
|
|
376
|
+
|
|
377
|
+
This repo uses [Maina](https://mainahq.com) for verification-first development.
|
|
378
|
+
Read \`.maina/constitution.md\` for project DNA — stack rules, conventions, and gates.
|
|
379
|
+
|
|
380
|
+
## Maina Workflow
|
|
381
|
+
|
|
382
|
+
Follow this order for every feature:
|
|
383
|
+
\`${WORKFLOW_ORDER}\`
|
|
384
|
+
|
|
385
|
+
## MCP Tools
|
|
386
|
+
|
|
387
|
+
Maina exposes MCP tools — use them in every session:
|
|
388
|
+
|
|
389
|
+
${MCP_TOOLS_TABLE}
|
|
390
|
+
|
|
391
|
+
## Commands
|
|
392
|
+
|
|
393
|
+
\`\`\`bash
|
|
394
|
+
maina verify # run full verification pipeline
|
|
395
|
+
maina commit # verify + commit
|
|
396
|
+
maina review # two-stage code review
|
|
397
|
+
maina context # generate focused codebase context
|
|
398
|
+
maina doctor # check tool health
|
|
399
|
+
maina plan # create feature with spec/plan/tasks
|
|
400
|
+
maina stats # show verification metrics
|
|
401
|
+
\`\`\`
|
|
402
|
+
|
|
403
|
+
## Conventions
|
|
404
|
+
|
|
405
|
+
- Runtime: ${stack.runtime}
|
|
406
|
+
- Test: \`${runCmd} test\`
|
|
407
|
+
- Conventional commits (feat, fix, refactor, test, docs, chore)
|
|
408
|
+
- No \`console.log\` in production code
|
|
409
|
+
- Diff-only: only fix issues on changed lines
|
|
410
|
+
- TDD always — write tests first
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function buildGeminiMd(stack: DetectedStack): string {
|
|
415
|
+
const runCmd = stack.runtime === "bun" ? "bun" : "npm";
|
|
416
|
+
return `# GEMINI.md
|
|
417
|
+
|
|
418
|
+
Instructions for Gemini CLI when working in this repository.
|
|
419
|
+
|
|
420
|
+
This repo uses [Maina](https://mainahq.com) for verification-first development.
|
|
421
|
+
Read \`.maina/constitution.md\` for project DNA — stack rules, conventions, and gates.
|
|
422
|
+
|
|
423
|
+
## Maina Workflow
|
|
424
|
+
|
|
425
|
+
Follow this order for every feature:
|
|
426
|
+
\`${WORKFLOW_ORDER}\`
|
|
427
|
+
|
|
428
|
+
## MCP Tools
|
|
429
|
+
|
|
430
|
+
Maina exposes MCP tools via \`.mcp.json\`. Use them:
|
|
431
|
+
|
|
432
|
+
${MCP_TOOLS_TABLE}
|
|
433
|
+
|
|
434
|
+
## Key Commands
|
|
435
|
+
|
|
436
|
+
- \`maina verify\` — run full verification pipeline
|
|
437
|
+
- \`maina commit\` — verify + commit
|
|
438
|
+
- \`maina review\` — two-stage code review
|
|
439
|
+
- \`maina context\` — generate focused codebase context
|
|
440
|
+
- \`maina doctor\` — check tool health
|
|
441
|
+
|
|
442
|
+
## Rules
|
|
443
|
+
|
|
444
|
+
- Runtime: ${stack.runtime}
|
|
445
|
+
- Test: \`${runCmd} test\`
|
|
446
|
+
- Conventional commits (feat, fix, refactor, test, docs, chore)
|
|
447
|
+
- No \`console.log\` in production code
|
|
448
|
+
- Diff-only: only report findings on changed lines
|
|
449
|
+
- TDD always — write tests first
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function buildCursorRules(stack: DetectedStack): string {
|
|
454
|
+
const runCmd = stack.runtime === "bun" ? "bun" : "npm";
|
|
455
|
+
return `# Cursor Rules
|
|
456
|
+
|
|
457
|
+
This repo uses Maina for verification-first development.
|
|
458
|
+
Read \`.maina/constitution.md\` for project DNA.
|
|
459
|
+
|
|
460
|
+
## Workflow Order
|
|
461
|
+
${WORKFLOW_ORDER}
|
|
462
|
+
|
|
463
|
+
## MCP Tools (via .mcp.json)
|
|
464
|
+
${MCP_TOOLS_TABLE}
|
|
465
|
+
|
|
466
|
+
## Commands
|
|
467
|
+
- maina verify — run full verification pipeline
|
|
468
|
+
- maina commit — verify + commit
|
|
469
|
+
- maina review — two-stage code review
|
|
470
|
+
- maina context — generate focused codebase context
|
|
471
|
+
|
|
472
|
+
## Conventions
|
|
473
|
+
- Runtime: ${stack.runtime}
|
|
474
|
+
- Test: ${runCmd} test
|
|
475
|
+
- Conventional commits
|
|
476
|
+
- No console.log in production
|
|
477
|
+
- Diff-only: only report findings on changed lines
|
|
478
|
+
- TDD: write tests first, then implement
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ── AI-Generated Constitution ───────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
function buildProjectSummary(repoRoot: string, stack: DetectedStack): string {
|
|
485
|
+
const parts: string[] = [];
|
|
486
|
+
parts.push("## Detected Project Stack");
|
|
487
|
+
parts.push(`- Runtime: ${stack.runtime}`);
|
|
488
|
+
parts.push(`- Primary language: ${stack.language}`);
|
|
489
|
+
parts.push(`- All languages: ${stack.languages.join(", ")}`);
|
|
490
|
+
parts.push(`- Test runner: ${stack.testRunner}`);
|
|
491
|
+
parts.push(`- Linter: ${stack.linter}`);
|
|
492
|
+
parts.push(`- Framework: ${stack.framework}`);
|
|
493
|
+
|
|
494
|
+
// Read package.json for extra context
|
|
495
|
+
const pkgPath = join(repoRoot, "package.json");
|
|
496
|
+
if (existsSync(pkgPath)) {
|
|
497
|
+
try {
|
|
498
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as Record<
|
|
499
|
+
string,
|
|
500
|
+
unknown
|
|
501
|
+
>;
|
|
502
|
+
const deps = Object.keys(
|
|
503
|
+
(pkg.dependencies as Record<string, string>) ?? {},
|
|
504
|
+
);
|
|
505
|
+
const devDeps = Object.keys(
|
|
506
|
+
(pkg.devDependencies as Record<string, string>) ?? {},
|
|
507
|
+
);
|
|
508
|
+
if (deps.length > 0) {
|
|
509
|
+
parts.push(`\n## Dependencies\n${deps.join(", ")}`);
|
|
510
|
+
}
|
|
511
|
+
if (devDeps.length > 0) {
|
|
512
|
+
parts.push(`\n## Dev Dependencies\n${devDeps.join(", ")}`);
|
|
513
|
+
}
|
|
514
|
+
if (pkg.description) {
|
|
515
|
+
parts.push(`\n## Project Description\n${pkg.description}`);
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
// Ignore parse errors
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Check for common config files
|
|
523
|
+
const configFiles: string[] = [];
|
|
524
|
+
const checks = [
|
|
525
|
+
"tsconfig.json",
|
|
526
|
+
"biome.json",
|
|
527
|
+
".eslintrc.json",
|
|
528
|
+
"jest.config.ts",
|
|
529
|
+
"vitest.config.ts",
|
|
530
|
+
"Dockerfile",
|
|
531
|
+
"docker-compose.yml",
|
|
532
|
+
".env.example",
|
|
533
|
+
"Makefile",
|
|
534
|
+
];
|
|
535
|
+
for (const f of checks) {
|
|
536
|
+
if (existsSync(join(repoRoot, f))) {
|
|
537
|
+
configFiles.push(f);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (configFiles.length > 0) {
|
|
541
|
+
parts.push(`\n## Config Files Found\n${configFiles.join(", ")}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return parts.join("\n");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function tryGenerateConstitution(
|
|
548
|
+
repoRoot: string,
|
|
549
|
+
stack: DetectedStack,
|
|
550
|
+
): Promise<string | null> {
|
|
551
|
+
try {
|
|
552
|
+
const { tryAIGenerate } = await import("../ai/try-generate");
|
|
553
|
+
const mainaDir = join(repoRoot, ".maina");
|
|
554
|
+
const summary = buildProjectSummary(repoRoot, stack);
|
|
555
|
+
|
|
556
|
+
const result = await tryAIGenerate(
|
|
557
|
+
"init-constitution",
|
|
558
|
+
mainaDir,
|
|
559
|
+
{
|
|
560
|
+
stack_runtime: stack.runtime,
|
|
561
|
+
stack_language: stack.language,
|
|
562
|
+
stack_languages: stack.languages.join(", "),
|
|
563
|
+
stack_testRunner: stack.testRunner,
|
|
564
|
+
stack_linter: stack.linter,
|
|
565
|
+
stack_framework: stack.framework,
|
|
566
|
+
},
|
|
567
|
+
`Generate a project constitution for this software project based on the detected stack information below.
|
|
568
|
+
|
|
569
|
+
A constitution defines non-negotiable rules injected into every AI call. It should include:
|
|
570
|
+
1. Stack section — runtime, language, linter, test runner, framework
|
|
571
|
+
2. Architecture section — key architectural constraints (infer from the stack)
|
|
572
|
+
3. Verification section — what must pass before code merges
|
|
573
|
+
4. Conventions section — coding conventions (infer from the stack)
|
|
574
|
+
|
|
575
|
+
Replace [NEEDS CLARIFICATION] placeholders with reasonable defaults based on the stack.
|
|
576
|
+
Keep it concise (under 50 lines). Use markdown format starting with "# Project Constitution".
|
|
577
|
+
|
|
578
|
+
${summary}`,
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
if (result.fromAI && result.text) {
|
|
582
|
+
return result.text;
|
|
583
|
+
}
|
|
584
|
+
} catch {
|
|
585
|
+
// AI unavailable — fall back to static template
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
216
590
|
const REVIEW_PROMPT_TEMPLATE = `# Review Prompt
|
|
217
591
|
|
|
218
592
|
Review the following code changes for:
|
|
@@ -266,11 +640,14 @@ interface FileEntry {
|
|
|
266
640
|
content: string;
|
|
267
641
|
}
|
|
268
642
|
|
|
269
|
-
function getFileManifest(
|
|
643
|
+
function getFileManifest(
|
|
644
|
+
stack: DetectedStack,
|
|
645
|
+
constitutionOverride?: string,
|
|
646
|
+
): FileEntry[] {
|
|
270
647
|
return [
|
|
271
648
|
{
|
|
272
649
|
relativePath: ".maina/constitution.md",
|
|
273
|
-
content: buildConstitution(stack),
|
|
650
|
+
content: constitutionOverride ?? buildConstitution(stack),
|
|
274
651
|
},
|
|
275
652
|
{
|
|
276
653
|
relativePath: ".maina/prompts/review.md",
|
|
@@ -288,6 +665,26 @@ function getFileManifest(stack: DetectedStack): FileEntry[] {
|
|
|
288
665
|
relativePath: ".github/workflows/maina-ci.yml",
|
|
289
666
|
content: buildCiWorkflow(stack),
|
|
290
667
|
},
|
|
668
|
+
{
|
|
669
|
+
relativePath: ".github/copilot-instructions.md",
|
|
670
|
+
content: buildCopilotInstructions(stack),
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
relativePath: ".mcp.json",
|
|
674
|
+
content: buildMcpJson(),
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
relativePath: "CLAUDE.md",
|
|
678
|
+
content: buildClaudeMd(stack),
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
relativePath: "GEMINI.md",
|
|
682
|
+
content: buildGeminiMd(stack),
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
relativePath: ".cursorrules",
|
|
686
|
+
content: buildCursorRules(stack),
|
|
687
|
+
},
|
|
291
688
|
];
|
|
292
689
|
}
|
|
293
690
|
|
|
@@ -341,6 +738,7 @@ export async function bootstrap(
|
|
|
341
738
|
options?: InitOptions,
|
|
342
739
|
): Promise<Result<InitReport>> {
|
|
343
740
|
const force = options?.force ?? false;
|
|
741
|
+
const aiGenerate = options?.aiGenerate ?? false;
|
|
344
742
|
const mainaDir = join(repoRoot, ".maina");
|
|
345
743
|
const created: string[] = [];
|
|
346
744
|
const skipped: string[] = [];
|
|
@@ -349,8 +747,8 @@ export async function bootstrap(
|
|
|
349
747
|
// Detect project stack from package.json
|
|
350
748
|
const detectedStack = detectStack(repoRoot);
|
|
351
749
|
|
|
352
|
-
// Detect available verification tools on PATH
|
|
353
|
-
const detectedToolsList = await detectTools();
|
|
750
|
+
// Detect available verification tools on PATH (filtered by project languages)
|
|
751
|
+
const detectedToolsList = await detectTools(detectedStack.languages);
|
|
354
752
|
|
|
355
753
|
// Ensure .maina/ exists
|
|
356
754
|
mkdirSync(mainaDir, { recursive: true });
|
|
@@ -360,8 +758,22 @@ export async function bootstrap(
|
|
|
360
758
|
mkdirSync(join(repoRoot, dir), { recursive: true });
|
|
361
759
|
}
|
|
362
760
|
|
|
761
|
+
// Try AI-generated constitution when requested
|
|
762
|
+
let constitutionOverride: string | undefined;
|
|
763
|
+
let aiGenerated = false;
|
|
764
|
+
if (aiGenerate) {
|
|
765
|
+
const aiConstitution = await tryGenerateConstitution(
|
|
766
|
+
repoRoot,
|
|
767
|
+
detectedStack,
|
|
768
|
+
);
|
|
769
|
+
if (aiConstitution) {
|
|
770
|
+
constitutionOverride = aiConstitution;
|
|
771
|
+
aiGenerated = true;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
363
775
|
// Scaffold each file with stack-aware templates
|
|
364
|
-
const manifest = getFileManifest(detectedStack);
|
|
776
|
+
const manifest = getFileManifest(detectedStack, constitutionOverride);
|
|
365
777
|
for (const entry of manifest) {
|
|
366
778
|
const fullPath = join(repoRoot, entry.relativePath);
|
|
367
779
|
const dirPath = join(fullPath, "..");
|
|
@@ -396,6 +808,7 @@ export async function bootstrap(
|
|
|
396
808
|
directory: mainaDir,
|
|
397
809
|
detectedStack,
|
|
398
810
|
detectedTools: detectedToolsList,
|
|
811
|
+
aiGenerated,
|
|
399
812
|
},
|
|
400
813
|
};
|
|
401
814
|
} catch (e) {
|