@massu/core 2.5.0 → 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 +231 -0
- package/commands/massu-bearings.md +1 -1
- package/commands/massu-recap.md +16 -5
- package/dist/cli.js +716 -258
- package/dist/hooks/auto-learning-pipeline.js +297 -56
- package/dist/hooks/classify-failure.js +323 -25
- package/dist/hooks/cost-tracker.js +47 -10
- package/dist/hooks/fix-detector.js +94 -10
- package/dist/hooks/incident-pipeline.js +77 -10
- package/dist/hooks/intent-suggester.js +77 -10
- package/dist/hooks/memory-recall.js +77 -10
- package/dist/hooks/post-edit-context.js +125 -11
- package/dist/hooks/post-tool-use.js +44 -7
- package/dist/hooks/pre-compact.js +43 -7
- package/dist/hooks/pre-delete-check.js +125 -13
- package/dist/hooks/pre-tool-use-gate.js +135 -17
- package/dist/hooks/quality-event.js +47 -10
- package/dist/hooks/rule-enforcement-pipeline.js +77 -10
- package/dist/hooks/security-gate.js +75 -10
- package/dist/hooks/session-end.js +43 -7
- package/dist/hooks/session-start.js +144 -38
- package/dist/hooks/user-prompt.js +43 -7
- 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(),
|
|
@@ -1339,6 +1346,16 @@ var init_memory_vector = __esm({
|
|
|
1339
1346
|
}
|
|
1340
1347
|
});
|
|
1341
1348
|
|
|
1349
|
+
// src/lib/timestamps.ts
|
|
1350
|
+
function nowIso() {
|
|
1351
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1352
|
+
}
|
|
1353
|
+
var init_timestamps = __esm({
|
|
1354
|
+
"src/lib/timestamps.ts"() {
|
|
1355
|
+
"use strict";
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1342
1359
|
// src/db-backup.ts
|
|
1343
1360
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1344
1361
|
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
@@ -3599,8 +3616,8 @@ function linkSessionToTask(db, sessionId, taskId) {
|
|
|
3599
3616
|
}
|
|
3600
3617
|
function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
|
|
3601
3618
|
const result = db.prepare(`
|
|
3602
|
-
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
|
|
3603
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3619
|
+
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
|
|
3620
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3604
3621
|
`).run(
|
|
3605
3622
|
sessionId,
|
|
3606
3623
|
turnNumber,
|
|
@@ -3609,14 +3626,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
|
|
|
3609
3626
|
toolCallsJson,
|
|
3610
3627
|
toolCallCount,
|
|
3611
3628
|
promptTokens,
|
|
3612
|
-
responseTokens
|
|
3629
|
+
responseTokens,
|
|
3630
|
+
nowIso()
|
|
3613
3631
|
);
|
|
3614
3632
|
return Number(result.lastInsertRowid);
|
|
3615
3633
|
}
|
|
3616
3634
|
function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
|
|
3617
3635
|
db.prepare(`
|
|
3618
|
-
INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
|
|
3619
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3636
|
+
INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved, created_at)
|
|
3637
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3620
3638
|
`).run(
|
|
3621
3639
|
sessionId,
|
|
3622
3640
|
turnNumber,
|
|
@@ -3625,7 +3643,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
|
|
|
3625
3643
|
inputSize,
|
|
3626
3644
|
outputSize,
|
|
3627
3645
|
success ? 1 : 0,
|
|
3628
|
-
filesInvolved ? JSON.stringify(filesInvolved) : null
|
|
3646
|
+
filesInvolved ? JSON.stringify(filesInvolved) : null,
|
|
3647
|
+
nowIso()
|
|
3629
3648
|
);
|
|
3630
3649
|
}
|
|
3631
3650
|
function getLastProcessedLine(db, sessionId) {
|
|
@@ -3884,6 +3903,7 @@ var init_memory_db = __esm({
|
|
|
3884
3903
|
init_db_driver();
|
|
3885
3904
|
init_config();
|
|
3886
3905
|
init_memory_vector();
|
|
3906
|
+
init_timestamps();
|
|
3887
3907
|
init_db_backup();
|
|
3888
3908
|
init_rule_delivery();
|
|
3889
3909
|
init_rule_candidate_store();
|
|
@@ -3975,27 +3995,256 @@ var init_hook_failure_signal = __esm({
|
|
|
3975
3995
|
// src/hooks/auto-learning-pipeline.ts
|
|
3976
3996
|
init_config();
|
|
3977
3997
|
import { execFileSync } from "child_process";
|
|
3978
|
-
import { existsSync as
|
|
3998
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, unlinkSync as unlinkSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3979
3999
|
import { tmpdir as tmpdir2 } from "os";
|
|
3980
4000
|
import { join as join6 } from "path";
|
|
3981
4001
|
|
|
3982
4002
|
// src/hooks/lib/write-hook-message.ts
|
|
3983
|
-
|
|
3984
|
-
|
|
4003
|
+
var HOOK_EVENTS = [
|
|
4004
|
+
"UserPromptSubmit",
|
|
4005
|
+
"UserPromptExpansion",
|
|
4006
|
+
"SessionStart",
|
|
4007
|
+
"PreToolUse",
|
|
4008
|
+
"PostToolUse",
|
|
4009
|
+
"Stop",
|
|
4010
|
+
"SubagentStop",
|
|
4011
|
+
"PreCompact"
|
|
4012
|
+
];
|
|
4013
|
+
var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
|
|
4014
|
+
function isHookEvent(value) {
|
|
4015
|
+
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
4016
|
+
}
|
|
4017
|
+
function writeHookContext(hookEventName, message) {
|
|
4018
|
+
if (!isHookEvent(hookEventName)) {
|
|
4019
|
+
process.stderr.write(
|
|
4020
|
+
`[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
|
|
4021
|
+
`
|
|
4022
|
+
);
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
4025
|
+
if (typeof message !== "string" || message.length === 0) return;
|
|
4026
|
+
let additionalContext = message;
|
|
4027
|
+
if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
|
|
4028
|
+
const marker = `
|
|
4029
|
+
[massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
|
|
4030
|
+
additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
|
|
4031
|
+
}
|
|
4032
|
+
process.stdout.write(
|
|
4033
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
|
|
4034
|
+
);
|
|
3985
4035
|
}
|
|
3986
4036
|
|
|
3987
4037
|
// src/hooks/auto-learning-pipeline.ts
|
|
3988
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
|
|
4158
|
+
var HOOK_EVENT = "Stop";
|
|
3989
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;
|
|
3990
4177
|
function getSessionFlagPath(sessionId) {
|
|
3991
4178
|
return join6(tmpdir2(), "massu-auto-learning", `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
3992
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
|
+
}
|
|
3993
4242
|
async function main() {
|
|
3994
4243
|
try {
|
|
3995
4244
|
const input = await readStdin();
|
|
3996
4245
|
const hookInput = JSON.parse(input);
|
|
3997
4246
|
const config = getConfig();
|
|
3998
|
-
if (config.autoLearning?.enabled === false) {
|
|
4247
|
+
if (config.autoLearning?.enabled === false || config.autoLearning?.fixDetection?.enabled === false) {
|
|
3999
4248
|
process.exit(0);
|
|
4000
4249
|
return;
|
|
4001
4250
|
}
|
|
@@ -4005,46 +4254,20 @@ async function main() {
|
|
|
4005
4254
|
const autoLearn = config.autoLearning;
|
|
4006
4255
|
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
4007
4256
|
let sessionFixes = [];
|
|
4008
|
-
if (
|
|
4257
|
+
if (existsSync10(flagPath)) {
|
|
4009
4258
|
try {
|
|
4010
|
-
sessionFixes =
|
|
4259
|
+
sessionFixes = readFileSync7(flagPath, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
4011
4260
|
} catch {
|
|
4012
4261
|
}
|
|
4013
4262
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
if (nameOnly.trim()) {
|
|
4023
|
-
const shortstat = execFileSync("git", ["diff", "--shortstat"], {
|
|
4024
|
-
cwd: root,
|
|
4025
|
-
timeout: 2e3,
|
|
4026
|
-
encoding: "utf-8",
|
|
4027
|
-
maxBuffer: 64 * 1024
|
|
4028
|
-
});
|
|
4029
|
-
const insertions = parseInt(shortstat.match(/(\d+) insertion/)?.[1] ?? "0", 10);
|
|
4030
|
-
const deletions = parseInt(shortstat.match(/(\d+) deletion/)?.[1] ?? "0", 10);
|
|
4031
|
-
const estimatedBytes = (insertions + deletions) * 80;
|
|
4032
|
-
if (estimatedBytes <= MAX_FULL_DIFF_BYTES) {
|
|
4033
|
-
const fullDiff = execFileSync("git", ["diff"], {
|
|
4034
|
-
cwd: root,
|
|
4035
|
-
timeout: 5e3,
|
|
4036
|
-
encoding: "utf-8",
|
|
4037
|
-
maxBuffer: MAX_FULL_DIFF_BYTES
|
|
4038
|
-
});
|
|
4039
|
-
const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|throw|raise|assert|validate|if.*null|if.*nil|if.*None|if.*undefined)/gm) || []).length;
|
|
4040
|
-
const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)/gm) || []).length;
|
|
4041
|
-
if (fixPatterns > 3 || removedBroken > 1) {
|
|
4042
|
-
uncommittedFix = true;
|
|
4043
|
-
}
|
|
4044
|
-
}
|
|
4045
|
-
}
|
|
4046
|
-
} catch {
|
|
4047
|
-
}
|
|
4263
|
+
const uncommittedFix = scanUncommittedForFix({
|
|
4264
|
+
root,
|
|
4265
|
+
sessionTouched: sessionTouchedFiles({
|
|
4266
|
+
root,
|
|
4267
|
+
transcriptPath: hookInput.transcript_path,
|
|
4268
|
+
flagPath
|
|
4269
|
+
})
|
|
4270
|
+
}).uncommittedFix;
|
|
4048
4271
|
if (sessionFixes.length === 0 && !uncommittedFix) {
|
|
4049
4272
|
cleanup(flagPath);
|
|
4050
4273
|
process.exit(0);
|
|
@@ -4073,10 +4296,18 @@ async function main() {
|
|
|
4073
4296
|
lines.push("");
|
|
4074
4297
|
lines.push(" Additional uncommitted fix patterns detected in git diff.");
|
|
4075
4298
|
}
|
|
4299
|
+
const todaysIncidents = recentIncidentFiles(join6(root, incidentDir));
|
|
4076
4300
|
lines.push("");
|
|
4077
|
-
|
|
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
|
+
}
|
|
4078
4309
|
lines.push("");
|
|
4079
|
-
if (autoLearn?.pipeline?.requireIncidentReport !== false) {
|
|
4310
|
+
if (autoLearn?.pipeline?.requireIncidentReport !== false && todaysIncidents.length === 0) {
|
|
4080
4311
|
lines.push(" STEP 1: INCIDENT REPORT");
|
|
4081
4312
|
lines.push(` For each distinct bug fixed, create: ${incidentDir}/YYYY-MM-DD-<slug>.md`);
|
|
4082
4313
|
lines.push(" Include: Date, Severity, Symptoms, Root Cause, Fix, Files Changed, Prevention Rules");
|
|
@@ -4104,7 +4335,7 @@ async function main() {
|
|
|
4104
4335
|
lines.push("");
|
|
4105
4336
|
lines.push("============================================================================");
|
|
4106
4337
|
lines.push("");
|
|
4107
|
-
|
|
4338
|
+
writeHookContext(HOOK_EVENT, lines.join("\n"));
|
|
4108
4339
|
cleanup(flagPath);
|
|
4109
4340
|
} catch (err) {
|
|
4110
4341
|
recordHookFailure("auto-learning-pipeline", err);
|
|
@@ -4113,9 +4344,9 @@ async function main() {
|
|
|
4113
4344
|
}
|
|
4114
4345
|
function cleanup(flagPath) {
|
|
4115
4346
|
try {
|
|
4116
|
-
if (
|
|
4347
|
+
if (existsSync10(flagPath)) unlinkSync2(flagPath);
|
|
4117
4348
|
const dir = join6(tmpdir2(), "massu-auto-learning");
|
|
4118
|
-
if (
|
|
4349
|
+
if (existsSync10(dir)) {
|
|
4119
4350
|
const now = Date.now();
|
|
4120
4351
|
for (const file of readdirSync2(dir)) {
|
|
4121
4352
|
const fullPath = join6(dir, file);
|
|
@@ -4132,17 +4363,27 @@ function cleanup(flagPath) {
|
|
|
4132
4363
|
}
|
|
4133
4364
|
}
|
|
4134
4365
|
function readStdin() {
|
|
4135
|
-
return new Promise((
|
|
4366
|
+
return new Promise((resolve6) => {
|
|
4136
4367
|
let data = "";
|
|
4137
4368
|
process.stdin.setEncoding("utf-8");
|
|
4138
4369
|
process.stdin.on("data", (chunk) => {
|
|
4139
4370
|
data += chunk;
|
|
4140
4371
|
});
|
|
4141
|
-
process.stdin.on("end", () =>
|
|
4142
|
-
setTimeout(() =>
|
|
4372
|
+
process.stdin.on("end", () => resolve6(data));
|
|
4373
|
+
setTimeout(() => resolve6(data), 5e3);
|
|
4143
4374
|
});
|
|
4144
4375
|
}
|
|
4145
|
-
|
|
4376
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
4377
|
+
main();
|
|
4378
|
+
}
|
|
4146
4379
|
export {
|
|
4147
|
-
|
|
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
|
|
4148
4389
|
};
|