@massu/core 0.7.0 → 0.8.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/README.md +2 -2
- package/dist/cli.js +163 -19
- package/dist/hooks/auto-learning-pipeline.js +14 -2
- package/dist/hooks/classify-failure.js +1146 -0
- package/dist/hooks/cost-tracker.js +31 -0
- package/dist/hooks/fix-detector.js +12 -0
- package/dist/hooks/incident-pipeline.js +698 -10
- package/dist/hooks/post-edit-context.js +12 -0
- package/dist/hooks/post-tool-use.js +40 -4
- package/dist/hooks/pre-compact.js +31 -0
- package/dist/hooks/pre-delete-check.js +12 -0
- package/dist/hooks/quality-event.js +31 -0
- package/dist/hooks/rule-enforcement-pipeline.js +14 -1
- package/dist/hooks/session-end.js +31 -0
- package/dist/hooks/session-start.js +32 -1
- package/dist/hooks/user-prompt.js +63 -1
- package/package.json +1 -1
- package/reference/hook-execution-order.md +25 -17
- package/src/commands/doctor.ts +1 -1
- package/src/commands/init.ts +19 -16
- package/src/config.ts +12 -0
- package/src/hooks/auto-learning-pipeline.ts +3 -3
- package/src/hooks/classify-failure.ts +259 -0
- package/src/hooks/incident-pipeline.ts +42 -0
- package/src/hooks/rule-enforcement-pipeline.ts +2 -1
- package/src/hooks/session-start.ts +1 -1
- package/src/hooks/user-prompt.ts +21 -1
- package/src/license.ts +2 -1
- package/src/mcp-bridge-tools.ts +1 -2
- package/src/memory-db.ts +201 -0
- package/src/memory-file-ingest.ts +13 -6
package/README.md
CHANGED
|
@@ -14,8 +14,8 @@ This sets up the MCP server, configuration, and lifecycle hooks in one command.
|
|
|
14
14
|
|
|
15
15
|
Massu is a source-available [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that adds governance capabilities to AI coding assistants like Claude Code. It provides:
|
|
16
16
|
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
17
|
+
- **73 MCP Tools** — quality analytics, cost tracking, security scoring, dependency analysis, and more
|
|
18
|
+
- **15 Lifecycle Hooks** — pre-commit gates, security scanning, intent suggestion, session management, and auto-learning pipeline
|
|
19
19
|
- **3-Database Architecture** — code graph (read-only), data (imports/mappings), and memory (sessions/analytics)
|
|
20
20
|
- **Config-Driven** — all project-specific data lives in `massu.config.yaml`
|
|
21
21
|
|
package/dist/cli.js
CHANGED
|
@@ -268,6 +268,18 @@ var init_config = __esm({
|
|
|
268
268
|
"added_missing_import"
|
|
269
269
|
])
|
|
270
270
|
}).default({}),
|
|
271
|
+
failureClassification: z.object({
|
|
272
|
+
enabled: z.boolean().default(true),
|
|
273
|
+
thresholds: z.object({
|
|
274
|
+
known: z.number().default(5),
|
|
275
|
+
similar: z.number().default(3)
|
|
276
|
+
}).default({}),
|
|
277
|
+
scoring: z.object({
|
|
278
|
+
diffPatternWeight: z.number().default(3),
|
|
279
|
+
filePatternWeight: z.number().default(2),
|
|
280
|
+
promptKeywordWeight: z.number().default(2)
|
|
281
|
+
}).default({})
|
|
282
|
+
}).default({}),
|
|
271
283
|
pipeline: z.object({
|
|
272
284
|
requireIncidentReport: z.boolean().default(true),
|
|
273
285
|
requirePreventionRule: z.boolean().default(true),
|
|
@@ -375,10 +387,12 @@ var init_config = __esm({
|
|
|
375
387
|
var memory_db_exports = {};
|
|
376
388
|
__export(memory_db_exports, {
|
|
377
389
|
addConversationTurn: () => addConversationTurn,
|
|
390
|
+
addFailureClass: () => addFailureClass,
|
|
378
391
|
addObservation: () => addObservation,
|
|
379
392
|
addSummary: () => addSummary,
|
|
380
393
|
addToolCallDetail: () => addToolCallDetail,
|
|
381
394
|
addUserPrompt: () => addUserPrompt,
|
|
395
|
+
appendIncidentToFailureClass: () => appendIncidentToFailureClass,
|
|
382
396
|
assignImportance: () => assignImportance,
|
|
383
397
|
autoDetectTaskId: () => autoDetectTaskId,
|
|
384
398
|
createSession: () => createSession,
|
|
@@ -390,6 +404,7 @@ __export(memory_db_exports, {
|
|
|
390
404
|
getCrossTaskProgress: () => getCrossTaskProgress,
|
|
391
405
|
getDecisionsAbout: () => getDecisionsAbout,
|
|
392
406
|
getFailedAttempts: () => getFailedAttempts,
|
|
407
|
+
getFailureClasses: () => getFailureClasses,
|
|
393
408
|
getLastProcessedLine: () => getLastProcessedLine,
|
|
394
409
|
getMemoryDb: () => getMemoryDb,
|
|
395
410
|
getObservabilityDbSize: () => getObservabilityDbSize,
|
|
@@ -406,6 +421,7 @@ __export(memory_db_exports, {
|
|
|
406
421
|
pruneOldObservations: () => pruneOldObservations,
|
|
407
422
|
removePendingSync: () => removePendingSync,
|
|
408
423
|
sanitizeFts5Query: () => sanitizeFts5Query,
|
|
424
|
+
scoreFailureClasses: () => scoreFailureClasses,
|
|
409
425
|
searchConversationTurns: () => searchConversationTurns,
|
|
410
426
|
searchObservations: () => searchObservations,
|
|
411
427
|
setLastProcessedLine: () => setLastProcessedLine
|
|
@@ -905,6 +921,25 @@ function initMemorySchema(db) {
|
|
|
905
921
|
features TEXT DEFAULT '[]'
|
|
906
922
|
);
|
|
907
923
|
`);
|
|
924
|
+
db.exec(`
|
|
925
|
+
CREATE TABLE IF NOT EXISTS failure_classes (
|
|
926
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
927
|
+
name TEXT NOT NULL UNIQUE,
|
|
928
|
+
description TEXT NOT NULL,
|
|
929
|
+
diff_patterns TEXT NOT NULL DEFAULT '[]',
|
|
930
|
+
file_patterns TEXT NOT NULL DEFAULT '[]',
|
|
931
|
+
prompt_keywords TEXT NOT NULL DEFAULT '[]',
|
|
932
|
+
incidents TEXT NOT NULL DEFAULT '[]',
|
|
933
|
+
rules TEXT NOT NULL DEFAULT '[]',
|
|
934
|
+
scanner_checks TEXT NOT NULL DEFAULT '[]',
|
|
935
|
+
known_message TEXT NOT NULL DEFAULT '',
|
|
936
|
+
needs_review INTEGER NOT NULL DEFAULT 0,
|
|
937
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
938
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
939
|
+
);
|
|
940
|
+
CREATE INDEX IF NOT EXISTS idx_fc_name ON failure_classes(name);
|
|
941
|
+
CREATE INDEX IF NOT EXISTS idx_fc_needs_review ON failure_classes(needs_review);
|
|
942
|
+
`);
|
|
908
943
|
}
|
|
909
944
|
function enqueueSyncPayload(db, payload) {
|
|
910
945
|
db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
|
|
@@ -1338,6 +1373,108 @@ function getObservabilityDbSize(db) {
|
|
|
1338
1373
|
estimated_size_mb: Math.round(pageCount * pageSize / (1024 * 1024) * 100) / 100
|
|
1339
1374
|
};
|
|
1340
1375
|
}
|
|
1376
|
+
function addFailureClass(db, opts) {
|
|
1377
|
+
const result = db.prepare(`
|
|
1378
|
+
INSERT OR IGNORE INTO failure_classes (name, description, diff_patterns, file_patterns, prompt_keywords, incidents, rules, scanner_checks, known_message, needs_review)
|
|
1379
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1380
|
+
`).run(
|
|
1381
|
+
opts.name,
|
|
1382
|
+
opts.description,
|
|
1383
|
+
JSON.stringify(opts.diffPatterns ?? []),
|
|
1384
|
+
JSON.stringify(opts.filePatterns ?? []),
|
|
1385
|
+
JSON.stringify(opts.promptKeywords ?? []),
|
|
1386
|
+
JSON.stringify(opts.incidents ?? []),
|
|
1387
|
+
JSON.stringify(opts.rules ?? []),
|
|
1388
|
+
JSON.stringify(opts.scannerChecks ?? []),
|
|
1389
|
+
opts.knownMessage ?? "",
|
|
1390
|
+
opts.needsReview ? 1 : 0
|
|
1391
|
+
);
|
|
1392
|
+
return Number(result.lastInsertRowid);
|
|
1393
|
+
}
|
|
1394
|
+
function getFailureClasses(db) {
|
|
1395
|
+
const rows = db.prepare("SELECT * FROM failure_classes ORDER BY name").all();
|
|
1396
|
+
return rows.map((row) => ({
|
|
1397
|
+
id: row.id,
|
|
1398
|
+
name: row.name,
|
|
1399
|
+
description: row.description,
|
|
1400
|
+
diff_patterns: JSON.parse(row.diff_patterns || "[]"),
|
|
1401
|
+
file_patterns: JSON.parse(row.file_patterns || "[]"),
|
|
1402
|
+
prompt_keywords: JSON.parse(row.prompt_keywords || "[]"),
|
|
1403
|
+
incidents: JSON.parse(row.incidents || "[]"),
|
|
1404
|
+
rules: JSON.parse(row.rules || "[]"),
|
|
1405
|
+
scanner_checks: JSON.parse(row.scanner_checks || "[]"),
|
|
1406
|
+
known_message: row.known_message,
|
|
1407
|
+
needs_review: !!row.needs_review
|
|
1408
|
+
}));
|
|
1409
|
+
}
|
|
1410
|
+
function appendIncidentToFailureClass(db, className, incidentId) {
|
|
1411
|
+
const row = db.prepare("SELECT incidents FROM failure_classes WHERE name = ?").get(className);
|
|
1412
|
+
if (!row) return;
|
|
1413
|
+
const incidents = JSON.parse(row.incidents || "[]");
|
|
1414
|
+
if (!incidents.includes(incidentId)) {
|
|
1415
|
+
incidents.push(incidentId);
|
|
1416
|
+
db.prepare("UPDATE failure_classes SET incidents = ?, updated_at = datetime('now') WHERE name = ?").run(JSON.stringify(incidents), className);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
|
|
1420
|
+
const classes = getFailureClasses(db);
|
|
1421
|
+
if (classes.length === 0) return null;
|
|
1422
|
+
const diffWeight = weights?.diffPatternWeight ?? 3;
|
|
1423
|
+
const fileWeight = weights?.filePatternWeight ?? 2;
|
|
1424
|
+
const promptWeight = weights?.promptKeywordWeight ?? 2;
|
|
1425
|
+
let bestMatch = null;
|
|
1426
|
+
for (const fc of classes) {
|
|
1427
|
+
let score = 0;
|
|
1428
|
+
for (const pattern of fc.diff_patterns) {
|
|
1429
|
+
if (!pattern) continue;
|
|
1430
|
+
try {
|
|
1431
|
+
if (new RegExp(pattern, "i").test(matchText)) {
|
|
1432
|
+
score += diffWeight;
|
|
1433
|
+
}
|
|
1434
|
+
} catch {
|
|
1435
|
+
if (matchText.toLowerCase().includes(pattern.toLowerCase())) {
|
|
1436
|
+
score += diffWeight;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
for (const pattern of fc.file_patterns) {
|
|
1441
|
+
if (!pattern) continue;
|
|
1442
|
+
try {
|
|
1443
|
+
if (new RegExp(pattern).test(filePath)) {
|
|
1444
|
+
score += fileWeight;
|
|
1445
|
+
}
|
|
1446
|
+
} catch {
|
|
1447
|
+
if (filePath.includes(pattern)) {
|
|
1448
|
+
score += fileWeight;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
if (promptContext) {
|
|
1453
|
+
for (const keyword of fc.prompt_keywords) {
|
|
1454
|
+
if (!keyword) continue;
|
|
1455
|
+
try {
|
|
1456
|
+
if (new RegExp(keyword, "i").test(promptContext)) {
|
|
1457
|
+
score += promptWeight;
|
|
1458
|
+
}
|
|
1459
|
+
} catch {
|
|
1460
|
+
if (promptContext.toLowerCase().includes(keyword.toLowerCase())) {
|
|
1461
|
+
score += promptWeight;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
1467
|
+
bestMatch = {
|
|
1468
|
+
name: fc.name,
|
|
1469
|
+
score,
|
|
1470
|
+
incidentCount: fc.incidents.length,
|
|
1471
|
+
rules: fc.rules,
|
|
1472
|
+
knownMessage: fc.known_message
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return bestMatch;
|
|
1477
|
+
}
|
|
1341
1478
|
var init_memory_db = __esm({
|
|
1342
1479
|
"src/memory-db.ts"() {
|
|
1343
1480
|
"use strict";
|
|
@@ -1348,7 +1485,6 @@ var init_memory_db = __esm({
|
|
|
1348
1485
|
// src/memory-file-ingest.ts
|
|
1349
1486
|
import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync } from "fs";
|
|
1350
1487
|
import { join } from "path";
|
|
1351
|
-
import { parse as parseYaml2 } from "yaml";
|
|
1352
1488
|
function ingestMemoryFile(db, sessionId, filePath) {
|
|
1353
1489
|
if (!existsSync3(filePath)) return "skipped";
|
|
1354
1490
|
const content = readFileSync2(filePath, "utf-8");
|
|
@@ -1360,7 +1496,13 @@ function ingestMemoryFile(db, sessionId, filePath) {
|
|
|
1360
1496
|
let confidence;
|
|
1361
1497
|
if (frontmatterMatch) {
|
|
1362
1498
|
try {
|
|
1363
|
-
const fm =
|
|
1499
|
+
const fm = {};
|
|
1500
|
+
for (const line of frontmatterMatch[1].split("\n")) {
|
|
1501
|
+
const sep = line.indexOf(":");
|
|
1502
|
+
if (sep > 0) {
|
|
1503
|
+
fm[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1364
1506
|
name = fm.name ?? basename8;
|
|
1365
1507
|
description = fm.description ?? "";
|
|
1366
1508
|
type = fm.type ?? "discovery";
|
|
@@ -1787,19 +1929,11 @@ function registerMcpServer(projectRoot) {
|
|
|
1787
1929
|
return true;
|
|
1788
1930
|
}
|
|
1789
1931
|
function resolveHooksDir() {
|
|
1790
|
-
const cwd = process.cwd();
|
|
1791
|
-
const nodeModulesPath = resolve4(cwd, "node_modules/@massu/core/dist/hooks");
|
|
1792
|
-
if (existsSync5(nodeModulesPath)) {
|
|
1793
|
-
return "node_modules/@massu/core/dist/hooks";
|
|
1794
|
-
}
|
|
1795
|
-
const localPath = resolve4(__dirname2, "../dist/hooks");
|
|
1796
|
-
if (existsSync5(localPath)) {
|
|
1797
|
-
return localPath;
|
|
1798
|
-
}
|
|
1799
1932
|
return "node_modules/@massu/core/dist/hooks";
|
|
1800
1933
|
}
|
|
1801
1934
|
function hookCmd(hooksDir, hookFile) {
|
|
1802
|
-
|
|
1935
|
+
const hookPath = `${hooksDir}/${hookFile}`;
|
|
1936
|
+
return `d="$PWD"; while [ "$d" != "/" ] && [ ! -f "$d/${hookPath}" ]; do d="$(dirname "$d")"; done; cd "$d" && node ${hookPath}`;
|
|
1803
1937
|
}
|
|
1804
1938
|
function buildHooksConfig(hooksDir) {
|
|
1805
1939
|
return {
|
|
@@ -1835,14 +1969,24 @@ function buildHooksConfig(hooksDir) {
|
|
|
1835
1969
|
{
|
|
1836
1970
|
matcher: "Edit|Write",
|
|
1837
1971
|
hooks: [
|
|
1838
|
-
{ type: "command", command: hookCmd(hooksDir, "post-edit-context.js"), timeout: 5 }
|
|
1972
|
+
{ type: "command", command: hookCmd(hooksDir, "post-edit-context.js"), timeout: 5 },
|
|
1973
|
+
{ type: "command", command: hookCmd(hooksDir, "fix-detector.js"), timeout: 5 },
|
|
1974
|
+
{ type: "command", command: hookCmd(hooksDir, "classify-failure.js"), timeout: 5 }
|
|
1975
|
+
]
|
|
1976
|
+
},
|
|
1977
|
+
{
|
|
1978
|
+
matcher: "Write",
|
|
1979
|
+
hooks: [
|
|
1980
|
+
{ type: "command", command: hookCmd(hooksDir, "incident-pipeline.js"), timeout: 5 },
|
|
1981
|
+
{ type: "command", command: hookCmd(hooksDir, "rule-enforcement-pipeline.js"), timeout: 5 }
|
|
1839
1982
|
]
|
|
1840
1983
|
}
|
|
1841
1984
|
],
|
|
1842
1985
|
Stop: [
|
|
1843
1986
|
{
|
|
1844
1987
|
hooks: [
|
|
1845
|
-
{ type: "command", command: hookCmd(hooksDir, "session-end.js"), timeout: 15 }
|
|
1988
|
+
{ type: "command", command: hookCmd(hooksDir, "session-end.js"), timeout: 15 },
|
|
1989
|
+
{ type: "command", command: hookCmd(hooksDir, "auto-learning-pipeline.js"), timeout: 10 }
|
|
1846
1990
|
]
|
|
1847
1991
|
}
|
|
1848
1992
|
],
|
|
@@ -2222,7 +2366,7 @@ var init_license = __esm({
|
|
|
2222
2366
|
enterprise: 3
|
|
2223
2367
|
};
|
|
2224
2368
|
TOOL_TIER_MAP = {
|
|
2225
|
-
// --- Free tier (
|
|
2369
|
+
// --- Free tier (13 tools: core navigation + basic memory + regression + license) ---
|
|
2226
2370
|
sync: "free",
|
|
2227
2371
|
context: "free",
|
|
2228
2372
|
impact: "free",
|
|
@@ -2232,6 +2376,7 @@ var init_license = __esm({
|
|
|
2232
2376
|
coupling_check: "free",
|
|
2233
2377
|
memory_search: "free",
|
|
2234
2378
|
memory_ingest: "free",
|
|
2379
|
+
memory_backfill: "free",
|
|
2235
2380
|
regression_risk: "free",
|
|
2236
2381
|
feature_health: "free",
|
|
2237
2382
|
license_status: "free",
|
|
@@ -2327,7 +2472,7 @@ __export(doctor_exports, {
|
|
|
2327
2472
|
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4 } from "fs";
|
|
2328
2473
|
import { resolve as resolve5, dirname as dirname5 } from "path";
|
|
2329
2474
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2330
|
-
import { parse as
|
|
2475
|
+
import { parse as parseYaml2 } from "yaml";
|
|
2331
2476
|
function checkConfig(projectRoot) {
|
|
2332
2477
|
const configPath = resolve5(projectRoot, "massu.config.yaml");
|
|
2333
2478
|
if (!existsSync6(configPath)) {
|
|
@@ -2335,7 +2480,7 @@ function checkConfig(projectRoot) {
|
|
|
2335
2480
|
}
|
|
2336
2481
|
try {
|
|
2337
2482
|
const content = readFileSync5(configPath, "utf-8");
|
|
2338
|
-
const parsed =
|
|
2483
|
+
const parsed = parseYaml2(content);
|
|
2339
2484
|
if (!parsed || typeof parsed !== "object") {
|
|
2340
2485
|
return { name: "Configuration", status: "fail", detail: "massu.config.yaml is empty or invalid YAML" };
|
|
2341
2486
|
}
|
|
@@ -2653,7 +2798,7 @@ async function runValidateConfig() {
|
|
|
2653
2798
|
}
|
|
2654
2799
|
try {
|
|
2655
2800
|
const content = readFileSync5(configPath, "utf-8");
|
|
2656
|
-
const parsed =
|
|
2801
|
+
const parsed = parseYaml2(content);
|
|
2657
2802
|
if (!parsed || typeof parsed !== "object") {
|
|
2658
2803
|
console.error("Error: massu.config.yaml is empty or not a valid YAML object");
|
|
2659
2804
|
process.exit(1);
|
|
@@ -12093,7 +12238,6 @@ var init_mcp_bridge_tools = __esm({
|
|
|
12093
12238
|
});
|
|
12094
12239
|
process.on("SIGTERM", () => {
|
|
12095
12240
|
for (const [name] of connections) disconnectServer(name);
|
|
12096
|
-
process.exit(0);
|
|
12097
12241
|
});
|
|
12098
12242
|
ENV_ALLOW_LIST = /* @__PURE__ */ new Set([
|
|
12099
12243
|
"PATH",
|
|
@@ -157,6 +157,18 @@ var AutoLearningConfigSchema = z.object({
|
|
|
157
157
|
"added_missing_import"
|
|
158
158
|
])
|
|
159
159
|
}).default({}),
|
|
160
|
+
failureClassification: z.object({
|
|
161
|
+
enabled: z.boolean().default(true),
|
|
162
|
+
thresholds: z.object({
|
|
163
|
+
known: z.number().default(5),
|
|
164
|
+
similar: z.number().default(3)
|
|
165
|
+
}).default({}),
|
|
166
|
+
scoring: z.object({
|
|
167
|
+
diffPatternWeight: z.number().default(3),
|
|
168
|
+
filePatternWeight: z.number().default(2),
|
|
169
|
+
promptKeywordWeight: z.number().default(2)
|
|
170
|
+
}).default({})
|
|
171
|
+
}).default({}),
|
|
160
172
|
pipeline: z.object({
|
|
161
173
|
requireIncidentReport: z.boolean().default(true),
|
|
162
174
|
requirePreventionRule: z.boolean().default(true),
|
|
@@ -362,8 +374,8 @@ async function main() {
|
|
|
362
374
|
const diff = execSync("git diff --name-only", { cwd: root, timeout: 3e3, encoding: "utf-8" });
|
|
363
375
|
if (diff.trim()) {
|
|
364
376
|
const fullDiff = execSync("git diff", { cwd: root, timeout: 5e3, encoding: "utf-8" });
|
|
365
|
-
const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard
|
|
366
|
-
const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash
|
|
377
|
+
const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|throw|raise|assert|validate|if.*null|if.*nil|if.*None|if.*undefined)/gm) || []).length;
|
|
378
|
+
const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)/gm) || []).length;
|
|
367
379
|
if (fixPatterns > 3 || removedBroken > 1) {
|
|
368
380
|
uncommittedFix = true;
|
|
369
381
|
}
|