@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
|
@@ -21,7 +21,8 @@ var DomainConfigSchema = z.object({
|
|
|
21
21
|
});
|
|
22
22
|
var PatternRuleConfigSchema = z.object({
|
|
23
23
|
pattern: z.string().default("**"),
|
|
24
|
-
rules: z.array(z.string()).default([])
|
|
24
|
+
rules: z.array(z.string()).default([]),
|
|
25
|
+
language: z.string().optional()
|
|
25
26
|
});
|
|
26
27
|
var CostModelSchema = z.object({
|
|
27
28
|
input_per_million: z.number(),
|
|
@@ -233,17 +234,59 @@ var PathsConfigSchema = z.object({
|
|
|
233
234
|
components: z.string().optional(),
|
|
234
235
|
hooks: z.string().optional()
|
|
235
236
|
});
|
|
237
|
+
var LanguageFrameworkEntrySchema = z.object({
|
|
238
|
+
framework: z.string().optional(),
|
|
239
|
+
test_framework: z.string().optional(),
|
|
240
|
+
test: z.string().optional(),
|
|
241
|
+
runtime: z.string().optional(),
|
|
242
|
+
orm: z.string().optional(),
|
|
243
|
+
router: z.string().optional(),
|
|
244
|
+
ui: z.string().optional()
|
|
245
|
+
}).passthrough();
|
|
246
|
+
var FrameworkConfigSchema = z.object({
|
|
247
|
+
type: z.string().default("typescript"),
|
|
248
|
+
primary: z.string().optional(),
|
|
249
|
+
router: z.string().default("none"),
|
|
250
|
+
orm: z.string().default("none"),
|
|
251
|
+
ui: z.string().default("none"),
|
|
252
|
+
languages: z.record(z.string(), LanguageFrameworkEntrySchema).optional()
|
|
253
|
+
}).passthrough();
|
|
254
|
+
var VerificationEntrySchema = z.object({
|
|
255
|
+
type: z.string().optional(),
|
|
256
|
+
test: z.string().optional(),
|
|
257
|
+
syntax: z.string().optional(),
|
|
258
|
+
lint: z.string().optional(),
|
|
259
|
+
build: z.string().optional()
|
|
260
|
+
}).passthrough();
|
|
261
|
+
var VerificationConfigSchema = z.record(z.string(), VerificationEntrySchema).optional();
|
|
262
|
+
var CanonicalPathsSchema = z.record(z.string(), z.string()).optional();
|
|
263
|
+
var VerificationTypesSchema = z.record(z.string(), z.string()).optional();
|
|
264
|
+
var DetectionRuleEntrySchema = z.object({
|
|
265
|
+
signals: z.array(z.string()).default([]),
|
|
266
|
+
priority: z.number().optional()
|
|
267
|
+
}).passthrough();
|
|
268
|
+
var DetectionConfigSchema = z.object({
|
|
269
|
+
rules: z.record(
|
|
270
|
+
z.string(),
|
|
271
|
+
// language
|
|
272
|
+
z.record(z.string(), DetectionRuleEntrySchema)
|
|
273
|
+
// framework -> rule entry
|
|
274
|
+
).optional(),
|
|
275
|
+
signal_weights: z.record(z.string(), z.number()).optional(),
|
|
276
|
+
disable_builtin: z.boolean().optional()
|
|
277
|
+
}).passthrough().optional();
|
|
236
278
|
var RawConfigSchema = z.object({
|
|
279
|
+
schema_version: z.union([z.literal(1), z.literal(2)]).default(1),
|
|
237
280
|
project: z.object({
|
|
238
281
|
name: z.string().default("my-project"),
|
|
239
282
|
root: z.string().default("auto")
|
|
240
283
|
}).default({ name: "my-project", root: "auto" }),
|
|
241
|
-
framework:
|
|
242
|
-
type:
|
|
243
|
-
router:
|
|
244
|
-
orm:
|
|
245
|
-
ui:
|
|
246
|
-
})
|
|
284
|
+
framework: FrameworkConfigSchema.default({
|
|
285
|
+
type: "typescript",
|
|
286
|
+
router: "none",
|
|
287
|
+
orm: "none",
|
|
288
|
+
ui: "none"
|
|
289
|
+
}),
|
|
247
290
|
paths: PathsConfigSchema.default({ source: "src", aliases: { "@": "src" } }),
|
|
248
291
|
toolPrefix: z.string().default("massu"),
|
|
249
292
|
dbAccessPattern: z.string().optional(),
|
|
@@ -258,8 +301,13 @@ var RawConfigSchema = z.object({
|
|
|
258
301
|
regression: RegressionConfigSchema,
|
|
259
302
|
cloud: CloudConfigSchema,
|
|
260
303
|
conventions: ConventionsConfigSchema,
|
|
304
|
+
autoLearning: AutoLearningConfigSchema,
|
|
261
305
|
python: PythonConfigSchema,
|
|
262
|
-
|
|
306
|
+
// P2-004 / P2-005 / P2-006 / P2-008: v2 extensions (all optional)
|
|
307
|
+
verification: VerificationConfigSchema,
|
|
308
|
+
canonical_paths: CanonicalPathsSchema,
|
|
309
|
+
verification_types: VerificationTypesSchema,
|
|
310
|
+
detection: DetectionConfigSchema
|
|
263
311
|
}).passthrough();
|
|
264
312
|
var _config = null;
|
|
265
313
|
var _projectRoot = null;
|
|
@@ -303,14 +351,47 @@ function getConfig() {
|
|
|
303
351
|
const content = readFileSync(configPath, "utf-8");
|
|
304
352
|
rawYaml = parseYaml(content) ?? {};
|
|
305
353
|
}
|
|
306
|
-
const
|
|
354
|
+
const result = RawConfigSchema.safeParse(rawYaml);
|
|
355
|
+
if (!result.success) {
|
|
356
|
+
const issues = result.error.issues.map((i) => {
|
|
357
|
+
const path = i.path.length > 0 ? i.path.join(".") : "(root)";
|
|
358
|
+
const received = "received" in i && i.received !== void 0 ? ` (received ${JSON.stringify(i.received)})` : "";
|
|
359
|
+
return ` - ${path}: ${i.message}${received}`;
|
|
360
|
+
}).join("\n");
|
|
361
|
+
throw new Error(
|
|
362
|
+
`Invalid massu.config.yaml at ${configPath}:
|
|
363
|
+
${issues}
|
|
364
|
+
Hint: run \`massu config refresh\` to regenerate a valid config or fix the listed fields manually.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const parsed = result.data;
|
|
307
368
|
const projectRoot = parsed.project.root === "auto" || !parsed.project.root ? root : resolve(root, parsed.project.root);
|
|
369
|
+
const fw = parsed.framework;
|
|
370
|
+
let router = fw.router;
|
|
371
|
+
let orm = fw.orm;
|
|
372
|
+
let ui = fw.ui;
|
|
373
|
+
if (fw.type === "multi" && fw.primary && fw.languages) {
|
|
374
|
+
const primaryEntry = fw.languages[fw.primary];
|
|
375
|
+
if (primaryEntry) {
|
|
376
|
+
if (router === "none" && primaryEntry.router) router = primaryEntry.router;
|
|
377
|
+
if (orm === "none" && primaryEntry.orm) orm = primaryEntry.orm;
|
|
378
|
+
if (ui === "none" && primaryEntry.ui) ui = primaryEntry.ui;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
308
381
|
_config = {
|
|
382
|
+
schema_version: parsed.schema_version,
|
|
309
383
|
project: {
|
|
310
384
|
name: parsed.project.name,
|
|
311
385
|
root: projectRoot
|
|
312
386
|
},
|
|
313
|
-
framework:
|
|
387
|
+
framework: {
|
|
388
|
+
type: fw.type,
|
|
389
|
+
router,
|
|
390
|
+
orm,
|
|
391
|
+
ui,
|
|
392
|
+
primary: fw.primary,
|
|
393
|
+
languages: fw.languages
|
|
394
|
+
},
|
|
314
395
|
paths: parsed.paths,
|
|
315
396
|
toolPrefix: parsed.toolPrefix,
|
|
316
397
|
dbAccessPattern: parsed.dbAccessPattern,
|
|
@@ -325,7 +406,12 @@ function getConfig() {
|
|
|
325
406
|
regression: parsed.regression,
|
|
326
407
|
cloud: parsed.cloud,
|
|
327
408
|
conventions: parsed.conventions,
|
|
328
|
-
|
|
409
|
+
autoLearning: parsed.autoLearning,
|
|
410
|
+
python: parsed.python,
|
|
411
|
+
verification: parsed.verification,
|
|
412
|
+
canonical_paths: parsed.canonical_paths,
|
|
413
|
+
verification_types: parsed.verification_types,
|
|
414
|
+
detection: parsed.detection
|
|
329
415
|
};
|
|
330
416
|
if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
|
|
331
417
|
_config.cloud = {
|
|
@@ -952,9 +1038,7 @@ function linkSessionToTask(db, sessionId, taskId) {
|
|
|
952
1038
|
}
|
|
953
1039
|
|
|
954
1040
|
// src/hooks/user-prompt.ts
|
|
955
|
-
import { existsSync as existsSync3
|
|
956
|
-
import { tmpdir } from "os";
|
|
957
|
-
import { join } from "path";
|
|
1041
|
+
import { existsSync as existsSync3 } from "fs";
|
|
958
1042
|
async function main() {
|
|
959
1043
|
try {
|
|
960
1044
|
const input = await readStdin();
|
|
@@ -1027,35 +1111,6 @@ async function main() {
|
|
|
1027
1111
|
}
|
|
1028
1112
|
} catch (_memoryNagErr) {
|
|
1029
1113
|
}
|
|
1030
|
-
try {
|
|
1031
|
-
const failureKeywords = [
|
|
1032
|
-
"bug",
|
|
1033
|
-
"broken",
|
|
1034
|
-
"crash",
|
|
1035
|
-
"error",
|
|
1036
|
-
"fail",
|
|
1037
|
-
"fix",
|
|
1038
|
-
"wrong",
|
|
1039
|
-
"missing",
|
|
1040
|
-
"undefined",
|
|
1041
|
-
"null",
|
|
1042
|
-
"exception",
|
|
1043
|
-
"stack trace",
|
|
1044
|
-
"regression",
|
|
1045
|
-
"revert",
|
|
1046
|
-
"doesn't work",
|
|
1047
|
-
"not working",
|
|
1048
|
-
"stopped working",
|
|
1049
|
-
"broke"
|
|
1050
|
-
];
|
|
1051
|
-
const promptLower = prompt.toLowerCase();
|
|
1052
|
-
const matched = failureKeywords.filter((kw) => promptLower.includes(kw));
|
|
1053
|
-
if (matched.length > 0) {
|
|
1054
|
-
const contextFile = join(tmpdir(), `massu-failure-context-${session_id.slice(0, 8)}-${Date.now()}`);
|
|
1055
|
-
writeFileSync(contextFile, matched.join(" "), "utf-8");
|
|
1056
|
-
}
|
|
1057
|
-
} catch {
|
|
1058
|
-
}
|
|
1059
1114
|
} finally {
|
|
1060
1115
|
db.close();
|
|
1061
1116
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement,
|
|
5
|
+
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
|
|
6
6
|
"main": "src/server.ts",
|
|
7
7
|
"bin": {
|
|
8
8
|
"massu": "./dist/cli.js"
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"prepublishOnly": "bash ../../scripts/prepublish-check.sh && npm run build"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
+
"@clack/prompts": "^0.9.1",
|
|
19
20
|
"better-sqlite3": "^12.6.2",
|
|
21
|
+
"fast-glob": "^3.3.0",
|
|
22
|
+
"smol-toml": "^1.3.0",
|
|
20
23
|
"yaml": "^2.4.0",
|
|
21
24
|
"zod": "^3.23.0"
|
|
22
25
|
},
|
|
@@ -29,13 +32,20 @@
|
|
|
29
32
|
},
|
|
30
33
|
"files": [
|
|
31
34
|
"src/**/*",
|
|
32
|
-
"!src
|
|
35
|
+
"!src/**/__tests__/**",
|
|
36
|
+
"!src/**/*.test.ts",
|
|
37
|
+
"!src/**/*.spec.ts",
|
|
38
|
+
"!**/.build/**",
|
|
39
|
+
"!**/.swiftpm/**",
|
|
40
|
+
"!**/node_modules/**",
|
|
41
|
+
"!**/__pycache__/**",
|
|
33
42
|
"dist/**/*",
|
|
34
43
|
"commands/**/*",
|
|
35
44
|
"agents/**/*",
|
|
36
45
|
"patterns/**/*",
|
|
37
46
|
"protocols/**/*",
|
|
38
47
|
"reference/**/*",
|
|
48
|
+
"templates/**/*",
|
|
39
49
|
"LICENSE"
|
|
40
50
|
],
|
|
41
51
|
"keywords": [
|
|
@@ -52,9 +52,9 @@ Security blocking -> advisory warnings -> matcher-specific -> observability.
|
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
## PostToolUse (
|
|
55
|
+
## PostToolUse (11 hooks)
|
|
56
56
|
|
|
57
|
-
Security scan -> immediate feedback -> context tracking ->
|
|
57
|
+
Security scan -> immediate feedback -> context tracking -> incident capture -> memory sync -> observability.
|
|
58
58
|
|
|
59
59
|
| position: 1 | CI monitor | standard | Bash(git push) -- immediate push feedback |
|
|
60
60
|
|---|---|---|---|
|
|
@@ -62,23 +62,17 @@ Security scan -> immediate feedback -> context tracking -> fix detection -> inci
|
|
|
62
62
|
| position: 3 | `pattern-feedback.sh` | standard | Edit\|Write -- immediate pattern violation feedback |
|
|
63
63
|
| position: 4 | `post-edit-context.js` | strict | Edit\|Write -- detailed semantic analysis |
|
|
64
64
|
| position: 5 | `post-tool-use.js` | standard | Edit\|Write\|Bash -- structured context tracking |
|
|
65
|
-
| position: 6 | `
|
|
66
|
-
| position: 7 | `auto-ingest
|
|
67
|
-
| position: 8 | `
|
|
68
|
-
| position: 9 | `
|
|
69
|
-
| position: 10 | `
|
|
70
|
-
| position: 11 | `
|
|
71
|
-
| position: 12 | `pattern-scanner.sh --single-file` | strict | Edit\|Write -- per-file pattern scan |
|
|
72
|
-
| position: 13 | `mcp-usage-tracker.sh` | strict | MCP tools -- append-only MCP audit log |
|
|
73
|
-
| position: 14 | `compaction-advisor.sh` | standard | Bash\|Edit\|Write\|Read\|Grep\|Glob -- context tracking, widest matcher |
|
|
65
|
+
| position: 6 | `auto-ingest-incident.sh` | strict | Edit\|Write -- auto-capture incident patterns |
|
|
66
|
+
| position: 7 | `memory-auto-ingest.sh` | standard | Write -- auto-sync memory files to codegraph SQLite DB |
|
|
67
|
+
| position: 8 | `validate-deliverables.sh` | strict | Bash\|Edit\|Write -- deliverable validation |
|
|
68
|
+
| position: 9 | `pattern-scanner.sh --single-file` | strict | Edit\|Write -- per-file pattern scan |
|
|
69
|
+
| position: 10 | `mcp-usage-tracker.sh` | strict | MCP tools -- append-only MCP audit log |
|
|
70
|
+
| position: 11 | `compaction-advisor.sh` | standard | Bash\|Edit\|Write\|Read\|Grep\|Glob -- context tracking, widest matcher |
|
|
74
71
|
|
|
75
72
|
**Dependencies**:
|
|
76
73
|
- `output-secret-filter.sh` MUST run before any feedback hooks -- security first
|
|
77
74
|
- `pattern-feedback.sh` before `post-tool-use.js` -- immediate feedback before tracking
|
|
78
|
-
- `
|
|
79
|
-
- `incident-pipeline.js` after `auto-ingest-incident.sh` -- incident must be captured first
|
|
80
|
-
- `rule-enforcement-pipeline.js` after `incident-pipeline.js` -- rule derivation before enforcement
|
|
81
|
-
- `memory-auto-ingest.sh` runs after pipeline hooks -- memory sync is data-writing, before validation
|
|
75
|
+
- `memory-auto-ingest.sh` runs after incident capture -- memory sync is data-writing, before validation
|
|
82
76
|
- `compaction-advisor.sh` MUST be last -- widest matcher, just counts tool calls
|
|
83
77
|
|
|
84
78
|
---
|
|
@@ -111,23 +105,21 @@ Quick state capture -> full DB snapshot.
|
|
|
111
105
|
|
|
112
106
|
---
|
|
113
107
|
|
|
114
|
-
## Stop (
|
|
108
|
+
## Stop (7 hooks)
|
|
115
109
|
|
|
116
|
-
Session summary ->
|
|
110
|
+
Session summary -> warnings -> memory extraction -> review -> validation.
|
|
117
111
|
|
|
118
112
|
| position: 1 | `session-end.js` | standard | Write session summary to memory DB |
|
|
119
113
|
|---|---|---|---|
|
|
120
|
-
| position: 2 |
|
|
121
|
-
| position: 3 |
|
|
122
|
-
| position: 4 | `
|
|
123
|
-
| position: 5 | `
|
|
124
|
-
| position: 6 | `
|
|
125
|
-
| position: 7 | `
|
|
126
|
-
| position: 8 | `pattern-extractor.sh` | advisory | Extract new patterns from session |
|
|
114
|
+
| position: 2 | Uncommitted changes warning | standard (inline) | Alert user about unstaged work |
|
|
115
|
+
| position: 3 | `memory-auto-extract.sh` | standard | Auto-extract memories from DB observations |
|
|
116
|
+
| position: 4 | `auto-review-on-stop.sh` | strict | Automated code review of session changes |
|
|
117
|
+
| position: 5 | `surface-review-findings.sh` | strict | Display review findings to user |
|
|
118
|
+
| position: 6 | `validate-deliverables.sh` | strict | Final deliverable validation |
|
|
119
|
+
| position: 7 | `pattern-extractor.sh` | advisory | Extract new patterns from session |
|
|
127
120
|
|
|
128
121
|
**Dependencies**:
|
|
129
122
|
- `session-end.js` MUST be position 1 -- writes DB data that `memory-auto-extract.sh` reads
|
|
130
|
-
- `auto-learning-pipeline.js` MUST run early -- needs to output mandatory instructions before session ends
|
|
131
123
|
- `memory-auto-extract.sh` MUST come after `session-end.js` -- depends on DB observations
|
|
132
124
|
- `surface-review-findings.sh` MUST come after `auto-review-on-stop.sh` -- displays its output
|
|
133
125
|
- `pattern-extractor.sh` runs last -- advisory tier (skipped in minimal/standard profiles)
|
package/src/cli.ts
CHANGED
|
@@ -52,6 +52,10 @@ async function main(): Promise<void> {
|
|
|
52
52
|
await runValidateConfig();
|
|
53
53
|
break;
|
|
54
54
|
}
|
|
55
|
+
case 'config': {
|
|
56
|
+
await handleConfigSubcommand(args.slice(1));
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
55
59
|
case '--help':
|
|
56
60
|
case '-h': {
|
|
57
61
|
printHelp();
|
|
@@ -70,6 +74,56 @@ async function main(): Promise<void> {
|
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
|
|
77
|
+
async function handleConfigSubcommand(configArgs: string[]): Promise<void> {
|
|
78
|
+
const sub = configArgs[0];
|
|
79
|
+
const flags = new Set(configArgs.slice(1));
|
|
80
|
+
switch (sub) {
|
|
81
|
+
case 'refresh': {
|
|
82
|
+
const { runConfigRefresh } = await import('./commands/config-refresh.ts');
|
|
83
|
+
const result = await runConfigRefresh({ dryRun: flags.has('--dry-run') });
|
|
84
|
+
process.exit(result.exitCode);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
case 'validate': {
|
|
88
|
+
const { runValidateConfig } = await import('./commands/doctor.ts');
|
|
89
|
+
await runValidateConfig();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
case 'upgrade': {
|
|
93
|
+
const { runConfigUpgrade } = await import('./commands/config-upgrade.ts');
|
|
94
|
+
const result = await runConfigUpgrade({
|
|
95
|
+
rollback: flags.has('--rollback'),
|
|
96
|
+
ci: flags.has('--ci') || flags.has('--yes'),
|
|
97
|
+
});
|
|
98
|
+
process.exit(result.exitCode);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
case 'doctor': {
|
|
102
|
+
const { runDoctor } = await import('./commands/doctor.ts');
|
|
103
|
+
await runDoctor();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
case 'check-drift': {
|
|
107
|
+
const { runConfigCheckDrift } = await import('./commands/config-check-drift.ts');
|
|
108
|
+
const result = await runConfigCheckDrift({ verbose: flags.has('--verbose') });
|
|
109
|
+
process.exit(result.exitCode);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
case '--help':
|
|
113
|
+
case '-h':
|
|
114
|
+
case undefined: {
|
|
115
|
+
printConfigHelp();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
default: {
|
|
119
|
+
process.stderr.write(`massu: unknown config subcommand: ${sub}\n`);
|
|
120
|
+
printConfigHelp();
|
|
121
|
+
process.exit(1);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
73
127
|
function printHelp(): void {
|
|
74
128
|
console.log(`
|
|
75
129
|
Massu AI - Engineering Governance Platform
|
|
@@ -82,19 +136,44 @@ Commands:
|
|
|
82
136
|
doctor Check installation health
|
|
83
137
|
install-hooks Install/update Claude Code hooks
|
|
84
138
|
install-commands Install/update slash commands
|
|
85
|
-
validate-config Validate massu.config.yaml
|
|
139
|
+
validate-config Validate massu.config.yaml (alias: config validate)
|
|
140
|
+
config <sub> Config lifecycle: refresh | validate | upgrade | doctor | check-drift
|
|
86
141
|
|
|
87
142
|
Options:
|
|
88
143
|
--help, -h Show this help message
|
|
89
144
|
--version, -v Show version
|
|
90
145
|
|
|
91
146
|
Getting started:
|
|
92
|
-
npx massu init
|
|
147
|
+
npx massu init # Full setup in one command
|
|
148
|
+
npx massu init --help # Show all init options (--ci, --force, --template)
|
|
149
|
+
npx massu config --help # Show config subcommands
|
|
93
150
|
|
|
94
151
|
Documentation: https://massu.ai/docs
|
|
95
152
|
`);
|
|
96
153
|
}
|
|
97
154
|
|
|
155
|
+
function printConfigHelp(): void {
|
|
156
|
+
console.log(`
|
|
157
|
+
massu config <subcommand>
|
|
158
|
+
|
|
159
|
+
Subcommands:
|
|
160
|
+
refresh Re-run detection and apply changes to massu.config.yaml.
|
|
161
|
+
--dry-run Print diff and exit without writing.
|
|
162
|
+
validate Validate massu.config.yaml (alias of \`massu validate-config\`).
|
|
163
|
+
upgrade Migrate a v1 config to schema_version=2.
|
|
164
|
+
--rollback Restore from .bak file.
|
|
165
|
+
--ci, --yes Non-interactive mode (no prompts).
|
|
166
|
+
doctor Run the full health check (alias of \`massu doctor\`).
|
|
167
|
+
check-drift CI-safe drift gate; exits 1 on drift.
|
|
168
|
+
--verbose Print detailed changes to stdout.
|
|
169
|
+
|
|
170
|
+
Examples:
|
|
171
|
+
npx massu config refresh --dry-run
|
|
172
|
+
npx massu config upgrade --ci
|
|
173
|
+
npx massu config check-drift --verbose
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
176
|
+
|
|
98
177
|
function printVersion(): void {
|
|
99
178
|
try {
|
|
100
179
|
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `massu config check-drift` — CI-safe drift gate.
|
|
6
|
+
*
|
|
7
|
+
* Reads massu.config.yaml, runs detection, and compares the result with both
|
|
8
|
+
* the stored fingerprint (if present) and the structural detectDrift() check.
|
|
9
|
+
*
|
|
10
|
+
* Flags:
|
|
11
|
+
* --verbose Emit the full changes[] to stdout (field: before -> after).
|
|
12
|
+
*
|
|
13
|
+
* Exit codes:
|
|
14
|
+
* 0 no drift
|
|
15
|
+
* 1 drift detected
|
|
16
|
+
* 2 config missing or unparseable
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync } from 'fs';
|
|
20
|
+
import { resolve } from 'path';
|
|
21
|
+
import { parse as parseYaml } from 'yaml';
|
|
22
|
+
import { runDetection } from '../detect/index.ts';
|
|
23
|
+
import { computeFingerprint, detectDrift, type DriftChange } from '../detect/drift.ts';
|
|
24
|
+
import type { AnyConfig } from '../detect/migrate.ts';
|
|
25
|
+
|
|
26
|
+
export interface ConfigCheckDriftOptions {
|
|
27
|
+
verbose?: boolean;
|
|
28
|
+
cwd?: string;
|
|
29
|
+
silent?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ConfigCheckDriftResult {
|
|
33
|
+
exitCode: 0 | 1 | 2;
|
|
34
|
+
drifted: boolean;
|
|
35
|
+
changes: DriftChange[];
|
|
36
|
+
storedFingerprint: string | null;
|
|
37
|
+
currentFingerprint: string | null;
|
|
38
|
+
message?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderChanges(changes: DriftChange[]): string {
|
|
42
|
+
if (changes.length === 0) return '(none)\n';
|
|
43
|
+
return changes
|
|
44
|
+
.map((c) => ` ${c.field}: ${JSON.stringify(c.before)} -> ${JSON.stringify(c.after)}`)
|
|
45
|
+
.join('\n') + '\n';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runConfigCheckDrift(
|
|
49
|
+
opts: ConfigCheckDriftOptions = {}
|
|
50
|
+
): Promise<ConfigCheckDriftResult> {
|
|
51
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
52
|
+
const configPath = resolve(cwd, 'massu.config.yaml');
|
|
53
|
+
const log = opts.silent ? () => {} : (s: string) => process.stdout.write(s);
|
|
54
|
+
const err = opts.silent ? () => {} : (s: string) => process.stderr.write(s);
|
|
55
|
+
|
|
56
|
+
if (!existsSync(configPath)) {
|
|
57
|
+
const message = 'massu.config.yaml not found. Run: npx massu init';
|
|
58
|
+
err(message + '\n');
|
|
59
|
+
return {
|
|
60
|
+
exitCode: 2,
|
|
61
|
+
drifted: false,
|
|
62
|
+
changes: [],
|
|
63
|
+
storedFingerprint: null,
|
|
64
|
+
currentFingerprint: null,
|
|
65
|
+
message,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let config: AnyConfig;
|
|
70
|
+
try {
|
|
71
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
72
|
+
const parsed = parseYaml(content);
|
|
73
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
74
|
+
throw new Error('config is not a YAML object');
|
|
75
|
+
}
|
|
76
|
+
config = parsed as AnyConfig;
|
|
77
|
+
} catch (e) {
|
|
78
|
+
const message = `Failed to parse massu.config.yaml: ${e instanceof Error ? e.message : String(e)}`;
|
|
79
|
+
err(message + '\n');
|
|
80
|
+
return {
|
|
81
|
+
exitCode: 2,
|
|
82
|
+
drifted: false,
|
|
83
|
+
changes: [],
|
|
84
|
+
storedFingerprint: null,
|
|
85
|
+
currentFingerprint: null,
|
|
86
|
+
message,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const detection = await runDetection(cwd);
|
|
91
|
+
const currentFp = computeFingerprint(detection);
|
|
92
|
+
const storedFp =
|
|
93
|
+
typeof (config.detection as Record<string, unknown> | undefined)?.fingerprint === 'string'
|
|
94
|
+
? ((config.detection as Record<string, unknown>).fingerprint as string)
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
const report = detectDrift(config, detection);
|
|
98
|
+
const fingerprintDrift = storedFp !== null && storedFp !== currentFp;
|
|
99
|
+
const drifted = report.drifted || fingerprintDrift;
|
|
100
|
+
|
|
101
|
+
if (!drifted) {
|
|
102
|
+
log('No drift detected.\n');
|
|
103
|
+
if (opts.verbose) {
|
|
104
|
+
log(`Fingerprint: ${currentFp}\n`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
exitCode: 0,
|
|
108
|
+
drifted: false,
|
|
109
|
+
changes: report.changes,
|
|
110
|
+
storedFingerprint: storedFp,
|
|
111
|
+
currentFingerprint: currentFp,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
err('Config drift detected; run `npx massu config refresh` to update.\n');
|
|
116
|
+
if (opts.verbose) {
|
|
117
|
+
if (storedFp !== null) {
|
|
118
|
+
log(`Fingerprint: ${storedFp.slice(0, 16)} -> ${currentFp.slice(0, 16)}\n`);
|
|
119
|
+
} else {
|
|
120
|
+
log(`Fingerprint (new): ${currentFp.slice(0, 16)}\n`);
|
|
121
|
+
}
|
|
122
|
+
log('Changes:\n');
|
|
123
|
+
log(renderChanges(report.changes));
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
exitCode: 1,
|
|
127
|
+
drifted: true,
|
|
128
|
+
changes: report.changes,
|
|
129
|
+
storedFingerprint: storedFp,
|
|
130
|
+
currentFingerprint: currentFp,
|
|
131
|
+
};
|
|
132
|
+
}
|