@massu/core 1.12.2 → 1.13.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/CHANGELOG.md +44 -0
- package/commands/massu-loop/references/auto-learning.md +1 -1
- package/dist/cli.js +94 -20
- package/dist/hooks/auto-learning-pipeline.js +11 -1
- package/dist/hooks/classify-failure.js +89 -2
- package/dist/hooks/cost-tracker.js +89 -2
- package/dist/hooks/fix-detector.js +11 -1
- package/dist/hooks/incident-pipeline.js +89 -2
- package/dist/hooks/post-edit-context.js +11 -1
- package/dist/hooks/post-tool-use.js +173 -3
- package/dist/hooks/pre-compact.js +89 -2
- package/dist/hooks/pre-delete-check.js +11 -1
- package/dist/hooks/pre-tool-use-gate.js +11 -1
- package/dist/hooks/quality-event.js +89 -2
- package/dist/hooks/rule-enforcement-pipeline.js +11 -1
- package/dist/hooks/session-end.js +89 -2
- package/dist/hooks/session-start.js +89 -2
- package/dist/hooks/user-prompt.js +346 -5
- package/package.json +1 -1
|
@@ -8108,7 +8108,17 @@ var AutoLearningConfigSchema = z.object({
|
|
|
8108
8108
|
requireIncidentReport: z.boolean().default(true),
|
|
8109
8109
|
requirePreventionRule: z.boolean().default(true),
|
|
8110
8110
|
requireEnforcement: z.boolean().default(true)
|
|
8111
|
-
}).default({})
|
|
8111
|
+
}).default({}),
|
|
8112
|
+
// plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
|
|
8113
|
+
// custom destinations for the rule-candidate funnel. The classifier matches
|
|
8114
|
+
// a candidate to one of these entries when none of the framework
|
|
8115
|
+
// destinations (pattern-scanner / claude-md-cr / corrections-md) apply.
|
|
8116
|
+
customDestinations: z.array(z.object({
|
|
8117
|
+
name: z.string(),
|
|
8118
|
+
path: z.string(),
|
|
8119
|
+
triggerKeywords: z.array(z.string()).default([]),
|
|
8120
|
+
template: z.string()
|
|
8121
|
+
})).default([])
|
|
8112
8122
|
}).optional();
|
|
8113
8123
|
var CloudConfigSchema = z.object({
|
|
8114
8124
|
enabled: z.boolean().default(false),
|
|
@@ -8501,6 +8511,66 @@ function getMemoryDb() {
|
|
|
8501
8511
|
initMemorySchema(db);
|
|
8502
8512
|
return db;
|
|
8503
8513
|
}
|
|
8514
|
+
function migrateAuditLogCheckExtension(db) {
|
|
8515
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
8516
|
+
if (!row) return;
|
|
8517
|
+
const expected = [
|
|
8518
|
+
"code_change",
|
|
8519
|
+
"rule_enforced",
|
|
8520
|
+
"approval",
|
|
8521
|
+
"review",
|
|
8522
|
+
"commit",
|
|
8523
|
+
"compaction",
|
|
8524
|
+
"rule_candidate_emitted",
|
|
8525
|
+
"rule_promoted",
|
|
8526
|
+
"rule_dismissed"
|
|
8527
|
+
];
|
|
8528
|
+
const checkClauseMatch = row.sql.match(/event_type\s+TEXT\s+NOT\s+NULL\s+CHECK\s*\(\s*event_type\s+IN\s*\(([\s\S]*?)\)\s*\)/i);
|
|
8529
|
+
if (checkClauseMatch) {
|
|
8530
|
+
const values = (checkClauseMatch[1].match(/'([^']+)'/g) ?? []).map((s) => s.slice(1, -1));
|
|
8531
|
+
if (expected.every((v) => values.includes(v))) return;
|
|
8532
|
+
}
|
|
8533
|
+
db.pragma("foreign_keys = OFF");
|
|
8534
|
+
try {
|
|
8535
|
+
db.exec("BEGIN TRANSACTION");
|
|
8536
|
+
db.exec(`
|
|
8537
|
+
CREATE TABLE audit_log_new (
|
|
8538
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8539
|
+
session_id TEXT NOT NULL,
|
|
8540
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
8541
|
+
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
8542
|
+
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
8543
|
+
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed'
|
|
8544
|
+
)),
|
|
8545
|
+
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
8546
|
+
model_id TEXT,
|
|
8547
|
+
file_path TEXT,
|
|
8548
|
+
change_type TEXT CHECK(change_type IN ('create', 'edit', 'delete')),
|
|
8549
|
+
rules_in_effect TEXT,
|
|
8550
|
+
approval_status TEXT CHECK(approval_status IN ('auto_approved', 'human_approved', 'pending', 'denied')),
|
|
8551
|
+
evidence TEXT,
|
|
8552
|
+
metadata TEXT,
|
|
8553
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
8554
|
+
);
|
|
8555
|
+
INSERT INTO audit_log_new SELECT * FROM audit_log;
|
|
8556
|
+
DROP TABLE audit_log;
|
|
8557
|
+
ALTER TABLE audit_log_new RENAME TO audit_log;
|
|
8558
|
+
CREATE INDEX IF NOT EXISTS idx_al_session ON audit_log(session_id);
|
|
8559
|
+
CREATE INDEX IF NOT EXISTS idx_al_file ON audit_log(file_path);
|
|
8560
|
+
CREATE INDEX IF NOT EXISTS idx_al_event ON audit_log(event_type);
|
|
8561
|
+
CREATE INDEX IF NOT EXISTS idx_al_timestamp ON audit_log(timestamp DESC);
|
|
8562
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_rule_promoted
|
|
8563
|
+
ON audit_log (event_type, json_extract(metadata, '$.prompt_hash'))
|
|
8564
|
+
WHERE event_type = 'rule_promoted';
|
|
8565
|
+
`);
|
|
8566
|
+
db.exec("COMMIT");
|
|
8567
|
+
} catch (err) {
|
|
8568
|
+
db.exec("ROLLBACK");
|
|
8569
|
+
throw err;
|
|
8570
|
+
} finally {
|
|
8571
|
+
db.pragma("foreign_keys = ON");
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8504
8574
|
function initMemorySchema(db) {
|
|
8505
8575
|
db.exec(`
|
|
8506
8576
|
-- Sessions table (linked to Claude Code session IDs)
|
|
@@ -8790,7 +8860,10 @@ function initMemorySchema(db) {
|
|
|
8790
8860
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
8791
8861
|
session_id TEXT NOT NULL,
|
|
8792
8862
|
timestamp TEXT DEFAULT (datetime('now')),
|
|
8793
|
-
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
8863
|
+
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
8864
|
+
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
8865
|
+
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed'
|
|
8866
|
+
)),
|
|
8794
8867
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
8795
8868
|
model_id TEXT,
|
|
8796
8869
|
file_path TEXT,
|
|
@@ -8805,7 +8878,21 @@ function initMemorySchema(db) {
|
|
|
8805
8878
|
CREATE INDEX IF NOT EXISTS idx_al_file ON audit_log(file_path);
|
|
8806
8879
|
CREATE INDEX IF NOT EXISTS idx_al_event ON audit_log(event_type);
|
|
8807
8880
|
CREATE INDEX IF NOT EXISTS idx_al_timestamp ON audit_log(timestamp DESC);
|
|
8881
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_rule_promoted
|
|
8882
|
+
ON audit_log (event_type, json_extract(metadata, '$.prompt_hash'))
|
|
8883
|
+
WHERE event_type = 'rule_promoted';
|
|
8884
|
+
`);
|
|
8885
|
+
db.exec(`
|
|
8886
|
+
CREATE TABLE IF NOT EXISTS prompt_outcomes_signal_blacklist (
|
|
8887
|
+
signal TEXT PRIMARY KEY,
|
|
8888
|
+
dismissal_count INTEGER NOT NULL DEFAULT 0,
|
|
8889
|
+
first_dismissed_at TEXT DEFAULT (datetime('now')),
|
|
8890
|
+
last_dismissed_at TEXT DEFAULT (datetime('now'))
|
|
8891
|
+
);
|
|
8892
|
+
CREATE INDEX IF NOT EXISTS idx_psb_count
|
|
8893
|
+
ON prompt_outcomes_signal_blacklist(dismissal_count DESC);
|
|
8808
8894
|
`);
|
|
8895
|
+
migrateAuditLogCheckExtension(db);
|
|
8809
8896
|
db.exec(`
|
|
8810
8897
|
CREATE TABLE IF NOT EXISTS validation_results (
|
|
8811
8898
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -175,7 +175,17 @@ var AutoLearningConfigSchema = z.object({
|
|
|
175
175
|
requireIncidentReport: z.boolean().default(true),
|
|
176
176
|
requirePreventionRule: z.boolean().default(true),
|
|
177
177
|
requireEnforcement: z.boolean().default(true)
|
|
178
|
-
}).default({})
|
|
178
|
+
}).default({}),
|
|
179
|
+
// plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
|
|
180
|
+
// custom destinations for the rule-candidate funnel. The classifier matches
|
|
181
|
+
// a candidate to one of these entries when none of the framework
|
|
182
|
+
// destinations (pattern-scanner / claude-md-cr / corrections-md) apply.
|
|
183
|
+
customDestinations: z.array(z.object({
|
|
184
|
+
name: z.string(),
|
|
185
|
+
path: z.string(),
|
|
186
|
+
triggerKeywords: z.array(z.string()).default([]),
|
|
187
|
+
template: z.string()
|
|
188
|
+
})).default([])
|
|
179
189
|
}).optional();
|
|
180
190
|
var CloudConfigSchema = z.object({
|
|
181
191
|
enabled: z.boolean().default(false),
|
|
@@ -562,6 +572,66 @@ function getMemoryDb() {
|
|
|
562
572
|
initMemorySchema(db);
|
|
563
573
|
return db;
|
|
564
574
|
}
|
|
575
|
+
function migrateAuditLogCheckExtension(db) {
|
|
576
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
577
|
+
if (!row) return;
|
|
578
|
+
const expected = [
|
|
579
|
+
"code_change",
|
|
580
|
+
"rule_enforced",
|
|
581
|
+
"approval",
|
|
582
|
+
"review",
|
|
583
|
+
"commit",
|
|
584
|
+
"compaction",
|
|
585
|
+
"rule_candidate_emitted",
|
|
586
|
+
"rule_promoted",
|
|
587
|
+
"rule_dismissed"
|
|
588
|
+
];
|
|
589
|
+
const checkClauseMatch = row.sql.match(/event_type\s+TEXT\s+NOT\s+NULL\s+CHECK\s*\(\s*event_type\s+IN\s*\(([\s\S]*?)\)\s*\)/i);
|
|
590
|
+
if (checkClauseMatch) {
|
|
591
|
+
const values = (checkClauseMatch[1].match(/'([^']+)'/g) ?? []).map((s) => s.slice(1, -1));
|
|
592
|
+
if (expected.every((v) => values.includes(v))) return;
|
|
593
|
+
}
|
|
594
|
+
db.pragma("foreign_keys = OFF");
|
|
595
|
+
try {
|
|
596
|
+
db.exec("BEGIN TRANSACTION");
|
|
597
|
+
db.exec(`
|
|
598
|
+
CREATE TABLE audit_log_new (
|
|
599
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
600
|
+
session_id TEXT NOT NULL,
|
|
601
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
602
|
+
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
603
|
+
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
604
|
+
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed'
|
|
605
|
+
)),
|
|
606
|
+
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
607
|
+
model_id TEXT,
|
|
608
|
+
file_path TEXT,
|
|
609
|
+
change_type TEXT CHECK(change_type IN ('create', 'edit', 'delete')),
|
|
610
|
+
rules_in_effect TEXT,
|
|
611
|
+
approval_status TEXT CHECK(approval_status IN ('auto_approved', 'human_approved', 'pending', 'denied')),
|
|
612
|
+
evidence TEXT,
|
|
613
|
+
metadata TEXT,
|
|
614
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
615
|
+
);
|
|
616
|
+
INSERT INTO audit_log_new SELECT * FROM audit_log;
|
|
617
|
+
DROP TABLE audit_log;
|
|
618
|
+
ALTER TABLE audit_log_new RENAME TO audit_log;
|
|
619
|
+
CREATE INDEX IF NOT EXISTS idx_al_session ON audit_log(session_id);
|
|
620
|
+
CREATE INDEX IF NOT EXISTS idx_al_file ON audit_log(file_path);
|
|
621
|
+
CREATE INDEX IF NOT EXISTS idx_al_event ON audit_log(event_type);
|
|
622
|
+
CREATE INDEX IF NOT EXISTS idx_al_timestamp ON audit_log(timestamp DESC);
|
|
623
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_rule_promoted
|
|
624
|
+
ON audit_log (event_type, json_extract(metadata, '$.prompt_hash'))
|
|
625
|
+
WHERE event_type = 'rule_promoted';
|
|
626
|
+
`);
|
|
627
|
+
db.exec("COMMIT");
|
|
628
|
+
} catch (err) {
|
|
629
|
+
db.exec("ROLLBACK");
|
|
630
|
+
throw err;
|
|
631
|
+
} finally {
|
|
632
|
+
db.pragma("foreign_keys = ON");
|
|
633
|
+
}
|
|
634
|
+
}
|
|
565
635
|
function initMemorySchema(db) {
|
|
566
636
|
db.exec(`
|
|
567
637
|
-- Sessions table (linked to Claude Code session IDs)
|
|
@@ -851,7 +921,10 @@ function initMemorySchema(db) {
|
|
|
851
921
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
852
922
|
session_id TEXT NOT NULL,
|
|
853
923
|
timestamp TEXT DEFAULT (datetime('now')),
|
|
854
|
-
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
924
|
+
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
925
|
+
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
926
|
+
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed'
|
|
927
|
+
)),
|
|
855
928
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
856
929
|
model_id TEXT,
|
|
857
930
|
file_path TEXT,
|
|
@@ -866,7 +939,21 @@ function initMemorySchema(db) {
|
|
|
866
939
|
CREATE INDEX IF NOT EXISTS idx_al_file ON audit_log(file_path);
|
|
867
940
|
CREATE INDEX IF NOT EXISTS idx_al_event ON audit_log(event_type);
|
|
868
941
|
CREATE INDEX IF NOT EXISTS idx_al_timestamp ON audit_log(timestamp DESC);
|
|
942
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_rule_promoted
|
|
943
|
+
ON audit_log (event_type, json_extract(metadata, '$.prompt_hash'))
|
|
944
|
+
WHERE event_type = 'rule_promoted';
|
|
869
945
|
`);
|
|
946
|
+
db.exec(`
|
|
947
|
+
CREATE TABLE IF NOT EXISTS prompt_outcomes_signal_blacklist (
|
|
948
|
+
signal TEXT PRIMARY KEY,
|
|
949
|
+
dismissal_count INTEGER NOT NULL DEFAULT 0,
|
|
950
|
+
first_dismissed_at TEXT DEFAULT (datetime('now')),
|
|
951
|
+
last_dismissed_at TEXT DEFAULT (datetime('now'))
|
|
952
|
+
);
|
|
953
|
+
CREATE INDEX IF NOT EXISTS idx_psb_count
|
|
954
|
+
ON prompt_outcomes_signal_blacklist(dismissal_count DESC);
|
|
955
|
+
`);
|
|
956
|
+
migrateAuditLogCheckExtension(db);
|
|
870
957
|
db.exec(`
|
|
871
958
|
CREATE TABLE IF NOT EXISTS validation_results (
|
|
872
959
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -1134,7 +1221,126 @@ function linkSessionToTask(db, sessionId, taskId) {
|
|
|
1134
1221
|
}
|
|
1135
1222
|
|
|
1136
1223
|
// src/hooks/user-prompt.ts
|
|
1137
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1224
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync, readFileSync as readFileSync2, readdirSync, openSync, fstatSync, readSync, closeSync } from "fs";
|
|
1225
|
+
import { join } from "path";
|
|
1226
|
+
|
|
1227
|
+
// src/rule-candidate-detector.ts
|
|
1228
|
+
var CORRECTION_DISMISSAL_PATTERN = /\b(nevermind|never\s+mind|forget\s+it|ignore\s+(that|it)|actually\s+you('?re|\s+were)\s+right|on\s+second\s+thought|no\s+wait|scratch\s+that|disregard|abandon\s+(that|it))\b/i;
|
|
1229
|
+
var RULE_CANDIDATE_THRESHOLD = 60;
|
|
1230
|
+
var STRONG_CORRECTION_PHRASES = [
|
|
1231
|
+
"that's wrong",
|
|
1232
|
+
"thats wrong",
|
|
1233
|
+
"that is wrong",
|
|
1234
|
+
"incorrect",
|
|
1235
|
+
"you broke",
|
|
1236
|
+
"you missed",
|
|
1237
|
+
"this is wrong",
|
|
1238
|
+
"should be",
|
|
1239
|
+
"not what i asked",
|
|
1240
|
+
"not what i wanted"
|
|
1241
|
+
];
|
|
1242
|
+
var NEGATION_TOKENS = /\b(no|not|never|don't|dont|wrong|incorrect)\b/i;
|
|
1243
|
+
var INSTRUCTION_TOKENS = /\b(instead|use\s+\w+|make\s+it|should\s+be|actually|change\s+\w+)\b/i;
|
|
1244
|
+
var SIGNAL_BASE_WEIGHTS = {
|
|
1245
|
+
strong_correction_phrase: 40,
|
|
1246
|
+
negation_plus_instruction: 30,
|
|
1247
|
+
prior_edit_or_write: 25,
|
|
1248
|
+
bugfix_or_refactor_category: 15,
|
|
1249
|
+
prompt_length_gt_10: 10,
|
|
1250
|
+
running_correction_streak: 15,
|
|
1251
|
+
short_length_floor: -50,
|
|
1252
|
+
slash_command_excluded: -100,
|
|
1253
|
+
dismissal_phrase_excluded: -100
|
|
1254
|
+
};
|
|
1255
|
+
function hasNegationPlusInstructionWithinWindow(prompt, windowWords) {
|
|
1256
|
+
const words = prompt.split(/\s+/);
|
|
1257
|
+
const negIdx = [];
|
|
1258
|
+
const instIdx = [];
|
|
1259
|
+
for (let i = 0; i < words.length; i++) {
|
|
1260
|
+
if (NEGATION_TOKENS.test(words[i])) negIdx.push(i);
|
|
1261
|
+
const slice = words.slice(i, i + 3).join(" ");
|
|
1262
|
+
if (INSTRUCTION_TOKENS.test(slice)) instIdx.push(i);
|
|
1263
|
+
}
|
|
1264
|
+
for (const n of negIdx) {
|
|
1265
|
+
for (const inst of instIdx) {
|
|
1266
|
+
if (Math.abs(n - inst) <= windowWords) return true;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
function applyDismissal(base, dismissalCount) {
|
|
1272
|
+
if (dismissalCount >= 5) return 0;
|
|
1273
|
+
if (dismissalCount > 0 && base > 0) return Math.max(0, base - dismissalCount * 10);
|
|
1274
|
+
return base;
|
|
1275
|
+
}
|
|
1276
|
+
function scoreCorrectionPrompt(inputs) {
|
|
1277
|
+
const { prompt, priorAssistantTurn, priorOutcomes, category, blacklist } = inputs;
|
|
1278
|
+
const promptLower = prompt.toLowerCase();
|
|
1279
|
+
const trimmed = prompt.trim();
|
|
1280
|
+
const words = trimmed.split(/\s+/).filter(Boolean);
|
|
1281
|
+
const signals = [];
|
|
1282
|
+
const addSignal = (name, evidence) => {
|
|
1283
|
+
const baseWeight = SIGNAL_BASE_WEIGHTS[name];
|
|
1284
|
+
const applied = applyDismissal(baseWeight, blacklist?.get(name) ?? 0);
|
|
1285
|
+
signals.push({ name, baseWeight, applied, evidence });
|
|
1286
|
+
};
|
|
1287
|
+
if (/^\/\w+/.test(trimmed)) {
|
|
1288
|
+
addSignal("slash_command_excluded", "starts with /");
|
|
1289
|
+
return { score: SIGNAL_BASE_WEIGHTS.slash_command_excluded, signals, emitCandidate: false };
|
|
1290
|
+
}
|
|
1291
|
+
if (CORRECTION_DISMISSAL_PATTERN.test(prompt)) {
|
|
1292
|
+
addSignal("dismissal_phrase_excluded", "matched CORRECTION_DISMISSAL_PATTERN");
|
|
1293
|
+
return { score: SIGNAL_BASE_WEIGHTS.dismissal_phrase_excluded, signals, emitCandidate: false };
|
|
1294
|
+
}
|
|
1295
|
+
if (words.length < 4) {
|
|
1296
|
+
addSignal("short_length_floor", `${words.length} words < 4`);
|
|
1297
|
+
}
|
|
1298
|
+
for (const phrase of STRONG_CORRECTION_PHRASES) {
|
|
1299
|
+
if (promptLower.includes(phrase)) {
|
|
1300
|
+
addSignal("strong_correction_phrase", `matched "${phrase}"`);
|
|
1301
|
+
break;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (hasNegationPlusInstructionWithinWindow(prompt, 8)) {
|
|
1305
|
+
addSignal("negation_plus_instruction", "negation + instructional token within 8-word window");
|
|
1306
|
+
}
|
|
1307
|
+
if (priorAssistantTurn?.hadEditOrWrite) {
|
|
1308
|
+
addSignal("prior_edit_or_write", "prior assistant turn contained Edit/Write/Bash");
|
|
1309
|
+
}
|
|
1310
|
+
if (category === "bugfix" || category === "refactor") {
|
|
1311
|
+
addSignal("bugfix_or_refactor_category", `category=${category}`);
|
|
1312
|
+
}
|
|
1313
|
+
if (words.length > 10) {
|
|
1314
|
+
addSignal("prompt_length_gt_10", `${words.length} words > 10`);
|
|
1315
|
+
}
|
|
1316
|
+
if (priorOutcomes && priorOutcomes.lastCorrectionsNeeded >= 1) {
|
|
1317
|
+
addSignal("running_correction_streak", `lastCorrectionsNeeded=${priorOutcomes.lastCorrectionsNeeded}`);
|
|
1318
|
+
}
|
|
1319
|
+
const score = signals.reduce((sum, s) => sum + s.applied, 0);
|
|
1320
|
+
return {
|
|
1321
|
+
score,
|
|
1322
|
+
signals,
|
|
1323
|
+
emitCandidate: score >= RULE_CANDIDATE_THRESHOLD
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/prompt-analyzer.ts
|
|
1328
|
+
import { createHash } from "crypto";
|
|
1329
|
+
function categorizePrompt(promptText) {
|
|
1330
|
+
const lower = promptText.toLowerCase();
|
|
1331
|
+
if (/\b(fix|bug|error|broken|issue|crash|fail)\b/.test(lower)) return "bugfix";
|
|
1332
|
+
if (/\b(refactor|rename|move|extract|cleanup|reorganize)\b/.test(lower)) return "refactor";
|
|
1333
|
+
if (/\b(what|how|why|where|when|explain|describe|tell me)\b/.test(lower)) return "question";
|
|
1334
|
+
if (/^\/\w+/.test(promptText.trim())) return "command";
|
|
1335
|
+
if (/\b(add|create|implement|build|new|feature)\b/.test(lower)) return "feature";
|
|
1336
|
+
return "feature";
|
|
1337
|
+
}
|
|
1338
|
+
function hashPrompt(promptText) {
|
|
1339
|
+
const normalized = promptText.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1340
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/hooks/user-prompt.ts
|
|
1138
1344
|
async function main() {
|
|
1139
1345
|
try {
|
|
1140
1346
|
const input = await readStdin();
|
|
@@ -1167,8 +1373,8 @@ async function main() {
|
|
|
1167
1373
|
if (fileRefs.length > 0) {
|
|
1168
1374
|
const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
|
|
1169
1375
|
if (knowledgeDbPath && existsSync3(knowledgeDbPath)) {
|
|
1170
|
-
const
|
|
1171
|
-
const kdb = new
|
|
1376
|
+
const BetterSqlite3Ctor = (await import("better-sqlite3")).default;
|
|
1377
|
+
const kdb = new BetterSqlite3Ctor(knowledgeDbPath, { readonly: true });
|
|
1172
1378
|
try {
|
|
1173
1379
|
const placeholders = fileRefs.map(() => "?").join(",");
|
|
1174
1380
|
const matches = kdb.prepare(
|
|
@@ -1207,6 +1413,74 @@ async function main() {
|
|
|
1207
1413
|
}
|
|
1208
1414
|
} catch (_memoryNagErr) {
|
|
1209
1415
|
}
|
|
1416
|
+
try {
|
|
1417
|
+
const priorTurn = detectPriorAssistantTurn(hookInput.transcript_path);
|
|
1418
|
+
const lastOutcome = db.prepare(
|
|
1419
|
+
"SELECT corrections_needed FROM prompt_outcomes WHERE session_id = ? ORDER BY id DESC LIMIT 1"
|
|
1420
|
+
).get(session_id);
|
|
1421
|
+
const blacklist = readSignalBlacklist(db);
|
|
1422
|
+
const scoreResult = scoreCorrectionPrompt({
|
|
1423
|
+
prompt,
|
|
1424
|
+
priorAssistantTurn: { hadEditOrWrite: priorTurn.hadEditOrWrite },
|
|
1425
|
+
priorOutcomes: lastOutcome ? { lastCorrectionsNeeded: lastOutcome.corrections_needed } : void 0,
|
|
1426
|
+
category: categorizePrompt(prompt),
|
|
1427
|
+
blacklist
|
|
1428
|
+
});
|
|
1429
|
+
const candidateDir = join(hookInput.cwd, ".massu", "rule-candidates");
|
|
1430
|
+
if (scoreResult.emitCandidate) {
|
|
1431
|
+
mkdirSync2(candidateDir, { recursive: true });
|
|
1432
|
+
const promptHash = hashPrompt(prompt);
|
|
1433
|
+
const candidatePath = join(candidateDir, `${promptHash}.json`);
|
|
1434
|
+
if (!existsSync3(candidatePath)) {
|
|
1435
|
+
writeFileSync(candidatePath, JSON.stringify({
|
|
1436
|
+
prompt,
|
|
1437
|
+
prompt_hash: promptHash,
|
|
1438
|
+
score: scoreResult.score,
|
|
1439
|
+
signals: scoreResult.signals,
|
|
1440
|
+
prior_turn_files: priorTurn.files,
|
|
1441
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1442
|
+
session_id
|
|
1443
|
+
}, null, 2));
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
if (existsSync3(candidateDir)) {
|
|
1447
|
+
const candidates = readdirSync(candidateDir).filter(
|
|
1448
|
+
(f) => f.endsWith(".json") && !f.startsWith(".")
|
|
1449
|
+
);
|
|
1450
|
+
const candidateCount = candidates.length;
|
|
1451
|
+
const surfacedPath = join(candidateDir, ".last-surfaced");
|
|
1452
|
+
let lastSurfaced = 0;
|
|
1453
|
+
if (existsSync3(surfacedPath)) {
|
|
1454
|
+
const raw = readFileSync2(surfacedPath, "utf-8").trim();
|
|
1455
|
+
const parsed = parseInt(raw, 10);
|
|
1456
|
+
if (!Number.isNaN(parsed)) lastSurfaced = parsed;
|
|
1457
|
+
}
|
|
1458
|
+
if (candidateCount > lastSurfaced) {
|
|
1459
|
+
process.stderr.write(
|
|
1460
|
+
`
|
|
1461
|
+
[RULE CANDIDATE] ${candidateCount} rule candidate(s) pending (\`/massu-rule list\`)
|
|
1462
|
+
|
|
1463
|
+
`
|
|
1464
|
+
);
|
|
1465
|
+
writeFileSync(surfacedPath, String(candidateCount));
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
} catch (candidateErr) {
|
|
1469
|
+
try {
|
|
1470
|
+
const dir = join(hookInput.cwd, ".massu", "rule-candidates");
|
|
1471
|
+
if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
|
|
1472
|
+
const logPath = join(dir, ".detector-failures.jsonl");
|
|
1473
|
+
const pre = existsSync3(logPath) ? readFileSync2(logPath, "utf-8") : "";
|
|
1474
|
+
const sep = pre && !pre.endsWith("\n") ? "\n" : "";
|
|
1475
|
+
writeFileSync(logPath, pre + sep + JSON.stringify({
|
|
1476
|
+
session_id,
|
|
1477
|
+
prompt_excerpt: prompt.slice(0, 200),
|
|
1478
|
+
error: candidateErr instanceof Error ? candidateErr.message : String(candidateErr),
|
|
1479
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1480
|
+
}) + "\n", "utf-8");
|
|
1481
|
+
} catch {
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1210
1484
|
} finally {
|
|
1211
1485
|
db.close();
|
|
1212
1486
|
}
|
|
@@ -1214,6 +1488,73 @@ async function main() {
|
|
|
1214
1488
|
}
|
|
1215
1489
|
process.exit(0);
|
|
1216
1490
|
}
|
|
1491
|
+
function detectPriorAssistantTurn(transcriptPath) {
|
|
1492
|
+
try {
|
|
1493
|
+
if (!transcriptPath || !existsSync3(transcriptPath)) {
|
|
1494
|
+
return { hadEditOrWrite: false, files: [] };
|
|
1495
|
+
}
|
|
1496
|
+
if (!transcriptPath.includes("/.claude/projects/")) {
|
|
1497
|
+
return { hadEditOrWrite: false, files: [] };
|
|
1498
|
+
}
|
|
1499
|
+
const fd = openSync(transcriptPath, "r");
|
|
1500
|
+
let buf;
|
|
1501
|
+
try {
|
|
1502
|
+
const stats = fstatSync(fd);
|
|
1503
|
+
const readLen = Math.min(stats.size, 200 * 1024);
|
|
1504
|
+
const offset = stats.size - readLen;
|
|
1505
|
+
buf = Buffer.alloc(readLen);
|
|
1506
|
+
readSync(fd, buf, 0, readLen, offset);
|
|
1507
|
+
} finally {
|
|
1508
|
+
closeSync(fd);
|
|
1509
|
+
}
|
|
1510
|
+
const lines = buf.toString("utf-8").split("\n").filter(Boolean);
|
|
1511
|
+
const files = [];
|
|
1512
|
+
let hadEditOrWrite = false;
|
|
1513
|
+
let foundAssistant = false;
|
|
1514
|
+
const WRITE_BASH = /\b(sed\s+-i|tee\s|printf\s.*?>|cat\s+<<.*?>|>>?\s*['"\w/.-]+)/;
|
|
1515
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1516
|
+
let entry = null;
|
|
1517
|
+
try {
|
|
1518
|
+
entry = JSON.parse(lines[i]);
|
|
1519
|
+
} catch {
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1523
|
+
if (entry.type === "user" && foundAssistant) break;
|
|
1524
|
+
if (entry.type !== "assistant") continue;
|
|
1525
|
+
foundAssistant = true;
|
|
1526
|
+
const content = entry.message?.content;
|
|
1527
|
+
if (!Array.isArray(content)) continue;
|
|
1528
|
+
for (const block of content) {
|
|
1529
|
+
if (!block || typeof block !== "object") continue;
|
|
1530
|
+
const b = block;
|
|
1531
|
+
if (b.type !== "tool_use") continue;
|
|
1532
|
+
const name = String(b.name ?? "");
|
|
1533
|
+
if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
|
|
1534
|
+
hadEditOrWrite = true;
|
|
1535
|
+
const fp = b.input?.file_path;
|
|
1536
|
+
if (typeof fp === "string") files.push(fp);
|
|
1537
|
+
} else if (name === "Bash") {
|
|
1538
|
+
const cmd = String(b.input?.command ?? "");
|
|
1539
|
+
if (WRITE_BASH.test(cmd)) hadEditOrWrite = true;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return { hadEditOrWrite, files: [...new Set(files)] };
|
|
1544
|
+
} catch (_e) {
|
|
1545
|
+
return { hadEditOrWrite: false, files: [] };
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function readSignalBlacklist(db) {
|
|
1549
|
+
try {
|
|
1550
|
+
const rows = db.prepare(
|
|
1551
|
+
"SELECT signal, dismissal_count FROM prompt_outcomes_signal_blacklist LIMIT 10000"
|
|
1552
|
+
).all();
|
|
1553
|
+
return new Map(rows.map((r) => [r.signal, r.dismissal_count]));
|
|
1554
|
+
} catch (_e) {
|
|
1555
|
+
return /* @__PURE__ */ new Map();
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1217
1558
|
function extractFileReferences(prompt) {
|
|
1218
1559
|
const filePattern = /(?:^|\s)((?:src|packages|lib)\/[\w./-]+\.(?:ts|tsx|js|jsx|md))/g;
|
|
1219
1560
|
const matches = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 59 workflow commands, 11 agents, 20+ patterns",
|
|
6
6
|
"main": "./dist/cli.js",
|