@massu/core 0.9.2 → 1.1.0
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/dist/cli.js +11182 -1559
- package/dist/hooks/auto-learning-pipeline.js +99 -19
- package/dist/hooks/classify-failure.js +99 -19
- package/dist/hooks/cost-tracker.js +97 -11
- package/dist/hooks/fix-detector.js +99 -19
- package/dist/hooks/incident-pipeline.js +97 -11
- package/dist/hooks/post-edit-context.js +97 -11
- package/dist/hooks/post-tool-use.js +101 -20
- package/dist/hooks/pre-compact.js +97 -11
- package/dist/hooks/pre-delete-check.js +97 -11
- package/dist/hooks/quality-event.js +97 -11
- package/dist/hooks/rule-enforcement-pipeline.js +97 -11
- package/dist/hooks/session-end.js +97 -11
- package/dist/hooks/session-start.js +8803 -782
- package/dist/hooks/user-prompt.js +98 -43
- package/package.json +13 -3
- package/reference/hook-execution-order.md +17 -25
- package/src/cli.ts +81 -2
- package/src/commands/config-check-drift.ts +132 -0
- package/src/commands/config-refresh.ts +224 -0
- package/src/commands/config-upgrade.ts +126 -0
- package/src/commands/doctor.ts +1 -29
- package/src/commands/init.ts +756 -216
- package/src/config.ts +168 -12
- package/src/detect/domain-inferrer.ts +142 -0
- package/src/detect/drift.ts +199 -0
- package/src/detect/framework-detector.ts +281 -0
- package/src/detect/index.ts +174 -0
- package/src/detect/migrate.ts +278 -0
- package/src/detect/monorepo-detector.ts +347 -0
- package/src/detect/package-detector.ts +728 -0
- package/src/detect/source-dir-detector.ts +264 -0
- package/src/detect/vr-command-map.ts +167 -0
- package/src/hooks/auto-learning-pipeline.ts +2 -2
- package/src/hooks/classify-failure.ts +2 -2
- package/src/hooks/fix-detector.ts +2 -2
- package/src/hooks/session-start.ts +43 -2
- package/src/hooks/user-prompt.ts +1 -21
- package/src/knowledge-indexer.ts +1 -1
- package/src/license.ts +1 -2
- package/src/memory-db.ts +0 -5
- package/src/memory-file-ingest.ts +6 -13
- package/src/tools.ts +0 -8
- package/templates/multi-runtime/massu.config.yaml +80 -0
- package/templates/python-django/massu.config.yaml +51 -0
- package/templates/python-fastapi/massu.config.yaml +50 -0
- package/templates/rust-actix/massu.config.yaml +38 -0
- package/templates/swift-ios/massu.config.yaml +37 -0
- package/templates/ts-nestjs/massu.config.yaml +43 -0
- package/templates/ts-nextjs/massu.config.yaml +43 -0
- package/README.md +0 -40
- package/src/claude-md-templates.ts +0 -342
- package/src/mcp-bridge-tools.ts +0 -458
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `massu config refresh` — re-run detection, diff against current config, apply
|
|
6
|
+
* or print-only (--dry-run).
|
|
7
|
+
*
|
|
8
|
+
* Merge semantics:
|
|
9
|
+
* - Detector-owned keys (framework, paths.source, verification, detection) are REFRESHED.
|
|
10
|
+
* - User-authored keys (rules, domains, canonical_paths, accessScopes,
|
|
11
|
+
* knownMismatches, dbAccessPattern, analytics, governance, security, team,
|
|
12
|
+
* regression, cloud, conventions, autoLearning, verification_types,
|
|
13
|
+
* python) are PRESERVED verbatim from the existing config.
|
|
14
|
+
*
|
|
15
|
+
* Flags:
|
|
16
|
+
* --dry-run Emit the diff to stdout, exit 0, never write.
|
|
17
|
+
* (none) Interactive: show diff, prompt for confirmation via @clack/prompts.
|
|
18
|
+
* When stdin is not a TTY, behaves as --dry-run with a note.
|
|
19
|
+
*
|
|
20
|
+
* Exit codes:
|
|
21
|
+
* 0 success (applied, or dry-run completed)
|
|
22
|
+
* 1 missing massu.config.yaml (run `massu init`)
|
|
23
|
+
* 2 unparseable massu.config.yaml
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync, readFileSync } from 'fs';
|
|
27
|
+
import { resolve } from 'path';
|
|
28
|
+
import { parse as parseYaml } from 'yaml';
|
|
29
|
+
import { runDetection } from '../detect/index.ts';
|
|
30
|
+
import { computeFingerprint } from '../detect/drift.ts';
|
|
31
|
+
import type { AnyConfig } from '../detect/migrate.ts';
|
|
32
|
+
import { buildConfigFromDetection, renderConfigYaml, writeConfigAtomic } from './init.ts';
|
|
33
|
+
|
|
34
|
+
const PRESERVED_FIELDS = [
|
|
35
|
+
'rules',
|
|
36
|
+
'domains',
|
|
37
|
+
'canonical_paths',
|
|
38
|
+
'verification_types',
|
|
39
|
+
'accessScopes',
|
|
40
|
+
'knownMismatches',
|
|
41
|
+
'dbAccessPattern',
|
|
42
|
+
'analytics',
|
|
43
|
+
'governance',
|
|
44
|
+
'security',
|
|
45
|
+
'team',
|
|
46
|
+
'regression',
|
|
47
|
+
'cloud',
|
|
48
|
+
'conventions',
|
|
49
|
+
'autoLearning',
|
|
50
|
+
'python',
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
export interface ConfigRefreshOptions {
|
|
54
|
+
dryRun?: boolean;
|
|
55
|
+
cwd?: string;
|
|
56
|
+
silent?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ConfigRefreshResult {
|
|
60
|
+
exitCode: 0 | 1 | 2;
|
|
61
|
+
applied: boolean;
|
|
62
|
+
dryRun: boolean;
|
|
63
|
+
diff: DiffLine[];
|
|
64
|
+
message?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DiffLine {
|
|
68
|
+
kind: 'add' | 'remove' | 'change' | 'same';
|
|
69
|
+
path: string;
|
|
70
|
+
before?: unknown;
|
|
71
|
+
after?: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function flatten(obj: unknown, prefix = ''): Record<string, unknown> {
|
|
75
|
+
const out: Record<string, unknown> = {};
|
|
76
|
+
if (obj === null || obj === undefined) {
|
|
77
|
+
out[prefix || '<root>'] = obj;
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
if (typeof obj !== 'object' || Array.isArray(obj)) {
|
|
81
|
+
out[prefix || '<root>'] = obj;
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
const rec = obj as Record<string, unknown>;
|
|
85
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
86
|
+
const p = prefix ? `${prefix}.${k}` : k;
|
|
87
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
88
|
+
Object.assign(out, flatten(v, p));
|
|
89
|
+
} else {
|
|
90
|
+
out[p] = v;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function computeDiff(before: AnyConfig, after: AnyConfig): DiffLine[] {
|
|
97
|
+
const b = flatten(before);
|
|
98
|
+
const a = flatten(after);
|
|
99
|
+
const keys = new Set<string>([...Object.keys(b), ...Object.keys(a)]);
|
|
100
|
+
const sorted = [...keys].sort();
|
|
101
|
+
const lines: DiffLine[] = [];
|
|
102
|
+
for (const k of sorted) {
|
|
103
|
+
const bVal = b[k];
|
|
104
|
+
const aVal = a[k];
|
|
105
|
+
const bHas = k in b;
|
|
106
|
+
const aHas = k in a;
|
|
107
|
+
if (bHas && !aHas) {
|
|
108
|
+
lines.push({ kind: 'remove', path: k, before: bVal });
|
|
109
|
+
} else if (!bHas && aHas) {
|
|
110
|
+
lines.push({ kind: 'add', path: k, after: aVal });
|
|
111
|
+
} else if (JSON.stringify(bVal) !== JSON.stringify(aVal)) {
|
|
112
|
+
lines.push({ kind: 'change', path: k, before: bVal, after: aVal });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function mergeRefresh(existing: AnyConfig, refreshed: AnyConfig): AnyConfig {
|
|
119
|
+
const out: AnyConfig = { ...refreshed };
|
|
120
|
+
for (const field of PRESERVED_FIELDS) {
|
|
121
|
+
if (existing[field] !== undefined) {
|
|
122
|
+
out[field] = existing[field];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderDiff(diff: DiffLine[]): string {
|
|
129
|
+
if (diff.length === 0) return '(no changes)\n';
|
|
130
|
+
const lines: string[] = [];
|
|
131
|
+
for (const d of diff) {
|
|
132
|
+
if (d.kind === 'add') lines.push(`+ ${d.path}: ${JSON.stringify(d.after)}`);
|
|
133
|
+
else if (d.kind === 'remove') lines.push(`- ${d.path}: ${JSON.stringify(d.before)}`);
|
|
134
|
+
else if (d.kind === 'change') {
|
|
135
|
+
lines.push(`~ ${d.path}: ${JSON.stringify(d.before)} -> ${JSON.stringify(d.after)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return lines.join('\n') + '\n';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function runConfigRefresh(opts: ConfigRefreshOptions = {}): Promise<ConfigRefreshResult> {
|
|
142
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
143
|
+
const configPath = resolve(cwd, 'massu.config.yaml');
|
|
144
|
+
const log = opts.silent ? () => {} : (s: string) => process.stdout.write(s);
|
|
145
|
+
|
|
146
|
+
if (!existsSync(configPath)) {
|
|
147
|
+
const message = 'massu.config.yaml not found. Run: npx massu init';
|
|
148
|
+
if (!opts.silent) process.stderr.write(message + '\n');
|
|
149
|
+
return { exitCode: 1, applied: false, dryRun: !!opts.dryRun, diff: [], message };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let existing: AnyConfig;
|
|
153
|
+
try {
|
|
154
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
155
|
+
const parsed = parseYaml(content);
|
|
156
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
157
|
+
throw new Error('config is not a YAML object');
|
|
158
|
+
}
|
|
159
|
+
existing = parsed as AnyConfig;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const message = `Failed to parse massu.config.yaml: ${err instanceof Error ? err.message : String(err)}`;
|
|
162
|
+
if (!opts.silent) process.stderr.write(message + '\n');
|
|
163
|
+
return { exitCode: 2, applied: false, dryRun: !!opts.dryRun, diff: [], message };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const detection = await runDetection(cwd);
|
|
167
|
+
const refreshed = buildConfigFromDetection({
|
|
168
|
+
projectRoot: cwd,
|
|
169
|
+
detection,
|
|
170
|
+
projectName: typeof (existing.project as Record<string, unknown> | undefined)?.name === 'string'
|
|
171
|
+
? (existing.project as Record<string, unknown>).name as string
|
|
172
|
+
: undefined,
|
|
173
|
+
});
|
|
174
|
+
// buildConfigFromDetection already stamps detection.fingerprint. Double-check.
|
|
175
|
+
if (!(refreshed.detection as Record<string, unknown> | undefined)?.fingerprint) {
|
|
176
|
+
refreshed.detection = { fingerprint: computeFingerprint(detection) };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const merged = mergeRefresh(existing, refreshed);
|
|
180
|
+
const diff = computeDiff(existing, merged);
|
|
181
|
+
|
|
182
|
+
if (opts.dryRun) {
|
|
183
|
+
log('Config diff (dry-run; no changes written):\n');
|
|
184
|
+
log(renderDiff(diff));
|
|
185
|
+
return { exitCode: 0, applied: false, dryRun: true, diff };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (diff.length === 0) {
|
|
189
|
+
log('No changes needed — config is already up to date.\n');
|
|
190
|
+
return { exitCode: 0, applied: false, dryRun: false, diff };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Interactive prompt; fall back to dry-run semantics when not a TTY.
|
|
194
|
+
if (!process.stdin.isTTY) {
|
|
195
|
+
log('Config diff (non-interactive; pass --dry-run to suppress this note or run interactively to apply):\n');
|
|
196
|
+
log(renderDiff(diff));
|
|
197
|
+
return {
|
|
198
|
+
exitCode: 0,
|
|
199
|
+
applied: false,
|
|
200
|
+
dryRun: false,
|
|
201
|
+
diff,
|
|
202
|
+
message: 'non-interactive shell; no changes written',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
log('Config diff:\n');
|
|
207
|
+
log(renderDiff(diff));
|
|
208
|
+
const { confirm } = await import('@clack/prompts');
|
|
209
|
+
const apply = await confirm({ message: 'Apply these changes to massu.config.yaml?' });
|
|
210
|
+
if (apply !== true) {
|
|
211
|
+
log('Aborted; no changes written.\n');
|
|
212
|
+
return { exitCode: 0, applied: false, dryRun: false, diff, message: 'aborted by user' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const yamlContent = renderConfigYaml(merged);
|
|
216
|
+
const writeRes = writeConfigAtomic(configPath, yamlContent);
|
|
217
|
+
if (!writeRes.validated) {
|
|
218
|
+
const message = `Failed to write config: ${writeRes.error}`;
|
|
219
|
+
if (!opts.silent) process.stderr.write(message + '\n');
|
|
220
|
+
return { exitCode: 2, applied: false, dryRun: false, diff, message };
|
|
221
|
+
}
|
|
222
|
+
log('Config refreshed.\n');
|
|
223
|
+
return { exitCode: 0, applied: true, dryRun: false, diff };
|
|
224
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `massu config upgrade` — migrate a v1 `massu.config.yaml` to schema_version=2.
|
|
6
|
+
*
|
|
7
|
+
* Flags:
|
|
8
|
+
* --rollback Restore massu.config.yaml from massu.config.yaml.bak.
|
|
9
|
+
* --ci / --yes Non-interactive; no prompts; detector wins on conflicts.
|
|
10
|
+
*
|
|
11
|
+
* Safety:
|
|
12
|
+
* - Writes .bak of the original before overwriting.
|
|
13
|
+
* - Atomic write via writeConfigAtomic (tmp + rename).
|
|
14
|
+
* - Idempotent: running on a schema_version=2 config is a no-op.
|
|
15
|
+
*
|
|
16
|
+
* Exit codes:
|
|
17
|
+
* 0 success (migrated, rolled back, or already current)
|
|
18
|
+
* 1 config missing / rollback source missing
|
|
19
|
+
* 2 parse or write failure
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync } from 'fs';
|
|
23
|
+
import { resolve } from 'path';
|
|
24
|
+
import { parse as parseYaml } from 'yaml';
|
|
25
|
+
import { runDetection } from '../detect/index.ts';
|
|
26
|
+
import { computeFingerprint } from '../detect/drift.ts';
|
|
27
|
+
import { migrateV1ToV2, type AnyConfig } from '../detect/migrate.ts';
|
|
28
|
+
import { renderConfigYaml, writeConfigAtomic } from './init.ts';
|
|
29
|
+
|
|
30
|
+
export interface ConfigUpgradeOptions {
|
|
31
|
+
rollback?: boolean;
|
|
32
|
+
ci?: boolean;
|
|
33
|
+
cwd?: string;
|
|
34
|
+
silent?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ConfigUpgradeResult {
|
|
38
|
+
exitCode: 0 | 1 | 2;
|
|
39
|
+
action: 'migrated' | 'already-current' | 'rolled-back' | 'none';
|
|
40
|
+
message?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runConfigUpgrade(opts: ConfigUpgradeOptions = {}): Promise<ConfigUpgradeResult> {
|
|
44
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
45
|
+
const configPath = resolve(cwd, 'massu.config.yaml');
|
|
46
|
+
const bakPath = `${configPath}.bak`;
|
|
47
|
+
const log = opts.silent ? () => {} : (s: string) => process.stdout.write(s);
|
|
48
|
+
const err = opts.silent ? () => {} : (s: string) => process.stderr.write(s);
|
|
49
|
+
|
|
50
|
+
if (opts.rollback) {
|
|
51
|
+
if (!existsSync(bakPath)) {
|
|
52
|
+
const message = `No backup found at ${bakPath}`;
|
|
53
|
+
err(message + '\n');
|
|
54
|
+
return { exitCode: 1, action: 'none', message };
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
copyFileSync(bakPath, configPath);
|
|
58
|
+
unlinkSync(bakPath);
|
|
59
|
+
log('Config restored from backup.\n');
|
|
60
|
+
return { exitCode: 0, action: 'rolled-back' };
|
|
61
|
+
} catch (e) {
|
|
62
|
+
const message = `Rollback failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
63
|
+
err(message + '\n');
|
|
64
|
+
return { exitCode: 2, action: 'none', message };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!existsSync(configPath)) {
|
|
69
|
+
const message = 'massu.config.yaml not found. Run: npx massu init';
|
|
70
|
+
err(message + '\n');
|
|
71
|
+
return { exitCode: 1, action: 'none', message };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let existing: AnyConfig;
|
|
75
|
+
try {
|
|
76
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
77
|
+
const parsed = parseYaml(content);
|
|
78
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
79
|
+
throw new Error('config is not a YAML object');
|
|
80
|
+
}
|
|
81
|
+
existing = parsed as AnyConfig;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
const message = `Failed to parse massu.config.yaml: ${e instanceof Error ? e.message : String(e)}`;
|
|
84
|
+
err(message + '\n');
|
|
85
|
+
return { exitCode: 2, action: 'none', message };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const schemaVersion = existing.schema_version;
|
|
89
|
+
if (schemaVersion === 2) {
|
|
90
|
+
log('Config is already at schema_version=2; nothing to do.\n');
|
|
91
|
+
return { exitCode: 0, action: 'already-current' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const detection = await runDetection(cwd);
|
|
95
|
+
const v2 = migrateV1ToV2(existing, detection);
|
|
96
|
+
v2.detection = {
|
|
97
|
+
...(v2.detection as Record<string, unknown> | undefined ?? {}),
|
|
98
|
+
fingerprint: computeFingerprint(detection),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Back up original before any write.
|
|
102
|
+
try {
|
|
103
|
+
const original = readFileSync(configPath, 'utf-8');
|
|
104
|
+
writeFileSync(bakPath, original, 'utf-8');
|
|
105
|
+
} catch (e) {
|
|
106
|
+
const message = `Failed to write backup: ${e instanceof Error ? e.message : String(e)}`;
|
|
107
|
+
err(message + '\n');
|
|
108
|
+
return { exitCode: 2, action: 'none', message };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const yamlContent = renderConfigYaml(v2);
|
|
112
|
+
const writeRes = writeConfigAtomic(configPath, yamlContent);
|
|
113
|
+
if (!writeRes.validated) {
|
|
114
|
+
const message = `Failed to write upgraded config: ${writeRes.error}`;
|
|
115
|
+
err(message + '\n');
|
|
116
|
+
return { exitCode: 2, action: 'none', message };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Non-interactive mode just proceeds; interactive mode currently has no
|
|
120
|
+
// prompt on migrate (the migrator is deterministic and always user-preserving).
|
|
121
|
+
// --ci / --yes remain accepted for script-pipeline safety.
|
|
122
|
+
void opts.ci;
|
|
123
|
+
|
|
124
|
+
log(`Config upgraded to schema_version=2. Backup saved at ${bakPath}\n`);
|
|
125
|
+
return { exitCode: 0, action: 'migrated' };
|
|
126
|
+
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -8,14 +8,13 @@
|
|
|
8
8
|
* 1. massu.config.yaml exists and parses correctly
|
|
9
9
|
* 2. .mcp.json has massu entry
|
|
10
10
|
* 3. .claude/settings.local.json has hooks config
|
|
11
|
-
* 4. All
|
|
11
|
+
* 4. All 11 compiled hook files exist
|
|
12
12
|
* 5. Knowledge DB exists (.massu/memory.db)
|
|
13
13
|
* 6. Memory directory exists (~/.claude/projects/.../memory/)
|
|
14
14
|
* 7. Shell hooks wired in settings.local.json
|
|
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
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
20
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
@@ -374,32 +373,6 @@ function checkPythonHealth(projectRoot: string): CheckResult | null {
|
|
|
374
373
|
};
|
|
375
374
|
}
|
|
376
375
|
|
|
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
|
-
|
|
403
376
|
// ============================================================
|
|
404
377
|
// Main Doctor Flow
|
|
405
378
|
// ============================================================
|
|
@@ -424,7 +397,6 @@ export async function runDoctor(): Promise<void> {
|
|
|
424
397
|
checkNodeVersion(),
|
|
425
398
|
await checkGitRepo(projectRoot),
|
|
426
399
|
await checkLicenseStatus(),
|
|
427
|
-
checkClaudeMd(projectRoot),
|
|
428
400
|
];
|
|
429
401
|
|
|
430
402
|
// Add Python health check if configured
|