@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
|
@@ -181,7 +181,17 @@ var AutoLearningConfigSchema = z.object({
|
|
|
181
181
|
requireIncidentReport: z.boolean().default(true),
|
|
182
182
|
requirePreventionRule: z.boolean().default(true),
|
|
183
183
|
requireEnforcement: z.boolean().default(true)
|
|
184
|
-
}).default({})
|
|
184
|
+
}).default({}),
|
|
185
|
+
// plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
|
|
186
|
+
// custom destinations for the rule-candidate funnel. The classifier matches
|
|
187
|
+
// a candidate to one of these entries when none of the framework
|
|
188
|
+
// destinations (pattern-scanner / claude-md-cr / corrections-md) apply.
|
|
189
|
+
customDestinations: z.array(z.object({
|
|
190
|
+
name: z.string(),
|
|
191
|
+
path: z.string(),
|
|
192
|
+
triggerKeywords: z.array(z.string()).default([]),
|
|
193
|
+
template: z.string()
|
|
194
|
+
})).default([])
|
|
185
195
|
}).optional();
|
|
186
196
|
var CloudConfigSchema = z.object({
|
|
187
197
|
enabled: z.boolean().default(false),
|
|
@@ -568,6 +578,66 @@ function getMemoryDb() {
|
|
|
568
578
|
initMemorySchema(db);
|
|
569
579
|
return db;
|
|
570
580
|
}
|
|
581
|
+
function migrateAuditLogCheckExtension(db) {
|
|
582
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
583
|
+
if (!row) return;
|
|
584
|
+
const expected = [
|
|
585
|
+
"code_change",
|
|
586
|
+
"rule_enforced",
|
|
587
|
+
"approval",
|
|
588
|
+
"review",
|
|
589
|
+
"commit",
|
|
590
|
+
"compaction",
|
|
591
|
+
"rule_candidate_emitted",
|
|
592
|
+
"rule_promoted",
|
|
593
|
+
"rule_dismissed"
|
|
594
|
+
];
|
|
595
|
+
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);
|
|
596
|
+
if (checkClauseMatch) {
|
|
597
|
+
const values = (checkClauseMatch[1].match(/'([^']+)'/g) ?? []).map((s) => s.slice(1, -1));
|
|
598
|
+
if (expected.every((v) => values.includes(v))) return;
|
|
599
|
+
}
|
|
600
|
+
db.pragma("foreign_keys = OFF");
|
|
601
|
+
try {
|
|
602
|
+
db.exec("BEGIN TRANSACTION");
|
|
603
|
+
db.exec(`
|
|
604
|
+
CREATE TABLE audit_log_new (
|
|
605
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
606
|
+
session_id TEXT NOT NULL,
|
|
607
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
608
|
+
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
609
|
+
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
610
|
+
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed'
|
|
611
|
+
)),
|
|
612
|
+
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
613
|
+
model_id TEXT,
|
|
614
|
+
file_path TEXT,
|
|
615
|
+
change_type TEXT CHECK(change_type IN ('create', 'edit', 'delete')),
|
|
616
|
+
rules_in_effect TEXT,
|
|
617
|
+
approval_status TEXT CHECK(approval_status IN ('auto_approved', 'human_approved', 'pending', 'denied')),
|
|
618
|
+
evidence TEXT,
|
|
619
|
+
metadata TEXT,
|
|
620
|
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
621
|
+
);
|
|
622
|
+
INSERT INTO audit_log_new SELECT * FROM audit_log;
|
|
623
|
+
DROP TABLE audit_log;
|
|
624
|
+
ALTER TABLE audit_log_new RENAME TO audit_log;
|
|
625
|
+
CREATE INDEX IF NOT EXISTS idx_al_session ON audit_log(session_id);
|
|
626
|
+
CREATE INDEX IF NOT EXISTS idx_al_file ON audit_log(file_path);
|
|
627
|
+
CREATE INDEX IF NOT EXISTS idx_al_event ON audit_log(event_type);
|
|
628
|
+
CREATE INDEX IF NOT EXISTS idx_al_timestamp ON audit_log(timestamp DESC);
|
|
629
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_rule_promoted
|
|
630
|
+
ON audit_log (event_type, json_extract(metadata, '$.prompt_hash'))
|
|
631
|
+
WHERE event_type = 'rule_promoted';
|
|
632
|
+
`);
|
|
633
|
+
db.exec("COMMIT");
|
|
634
|
+
} catch (err) {
|
|
635
|
+
db.exec("ROLLBACK");
|
|
636
|
+
throw err;
|
|
637
|
+
} finally {
|
|
638
|
+
db.pragma("foreign_keys = ON");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
571
641
|
function initMemorySchema(db) {
|
|
572
642
|
db.exec(`
|
|
573
643
|
-- Sessions table (linked to Claude Code session IDs)
|
|
@@ -857,7 +927,10 @@ function initMemorySchema(db) {
|
|
|
857
927
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
858
928
|
session_id TEXT NOT NULL,
|
|
859
929
|
timestamp TEXT DEFAULT (datetime('now')),
|
|
860
|
-
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
930
|
+
event_type TEXT NOT NULL CHECK(event_type IN (
|
|
931
|
+
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
932
|
+
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed'
|
|
933
|
+
)),
|
|
861
934
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
862
935
|
model_id TEXT,
|
|
863
936
|
file_path TEXT,
|
|
@@ -872,7 +945,21 @@ function initMemorySchema(db) {
|
|
|
872
945
|
CREATE INDEX IF NOT EXISTS idx_al_file ON audit_log(file_path);
|
|
873
946
|
CREATE INDEX IF NOT EXISTS idx_al_event ON audit_log(event_type);
|
|
874
947
|
CREATE INDEX IF NOT EXISTS idx_al_timestamp ON audit_log(timestamp DESC);
|
|
948
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_rule_promoted
|
|
949
|
+
ON audit_log (event_type, json_extract(metadata, '$.prompt_hash'))
|
|
950
|
+
WHERE event_type = 'rule_promoted';
|
|
951
|
+
`);
|
|
952
|
+
db.exec(`
|
|
953
|
+
CREATE TABLE IF NOT EXISTS prompt_outcomes_signal_blacklist (
|
|
954
|
+
signal TEXT PRIMARY KEY,
|
|
955
|
+
dismissal_count INTEGER NOT NULL DEFAULT 0,
|
|
956
|
+
first_dismissed_at TEXT DEFAULT (datetime('now')),
|
|
957
|
+
last_dismissed_at TEXT DEFAULT (datetime('now'))
|
|
958
|
+
);
|
|
959
|
+
CREATE INDEX IF NOT EXISTS idx_psb_count
|
|
960
|
+
ON prompt_outcomes_signal_blacklist(dismissal_count DESC);
|
|
875
961
|
`);
|
|
962
|
+
migrateAuditLogCheckExtension(db);
|
|
876
963
|
db.exec(`
|
|
877
964
|
CREATE TABLE IF NOT EXISTS validation_results (
|
|
878
965
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -1878,9 +1965,67 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
|
|
|
1878
1965
|
}
|
|
1879
1966
|
|
|
1880
1967
|
// src/hooks/post-tool-use.ts
|
|
1881
|
-
import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync2 } from "fs";
|
|
1968
|
+
import { readFileSync as readFileSync6, writeFileSync, existsSync as existsSync7, mkdirSync as mkdirSync2, statSync as statSync2 } from "fs";
|
|
1882
1969
|
import { join as join2 } from "path";
|
|
1883
1970
|
|
|
1971
|
+
// src/lib/recurrence-incrementer.ts
|
|
1972
|
+
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
1973
|
+
var FAIL_LINE = /^\s+FAIL:\s*(.*)$/;
|
|
1974
|
+
var FILE_TOKEN = /[\w./-]+\.(?:ts|tsx|js|jsx|sh|md|yaml|yml|json)\b/g;
|
|
1975
|
+
function incrementRecurrenceCountsForScannerFailures(db, sessionId, scannerStdout) {
|
|
1976
|
+
const stripped = scannerStdout.replace(ANSI, "");
|
|
1977
|
+
const lines = stripped.split("\n");
|
|
1978
|
+
const failedFiles = /* @__PURE__ */ new Set();
|
|
1979
|
+
for (const line of lines) {
|
|
1980
|
+
const m = line.match(FAIL_LINE);
|
|
1981
|
+
if (!m) continue;
|
|
1982
|
+
const matches = m[1].match(FILE_TOKEN);
|
|
1983
|
+
if (matches) {
|
|
1984
|
+
for (const f of matches) failedFiles.add(f);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
if (failedFiles.size === 0) return 0;
|
|
1988
|
+
const sessionEdits = db.prepare(`
|
|
1989
|
+
SELECT DISTINCT file_path FROM audit_log
|
|
1990
|
+
WHERE session_id = ?
|
|
1991
|
+
AND event_type = 'code_change'
|
|
1992
|
+
AND change_type IN ('edit', 'create')
|
|
1993
|
+
AND timestamp > datetime('now', '-24 hours')
|
|
1994
|
+
LIMIT 10000
|
|
1995
|
+
`).all(sessionId);
|
|
1996
|
+
const sessionFiles = new Set(
|
|
1997
|
+
sessionEdits.map((r) => r.file_path).filter((p) => Boolean(p))
|
|
1998
|
+
);
|
|
1999
|
+
if (sessionFiles.size === 0) return 0;
|
|
2000
|
+
const intersection = /* @__PURE__ */ new Set();
|
|
2001
|
+
for (const f of failedFiles) {
|
|
2002
|
+
if (sessionFiles.has(f)) {
|
|
2003
|
+
intersection.add(f);
|
|
2004
|
+
} else {
|
|
2005
|
+
for (const sf of sessionFiles) {
|
|
2006
|
+
if (sf.endsWith("/" + f) || f.endsWith("/" + sf)) {
|
|
2007
|
+
intersection.add(sf);
|
|
2008
|
+
break;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
if (intersection.size === 0) return 0;
|
|
2014
|
+
const updateStmt = db.prepare(`
|
|
2015
|
+
UPDATE audit_log
|
|
2016
|
+
SET metadata = json_set(metadata, '$.recurrence_count', COALESCE(json_extract(metadata, '$.recurrence_count'), 0) + 1)
|
|
2017
|
+
WHERE event_type = 'rule_promoted'
|
|
2018
|
+
AND file_path = ?
|
|
2019
|
+
AND timestamp > datetime('now', '-7 days')
|
|
2020
|
+
`);
|
|
2021
|
+
let updates = 0;
|
|
2022
|
+
for (const f of intersection) {
|
|
2023
|
+
const r = updateStmt.run(f);
|
|
2024
|
+
updates += r.changes;
|
|
2025
|
+
}
|
|
2026
|
+
return updates;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1884
2029
|
// src/memory-file-ingest.ts
|
|
1885
2030
|
import { readFileSync as readFileSync5, existsSync as existsSync6, readdirSync } from "fs";
|
|
1886
2031
|
import { parse as parseYaml2 } from "yaml";
|
|
@@ -2088,6 +2233,31 @@ async function main() {
|
|
|
2088
2233
|
}
|
|
2089
2234
|
} catch (_knowledgeErr) {
|
|
2090
2235
|
}
|
|
2236
|
+
try {
|
|
2237
|
+
if (tool_name === "Bash") {
|
|
2238
|
+
const command = tool_input.command ?? "";
|
|
2239
|
+
if (/massu-pattern-scanner\.sh/.test(command)) {
|
|
2240
|
+
const stdout = tool_response ?? "";
|
|
2241
|
+
incrementRecurrenceCountsForScannerFailures(db, session_id, stdout);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
} catch (err) {
|
|
2245
|
+
try {
|
|
2246
|
+
const projectRoot = process.cwd();
|
|
2247
|
+
const dir = join2(projectRoot, ".massu", "rule-candidates");
|
|
2248
|
+
if (!existsSync7(dir)) mkdirSync2(dir, { recursive: true });
|
|
2249
|
+
const logPath = join2(dir, ".cr53-increment-failures.jsonl");
|
|
2250
|
+
const pre = existsSync7(logPath) ? readFileSync6(logPath, "utf-8") : "";
|
|
2251
|
+
const sep = pre && !pre.endsWith("\n") ? "\n" : "";
|
|
2252
|
+
writeFileSync(logPath, pre + sep + JSON.stringify({
|
|
2253
|
+
session_id,
|
|
2254
|
+
scanner_output_excerpt: (tool_response ?? "").slice(0, 200),
|
|
2255
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2256
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2257
|
+
}) + "\n", "utf-8");
|
|
2258
|
+
} catch {
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2091
2261
|
} finally {
|
|
2092
2262
|
db.close();
|
|
2093
2263
|
}
|
|
@@ -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';
|
|
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);
|
|
869
955
|
`);
|
|
956
|
+
migrateAuditLogCheckExtension(db);
|
|
870
957
|
db.exec(`
|
|
871
958
|
CREATE TABLE IF NOT EXISTS validation_results (
|
|
872
959
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -174,7 +174,17 @@ var AutoLearningConfigSchema = z.object({
|
|
|
174
174
|
requireIncidentReport: z.boolean().default(true),
|
|
175
175
|
requirePreventionRule: z.boolean().default(true),
|
|
176
176
|
requireEnforcement: z.boolean().default(true)
|
|
177
|
-
}).default({})
|
|
177
|
+
}).default({}),
|
|
178
|
+
// plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
|
|
179
|
+
// custom destinations for the rule-candidate funnel. The classifier matches
|
|
180
|
+
// a candidate to one of these entries when none of the framework
|
|
181
|
+
// destinations (pattern-scanner / claude-md-cr / corrections-md) apply.
|
|
182
|
+
customDestinations: z.array(z.object({
|
|
183
|
+
name: z.string(),
|
|
184
|
+
path: z.string(),
|
|
185
|
+
triggerKeywords: z.array(z.string()).default([]),
|
|
186
|
+
template: z.string()
|
|
187
|
+
})).default([])
|
|
178
188
|
}).optional();
|
|
179
189
|
var CloudConfigSchema = z.object({
|
|
180
190
|
enabled: z.boolean().default(false),
|
|
@@ -306,7 +306,17 @@ var AutoLearningConfigSchema = z.object({
|
|
|
306
306
|
requireIncidentReport: z.boolean().default(true),
|
|
307
307
|
requirePreventionRule: z.boolean().default(true),
|
|
308
308
|
requireEnforcement: z.boolean().default(true)
|
|
309
|
-
}).default({})
|
|
309
|
+
}).default({}),
|
|
310
|
+
// plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
|
|
311
|
+
// custom destinations for the rule-candidate funnel. The classifier matches
|
|
312
|
+
// a candidate to one of these entries when none of the framework
|
|
313
|
+
// destinations (pattern-scanner / claude-md-cr / corrections-md) apply.
|
|
314
|
+
customDestinations: z.array(z.object({
|
|
315
|
+
name: z.string(),
|
|
316
|
+
path: z.string(),
|
|
317
|
+
triggerKeywords: z.array(z.string()).default([]),
|
|
318
|
+
template: z.string()
|
|
319
|
+
})).default([])
|
|
310
320
|
}).optional();
|
|
311
321
|
var CloudConfigSchema = z.object({
|
|
312
322
|
enabled: z.boolean().default(false),
|
|
@@ -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';
|
|
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);
|
|
869
955
|
`);
|
|
956
|
+
migrateAuditLogCheckExtension(db);
|
|
870
957
|
db.exec(`
|
|
871
958
|
CREATE TABLE IF NOT EXISTS validation_results (
|
|
872
959
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -166,7 +166,17 @@ var AutoLearningConfigSchema = z.object({
|
|
|
166
166
|
requireIncidentReport: z.boolean().default(true),
|
|
167
167
|
requirePreventionRule: z.boolean().default(true),
|
|
168
168
|
requireEnforcement: z.boolean().default(true)
|
|
169
|
-
}).default({})
|
|
169
|
+
}).default({}),
|
|
170
|
+
// plan-v0.2-interactive-rule-approval P-D-008 / P-D-009: project-configured
|
|
171
|
+
// custom destinations for the rule-candidate funnel. The classifier matches
|
|
172
|
+
// a candidate to one of these entries when none of the framework
|
|
173
|
+
// destinations (pattern-scanner / claude-md-cr / corrections-md) apply.
|
|
174
|
+
customDestinations: z.array(z.object({
|
|
175
|
+
name: z.string(),
|
|
176
|
+
path: z.string(),
|
|
177
|
+
triggerKeywords: z.array(z.string()).default([]),
|
|
178
|
+
template: z.string()
|
|
179
|
+
})).default([])
|
|
170
180
|
}).optional();
|
|
171
181
|
var CloudConfigSchema = z.object({
|
|
172
182
|
enabled: z.boolean().default(false),
|
|
@@ -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';
|
|
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);
|
|
869
955
|
`);
|
|
956
|
+
migrateAuditLogCheckExtension(db);
|
|
870
957
|
db.exec(`
|
|
871
958
|
CREATE TABLE IF NOT EXISTS validation_results (
|
|
872
959
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|