@massu/core 2.5.1 → 2.5.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/CHANGELOG.md +170 -0
- package/dist/cli.js +578 -212
- package/dist/hooks/auto-learning-pipeline.js +244 -47
- package/dist/hooks/classify-failure.js +272 -18
- package/dist/hooks/cost-tracker.js +24 -1
- package/dist/hooks/fix-detector.js +40 -1
- package/dist/hooks/incident-pipeline.js +24 -1
- package/dist/hooks/intent-suggester.js +24 -1
- package/dist/hooks/memory-recall.js +24 -1
- package/dist/hooks/post-edit-context.js +72 -2
- package/dist/hooks/post-tool-use.js +25 -1
- package/dist/hooks/pre-compact.js +24 -1
- package/dist/hooks/pre-delete-check.js +72 -4
- package/dist/hooks/pre-tool-use-gate.js +78 -6
- package/dist/hooks/quality-event.js +24 -1
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +22 -1
- package/dist/hooks/session-end.js +24 -1
- package/dist/hooks/session-start.js +24 -1
- package/dist/hooks/user-prompt.js +24 -1
- package/package.json +1 -1
|
@@ -697,6 +697,13 @@ var init_config = __esm({
|
|
|
697
697
|
});
|
|
698
698
|
LanguageFrameworkEntrySchema = z2.object({
|
|
699
699
|
framework: z2.string().optional(),
|
|
700
|
+
// `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
|
|
701
|
+
// by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
|
|
702
|
+
// `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
|
|
703
|
+
// derives the index builders' candidate set from it, so it is typed here rather than
|
|
704
|
+
// read as an unknown: a consumer that has to shape-check its own SoT is one branch away
|
|
705
|
+
// from silently contributing `undefined` to a path predicate.
|
|
706
|
+
source_dirs: z2.array(z2.string()).optional(),
|
|
700
707
|
test_framework: z2.string().optional(),
|
|
701
708
|
test: z2.string().optional(),
|
|
702
709
|
runtime: z2.string().optional(),
|
|
@@ -3988,7 +3995,7 @@ var init_hook_failure_signal = __esm({
|
|
|
3988
3995
|
// src/hooks/auto-learning-pipeline.ts
|
|
3989
3996
|
init_config();
|
|
3990
3997
|
import { execFileSync } from "child_process";
|
|
3991
|
-
import { existsSync as
|
|
3998
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, unlinkSync as unlinkSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3992
3999
|
import { tmpdir as tmpdir2 } from "os";
|
|
3993
4000
|
import { join as join6 } from "path";
|
|
3994
4001
|
|
|
@@ -4029,17 +4036,215 @@ function writeHookContext(hookEventName, message) {
|
|
|
4029
4036
|
|
|
4030
4037
|
// src/hooks/auto-learning-pipeline.ts
|
|
4031
4038
|
init_hook_failure_signal();
|
|
4039
|
+
|
|
4040
|
+
// src/hooks/lib/is-direct-invocation.ts
|
|
4041
|
+
import { pathToFileURL } from "url";
|
|
4042
|
+
function isDirectInvocation(moduleUrl) {
|
|
4043
|
+
const entry = process.argv[1];
|
|
4044
|
+
if (!entry) return false;
|
|
4045
|
+
try {
|
|
4046
|
+
return moduleUrl === pathToFileURL(entry).href;
|
|
4047
|
+
} catch {
|
|
4048
|
+
return false;
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
// src/hooks/lib/session-touched-files.ts
|
|
4053
|
+
import { readFileSync as readFileSync6, existsSync as existsSync9, realpathSync } from "fs";
|
|
4054
|
+
import { relative, isAbsolute, resolve as resolve5 } from "path";
|
|
4055
|
+
var FILE_WRITING_TOOLS = Object.freeze({
|
|
4056
|
+
Edit: ["file_path"],
|
|
4057
|
+
Write: ["file_path"],
|
|
4058
|
+
MultiEdit: ["file_path"],
|
|
4059
|
+
NotebookEdit: ["notebook_path", "file_path"]
|
|
4060
|
+
});
|
|
4061
|
+
function canonical(p) {
|
|
4062
|
+
try {
|
|
4063
|
+
return realpathSync(p);
|
|
4064
|
+
} catch {
|
|
4065
|
+
return p;
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
function toRepoRelative(root, filePath) {
|
|
4069
|
+
if (typeof filePath !== "string" || filePath === "") return null;
|
|
4070
|
+
const absRaw = isAbsolute(filePath) ? filePath : resolve5(root, filePath);
|
|
4071
|
+
const canonicalRoot = canonical(root);
|
|
4072
|
+
const abs = resolve5(canonical(resolve5(absRaw, "..")), absRaw.split("/").pop() ?? "");
|
|
4073
|
+
const rel = relative(canonicalRoot, abs).split("\\").join("/");
|
|
4074
|
+
if (rel === "" || rel.startsWith("../")) return null;
|
|
4075
|
+
return rel;
|
|
4076
|
+
}
|
|
4077
|
+
function transcriptTouchedFiles(root, transcriptPath) {
|
|
4078
|
+
let raw;
|
|
4079
|
+
try {
|
|
4080
|
+
raw = readFileSync6(transcriptPath, "utf-8");
|
|
4081
|
+
} catch {
|
|
4082
|
+
return null;
|
|
4083
|
+
}
|
|
4084
|
+
const out = /* @__PURE__ */ new Set();
|
|
4085
|
+
for (const line of raw.split("\n")) {
|
|
4086
|
+
if (!line.trim()) continue;
|
|
4087
|
+
let entry;
|
|
4088
|
+
try {
|
|
4089
|
+
entry = JSON.parse(line);
|
|
4090
|
+
} catch {
|
|
4091
|
+
continue;
|
|
4092
|
+
}
|
|
4093
|
+
const message = entry.message;
|
|
4094
|
+
const content = message?.content;
|
|
4095
|
+
if (!Array.isArray(content)) continue;
|
|
4096
|
+
for (const block of content) {
|
|
4097
|
+
if (!block || typeof block !== "object") continue;
|
|
4098
|
+
const b = block;
|
|
4099
|
+
if (b.type !== "tool_use") continue;
|
|
4100
|
+
const keys = b.name ? FILE_WRITING_TOOLS[b.name] : void 0;
|
|
4101
|
+
if (!keys || !b.input) continue;
|
|
4102
|
+
for (const key of keys) {
|
|
4103
|
+
const rel = toRepoRelative(root, b.input[key]);
|
|
4104
|
+
if (rel) out.add(rel);
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
return out;
|
|
4109
|
+
}
|
|
4110
|
+
function flagFileTouchedFiles(root, flagPath) {
|
|
4111
|
+
if (!existsSync9(flagPath)) return null;
|
|
4112
|
+
let raw;
|
|
4113
|
+
try {
|
|
4114
|
+
raw = readFileSync6(flagPath, "utf-8");
|
|
4115
|
+
} catch {
|
|
4116
|
+
return null;
|
|
4117
|
+
}
|
|
4118
|
+
const out = /* @__PURE__ */ new Set();
|
|
4119
|
+
for (const line of raw.split("\n")) {
|
|
4120
|
+
if (!line.trim()) continue;
|
|
4121
|
+
try {
|
|
4122
|
+
const rel = toRepoRelative(root, JSON.parse(line).file ?? "");
|
|
4123
|
+
if (rel) out.add(rel);
|
|
4124
|
+
} catch {
|
|
4125
|
+
continue;
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
return out;
|
|
4129
|
+
}
|
|
4130
|
+
function sessionTouchedFiles(opts) {
|
|
4131
|
+
const sources = [];
|
|
4132
|
+
if (opts.transcriptPath) sources.push(transcriptTouchedFiles(opts.root, opts.transcriptPath));
|
|
4133
|
+
if (opts.flagPath) sources.push(flagFileTouchedFiles(opts.root, opts.flagPath));
|
|
4134
|
+
const readable = sources.filter((s) => s !== null);
|
|
4135
|
+
if (readable.length === 0) return null;
|
|
4136
|
+
const union = /* @__PURE__ */ new Set();
|
|
4137
|
+
for (const s of readable) for (const f of s) union.add(f);
|
|
4138
|
+
return union;
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
// src/hooks/lib/git-safe-env.ts
|
|
4142
|
+
var GIT_ENV_LEAKS = [
|
|
4143
|
+
"GIT_DIR",
|
|
4144
|
+
"GIT_WORK_TREE",
|
|
4145
|
+
"GIT_INDEX_FILE",
|
|
4146
|
+
"GIT_OBJECT_DIRECTORY",
|
|
4147
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
4148
|
+
"GIT_COMMON_DIR",
|
|
4149
|
+
"GIT_PREFIX"
|
|
4150
|
+
];
|
|
4151
|
+
function gitSafeEnv(extra = {}, source = process.env) {
|
|
4152
|
+
const e = { ...source, ...extra };
|
|
4153
|
+
for (const k of GIT_ENV_LEAKS) delete e[k];
|
|
4154
|
+
return e;
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
// src/hooks/auto-learning-pipeline.ts
|
|
4032
4158
|
var HOOK_EVENT = "Stop";
|
|
4033
4159
|
var MAX_FULL_DIFF_BYTES = 2 * 1024 * 1024;
|
|
4160
|
+
var DOC_EXCLUDE_PATHSPEC = [
|
|
4161
|
+
":(exclude)*.md",
|
|
4162
|
+
":(exclude)*.markdown",
|
|
4163
|
+
":(exclude)*.mdx",
|
|
4164
|
+
":(exclude)*.rst",
|
|
4165
|
+
":(exclude)*.adoc",
|
|
4166
|
+
":(exclude)*.txt",
|
|
4167
|
+
":(exclude)docs/**",
|
|
4168
|
+
// massu's OWN runtime artifacts: agent-result JSONL, workflow logs, rule candidates.
|
|
4169
|
+
// Audit findings quote source lines verbatim, so leaving these in scope re-creates the
|
|
4170
|
+
// prose-as-code false positive with the hook's own output as the input.
|
|
4171
|
+
":(exclude).massu/**"
|
|
4172
|
+
];
|
|
4173
|
+
var CODE_ONLY_PATHSPEC = ["--", ".", ...DOC_EXCLUDE_PATHSPEC];
|
|
4174
|
+
var MAX_SESSION_PATHSPEC_FILES = 500;
|
|
4175
|
+
var FIX_PATTERN_RE = /^\+.*(?:\b(?:try|except|catch|guard|throw|raise|assert|validate)\b|\bif\b.*\b(?:null|nil|None|undefined)\b)/gm;
|
|
4176
|
+
var REMOVED_BROKEN_RE = /^-.*\b(?:bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)\b/gm;
|
|
4034
4177
|
function getSessionFlagPath(sessionId) {
|
|
4035
4178
|
return join6(tmpdir2(), "massu-auto-learning", `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
4036
4179
|
}
|
|
4180
|
+
var RECENT_INCIDENT_WINDOW_MS = 12 * 60 * 60 * 1e3;
|
|
4181
|
+
function recentIncidentFiles(dir, nowMs = Date.now()) {
|
|
4182
|
+
try {
|
|
4183
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md")).filter((f) => {
|
|
4184
|
+
try {
|
|
4185
|
+
return nowMs - statSync2(join6(dir, f)).mtimeMs <= RECENT_INCIDENT_WINDOW_MS;
|
|
4186
|
+
} catch {
|
|
4187
|
+
return false;
|
|
4188
|
+
}
|
|
4189
|
+
}).sort();
|
|
4190
|
+
} catch {
|
|
4191
|
+
return [];
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
function scanUncommittedForFix(opts) {
|
|
4195
|
+
const git = opts.git ?? ((args, t, m) => execFileSync("git", [...args], {
|
|
4196
|
+
cwd: opts.root,
|
|
4197
|
+
// `cwd` does NOT scope git: GIT_DIR in the environment outranks it, and a leaked one
|
|
4198
|
+
// would make this read a different repository while looking correct. This hook runs
|
|
4199
|
+
// at Stop and is never reachable from a git hook, so nothing here depends on the
|
|
4200
|
+
// index a commit-time hook is handed — the variables are removed rather than asserted.
|
|
4201
|
+
env: gitSafeEnv(),
|
|
4202
|
+
timeout: t,
|
|
4203
|
+
encoding: "utf-8",
|
|
4204
|
+
maxBuffer: m
|
|
4205
|
+
}));
|
|
4206
|
+
if (opts.sessionTouched === null) {
|
|
4207
|
+
return { uncommittedFix: false, reason: "unattributable", filesScanned: 0 };
|
|
4208
|
+
}
|
|
4209
|
+
if (opts.sessionTouched.size === 0) {
|
|
4210
|
+
return { uncommittedFix: false, reason: "not-this-session", filesScanned: 0 };
|
|
4211
|
+
}
|
|
4212
|
+
try {
|
|
4213
|
+
const nameOnly = git(["diff", "--name-only", ...CODE_ONLY_PATHSPEC], 3e3, 1024 * 1024);
|
|
4214
|
+
const changed = nameOnly.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
4215
|
+
if (changed.length === 0) {
|
|
4216
|
+
return { uncommittedFix: false, reason: "no-changes", filesScanned: 0 };
|
|
4217
|
+
}
|
|
4218
|
+
const mine = changed.filter((f) => opts.sessionTouched.has(f));
|
|
4219
|
+
if (mine.length === 0) {
|
|
4220
|
+
return { uncommittedFix: false, reason: "not-this-session", filesScanned: 0 };
|
|
4221
|
+
}
|
|
4222
|
+
const scoped = mine.length <= MAX_SESSION_PATHSPEC_FILES ? ["--", ...mine, ...DOC_EXCLUDE_PATHSPEC] : [...CODE_ONLY_PATHSPEC];
|
|
4223
|
+
const shortstat = git(["diff", "--shortstat", ...scoped], 2e3, 64 * 1024);
|
|
4224
|
+
const insertions = parseInt(shortstat.match(/(\d+) insertion/)?.[1] ?? "0", 10);
|
|
4225
|
+
const deletions = parseInt(shortstat.match(/(\d+) deletion/)?.[1] ?? "0", 10);
|
|
4226
|
+
if ((insertions + deletions) * 80 > MAX_FULL_DIFF_BYTES) {
|
|
4227
|
+
return { uncommittedFix: false, reason: "over-cap", filesScanned: mine.length };
|
|
4228
|
+
}
|
|
4229
|
+
const fullDiff = git(["diff", ...scoped], 5e3, MAX_FULL_DIFF_BYTES);
|
|
4230
|
+
const fixPatterns = (fullDiff.match(FIX_PATTERN_RE) || []).length;
|
|
4231
|
+
const removedBroken = (fullDiff.match(REMOVED_BROKEN_RE) || []).length;
|
|
4232
|
+
const hit = fixPatterns > 3 || removedBroken > 1;
|
|
4233
|
+
return {
|
|
4234
|
+
uncommittedFix: hit,
|
|
4235
|
+
reason: hit ? "fix-detected" : "no-signal",
|
|
4236
|
+
filesScanned: mine.length
|
|
4237
|
+
};
|
|
4238
|
+
} catch {
|
|
4239
|
+
return { uncommittedFix: false, reason: "git-unavailable", filesScanned: 0 };
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4037
4242
|
async function main() {
|
|
4038
4243
|
try {
|
|
4039
4244
|
const input = await readStdin();
|
|
4040
4245
|
const hookInput = JSON.parse(input);
|
|
4041
4246
|
const config = getConfig();
|
|
4042
|
-
if (config.autoLearning?.enabled === false) {
|
|
4247
|
+
if (config.autoLearning?.enabled === false || config.autoLearning?.fixDetection?.enabled === false) {
|
|
4043
4248
|
process.exit(0);
|
|
4044
4249
|
return;
|
|
4045
4250
|
}
|
|
@@ -4049,46 +4254,20 @@ async function main() {
|
|
|
4049
4254
|
const autoLearn = config.autoLearning;
|
|
4050
4255
|
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
4051
4256
|
let sessionFixes = [];
|
|
4052
|
-
if (
|
|
4257
|
+
if (existsSync10(flagPath)) {
|
|
4053
4258
|
try {
|
|
4054
|
-
sessionFixes =
|
|
4259
|
+
sessionFixes = readFileSync7(flagPath, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
4055
4260
|
} catch {
|
|
4056
4261
|
}
|
|
4057
4262
|
}
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
if (nameOnly.trim()) {
|
|
4067
|
-
const shortstat = execFileSync("git", ["diff", "--shortstat"], {
|
|
4068
|
-
cwd: root,
|
|
4069
|
-
timeout: 2e3,
|
|
4070
|
-
encoding: "utf-8",
|
|
4071
|
-
maxBuffer: 64 * 1024
|
|
4072
|
-
});
|
|
4073
|
-
const insertions = parseInt(shortstat.match(/(\d+) insertion/)?.[1] ?? "0", 10);
|
|
4074
|
-
const deletions = parseInt(shortstat.match(/(\d+) deletion/)?.[1] ?? "0", 10);
|
|
4075
|
-
const estimatedBytes = (insertions + deletions) * 80;
|
|
4076
|
-
if (estimatedBytes <= MAX_FULL_DIFF_BYTES) {
|
|
4077
|
-
const fullDiff = execFileSync("git", ["diff"], {
|
|
4078
|
-
cwd: root,
|
|
4079
|
-
timeout: 5e3,
|
|
4080
|
-
encoding: "utf-8",
|
|
4081
|
-
maxBuffer: MAX_FULL_DIFF_BYTES
|
|
4082
|
-
});
|
|
4083
|
-
const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|throw|raise|assert|validate|if.*null|if.*nil|if.*None|if.*undefined)/gm) || []).length;
|
|
4084
|
-
const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)/gm) || []).length;
|
|
4085
|
-
if (fixPatterns > 3 || removedBroken > 1) {
|
|
4086
|
-
uncommittedFix = true;
|
|
4087
|
-
}
|
|
4088
|
-
}
|
|
4089
|
-
}
|
|
4090
|
-
} catch {
|
|
4091
|
-
}
|
|
4263
|
+
const uncommittedFix = scanUncommittedForFix({
|
|
4264
|
+
root,
|
|
4265
|
+
sessionTouched: sessionTouchedFiles({
|
|
4266
|
+
root,
|
|
4267
|
+
transcriptPath: hookInput.transcript_path,
|
|
4268
|
+
flagPath
|
|
4269
|
+
})
|
|
4270
|
+
}).uncommittedFix;
|
|
4092
4271
|
if (sessionFixes.length === 0 && !uncommittedFix) {
|
|
4093
4272
|
cleanup(flagPath);
|
|
4094
4273
|
process.exit(0);
|
|
@@ -4117,10 +4296,18 @@ async function main() {
|
|
|
4117
4296
|
lines.push("");
|
|
4118
4297
|
lines.push(" Additional uncommitted fix patterns detected in git diff.");
|
|
4119
4298
|
}
|
|
4299
|
+
const todaysIncidents = recentIncidentFiles(join6(root, incidentDir));
|
|
4120
4300
|
lines.push("");
|
|
4121
|
-
|
|
4301
|
+
if (todaysIncidents.length > 0) {
|
|
4302
|
+
lines.push(" ALREADY SATISFIED TODAY (verify these cover the fixes above):");
|
|
4303
|
+
for (const f of todaysIncidents.slice(0, 5)) lines.push(` - ${incidentDir}/${f}`);
|
|
4304
|
+
lines.push("");
|
|
4305
|
+
lines.push(" If they do, nothing further is required. Otherwise:");
|
|
4306
|
+
} else {
|
|
4307
|
+
lines.push(" Complete these steps before this session ends:");
|
|
4308
|
+
}
|
|
4122
4309
|
lines.push("");
|
|
4123
|
-
if (autoLearn?.pipeline?.requireIncidentReport !== false) {
|
|
4310
|
+
if (autoLearn?.pipeline?.requireIncidentReport !== false && todaysIncidents.length === 0) {
|
|
4124
4311
|
lines.push(" STEP 1: INCIDENT REPORT");
|
|
4125
4312
|
lines.push(` For each distinct bug fixed, create: ${incidentDir}/YYYY-MM-DD-<slug>.md`);
|
|
4126
4313
|
lines.push(" Include: Date, Severity, Symptoms, Root Cause, Fix, Files Changed, Prevention Rules");
|
|
@@ -4157,9 +4344,9 @@ async function main() {
|
|
|
4157
4344
|
}
|
|
4158
4345
|
function cleanup(flagPath) {
|
|
4159
4346
|
try {
|
|
4160
|
-
if (
|
|
4347
|
+
if (existsSync10(flagPath)) unlinkSync2(flagPath);
|
|
4161
4348
|
const dir = join6(tmpdir2(), "massu-auto-learning");
|
|
4162
|
-
if (
|
|
4349
|
+
if (existsSync10(dir)) {
|
|
4163
4350
|
const now = Date.now();
|
|
4164
4351
|
for (const file of readdirSync2(dir)) {
|
|
4165
4352
|
const fullPath = join6(dir, file);
|
|
@@ -4176,17 +4363,27 @@ function cleanup(flagPath) {
|
|
|
4176
4363
|
}
|
|
4177
4364
|
}
|
|
4178
4365
|
function readStdin() {
|
|
4179
|
-
return new Promise((
|
|
4366
|
+
return new Promise((resolve6) => {
|
|
4180
4367
|
let data = "";
|
|
4181
4368
|
process.stdin.setEncoding("utf-8");
|
|
4182
4369
|
process.stdin.on("data", (chunk) => {
|
|
4183
4370
|
data += chunk;
|
|
4184
4371
|
});
|
|
4185
|
-
process.stdin.on("end", () =>
|
|
4186
|
-
setTimeout(() =>
|
|
4372
|
+
process.stdin.on("end", () => resolve6(data));
|
|
4373
|
+
setTimeout(() => resolve6(data), 5e3);
|
|
4187
4374
|
});
|
|
4188
4375
|
}
|
|
4189
|
-
|
|
4376
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
4377
|
+
main();
|
|
4378
|
+
}
|
|
4190
4379
|
export {
|
|
4191
|
-
|
|
4380
|
+
CODE_ONLY_PATHSPEC,
|
|
4381
|
+
DOC_EXCLUDE_PATHSPEC,
|
|
4382
|
+
FIX_PATTERN_RE,
|
|
4383
|
+
MAX_FULL_DIFF_BYTES,
|
|
4384
|
+
MAX_SESSION_PATHSPEC_FILES,
|
|
4385
|
+
RECENT_INCIDENT_WINDOW_MS,
|
|
4386
|
+
REMOVED_BROKEN_RE,
|
|
4387
|
+
recentIncidentFiles,
|
|
4388
|
+
scanUncommittedForFix
|
|
4192
4389
|
};
|