@massu/core 1.10.8 → 1.12.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 +1334 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +581 -448
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +67 -19
- package/dist/hooks/pre-compact.js +28 -3
- package/dist/hooks/pre-delete-check.js +53 -27
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +31 -6
- package/dist/hooks/session-start.js +8709 -8459
- package/dist/hooks/user-prompt.js +25 -1
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
-
|
|
5
|
-
// ============================================================
|
|
6
|
-
// PreToolUse Hook: Pre-Deletion Feature Impact Check
|
|
7
|
-
// Detects file deletion patterns (rm, git rm, Write with empty content)
|
|
8
|
-
// and runs sentinel impact analysis. Blocks if critical features orphaned.
|
|
9
|
-
// Also protects knowledge system files from accidental deletion (P7-005).
|
|
10
|
-
// Must complete in <500ms.
|
|
11
|
-
// ============================================================
|
|
12
|
-
|
|
13
|
-
import Database from 'better-sqlite3';
|
|
14
|
-
import { resolve } from 'path';
|
|
15
|
-
import { existsSync } from 'fs';
|
|
16
|
-
import { getFeatureImpact } from '../sentinel-db.ts';
|
|
17
|
-
import { getProjectRoot, getResolvedPaths } from '../config.ts';
|
|
18
|
-
import { t } from '../lib/sql-table-names.ts';
|
|
19
|
-
|
|
20
|
-
interface HookInput {
|
|
21
|
-
session_id: string;
|
|
22
|
-
tool_name: string;
|
|
23
|
-
tool_input: {
|
|
24
|
-
command?: string;
|
|
25
|
-
file_path?: string;
|
|
26
|
-
content?: string;
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const PROJECT_ROOT = getProjectRoot();
|
|
31
|
-
|
|
32
|
-
// Knowledge system files that must never be silently deleted.
|
|
33
|
-
// These files underpin knowledge indexing and memory retrieval.
|
|
34
|
-
const KNOWLEDGE_PROTECTED_FILES = [
|
|
35
|
-
'knowledge-db.ts',
|
|
36
|
-
'knowledge-indexer.ts',
|
|
37
|
-
'knowledge-tools.ts',
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
function getDataDb(): Database.Database | null {
|
|
41
|
-
const dbPath = getResolvedPaths().dataDbPath;
|
|
42
|
-
if (!existsSync(dbPath)) return null;
|
|
43
|
-
try {
|
|
44
|
-
const db = new Database(dbPath, { readonly: true });
|
|
45
|
-
db.pragma('journal_mode = WAL');
|
|
46
|
-
return db;
|
|
47
|
-
} catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* P7-005: Check if the tool call targets a protected knowledge system file.
|
|
54
|
-
* Returns a warning message if a protected file would be deleted/emptied.
|
|
55
|
-
*/
|
|
56
|
-
function checkKnowledgeFileProtection(input: HookInput): string | null {
|
|
57
|
-
const candidateFiles: string[] = [];
|
|
58
|
-
|
|
59
|
-
if (input.tool_name === 'Bash' && input.tool_input.command) {
|
|
60
|
-
const cmd = input.tool_input.command;
|
|
61
|
-
const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
|
|
62
|
-
if (rmMatch) {
|
|
63
|
-
const parts = rmMatch[1].split(/\s+/).filter(p => !p.startsWith('-'));
|
|
64
|
-
candidateFiles.push(...parts);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (input.tool_name === 'Write' && input.tool_input.file_path) {
|
|
69
|
-
const content = input.tool_input.content || '';
|
|
70
|
-
if (content.trim().length === 0) {
|
|
71
|
-
candidateFiles.push(input.tool_input.file_path);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (const f of candidateFiles) {
|
|
76
|
-
const basename = f.split('/').pop() ?? f;
|
|
77
|
-
if (KNOWLEDGE_PROTECTED_FILES.includes(basename)) {
|
|
78
|
-
return (
|
|
79
|
-
`KNOWLEDGE SYSTEM PROTECTION: "${basename}" is a core knowledge system file. ` +
|
|
80
|
-
`Deleting it will break knowledge indexing and memory retrieval. ` +
|
|
81
|
-
`Create a replacement before removing.`
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function extractDeletedFiles(input: HookInput): string[] {
|
|
90
|
-
const files: string[] = [];
|
|
91
|
-
|
|
92
|
-
if (input.tool_name === 'Bash' && input.tool_input.command) {
|
|
93
|
-
const cmd = input.tool_input.command;
|
|
94
|
-
|
|
95
|
-
// Detect rm commands
|
|
96
|
-
const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
|
|
97
|
-
if (rmMatch) {
|
|
98
|
-
const paths = rmMatch[1].split(/\s+/).filter(p => !p.startsWith('-'));
|
|
99
|
-
for (const p of paths) {
|
|
100
|
-
const relPath = p.startsWith('src/') ? p : p.replace(PROJECT_ROOT + '/', '');
|
|
101
|
-
if (relPath.startsWith('src/') || relPath.endsWith('.py')) {
|
|
102
|
-
files.push(relPath);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Detect Write tool with empty content (file replacement that empties)
|
|
109
|
-
if (input.tool_name === 'Write' && input.tool_input.file_path) {
|
|
110
|
-
const content = input.tool_input.content || '';
|
|
111
|
-
if (content.trim().length === 0) {
|
|
112
|
-
const relPath = input.tool_input.file_path.replace(PROJECT_ROOT + '/', '');
|
|
113
|
-
if (relPath.startsWith('src/') || relPath.endsWith('.py')) {
|
|
114
|
-
files.push(relPath);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return files;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function main(): Promise<void> {
|
|
123
|
-
try {
|
|
124
|
-
const input = await readStdin();
|
|
125
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
126
|
-
|
|
127
|
-
// P7-005: Check knowledge file protection before anything else
|
|
128
|
-
const knowledgeWarning = checkKnowledgeFileProtection(hookInput);
|
|
129
|
-
if (knowledgeWarning) {
|
|
130
|
-
process.stdout.write(JSON.stringify({ message: knowledgeWarning }));
|
|
131
|
-
process.exit(0);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const deletedFiles = extractDeletedFiles(hookInput);
|
|
136
|
-
if (deletedFiles.length === 0) {
|
|
137
|
-
process.exit(0);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const db = getDataDb();
|
|
142
|
-
if (!db) {
|
|
143
|
-
// No database available - can't check
|
|
144
|
-
process.exit(0);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// The sentinel registry table name (defined by sentinel-db schema).
|
|
149
|
-
// P-H032: now config-driven via t() helper from lib/sql-table-names.ts.
|
|
150
|
-
const SENTINEL_TABLE = t('sentinel');
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
// Check if any sentinel tables exist
|
|
154
|
-
const tableExists = db.prepare(
|
|
155
|
-
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`
|
|
156
|
-
).get(SENTINEL_TABLE);
|
|
157
|
-
|
|
158
|
-
if (!tableExists) {
|
|
159
|
-
process.exit(0);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const impact = getFeatureImpact(db, deletedFiles);
|
|
164
|
-
|
|
165
|
-
if (impact.blocked) {
|
|
166
|
-
const msg = [
|
|
167
|
-
`SENTINEL IMPACT WARNING: Deleting ${deletedFiles.length} file(s) would affect features:`,
|
|
168
|
-
'',
|
|
169
|
-
];
|
|
170
|
-
|
|
171
|
-
if (impact.orphaned.length > 0) {
|
|
172
|
-
msg.push(`ORPHANED (${impact.orphaned.length} features - no primary components left):`);
|
|
173
|
-
for (const item of impact.orphaned) {
|
|
174
|
-
msg.push(` - ${item.feature.feature_key} [${item.feature.priority}]: ${item.feature.title}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (impact.degraded.length > 0) {
|
|
179
|
-
msg.push(`DEGRADED (${impact.degraded.length} features - some components removed):`);
|
|
180
|
-
for (const item of impact.degraded) {
|
|
181
|
-
msg.push(` - ${item.feature.feature_key}: ${item.feature.title}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
msg.push('');
|
|
186
|
-
msg.push('Create a migration plan before deleting these files.');
|
|
187
|
-
|
|
188
|
-
// Output warning but don't block (user can proceed)
|
|
189
|
-
process.stdout.write(JSON.stringify({ message: msg.join('\n') }));
|
|
190
|
-
}
|
|
191
|
-
// Check Python import graph for deleted .py files
|
|
192
|
-
const pyFiles = deletedFiles.filter(f => f.endsWith('.py'));
|
|
193
|
-
if (pyFiles.length > 0) {
|
|
194
|
-
try {
|
|
195
|
-
for (const pyFile of pyFiles) {
|
|
196
|
-
const importers = db.prepare(
|
|
197
|
-
`SELECT source_file FROM ${t('py_imports')} WHERE target_file = ?`
|
|
198
|
-
).all(pyFile) as { source_file: string }[];
|
|
199
|
-
|
|
200
|
-
const routes = db.prepare(
|
|
201
|
-
`SELECT method, path FROM ${t('py_routes')} WHERE file = ?`
|
|
202
|
-
).all(pyFile) as { method: string; path: string }[];
|
|
203
|
-
|
|
204
|
-
const models = db.prepare(
|
|
205
|
-
`SELECT class_name FROM ${t('py_models')} WHERE file = ?`
|
|
206
|
-
).all(pyFile) as { class_name: string }[];
|
|
207
|
-
|
|
208
|
-
if (importers.length > 0 || routes.length > 0 || models.length > 0) {
|
|
209
|
-
const parts: string[] = [];
|
|
210
|
-
if (importers.length > 0) parts.push(`imported by ${importers.length} files`);
|
|
211
|
-
if (routes.length > 0) parts.push(`defines ${routes.length} routes`);
|
|
212
|
-
if (models.length > 0) parts.push(`defines ${models.length} models`);
|
|
213
|
-
|
|
214
|
-
const msg = `PYTHON IMPACT: "${pyFile}" ${parts.join(', ')}. Check dependents before deleting.`;
|
|
215
|
-
process.stdout.write(JSON.stringify({ message: msg }));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
} catch {
|
|
219
|
-
// Python tables may not exist yet
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
} finally {
|
|
223
|
-
db.close();
|
|
224
|
-
}
|
|
225
|
-
} catch {
|
|
226
|
-
// Hooks must never crash
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
process.exit(0);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function readStdin(): Promise<string> {
|
|
233
|
-
return new Promise((resolve) => {
|
|
234
|
-
let data = '';
|
|
235
|
-
process.stdin.setEncoding('utf-8');
|
|
236
|
-
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
237
|
-
process.stdin.on('end', () => resolve(data));
|
|
238
|
-
// Timeout to prevent hanging
|
|
239
|
-
setTimeout(() => resolve(data), 400);
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
main();
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
-
|
|
5
|
-
// ============================================================
|
|
6
|
-
// PostToolUse Hook: Quality Event Recorder
|
|
7
|
-
// Parses tool responses for quality signals (test failures,
|
|
8
|
-
// type errors, build failures) and records them for analytics.
|
|
9
|
-
// Must complete in <500ms.
|
|
10
|
-
// ============================================================
|
|
11
|
-
|
|
12
|
-
import { getMemoryDb } from '../memory-db.ts';
|
|
13
|
-
|
|
14
|
-
interface HookInput {
|
|
15
|
-
session_id: string;
|
|
16
|
-
transcript_path: string;
|
|
17
|
-
cwd: string;
|
|
18
|
-
hook_event_name: string;
|
|
19
|
-
tool_name: string;
|
|
20
|
-
tool_input: Record<string, unknown>;
|
|
21
|
-
tool_response: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface QualitySignal {
|
|
25
|
-
event_type: string;
|
|
26
|
-
details: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const TEST_FAILURE_PATTERNS: RegExp[] = [
|
|
30
|
-
/\bFAIL\b/,
|
|
31
|
-
/✗/,
|
|
32
|
-
/\bfailed\b/i,
|
|
33
|
-
/\bError:/,
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const TYPE_ERROR_PATTERNS: RegExp[] = [
|
|
37
|
-
/error TS\d+/,
|
|
38
|
-
/\btsc\b.*error/i,
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
const BUILD_FAILURE_PATTERNS: RegExp[] = [
|
|
42
|
-
/Build failed/i,
|
|
43
|
-
/\besbuild\b.*error/i,
|
|
44
|
-
/\besbuild\b.*failed/i,
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
function detectQualitySignals(toolResponse: string): QualitySignal[] {
|
|
48
|
-
const signals: QualitySignal[] = [];
|
|
49
|
-
const response = toolResponse ?? '';
|
|
50
|
-
|
|
51
|
-
for (const pattern of TEST_FAILURE_PATTERNS) {
|
|
52
|
-
if (pattern.test(response)) {
|
|
53
|
-
const match = response.match(pattern);
|
|
54
|
-
signals.push({
|
|
55
|
-
event_type: 'test_failure',
|
|
56
|
-
details: match ? match[0].slice(0, 500) : 'Test failure detected',
|
|
57
|
-
});
|
|
58
|
-
break; // One signal per category
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
for (const pattern of TYPE_ERROR_PATTERNS) {
|
|
63
|
-
if (pattern.test(response)) {
|
|
64
|
-
const match = response.match(pattern);
|
|
65
|
-
signals.push({
|
|
66
|
-
event_type: 'type_error',
|
|
67
|
-
details: match ? match[0].slice(0, 500) : 'TypeScript error detected',
|
|
68
|
-
});
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
for (const pattern of BUILD_FAILURE_PATTERNS) {
|
|
74
|
-
if (pattern.test(response)) {
|
|
75
|
-
const match = response.match(pattern);
|
|
76
|
-
signals.push({
|
|
77
|
-
event_type: 'build_failure',
|
|
78
|
-
details: match ? match[0].slice(0, 500) : 'Build failure detected',
|
|
79
|
-
});
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return signals;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function main(): Promise<void> {
|
|
88
|
-
try {
|
|
89
|
-
const input = await readStdin();
|
|
90
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
91
|
-
const { session_id, tool_name, tool_response } = hookInput;
|
|
92
|
-
|
|
93
|
-
const signals = detectQualitySignals(tool_response);
|
|
94
|
-
if (signals.length === 0) {
|
|
95
|
-
process.exit(0);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const db = getMemoryDb();
|
|
100
|
-
try {
|
|
101
|
-
const stmt = db.prepare(`
|
|
102
|
-
INSERT INTO quality_events (session_id, event_type, tool_name, details)
|
|
103
|
-
VALUES (?, ?, ?, ?)
|
|
104
|
-
`);
|
|
105
|
-
for (const signal of signals) {
|
|
106
|
-
stmt.run(session_id, signal.event_type, tool_name, signal.details);
|
|
107
|
-
}
|
|
108
|
-
} finally {
|
|
109
|
-
db.close();
|
|
110
|
-
}
|
|
111
|
-
} catch (_e) {
|
|
112
|
-
// Best-effort: never block Claude Code
|
|
113
|
-
}
|
|
114
|
-
process.exit(0);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function readStdin(): Promise<string> {
|
|
118
|
-
return new Promise((resolve) => {
|
|
119
|
-
let data = '';
|
|
120
|
-
process.stdin.setEncoding('utf-8');
|
|
121
|
-
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
122
|
-
process.stdin.on('end', () => resolve(data));
|
|
123
|
-
setTimeout(() => resolve(data), 3000);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
main();
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
-
|
|
5
|
-
// ============================================================
|
|
6
|
-
// PostToolUse Hook: Rule-to-Enforcement Pipeline
|
|
7
|
-
// When a new prevention rule (feedback_*.md) is written,
|
|
8
|
-
// automatically triggers the final step: enforcement placement.
|
|
9
|
-
//
|
|
10
|
-
// Part of the Auto-Learning Pipeline:
|
|
11
|
-
// Fix Detected → Incident Report → Rule Derived → [ENFORCEMENT]
|
|
12
|
-
//
|
|
13
|
-
// Triggers on: Write to memory/feedback_*.md (configurable)
|
|
14
|
-
// Must complete in <500ms.
|
|
15
|
-
// ============================================================
|
|
16
|
-
|
|
17
|
-
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
18
|
-
import { basename, resolve } from 'path';
|
|
19
|
-
import { getProjectRoot, getConfig } from '../config.ts';
|
|
20
|
-
|
|
21
|
-
interface HookInput {
|
|
22
|
-
session_id: string;
|
|
23
|
-
tool_name: string;
|
|
24
|
-
tool_input: { file_path?: string; content?: string };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function main(): Promise<void> {
|
|
28
|
-
try {
|
|
29
|
-
const input = await readStdin();
|
|
30
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
31
|
-
const filePath = hookInput.tool_input?.file_path;
|
|
32
|
-
|
|
33
|
-
if (!filePath) {
|
|
34
|
-
process.exit(0);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const config = getConfig();
|
|
39
|
-
if (config.autoLearning?.enabled === false) {
|
|
40
|
-
process.exit(0);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const root = getProjectRoot();
|
|
45
|
-
const memoryDir = config.autoLearning?.memoryDir ?? 'memory';
|
|
46
|
-
const enforcementDir = config.autoLearning?.enforcementHooksDir ?? 'scripts/hooks';
|
|
47
|
-
// Only trigger on feedback rule files
|
|
48
|
-
const relPath = filePath.startsWith(root + '/') ? filePath.slice(root.length + 1) : filePath;
|
|
49
|
-
const fileName = basename(filePath);
|
|
50
|
-
|
|
51
|
-
// Match feedback_*.md in either relative or absolute memory paths
|
|
52
|
-
if (!fileName.startsWith('feedback_') || !fileName.endsWith('.md')) {
|
|
53
|
-
process.exit(0);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Must be in a memory-like directory
|
|
58
|
-
const claudeDir = config.conventions?.claudeDirName ?? '.claude';
|
|
59
|
-
if (!relPath.includes(memoryDir) && !relPath.includes('memory/') && !relPath.includes(claudeDir + '/')) {
|
|
60
|
-
process.exit(0);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!existsSync(filePath)) {
|
|
65
|
-
process.exit(0);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (config.autoLearning?.pipeline?.requireEnforcement === false) {
|
|
70
|
-
process.exit(0);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Extract rule details from the file
|
|
75
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
76
|
-
const nameMatch = content.match(/^name:\s*(.+)/m);
|
|
77
|
-
const descMatch = content.match(/^description:\s*(.+)/m);
|
|
78
|
-
const ruleName = nameMatch?.[1]?.trim() ?? fileName;
|
|
79
|
-
const ruleDesc = descMatch?.[1]?.trim() ?? '';
|
|
80
|
-
|
|
81
|
-
// Check if this rule already has enforcement in any hook file
|
|
82
|
-
const enforcementDirAbs = resolve(root, enforcementDir);
|
|
83
|
-
let hasEnforcement = false;
|
|
84
|
-
if (existsSync(enforcementDirAbs)) {
|
|
85
|
-
const hookFiles = readdirSync(enforcementDirAbs).filter(f => f.endsWith('.sh') || f.endsWith('.ts') || f.endsWith('.js'));
|
|
86
|
-
for (const hookFile of hookFiles) {
|
|
87
|
-
try {
|
|
88
|
-
const hookContent = readFileSync(resolve(enforcementDirAbs, hookFile), 'utf-8');
|
|
89
|
-
if (hookContent.includes(fileName)) {
|
|
90
|
-
hasEnforcement = true;
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
} catch { /* ignore read errors */ }
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (hasEnforcement) {
|
|
98
|
-
// Already has enforcement — no action needed
|
|
99
|
-
process.exit(0);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Output enforcement placement instructions
|
|
104
|
-
const lines: string[] = [];
|
|
105
|
-
lines.push('');
|
|
106
|
-
lines.push('============================================================================');
|
|
107
|
-
lines.push(' AUTO-LEARNING: New Rule Created — Enforcement Placement Required');
|
|
108
|
-
lines.push('============================================================================');
|
|
109
|
-
lines.push('');
|
|
110
|
-
lines.push(` Rule: ${ruleName}`);
|
|
111
|
-
lines.push(` File: ${filePath}`);
|
|
112
|
-
if (ruleDesc) {
|
|
113
|
-
lines.push(` Description: ${ruleDesc}`);
|
|
114
|
-
}
|
|
115
|
-
lines.push('');
|
|
116
|
-
lines.push(' This rule has NO automated enforcement yet. Add it now.');
|
|
117
|
-
lines.push('');
|
|
118
|
-
lines.push(' ANALYZE the rule and determine enforcement layer(s):');
|
|
119
|
-
lines.push('');
|
|
120
|
-
lines.push(' 1. STATICALLY DETECTABLE? (grep/regex can find violations in code)');
|
|
121
|
-
lines.push(` → Add check to: ${enforcementDir}/pattern-feedback hook`);
|
|
122
|
-
lines.push(` → Also add to pre-commit hook if critical`);
|
|
123
|
-
lines.push('');
|
|
124
|
-
lines.push(' 2. ABOUT EDITING CERTAIN FILES? (auth, infra, routers, etc.)');
|
|
125
|
-
lines.push(` → Add warning to: ${enforcementDir}/blast-radius hook`);
|
|
126
|
-
lines.push('');
|
|
127
|
-
lines.push(' 3. ABOUT DANGEROUS COMMANDS? (kill, rm, destructive ops)');
|
|
128
|
-
lines.push(` → Add block to: ${enforcementDir}/dangerous-command hook`);
|
|
129
|
-
lines.push('');
|
|
130
|
-
lines.push(' 4. NEEDS RUNTIME MONITORING? (can only be detected at runtime)');
|
|
131
|
-
lines.push(' → Create a monitoring/audit producer');
|
|
132
|
-
lines.push('');
|
|
133
|
-
lines.push(' 5. AI-GUIDANCE ONLY? (philosophy, process, judgment calls)');
|
|
134
|
-
lines.push(' → Memory rule is sufficient (already created)');
|
|
135
|
-
lines.push('');
|
|
136
|
-
lines.push(' AFTER adding enforcement, test the hook to verify it detects violations.');
|
|
137
|
-
lines.push('');
|
|
138
|
-
lines.push(' This step is MANDATORY per the auto-learning pipeline.');
|
|
139
|
-
lines.push('============================================================================');
|
|
140
|
-
lines.push('');
|
|
141
|
-
|
|
142
|
-
console.log(lines.join('\n'));
|
|
143
|
-
} catch {
|
|
144
|
-
// Best-effort: never block Claude Code
|
|
145
|
-
}
|
|
146
|
-
process.exit(0);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function readStdin(): Promise<string> {
|
|
150
|
-
return new Promise((resolve) => {
|
|
151
|
-
let data = '';
|
|
152
|
-
process.stdin.setEncoding('utf-8');
|
|
153
|
-
process.stdin.on('data', (chunk: string) => { data += chunk; });
|
|
154
|
-
process.stdin.on('end', () => resolve(data));
|
|
155
|
-
setTimeout(() => resolve(data), 3000);
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
main();
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
-
|
|
5
|
-
// ============================================================
|
|
6
|
-
// PreToolUse Hook: Security Gate
|
|
7
|
-
// Validates tool calls against security policies.
|
|
8
|
-
// Checks Bash commands for dangerous patterns and Write/Edit
|
|
9
|
-
// tool calls for protected file paths.
|
|
10
|
-
// Must complete in <500ms.
|
|
11
|
-
// ============================================================
|
|
12
|
-
|
|
13
|
-
// Force module mode for TypeScript (no external deps needed)
|
|
14
|
-
export {};
|
|
15
|
-
|
|
16
|
-
interface HookInput {
|
|
17
|
-
session_id: string;
|
|
18
|
-
tool_name: string;
|
|
19
|
-
tool_input: {
|
|
20
|
-
command?: string;
|
|
21
|
-
file_path?: string;
|
|
22
|
-
content?: string;
|
|
23
|
-
new_string?: string;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const DANGEROUS_BASH_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
|
|
28
|
-
{ pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: 'rm -rf /' },
|
|
29
|
-
{ pattern: /rm\s+-[a-z]*f[a-z]*r[a-z]*\s+\/(?:\s|$)/, label: 'rm -rf /' },
|
|
30
|
-
{ pattern: /curl\s+.*\|\s*(?:bash|sh|zsh)/, label: 'curl | bash (remote code execution)' },
|
|
31
|
-
{ pattern: /wget\s+.*\|\s*(?:bash|sh|zsh)/, label: 'wget | bash (remote code execution)' },
|
|
32
|
-
{ pattern: /chmod\s+777/, label: 'chmod 777 (world-writable permissions)' },
|
|
33
|
-
{ pattern: /chmod\s+-R\s+777/, label: 'chmod -R 777 (world-writable permissions)' },
|
|
34
|
-
{ pattern: />\s*\/etc\/passwd/, label: 'write to /etc/passwd' },
|
|
35
|
-
{ pattern: />\s*\/etc\/shadow/, label: 'write to /etc/shadow' },
|
|
36
|
-
{ pattern: />\s*\/etc\/sudoers/, label: 'write to /etc/sudoers' },
|
|
37
|
-
{ pattern: /dd\s+if=.*of=\/dev\/(?:sda|sdb|hda|hdb|nvme)/, label: 'dd to raw device' },
|
|
38
|
-
{ pattern: /mkfs\s+\/dev\//, label: 'format disk device' },
|
|
39
|
-
{ pattern: /:\(\)\s*\{\s*:\|:\s*&\s*\}/, label: 'fork bomb' },
|
|
40
|
-
{ pattern: /eval\s+.*\$\(.*curl/, label: 'eval with remote curl' },
|
|
41
|
-
{ pattern: /base64\s+-d\s+.*\|\s*(?:bash|sh|zsh)/, label: 'base64 decoded shell exec' },
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const PROTECTED_FILE_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
|
|
45
|
-
{ pattern: /\.env$/, label: '.env file' },
|
|
46
|
-
{ pattern: /\.env\./, label: '.env.* file' },
|
|
47
|
-
{ pattern: /credentials(?:\.json)?$/, label: 'credentials file' },
|
|
48
|
-
{ pattern: /\.pem$/, label: '.pem certificate/key file' },
|
|
49
|
-
{ pattern: /\.key$/, label: '.key file' },
|
|
50
|
-
{ pattern: /\.p12$/, label: '.p12 keystore file' },
|
|
51
|
-
{ pattern: /\.pfx$/, label: '.pfx keystore file' },
|
|
52
|
-
{ pattern: /id_rsa$/, label: 'RSA private key' },
|
|
53
|
-
{ pattern: /id_ed25519$/, label: 'Ed25519 private key' },
|
|
54
|
-
{ pattern: /id_ecdsa$/, label: 'ECDSA private key' },
|
|
55
|
-
{ pattern: /\.ssh\/config$/, label: 'SSH config file' },
|
|
56
|
-
{ pattern: /secrets\.yaml$/, label: 'secrets.yaml file' },
|
|
57
|
-
{ pattern: /secrets\.yml$/, label: 'secrets.yml file' },
|
|
58
|
-
{ pattern: /\.netrc$/, label: '.netrc credentials file' },
|
|
59
|
-
{ pattern: /aws\/credentials$/, label: 'AWS credentials file' },
|
|
60
|
-
{ pattern: /kubeconfig$/, label: 'Kubernetes config file' },
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
function checkBashCommand(command: string): string | null {
|
|
64
|
-
for (const { pattern, label } of DANGEROUS_BASH_PATTERNS) {
|
|
65
|
-
if (pattern.test(command)) {
|
|
66
|
-
return label;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function checkFilePath(filePath: string): string | null {
|
|
73
|
-
for (const { pattern, label } of PROTECTED_FILE_PATTERNS) {
|
|
74
|
-
if (pattern.test(filePath)) {
|
|
75
|
-
return label;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const DANGEROUS_PYTHON_PATTERNS: Array<{ pattern: RegExp; label: string }> = [
|
|
82
|
-
{ pattern: /\beval\s*\(/, label: 'Python eval() — arbitrary code execution' },
|
|
83
|
-
{ pattern: /\bexec\s*\(/, label: 'Python exec() — arbitrary code execution' },
|
|
84
|
-
{ pattern: /\b__import__\s*\(/, label: 'Python __import__() — dynamic import (potential code injection)' },
|
|
85
|
-
{ pattern: /subprocess\.call\([^)]*shell\s*=\s*True/, label: 'subprocess.call(shell=True) — shell injection risk' },
|
|
86
|
-
{ pattern: /subprocess\.Popen\([^)]*shell\s*=\s*True/, label: 'subprocess.Popen(shell=True) — shell injection risk' },
|
|
87
|
-
{ pattern: /os\.system\s*\(/, label: 'os.system() — shell injection risk' },
|
|
88
|
-
{ pattern: /\bf['"].*\{.*\}.*['"].*(?:execute|cursor|query)/, label: 'f-string in SQL — SQL injection risk' },
|
|
89
|
-
{ pattern: /['"].*%s.*['"].*%.*(?:execute|cursor|query)/, label: 'String formatting in SQL — SQL injection risk' },
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
function checkPythonContent(content: string): string | null {
|
|
93
|
-
for (const { pattern, label } of DANGEROUS_PYTHON_PATTERNS) {
|
|
94
|
-
if (pattern.test(content)) {
|
|
95
|
-
return label;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function main(): Promise<void> {
|
|
102
|
-
try {
|
|
103
|
-
const input = await readStdin();
|
|
104
|
-
const hookInput = JSON.parse(input) as HookInput;
|
|
105
|
-
const { tool_name, tool_input } = hookInput;
|
|
106
|
-
|
|
107
|
-
if (tool_name === 'Bash' && tool_input.command) {
|
|
108
|
-
const violation = checkBashCommand(tool_input.command);
|
|
109
|
-
if (violation) {
|
|
110
|
-
process.stdout.write(JSON.stringify({
|
|
111
|
-
message: `SECURITY GATE: Dangerous command pattern detected: ${violation}\nCommand: ${tool_input.command.slice(0, 200)}\nReview carefully before proceeding.`,
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if ((tool_name === 'Write' || tool_name === 'Edit') && tool_input.file_path) {
|
|
117
|
-
const violation = checkFilePath(tool_input.file_path);
|
|
118
|
-
if (violation) {
|
|
119
|
-
process.stdout.write(JSON.stringify({
|
|
120
|
-
message: `SECURITY GATE: Attempt to write to protected file: ${violation}\nPath: ${tool_input.file_path}\nEnsure this is intentional and no secrets will be exposed.`,
|
|
121
|
-
}));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Check Python file content for dangerous patterns (Write uses content, Edit uses new_string)
|
|
126
|
-
const pyContent = tool_input.content || tool_input.new_string;
|
|
127
|
-
if ((tool_name === 'Write' || tool_name === 'Edit') && tool_input.file_path?.endsWith('.py') && pyContent) {
|
|
128
|
-
const pyViolation = checkPythonContent(pyContent);
|
|
129
|
-
if (pyViolation) {
|
|
130
|
-
process.stdout.write(JSON.stringify({
|
|
131
|
-
message: `SECURITY GATE: Dangerous Python pattern detected: ${pyViolation}\nFile: ${tool_input.file_path}\nReview carefully before proceeding.`,
|
|
132
|
-
}));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
} catch {
|
|
136
|
-
// Hooks must never crash
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
process.exit(0);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function readStdin(): Promise<string> {
|
|
143
|
-
return new Promise((resolve) => {
|
|
144
|
-
let data = '';
|
|
145
|
-
process.stdin.setEncoding('utf-8');
|
|
146
|
-
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
147
|
-
process.stdin.on('end', () => resolve(data));
|
|
148
|
-
// Timeout to prevent hanging
|
|
149
|
-
setTimeout(() => resolve(data), 400);
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
main();
|