@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/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 +614 -77
- 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,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
|
-
//
|
|
49
|
-
const
|
|
50
|
-
|
|
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
|
-
|
|
101
|
+
// .NET (C#/F#) — check for .csproj, .fsproj, .sln files
|
|
53
102
|
try {
|
|
54
|
-
|
|
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
|
-
|
|
113
|
+
// Directory not readable — skip
|
|
57
114
|
}
|
|
58
115
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
//
|
|
241
|
+
// ── Infer conventions from project context ───────────────────────────
|
|
242
|
+
const conventions: string[] = [];
|
|
243
|
+
|
|
244
|
+
// Check for conventional commits
|
|
66
245
|
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
existsSync(join(repoRoot, "
|
|
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
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
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
|
-
//
|
|
82
|
-
if (existsSync(join(repoRoot, "tsconfig.json"))
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
//
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
//
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
} else if (
|
|
103
|
-
|
|
104
|
-
} else if (
|
|
105
|
-
|
|
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
|
-
//
|
|
109
|
-
if (
|
|
110
|
-
|
|
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
|
-
|
|
420
|
+
${archSection}
|
|
164
421
|
|
|
165
422
|
## Verification
|
|
166
|
-
|
|
167
|
-
- Diff-only: only report findings on changed lines
|
|
423
|
+
${verifyLines.join("\n")}
|
|
168
424
|
|
|
169
425
|
## Conventions
|
|
170
|
-
|
|
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
|
-
|
|
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(
|
|
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) {
|