@massu/core 0.8.1 → 0.9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, auto-learning pipeline, tiered tooling (12 free / 72 total), 55+ workflow commands, 15 agents, 20+ patterns",
6
6
  "main": "src/server.ts",
@@ -0,0 +1,342 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ /**
5
+ * CLAUDE.md content generation — framework-aware templates and directory scanning.
6
+ *
7
+ * Used by `massu init` to generate a tailored CLAUDE.md for the detected project.
8
+ */
9
+
10
+ import { readdirSync, statSync } from 'fs';
11
+ import { resolve, relative, basename } from 'path';
12
+
13
+ // ============================================================
14
+ // Types (re-used from init.ts but kept minimal to avoid circular deps)
15
+ // ============================================================
16
+
17
+ interface FrameworkInfo {
18
+ type: string;
19
+ router: string;
20
+ orm: string;
21
+ ui: string;
22
+ }
23
+
24
+ interface PythonInfo {
25
+ detected: boolean;
26
+ root: string;
27
+ hasFastapi: boolean;
28
+ hasSqlalchemy: boolean;
29
+ hasAlembic: boolean;
30
+ }
31
+
32
+ // ============================================================
33
+ // Directory Structure Scanner (P-A03)
34
+ // ============================================================
35
+
36
+ const EXCLUDED_DIRS = new Set([
37
+ 'node_modules', '.git', '.venv', 'venv', '__pycache__',
38
+ 'dist', 'build', '.next', '.nuxt', '.svelte-kit', 'coverage',
39
+ '.massu', '.turbo', '.cache', '.output',
40
+ ]);
41
+
42
+ export function scanDirectoryStructure(projectRoot: string, maxDepth: number = 2): string {
43
+ const lines: string[] = [];
44
+ const rootName = basename(projectRoot);
45
+ lines.push(`${rootName}/`);
46
+ scanLevel(projectRoot, '', maxDepth, 0, lines);
47
+ return lines.join('\n');
48
+ }
49
+
50
+ function scanLevel(
51
+ dir: string,
52
+ prefix: string,
53
+ maxDepth: number,
54
+ currentDepth: number,
55
+ lines: string[],
56
+ ): void {
57
+ if (currentDepth >= maxDepth) return;
58
+
59
+ let entries: string[];
60
+ try {
61
+ entries = readdirSync(dir).sort();
62
+ } catch {
63
+ return;
64
+ }
65
+
66
+ // Separate dirs and files, filter excluded
67
+ const dirs: string[] = [];
68
+ const files: string[] = [];
69
+ for (const entry of entries) {
70
+ if (entry.startsWith('.') && EXCLUDED_DIRS.has(entry)) continue;
71
+ if (EXCLUDED_DIRS.has(entry)) continue;
72
+ try {
73
+ const stat = statSync(resolve(dir, entry));
74
+ if (stat.isDirectory()) dirs.push(entry);
75
+ else files.push(entry);
76
+ } catch {
77
+ // Skip unreadable entries
78
+ }
79
+ }
80
+
81
+ const allEntries = [...dirs, ...files];
82
+ for (let i = 0; i < allEntries.length; i++) {
83
+ const entry = allEntries[i];
84
+ const isLast = i === allEntries.length - 1;
85
+ const connector = isLast ? '\u2514\u2500\u2500 ' : '\u251c\u2500\u2500 ';
86
+ const childPrefix = isLast ? ' ' : '\u2502 ';
87
+ const isDir = dirs.includes(entry);
88
+
89
+ lines.push(`${prefix}${connector}${entry}${isDir ? '/' : ''}`);
90
+
91
+ if (isDir) {
92
+ scanLevel(resolve(dir, entry), prefix + childPrefix, maxDepth, currentDepth + 1, lines);
93
+ }
94
+ }
95
+ }
96
+
97
+ // ============================================================
98
+ // Content Builder (P-A02)
99
+ // ============================================================
100
+
101
+ export function buildClaudeMdContent(
102
+ projectName: string,
103
+ projectRoot: string,
104
+ framework: FrameworkInfo,
105
+ python: PythonInfo,
106
+ ): string {
107
+ const sections: string[] = [];
108
+
109
+ // 1. Project Overview
110
+ sections.push(buildProjectOverview(projectName, framework, python));
111
+
112
+ // 2. Tech Stack
113
+ sections.push(buildTechStack(framework, python));
114
+
115
+ // 3. Directory Structure
116
+ sections.push(buildDirectorySection(projectRoot));
117
+
118
+ // 4. Coding Conventions
119
+ sections.push(buildCodingConventions(framework, python));
120
+
121
+ // 5. Testing
122
+ sections.push(buildTestingSection(framework, python));
123
+
124
+ // 6. Massu Workflow
125
+ sections.push(buildMassuWorkflow());
126
+
127
+ // 7. Memory System
128
+ sections.push(buildMemorySystem());
129
+
130
+ // 8. Critical Rules
131
+ sections.push(buildCriticalRules());
132
+
133
+ return sections.join('\n\n---\n\n') + '\n';
134
+ }
135
+
136
+ // ---- Section Builders ----
137
+
138
+ function buildProjectOverview(
139
+ projectName: string,
140
+ framework: FrameworkInfo,
141
+ python: PythonInfo,
142
+ ): string {
143
+ const stack: string[] = [];
144
+ if (framework.type !== 'javascript') stack.push(capitalize(framework.type));
145
+ if (framework.ui !== 'none') stack.push(formatUiName(framework.ui));
146
+ if (framework.router !== 'none') stack.push(framework.router.toUpperCase());
147
+ if (framework.orm !== 'none') stack.push(capitalize(framework.orm));
148
+ if (python.detected) {
149
+ stack.push('Python');
150
+ if (python.hasFastapi) stack.push('FastAPI');
151
+ }
152
+
153
+ const stackStr = stack.length > 0 ? stack.join(', ') : 'JavaScript';
154
+
155
+ return `# ${projectName}
156
+
157
+ ## Project Overview
158
+
159
+ ${projectName} is a ${stackStr} project.
160
+
161
+ <!-- Add a brief description of what this project does -->`;
162
+ }
163
+
164
+ function buildTechStack(framework: FrameworkInfo, python: PythonInfo): string {
165
+ const rows: string[] = [];
166
+ rows.push('| Technology | Details |');
167
+ rows.push('|-----------|---------|');
168
+
169
+ rows.push(`| Language | ${capitalize(framework.type)} |`);
170
+ if (framework.ui !== 'none') rows.push(`| UI Framework | ${formatUiName(framework.ui)} |`);
171
+ if (framework.router !== 'none') rows.push(`| Router/API | ${framework.router.toUpperCase()} |`);
172
+ if (framework.orm !== 'none') rows.push(`| ORM | ${capitalize(framework.orm)} |`);
173
+ if (python.detected) {
174
+ rows.push('| Python | Yes |');
175
+ if (python.hasFastapi) rows.push('| Python Framework | FastAPI |');
176
+ if (python.hasSqlalchemy) rows.push('| Python ORM | SQLAlchemy |');
177
+ if (python.hasAlembic) rows.push('| Migrations | Alembic |');
178
+ }
179
+
180
+ return `## Tech Stack\n\n${rows.join('\n')}`;
181
+ }
182
+
183
+ function buildDirectorySection(projectRoot: string): string {
184
+ const tree = scanDirectoryStructure(projectRoot);
185
+ return `## Directory Structure\n\n\`\`\`\n${tree}\n\`\`\``;
186
+ }
187
+
188
+ function buildCodingConventions(framework: FrameworkInfo, python: PythonInfo): string {
189
+ const rules: string[] = [];
190
+
191
+ // Language-level conventions
192
+ if (framework.type === 'typescript') {
193
+ rules.push('- Use ESM imports (`import`), not CommonJS (`require`)');
194
+ rules.push('- Enable strict TypeScript (`strict: true` in tsconfig.json)');
195
+ rules.push('- Prefer explicit types over `any`');
196
+ }
197
+
198
+ // UI framework conventions
199
+ switch (framework.ui) {
200
+ case 'nextjs':
201
+ rules.push('- Use App Router conventions (`app/` directory)');
202
+ rules.push('- Default to Server Components; add `"use client"` only when needed');
203
+ rules.push('- Use `next/image` for images, `next/link` for navigation');
204
+ rules.push('- API routes go in `app/api/` using Route Handlers');
205
+ break;
206
+ case 'sveltekit':
207
+ rules.push('- Use load functions for data fetching (`+page.server.ts`)');
208
+ rules.push('- Use form actions for mutations');
209
+ rules.push('- Server-only code in `+server.ts` files');
210
+ break;
211
+ case 'react':
212
+ rules.push('- Prefer functional components with hooks');
213
+ rules.push('- Colocate component, styles, and tests');
214
+ break;
215
+ }
216
+
217
+ // Router conventions
218
+ if (framework.router === 'trpc') {
219
+ rules.push('- Define tRPC routers with Zod input validation');
220
+ rules.push('- Keep router files focused (one domain per router)');
221
+ }
222
+
223
+ // ORM conventions
224
+ if (framework.orm === 'prisma') {
225
+ rules.push('- Define models in `prisma/schema.prisma`');
226
+ rules.push('- Run `npx prisma generate` after schema changes');
227
+ } else if (framework.orm === 'drizzle') {
228
+ rules.push('- Define schemas with Drizzle table builders');
229
+ rules.push('- Run migrations with `drizzle-kit`');
230
+ }
231
+
232
+ // Python conventions
233
+ if (python.detected) {
234
+ rules.push('- Use type hints for function signatures');
235
+ rules.push('- Use `async def` for async endpoints');
236
+ if (python.hasFastapi) {
237
+ rules.push('- Use Pydantic models for request/response schemas');
238
+ rules.push('- Organize routes with `APIRouter`');
239
+ }
240
+ if (python.hasSqlalchemy) {
241
+ rules.push('- Use SQLAlchemy 2.0 style (select/insert builders)');
242
+ }
243
+ }
244
+
245
+ if (rules.length === 0) {
246
+ rules.push('- Follow consistent naming conventions');
247
+ rules.push('- Keep functions small and focused');
248
+ }
249
+
250
+ return `## Coding Conventions\n\n${rules.join('\n')}`;
251
+ }
252
+
253
+ function buildTestingSection(framework: FrameworkInfo, python: PythonInfo): string {
254
+ const lines: string[] = [];
255
+
256
+ if (framework.type === 'typescript') {
257
+ lines.push('- Test framework: vitest (or jest)');
258
+ lines.push('- Test files: `__tests__/*.test.ts` or `*.test.ts` colocated');
259
+ lines.push('- Run tests: `npm test`');
260
+ }
261
+
262
+ if (python.detected) {
263
+ lines.push('- Python tests: pytest');
264
+ lines.push('- Test files: `tests/` directory or `test_*.py` files');
265
+ lines.push('- Run: `pytest`');
266
+ }
267
+
268
+ if (lines.length === 0) {
269
+ lines.push('- Configure a test framework for this project');
270
+ lines.push('- Run tests before committing changes');
271
+ }
272
+
273
+ return `## Testing\n\n${lines.join('\n')}`;
274
+ }
275
+
276
+ function buildMassuWorkflow(): string {
277
+ return `## Massu Workflow
278
+
279
+ This project uses [Massu AI](https://massu.ai) for development governance.
280
+
281
+ ### Common Commands
282
+
283
+ | Command | Purpose |
284
+ |---------|---------|
285
+ | \`/massu-create-plan\` | Create an implementation plan |
286
+ | \`/massu-plan\` | Audit and improve a plan |
287
+ | \`/massu-golden-path\` | Full implementation flow (plan to push) |
288
+ | \`/massu-test\` | Run tests with failure analysis |
289
+ | \`/massu-commit\` | Pre-commit verification gate |
290
+ | \`/massu-push\` | Pre-push verification gate |
291
+ | \`/massu-status\` | Project health dashboard |
292
+ | \`/massu-debug\` | Systematic debugging |
293
+
294
+ ### Workflow Flow
295
+
296
+ \`\`\`
297
+ /massu-create-plan -> /massu-plan (audit) -> /massu-golden-path (implement + push)
298
+ \`\`\``;
299
+ }
300
+
301
+ function buildMemorySystem(): string {
302
+ return `## Memory System
303
+
304
+ Massu maintains persistent memory across sessions in \`~/.claude/projects/.../memory/\`.
305
+
306
+ - **User memories**: Your role, preferences, and expertise
307
+ - **Feedback memories**: Corrections and validated approaches
308
+ - **Project memories**: Ongoing work, decisions, deadlines
309
+ - **Reference memories**: External resources and tools
310
+
311
+ Memory is automatically loaded at session start and updated as you work.`;
312
+ }
313
+
314
+ function buildCriticalRules(): string {
315
+ return `## Critical Rules
316
+
317
+ 1. **Never commit secrets** — no API keys, tokens, or credentials in code
318
+ 2. **Run tests before committing** — all tests must pass
319
+ 3. **Verify before claiming done** — use VR-* verification checks
320
+ 4. **Fix all issues encountered** — pre-existing issues get fixed too
321
+ 5. **Read before editing** — understand existing code before modifying
322
+
323
+ <!-- Add project-specific rules as you discover them -->`;
324
+ }
325
+
326
+ // ---- Helpers ----
327
+
328
+ function capitalize(str: string): string {
329
+ return str.charAt(0).toUpperCase() + str.slice(1);
330
+ }
331
+
332
+ function formatUiName(name: string): string {
333
+ const names: Record<string, string> = {
334
+ nextjs: 'Next.js',
335
+ sveltekit: 'SvelteKit',
336
+ nuxt: 'Nuxt',
337
+ angular: 'Angular',
338
+ vue: 'Vue',
339
+ react: 'React',
340
+ };
341
+ return names[name] ?? capitalize(name);
342
+ }
@@ -15,6 +15,7 @@
15
15
  * 8. better-sqlite3 native module loads
16
16
  * 9. Node.js version >= 18
17
17
  * 10. Git repository detected
18
+ * 11. CLAUDE.md exists with content
18
19
  */
19
20
 
20
21
  import { existsSync, readFileSync, readdirSync } from 'fs';
@@ -373,6 +374,32 @@ function checkPythonHealth(projectRoot: string): CheckResult | null {
373
374
  };
374
375
  }
375
376
 
377
+ function checkClaudeMd(projectRoot: string): CheckResult {
378
+ const claudeMdPath = resolve(projectRoot, 'CLAUDE.md');
379
+ if (!existsSync(claudeMdPath)) {
380
+ return {
381
+ name: 'CLAUDE.md',
382
+ status: 'warn',
383
+ detail: 'CLAUDE.md not found. Run: npx massu init (or create manually)',
384
+ };
385
+ }
386
+
387
+ const content = readFileSync(claudeMdPath, 'utf-8');
388
+ if (content.trim().length < 50) {
389
+ return {
390
+ name: 'CLAUDE.md',
391
+ status: 'warn',
392
+ detail: 'CLAUDE.md exists but appears empty or minimal',
393
+ };
394
+ }
395
+
396
+ return {
397
+ name: 'CLAUDE.md',
398
+ status: 'pass',
399
+ detail: 'CLAUDE.md found and has content',
400
+ };
401
+ }
402
+
376
403
  // ============================================================
377
404
  // Main Doctor Flow
378
405
  // ============================================================
@@ -397,6 +424,7 @@ export async function runDoctor(): Promise<void> {
397
424
  checkNodeVersion(),
398
425
  await checkGitRepo(projectRoot),
399
426
  await checkLicenseStatus(),
427
+ checkClaudeMd(projectRoot),
400
428
  ];
401
429
 
402
430
  // Add Python health check if configured
@@ -18,6 +18,7 @@ import { resolve, basename, dirname } from 'path';
18
18
  import { fileURLToPath } from 'url';
19
19
  import { homedir } from 'os';
20
20
  import { backfillMemoryFiles } from '../memory-file-ingest.ts';
21
+ import { buildClaudeMdContent } from '../claude-md-templates.ts';
21
22
 
22
23
  const __filename = fileURLToPath(import.meta.url);
23
24
  const __dirname = dirname(__filename);
@@ -37,6 +38,8 @@ interface FrameworkDetection {
37
38
  }
38
39
 
39
40
  interface InitResult {
41
+ claudeMdCreated: boolean;
42
+ claudeMdSkipped: boolean;
40
43
  configCreated: boolean;
41
44
  configSkipped: boolean;
42
45
  mcpRegistered: boolean;
@@ -289,6 +292,28 @@ ${yamlStringify(config)}`;
289
292
  return true;
290
293
  }
291
294
 
295
+ // ============================================================
296
+ // CLAUDE.md Generation
297
+ // ============================================================
298
+
299
+ export function generateClaudeMd(
300
+ projectRoot: string,
301
+ framework: FrameworkDetection,
302
+ python: PythonDetection,
303
+ ): { created: boolean; skipped: boolean } {
304
+ const claudeMdPath = resolve(projectRoot, 'CLAUDE.md');
305
+
306
+ // NEVER overwrite existing CLAUDE.md
307
+ if (existsSync(claudeMdPath)) {
308
+ return { created: false, skipped: true };
309
+ }
310
+
311
+ const projectName = basename(projectRoot);
312
+ const content = buildClaudeMdContent(projectName, projectRoot, framework, python);
313
+ writeFileSync(claudeMdPath, content, 'utf-8');
314
+ return { created: true, skipped: false };
315
+ }
316
+
292
317
  // ============================================================
293
318
  // MCP Server Registration
294
319
  // ============================================================
@@ -552,6 +577,14 @@ export async function runInit(): Promise<void> {
552
577
  console.log(` Detected: ${pyParts.join(', ')} (root: ${python.root})`);
553
578
  }
554
579
 
580
+ // Step 1.5: Generate CLAUDE.md (MUST be first content step — incident 2026-04-13)
581
+ const claudeMdResult = generateClaudeMd(projectRoot, framework, python);
582
+ if (claudeMdResult.created) {
583
+ console.log(' Created CLAUDE.md (project instructions for Claude Code)');
584
+ } else {
585
+ console.log(' CLAUDE.md already exists (preserved)');
586
+ }
587
+
555
588
  // Step 2: Create config
556
589
  const configCreated = generateConfig(projectRoot, framework);
557
590
  if (configCreated) {
@@ -599,7 +599,7 @@ export function indexAllKnowledge(db: Database.Database): IndexStats {
599
599
  // Parse plan documents for structured metadata
600
600
  if (category === 'plan') {
601
601
  // Extract plan items (P1-001, P2-001, etc.)
602
- const planItemRegex = /^###\s+(P\d+-\d+):\s+(.+)$/gm;
602
+ const planItemRegex = /^###\s+(P[-A-Z]*\d*-?\w+):\s+(.+)$/gm;
603
603
  let planMatch;
604
604
  while ((planMatch = planItemRegex.exec(content)) !== null) {
605
605
  insertChunk.run(docId, 'pattern', planMatch[1], `${planMatch[1]}: ${planMatch[2]}`, null, null, JSON.stringify({ plan_item_id: planMatch[1] }));