@massu/core 1.9.3 → 1.10.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/dist/cli.js +561 -283
- package/dist/hooks/auto-learning-pipeline.js +30 -8
- package/dist/hooks/classify-failure.js +8 -1
- package/dist/hooks/cost-tracker.js +8 -1
- package/dist/hooks/fix-detector.js +3 -3
- package/dist/hooks/incident-pipeline.js +8 -1
- package/dist/hooks/post-edit-context.js +8 -1
- package/dist/hooks/post-tool-use.js +98 -1
- package/dist/hooks/pre-compact.js +8 -1
- package/dist/hooks/pre-delete-check.js +8 -1
- package/dist/hooks/quality-event.js +8 -1
- package/dist/hooks/session-end.js +18 -2
- package/dist/hooks/session-start.js +8 -1
- package/dist/hooks/user-prompt.js +8 -1
- package/package.json +2 -2
- package/src/backfill-sessions.ts +3 -2
- package/src/cli.ts +10 -0
- package/src/cloud-sync.ts +18 -0
- package/src/commands/doctor.ts +9 -14
- package/src/commands/hook-runner.ts +145 -0
- package/src/commands/init.ts +239 -29
- package/src/commands/install-commands.ts +10 -0
- package/src/commands/install-hooks.ts +2 -1
- package/src/commands/template-engine.ts +41 -0
- package/src/config.ts +2 -1
- package/src/hooks/auto-learning-pipeline.ts +43 -10
- package/src/hooks/fix-detector.ts +3 -3
- package/src/hooks/post-tool-use.ts +91 -1
- package/src/lib/hook-registry.ts +43 -0
- package/src/lib/memory-path.ts +49 -0
- package/src/security/registry-pubkey.generated.ts +1 -1
- package/src/tool-db-needs.ts +8 -2
- package/src/tools.ts +23 -7
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/hooks/auto-learning-pipeline.ts
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
6
6
|
import { existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync, readdirSync, statSync } from "fs";
|
|
7
7
|
import { tmpdir } from "os";
|
|
8
8
|
import { join } from "path";
|
|
@@ -490,6 +490,7 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
// src/hooks/auto-learning-pipeline.ts
|
|
493
|
+
var MAX_FULL_DIFF_BYTES = 2 * 1024 * 1024;
|
|
493
494
|
function getSessionFlagPath(sessionId) {
|
|
494
495
|
return join(tmpdir(), "massu-auto-learning", `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
495
496
|
}
|
|
@@ -516,13 +517,34 @@ async function main() {
|
|
|
516
517
|
}
|
|
517
518
|
let uncommittedFix = false;
|
|
518
519
|
try {
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
520
|
+
const nameOnly = execFileSync("git", ["diff", "--name-only"], {
|
|
521
|
+
cwd: root,
|
|
522
|
+
timeout: 3e3,
|
|
523
|
+
encoding: "utf-8",
|
|
524
|
+
maxBuffer: 1024 * 1024
|
|
525
|
+
});
|
|
526
|
+
if (nameOnly.trim()) {
|
|
527
|
+
const shortstat = execFileSync("git", ["diff", "--shortstat"], {
|
|
528
|
+
cwd: root,
|
|
529
|
+
timeout: 2e3,
|
|
530
|
+
encoding: "utf-8",
|
|
531
|
+
maxBuffer: 64 * 1024
|
|
532
|
+
});
|
|
533
|
+
const insertions = parseInt(shortstat.match(/(\d+) insertion/)?.[1] ?? "0", 10);
|
|
534
|
+
const deletions = parseInt(shortstat.match(/(\d+) deletion/)?.[1] ?? "0", 10);
|
|
535
|
+
const estimatedBytes = (insertions + deletions) * 80;
|
|
536
|
+
if (estimatedBytes <= MAX_FULL_DIFF_BYTES) {
|
|
537
|
+
const fullDiff = execFileSync("git", ["diff"], {
|
|
538
|
+
cwd: root,
|
|
539
|
+
timeout: 5e3,
|
|
540
|
+
encoding: "utf-8",
|
|
541
|
+
maxBuffer: MAX_FULL_DIFF_BYTES
|
|
542
|
+
});
|
|
543
|
+
const fixPatterns = (fullDiff.match(/^\+.*(try|except|catch|guard|throw|raise|assert|validate|if.*null|if.*nil|if.*None|if.*undefined)/gm) || []).length;
|
|
544
|
+
const removedBroken = (fullDiff.match(/^-.*(bug|broken|crash|wrong|incorrect|typo|fail|error|miss|stale)/gm) || []).length;
|
|
545
|
+
if (fixPatterns > 3 || removedBroken > 1) {
|
|
546
|
+
uncommittedFix = true;
|
|
547
|
+
}
|
|
526
548
|
}
|
|
527
549
|
}
|
|
528
550
|
} catch {
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import{createRequire as __cr}from"module";const require=__cr(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/hooks/fix-detector.ts
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
6
6
|
import { existsSync as existsSync2, appendFileSync, mkdirSync, readFileSync as readFileSync2 } from "fs";
|
|
7
7
|
import { tmpdir } from "os";
|
|
8
8
|
import { join } from "path";
|
|
@@ -565,9 +565,9 @@ async function main() {
|
|
|
565
565
|
const root = getProjectRoot();
|
|
566
566
|
let diff = "";
|
|
567
567
|
try {
|
|
568
|
-
diff =
|
|
568
|
+
diff = execFileSync("git", ["diff", "--", filePath], { cwd: root, timeout: 3e3, encoding: "utf-8" });
|
|
569
569
|
if (!diff) {
|
|
570
|
-
diff =
|
|
570
|
+
diff = execFileSync("git", ["diff", "HEAD", "--", filePath], { cwd: root, timeout: 3e3, encoding: "utf-8" });
|
|
571
571
|
}
|
|
572
572
|
} catch {
|
|
573
573
|
process.exit(0);
|
|
@@ -11,6 +11,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { parse as parseYaml } from "yaml";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
// src/lib/memory-path.ts
|
|
16
|
+
function encodeMemoryDirName(projectRoot) {
|
|
17
|
+
return projectRoot.replace(/\//g, "-");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/config.ts
|
|
14
21
|
var DomainConfigSchema = z.object({
|
|
15
22
|
name: z.string().default("Unknown"),
|
|
16
23
|
routers: z.array(z.string()).default([]),
|
|
@@ -515,7 +522,7 @@ function getResolvedPaths() {
|
|
|
515
522
|
plansDir: resolve(root, "docs/plans"),
|
|
516
523
|
docsDir: resolve(root, "docs"),
|
|
517
524
|
claudeDir: resolve(root, claudeDirName),
|
|
518
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
525
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
519
526
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
520
527
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
521
528
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -10,6 +10,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { parse as parseYaml } from "yaml";
|
|
12
12
|
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
// src/lib/memory-path.ts
|
|
15
|
+
function encodeMemoryDirName(projectRoot) {
|
|
16
|
+
return projectRoot.replace(/\//g, "-");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/config.ts
|
|
13
20
|
var DomainConfigSchema = z.object({
|
|
14
21
|
name: z.string().default("Unknown"),
|
|
15
22
|
routers: z.array(z.string()).default([]),
|
|
@@ -514,7 +521,7 @@ function getResolvedPaths() {
|
|
|
514
521
|
plansDir: resolve(root, "docs/plans"),
|
|
515
522
|
docsDir: resolve(root, "docs"),
|
|
516
523
|
claudeDir: resolve(root, claudeDirName),
|
|
517
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
524
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
518
525
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
519
526
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
520
527
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -1448,6 +1455,32 @@ function trackModification(db, featureKey) {
|
|
|
1448
1455
|
`).run(featureKey);
|
|
1449
1456
|
}
|
|
1450
1457
|
}
|
|
1458
|
+
function recordTestResult(db, featureKey, passing, failing) {
|
|
1459
|
+
const existing = db.prepare(
|
|
1460
|
+
"SELECT * FROM feature_health WHERE feature_key = ?"
|
|
1461
|
+
).get(featureKey);
|
|
1462
|
+
const healthScore = calculateHealthScore(passing, failing, 0, (/* @__PURE__ */ new Date()).toISOString(), existing?.last_modified);
|
|
1463
|
+
db.prepare(`
|
|
1464
|
+
INSERT INTO feature_health
|
|
1465
|
+
(feature_key, last_tested, test_coverage_pct, health_score, tests_passing, tests_failing, modifications_since_test)
|
|
1466
|
+
VALUES (?, datetime('now'), ?, ?, ?, ?, 0)
|
|
1467
|
+
ON CONFLICT(feature_key) DO UPDATE SET
|
|
1468
|
+
last_tested = datetime('now'),
|
|
1469
|
+
health_score = ?,
|
|
1470
|
+
tests_passing = ?,
|
|
1471
|
+
tests_failing = ?,
|
|
1472
|
+
modifications_since_test = 0
|
|
1473
|
+
`).run(
|
|
1474
|
+
featureKey,
|
|
1475
|
+
passing > 0 ? passing / (passing + failing) * 100 : 0,
|
|
1476
|
+
healthScore,
|
|
1477
|
+
passing,
|
|
1478
|
+
failing,
|
|
1479
|
+
healthScore,
|
|
1480
|
+
passing,
|
|
1481
|
+
failing
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1451
1484
|
|
|
1452
1485
|
// src/import-resolver.ts
|
|
1453
1486
|
import { readFileSync as readFileSync2, existsSync as existsSync3, statSync } from "fs";
|
|
@@ -1952,6 +1985,26 @@ async function main() {
|
|
|
1952
1985
|
}
|
|
1953
1986
|
} catch (_securityErr) {
|
|
1954
1987
|
}
|
|
1988
|
+
try {
|
|
1989
|
+
if (tool_name === "Bash") {
|
|
1990
|
+
const command = tool_input.command ?? "";
|
|
1991
|
+
if (isTestRunnerCommand(command)) {
|
|
1992
|
+
const counts = parseTestRunOutput(tool_response ?? "");
|
|
1993
|
+
if (counts) {
|
|
1994
|
+
const modifiedFeatures = db.prepare(
|
|
1995
|
+
"SELECT feature_key FROM feature_health WHERE modifications_since_test > 0"
|
|
1996
|
+
).all();
|
|
1997
|
+
for (const row of modifiedFeatures) {
|
|
1998
|
+
recordTestResult(db, row.feature_key, counts.passing, counts.failing);
|
|
1999
|
+
}
|
|
2000
|
+
if (modifiedFeatures.length === 0) {
|
|
2001
|
+
recordTestResult(db, "_session_test_run", counts.passing, counts.failing);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
} catch (_testResultErr) {
|
|
2007
|
+
}
|
|
1955
2008
|
try {
|
|
1956
2009
|
if (tool_name === "Edit" || tool_name === "Write") {
|
|
1957
2010
|
const filePath = tool_input.file_path ?? "";
|
|
@@ -2027,6 +2080,50 @@ function updatePlanProgress(db, sessionId, progress) {
|
|
|
2027
2080
|
addSummary(db, sessionId, { planProgress: progressMap });
|
|
2028
2081
|
}
|
|
2029
2082
|
}
|
|
2083
|
+
function isTestRunnerCommand(command) {
|
|
2084
|
+
const trimmed = command.trim().toLowerCase();
|
|
2085
|
+
const stripped = trimmed.replace(/^cd\s+\S+\s*(&&|;)\s*/, "").replace(/^\(\s*cd\s+\S+\s*(&&|;)\s*/, "");
|
|
2086
|
+
const testRunnerPrefixes = [
|
|
2087
|
+
"npm test",
|
|
2088
|
+
"npm run test",
|
|
2089
|
+
"npx vitest",
|
|
2090
|
+
"npx jest",
|
|
2091
|
+
"vitest",
|
|
2092
|
+
"jest",
|
|
2093
|
+
"pnpm test",
|
|
2094
|
+
"pnpm run test",
|
|
2095
|
+
"yarn test",
|
|
2096
|
+
"pytest",
|
|
2097
|
+
"go test",
|
|
2098
|
+
"cargo test"
|
|
2099
|
+
];
|
|
2100
|
+
return testRunnerPrefixes.some((prefix) => stripped.startsWith(prefix));
|
|
2101
|
+
}
|
|
2102
|
+
function parseTestRunOutput(output) {
|
|
2103
|
+
const vitestSplit = output.match(/Tests?\s+(?:(\d+)\s+failed\s+\|\s+)?(\d+)\s+passed/);
|
|
2104
|
+
if (vitestSplit) {
|
|
2105
|
+
return {
|
|
2106
|
+
passing: parseInt(vitestSplit[2], 10),
|
|
2107
|
+
failing: vitestSplit[1] ? parseInt(vitestSplit[1], 10) : 0
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
const jest = output.match(/Tests?:\s+(?:(\d+)\s+failed,\s+)?(\d+)\s+passed/);
|
|
2111
|
+
if (jest) {
|
|
2112
|
+
return {
|
|
2113
|
+
passing: parseInt(jest[2], 10),
|
|
2114
|
+
failing: jest[1] ? parseInt(jest[1], 10) : 0
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
const pytestPassed = output.match(/(\d+)\s+passed/);
|
|
2118
|
+
const pytestFailed = output.match(/(\d+)\s+failed/);
|
|
2119
|
+
if (pytestPassed) {
|
|
2120
|
+
return {
|
|
2121
|
+
passing: parseInt(pytestPassed[1], 10),
|
|
2122
|
+
failing: pytestFailed ? parseInt(pytestFailed[1], 10) : 0
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
return null;
|
|
2126
|
+
}
|
|
2030
2127
|
function readStdin() {
|
|
2031
2128
|
return new Promise((resolve5) => {
|
|
2032
2129
|
let data = "";
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -11,6 +11,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { parse as parseYaml } from "yaml";
|
|
13
13
|
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
// src/lib/memory-path.ts
|
|
16
|
+
function encodeMemoryDirName(projectRoot) {
|
|
17
|
+
return projectRoot.replace(/\//g, "-");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/config.ts
|
|
14
21
|
var DomainConfigSchema = z.object({
|
|
15
22
|
name: z.string().default("Unknown"),
|
|
16
23
|
routers: z.array(z.string()).default([]),
|
|
@@ -515,7 +522,7 @@ function getResolvedPaths() {
|
|
|
515
522
|
plansDir: resolve(root, "docs/plans"),
|
|
516
523
|
docsDir: resolve(root, "docs"),
|
|
517
524
|
claudeDir: resolve(root, claudeDirName),
|
|
518
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
525
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
519
526
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
520
527
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
521
528
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -1411,6 +1418,7 @@ function estimateTokens(text) {
|
|
|
1411
1418
|
// src/cloud-sync.ts
|
|
1412
1419
|
var MAX_RETRIES = 3;
|
|
1413
1420
|
var RETRY_DELAYS = [1e3, 2e3, 4e3];
|
|
1421
|
+
var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS = 2e3;
|
|
1414
1422
|
async function syncToCloud(db, payload) {
|
|
1415
1423
|
const config = getConfig();
|
|
1416
1424
|
const cloud = config.cloud;
|
|
@@ -1436,6 +1444,7 @@ async function syncToCloud(db, payload) {
|
|
|
1436
1444
|
filteredPayload.audit = payload.audit;
|
|
1437
1445
|
}
|
|
1438
1446
|
let lastError = "";
|
|
1447
|
+
const requestTimeoutMs = cloud.requestTimeoutMs ?? DEFAULT_CLOUD_REQUEST_TIMEOUT_MS;
|
|
1439
1448
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
1440
1449
|
try {
|
|
1441
1450
|
const response = await fetch(endpoint, {
|
|
@@ -1444,7 +1453,11 @@ async function syncToCloud(db, payload) {
|
|
|
1444
1453
|
"Content-Type": "application/json",
|
|
1445
1454
|
"Authorization": `Bearer ${cloud.apiKey}`
|
|
1446
1455
|
},
|
|
1447
|
-
body: JSON.stringify(filteredPayload)
|
|
1456
|
+
body: JSON.stringify(filteredPayload),
|
|
1457
|
+
// P-H003: bounded request — AbortSignal.timeout fires AbortError when
|
|
1458
|
+
// the request stalls (DNS failure, TCP unreachable, slow server). Cleans
|
|
1459
|
+
// up before hook timeout kills the whole process.
|
|
1460
|
+
signal: AbortSignal.timeout(requestTimeoutMs)
|
|
1448
1461
|
});
|
|
1449
1462
|
if (!response.ok) {
|
|
1450
1463
|
lastError = `HTTP ${response.status}: ${response.statusText}`;
|
|
@@ -1469,6 +1482,9 @@ async function syncToCloud(db, payload) {
|
|
|
1469
1482
|
};
|
|
1470
1483
|
} catch (err) {
|
|
1471
1484
|
lastError = err instanceof Error ? err.message : String(err);
|
|
1485
|
+
if (err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError")) {
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1472
1488
|
if (attempt < MAX_RETRIES - 1) {
|
|
1473
1489
|
await sleep(RETRY_DELAYS[attempt]);
|
|
1474
1490
|
continue;
|
|
@@ -5804,6 +5804,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
5804
5804
|
import { homedir } from "os";
|
|
5805
5805
|
import { parse as parseYaml } from "yaml";
|
|
5806
5806
|
import { z } from "zod";
|
|
5807
|
+
|
|
5808
|
+
// src/lib/memory-path.ts
|
|
5809
|
+
function encodeMemoryDirName(projectRoot) {
|
|
5810
|
+
return projectRoot.replace(/\//g, "-");
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
// src/config.ts
|
|
5807
5814
|
var DomainConfigSchema = z.object({
|
|
5808
5815
|
name: z.string().default("Unknown"),
|
|
5809
5816
|
routers: z.array(z.string()).default([]),
|
|
@@ -6308,7 +6315,7 @@ function getResolvedPaths() {
|
|
|
6308
6315
|
plansDir: resolve(root, "docs/plans"),
|
|
6309
6316
|
docsDir: resolve(root, "docs"),
|
|
6310
6317
|
claudeDir: resolve(root, claudeDirName),
|
|
6311
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
6318
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
6312
6319
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
6313
6320
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
6314
6321
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
|
@@ -12,6 +12,13 @@ import { existsSync, readFileSync } from "fs";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { parse as parseYaml } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
// src/lib/memory-path.ts
|
|
17
|
+
function encodeMemoryDirName(projectRoot) {
|
|
18
|
+
return projectRoot.replace(/\//g, "-");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/config.ts
|
|
15
22
|
var DomainConfigSchema = z.object({
|
|
16
23
|
name: z.string().default("Unknown"),
|
|
17
24
|
routers: z.array(z.string()).default([]),
|
|
@@ -516,7 +523,7 @@ function getResolvedPaths() {
|
|
|
516
523
|
plansDir: resolve(root, "docs/plans"),
|
|
517
524
|
docsDir: resolve(root, "docs"),
|
|
518
525
|
claudeDir: resolve(root, claudeDirName),
|
|
519
|
-
memoryDir: resolve(homedir(), claudeDirName, "projects", root
|
|
526
|
+
memoryDir: resolve(homedir(), claudeDirName, "projects", encodeMemoryDirName(root), "memory"),
|
|
520
527
|
sessionStatePath: resolve(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
521
528
|
sessionArchivePath: resolve(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
522
529
|
mcpJsonPath: resolve(root, ".mcp.json"),
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free /
|
|
5
|
+
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 59 workflow commands, 11 agents, 20+ patterns",
|
|
6
6
|
"main": "src/server.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/server.ts",
|
package/src/backfill-sessions.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { getMemoryDb, createSession, addObservation, addSummary, addUserPrompt,
|
|
|
14
14
|
import { parseTranscript, extractUserMessages, getLastAssistantMessage } from './transcript-parser.ts';
|
|
15
15
|
import { extractObservationsFromEntries } from './observation-extractor.ts';
|
|
16
16
|
import { getProjectRoot, getConfig } from './config.ts';
|
|
17
|
+
import { encodeMemoryDirName } from './lib/memory-path.ts';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Auto-detect the Claude Code project transcript directory.
|
|
@@ -23,8 +24,8 @@ function findTranscriptDir(): string {
|
|
|
23
24
|
const home = process.env.HOME ?? '~';
|
|
24
25
|
const projectRoot = getProjectRoot();
|
|
25
26
|
const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
|
|
26
|
-
// Claude Code escapes the path by replacing / with
|
|
27
|
-
const escapedPath = projectRoot
|
|
27
|
+
// Claude Code escapes the path by replacing / with -. Shared helper is SoT.
|
|
28
|
+
const escapedPath = encodeMemoryDirName(projectRoot);
|
|
28
29
|
const candidate = resolve(home, `${claudeDirName}/projects`, escapedPath);
|
|
29
30
|
if (existsSync(candidate)) return candidate;
|
|
30
31
|
// Fallback: scan projects dir for directories matching the project name
|
package/src/cli.ts
CHANGED
|
@@ -42,6 +42,16 @@ async function main(): Promise<void> {
|
|
|
42
42
|
await runInstallHooks();
|
|
43
43
|
break;
|
|
44
44
|
}
|
|
45
|
+
case 'hook-runner': {
|
|
46
|
+
// Dynamic hook dispatcher — invoked by Claude Code's hook command lines
|
|
47
|
+
// (settings.json -> `npx -y @massu/core@<version> hook-runner <name>`).
|
|
48
|
+
// Closes P-003 by resolving the hook file at fire-time instead of baking
|
|
49
|
+
// an absolute npx-cache path at install-time.
|
|
50
|
+
const { runHookRunner } = await import('./commands/hook-runner.ts');
|
|
51
|
+
const result = await runHookRunner(args.slice(1));
|
|
52
|
+
process.exit(result.exitCode);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
45
55
|
case 'install-commands': {
|
|
46
56
|
const { runInstallCommands } = await import('./commands/install-commands.ts');
|
|
47
57
|
await runInstallCommands();
|
package/src/cloud-sync.ts
CHANGED
|
@@ -60,6 +60,12 @@ export interface SyncResult {
|
|
|
60
60
|
const MAX_RETRIES = 3;
|
|
61
61
|
const RETRY_DELAYS = [1000, 2000, 4000]; // exponential backoff
|
|
62
62
|
|
|
63
|
+
// P-H003 (plan-stage-c-high-batch): bound each HTTP request so offline
|
|
64
|
+
// customers don't burn the entire Stop-hook 15s budget on a single
|
|
65
|
+
// unreachable endpoint. Default 2_000ms is well under hook timeout while
|
|
66
|
+
// still tolerating typical latency. Override via config.cloud.requestTimeoutMs.
|
|
67
|
+
const DEFAULT_CLOUD_REQUEST_TIMEOUT_MS = 2_000;
|
|
68
|
+
|
|
63
69
|
/**
|
|
64
70
|
* Sync data to the cloud endpoint.
|
|
65
71
|
* Respects config flags for selective sync.
|
|
@@ -103,6 +109,8 @@ export async function syncToCloud(
|
|
|
103
109
|
|
|
104
110
|
// Attempt sync with retry
|
|
105
111
|
let lastError = '';
|
|
112
|
+
const requestTimeoutMs = (cloud as { requestTimeoutMs?: number }).requestTimeoutMs
|
|
113
|
+
?? DEFAULT_CLOUD_REQUEST_TIMEOUT_MS;
|
|
106
114
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
107
115
|
try {
|
|
108
116
|
const response = await fetch(endpoint, {
|
|
@@ -112,6 +120,10 @@ export async function syncToCloud(
|
|
|
112
120
|
'Authorization': `Bearer ${cloud.apiKey}`,
|
|
113
121
|
},
|
|
114
122
|
body: JSON.stringify(filteredPayload),
|
|
123
|
+
// P-H003: bounded request — AbortSignal.timeout fires AbortError when
|
|
124
|
+
// the request stalls (DNS failure, TCP unreachable, slow server). Cleans
|
|
125
|
+
// up before hook timeout kills the whole process.
|
|
126
|
+
signal: AbortSignal.timeout(requestTimeoutMs),
|
|
115
127
|
});
|
|
116
128
|
|
|
117
129
|
if (!response.ok) {
|
|
@@ -140,6 +152,12 @@ export async function syncToCloud(
|
|
|
140
152
|
};
|
|
141
153
|
} catch (err) {
|
|
142
154
|
lastError = err instanceof Error ? err.message : String(err);
|
|
155
|
+
// P-H003: AbortError from AbortSignal.timeout means the request stalled
|
|
156
|
+
// (customer offline / DNS failure / unreachable). Don't burn the remaining
|
|
157
|
+
// hook budget retrying; queue for later and bail.
|
|
158
|
+
if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError')) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
143
161
|
if (attempt < MAX_RETRIES - 1) {
|
|
144
162
|
await sleep(RETRY_DELAYS[attempt]);
|
|
145
163
|
continue;
|
package/src/commands/doctor.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 1. massu.config.yaml exists and parses correctly
|
|
9
9
|
* 2. .mcp.json has massu entry
|
|
10
10
|
* 3. .claude/settings.local.json has hooks config
|
|
11
|
-
* 4. All
|
|
11
|
+
* 4. All compiled hook files exist (count sourced from lib/hook-registry.ts SoT)
|
|
12
12
|
* 5. Knowledge DB exists (.massu/memory.db)
|
|
13
13
|
* 6. Memory directory exists (~/.claude/projects/.../memory/)
|
|
14
14
|
* 7. Shell hooks wired in settings.local.json
|
|
@@ -24,6 +24,7 @@ import { parse as parseYaml } from 'yaml';
|
|
|
24
24
|
import { getConfig, getResolvedPaths } from '../config.ts';
|
|
25
25
|
import { getCurrentTier, getLicenseInfo, daysUntilExpiry } from '../license.ts';
|
|
26
26
|
import { readSettingsAtPath } from '../lib/settings-local.ts';
|
|
27
|
+
import { getExpectedHookFiles } from '../lib/hook-registry.ts';
|
|
27
28
|
|
|
28
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
30
|
const __dirname = dirname(__filename);
|
|
@@ -42,19 +43,13 @@ interface CheckResult {
|
|
|
42
43
|
// Hook Files
|
|
43
44
|
// ============================================================
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'post-edit-context.js',
|
|
53
|
-
'security-gate.js',
|
|
54
|
-
'cost-tracker.js',
|
|
55
|
-
'quality-event.js',
|
|
56
|
-
'intent-suggester.js',
|
|
57
|
-
];
|
|
46
|
+
// P-H001 (plan-stage-c-high-batch): drift-fix. EXPECTED_HOOKS now consumes
|
|
47
|
+
// the lib/hook-registry.ts SoT; the registry-parity drift-guard test asserts
|
|
48
|
+
// this list matches `src/hooks/*.ts` AND `buildHooksConfig()`. Previously
|
|
49
|
+
// a hand-maintained list of 11 entries silently drifted from the 16 hooks
|
|
50
|
+
// actually registered by installHooks(), letting hook files go missing
|
|
51
|
+
// without doctor noticing.
|
|
52
|
+
const EXPECTED_HOOKS: readonly string[] = getExpectedHookFiles();
|
|
58
53
|
|
|
59
54
|
// ============================================================
|
|
60
55
|
// Individual Checks
|