@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
|
@@ -4,6 +4,12 @@ var __defProp = Object.defineProperty;
|
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
7
13
|
var __esm = (fn, res, err) => function __init() {
|
|
8
14
|
if (err) throw err[0];
|
|
9
15
|
try {
|
|
@@ -697,6 +703,13 @@ var init_config = __esm({
|
|
|
697
703
|
});
|
|
698
704
|
LanguageFrameworkEntrySchema = z2.object({
|
|
699
705
|
framework: z2.string().optional(),
|
|
706
|
+
// `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
|
|
707
|
+
// by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
|
|
708
|
+
// `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
|
|
709
|
+
// derives the index builders' candidate set from it, so it is typed here rather than
|
|
710
|
+
// read as an unknown: a consumer that has to shape-check its own SoT is one branch away
|
|
711
|
+
// from silently contributing `undefined` to a path predicate.
|
|
712
|
+
source_dirs: z2.array(z2.string()).optional(),
|
|
700
713
|
test_framework: z2.string().optional(),
|
|
701
714
|
test: z2.string().optional(),
|
|
702
715
|
runtime: z2.string().optional(),
|
|
@@ -1339,6 +1352,16 @@ var init_memory_vector = __esm({
|
|
|
1339
1352
|
}
|
|
1340
1353
|
});
|
|
1341
1354
|
|
|
1355
|
+
// src/lib/timestamps.ts
|
|
1356
|
+
function nowIso() {
|
|
1357
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1358
|
+
}
|
|
1359
|
+
var init_timestamps = __esm({
|
|
1360
|
+
"src/lib/timestamps.ts"() {
|
|
1361
|
+
"use strict";
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1342
1365
|
// src/db-backup.ts
|
|
1343
1366
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1344
1367
|
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
@@ -1804,8 +1827,8 @@ function resolveModelDir() {
|
|
|
1804
1827
|
function resolveWasmDir() {
|
|
1805
1828
|
try {
|
|
1806
1829
|
const req3 = createRequire3(import.meta.url);
|
|
1807
|
-
const
|
|
1808
|
-
return dirname6(
|
|
1830
|
+
const main3 = req3.resolve(ORT_PKG);
|
|
1831
|
+
return dirname6(main3);
|
|
1809
1832
|
} catch {
|
|
1810
1833
|
return null;
|
|
1811
1834
|
}
|
|
@@ -3670,8 +3693,8 @@ function linkSessionToTask(db, sessionId, taskId) {
|
|
|
3670
3693
|
}
|
|
3671
3694
|
function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
|
|
3672
3695
|
const result = db.prepare(`
|
|
3673
|
-
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
|
|
3674
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3696
|
+
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
|
|
3697
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3675
3698
|
`).run(
|
|
3676
3699
|
sessionId,
|
|
3677
3700
|
turnNumber,
|
|
@@ -3680,14 +3703,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
|
|
|
3680
3703
|
toolCallsJson,
|
|
3681
3704
|
toolCallCount,
|
|
3682
3705
|
promptTokens,
|
|
3683
|
-
responseTokens
|
|
3706
|
+
responseTokens,
|
|
3707
|
+
nowIso()
|
|
3684
3708
|
);
|
|
3685
3709
|
return Number(result.lastInsertRowid);
|
|
3686
3710
|
}
|
|
3687
3711
|
function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
|
|
3688
3712
|
db.prepare(`
|
|
3689
|
-
INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
|
|
3690
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3713
|
+
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)
|
|
3714
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3691
3715
|
`).run(
|
|
3692
3716
|
sessionId,
|
|
3693
3717
|
turnNumber,
|
|
@@ -3696,7 +3720,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
|
|
|
3696
3720
|
inputSize,
|
|
3697
3721
|
outputSize,
|
|
3698
3722
|
success ? 1 : 0,
|
|
3699
|
-
filesInvolved ? JSON.stringify(filesInvolved) : null
|
|
3723
|
+
filesInvolved ? JSON.stringify(filesInvolved) : null,
|
|
3724
|
+
nowIso()
|
|
3700
3725
|
);
|
|
3701
3726
|
}
|
|
3702
3727
|
function getLastProcessedLine(db, sessionId) {
|
|
@@ -3955,6 +3980,7 @@ var init_memory_db = __esm({
|
|
|
3955
3980
|
init_db_driver();
|
|
3956
3981
|
init_config();
|
|
3957
3982
|
init_memory_vector();
|
|
3983
|
+
init_timestamps();
|
|
3958
3984
|
init_db_backup();
|
|
3959
3985
|
init_rule_delivery();
|
|
3960
3986
|
init_rule_candidate_store();
|
|
@@ -3975,17 +4001,283 @@ var init_memory_db = __esm({
|
|
|
3975
4001
|
// src/hooks/classify-failure.ts
|
|
3976
4002
|
init_config();
|
|
3977
4003
|
init_memory_db();
|
|
3978
|
-
import { existsSync as
|
|
4004
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, readdirSync as readdirSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
4005
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
4006
|
+
import { join as join7, basename as basename3 } from "path";
|
|
4007
|
+
|
|
4008
|
+
// src/hooks/fix-detector.ts
|
|
4009
|
+
init_config();
|
|
4010
|
+
import { execFileSync } from "child_process";
|
|
4011
|
+
import { existsSync as existsSync9, appendFileSync as appendFileSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync6 } from "fs";
|
|
3979
4012
|
import { tmpdir as tmpdir2 } from "os";
|
|
3980
|
-
import { join as join6
|
|
4013
|
+
import { join as join6 } from "path";
|
|
3981
4014
|
|
|
3982
4015
|
// src/hooks/lib/write-hook-message.ts
|
|
3983
|
-
|
|
3984
|
-
|
|
4016
|
+
var HOOK_EVENTS = [
|
|
4017
|
+
"UserPromptSubmit",
|
|
4018
|
+
"UserPromptExpansion",
|
|
4019
|
+
"SessionStart",
|
|
4020
|
+
"PreToolUse",
|
|
4021
|
+
"PostToolUse",
|
|
4022
|
+
"Stop",
|
|
4023
|
+
"SubagentStop",
|
|
4024
|
+
"PreCompact"
|
|
4025
|
+
];
|
|
4026
|
+
var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
|
|
4027
|
+
function isHookEvent(value) {
|
|
4028
|
+
return typeof value === "string" && HOOK_EVENTS.includes(value);
|
|
4029
|
+
}
|
|
4030
|
+
function writeHookContext(hookEventName, message) {
|
|
4031
|
+
if (!isHookEvent(hookEventName)) {
|
|
4032
|
+
process.stderr.write(
|
|
4033
|
+
`[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
|
|
4034
|
+
`
|
|
4035
|
+
);
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
if (typeof message !== "string" || message.length === 0) return;
|
|
4039
|
+
let additionalContext = message;
|
|
4040
|
+
if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
|
|
4041
|
+
const marker = `
|
|
4042
|
+
[massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
|
|
4043
|
+
additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
|
|
4044
|
+
}
|
|
4045
|
+
process.stdout.write(
|
|
4046
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
|
|
4047
|
+
);
|
|
4048
|
+
}
|
|
4049
|
+
|
|
4050
|
+
// src/hooks/fix-detector.ts
|
|
4051
|
+
init_hook_failure_signal();
|
|
4052
|
+
|
|
4053
|
+
// src/hooks/lib/is-direct-invocation.ts
|
|
4054
|
+
import { pathToFileURL } from "url";
|
|
4055
|
+
function isDirectInvocation(moduleUrl) {
|
|
4056
|
+
const entry = process.argv[1];
|
|
4057
|
+
if (!entry) return false;
|
|
4058
|
+
try {
|
|
4059
|
+
return moduleUrl === pathToFileURL(entry).href;
|
|
4060
|
+
} catch {
|
|
4061
|
+
return false;
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
// src/hooks/fix-detector.ts
|
|
4066
|
+
var HOOK_EVENT = "PostToolUse";
|
|
4067
|
+
var TEST_OR_FIXTURE_PATH_PATTERNS = [
|
|
4068
|
+
/(^|\/)(__tests__|__fixtures__|tests?|fixtures?|spec)\//,
|
|
4069
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/,
|
|
4070
|
+
/(^|\/)(test_[^/]+|[^/]+_test)\.py$/
|
|
4071
|
+
];
|
|
4072
|
+
function isTestOrFixturePath(filePath) {
|
|
4073
|
+
return TEST_OR_FIXTURE_PATH_PATTERNS.some((re) => re.test(filePath));
|
|
4074
|
+
}
|
|
4075
|
+
var FIX_HEURISTICS = [
|
|
4076
|
+
{
|
|
4077
|
+
name: "removed_broken_code",
|
|
4078
|
+
test: (diff) => /^-.*\b(bug|broken|wrong|incorrect|typo|crash|error|fail|miss|stale)\b/m.test(diff)
|
|
4079
|
+
},
|
|
4080
|
+
{
|
|
4081
|
+
name: "added_error_handling",
|
|
4082
|
+
test: (diff) => {
|
|
4083
|
+
const added = (diff.match(/^\+.*(try|except|catch|guard|if.*nil|if.*None|validate|assert|raise|throw)/gm) || []).length;
|
|
4084
|
+
return added > 2;
|
|
4085
|
+
}
|
|
4086
|
+
},
|
|
4087
|
+
{
|
|
4088
|
+
name: "method_name_correction",
|
|
4089
|
+
test: (diff) => {
|
|
4090
|
+
const removed = diff.match(/^-.*\.([a-z_]+)\(/m);
|
|
4091
|
+
const added = diff.match(/^\+.*\.([a-z_]+)\(/m);
|
|
4092
|
+
return !!(removed && added && removed[1] !== added[1]);
|
|
4093
|
+
}
|
|
4094
|
+
},
|
|
4095
|
+
{
|
|
4096
|
+
name: "auth_fix",
|
|
4097
|
+
test: (diff) => /^\+.*(token|auth|header|X-Service|Bearer|credential)/im.test(diff)
|
|
4098
|
+
},
|
|
4099
|
+
{
|
|
4100
|
+
name: "nil_handling_fix",
|
|
4101
|
+
test: (diff) => /^\+.*(= nil|= None|\.isNil|is None|!= nil|is not None|guard let|if let|optional)/m.test(diff) && /^-/m.test(diff)
|
|
4102
|
+
},
|
|
4103
|
+
{
|
|
4104
|
+
name: "concurrency_fix",
|
|
4105
|
+
test: (diff) => /^\+.*(timeout|semaphore|lock|mutex|throttle|rate.limit|max_conn)/im.test(diff)
|
|
4106
|
+
},
|
|
4107
|
+
{
|
|
4108
|
+
name: "async_pattern_fix",
|
|
4109
|
+
test: (diff) => /^\+.*(@MainActor|async with|asyncio\.timeout|\.await)/.test(diff) && /^-/m.test(diff)
|
|
4110
|
+
},
|
|
4111
|
+
{
|
|
4112
|
+
name: "added_missing_import",
|
|
4113
|
+
test: (diff) => /^\+.*(import|from.*import|require)/.test(diff) && !/^-.*(import|from.*import|require)/m.test(diff)
|
|
4114
|
+
}
|
|
4115
|
+
];
|
|
4116
|
+
function getSessionFlagPath(sessionId) {
|
|
4117
|
+
const dir = join6(tmpdir2(), "massu-auto-learning");
|
|
4118
|
+
if (!existsSync9(dir)) {
|
|
4119
|
+
mkdirSync7(dir, { recursive: true });
|
|
4120
|
+
}
|
|
4121
|
+
return join6(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
4122
|
+
}
|
|
4123
|
+
function _stateDir() {
|
|
4124
|
+
const dir = join6(tmpdir2(), "massu-fix-detector-state");
|
|
4125
|
+
if (!existsSync9(dir)) {
|
|
4126
|
+
mkdirSync7(dir, { recursive: true });
|
|
4127
|
+
}
|
|
4128
|
+
return dir;
|
|
4129
|
+
}
|
|
4130
|
+
function _cwdHash(cwd) {
|
|
4131
|
+
let h = 5381;
|
|
4132
|
+
for (let i = 0; i < cwd.length; i++) {
|
|
4133
|
+
h = (h << 5) + h + cwd.charCodeAt(i) | 0;
|
|
4134
|
+
}
|
|
4135
|
+
return Math.abs(h).toString(36);
|
|
4136
|
+
}
|
|
4137
|
+
function isGitWorkTreeFast(cwd) {
|
|
4138
|
+
try {
|
|
4139
|
+
const cachePath = join6(_stateDir(), `worktree-${_cwdHash(cwd)}.json`);
|
|
4140
|
+
if (existsSync9(cachePath)) {
|
|
4141
|
+
const cached = JSON.parse(readFileSync6(cachePath, "utf-8"));
|
|
4142
|
+
const ageMs = Date.now() - cached.ts;
|
|
4143
|
+
if (ageMs < 36e5) return cached.isWorkTree;
|
|
4144
|
+
}
|
|
4145
|
+
let isWorkTree = false;
|
|
4146
|
+
try {
|
|
4147
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd, timeout: 500, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
4148
|
+
isWorkTree = true;
|
|
4149
|
+
} catch {
|
|
4150
|
+
isWorkTree = false;
|
|
4151
|
+
}
|
|
4152
|
+
try {
|
|
4153
|
+
appendFileSync3(cachePath, "");
|
|
4154
|
+
const fs = __require("fs");
|
|
4155
|
+
fs.writeFileSync(cachePath, JSON.stringify({ isWorkTree, ts: Date.now() }));
|
|
4156
|
+
} catch {
|
|
4157
|
+
}
|
|
4158
|
+
return isWorkTree;
|
|
4159
|
+
} catch {
|
|
4160
|
+
return false;
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
function _disabledFlagPath(sessionId) {
|
|
4164
|
+
return join6(_stateDir(), `disabled-${sessionId.slice(0, 12)}.flag`);
|
|
4165
|
+
}
|
|
4166
|
+
function isSessionAutoDisabled(sessionId) {
|
|
4167
|
+
try {
|
|
4168
|
+
return existsSync9(_disabledFlagPath(sessionId));
|
|
4169
|
+
} catch {
|
|
4170
|
+
return false;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
function markSessionAutoDisabled(sessionId, elapsedMs) {
|
|
4174
|
+
try {
|
|
4175
|
+
const fs = __require("fs");
|
|
4176
|
+
fs.writeFileSync(_disabledFlagPath(sessionId), `disabled: previous git diff took ${elapsedMs}ms
|
|
4177
|
+
`);
|
|
4178
|
+
} catch {
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
async function main() {
|
|
4182
|
+
try {
|
|
4183
|
+
const input = await readStdin();
|
|
4184
|
+
const hookInput = JSON.parse(input);
|
|
4185
|
+
const filePath = hookInput.tool_input?.file_path;
|
|
4186
|
+
if (!filePath || !existsSync9(filePath)) {
|
|
4187
|
+
process.exit(0);
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
if (!/\.(py|swift|ts|tsx|js|jsx|rs|go|rb|sh)$/.test(filePath)) {
|
|
4191
|
+
process.exit(0);
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
const config = getConfig();
|
|
4195
|
+
const incidentDir = config.autoLearning?.incidentDir ?? "docs/incidents";
|
|
4196
|
+
const memoryDir = config.autoLearning?.memoryDir ?? "memory";
|
|
4197
|
+
if (filePath.includes(incidentDir) || filePath.includes(memoryDir) || filePath.includes("MEMORY.md")) {
|
|
4198
|
+
process.exit(0);
|
|
4199
|
+
return;
|
|
4200
|
+
}
|
|
4201
|
+
if (isTestOrFixturePath(filePath)) {
|
|
4202
|
+
process.exit(0);
|
|
4203
|
+
return;
|
|
4204
|
+
}
|
|
4205
|
+
if (config.autoLearning?.enabled === false || config.autoLearning?.fixDetection?.enabled === false) {
|
|
4206
|
+
process.exit(0);
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
4209
|
+
const root = getProjectRoot();
|
|
4210
|
+
if (!isGitWorkTreeFast(root) || isSessionAutoDisabled(hookInput.session_id)) {
|
|
4211
|
+
process.exit(0);
|
|
4212
|
+
return;
|
|
4213
|
+
}
|
|
4214
|
+
let diff = "";
|
|
4215
|
+
try {
|
|
4216
|
+
const startMs = Date.now();
|
|
4217
|
+
diff = execFileSync("git", ["diff", "--", filePath], { cwd: root, timeout: 3e3, encoding: "utf-8" });
|
|
4218
|
+
if (!diff) {
|
|
4219
|
+
diff = execFileSync("git", ["diff", "HEAD", "--", filePath], { cwd: root, timeout: 3e3, encoding: "utf-8" });
|
|
4220
|
+
}
|
|
4221
|
+
const elapsedMs = Date.now() - startMs;
|
|
4222
|
+
if (elapsedMs > 2e3) {
|
|
4223
|
+
markSessionAutoDisabled(hookInput.session_id, elapsedMs);
|
|
4224
|
+
}
|
|
4225
|
+
} catch {
|
|
4226
|
+
process.exit(0);
|
|
4227
|
+
return;
|
|
4228
|
+
}
|
|
4229
|
+
if (!diff) {
|
|
4230
|
+
process.exit(0);
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
const enabledSignals = new Set(config.autoLearning?.fixDetection?.signals ?? FIX_HEURISTICS.map((h) => h.name));
|
|
4234
|
+
const detected = [];
|
|
4235
|
+
for (const heuristic of FIX_HEURISTICS) {
|
|
4236
|
+
if (enabledSignals.has(heuristic.name) && heuristic.test(diff)) {
|
|
4237
|
+
detected.push(heuristic.name);
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
if (detected.length === 0) {
|
|
4241
|
+
process.exit(0);
|
|
4242
|
+
return;
|
|
4243
|
+
}
|
|
4244
|
+
const signal = {
|
|
4245
|
+
file: filePath,
|
|
4246
|
+
signals: detected,
|
|
4247
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4248
|
+
};
|
|
4249
|
+
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
4250
|
+
appendFileSync3(flagPath, JSON.stringify(signal) + "\n");
|
|
4251
|
+
const lines = readFileSync6(flagPath, "utf-8").split("\n").filter(Boolean);
|
|
4252
|
+
if (lines.length === 1) {
|
|
4253
|
+
writeHookContext(
|
|
4254
|
+
HOOK_EVENT,
|
|
4255
|
+
`[Massu Auto-Learning] Bug fix detected in ${filePath} (signals: ${detected.join(", ")}). The auto-learning pipeline will prompt you at session end to create an incident report, derive a prevention rule, and add enforcement.`
|
|
4256
|
+
);
|
|
4257
|
+
}
|
|
4258
|
+
} catch (err) {
|
|
4259
|
+
recordHookFailure("fix-detector", err);
|
|
4260
|
+
}
|
|
4261
|
+
process.exit(0);
|
|
4262
|
+
}
|
|
4263
|
+
function readStdin() {
|
|
4264
|
+
return new Promise((resolve5) => {
|
|
4265
|
+
let data = "";
|
|
4266
|
+
process.stdin.setEncoding("utf-8");
|
|
4267
|
+
process.stdin.on("data", (chunk) => {
|
|
4268
|
+
data += chunk;
|
|
4269
|
+
});
|
|
4270
|
+
process.stdin.on("end", () => resolve5(data));
|
|
4271
|
+
setTimeout(() => resolve5(data), 3e3);
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
4275
|
+
main();
|
|
3985
4276
|
}
|
|
3986
4277
|
|
|
3987
4278
|
// src/hooks/classify-failure.ts
|
|
3988
4279
|
init_hook_failure_signal();
|
|
4280
|
+
var HOOK_EVENT2 = "PostToolUse";
|
|
3989
4281
|
var BUG_FIX_INDICATORS = [
|
|
3990
4282
|
/\b(catch|error|throw|fail|fix|bug|broken|missing|crash|wrong|typo|incorrect)\b/i
|
|
3991
4283
|
];
|
|
@@ -3993,7 +4285,7 @@ var CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|py|swift|rs|go|rb|sh)$/;
|
|
|
3993
4285
|
function getDedupeMarkerPath(sessionId, filePath) {
|
|
3994
4286
|
const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3995
4287
|
const hash = simpleHash(filePath);
|
|
3996
|
-
return
|
|
4288
|
+
return join7(tmpdir3(), `massu-classify-${day}-${sessionId.slice(0, 8)}-${hash}`);
|
|
3997
4289
|
}
|
|
3998
4290
|
function simpleHash(str) {
|
|
3999
4291
|
let h = 0;
|
|
@@ -4003,13 +4295,13 @@ function simpleHash(str) {
|
|
|
4003
4295
|
return Math.abs(h).toString(36);
|
|
4004
4296
|
}
|
|
4005
4297
|
function readFailureContextFiles() {
|
|
4006
|
-
const dir =
|
|
4298
|
+
const dir = tmpdir3();
|
|
4007
4299
|
let context = "";
|
|
4008
4300
|
try {
|
|
4009
4301
|
const files = readdirSync2(dir).filter((f) => f.startsWith("massu-failure-context-"));
|
|
4010
4302
|
for (const file of files) {
|
|
4011
4303
|
try {
|
|
4012
|
-
context += " " +
|
|
4304
|
+
context += " " + readFileSync7(join7(dir, file), "utf-8");
|
|
4013
4305
|
} catch {
|
|
4014
4306
|
}
|
|
4015
4307
|
}
|
|
@@ -4018,21 +4310,21 @@ function readFailureContextFiles() {
|
|
|
4018
4310
|
return context.trim();
|
|
4019
4311
|
}
|
|
4020
4312
|
function cleanupFailureContextFiles() {
|
|
4021
|
-
const dir =
|
|
4313
|
+
const dir = tmpdir3();
|
|
4022
4314
|
try {
|
|
4023
4315
|
const files = readdirSync2(dir).filter((f) => f.startsWith("massu-failure-context-"));
|
|
4024
4316
|
for (const file of files) {
|
|
4025
4317
|
try {
|
|
4026
|
-
unlinkSync2(
|
|
4318
|
+
unlinkSync2(join7(dir, file));
|
|
4027
4319
|
} catch {
|
|
4028
4320
|
}
|
|
4029
4321
|
}
|
|
4030
4322
|
} catch {
|
|
4031
4323
|
}
|
|
4032
4324
|
}
|
|
4033
|
-
async function
|
|
4325
|
+
async function main2() {
|
|
4034
4326
|
try {
|
|
4035
|
-
const input = await
|
|
4327
|
+
const input = await readStdin2();
|
|
4036
4328
|
const hookInput = JSON.parse(input);
|
|
4037
4329
|
const filePath = hookInput.tool_input?.file_path;
|
|
4038
4330
|
if (!filePath) {
|
|
@@ -4056,6 +4348,10 @@ async function main() {
|
|
|
4056
4348
|
process.exit(0);
|
|
4057
4349
|
return;
|
|
4058
4350
|
}
|
|
4351
|
+
if (isTestOrFixturePath(relPath) || isTestOrFixturePath(filePath)) {
|
|
4352
|
+
process.exit(0);
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4059
4355
|
const oldString = hookInput.tool_input?.old_string ?? "";
|
|
4060
4356
|
const newString = hookInput.tool_input?.new_string ?? "";
|
|
4061
4357
|
const content = hookInput.tool_input?.content ?? "";
|
|
@@ -4085,7 +4381,7 @@ async function main() {
|
|
|
4085
4381
|
return;
|
|
4086
4382
|
}
|
|
4087
4383
|
const dedupeMarker = getDedupeMarkerPath(hookInput.session_id, filePath);
|
|
4088
|
-
if (
|
|
4384
|
+
if (existsSync10(dedupeMarker)) {
|
|
4089
4385
|
process.exit(0);
|
|
4090
4386
|
return;
|
|
4091
4387
|
}
|
|
@@ -4127,7 +4423,7 @@ function outputKnownPattern(match) {
|
|
|
4127
4423
|
lines.push(` Covered by: ${match.rules.join(", ")}`);
|
|
4128
4424
|
}
|
|
4129
4425
|
lines.push(" No new rules needed. Reference existing incident if logging.");
|
|
4130
|
-
|
|
4426
|
+
writeHookContext(HOOK_EVENT2, lines.join("\n"));
|
|
4131
4427
|
}
|
|
4132
4428
|
function outputSimilarPattern(match) {
|
|
4133
4429
|
const lines = [];
|
|
@@ -4137,7 +4433,7 @@ function outputSimilarPattern(match) {
|
|
|
4137
4433
|
lines.push(` Check if existing rules cover this case: ${match.rules.join(", ")}`);
|
|
4138
4434
|
}
|
|
4139
4435
|
lines.push(" If genuinely new: create incident + prevention rule + enforcement.");
|
|
4140
|
-
|
|
4436
|
+
writeHookContext(HOOK_EVENT2, lines.join("\n"));
|
|
4141
4437
|
}
|
|
4142
4438
|
function outputNewPattern(fileName, match) {
|
|
4143
4439
|
const lines = [];
|
|
@@ -4151,9 +4447,9 @@ function outputNewPattern(fileName, match) {
|
|
|
4151
4447
|
lines.push(" 1. INCIDENT REPORT");
|
|
4152
4448
|
lines.push(" 2. PREVENTION RULE (if new failure pattern)");
|
|
4153
4449
|
lines.push(" 3. ENFORCEMENT (hook or static check)");
|
|
4154
|
-
|
|
4450
|
+
writeHookContext(HOOK_EVENT2, lines.join("\n"));
|
|
4155
4451
|
}
|
|
4156
|
-
function
|
|
4452
|
+
function readStdin2() {
|
|
4157
4453
|
return new Promise((resolve5) => {
|
|
4158
4454
|
let data = "";
|
|
4159
4455
|
process.stdin.setEncoding("utf-8");
|
|
@@ -4164,4 +4460,6 @@ function readStdin() {
|
|
|
4164
4460
|
setTimeout(() => resolve5(data), 3e3);
|
|
4165
4461
|
});
|
|
4166
4462
|
}
|
|
4167
|
-
|
|
4463
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
4464
|
+
main2();
|
|
4465
|
+
}
|
|
@@ -1167,6 +1167,13 @@ var init_config = __esm({
|
|
|
1167
1167
|
});
|
|
1168
1168
|
LanguageFrameworkEntrySchema = z2.object({
|
|
1169
1169
|
framework: z2.string().optional(),
|
|
1170
|
+
// `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
|
|
1171
|
+
// by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
|
|
1172
|
+
// `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
|
|
1173
|
+
// derives the index builders' candidate set from it, so it is typed here rather than
|
|
1174
|
+
// read as an unknown: a consumer that has to shape-check its own SoT is one branch away
|
|
1175
|
+
// from silently contributing `undefined` to a path predicate.
|
|
1176
|
+
source_dirs: z2.array(z2.string()).optional(),
|
|
1170
1177
|
test_framework: z2.string().optional(),
|
|
1171
1178
|
test: z2.string().optional(),
|
|
1172
1179
|
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";
|
|
@@ -3670,8 +3687,8 @@ function linkSessionToTask(db, sessionId, taskId) {
|
|
|
3670
3687
|
}
|
|
3671
3688
|
function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
|
|
3672
3689
|
const result = db.prepare(`
|
|
3673
|
-
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
|
|
3674
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3690
|
+
INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
|
|
3691
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3675
3692
|
`).run(
|
|
3676
3693
|
sessionId,
|
|
3677
3694
|
turnNumber,
|
|
@@ -3680,14 +3697,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
|
|
|
3680
3697
|
toolCallsJson,
|
|
3681
3698
|
toolCallCount,
|
|
3682
3699
|
promptTokens,
|
|
3683
|
-
responseTokens
|
|
3700
|
+
responseTokens,
|
|
3701
|
+
nowIso()
|
|
3684
3702
|
);
|
|
3685
3703
|
return Number(result.lastInsertRowid);
|
|
3686
3704
|
}
|
|
3687
3705
|
function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
|
|
3688
3706
|
db.prepare(`
|
|
3689
|
-
INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
|
|
3690
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
3707
|
+
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)
|
|
3708
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3691
3709
|
`).run(
|
|
3692
3710
|
sessionId,
|
|
3693
3711
|
turnNumber,
|
|
@@ -3696,7 +3714,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
|
|
|
3696
3714
|
inputSize,
|
|
3697
3715
|
outputSize,
|
|
3698
3716
|
success ? 1 : 0,
|
|
3699
|
-
filesInvolved ? JSON.stringify(filesInvolved) : null
|
|
3717
|
+
filesInvolved ? JSON.stringify(filesInvolved) : null,
|
|
3718
|
+
nowIso()
|
|
3700
3719
|
);
|
|
3701
3720
|
}
|
|
3702
3721
|
function getLastProcessedLine(db, sessionId) {
|
|
@@ -3955,6 +3974,7 @@ var init_memory_db = __esm({
|
|
|
3955
3974
|
init_db_driver();
|
|
3956
3975
|
init_config();
|
|
3957
3976
|
init_memory_vector();
|
|
3977
|
+
init_timestamps();
|
|
3958
3978
|
init_db_backup();
|
|
3959
3979
|
init_rule_delivery();
|
|
3960
3980
|
init_rule_candidate_store();
|
|
@@ -4072,6 +4092,21 @@ function toolResponseText(raw) {
|
|
|
4072
4092
|
|
|
4073
4093
|
// src/hooks/cost-tracker.ts
|
|
4074
4094
|
init_hook_failure_signal();
|
|
4095
|
+
init_timestamps();
|
|
4096
|
+
|
|
4097
|
+
// src/hooks/lib/is-direct-invocation.ts
|
|
4098
|
+
import { pathToFileURL } from "url";
|
|
4099
|
+
function isDirectInvocation(moduleUrl) {
|
|
4100
|
+
const entry = process.argv[1];
|
|
4101
|
+
if (!entry) return false;
|
|
4102
|
+
try {
|
|
4103
|
+
return moduleUrl === pathToFileURL(entry).href;
|
|
4104
|
+
} catch {
|
|
4105
|
+
return false;
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
|
|
4109
|
+
// src/hooks/cost-tracker.ts
|
|
4075
4110
|
var CHARS_PER_TOKEN = 4;
|
|
4076
4111
|
function estimateTokens(text) {
|
|
4077
4112
|
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
@@ -4087,9 +4122,9 @@ async function main() {
|
|
|
4087
4122
|
const db = getMemoryDb();
|
|
4088
4123
|
try {
|
|
4089
4124
|
db.prepare(`
|
|
4090
|
-
INSERT INTO tool_cost_events (session_id, tool_name, estimated_input_tokens, estimated_output_tokens, model)
|
|
4091
|
-
VALUES (?, ?, ?, ?, ?)
|
|
4092
|
-
`).run(session_id, tool_name, estimatedInputTokens, estimatedOutputTokens, "");
|
|
4125
|
+
INSERT INTO tool_cost_events (session_id, tool_name, estimated_input_tokens, estimated_output_tokens, model, created_at)
|
|
4126
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
4127
|
+
`).run(session_id, tool_name, estimatedInputTokens, estimatedOutputTokens, "", nowIso());
|
|
4093
4128
|
} finally {
|
|
4094
4129
|
db.close();
|
|
4095
4130
|
}
|
|
@@ -4109,4 +4144,6 @@ function readStdin() {
|
|
|
4109
4144
|
setTimeout(() => resolve5(data), 3e3);
|
|
4110
4145
|
});
|
|
4111
4146
|
}
|
|
4112
|
-
|
|
4147
|
+
if (isDirectInvocation(import.meta.url)) {
|
|
4148
|
+
main();
|
|
4149
|
+
}
|