@mainahq/core 1.0.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 +1 -1
- 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 +443 -74
- package/src/verify/__tests__/detect-filter.test.ts +303 -0
- package/src/verify/detect.ts +162 -25
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 {
|
|
@@ -179,6 +267,11 @@ function buildAgentsMd(stack: DetectedStack): string {
|
|
|
179
267
|
|
|
180
268
|
This repo uses [Maina](https://github.com/mainahq/maina) for verification-first development.
|
|
181
269
|
|
|
270
|
+
## Workflow Order
|
|
271
|
+
|
|
272
|
+
Follow this order for every feature:
|
|
273
|
+
\`${WORKFLOW_ORDER}\`
|
|
274
|
+
|
|
182
275
|
## Quick Start
|
|
183
276
|
\`\`\`bash
|
|
184
277
|
${installCmd}
|
|
@@ -199,6 +292,10 @@ 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
|
|------|---------|-----------|
|
|
@@ -206,6 +303,9 @@ maina commit # verify + commit
|
|
|
206
303
|
| \`AGENTS.md\` | Agent instructions — commands, conventions | Team |
|
|
207
304
|
| \`.github/copilot-instructions.md\` | Copilot agent instructions + MCP tools | Team |
|
|
208
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 |
|
|
209
309
|
| \`.maina/prompts/*.md\` | Prompt overrides for review/commit/etc | Maina (via \`maina learn\`) |
|
|
210
310
|
|
|
211
311
|
## Runtime
|
|
@@ -220,7 +320,12 @@ function buildCopilotInstructions(stack: DetectedStack): string {
|
|
|
220
320
|
|
|
221
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.
|
|
222
322
|
|
|
223
|
-
## Workflow
|
|
323
|
+
## Workflow Order
|
|
324
|
+
|
|
325
|
+
Follow this order for every feature:
|
|
326
|
+
\`${WORKFLOW_ORDER}\`
|
|
327
|
+
|
|
328
|
+
## Step-by-step
|
|
224
329
|
|
|
225
330
|
1. **Get context** — call \`maina getContext\` to understand codebase state
|
|
226
331
|
2. **Write tests first** — TDD always. Write failing tests, then implement
|
|
@@ -230,14 +335,7 @@ You are working on a codebase verified by [Maina](https://mainahq.com), the veri
|
|
|
230
335
|
|
|
231
336
|
## Available MCP Tools
|
|
232
337
|
|
|
233
|
-
|
|
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 |
|
|
338
|
+
${MCP_TOOLS_TABLE}
|
|
241
339
|
|
|
242
340
|
## Conventions
|
|
243
341
|
|
|
@@ -253,6 +351,242 @@ Issues labeled \`audit\` come from maina's daily verification. Fix the specific
|
|
|
253
351
|
`;
|
|
254
352
|
}
|
|
255
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
|
+
|
|
256
590
|
const REVIEW_PROMPT_TEMPLATE = `# Review Prompt
|
|
257
591
|
|
|
258
592
|
Review the following code changes for:
|
|
@@ -306,11 +640,14 @@ interface FileEntry {
|
|
|
306
640
|
content: string;
|
|
307
641
|
}
|
|
308
642
|
|
|
309
|
-
function getFileManifest(
|
|
643
|
+
function getFileManifest(
|
|
644
|
+
stack: DetectedStack,
|
|
645
|
+
constitutionOverride?: string,
|
|
646
|
+
): FileEntry[] {
|
|
310
647
|
return [
|
|
311
648
|
{
|
|
312
649
|
relativePath: ".maina/constitution.md",
|
|
313
|
-
content: buildConstitution(stack),
|
|
650
|
+
content: constitutionOverride ?? buildConstitution(stack),
|
|
314
651
|
},
|
|
315
652
|
{
|
|
316
653
|
relativePath: ".maina/prompts/review.md",
|
|
@@ -332,6 +669,22 @@ function getFileManifest(stack: DetectedStack): FileEntry[] {
|
|
|
332
669
|
relativePath: ".github/copilot-instructions.md",
|
|
333
670
|
content: buildCopilotInstructions(stack),
|
|
334
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
|
+
},
|
|
335
688
|
];
|
|
336
689
|
}
|
|
337
690
|
|
|
@@ -385,6 +738,7 @@ export async function bootstrap(
|
|
|
385
738
|
options?: InitOptions,
|
|
386
739
|
): Promise<Result<InitReport>> {
|
|
387
740
|
const force = options?.force ?? false;
|
|
741
|
+
const aiGenerate = options?.aiGenerate ?? false;
|
|
388
742
|
const mainaDir = join(repoRoot, ".maina");
|
|
389
743
|
const created: string[] = [];
|
|
390
744
|
const skipped: string[] = [];
|
|
@@ -393,8 +747,8 @@ export async function bootstrap(
|
|
|
393
747
|
// Detect project stack from package.json
|
|
394
748
|
const detectedStack = detectStack(repoRoot);
|
|
395
749
|
|
|
396
|
-
// Detect available verification tools on PATH
|
|
397
|
-
const detectedToolsList = await detectTools();
|
|
750
|
+
// Detect available verification tools on PATH (filtered by project languages)
|
|
751
|
+
const detectedToolsList = await detectTools(detectedStack.languages);
|
|
398
752
|
|
|
399
753
|
// Ensure .maina/ exists
|
|
400
754
|
mkdirSync(mainaDir, { recursive: true });
|
|
@@ -404,8 +758,22 @@ export async function bootstrap(
|
|
|
404
758
|
mkdirSync(join(repoRoot, dir), { recursive: true });
|
|
405
759
|
}
|
|
406
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
|
+
|
|
407
775
|
// Scaffold each file with stack-aware templates
|
|
408
|
-
const manifest = getFileManifest(detectedStack);
|
|
776
|
+
const manifest = getFileManifest(detectedStack, constitutionOverride);
|
|
409
777
|
for (const entry of manifest) {
|
|
410
778
|
const fullPath = join(repoRoot, entry.relativePath);
|
|
411
779
|
const dirPath = join(fullPath, "..");
|
|
@@ -440,6 +808,7 @@ export async function bootstrap(
|
|
|
440
808
|
directory: mainaDir,
|
|
441
809
|
detectedStack,
|
|
442
810
|
detectedTools: detectedToolsList,
|
|
811
|
+
aiGenerated,
|
|
443
812
|
},
|
|
444
813
|
};
|
|
445
814
|
} catch (e) {
|