@massu/core 0.1.1 → 0.4.0
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/commands/_shared-preamble.md +76 -0
- package/commands/massu-audit-deps.md +211 -0
- package/commands/massu-changelog.md +174 -0
- package/commands/massu-cleanup.md +315 -0
- package/commands/massu-commit.md +481 -0
- package/commands/massu-create-plan.md +752 -0
- package/commands/massu-dead-code.md +131 -0
- package/commands/massu-debug.md +484 -0
- package/commands/massu-deploy.md +91 -0
- package/commands/massu-deps.md +374 -0
- package/commands/massu-doc-gen.md +279 -0
- package/commands/massu-docs.md +364 -0
- package/commands/massu-estimate.md +313 -0
- package/commands/massu-golden-path.md +973 -0
- package/commands/massu-guide.md +167 -0
- package/commands/massu-hotfix.md +480 -0
- package/commands/massu-loop-playwright.md +837 -0
- package/commands/massu-loop.md +775 -0
- package/commands/massu-new-feature.md +511 -0
- package/commands/massu-parity.md +214 -0
- package/commands/massu-plan.md +456 -0
- package/commands/massu-push-light.md +207 -0
- package/commands/massu-push.md +434 -0
- package/commands/massu-refactor.md +410 -0
- package/commands/massu-release.md +363 -0
- package/commands/massu-review.md +238 -0
- package/commands/massu-simplify.md +281 -0
- package/commands/massu-status.md +278 -0
- package/commands/massu-tdd.md +201 -0
- package/commands/massu-test.md +516 -0
- package/commands/massu-verify-playwright.md +281 -0
- package/commands/massu-verify.md +667 -0
- package/dist/cli.js +7772 -3140
- package/dist/hooks/cost-tracker.js +103 -40
- package/dist/hooks/post-edit-context.js +74 -8
- package/dist/hooks/post-tool-use.js +268 -106
- package/dist/hooks/pre-compact.js +167 -43
- package/dist/hooks/pre-delete-check.js +159 -42
- package/dist/hooks/quality-event.js +103 -40
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +143 -84
- package/dist/hooks/session-start.js +186 -49
- package/dist/hooks/user-prompt.js +189 -43
- package/package.json +10 -15
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -0
- package/src/cloud-sync.ts +14 -18
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +230 -5
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/cost-tracker.ts +11 -6
- package/src/db.ts +115 -2
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +21 -16
- package/src/hooks/post-edit-context.ts +4 -4
- package/src/hooks/post-tool-use.ts +130 -0
- package/src/hooks/pre-compact.ts +23 -1
- package/src/hooks/pre-delete-check.ts +92 -4
- package/src/hooks/security-gate.ts +32 -0
- package/src/hooks/session-end.ts +3 -3
- package/src/hooks/session-start.ts +99 -6
- package/src/hooks/user-prompt.ts +46 -1
- package/src/import-resolver.ts +2 -1
- package/src/knowledge-db.ts +169 -0
- package/src/knowledge-indexer.ts +704 -0
- package/src/knowledge-tools.ts +1413 -0
- package/src/license.ts +482 -0
- package/src/memory-db.ts +1364 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -2
- package/src/prompt-analyzer.ts +9 -2
- package/src/python/coupling-detector.ts +124 -0
- package/src/python/domain-enforcer.ts +83 -0
- package/src/python/impact-analyzer.ts +95 -0
- package/src/python/import-parser.ts +244 -0
- package/src/python/import-resolver.ts +135 -0
- package/src/python/migration-indexer.ts +115 -0
- package/src/python/migration-parser.ts +332 -0
- package/src/python/model-indexer.ts +70 -0
- package/src/python/model-parser.ts +279 -0
- package/src/python/route-indexer.ts +58 -0
- package/src/python/route-parser.ts +317 -0
- package/src/python-tools.ts +629 -0
- package/src/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +45 -89
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +29 -7
- package/src/session-archiver.ts +4 -5
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +1032 -44
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/README.md +0 -40
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- package/src/tool-helpers.ts +0 -41
|
@@ -3,12 +3,13 @@ import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
|
|
|
3
3
|
|
|
4
4
|
// src/memory-db.ts
|
|
5
5
|
import Database from "better-sqlite3";
|
|
6
|
-
import { dirname as dirname2 } from "path";
|
|
6
|
+
import { dirname as dirname2, basename } from "path";
|
|
7
7
|
import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
10
|
import { resolve, dirname } from "path";
|
|
11
11
|
import { existsSync, readFileSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
12
13
|
import { parse as parseYaml } from "yaml";
|
|
13
14
|
import { z } from "zod";
|
|
14
15
|
var DomainConfigSchema = z.object({
|
|
@@ -140,6 +141,49 @@ var CloudConfigSchema = z.object({
|
|
|
140
141
|
audit: z.boolean().default(true)
|
|
141
142
|
}).default({ memory: true, analytics: true, audit: true })
|
|
142
143
|
}).optional();
|
|
144
|
+
var ConventionsConfigSchema = z.object({
|
|
145
|
+
claudeDirName: z.string().default(".claude").refine(
|
|
146
|
+
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
147
|
+
{ message: 'claudeDirName must not contain ".." or start with "/"' }
|
|
148
|
+
),
|
|
149
|
+
sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
|
|
150
|
+
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
151
|
+
{ message: 'sessionStatePath must not contain ".." or start with "/"' }
|
|
152
|
+
),
|
|
153
|
+
sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
|
|
154
|
+
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
155
|
+
{ message: 'sessionArchivePath must not contain ".." or start with "/"' }
|
|
156
|
+
),
|
|
157
|
+
knowledgeCategories: z.array(z.string()).default([
|
|
158
|
+
"patterns",
|
|
159
|
+
"commands",
|
|
160
|
+
"incidents",
|
|
161
|
+
"reference",
|
|
162
|
+
"protocols",
|
|
163
|
+
"checklists",
|
|
164
|
+
"playbooks",
|
|
165
|
+
"critical",
|
|
166
|
+
"scripts",
|
|
167
|
+
"status",
|
|
168
|
+
"templates",
|
|
169
|
+
"loop-state",
|
|
170
|
+
"session-state",
|
|
171
|
+
"agents"
|
|
172
|
+
]),
|
|
173
|
+
knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
|
|
174
|
+
excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
|
|
175
|
+
}).optional();
|
|
176
|
+
var PythonDomainConfigSchema = z.object({
|
|
177
|
+
name: z.string(),
|
|
178
|
+
packages: z.array(z.string()),
|
|
179
|
+
allowed_imports_from: z.array(z.string()).default([])
|
|
180
|
+
});
|
|
181
|
+
var PythonConfigSchema = z.object({
|
|
182
|
+
root: z.string(),
|
|
183
|
+
alembic_dir: z.string().optional(),
|
|
184
|
+
domains: z.array(PythonDomainConfigSchema).default([]),
|
|
185
|
+
exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
|
|
186
|
+
}).optional();
|
|
143
187
|
var PathsConfigSchema = z.object({
|
|
144
188
|
source: z.string().default("src"),
|
|
145
189
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
@@ -174,7 +218,9 @@ var RawConfigSchema = z.object({
|
|
|
174
218
|
security: SecurityConfigSchema,
|
|
175
219
|
team: TeamConfigSchema,
|
|
176
220
|
regression: RegressionConfigSchema,
|
|
177
|
-
cloud: CloudConfigSchema
|
|
221
|
+
cloud: CloudConfigSchema,
|
|
222
|
+
conventions: ConventionsConfigSchema,
|
|
223
|
+
python: PythonConfigSchema
|
|
178
224
|
}).passthrough();
|
|
179
225
|
var _config = null;
|
|
180
226
|
var _projectRoot = null;
|
|
@@ -238,13 +284,24 @@ function getConfig() {
|
|
|
238
284
|
security: parsed.security,
|
|
239
285
|
team: parsed.team,
|
|
240
286
|
regression: parsed.regression,
|
|
241
|
-
cloud: parsed.cloud
|
|
287
|
+
cloud: parsed.cloud,
|
|
288
|
+
conventions: parsed.conventions,
|
|
289
|
+
python: parsed.python
|
|
242
290
|
};
|
|
291
|
+
if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
|
|
292
|
+
_config.cloud = {
|
|
293
|
+
enabled: true,
|
|
294
|
+
sync: { memory: true, analytics: true, audit: true },
|
|
295
|
+
..._config.cloud,
|
|
296
|
+
apiKey: process.env.MASSU_API_KEY
|
|
297
|
+
};
|
|
298
|
+
}
|
|
243
299
|
return _config;
|
|
244
300
|
}
|
|
245
301
|
function getResolvedPaths() {
|
|
246
302
|
const config = getConfig();
|
|
247
303
|
const root = getProjectRoot();
|
|
304
|
+
const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
|
|
248
305
|
return {
|
|
249
306
|
codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
|
|
250
307
|
dataDbPath: resolve(root, ".massu/data.db"),
|
|
@@ -260,22 +317,43 @@ function getResolvedPaths() {
|
|
|
260
317
|
),
|
|
261
318
|
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
262
319
|
indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
|
|
263
|
-
patternsDir: resolve(root, "
|
|
264
|
-
claudeMdPath: resolve(root, "
|
|
320
|
+
patternsDir: resolve(root, claudeDirName, "patterns"),
|
|
321
|
+
claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
|
|
265
322
|
docsMapPath: resolve(root, ".massu/docs-map.json"),
|
|
266
323
|
helpSitePath: resolve(root, "../" + config.project.name + "-help"),
|
|
267
|
-
memoryDbPath: resolve(root, ".massu/memory.db")
|
|
324
|
+
memoryDbPath: resolve(root, ".massu/memory.db"),
|
|
325
|
+
knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
|
|
326
|
+
plansDir: resolve(root, "docs/plans"),
|
|
327
|
+
docsDir: resolve(root, "docs"),
|
|
328
|
+
claudeDir: resolve(root, claudeDirName),
|
|
329
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
|
|
330
|
+
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
331
|
+
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
332
|
+
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
333
|
+
settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
|
|
268
334
|
};
|
|
269
335
|
}
|
|
270
336
|
|
|
271
|
-
// src/memory-
|
|
337
|
+
// src/memory-db.ts
|
|
338
|
+
function getMemoryDb() {
|
|
339
|
+
const dbPath = getResolvedPaths().memoryDbPath;
|
|
340
|
+
const dir = dirname2(dbPath);
|
|
341
|
+
if (!existsSync2(dir)) {
|
|
342
|
+
mkdirSync(dir, { recursive: true });
|
|
343
|
+
}
|
|
344
|
+
const db = new Database(dbPath);
|
|
345
|
+
db.pragma("journal_mode = WAL");
|
|
346
|
+
db.pragma("foreign_keys = ON");
|
|
347
|
+
initMemorySchema(db);
|
|
348
|
+
return db;
|
|
349
|
+
}
|
|
272
350
|
function initMemorySchema(db) {
|
|
273
351
|
db.exec(`
|
|
274
352
|
-- Sessions table (linked to Claude Code session IDs)
|
|
275
353
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
276
354
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
277
355
|
session_id TEXT UNIQUE NOT NULL,
|
|
278
|
-
project TEXT NOT NULL DEFAULT '
|
|
356
|
+
project TEXT NOT NULL DEFAULT 'my-project',
|
|
279
357
|
git_branch TEXT,
|
|
280
358
|
started_at TEXT NOT NULL,
|
|
281
359
|
started_at_epoch INTEGER NOT NULL,
|
|
@@ -330,9 +408,7 @@ function initMemorySchema(db) {
|
|
|
330
408
|
content_rowid='id'
|
|
331
409
|
);
|
|
332
410
|
`);
|
|
333
|
-
} catch (
|
|
334
|
-
process.stderr.write(`FTS5 setup warning: ${e instanceof Error ? e.message : String(e)}
|
|
335
|
-
`);
|
|
411
|
+
} catch (_e) {
|
|
336
412
|
}
|
|
337
413
|
db.exec(`
|
|
338
414
|
CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
|
|
@@ -392,9 +468,7 @@ function initMemorySchema(db) {
|
|
|
392
468
|
content_rowid='id'
|
|
393
469
|
);
|
|
394
470
|
`);
|
|
395
|
-
} catch (
|
|
396
|
-
process.stderr.write(`FTS5 setup warning: ${e instanceof Error ? e.message : String(e)}
|
|
397
|
-
`);
|
|
471
|
+
} catch (_e) {
|
|
398
472
|
}
|
|
399
473
|
db.exec(`
|
|
400
474
|
CREATE TRIGGER IF NOT EXISTS prompts_ai AFTER INSERT ON user_prompts BEGIN
|
|
@@ -464,9 +538,7 @@ function initMemorySchema(db) {
|
|
|
464
538
|
content_rowid=id
|
|
465
539
|
);
|
|
466
540
|
`);
|
|
467
|
-
} catch (
|
|
468
|
-
process.stderr.write(`FTS5 setup warning: ${e instanceof Error ? e.message : String(e)}
|
|
469
|
-
`);
|
|
541
|
+
} catch (_e) {
|
|
470
542
|
}
|
|
471
543
|
db.exec(`
|
|
472
544
|
CREATE TRIGGER IF NOT EXISTS ct_fts_insert AFTER INSERT ON conversation_turns BEGIN
|
|
@@ -490,7 +562,7 @@ function initMemorySchema(db) {
|
|
|
490
562
|
CREATE TABLE IF NOT EXISTS session_quality_scores (
|
|
491
563
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
492
564
|
session_id TEXT NOT NULL UNIQUE,
|
|
493
|
-
project TEXT NOT NULL DEFAULT '
|
|
565
|
+
project TEXT NOT NULL DEFAULT 'my-project',
|
|
494
566
|
score INTEGER NOT NULL DEFAULT 100,
|
|
495
567
|
security_score INTEGER NOT NULL DEFAULT 100,
|
|
496
568
|
architecture_score INTEGER NOT NULL DEFAULT 100,
|
|
@@ -513,7 +585,7 @@ function initMemorySchema(db) {
|
|
|
513
585
|
CREATE TABLE IF NOT EXISTS session_costs (
|
|
514
586
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
515
587
|
session_id TEXT NOT NULL UNIQUE,
|
|
516
|
-
project TEXT NOT NULL DEFAULT '
|
|
588
|
+
project TEXT NOT NULL DEFAULT 'my-project',
|
|
517
589
|
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
518
590
|
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
519
591
|
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
@@ -740,10 +812,40 @@ function initMemorySchema(db) {
|
|
|
740
812
|
);
|
|
741
813
|
CREATE INDEX IF NOT EXISTS idx_pending_sync_created ON pending_sync(created_at ASC);
|
|
742
814
|
`);
|
|
815
|
+
db.exec(`
|
|
816
|
+
CREATE TABLE IF NOT EXISTS license_cache (
|
|
817
|
+
api_key_hash TEXT PRIMARY KEY,
|
|
818
|
+
tier TEXT NOT NULL,
|
|
819
|
+
valid_until TEXT NOT NULL,
|
|
820
|
+
last_validated TEXT NOT NULL,
|
|
821
|
+
features TEXT DEFAULT '[]'
|
|
822
|
+
);
|
|
823
|
+
`);
|
|
824
|
+
}
|
|
825
|
+
function assignImportance(type, vrResult) {
|
|
826
|
+
switch (type) {
|
|
827
|
+
case "decision":
|
|
828
|
+
case "failed_attempt":
|
|
829
|
+
return 5;
|
|
830
|
+
case "cr_violation":
|
|
831
|
+
case "incident_near_miss":
|
|
832
|
+
return 4;
|
|
833
|
+
case "vr_check":
|
|
834
|
+
return vrResult === "PASS" ? 2 : 4;
|
|
835
|
+
case "pattern_compliance":
|
|
836
|
+
return vrResult === "PASS" ? 2 : 4;
|
|
837
|
+
case "feature":
|
|
838
|
+
case "bugfix":
|
|
839
|
+
return 3;
|
|
840
|
+
case "refactor":
|
|
841
|
+
return 2;
|
|
842
|
+
case "file_change":
|
|
843
|
+
case "discovery":
|
|
844
|
+
return 1;
|
|
845
|
+
default:
|
|
846
|
+
return 3;
|
|
847
|
+
}
|
|
743
848
|
}
|
|
744
|
-
|
|
745
|
-
// src/memory-queries.ts
|
|
746
|
-
import { basename } from "path";
|
|
747
849
|
function autoDetectTaskId(planFile) {
|
|
748
850
|
if (!planFile) return null;
|
|
749
851
|
const base = basename(planFile);
|
|
@@ -757,6 +859,29 @@ function createSession(db, sessionId, opts) {
|
|
|
757
859
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
758
860
|
`).run(sessionId, opts?.branch ?? null, opts?.planFile ?? null, taskId, now.toISOString(), Math.floor(now.getTime() / 1e3));
|
|
759
861
|
}
|
|
862
|
+
function addObservation(db, sessionId, type, title, detail, opts) {
|
|
863
|
+
const now = /* @__PURE__ */ new Date();
|
|
864
|
+
const importance = opts?.importance ?? assignImportance(type, opts?.evidence?.includes("PASS") ? "PASS" : void 0);
|
|
865
|
+
const result = db.prepare(`
|
|
866
|
+
INSERT INTO observations (session_id, type, title, detail, files_involved, plan_item, cr_rule, vr_type, evidence, importance, original_tokens, created_at, created_at_epoch)
|
|
867
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
868
|
+
`).run(
|
|
869
|
+
sessionId,
|
|
870
|
+
type,
|
|
871
|
+
title,
|
|
872
|
+
detail,
|
|
873
|
+
JSON.stringify(opts?.filesInvolved ?? []),
|
|
874
|
+
opts?.planItem ?? null,
|
|
875
|
+
opts?.crRule ?? null,
|
|
876
|
+
opts?.vrType ?? null,
|
|
877
|
+
opts?.evidence ?? null,
|
|
878
|
+
importance,
|
|
879
|
+
opts?.originalTokens ?? 0,
|
|
880
|
+
now.toISOString(),
|
|
881
|
+
Math.floor(now.getTime() / 1e3)
|
|
882
|
+
);
|
|
883
|
+
return Number(result.lastInsertRowid);
|
|
884
|
+
}
|
|
760
885
|
function addSummary(db, sessionId, summary) {
|
|
761
886
|
const now = /* @__PURE__ */ new Date();
|
|
762
887
|
db.prepare(`
|
|
@@ -779,24 +904,6 @@ function addSummary(db, sessionId, summary) {
|
|
|
779
904
|
);
|
|
780
905
|
}
|
|
781
906
|
|
|
782
|
-
// src/memory-db.ts
|
|
783
|
-
var initializedPaths = /* @__PURE__ */ new Set();
|
|
784
|
-
function getMemoryDb() {
|
|
785
|
-
const dbPath = getResolvedPaths().memoryDbPath;
|
|
786
|
-
const dir = dirname2(dbPath);
|
|
787
|
-
if (!existsSync2(dir)) {
|
|
788
|
-
mkdirSync(dir, { recursive: true });
|
|
789
|
-
}
|
|
790
|
-
const db = new Database(dbPath);
|
|
791
|
-
db.pragma("journal_mode = WAL");
|
|
792
|
-
db.pragma("foreign_keys = ON");
|
|
793
|
-
if (!initializedPaths.has(dbPath)) {
|
|
794
|
-
initMemorySchema(db);
|
|
795
|
-
initializedPaths.add(dbPath);
|
|
796
|
-
}
|
|
797
|
-
return db;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
907
|
// src/audit-trail.ts
|
|
801
908
|
function logAuditEntry(db, entry) {
|
|
802
909
|
db.prepare(`
|
|
@@ -833,6 +940,23 @@ async function main() {
|
|
|
833
940
|
).all(session_id);
|
|
834
941
|
const summary = buildSnapshotSummary(observations, prompts);
|
|
835
942
|
addSummary(db, session_id, summary);
|
|
943
|
+
try {
|
|
944
|
+
const knowledgeObs = observations.filter(
|
|
945
|
+
(o) => o.title?.includes("knowledge") || o.title?.includes("Knowledge") || o.detail?.includes("knowledge")
|
|
946
|
+
);
|
|
947
|
+
if (knowledgeObs.length > 0) {
|
|
948
|
+
const knowledgeContext = knowledgeObs.map((o) => `[${o.type}] ${o.title}`).join("; ");
|
|
949
|
+
addObservation(
|
|
950
|
+
db,
|
|
951
|
+
session_id,
|
|
952
|
+
"discovery",
|
|
953
|
+
"Knowledge context preserved before compaction",
|
|
954
|
+
knowledgeContext,
|
|
955
|
+
{ importance: 4 }
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
} catch (_knowledgeErr) {
|
|
959
|
+
}
|
|
836
960
|
try {
|
|
837
961
|
logAuditEntry(db, {
|
|
838
962
|
sessionId: session_id,
|
|
@@ -894,14 +1018,14 @@ function safeParseJson(json, fallback) {
|
|
|
894
1018
|
}
|
|
895
1019
|
}
|
|
896
1020
|
function readStdin() {
|
|
897
|
-
return new Promise((
|
|
1021
|
+
return new Promise((resolve3) => {
|
|
898
1022
|
let data = "";
|
|
899
1023
|
process.stdin.setEncoding("utf-8");
|
|
900
1024
|
process.stdin.on("data", (chunk) => {
|
|
901
1025
|
data += chunk;
|
|
902
1026
|
});
|
|
903
|
-
process.stdin.on("end", () =>
|
|
904
|
-
setTimeout(() =>
|
|
1027
|
+
process.stdin.on("end", () => resolve3(data));
|
|
1028
|
+
setTimeout(() => resolve3(data), 3e3);
|
|
905
1029
|
});
|
|
906
1030
|
}
|
|
907
1031
|
main();
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/hooks/pre-delete-check.ts
|
|
5
|
-
import
|
|
5
|
+
import Database2 from "better-sqlite3";
|
|
6
6
|
import { existsSync as existsSync2 } from "fs";
|
|
7
7
|
|
|
8
8
|
// src/config.ts
|
|
9
9
|
import { resolve, dirname } from "path";
|
|
10
10
|
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
11
12
|
import { parse as parseYaml } from "yaml";
|
|
12
13
|
import { z } from "zod";
|
|
13
14
|
var DomainConfigSchema = z.object({
|
|
@@ -139,6 +140,49 @@ var CloudConfigSchema = z.object({
|
|
|
139
140
|
audit: z.boolean().default(true)
|
|
140
141
|
}).default({ memory: true, analytics: true, audit: true })
|
|
141
142
|
}).optional();
|
|
143
|
+
var ConventionsConfigSchema = z.object({
|
|
144
|
+
claudeDirName: z.string().default(".claude").refine(
|
|
145
|
+
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
146
|
+
{ message: 'claudeDirName must not contain ".." or start with "/"' }
|
|
147
|
+
),
|
|
148
|
+
sessionStatePath: z.string().default(".claude/session-state/CURRENT.md").refine(
|
|
149
|
+
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
150
|
+
{ message: 'sessionStatePath must not contain ".." or start with "/"' }
|
|
151
|
+
),
|
|
152
|
+
sessionArchivePath: z.string().default(".claude/session-state/archive").refine(
|
|
153
|
+
(s) => !s.includes("..") && !s.startsWith("/"),
|
|
154
|
+
{ message: 'sessionArchivePath must not contain ".." or start with "/"' }
|
|
155
|
+
),
|
|
156
|
+
knowledgeCategories: z.array(z.string()).default([
|
|
157
|
+
"patterns",
|
|
158
|
+
"commands",
|
|
159
|
+
"incidents",
|
|
160
|
+
"reference",
|
|
161
|
+
"protocols",
|
|
162
|
+
"checklists",
|
|
163
|
+
"playbooks",
|
|
164
|
+
"critical",
|
|
165
|
+
"scripts",
|
|
166
|
+
"status",
|
|
167
|
+
"templates",
|
|
168
|
+
"loop-state",
|
|
169
|
+
"session-state",
|
|
170
|
+
"agents"
|
|
171
|
+
]),
|
|
172
|
+
knowledgeSourceFiles: z.array(z.string()).default(["CLAUDE.md", "MEMORY.md", "corrections.md"]),
|
|
173
|
+
excludePatterns: z.array(z.string()).default(["/ARCHIVE/", "/SESSION-HISTORY/"])
|
|
174
|
+
}).optional();
|
|
175
|
+
var PythonDomainConfigSchema = z.object({
|
|
176
|
+
name: z.string(),
|
|
177
|
+
packages: z.array(z.string()),
|
|
178
|
+
allowed_imports_from: z.array(z.string()).default([])
|
|
179
|
+
});
|
|
180
|
+
var PythonConfigSchema = z.object({
|
|
181
|
+
root: z.string(),
|
|
182
|
+
alembic_dir: z.string().optional(),
|
|
183
|
+
domains: z.array(PythonDomainConfigSchema).default([]),
|
|
184
|
+
exclude_dirs: z.array(z.string()).default(["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache"])
|
|
185
|
+
}).optional();
|
|
142
186
|
var PathsConfigSchema = z.object({
|
|
143
187
|
source: z.string().default("src"),
|
|
144
188
|
aliases: z.record(z.string(), z.string()).default({ "@": "src" }),
|
|
@@ -173,7 +217,9 @@ var RawConfigSchema = z.object({
|
|
|
173
217
|
security: SecurityConfigSchema,
|
|
174
218
|
team: TeamConfigSchema,
|
|
175
219
|
regression: RegressionConfigSchema,
|
|
176
|
-
cloud: CloudConfigSchema
|
|
220
|
+
cloud: CloudConfigSchema,
|
|
221
|
+
conventions: ConventionsConfigSchema,
|
|
222
|
+
python: PythonConfigSchema
|
|
177
223
|
}).passthrough();
|
|
178
224
|
var _config = null;
|
|
179
225
|
var _projectRoot = null;
|
|
@@ -237,13 +283,24 @@ function getConfig() {
|
|
|
237
283
|
security: parsed.security,
|
|
238
284
|
team: parsed.team,
|
|
239
285
|
regression: parsed.regression,
|
|
240
|
-
cloud: parsed.cloud
|
|
286
|
+
cloud: parsed.cloud,
|
|
287
|
+
conventions: parsed.conventions,
|
|
288
|
+
python: parsed.python
|
|
241
289
|
};
|
|
290
|
+
if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
|
|
291
|
+
_config.cloud = {
|
|
292
|
+
enabled: true,
|
|
293
|
+
sync: { memory: true, analytics: true, audit: true },
|
|
294
|
+
..._config.cloud,
|
|
295
|
+
apiKey: process.env.MASSU_API_KEY
|
|
296
|
+
};
|
|
297
|
+
}
|
|
242
298
|
return _config;
|
|
243
299
|
}
|
|
244
300
|
function getResolvedPaths() {
|
|
245
301
|
const config = getConfig();
|
|
246
302
|
const root = getProjectRoot();
|
|
303
|
+
const claudeDirName = config.conventions?.claudeDirName ?? ".claude";
|
|
247
304
|
return {
|
|
248
305
|
codegraphDbPath: resolve(root, ".codegraph/codegraph.db"),
|
|
249
306
|
dataDbPath: resolve(root, ".massu/data.db"),
|
|
@@ -259,14 +316,26 @@ function getResolvedPaths() {
|
|
|
259
316
|
),
|
|
260
317
|
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
261
318
|
indexFiles: ["index.ts", "index.tsx", "index.js", "index.jsx"],
|
|
262
|
-
patternsDir: resolve(root, "
|
|
263
|
-
claudeMdPath: resolve(root, "
|
|
319
|
+
patternsDir: resolve(root, claudeDirName, "patterns"),
|
|
320
|
+
claudeMdPath: resolve(root, claudeDirName, "CLAUDE.md"),
|
|
264
321
|
docsMapPath: resolve(root, ".massu/docs-map.json"),
|
|
265
322
|
helpSitePath: resolve(root, "../" + config.project.name + "-help"),
|
|
266
|
-
memoryDbPath: resolve(root, ".massu/memory.db")
|
|
323
|
+
memoryDbPath: resolve(root, ".massu/memory.db"),
|
|
324
|
+
knowledgeDbPath: resolve(root, ".massu/knowledge.db"),
|
|
325
|
+
plansDir: resolve(root, "docs/plans"),
|
|
326
|
+
docsDir: resolve(root, "docs"),
|
|
327
|
+
claudeDir: resolve(root, claudeDirName),
|
|
328
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", root.replace(/\//g, "-"), "memory"),
|
|
329
|
+
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
330
|
+
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
331
|
+
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
332
|
+
settingsLocalPath: resolve(root, claudeDirName, "settings.local.json")
|
|
267
333
|
};
|
|
268
334
|
}
|
|
269
335
|
|
|
336
|
+
// src/memory-db.ts
|
|
337
|
+
import Database from "better-sqlite3";
|
|
338
|
+
|
|
270
339
|
// src/sentinel-db.ts
|
|
271
340
|
var PROJECT_ROOT = getProjectRoot();
|
|
272
341
|
function parsePortalScope(raw) {
|
|
@@ -294,45 +363,32 @@ function toFeature(row) {
|
|
|
294
363
|
removed_reason: row.removed_reason || null
|
|
295
364
|
};
|
|
296
365
|
}
|
|
366
|
+
function getFeatureById(db, id) {
|
|
367
|
+
const row = db.prepare("SELECT * FROM massu_sentinel WHERE id = ?").get(id);
|
|
368
|
+
return row ? toFeature(row) : null;
|
|
369
|
+
}
|
|
297
370
|
function getFeatureImpact(db, filePaths) {
|
|
298
|
-
if (filePaths.length === 0) {
|
|
299
|
-
return { files_analyzed: [], orphaned: [], degraded: [], unaffected: [], blocked: false, block_reason: null };
|
|
300
|
-
}
|
|
301
371
|
const fileSet = new Set(filePaths);
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
const featurePlaceholders = affectedFeatureIds.map(() => "?").join(",");
|
|
311
|
-
const featureRows = db.prepare(
|
|
312
|
-
`SELECT * FROM massu_sentinel WHERE id IN (${featurePlaceholders}) AND status = 'active'`
|
|
313
|
-
).all(...affectedFeatureIds);
|
|
314
|
-
const featuresById = new Map(featureRows.map((r) => [r.id, toFeature(r)]));
|
|
315
|
-
const allComponents = db.prepare(
|
|
316
|
-
`SELECT feature_id, component_file, is_primary FROM massu_sentinel_components WHERE feature_id IN (${featurePlaceholders})`
|
|
317
|
-
).all(...affectedFeatureIds);
|
|
318
|
-
const componentsByFeature = /* @__PURE__ */ new Map();
|
|
319
|
-
for (const comp of allComponents) {
|
|
320
|
-
let list = componentsByFeature.get(comp.feature_id);
|
|
321
|
-
if (!list) {
|
|
322
|
-
list = [];
|
|
323
|
-
componentsByFeature.set(comp.feature_id, list);
|
|
372
|
+
const affectedFeatureIds = /* @__PURE__ */ new Set();
|
|
373
|
+
for (const filePath of filePaths) {
|
|
374
|
+
const links = db.prepare(
|
|
375
|
+
"SELECT feature_id FROM massu_sentinel_components WHERE component_file = ?"
|
|
376
|
+
).all(filePath);
|
|
377
|
+
for (const link of links) {
|
|
378
|
+
affectedFeatureIds.add(link.feature_id);
|
|
324
379
|
}
|
|
325
|
-
list.push(comp);
|
|
326
380
|
}
|
|
327
381
|
const orphaned = [];
|
|
328
382
|
const degraded = [];
|
|
329
383
|
const unaffected = [];
|
|
330
384
|
for (const featureId of affectedFeatureIds) {
|
|
331
|
-
const feature =
|
|
332
|
-
if (!feature) continue;
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
385
|
+
const feature = getFeatureById(db, featureId);
|
|
386
|
+
if (!feature || feature.status !== "active") continue;
|
|
387
|
+
const allComponents = db.prepare(
|
|
388
|
+
"SELECT component_file, is_primary FROM massu_sentinel_components WHERE feature_id = ?"
|
|
389
|
+
).all(featureId);
|
|
390
|
+
const affected = allComponents.filter((c) => fileSet.has(c.component_file));
|
|
391
|
+
const remaining = allComponents.filter((c) => !fileSet.has(c.component_file));
|
|
336
392
|
const primaryAffected = affected.some((c) => c.is_primary);
|
|
337
393
|
const item = {
|
|
338
394
|
feature,
|
|
@@ -364,17 +420,46 @@ function getFeatureImpact(db, filePaths) {
|
|
|
364
420
|
|
|
365
421
|
// src/hooks/pre-delete-check.ts
|
|
366
422
|
var PROJECT_ROOT2 = getProjectRoot();
|
|
423
|
+
var KNOWLEDGE_PROTECTED_FILES = [
|
|
424
|
+
"knowledge-db.ts",
|
|
425
|
+
"knowledge-indexer.ts",
|
|
426
|
+
"knowledge-tools.ts"
|
|
427
|
+
];
|
|
367
428
|
function getDataDb() {
|
|
368
429
|
const dbPath = getResolvedPaths().dataDbPath;
|
|
369
430
|
if (!existsSync2(dbPath)) return null;
|
|
370
431
|
try {
|
|
371
|
-
const db = new
|
|
432
|
+
const db = new Database2(dbPath, { readonly: true });
|
|
372
433
|
db.pragma("journal_mode = WAL");
|
|
373
434
|
return db;
|
|
374
435
|
} catch {
|
|
375
436
|
return null;
|
|
376
437
|
}
|
|
377
438
|
}
|
|
439
|
+
function checkKnowledgeFileProtection(input) {
|
|
440
|
+
const candidateFiles = [];
|
|
441
|
+
if (input.tool_name === "Bash" && input.tool_input.command) {
|
|
442
|
+
const cmd = input.tool_input.command;
|
|
443
|
+
const rmMatch = cmd.match(/(?:rm|git\s+rm)\s+(?:-[rf]*\s+)*(.+)/);
|
|
444
|
+
if (rmMatch) {
|
|
445
|
+
const parts = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
|
|
446
|
+
candidateFiles.push(...parts);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (input.tool_name === "Write" && input.tool_input.file_path) {
|
|
450
|
+
const content = input.tool_input.content || "";
|
|
451
|
+
if (content.trim().length === 0) {
|
|
452
|
+
candidateFiles.push(input.tool_input.file_path);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
for (const f of candidateFiles) {
|
|
456
|
+
const basename = f.split("/").pop() ?? f;
|
|
457
|
+
if (KNOWLEDGE_PROTECTED_FILES.includes(basename)) {
|
|
458
|
+
return `KNOWLEDGE SYSTEM PROTECTION: "${basename}" is a core knowledge system file. Deleting it will break knowledge indexing and memory retrieval. Create a replacement before removing.`;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
378
463
|
function extractDeletedFiles(input) {
|
|
379
464
|
const files = [];
|
|
380
465
|
if (input.tool_name === "Bash" && input.tool_input.command) {
|
|
@@ -384,7 +469,7 @@ function extractDeletedFiles(input) {
|
|
|
384
469
|
const paths = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
|
|
385
470
|
for (const p of paths) {
|
|
386
471
|
const relPath = p.startsWith("src/") ? p : p.replace(PROJECT_ROOT2 + "/", "");
|
|
387
|
-
if (relPath.startsWith("src/")) {
|
|
472
|
+
if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
|
|
388
473
|
files.push(relPath);
|
|
389
474
|
}
|
|
390
475
|
}
|
|
@@ -394,7 +479,7 @@ function extractDeletedFiles(input) {
|
|
|
394
479
|
const content = input.tool_input.content || "";
|
|
395
480
|
if (content.trim().length === 0) {
|
|
396
481
|
const relPath = input.tool_input.file_path.replace(PROJECT_ROOT2 + "/", "");
|
|
397
|
-
if (relPath.startsWith("src/")) {
|
|
482
|
+
if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
|
|
398
483
|
files.push(relPath);
|
|
399
484
|
}
|
|
400
485
|
}
|
|
@@ -405,6 +490,12 @@ async function main() {
|
|
|
405
490
|
try {
|
|
406
491
|
const input = await readStdin();
|
|
407
492
|
const hookInput = JSON.parse(input);
|
|
493
|
+
const knowledgeWarning = checkKnowledgeFileProtection(hookInput);
|
|
494
|
+
if (knowledgeWarning) {
|
|
495
|
+
process.stdout.write(JSON.stringify({ message: knowledgeWarning }));
|
|
496
|
+
process.exit(0);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
408
499
|
const deletedFiles = extractDeletedFiles(hookInput);
|
|
409
500
|
if (deletedFiles.length === 0) {
|
|
410
501
|
process.exit(0);
|
|
@@ -415,10 +506,11 @@ async function main() {
|
|
|
415
506
|
process.exit(0);
|
|
416
507
|
return;
|
|
417
508
|
}
|
|
509
|
+
const SENTINEL_TABLE = "massu_sentinel";
|
|
418
510
|
try {
|
|
419
511
|
const tableExists = db.prepare(
|
|
420
|
-
|
|
421
|
-
).get();
|
|
512
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`
|
|
513
|
+
).get(SENTINEL_TABLE);
|
|
422
514
|
if (!tableExists) {
|
|
423
515
|
process.exit(0);
|
|
424
516
|
return;
|
|
@@ -445,6 +537,31 @@ async function main() {
|
|
|
445
537
|
msg.push("Create a migration plan before deleting these files.");
|
|
446
538
|
process.stdout.write(JSON.stringify({ message: msg.join("\n") }));
|
|
447
539
|
}
|
|
540
|
+
const pyFiles = deletedFiles.filter((f) => f.endsWith(".py"));
|
|
541
|
+
if (pyFiles.length > 0) {
|
|
542
|
+
try {
|
|
543
|
+
for (const pyFile of pyFiles) {
|
|
544
|
+
const importers = db.prepare(
|
|
545
|
+
"SELECT source_file FROM massu_py_imports WHERE target_file = ?"
|
|
546
|
+
).all(pyFile);
|
|
547
|
+
const routes = db.prepare(
|
|
548
|
+
"SELECT method, path FROM massu_py_routes WHERE file = ?"
|
|
549
|
+
).all(pyFile);
|
|
550
|
+
const models = db.prepare(
|
|
551
|
+
"SELECT class_name FROM massu_py_models WHERE file = ?"
|
|
552
|
+
).all(pyFile);
|
|
553
|
+
if (importers.length > 0 || routes.length > 0 || models.length > 0) {
|
|
554
|
+
const parts = [];
|
|
555
|
+
if (importers.length > 0) parts.push(`imported by ${importers.length} files`);
|
|
556
|
+
if (routes.length > 0) parts.push(`defines ${routes.length} routes`);
|
|
557
|
+
if (models.length > 0) parts.push(`defines ${models.length} models`);
|
|
558
|
+
const msg = `PYTHON IMPACT: "${pyFile}" ${parts.join(", ")}. Check dependents before deleting.`;
|
|
559
|
+
process.stdout.write(JSON.stringify({ message: msg }));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
}
|
|
448
565
|
} finally {
|
|
449
566
|
db.close();
|
|
450
567
|
}
|