@rely-ai/caliber 1.12.14 → 1.12.15
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/bin.js +702 -560
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -2443,9 +2443,19 @@ function buildGeneratePrompt(fingerprint, targetAgent, prompt, failingChecks, cu
|
|
|
2443
2443
|
if (isTargetedFix) {
|
|
2444
2444
|
parts.push(`TARGETED FIX MODE \u2014 current score: ${currentScore}/100, target: ${targetAgent}`);
|
|
2445
2445
|
parts.push(`
|
|
2446
|
-
The existing config is already high quality. ONLY fix these specific failing checks
|
|
2446
|
+
The existing config is already high quality. ONLY fix these specific failing checks:
|
|
2447
|
+
`);
|
|
2447
2448
|
for (const check of failingChecks) {
|
|
2448
|
-
|
|
2449
|
+
if (check.fix) {
|
|
2450
|
+
parts.push(`- **${check.name}**`);
|
|
2451
|
+
parts.push(` Action: ${check.fix.instruction}`);
|
|
2452
|
+
if (check.fix.data && Object.keys(check.fix.data).length > 0) {
|
|
2453
|
+
const dataStr = Object.entries(check.fix.data).map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : String(v)}`).join("; ");
|
|
2454
|
+
parts.push(` Data: ${dataStr}`);
|
|
2455
|
+
}
|
|
2456
|
+
} else {
|
|
2457
|
+
parts.push(`- ${check.name}${check.suggestion ? `: ${check.suggestion}` : ""}`);
|
|
2458
|
+
}
|
|
2449
2459
|
}
|
|
2450
2460
|
if (passingChecks && passingChecks.length > 0) {
|
|
2451
2461
|
parts.push(`
|
|
@@ -2460,9 +2470,10 @@ IMPORTANT RULES FOR TARGETED FIX:
|
|
|
2460
2470
|
- Do NOT rewrite, restructure, rephrase, or make cosmetic changes.
|
|
2461
2471
|
- Preserve the existing content as-is except for targeted fixes.
|
|
2462
2472
|
- If a skill file is not related to a failing check, return it EXACTLY as-is, character for character.
|
|
2463
|
-
- For
|
|
2464
|
-
- For
|
|
2465
|
-
- For
|
|
2473
|
+
- For reference accuracy issues: DELETE non-existent paths. Do NOT replace with guessed paths.
|
|
2474
|
+
- For concise config issues: Remove the least important lines to get under the token limit. Do NOT add new content.
|
|
2475
|
+
- For grounding issues: Add references to the listed project directories in the appropriate sections.
|
|
2476
|
+
- Every path or name you reference MUST exist in the project \u2014 use the file tree provided below.`);
|
|
2466
2477
|
} else if (hasExistingConfigs) {
|
|
2467
2478
|
parts.push(`Audit and improve the existing coding agent configuration for target: ${targetAgent}`);
|
|
2468
2479
|
} else {
|
|
@@ -3728,12 +3739,12 @@ async function runInteractiveProviderSetup(options) {
|
|
|
3728
3739
|
}
|
|
3729
3740
|
|
|
3730
3741
|
// src/scoring/index.ts
|
|
3731
|
-
import { existsSync as
|
|
3742
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3732
3743
|
import { join as join8 } from "path";
|
|
3733
3744
|
|
|
3734
3745
|
// src/scoring/checks/existence.ts
|
|
3735
|
-
import { existsSync as
|
|
3736
|
-
import { join as
|
|
3746
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
3747
|
+
import { join as join2 } from "path";
|
|
3737
3748
|
|
|
3738
3749
|
// src/scoring/constants.ts
|
|
3739
3750
|
var POINTS_CLAUDE_MD_EXISTS = 6;
|
|
@@ -3744,52 +3755,51 @@ var POINTS_SKILLS_BONUS_CAP = 2;
|
|
|
3744
3755
|
var POINTS_CURSOR_MDC_RULES = 3;
|
|
3745
3756
|
var POINTS_MCP_SERVERS = 3;
|
|
3746
3757
|
var POINTS_CROSS_PLATFORM_PARITY = 2;
|
|
3747
|
-
var
|
|
3748
|
-
var
|
|
3749
|
-
var
|
|
3758
|
+
var POINTS_EXECUTABLE_CONTENT = 8;
|
|
3759
|
+
var POINTS_CONCISE_CONFIG = 6;
|
|
3760
|
+
var POINTS_CONCRETENESS = 4;
|
|
3750
3761
|
var POINTS_NO_DIR_TREE = 3;
|
|
3751
3762
|
var POINTS_NO_DUPLICATES = 2;
|
|
3752
|
-
var
|
|
3753
|
-
var
|
|
3754
|
-
var
|
|
3755
|
-
var
|
|
3756
|
-
var
|
|
3757
|
-
var POINTS_PATHS_VALID = 4;
|
|
3758
|
-
var POINTS_CONFIG_DRIFT = 5;
|
|
3763
|
+
var POINTS_HAS_STRUCTURE = 2;
|
|
3764
|
+
var POINTS_PROJECT_GROUNDING = 12;
|
|
3765
|
+
var POINTS_REFERENCE_DENSITY = 8;
|
|
3766
|
+
var POINTS_REFERENCES_VALID = 8;
|
|
3767
|
+
var POINTS_CONFIG_DRIFT = 7;
|
|
3759
3768
|
var POINTS_FRESHNESS = 4;
|
|
3760
3769
|
var POINTS_NO_SECRETS = 4;
|
|
3761
3770
|
var POINTS_PERMISSIONS = 2;
|
|
3762
3771
|
var POINTS_HOOKS = 2;
|
|
3763
3772
|
var POINTS_AGENTS_MD = 1;
|
|
3764
3773
|
var POINTS_OPEN_SKILLS_FORMAT = 2;
|
|
3765
|
-
var
|
|
3766
|
-
{
|
|
3767
|
-
{
|
|
3768
|
-
{
|
|
3769
|
-
{
|
|
3774
|
+
var TOKEN_BUDGET_THRESHOLDS = [
|
|
3775
|
+
{ maxTokens: 2e3, points: 6 },
|
|
3776
|
+
{ maxTokens: 3500, points: 5 },
|
|
3777
|
+
{ maxTokens: 5e3, points: 4 },
|
|
3778
|
+
{ maxTokens: 8e3, points: 2 },
|
|
3779
|
+
{ maxTokens: 12e3, points: 1 }
|
|
3780
|
+
];
|
|
3781
|
+
var CODE_BLOCK_THRESHOLDS = [
|
|
3782
|
+
{ minBlocks: 3, points: 8 },
|
|
3783
|
+
{ minBlocks: 2, points: 6 },
|
|
3784
|
+
{ minBlocks: 1, points: 3 }
|
|
3770
3785
|
];
|
|
3771
|
-
var
|
|
3772
|
-
{
|
|
3773
|
-
{
|
|
3774
|
-
{
|
|
3775
|
-
{
|
|
3786
|
+
var FRESHNESS_COMMIT_THRESHOLDS = [
|
|
3787
|
+
{ maxCommits: 5, points: 4 },
|
|
3788
|
+
{ maxCommits: 15, points: 3 },
|
|
3789
|
+
{ maxCommits: 30, points: 2 },
|
|
3790
|
+
{ maxCommits: 60, points: 1 }
|
|
3776
3791
|
];
|
|
3777
|
-
var
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
/maintain\s+readability/i,
|
|
3783
|
-
/keep\s+it\s+simple/i,
|
|
3784
|
-
/use\s+appropriate\s+patterns/i,
|
|
3785
|
-
/follow\s+coding\s+standards/i
|
|
3792
|
+
var CONCRETENESS_THRESHOLDS = [
|
|
3793
|
+
{ minRatio: 0.7, points: 4 },
|
|
3794
|
+
{ minRatio: 0.5, points: 3 },
|
|
3795
|
+
{ minRatio: 0.3, points: 2 },
|
|
3796
|
+
{ minRatio: 0.15, points: 1 }
|
|
3786
3797
|
];
|
|
3787
|
-
var
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
/npx\s+tsc/i
|
|
3798
|
+
var GROUNDING_THRESHOLDS = [
|
|
3799
|
+
{ minRatio: 0.5, points: 12 },
|
|
3800
|
+
{ minRatio: 0.35, points: 9 },
|
|
3801
|
+
{ minRatio: 0.2, points: 6 },
|
|
3802
|
+
{ minRatio: 0.1, points: 3 }
|
|
3793
3803
|
];
|
|
3794
3804
|
var SECRET_PATTERNS = [
|
|
3795
3805
|
/sk-[a-zA-Z0-9]{20,}/,
|
|
@@ -3800,12 +3810,14 @@ var SECRET_PATTERNS = [
|
|
|
3800
3810
|
/xox[bpors]-[a-zA-Z0-9\-]{10,}/,
|
|
3801
3811
|
/(?:password|secret|token|api_key)\s*[:=]\s*["'][^"']{8,}["']/i
|
|
3802
3812
|
];
|
|
3803
|
-
var
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3813
|
+
var SECRET_PLACEHOLDER_PATTERNS = [
|
|
3814
|
+
/your[_-]/i,
|
|
3815
|
+
/xxx/i,
|
|
3816
|
+
/example/i,
|
|
3817
|
+
/placeholder/i,
|
|
3818
|
+
/TODO/i,
|
|
3819
|
+
/CHANGE[_-]?ME/i,
|
|
3820
|
+
/<[^>]+>/
|
|
3809
3821
|
];
|
|
3810
3822
|
var CURSOR_ONLY_CHECKS = /* @__PURE__ */ new Set([
|
|
3811
3823
|
"cursor_rules_exist",
|
|
@@ -3822,6 +3834,9 @@ var BOTH_ONLY_CHECKS = /* @__PURE__ */ new Set([
|
|
|
3822
3834
|
var CODEX_ONLY_CHECKS = /* @__PURE__ */ new Set([
|
|
3823
3835
|
"codex_agents_md_exists"
|
|
3824
3836
|
]);
|
|
3837
|
+
var NON_CODEX_CHECKS = /* @__PURE__ */ new Set([
|
|
3838
|
+
"agents_md_exists"
|
|
3839
|
+
]);
|
|
3825
3840
|
var GRADE_THRESHOLDS = [
|
|
3826
3841
|
{ minScore: 85, grade: "A" },
|
|
3827
3842
|
{ minScore: 70, grade: "B" },
|
|
@@ -3836,187 +3851,10 @@ function computeGrade(score) {
|
|
|
3836
3851
|
return "F";
|
|
3837
3852
|
}
|
|
3838
3853
|
|
|
3839
|
-
// src/scoring/checks/coverage.ts
|
|
3840
|
-
import { readFileSync as readFileSync2, readdirSync } from "fs";
|
|
3841
|
-
import { join as join2 } from "path";
|
|
3842
|
-
function readFileOrNull2(path27) {
|
|
3843
|
-
try {
|
|
3844
|
-
return readFileSync2(path27, "utf-8");
|
|
3845
|
-
} catch {
|
|
3846
|
-
return null;
|
|
3847
|
-
}
|
|
3848
|
-
}
|
|
3849
|
-
function collectAllConfigContent(dir) {
|
|
3850
|
-
const parts = [];
|
|
3851
|
-
const claudeMd = readFileOrNull2(join2(dir, "CLAUDE.md"));
|
|
3852
|
-
if (claudeMd) parts.push(claudeMd);
|
|
3853
|
-
const cursorrules = readFileOrNull2(join2(dir, ".cursorrules"));
|
|
3854
|
-
if (cursorrules) parts.push(cursorrules);
|
|
3855
|
-
for (const skillsDir of [join2(dir, ".claude", "skills"), join2(dir, ".cursor", "skills")]) {
|
|
3856
|
-
try {
|
|
3857
|
-
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
3858
|
-
for (const entry of entries) {
|
|
3859
|
-
if (entry.isDirectory()) {
|
|
3860
|
-
const skill = readFileOrNull2(join2(skillsDir, entry.name, "SKILL.md"));
|
|
3861
|
-
if (skill) parts.push(skill);
|
|
3862
|
-
}
|
|
3863
|
-
}
|
|
3864
|
-
} catch {
|
|
3865
|
-
}
|
|
3866
|
-
}
|
|
3867
|
-
try {
|
|
3868
|
-
const rulesDir = join2(dir, ".cursor", "rules");
|
|
3869
|
-
const mdcFiles = readdirSync(rulesDir).filter((f) => f.endsWith(".mdc"));
|
|
3870
|
-
for (const f of mdcFiles) {
|
|
3871
|
-
const content = readFileOrNull2(join2(rulesDir, f));
|
|
3872
|
-
if (content) parts.push(content);
|
|
3873
|
-
}
|
|
3874
|
-
} catch {
|
|
3875
|
-
}
|
|
3876
|
-
return parts.join("\n").toLowerCase();
|
|
3877
|
-
}
|
|
3878
|
-
function hasExternalServices(dir) {
|
|
3879
|
-
const allDeps = [
|
|
3880
|
-
...extractNpmDeps(dir),
|
|
3881
|
-
...extractPythonDeps(dir),
|
|
3882
|
-
...extractGoDeps(dir),
|
|
3883
|
-
...extractRustDeps(dir)
|
|
3884
|
-
];
|
|
3885
|
-
return detectServices(dir, allDeps).length > 0;
|
|
3886
|
-
}
|
|
3887
|
-
function detectServices(dir, deps) {
|
|
3888
|
-
const serviceMap = {
|
|
3889
|
-
"postgresql": ["pg", "postgres", "knex", "drizzle-orm", "prisma", "sequelize", "typeorm", "psycopg2", "sqlalchemy", "diesel"],
|
|
3890
|
-
"mongodb": ["mongoose", "mongodb", "mongod", "pymongo", "motor"],
|
|
3891
|
-
"redis": ["redis", "ioredis", "bull", "bullmq", "aioredis"],
|
|
3892
|
-
"supabase": ["@supabase/supabase-js", "supabase", "supabase-py"],
|
|
3893
|
-
"firebase": ["firebase", "firebase-admin", "@firebase/app"],
|
|
3894
|
-
"aws": ["aws-sdk", "@aws-sdk/client-s3", "boto3", "aws-cdk"],
|
|
3895
|
-
"stripe": ["stripe", "@stripe/stripe-js"],
|
|
3896
|
-
"github": ["@octokit/rest", "octokit", "pygithub"],
|
|
3897
|
-
"slack": ["@slack/web-api", "@slack/bolt", "slack-sdk"],
|
|
3898
|
-
"sentry": ["@sentry/node", "@sentry/react", "sentry-sdk"]
|
|
3899
|
-
};
|
|
3900
|
-
const detected = [];
|
|
3901
|
-
const depSet = new Set(deps.map((d) => d.toLowerCase()));
|
|
3902
|
-
for (const [service, markers] of Object.entries(serviceMap)) {
|
|
3903
|
-
if (markers.some((m) => depSet.has(m))) {
|
|
3904
|
-
detected.push(service);
|
|
3905
|
-
}
|
|
3906
|
-
}
|
|
3907
|
-
return detected;
|
|
3908
|
-
}
|
|
3909
|
-
function getConfiguredMcpServers(dir) {
|
|
3910
|
-
const servers = /* @__PURE__ */ new Set();
|
|
3911
|
-
const mcpFiles = [
|
|
3912
|
-
".mcp.json",
|
|
3913
|
-
".cursor/mcp.json",
|
|
3914
|
-
".claude/settings.local.json",
|
|
3915
|
-
".claude/settings.json"
|
|
3916
|
-
];
|
|
3917
|
-
for (const rel of mcpFiles) {
|
|
3918
|
-
try {
|
|
3919
|
-
const content = readFileSync2(join2(dir, rel), "utf-8");
|
|
3920
|
-
const parsed = JSON.parse(content);
|
|
3921
|
-
const mcpServers = parsed.mcpServers;
|
|
3922
|
-
if (mcpServers) {
|
|
3923
|
-
for (const name of Object.keys(mcpServers)) {
|
|
3924
|
-
servers.add(name.toLowerCase());
|
|
3925
|
-
}
|
|
3926
|
-
}
|
|
3927
|
-
} catch {
|
|
3928
|
-
}
|
|
3929
|
-
}
|
|
3930
|
-
return servers;
|
|
3931
|
-
}
|
|
3932
|
-
function checkCoverage(dir) {
|
|
3933
|
-
const checks = [];
|
|
3934
|
-
const allDeps = [
|
|
3935
|
-
...extractNpmDeps(dir),
|
|
3936
|
-
...extractPythonDeps(dir),
|
|
3937
|
-
...extractGoDeps(dir),
|
|
3938
|
-
...extractRustDeps(dir)
|
|
3939
|
-
];
|
|
3940
|
-
const configContent = collectAllConfigContent(dir);
|
|
3941
|
-
const mentionedDeps = [];
|
|
3942
|
-
const unmatchedDeps = [];
|
|
3943
|
-
for (const dep of allDeps) {
|
|
3944
|
-
const normalized = dep.replace(/^@[^/]+\//, "").toLowerCase();
|
|
3945
|
-
const variants = [
|
|
3946
|
-
normalized,
|
|
3947
|
-
normalized.replace(/-/g, "_"),
|
|
3948
|
-
normalized.replace(/_/g, "-"),
|
|
3949
|
-
normalized.replace(/-/g, "")
|
|
3950
|
-
];
|
|
3951
|
-
if (variants.some((v) => configContent.includes(v))) {
|
|
3952
|
-
mentionedDeps.push(dep);
|
|
3953
|
-
} else {
|
|
3954
|
-
unmatchedDeps.push(dep);
|
|
3955
|
-
}
|
|
3956
|
-
}
|
|
3957
|
-
const depCoverageRatio = allDeps.length > 0 ? mentionedDeps.length / allDeps.length : 1;
|
|
3958
|
-
const effectiveRatio = depCoverageRatio >= 0.85 ? 1 : depCoverageRatio;
|
|
3959
|
-
const depPoints = allDeps.length === 0 ? POINTS_DEP_COVERAGE : Math.round(effectiveRatio * POINTS_DEP_COVERAGE);
|
|
3960
|
-
const topUnmatched = unmatchedDeps.slice(0, 3);
|
|
3961
|
-
checks.push({
|
|
3962
|
-
id: "dep_coverage",
|
|
3963
|
-
name: "Dependency coverage",
|
|
3964
|
-
category: "coverage",
|
|
3965
|
-
maxPoints: POINTS_DEP_COVERAGE,
|
|
3966
|
-
earnedPoints: depPoints,
|
|
3967
|
-
passed: depCoverageRatio >= 0.5,
|
|
3968
|
-
detail: allDeps.length === 0 ? "No dependencies detected" : `${mentionedDeps.length}/${allDeps.length} deps mentioned in configs (${Math.round(depCoverageRatio * 100)}%)`,
|
|
3969
|
-
suggestion: topUnmatched.length > 0 ? `Missing coverage for: ${topUnmatched.join(", ")}${unmatchedDeps.length > 3 ? ` (+${unmatchedDeps.length - 3} more)` : ""}` : void 0
|
|
3970
|
-
});
|
|
3971
|
-
const detectedServices = detectServices(dir, allDeps);
|
|
3972
|
-
const mcpServers = getConfiguredMcpServers(dir);
|
|
3973
|
-
const mcpServerNames = Array.from(mcpServers).join(" ");
|
|
3974
|
-
const coveredServices = [];
|
|
3975
|
-
const uncoveredServices = [];
|
|
3976
|
-
for (const service of detectedServices) {
|
|
3977
|
-
if (mcpServerNames.includes(service) || configContent.includes(`${service} mcp`) || configContent.includes(`mcp.*${service}`)) {
|
|
3978
|
-
coveredServices.push(service);
|
|
3979
|
-
} else {
|
|
3980
|
-
uncoveredServices.push(service);
|
|
3981
|
-
}
|
|
3982
|
-
}
|
|
3983
|
-
const serviceCoverageRatio = detectedServices.length > 0 ? coveredServices.length / detectedServices.length : 1;
|
|
3984
|
-
const servicePoints = detectedServices.length === 0 ? POINTS_SERVICE_COVERAGE : Math.round(serviceCoverageRatio * POINTS_SERVICE_COVERAGE);
|
|
3985
|
-
checks.push({
|
|
3986
|
-
id: "service_coverage",
|
|
3987
|
-
name: "Service/MCP coverage",
|
|
3988
|
-
category: "coverage",
|
|
3989
|
-
maxPoints: POINTS_SERVICE_COVERAGE,
|
|
3990
|
-
earnedPoints: servicePoints,
|
|
3991
|
-
passed: serviceCoverageRatio >= 0.5,
|
|
3992
|
-
detail: detectedServices.length === 0 ? "No external services detected" : `${coveredServices.length}/${detectedServices.length} services have MCP/config coverage`,
|
|
3993
|
-
suggestion: uncoveredServices.length > 0 ? `No MCP server for: ${uncoveredServices.join(", ")} \u2014 consider adding MCP servers for these` : void 0
|
|
3994
|
-
});
|
|
3995
|
-
let mcpPoints;
|
|
3996
|
-
if (detectedServices.length === 0) {
|
|
3997
|
-
mcpPoints = POINTS_MCP_COVERAGE;
|
|
3998
|
-
} else if (mcpServers.size > 0) {
|
|
3999
|
-
mcpPoints = Math.round(serviceCoverageRatio * POINTS_MCP_COVERAGE);
|
|
4000
|
-
} else {
|
|
4001
|
-
mcpPoints = 0;
|
|
4002
|
-
}
|
|
4003
|
-
checks.push({
|
|
4004
|
-
id: "mcp_completeness",
|
|
4005
|
-
name: "MCP completeness",
|
|
4006
|
-
category: "coverage",
|
|
4007
|
-
maxPoints: POINTS_MCP_COVERAGE,
|
|
4008
|
-
earnedPoints: mcpPoints,
|
|
4009
|
-
passed: mcpPoints >= POINTS_MCP_COVERAGE / 2,
|
|
4010
|
-
detail: detectedServices.length === 0 ? "No external services detected (MCP not needed)" : mcpServers.size === 0 ? "No MCP servers configured" : `${mcpServers.size} MCP server${mcpServers.size === 1 ? "" : "s"} configured`,
|
|
4011
|
-
suggestion: mcpServers.size === 0 && detectedServices.length > 0 ? `Project uses ${detectedServices.join(", ")} but has no MCP servers` : void 0
|
|
4012
|
-
});
|
|
4013
|
-
return checks;
|
|
4014
|
-
}
|
|
4015
|
-
|
|
4016
3854
|
// src/scoring/checks/existence.ts
|
|
4017
3855
|
function countFiles(dir, pattern) {
|
|
4018
3856
|
try {
|
|
4019
|
-
return
|
|
3857
|
+
return readdirSync(dir, { recursive: true }).map(String).filter((f) => pattern.test(f));
|
|
4020
3858
|
} catch {
|
|
4021
3859
|
return [];
|
|
4022
3860
|
}
|
|
@@ -4032,7 +3870,7 @@ function hasMcpServers(dir) {
|
|
|
4032
3870
|
];
|
|
4033
3871
|
for (const rel of mcpFiles) {
|
|
4034
3872
|
try {
|
|
4035
|
-
const content =
|
|
3873
|
+
const content = readFileSync2(join2(dir, rel), "utf-8");
|
|
4036
3874
|
const parsed = JSON.parse(content);
|
|
4037
3875
|
const servers = parsed.mcpServers;
|
|
4038
3876
|
if (servers && Object.keys(servers).length > 0) {
|
|
@@ -4046,7 +3884,7 @@ function hasMcpServers(dir) {
|
|
|
4046
3884
|
}
|
|
4047
3885
|
function checkExistence(dir) {
|
|
4048
3886
|
const checks = [];
|
|
4049
|
-
const claudeMdExists =
|
|
3887
|
+
const claudeMdExists = existsSync2(join2(dir, "CLAUDE.md"));
|
|
4050
3888
|
checks.push({
|
|
4051
3889
|
id: "claude_md_exists",
|
|
4052
3890
|
name: "CLAUDE.md exists",
|
|
@@ -4055,10 +3893,15 @@ function checkExistence(dir) {
|
|
|
4055
3893
|
earnedPoints: claudeMdExists ? POINTS_CLAUDE_MD_EXISTS : 0,
|
|
4056
3894
|
passed: claudeMdExists,
|
|
4057
3895
|
detail: claudeMdExists ? "Found at project root" : "Not found",
|
|
4058
|
-
suggestion: claudeMdExists ? void 0 : "Create a CLAUDE.md with project context and commands"
|
|
3896
|
+
suggestion: claudeMdExists ? void 0 : "Create a CLAUDE.md with project context and commands",
|
|
3897
|
+
fix: claudeMdExists ? void 0 : {
|
|
3898
|
+
action: "create_file",
|
|
3899
|
+
data: { file: "CLAUDE.md" },
|
|
3900
|
+
instruction: "Create CLAUDE.md with project context, commands, architecture, and conventions."
|
|
3901
|
+
}
|
|
4059
3902
|
});
|
|
4060
|
-
const hasCursorrules =
|
|
4061
|
-
const cursorRulesDir =
|
|
3903
|
+
const hasCursorrules = existsSync2(join2(dir, ".cursorrules"));
|
|
3904
|
+
const cursorRulesDir = existsSync2(join2(dir, ".cursor", "rules"));
|
|
4062
3905
|
const cursorRulesExist = hasCursorrules || cursorRulesDir;
|
|
4063
3906
|
checks.push({
|
|
4064
3907
|
id: "cursor_rules_exist",
|
|
@@ -4068,9 +3911,14 @@ function checkExistence(dir) {
|
|
|
4068
3911
|
earnedPoints: cursorRulesExist ? POINTS_CURSOR_RULES_EXIST : 0,
|
|
4069
3912
|
passed: cursorRulesExist,
|
|
4070
3913
|
detail: hasCursorrules ? ".cursorrules found" : cursorRulesDir ? ".cursor/rules/ found" : "No Cursor rules",
|
|
4071
|
-
suggestion: cursorRulesExist ? void 0 : "Add .cursor/rules/ for Cursor users on your team"
|
|
3914
|
+
suggestion: cursorRulesExist ? void 0 : "Add .cursor/rules/ for Cursor users on your team",
|
|
3915
|
+
fix: cursorRulesExist ? void 0 : {
|
|
3916
|
+
action: "create_file",
|
|
3917
|
+
data: { file: ".cursor/rules/" },
|
|
3918
|
+
instruction: "Create .cursor/rules/ with project-specific Cursor rules."
|
|
3919
|
+
}
|
|
4072
3920
|
});
|
|
4073
|
-
const agentsMdExists =
|
|
3921
|
+
const agentsMdExists = existsSync2(join2(dir, "AGENTS.md"));
|
|
4074
3922
|
checks.push({
|
|
4075
3923
|
id: "codex_agents_md_exists",
|
|
4076
3924
|
name: "AGENTS.md exists",
|
|
@@ -4079,10 +3927,15 @@ function checkExistence(dir) {
|
|
|
4079
3927
|
earnedPoints: agentsMdExists ? POINTS_CLAUDE_MD_EXISTS : 0,
|
|
4080
3928
|
passed: agentsMdExists,
|
|
4081
3929
|
detail: agentsMdExists ? "Found at project root" : "Not found",
|
|
4082
|
-
suggestion: agentsMdExists ? void 0 : "Create AGENTS.md with project context for Codex"
|
|
3930
|
+
suggestion: agentsMdExists ? void 0 : "Create AGENTS.md with project context for Codex",
|
|
3931
|
+
fix: agentsMdExists ? void 0 : {
|
|
3932
|
+
action: "create_file",
|
|
3933
|
+
data: { file: "AGENTS.md" },
|
|
3934
|
+
instruction: "Create AGENTS.md with project context for Codex."
|
|
3935
|
+
}
|
|
4083
3936
|
});
|
|
4084
|
-
const claudeSkills = countFiles(
|
|
4085
|
-
const codexSkills = countFiles(
|
|
3937
|
+
const claudeSkills = countFiles(join2(dir, ".claude", "skills"), /\.(md|SKILL\.md)$/);
|
|
3938
|
+
const codexSkills = countFiles(join2(dir, ".agents", "skills"), /SKILL\.md$/);
|
|
4086
3939
|
const skillCount = claudeSkills.length + codexSkills.length;
|
|
4087
3940
|
const skillBase = skillCount >= 1 ? POINTS_SKILLS_EXIST : 0;
|
|
4088
3941
|
const skillBonus = Math.min((skillCount - 1) * POINTS_SKILLS_BONUS_PER_EXTRA, POINTS_SKILLS_BONUS_CAP);
|
|
@@ -4096,9 +3949,14 @@ function checkExistence(dir) {
|
|
|
4096
3949
|
earnedPoints: Math.min(skillPoints, maxSkillPoints),
|
|
4097
3950
|
passed: skillCount >= 1,
|
|
4098
3951
|
detail: skillCount === 0 ? "No skills found" : `${skillCount} skill${skillCount === 1 ? "" : "s"} found`,
|
|
4099
|
-
suggestion: skillCount === 0 ? "Add .claude/skills/ with project-specific workflows" : skillCount < 3 ? "Optimal is 2-3 focused skills
|
|
3952
|
+
suggestion: skillCount === 0 ? "Add .claude/skills/ with project-specific workflows" : skillCount < 3 ? "Optimal is 2-3 focused skills" : void 0,
|
|
3953
|
+
fix: skillCount === 0 ? {
|
|
3954
|
+
action: "create_skills",
|
|
3955
|
+
data: { currentCount: 0 },
|
|
3956
|
+
instruction: "Create .claude/skills/ with 2-3 project-specific workflow skills."
|
|
3957
|
+
} : void 0
|
|
4100
3958
|
});
|
|
4101
|
-
const mdcFiles = countFiles(
|
|
3959
|
+
const mdcFiles = countFiles(join2(dir, ".cursor", "rules"), /\.mdc$/);
|
|
4102
3960
|
const mdcCount = mdcFiles.length;
|
|
4103
3961
|
checks.push({
|
|
4104
3962
|
id: "cursor_mdc_rules",
|
|
@@ -4108,20 +3966,28 @@ function checkExistence(dir) {
|
|
|
4108
3966
|
earnedPoints: mdcCount >= 1 ? POINTS_CURSOR_MDC_RULES : 0,
|
|
4109
3967
|
passed: mdcCount >= 1,
|
|
4110
3968
|
detail: mdcCount === 0 ? "No .mdc rule files" : `${mdcCount} .mdc rule${mdcCount === 1 ? "" : "s"} found`,
|
|
4111
|
-
suggestion: mdcCount === 0 ? "Add .cursor/rules/*.mdc with frontmatter for Cursor" : void 0
|
|
3969
|
+
suggestion: mdcCount === 0 ? "Add .cursor/rules/*.mdc with frontmatter for Cursor" : void 0,
|
|
3970
|
+
fix: mdcCount === 0 ? {
|
|
3971
|
+
action: "create_mdc_rules",
|
|
3972
|
+
data: {},
|
|
3973
|
+
instruction: "Create .cursor/rules/*.mdc files with YAML frontmatter for Cursor."
|
|
3974
|
+
} : void 0
|
|
4112
3975
|
});
|
|
4113
3976
|
const mcp = hasMcpServers(dir);
|
|
4114
|
-
const hasServices = hasExternalServices(dir);
|
|
4115
|
-
const mcpPassed = mcp.count >= 1 || !hasServices;
|
|
4116
3977
|
checks.push({
|
|
4117
3978
|
id: "mcp_servers",
|
|
4118
3979
|
name: "MCP servers configured",
|
|
4119
3980
|
category: "existence",
|
|
4120
3981
|
maxPoints: POINTS_MCP_SERVERS,
|
|
4121
|
-
earnedPoints:
|
|
4122
|
-
passed:
|
|
4123
|
-
detail: mcp.count > 0 ? `${mcp.count} server${mcp.count === 1 ? "" : "s"} in ${mcp.sources.join(", ")}` :
|
|
4124
|
-
suggestion:
|
|
3982
|
+
earnedPoints: mcp.count >= 1 ? POINTS_MCP_SERVERS : 0,
|
|
3983
|
+
passed: mcp.count >= 1,
|
|
3984
|
+
detail: mcp.count > 0 ? `${mcp.count} server${mcp.count === 1 ? "" : "s"} in ${mcp.sources.join(", ")}` : "No MCP servers configured",
|
|
3985
|
+
suggestion: mcp.count === 0 ? "Configure MCP servers in .mcp.json for external service access" : void 0,
|
|
3986
|
+
fix: mcp.count === 0 ? {
|
|
3987
|
+
action: "configure_mcp",
|
|
3988
|
+
data: {},
|
|
3989
|
+
instruction: "Add MCP server configurations in .mcp.json for any external services the project uses."
|
|
3990
|
+
} : void 0
|
|
4125
3991
|
});
|
|
4126
3992
|
const hasClaudeConfigs = claudeMdExists || skillCount > 0;
|
|
4127
3993
|
const hasCursorConfigs = cursorRulesExist || mdcCount > 0;
|
|
@@ -4134,89 +4000,298 @@ function checkExistence(dir) {
|
|
|
4134
4000
|
earnedPoints: hasParity ? POINTS_CROSS_PLATFORM_PARITY : 0,
|
|
4135
4001
|
passed: hasParity,
|
|
4136
4002
|
detail: hasParity ? "Both Claude Code and Cursor configured" : hasClaudeConfigs ? "Only Claude Code \u2014 no Cursor configs" : hasCursorConfigs ? "Only Cursor \u2014 no Claude Code configs" : "Neither platform configured",
|
|
4137
|
-
suggestion: hasParity ? void 0 : "Add configs for both platforms so all teammates get context"
|
|
4003
|
+
suggestion: hasParity ? void 0 : "Add configs for both platforms so all teammates get context",
|
|
4004
|
+
fix: hasParity ? void 0 : {
|
|
4005
|
+
action: "add_platform",
|
|
4006
|
+
data: { hasClaude: hasClaudeConfigs, hasCursor: hasCursorConfigs },
|
|
4007
|
+
instruction: hasClaudeConfigs ? "Add Cursor rules (.cursor/rules/) for cross-platform support." : "Add CLAUDE.md for cross-platform support."
|
|
4008
|
+
}
|
|
4138
4009
|
});
|
|
4139
4010
|
return checks;
|
|
4140
4011
|
}
|
|
4141
4012
|
|
|
4142
4013
|
// src/scoring/checks/quality.ts
|
|
4143
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
4144
4014
|
import { join as join4 } from "path";
|
|
4145
|
-
|
|
4015
|
+
|
|
4016
|
+
// src/scoring/utils.ts
|
|
4017
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
4018
|
+
import { join as join3, relative } from "path";
|
|
4019
|
+
function readFileOrNull2(filePath) {
|
|
4146
4020
|
try {
|
|
4147
|
-
return
|
|
4021
|
+
return readFileSync3(filePath, "utf-8");
|
|
4148
4022
|
} catch {
|
|
4149
4023
|
return null;
|
|
4150
4024
|
}
|
|
4151
4025
|
}
|
|
4152
|
-
|
|
4153
|
-
|
|
4026
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
4027
|
+
"node_modules",
|
|
4028
|
+
".git",
|
|
4029
|
+
"dist",
|
|
4030
|
+
"build",
|
|
4031
|
+
"out",
|
|
4032
|
+
".next",
|
|
4033
|
+
".nuxt",
|
|
4034
|
+
"__pycache__",
|
|
4035
|
+
".venv",
|
|
4036
|
+
"venv",
|
|
4037
|
+
"env",
|
|
4038
|
+
".env",
|
|
4039
|
+
"target",
|
|
4040
|
+
"vendor",
|
|
4041
|
+
".cache",
|
|
4042
|
+
".parcel-cache",
|
|
4043
|
+
"coverage",
|
|
4044
|
+
".nyc_output",
|
|
4045
|
+
".turbo",
|
|
4046
|
+
".caliber",
|
|
4047
|
+
".claude",
|
|
4048
|
+
".cursor",
|
|
4049
|
+
".agents",
|
|
4050
|
+
".codex"
|
|
4051
|
+
]);
|
|
4052
|
+
var IGNORED_FILES = /* @__PURE__ */ new Set([
|
|
4053
|
+
".DS_Store",
|
|
4054
|
+
"Thumbs.db",
|
|
4055
|
+
".gitignore",
|
|
4056
|
+
".editorconfig",
|
|
4057
|
+
".prettierrc",
|
|
4058
|
+
".prettierignore",
|
|
4059
|
+
".eslintignore",
|
|
4060
|
+
"package-lock.json",
|
|
4061
|
+
"yarn.lock",
|
|
4062
|
+
"pnpm-lock.yaml",
|
|
4063
|
+
"bun.lockb"
|
|
4064
|
+
]);
|
|
4065
|
+
function collectProjectStructure(dir, maxDepth = 2) {
|
|
4066
|
+
const dirs = [];
|
|
4067
|
+
const files = [];
|
|
4068
|
+
function walk(currentDir, depth) {
|
|
4069
|
+
if (depth > maxDepth) return;
|
|
4070
|
+
try {
|
|
4071
|
+
const entries = readdirSync2(currentDir, { withFileTypes: true });
|
|
4072
|
+
for (const entry of entries) {
|
|
4073
|
+
const name = entry.name;
|
|
4074
|
+
if (name.startsWith(".") && IGNORED_DIRS.has(name)) continue;
|
|
4075
|
+
if (IGNORED_FILES.has(name)) continue;
|
|
4076
|
+
const rel = relative(dir, join3(currentDir, name));
|
|
4077
|
+
if (entry.isDirectory()) {
|
|
4078
|
+
if (IGNORED_DIRS.has(name)) continue;
|
|
4079
|
+
dirs.push(rel);
|
|
4080
|
+
walk(join3(currentDir, name), depth + 1);
|
|
4081
|
+
} else if (entry.isFile()) {
|
|
4082
|
+
files.push(rel);
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
} catch {
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
walk(dir, 0);
|
|
4089
|
+
return { dirs, files };
|
|
4154
4090
|
}
|
|
4091
|
+
function collectAllConfigContent(dir) {
|
|
4092
|
+
const parts = [];
|
|
4093
|
+
for (const file of ["CLAUDE.md", ".cursorrules", "AGENTS.md"]) {
|
|
4094
|
+
const content = readFileOrNull2(join3(dir, file));
|
|
4095
|
+
if (content) parts.push(content);
|
|
4096
|
+
}
|
|
4097
|
+
for (const skillsDir of [join3(dir, ".claude", "skills"), join3(dir, ".agents", "skills")]) {
|
|
4098
|
+
try {
|
|
4099
|
+
const entries = readdirSync2(skillsDir, { withFileTypes: true });
|
|
4100
|
+
for (const entry of entries) {
|
|
4101
|
+
if (entry.isDirectory()) {
|
|
4102
|
+
const skill = readFileOrNull2(join3(skillsDir, entry.name, "SKILL.md"));
|
|
4103
|
+
if (skill) parts.push(skill);
|
|
4104
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
4105
|
+
const content = readFileOrNull2(join3(skillsDir, entry.name));
|
|
4106
|
+
if (content) parts.push(content);
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
} catch {
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
try {
|
|
4113
|
+
const rulesDir = join3(dir, ".cursor", "rules");
|
|
4114
|
+
const mdcFiles = readdirSync2(rulesDir).filter((f) => f.endsWith(".mdc"));
|
|
4115
|
+
for (const f of mdcFiles) {
|
|
4116
|
+
const content = readFileOrNull2(join3(rulesDir, f));
|
|
4117
|
+
if (content) parts.push(content);
|
|
4118
|
+
}
|
|
4119
|
+
} catch {
|
|
4120
|
+
}
|
|
4121
|
+
return parts.join("\n");
|
|
4122
|
+
}
|
|
4123
|
+
function estimateTokens2(text) {
|
|
4124
|
+
return Math.ceil(text.length / 4);
|
|
4125
|
+
}
|
|
4126
|
+
function analyzeMarkdownStructure(content) {
|
|
4127
|
+
const lines = content.split("\n");
|
|
4128
|
+
let headingCount = 0;
|
|
4129
|
+
let h2Count = 0;
|
|
4130
|
+
let h3Count = 0;
|
|
4131
|
+
let codeBlockCount = 0;
|
|
4132
|
+
let codeBlockLines = 0;
|
|
4133
|
+
let listItemCount = 0;
|
|
4134
|
+
let inlineCodeCount = 0;
|
|
4135
|
+
let inCodeBlock = false;
|
|
4136
|
+
for (const line of lines) {
|
|
4137
|
+
const trimmed = line.trim();
|
|
4138
|
+
if (trimmed.startsWith("```")) {
|
|
4139
|
+
if (!inCodeBlock) codeBlockCount++;
|
|
4140
|
+
inCodeBlock = !inCodeBlock;
|
|
4141
|
+
continue;
|
|
4142
|
+
}
|
|
4143
|
+
if (inCodeBlock) {
|
|
4144
|
+
codeBlockLines++;
|
|
4145
|
+
continue;
|
|
4146
|
+
}
|
|
4147
|
+
if (trimmed.startsWith("## ") && !trimmed.startsWith("### ")) h2Count++;
|
|
4148
|
+
if (trimmed.startsWith("### ")) h3Count++;
|
|
4149
|
+
if (trimmed.startsWith("#")) headingCount++;
|
|
4150
|
+
if (/^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) listItemCount++;
|
|
4151
|
+
const inlineMatches = trimmed.match(/`[^`]+`/g);
|
|
4152
|
+
if (inlineMatches) inlineCodeCount += inlineMatches.length;
|
|
4153
|
+
}
|
|
4154
|
+
return {
|
|
4155
|
+
headingCount,
|
|
4156
|
+
h2Count,
|
|
4157
|
+
h3Count,
|
|
4158
|
+
codeBlockCount,
|
|
4159
|
+
codeBlockLines,
|
|
4160
|
+
listItemCount,
|
|
4161
|
+
inlineCodeCount,
|
|
4162
|
+
totalLines: lines.length,
|
|
4163
|
+
nonEmptyLines: lines.filter((l) => l.trim().length > 0).length
|
|
4164
|
+
};
|
|
4165
|
+
}
|
|
4166
|
+
function extractReferences(content) {
|
|
4167
|
+
const refs = /* @__PURE__ */ new Set();
|
|
4168
|
+
const backtickPattern = /`([^`]+)`/g;
|
|
4169
|
+
let match;
|
|
4170
|
+
while ((match = backtickPattern.exec(content)) !== null) {
|
|
4171
|
+
const term = match[1].trim();
|
|
4172
|
+
if ((term.includes("/") || /\.\w{1,5}$/.test(term)) && !term.startsWith("-") && term.length < 200) {
|
|
4173
|
+
if (term.startsWith("@") && (term.match(/\//g) || []).length === 1) continue;
|
|
4174
|
+
if (term.includes(" ")) continue;
|
|
4175
|
+
if (/^\d+\.\d+/.test(term)) continue;
|
|
4176
|
+
if (term.includes("/") && !/\.\w{1,5}$/.test(term)) {
|
|
4177
|
+
if (term !== term.toLowerCase() && !/^[a-z]/.test(term)) continue;
|
|
4178
|
+
const segments = term.split("/");
|
|
4179
|
+
if (segments.every((s) => s.length <= 3)) continue;
|
|
4180
|
+
}
|
|
4181
|
+
const cleaned = term.replace(/[,;:!?)]+$/, "");
|
|
4182
|
+
if (cleaned.length > 1) refs.add(cleaned);
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
const pathPattern = /(?:^|\s)((?:[a-zA-Z0-9_@.-]+\/)+[a-zA-Z0-9_.*-]+\.[a-zA-Z]{1,5})/gm;
|
|
4186
|
+
while ((match = pathPattern.exec(content)) !== null) {
|
|
4187
|
+
const term = match[1].trim();
|
|
4188
|
+
if (term.length > 2 && term.length < 200) {
|
|
4189
|
+
if (term.startsWith("@") && (term.match(/\//g) || []).length === 1) continue;
|
|
4190
|
+
const cleaned = term.replace(/[,;:!?)]+$/, "");
|
|
4191
|
+
if (cleaned.length > 1) refs.add(cleaned);
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
return Array.from(refs);
|
|
4195
|
+
}
|
|
4196
|
+
function classifyLine(line, inCodeBlock) {
|
|
4197
|
+
if (inCodeBlock) return "concrete";
|
|
4198
|
+
const trimmed = line.trim();
|
|
4199
|
+
if (trimmed.length === 0) return "neutral";
|
|
4200
|
+
if (trimmed.startsWith("#")) return "neutral";
|
|
4201
|
+
if (/`[^`]+`/.test(trimmed)) return "concrete";
|
|
4202
|
+
if (/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+\.[a-zA-Z]{1,5}/.test(trimmed)) return "concrete";
|
|
4203
|
+
if (/[a-zA-Z0-9_]{4,}\/[a-zA-Z0-9_.-]/.test(trimmed)) return "concrete";
|
|
4204
|
+
if (/\b[a-zA-Z0-9_-]+\.[a-zA-Z]{1,5}\b/.test(trimmed) && !/\b(e\.g|i\.e|vs|etc)\b/i.test(trimmed)) return "concrete";
|
|
4205
|
+
return "abstract";
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
// src/scoring/checks/quality.ts
|
|
4155
4209
|
function checkQuality(dir) {
|
|
4156
4210
|
const checks = [];
|
|
4157
|
-
const claudeMd =
|
|
4158
|
-
const cursorrules =
|
|
4159
|
-
const agentsMd =
|
|
4211
|
+
const claudeMd = readFileOrNull2(join4(dir, "CLAUDE.md"));
|
|
4212
|
+
const cursorrules = readFileOrNull2(join4(dir, ".cursorrules"));
|
|
4213
|
+
const agentsMd = readFileOrNull2(join4(dir, "AGENTS.md"));
|
|
4160
4214
|
const allContent = [claudeMd, cursorrules, agentsMd].filter(Boolean);
|
|
4161
4215
|
const combinedContent = allContent.join("\n");
|
|
4162
|
-
const primaryInstructions = claudeMd ?? agentsMd;
|
|
4163
|
-
const
|
|
4164
|
-
const
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
}).filter(Boolean) : [];
|
|
4216
|
+
const primaryInstructions = claudeMd ?? agentsMd ?? cursorrules;
|
|
4217
|
+
const structure = primaryInstructions ? analyzeMarkdownStructure(primaryInstructions) : null;
|
|
4218
|
+
const codeBlockCount = structure?.codeBlockCount ?? 0;
|
|
4219
|
+
const codeBlockThreshold = CODE_BLOCK_THRESHOLDS.find((t) => codeBlockCount >= t.minBlocks);
|
|
4220
|
+
const execPoints = codeBlockThreshold?.points ?? 0;
|
|
4168
4221
|
checks.push({
|
|
4169
|
-
id: "
|
|
4170
|
-
name: "
|
|
4222
|
+
id: "has_executable_content",
|
|
4223
|
+
name: "Executable content (code blocks)",
|
|
4171
4224
|
category: "quality",
|
|
4172
|
-
maxPoints:
|
|
4173
|
-
earnedPoints:
|
|
4174
|
-
passed:
|
|
4175
|
-
detail:
|
|
4176
|
-
suggestion:
|
|
4225
|
+
maxPoints: POINTS_EXECUTABLE_CONTENT,
|
|
4226
|
+
earnedPoints: execPoints,
|
|
4227
|
+
passed: execPoints >= 6,
|
|
4228
|
+
detail: primaryInstructions ? `${codeBlockCount} code block${codeBlockCount === 1 ? "" : "s"} found` : "No instructions file to check",
|
|
4229
|
+
suggestion: execPoints < 6 ? "Add code blocks with project commands, build steps, and common workflows" : void 0,
|
|
4230
|
+
fix: execPoints < 6 ? {
|
|
4231
|
+
action: "add_code_blocks",
|
|
4232
|
+
data: { currentCount: codeBlockCount, targetCount: 3 },
|
|
4233
|
+
instruction: `Add code blocks with executable commands. Currently ${codeBlockCount}, need at least 3 for full points.`
|
|
4234
|
+
} : void 0
|
|
4177
4235
|
});
|
|
4178
|
-
const
|
|
4179
|
-
const
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
if (primaryFile) {
|
|
4183
|
-
lineCount = countLines(primaryFile);
|
|
4184
|
-
const threshold = BLOAT_THRESHOLDS.find((t) => lineCount <= t.maxLines);
|
|
4185
|
-
bloatPoints = threshold ? threshold.points : 0;
|
|
4186
|
-
} else {
|
|
4187
|
-
bloatPoints = POINTS_NOT_BLOATED;
|
|
4188
|
-
}
|
|
4236
|
+
const totalContent = collectAllConfigContent(dir);
|
|
4237
|
+
const totalTokens = estimateTokens2(totalContent);
|
|
4238
|
+
const tokenThreshold = TOKEN_BUDGET_THRESHOLDS.find((t) => totalTokens <= t.maxTokens);
|
|
4239
|
+
const tokenPoints = totalContent.length === 0 ? POINTS_CONCISE_CONFIG : tokenThreshold?.points ?? 0;
|
|
4189
4240
|
checks.push({
|
|
4190
|
-
id: "
|
|
4191
|
-
name: "Concise
|
|
4241
|
+
id: "concise_config",
|
|
4242
|
+
name: "Concise config (token budget)",
|
|
4192
4243
|
category: "quality",
|
|
4193
|
-
maxPoints:
|
|
4194
|
-
earnedPoints:
|
|
4195
|
-
passed:
|
|
4196
|
-
detail:
|
|
4197
|
-
suggestion:
|
|
4244
|
+
maxPoints: POINTS_CONCISE_CONFIG,
|
|
4245
|
+
earnedPoints: tokenPoints,
|
|
4246
|
+
passed: tokenPoints >= 4,
|
|
4247
|
+
detail: totalContent.length === 0 ? "No config files to measure" : `~${totalTokens} tokens total across all config files`,
|
|
4248
|
+
suggestion: tokenPoints < 4 && totalContent.length > 0 ? `Total config is ~${totalTokens} tokens \u2014 reduce to under 5000 for better agent performance` : void 0,
|
|
4249
|
+
fix: tokenPoints < 4 && totalContent.length > 0 ? {
|
|
4250
|
+
action: "reduce_size",
|
|
4251
|
+
data: { currentTokens: totalTokens, targetTokens: 5e3 },
|
|
4252
|
+
instruction: `Reduce total config from ~${totalTokens} tokens to under 5000.`
|
|
4253
|
+
} : void 0
|
|
4198
4254
|
});
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4255
|
+
let concreteCount = 0;
|
|
4256
|
+
let abstractCount = 0;
|
|
4257
|
+
const abstractExamples = [];
|
|
4258
|
+
if (primaryInstructions) {
|
|
4259
|
+
let inCodeBlock2 = false;
|
|
4260
|
+
for (const line of primaryInstructions.split("\n")) {
|
|
4261
|
+
if (line.trim().startsWith("```")) {
|
|
4262
|
+
inCodeBlock2 = !inCodeBlock2;
|
|
4263
|
+
continue;
|
|
4264
|
+
}
|
|
4265
|
+
const classification = classifyLine(line, inCodeBlock2);
|
|
4266
|
+
if (classification === "neutral") continue;
|
|
4267
|
+
if (classification === "concrete") {
|
|
4268
|
+
concreteCount++;
|
|
4269
|
+
} else {
|
|
4270
|
+
abstractCount++;
|
|
4271
|
+
if (abstractExamples.length < 3) {
|
|
4272
|
+
abstractExamples.push(line.trim().slice(0, 80));
|
|
4207
4273
|
}
|
|
4208
4274
|
}
|
|
4209
4275
|
}
|
|
4210
4276
|
}
|
|
4277
|
+
const totalMeaningful = concreteCount + abstractCount;
|
|
4278
|
+
const concreteRatio = totalMeaningful > 0 ? concreteCount / totalMeaningful : 1;
|
|
4279
|
+
const concretenessThreshold = CONCRETENESS_THRESHOLDS.find((t) => concreteRatio >= t.minRatio);
|
|
4280
|
+
const concretenessPoints = totalMeaningful === 0 ? 0 : concretenessThreshold?.points ?? 0;
|
|
4211
4281
|
checks.push({
|
|
4212
|
-
id: "
|
|
4213
|
-
name: "
|
|
4282
|
+
id: "concreteness",
|
|
4283
|
+
name: "Concrete instructions",
|
|
4214
4284
|
category: "quality",
|
|
4215
|
-
maxPoints:
|
|
4216
|
-
earnedPoints:
|
|
4217
|
-
passed:
|
|
4218
|
-
detail:
|
|
4219
|
-
suggestion:
|
|
4285
|
+
maxPoints: POINTS_CONCRETENESS,
|
|
4286
|
+
earnedPoints: concretenessPoints,
|
|
4287
|
+
passed: concretenessPoints >= 3,
|
|
4288
|
+
detail: totalMeaningful === 0 ? "No content to analyze" : `${Math.round(concreteRatio * 100)}% of lines reference specific files, paths, or code`,
|
|
4289
|
+
suggestion: concretenessPoints < 3 && totalMeaningful > 0 ? `${abstractCount} lines are generic prose \u2014 replace with specific instructions referencing project files` : void 0,
|
|
4290
|
+
fix: concretenessPoints < 3 && totalMeaningful > 0 ? {
|
|
4291
|
+
action: "replace_vague",
|
|
4292
|
+
data: { abstractLines: abstractExamples, abstractCount, concreteCount, ratio: Math.round(concreteRatio * 100) },
|
|
4293
|
+
instruction: `Replace generic prose with specific references. Examples of vague lines: ${abstractExamples.join("; ")}`
|
|
4294
|
+
} : void 0
|
|
4220
4295
|
});
|
|
4221
4296
|
const treeLinePattern = /[├└│─┬]/;
|
|
4222
4297
|
let treeLineCount = 0;
|
|
@@ -4241,7 +4316,12 @@ function checkQuality(dir) {
|
|
|
4241
4316
|
earnedPoints: hasLargeTree ? 0 : POINTS_NO_DIR_TREE,
|
|
4242
4317
|
passed: !hasLargeTree,
|
|
4243
4318
|
detail: hasLargeTree ? `${treeLineCount}-line directory tree detected in code block` : "No large directory trees found",
|
|
4244
|
-
suggestion: hasLargeTree ? "Remove directory tree listings \u2014 agents discover project structure by reading code" : void 0
|
|
4319
|
+
suggestion: hasLargeTree ? "Remove directory tree listings \u2014 agents discover project structure by reading code" : void 0,
|
|
4320
|
+
fix: hasLargeTree ? {
|
|
4321
|
+
action: "remove_tree",
|
|
4322
|
+
data: { treeLines: treeLineCount },
|
|
4323
|
+
instruction: "Remove directory tree listings from code blocks. Reference key directories inline instead."
|
|
4324
|
+
} : void 0
|
|
4245
4325
|
});
|
|
4246
4326
|
let duplicatePercent = 0;
|
|
4247
4327
|
if (claudeMd && cursorrules) {
|
|
@@ -4261,276 +4341,320 @@ function checkQuality(dir) {
|
|
|
4261
4341
|
earnedPoints: hasDuplicates ? 0 : POINTS_NO_DUPLICATES,
|
|
4262
4342
|
passed: !hasDuplicates,
|
|
4263
4343
|
detail: claudeMd && cursorrules ? hasDuplicates ? `${duplicatePercent}% overlap between CLAUDE.md and .cursorrules` : `${duplicatePercent}% overlap \u2014 acceptable` : "Only one context file (no duplication possible)",
|
|
4264
|
-
suggestion: hasDuplicates ? "CLAUDE.md and .cursorrules share >50% content \u2014 deduplicate to save tokens" : void 0
|
|
4344
|
+
suggestion: hasDuplicates ? "CLAUDE.md and .cursorrules share >50% content \u2014 deduplicate to save tokens" : void 0,
|
|
4345
|
+
fix: hasDuplicates ? {
|
|
4346
|
+
action: "deduplicate",
|
|
4347
|
+
data: { overlapPercent: duplicatePercent },
|
|
4348
|
+
instruction: "Deduplicate content between CLAUDE.md and .cursorrules. Each file should contain platform-specific instructions only."
|
|
4349
|
+
} : void 0
|
|
4265
4350
|
});
|
|
4266
|
-
const
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4351
|
+
const structureScore = structure ? (structure.h2Count >= 3 ? 1 : 0) + (structure.listItemCount >= 3 ? 1 : 0) : 0;
|
|
4352
|
+
checks.push({
|
|
4353
|
+
id: "has_structure",
|
|
4354
|
+
name: "Structured with headings",
|
|
4355
|
+
category: "quality",
|
|
4356
|
+
maxPoints: POINTS_HAS_STRUCTURE,
|
|
4357
|
+
earnedPoints: primaryInstructions ? structureScore : 0,
|
|
4358
|
+
passed: structureScore >= 2,
|
|
4359
|
+
detail: primaryInstructions ? `${structure.h2Count} sections, ${structure.listItemCount} list items` : "No instructions file to check",
|
|
4360
|
+
suggestion: structureScore < 2 && primaryInstructions ? "Add at least 3 markdown sections (##) and use lists for multi-item instructions" : void 0,
|
|
4361
|
+
fix: structureScore < 2 && primaryInstructions ? {
|
|
4362
|
+
action: "add_structure",
|
|
4363
|
+
data: { currentH2: structure.h2Count, currentLists: structure.listItemCount },
|
|
4364
|
+
instruction: "Organize content into sections with ## headings and use bullet lists for instructions."
|
|
4365
|
+
} : void 0
|
|
4366
|
+
});
|
|
4367
|
+
return checks;
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
// src/scoring/checks/grounding.ts
|
|
4371
|
+
function checkGrounding(dir) {
|
|
4372
|
+
const checks = [];
|
|
4373
|
+
const configContent = collectAllConfigContent(dir);
|
|
4374
|
+
const configLower = configContent.toLowerCase();
|
|
4375
|
+
const projectStructure = collectProjectStructure(dir);
|
|
4376
|
+
const allProjectEntries = [
|
|
4377
|
+
...projectStructure.dirs,
|
|
4378
|
+
...projectStructure.files
|
|
4379
|
+
];
|
|
4380
|
+
const meaningfulEntries = allProjectEntries.filter((e) => e.length > 2);
|
|
4381
|
+
const mentioned = [];
|
|
4382
|
+
const notMentioned = [];
|
|
4383
|
+
for (const entry of meaningfulEntries) {
|
|
4384
|
+
const entryLower = entry.toLowerCase();
|
|
4385
|
+
const variants = [
|
|
4386
|
+
entryLower,
|
|
4387
|
+
entryLower.replace(/\\/g, "/")
|
|
4388
|
+
];
|
|
4389
|
+
const lastSegment = entry.split("/").pop()?.toLowerCase();
|
|
4390
|
+
if (lastSegment && lastSegment.length > 3) {
|
|
4391
|
+
variants.push(lastSegment);
|
|
4274
4392
|
}
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4393
|
+
const ismentioned = variants.some((v) => {
|
|
4394
|
+
const escaped = v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4395
|
+
return new RegExp(`(?:^|[\\s\`/"'\\.,(])${escaped}(?:[\\s\`/"'.,;:!?)\\\\]|$)`, "i").test(configLower);
|
|
4396
|
+
});
|
|
4397
|
+
if (ismentioned) {
|
|
4398
|
+
mentioned.push(entry);
|
|
4399
|
+
} else {
|
|
4400
|
+
notMentioned.push(entry);
|
|
4281
4401
|
}
|
|
4282
4402
|
}
|
|
4283
|
-
const
|
|
4403
|
+
const groundingRatio = meaningfulEntries.length > 0 ? mentioned.length / meaningfulEntries.length : 0;
|
|
4404
|
+
const groundingThreshold = GROUNDING_THRESHOLDS.find((t) => groundingRatio >= t.minRatio);
|
|
4405
|
+
const groundingPoints = meaningfulEntries.length === 0 ? 0 : groundingThreshold?.points ?? 0;
|
|
4406
|
+
const topDirs = projectStructure.dirs.filter((d) => !d.includes("/")).filter((d) => d.length > 2);
|
|
4407
|
+
const matchesConfig = (name) => {
|
|
4408
|
+
const escaped = name.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4409
|
+
return new RegExp(`(?:^|[\\s\`/"'\\.,(])${escaped}(?:[\\s\`/"'.,;:!?)\\\\]|$)`, "i").test(configLower);
|
|
4410
|
+
};
|
|
4411
|
+
const unmentionedTopDirs = topDirs.filter((d) => !matchesConfig(d));
|
|
4412
|
+
const mentionedTopDirs = topDirs.filter((d) => matchesConfig(d));
|
|
4284
4413
|
checks.push({
|
|
4285
|
-
id: "
|
|
4286
|
-
name: "
|
|
4287
|
-
category: "
|
|
4288
|
-
maxPoints:
|
|
4289
|
-
earnedPoints:
|
|
4290
|
-
passed:
|
|
4291
|
-
detail:
|
|
4292
|
-
suggestion:
|
|
4414
|
+
id: "project_grounding",
|
|
4415
|
+
name: "Project grounding",
|
|
4416
|
+
category: "grounding",
|
|
4417
|
+
maxPoints: POINTS_PROJECT_GROUNDING,
|
|
4418
|
+
earnedPoints: groundingPoints,
|
|
4419
|
+
passed: groundingRatio >= 0.2,
|
|
4420
|
+
detail: meaningfulEntries.length === 0 ? "No project structure detected" : `${mentioned.length}/${meaningfulEntries.length} project entries referenced in config (${Math.round(groundingRatio * 100)}%)`,
|
|
4421
|
+
suggestion: unmentionedTopDirs.length > 0 ? `Config doesn't mention: ${unmentionedTopDirs.slice(0, 5).join(", ")}${unmentionedTopDirs.length > 5 ? ` (+${unmentionedTopDirs.length - 5} more)` : ""}` : void 0,
|
|
4422
|
+
fix: groundingPoints < POINTS_PROJECT_GROUNDING ? {
|
|
4423
|
+
action: "add_references",
|
|
4424
|
+
data: {
|
|
4425
|
+
missing: unmentionedTopDirs.slice(0, 10),
|
|
4426
|
+
mentioned: mentionedTopDirs.slice(0, 10),
|
|
4427
|
+
totalEntries: meaningfulEntries.length,
|
|
4428
|
+
coverage: Math.round(groundingRatio * 100)
|
|
4429
|
+
},
|
|
4430
|
+
instruction: `Reference these project directories in your config: ${unmentionedTopDirs.slice(0, 5).join(", ")}`
|
|
4431
|
+
} : void 0
|
|
4432
|
+
});
|
|
4433
|
+
const refs = extractReferences(configContent);
|
|
4434
|
+
const mdStructure = analyzeMarkdownStructure(configContent);
|
|
4435
|
+
const totalSpecificRefs = refs.length + mdStructure.inlineCodeCount;
|
|
4436
|
+
const density = mdStructure.nonEmptyLines > 0 ? totalSpecificRefs / mdStructure.nonEmptyLines * 100 : 0;
|
|
4437
|
+
let densityPoints = 0;
|
|
4438
|
+
if (configContent.length === 0) {
|
|
4439
|
+
densityPoints = 0;
|
|
4440
|
+
} else if (density >= 40) {
|
|
4441
|
+
densityPoints = POINTS_REFERENCE_DENSITY;
|
|
4442
|
+
} else if (density >= 25) {
|
|
4443
|
+
densityPoints = Math.round(POINTS_REFERENCE_DENSITY * 0.75);
|
|
4444
|
+
} else if (density >= 15) {
|
|
4445
|
+
densityPoints = Math.round(POINTS_REFERENCE_DENSITY * 0.5);
|
|
4446
|
+
} else if (density >= 5) {
|
|
4447
|
+
densityPoints = Math.round(POINTS_REFERENCE_DENSITY * 0.25);
|
|
4448
|
+
}
|
|
4449
|
+
checks.push({
|
|
4450
|
+
id: "reference_density",
|
|
4451
|
+
name: "Reference density",
|
|
4452
|
+
category: "grounding",
|
|
4453
|
+
maxPoints: POINTS_REFERENCE_DENSITY,
|
|
4454
|
+
earnedPoints: densityPoints,
|
|
4455
|
+
passed: densityPoints >= Math.round(POINTS_REFERENCE_DENSITY * 0.5),
|
|
4456
|
+
detail: configContent.length === 0 ? "No config content" : `${totalSpecificRefs} specific references across ${mdStructure.nonEmptyLines} lines (${Math.round(density)}%)`,
|
|
4457
|
+
suggestion: densityPoints < Math.round(POINTS_REFERENCE_DENSITY * 0.5) && configContent.length > 0 ? "Use backticks and paths to reference specific files, commands, and identifiers" : void 0,
|
|
4458
|
+
fix: densityPoints < Math.round(POINTS_REFERENCE_DENSITY * 0.5) && configContent.length > 0 ? {
|
|
4459
|
+
action: "add_inline_refs",
|
|
4460
|
+
data: { currentDensity: Math.round(density), currentRefs: totalSpecificRefs, lines: mdStructure.nonEmptyLines },
|
|
4461
|
+
instruction: "Add more inline code references (backticks) for file paths, commands, and identifiers."
|
|
4462
|
+
} : void 0
|
|
4293
4463
|
});
|
|
4294
4464
|
return checks;
|
|
4295
4465
|
}
|
|
4296
4466
|
|
|
4297
4467
|
// src/scoring/checks/accuracy.ts
|
|
4298
|
-
import { existsSync as
|
|
4468
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4469
|
+
import { execSync as execSync8 } from "child_process";
|
|
4299
4470
|
import { join as join5 } from "path";
|
|
4300
|
-
function
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
return null;
|
|
4305
|
-
}
|
|
4306
|
-
}
|
|
4307
|
-
function readJsonOrNull2(path27) {
|
|
4308
|
-
const content = readFileOrNull4(path27);
|
|
4309
|
-
if (!content) return null;
|
|
4310
|
-
try {
|
|
4311
|
-
return JSON.parse(content);
|
|
4312
|
-
} catch {
|
|
4313
|
-
return null;
|
|
4314
|
-
}
|
|
4315
|
-
}
|
|
4316
|
-
function getPackageScripts(dir) {
|
|
4317
|
-
const pkg3 = readJsonOrNull2(join5(dir, "package.json"));
|
|
4318
|
-
if (!pkg3?.scripts) return /* @__PURE__ */ new Set();
|
|
4319
|
-
return new Set(Object.keys(pkg3.scripts));
|
|
4320
|
-
}
|
|
4321
|
-
function validateDocumentedCommands(dir) {
|
|
4322
|
-
const claudeMd = readFileOrNull4(join5(dir, "CLAUDE.md"));
|
|
4323
|
-
if (!claudeMd) return { valid: [], invalid: [], total: 0 };
|
|
4324
|
-
const scripts = getPackageScripts(dir);
|
|
4471
|
+
function validateReferences(dir) {
|
|
4472
|
+
const configContent = collectAllConfigContent(dir);
|
|
4473
|
+
if (!configContent) return { valid: [], invalid: [], total: 0 };
|
|
4474
|
+
const refs = extractReferences(configContent);
|
|
4325
4475
|
const valid = [];
|
|
4326
4476
|
const invalid = [];
|
|
4327
|
-
const
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
if (
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
} else {
|
|
4339
|
-
valid.push(match[0]);
|
|
4340
|
-
}
|
|
4341
|
-
continue;
|
|
4342
|
-
}
|
|
4343
|
-
if (scripts.has(scriptName)) {
|
|
4344
|
-
valid.push(match[0]);
|
|
4477
|
+
for (const ref of refs) {
|
|
4478
|
+
if (/^https?:\/\//.test(ref)) continue;
|
|
4479
|
+
if (/^\d+\.\d+/.test(ref)) continue;
|
|
4480
|
+
if (ref.startsWith("#")) continue;
|
|
4481
|
+
if (ref.startsWith("@")) continue;
|
|
4482
|
+
if (ref.includes("*")) continue;
|
|
4483
|
+
if (ref.includes("..")) continue;
|
|
4484
|
+
if (!ref.includes("/") && !ref.includes(".")) continue;
|
|
4485
|
+
const fullPath = join5(dir, ref);
|
|
4486
|
+
if (existsSync4(fullPath)) {
|
|
4487
|
+
valid.push(ref);
|
|
4345
4488
|
} else {
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
const npxPattern = /npx\s+(\S+)/g;
|
|
4350
|
-
while ((match = npxPattern.exec(claudeMd)) !== null) {
|
|
4351
|
-
const tool = match[1];
|
|
4352
|
-
if (seen.has(`npx-${tool}`)) continue;
|
|
4353
|
-
seen.add(`npx-${tool}`);
|
|
4354
|
-
valid.push(match[0]);
|
|
4355
|
-
}
|
|
4356
|
-
const makePattern = /make\s+(\S+)/g;
|
|
4357
|
-
if (existsSync5(join5(dir, "Makefile"))) {
|
|
4358
|
-
const makefile = readFileOrNull4(join5(dir, "Makefile"));
|
|
4359
|
-
const makeTargets = /* @__PURE__ */ new Set();
|
|
4360
|
-
if (makefile) {
|
|
4361
|
-
for (const line of makefile.split("\n")) {
|
|
4362
|
-
const targetMatch = line.match(/^([a-zA-Z_-]+)\s*:/);
|
|
4363
|
-
if (targetMatch) makeTargets.add(targetMatch[1]);
|
|
4364
|
-
}
|
|
4365
|
-
}
|
|
4366
|
-
while ((match = makePattern.exec(claudeMd)) !== null) {
|
|
4367
|
-
const target = match[1];
|
|
4368
|
-
if (seen.has(`make-${target}`)) continue;
|
|
4369
|
-
seen.add(`make-${target}`);
|
|
4370
|
-
if (makeTargets.has(target)) {
|
|
4371
|
-
valid.push(match[0]);
|
|
4489
|
+
const withoutTrailing = ref.replace(/\/+$/, "");
|
|
4490
|
+
if (withoutTrailing !== ref && existsSync4(join5(dir, withoutTrailing))) {
|
|
4491
|
+
valid.push(ref);
|
|
4372
4492
|
} else {
|
|
4373
|
-
invalid.push(
|
|
4493
|
+
invalid.push(ref);
|
|
4374
4494
|
}
|
|
4375
4495
|
}
|
|
4376
4496
|
}
|
|
4377
4497
|
return { valid, invalid, total: valid.length + invalid.length };
|
|
4378
4498
|
}
|
|
4379
|
-
function
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
const pathPattern = /(?:^|\s|`|"|')((src|lib|app|apps|packages|cmd|internal|test|tests|scripts|config|public|pages|components|routes|services|middleware|utils|helpers)\/[a-zA-Z0-9_./-]+\.[a-zA-Z]{1,5})/gm;
|
|
4385
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4386
|
-
let match;
|
|
4387
|
-
while ((match = pathPattern.exec(claudeMd)) !== null) {
|
|
4388
|
-
const filePath = match[1];
|
|
4389
|
-
if (seen.has(filePath)) continue;
|
|
4390
|
-
seen.add(filePath);
|
|
4391
|
-
if (/\/path\/to\/|\/example[s]?\/|\/your[_-]|\/foo\/|\/bar\//.test(filePath)) continue;
|
|
4392
|
-
if (existsSync5(join5(dir, filePath))) {
|
|
4393
|
-
valid.push(filePath);
|
|
4394
|
-
} else {
|
|
4395
|
-
invalid.push(filePath);
|
|
4396
|
-
}
|
|
4499
|
+
function detectGitDrift(dir) {
|
|
4500
|
+
try {
|
|
4501
|
+
execSync8("git rev-parse --git-dir", { cwd: dir, stdio: ["pipe", "pipe", "pipe"] });
|
|
4502
|
+
} catch {
|
|
4503
|
+
return { commitsSinceConfigUpdate: 0, lastConfigCommit: null, isGitRepo: false };
|
|
4397
4504
|
}
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
const srcDirs = ["src", "lib", "app", "cmd", "internal", "pages", "components"];
|
|
4402
|
-
let latestSrcMtime = 0;
|
|
4403
|
-
for (const srcDir of srcDirs) {
|
|
4404
|
-
const fullPath = join5(dir, srcDir);
|
|
4405
|
-
if (!existsSync5(fullPath)) continue;
|
|
4505
|
+
const configFiles = ["CLAUDE.md", "AGENTS.md", ".cursorrules", ".cursor/rules"];
|
|
4506
|
+
let latestConfigCommitHash = null;
|
|
4507
|
+
for (const file of configFiles) {
|
|
4406
4508
|
try {
|
|
4407
|
-
const
|
|
4408
|
-
|
|
4509
|
+
const hash = execSync8(
|
|
4510
|
+
`git log -1 --format=%H -- "${file}"`,
|
|
4511
|
+
{ cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4512
|
+
).trim();
|
|
4513
|
+
if (!hash) continue;
|
|
4514
|
+
if (!latestConfigCommitHash) {
|
|
4515
|
+
latestConfigCommitHash = hash;
|
|
4516
|
+
} else {
|
|
4409
4517
|
try {
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4518
|
+
execSync8(
|
|
4519
|
+
`git merge-base --is-ancestor ${latestConfigCommitHash} ${hash}`,
|
|
4520
|
+
{ cwd: dir, stdio: ["pipe", "pipe", "pipe"] }
|
|
4521
|
+
);
|
|
4522
|
+
latestConfigCommitHash = hash;
|
|
4414
4523
|
} catch {
|
|
4415
4524
|
}
|
|
4416
4525
|
}
|
|
4417
4526
|
} catch {
|
|
4418
4527
|
}
|
|
4419
4528
|
}
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
for (const configFile of configFiles) {
|
|
4423
|
-
try {
|
|
4424
|
-
const stat = statSync(join5(dir, configFile));
|
|
4425
|
-
if (stat.mtime.getTime() > latestConfigMtime) {
|
|
4426
|
-
latestConfigMtime = stat.mtime.getTime();
|
|
4427
|
-
}
|
|
4428
|
-
} catch {
|
|
4429
|
-
}
|
|
4529
|
+
if (!latestConfigCommitHash) {
|
|
4530
|
+
return { commitsSinceConfigUpdate: 0, lastConfigCommit: null, isGitRepo: true };
|
|
4430
4531
|
}
|
|
4431
|
-
|
|
4432
|
-
|
|
4532
|
+
try {
|
|
4533
|
+
const countStr = execSync8(
|
|
4534
|
+
`git rev-list --count ${latestConfigCommitHash}..HEAD`,
|
|
4535
|
+
{ cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4536
|
+
).trim();
|
|
4537
|
+
const commitsSince = parseInt(countStr, 10) || 0;
|
|
4538
|
+
const lastDate = execSync8(
|
|
4539
|
+
`git log -1 --format=%ci ${latestConfigCommitHash}`,
|
|
4540
|
+
{ cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4541
|
+
).trim();
|
|
4542
|
+
return {
|
|
4543
|
+
commitsSinceConfigUpdate: commitsSince,
|
|
4544
|
+
lastConfigCommit: lastDate,
|
|
4545
|
+
isGitRepo: true
|
|
4546
|
+
};
|
|
4547
|
+
} catch {
|
|
4548
|
+
return { commitsSinceConfigUpdate: 0, lastConfigCommit: latestConfigCommitHash, isGitRepo: true };
|
|
4433
4549
|
}
|
|
4434
|
-
const driftMs = latestSrcMtime - latestConfigMtime;
|
|
4435
|
-
const driftDays = Math.max(0, Math.floor(driftMs / (1e3 * 60 * 60 * 24)));
|
|
4436
|
-
return {
|
|
4437
|
-
driftDays,
|
|
4438
|
-
srcLastModified: new Date(latestSrcMtime),
|
|
4439
|
-
configLastModified: new Date(latestConfigMtime)
|
|
4440
|
-
};
|
|
4441
4550
|
}
|
|
4442
4551
|
function checkAccuracy(dir) {
|
|
4443
4552
|
const checks = [];
|
|
4444
|
-
const
|
|
4445
|
-
const
|
|
4446
|
-
const
|
|
4447
|
-
checks.push({
|
|
4448
|
-
id: "commands_valid",
|
|
4449
|
-
name: "Documented commands exist",
|
|
4450
|
-
category: "accuracy",
|
|
4451
|
-
maxPoints: POINTS_COMMANDS_VALID,
|
|
4452
|
-
earnedPoints: cmdPoints,
|
|
4453
|
-
passed: cmdRatio >= 0.8,
|
|
4454
|
-
detail: cmds.total === 0 ? "No commands documented" : `${cmds.valid.length}/${cmds.total} commands verified`,
|
|
4455
|
-
suggestion: cmds.invalid.length > 0 ? `Remove these invalid commands from CLAUDE.md: ${cmds.invalid.join("; ")}` : void 0
|
|
4456
|
-
});
|
|
4457
|
-
const paths = validateDocumentedPaths(dir);
|
|
4458
|
-
const pathRatio = paths.total > 0 ? paths.valid.length / paths.total : 1;
|
|
4459
|
-
const pathPoints = paths.total === 0 ? POINTS_PATHS_VALID : Math.round(pathRatio * POINTS_PATHS_VALID);
|
|
4553
|
+
const refs = validateReferences(dir);
|
|
4554
|
+
const refRatio = refs.total > 0 ? refs.valid.length / refs.total : 0;
|
|
4555
|
+
const refPoints = refs.total === 0 ? 0 : Math.round(refRatio * POINTS_REFERENCES_VALID);
|
|
4460
4556
|
checks.push({
|
|
4461
|
-
id: "
|
|
4462
|
-
name: "
|
|
4557
|
+
id: "references_valid",
|
|
4558
|
+
name: "References point to real files",
|
|
4463
4559
|
category: "accuracy",
|
|
4464
|
-
maxPoints:
|
|
4465
|
-
earnedPoints:
|
|
4466
|
-
passed:
|
|
4467
|
-
detail:
|
|
4468
|
-
suggestion:
|
|
4560
|
+
maxPoints: POINTS_REFERENCES_VALID,
|
|
4561
|
+
earnedPoints: refPoints,
|
|
4562
|
+
passed: refs.total === 0 ? false : refRatio >= 0.8,
|
|
4563
|
+
detail: refs.total === 0 ? "No file references found in config" : `${refs.valid.length}/${refs.total} references verified`,
|
|
4564
|
+
suggestion: refs.invalid.length > 0 ? `These references don't exist: ${refs.invalid.slice(0, 3).join(", ")}${refs.invalid.length > 3 ? ` (+${refs.invalid.length - 3} more)` : ""}` : refs.total === 0 ? "Add file path references to make your config grounded in the project" : void 0,
|
|
4565
|
+
fix: refs.invalid.length > 0 ? {
|
|
4566
|
+
action: "fix_references",
|
|
4567
|
+
data: { invalid: refs.invalid.slice(0, 10), valid: refs.valid.slice(0, 10) },
|
|
4568
|
+
instruction: `Remove or update these non-existent paths: ${refs.invalid.slice(0, 5).join(", ")}`
|
|
4569
|
+
} : refs.total === 0 ? {
|
|
4570
|
+
action: "add_references",
|
|
4571
|
+
data: { currentRefs: 0 },
|
|
4572
|
+
instruction: "Add file path references (e.g., `src/index.ts`) to ground the config in the project."
|
|
4573
|
+
} : void 0
|
|
4469
4574
|
});
|
|
4470
|
-
const drift =
|
|
4575
|
+
const drift = detectGitDrift(dir);
|
|
4471
4576
|
let driftPoints = POINTS_CONFIG_DRIFT;
|
|
4472
|
-
if (drift.
|
|
4473
|
-
|
|
4474
|
-
else if (drift.
|
|
4475
|
-
|
|
4577
|
+
if (!drift.isGitRepo) {
|
|
4578
|
+
driftPoints = POINTS_CONFIG_DRIFT;
|
|
4579
|
+
} else if (!drift.lastConfigCommit) {
|
|
4580
|
+
driftPoints = 0;
|
|
4581
|
+
} else if (drift.commitsSinceConfigUpdate > 50) {
|
|
4582
|
+
driftPoints = 0;
|
|
4583
|
+
} else if (drift.commitsSinceConfigUpdate > 30) {
|
|
4584
|
+
driftPoints = Math.round(POINTS_CONFIG_DRIFT * 0.25);
|
|
4585
|
+
} else if (drift.commitsSinceConfigUpdate > 15) {
|
|
4586
|
+
driftPoints = Math.round(POINTS_CONFIG_DRIFT * 0.5);
|
|
4587
|
+
} else if (drift.commitsSinceConfigUpdate > 5) {
|
|
4588
|
+
driftPoints = Math.round(POINTS_CONFIG_DRIFT * 0.75);
|
|
4589
|
+
}
|
|
4476
4590
|
checks.push({
|
|
4477
4591
|
id: "config_drift",
|
|
4478
4592
|
name: "Config freshness vs code",
|
|
4479
4593
|
category: "accuracy",
|
|
4480
4594
|
maxPoints: POINTS_CONFIG_DRIFT,
|
|
4481
4595
|
earnedPoints: driftPoints,
|
|
4482
|
-
passed: drift.
|
|
4483
|
-
detail: drift.
|
|
4484
|
-
suggestion: drift.
|
|
4596
|
+
passed: drift.commitsSinceConfigUpdate <= 15 || !drift.isGitRepo,
|
|
4597
|
+
detail: !drift.isGitRepo ? "Not a git repository \u2014 skipping drift check" : !drift.lastConfigCommit ? "Config files not tracked in git" : drift.commitsSinceConfigUpdate === 0 ? "Config is up to date with latest commits" : `${drift.commitsSinceConfigUpdate} commit${drift.commitsSinceConfigUpdate === 1 ? "" : "s"} since last config update`,
|
|
4598
|
+
suggestion: drift.commitsSinceConfigUpdate > 15 ? `Code has had ${drift.commitsSinceConfigUpdate} commits since last config update \u2014 run \`caliber refresh\` to sync` : void 0,
|
|
4599
|
+
fix: drift.commitsSinceConfigUpdate > 15 ? {
|
|
4600
|
+
action: "refresh_config",
|
|
4601
|
+
data: { commitsSince: drift.commitsSinceConfigUpdate, lastConfigCommit: drift.lastConfigCommit },
|
|
4602
|
+
instruction: `Config is ${drift.commitsSinceConfigUpdate} commits behind. Review recent changes and update config accordingly.`
|
|
4603
|
+
} : void 0
|
|
4485
4604
|
});
|
|
4486
4605
|
return checks;
|
|
4487
4606
|
}
|
|
4488
4607
|
|
|
4489
4608
|
// src/scoring/checks/freshness.ts
|
|
4490
|
-
import {
|
|
4609
|
+
import { execSync as execSync9 } from "child_process";
|
|
4491
4610
|
import { join as join6 } from "path";
|
|
4492
|
-
function
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
}
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4611
|
+
function getCommitsSinceConfigUpdate(dir) {
|
|
4612
|
+
const configFiles = ["CLAUDE.md", "AGENTS.md", ".cursorrules"];
|
|
4613
|
+
for (const file of configFiles) {
|
|
4614
|
+
try {
|
|
4615
|
+
const hash = execSync9(
|
|
4616
|
+
`git log -1 --format=%H -- "${file}"`,
|
|
4617
|
+
{ cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4618
|
+
).trim();
|
|
4619
|
+
if (hash) {
|
|
4620
|
+
const countStr = execSync9(
|
|
4621
|
+
`git rev-list --count ${hash}..HEAD`,
|
|
4622
|
+
{ cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
4623
|
+
).trim();
|
|
4624
|
+
return parseInt(countStr, 10) || 0;
|
|
4625
|
+
}
|
|
4626
|
+
} catch {
|
|
4627
|
+
}
|
|
4507
4628
|
}
|
|
4629
|
+
return null;
|
|
4508
4630
|
}
|
|
4509
4631
|
function checkFreshness(dir) {
|
|
4510
4632
|
const checks = [];
|
|
4511
|
-
const
|
|
4512
|
-
const agentsMdPath = join6(dir, "AGENTS.md");
|
|
4513
|
-
const primaryPath = existsSync6(claudeMdPath) ? claudeMdPath : agentsMdPath;
|
|
4514
|
-
const primaryName = existsSync6(claudeMdPath) ? "CLAUDE.md" : "AGENTS.md";
|
|
4515
|
-
const daysOld = daysSinceModified(primaryPath);
|
|
4633
|
+
const commitsSince = getCommitsSinceConfigUpdate(dir);
|
|
4516
4634
|
let freshnessPoints = 0;
|
|
4517
4635
|
let freshnessDetail = "";
|
|
4518
|
-
if (
|
|
4519
|
-
freshnessDetail = "
|
|
4636
|
+
if (commitsSince === null) {
|
|
4637
|
+
freshnessDetail = "Config files not tracked in git";
|
|
4638
|
+
freshnessPoints = 0;
|
|
4520
4639
|
} else {
|
|
4521
|
-
const threshold =
|
|
4640
|
+
const threshold = FRESHNESS_COMMIT_THRESHOLDS.find((t) => commitsSince <= t.maxCommits);
|
|
4522
4641
|
freshnessPoints = threshold ? threshold.points : 0;
|
|
4523
|
-
freshnessDetail =
|
|
4642
|
+
freshnessDetail = commitsSince === 0 ? "Config updated in the latest commit" : `${commitsSince} commit${commitsSince === 1 ? "" : "s"} since last config update`;
|
|
4524
4643
|
}
|
|
4525
4644
|
checks.push({
|
|
4526
4645
|
id: "claude_md_freshness",
|
|
4527
|
-
name:
|
|
4646
|
+
name: "Config freshness",
|
|
4528
4647
|
category: "freshness",
|
|
4529
4648
|
maxPoints: POINTS_FRESHNESS,
|
|
4530
4649
|
earnedPoints: freshnessPoints,
|
|
4531
|
-
passed: freshnessPoints >=
|
|
4650
|
+
passed: freshnessPoints >= 3,
|
|
4532
4651
|
detail: freshnessDetail,
|
|
4533
|
-
suggestion:
|
|
4652
|
+
suggestion: commitsSince !== null && freshnessPoints < 3 ? `Config is ${commitsSince} commits behind \u2014 run \`caliber refresh\` to update it` : void 0,
|
|
4653
|
+
fix: commitsSince !== null && freshnessPoints < 3 ? {
|
|
4654
|
+
action: "refresh_config",
|
|
4655
|
+
data: { commitsSince },
|
|
4656
|
+
instruction: `Config is ${commitsSince} commits behind. Update it to reflect recent changes.`
|
|
4657
|
+
} : void 0
|
|
4534
4658
|
});
|
|
4535
4659
|
const filesToScan = [
|
|
4536
4660
|
"CLAUDE.md",
|
|
@@ -4543,17 +4667,16 @@ function checkFreshness(dir) {
|
|
|
4543
4667
|
];
|
|
4544
4668
|
const secretFindings = [];
|
|
4545
4669
|
for (const rel of filesToScan) {
|
|
4546
|
-
const content =
|
|
4670
|
+
const content = readFileOrNull2(join6(dir, rel));
|
|
4547
4671
|
if (!content) continue;
|
|
4548
4672
|
const lines = content.split("\n");
|
|
4549
4673
|
for (let i = 0; i < lines.length; i++) {
|
|
4550
4674
|
for (const pattern of SECRET_PATTERNS) {
|
|
4551
4675
|
if (pattern.test(lines[i])) {
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
line: i + 1
|
|
4555
|
-
|
|
4556
|
-
});
|
|
4676
|
+
const isPlaceholder = SECRET_PLACEHOLDER_PATTERNS.some((p) => p.test(lines[i]));
|
|
4677
|
+
if (!isPlaceholder) {
|
|
4678
|
+
secretFindings.push({ file: rel, line: i + 1 });
|
|
4679
|
+
}
|
|
4557
4680
|
break;
|
|
4558
4681
|
}
|
|
4559
4682
|
}
|
|
@@ -4564,17 +4687,21 @@ function checkFreshness(dir) {
|
|
|
4564
4687
|
id: "no_secrets",
|
|
4565
4688
|
name: "No secrets in config files",
|
|
4566
4689
|
category: "freshness",
|
|
4567
|
-
maxPoints: POINTS_NO_SECRETS,
|
|
4568
|
-
// This is a penalty: -8 if secrets found, +8 if clean
|
|
4569
4690
|
earnedPoints: hasSecrets ? -POINTS_NO_SECRETS : POINTS_NO_SECRETS,
|
|
4691
|
+
maxPoints: POINTS_NO_SECRETS,
|
|
4570
4692
|
passed: !hasSecrets,
|
|
4571
4693
|
detail: hasSecrets ? `${secretFindings.length} potential secret${secretFindings.length === 1 ? "" : "s"} found in ${secretFindings[0].file}:${secretFindings[0].line}` : "No secrets detected",
|
|
4572
|
-
suggestion: hasSecrets ? `Remove secrets from ${secretFindings[0].file}:${secretFindings[0].line} \u2014 use environment variables instead` : void 0
|
|
4694
|
+
suggestion: hasSecrets ? `Remove secrets from ${secretFindings[0].file}:${secretFindings[0].line} \u2014 use environment variables instead` : void 0,
|
|
4695
|
+
fix: hasSecrets ? {
|
|
4696
|
+
action: "remove_secrets",
|
|
4697
|
+
data: { findings: secretFindings.slice(0, 5) },
|
|
4698
|
+
instruction: `Remove credentials from ${secretFindings[0].file}:${secretFindings[0].line}. Use environment variable references instead.`
|
|
4699
|
+
} : void 0
|
|
4573
4700
|
});
|
|
4574
4701
|
const settingsPath = join6(dir, ".claude", "settings.json");
|
|
4575
4702
|
let hasPermissions = false;
|
|
4576
4703
|
let permissionDetail = "";
|
|
4577
|
-
const settingsContent =
|
|
4704
|
+
const settingsContent = readFileOrNull2(settingsPath);
|
|
4578
4705
|
if (settingsContent) {
|
|
4579
4706
|
try {
|
|
4580
4707
|
const settings = JSON.parse(settingsContent);
|
|
@@ -4596,27 +4723,25 @@ function checkFreshness(dir) {
|
|
|
4596
4723
|
earnedPoints: hasPermissions ? POINTS_PERMISSIONS : 0,
|
|
4597
4724
|
passed: hasPermissions,
|
|
4598
4725
|
detail: permissionDetail,
|
|
4599
|
-
suggestion: hasPermissions ? void 0 : "Add permissions.allow to .claude/settings.json for safer agent execution"
|
|
4726
|
+
suggestion: hasPermissions ? void 0 : "Add permissions.allow to .claude/settings.json for safer agent execution",
|
|
4727
|
+
fix: hasPermissions ? void 0 : {
|
|
4728
|
+
action: "add_permissions",
|
|
4729
|
+
data: {},
|
|
4730
|
+
instruction: "Add a permissions.allow list to .claude/settings.json with commonly used commands."
|
|
4731
|
+
}
|
|
4600
4732
|
});
|
|
4601
4733
|
return checks;
|
|
4602
4734
|
}
|
|
4603
4735
|
|
|
4604
4736
|
// src/scoring/checks/bonus.ts
|
|
4605
|
-
import { existsSync as
|
|
4606
|
-
import { execSync as
|
|
4737
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
|
|
4738
|
+
import { execSync as execSync10 } from "child_process";
|
|
4607
4739
|
import { join as join7 } from "path";
|
|
4608
|
-
function readFileOrNull6(path27) {
|
|
4609
|
-
try {
|
|
4610
|
-
return readFileSync7(path27, "utf-8");
|
|
4611
|
-
} catch {
|
|
4612
|
-
return null;
|
|
4613
|
-
}
|
|
4614
|
-
}
|
|
4615
4740
|
function hasPreCommitHook(dir) {
|
|
4616
4741
|
try {
|
|
4617
|
-
const gitDir =
|
|
4742
|
+
const gitDir = execSync10("git rev-parse --git-dir", { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
4618
4743
|
const hookPath = join7(gitDir, "hooks", "pre-commit");
|
|
4619
|
-
const content =
|
|
4744
|
+
const content = readFileOrNull2(hookPath);
|
|
4620
4745
|
return content ? content.includes("caliber") : false;
|
|
4621
4746
|
} catch {
|
|
4622
4747
|
return false;
|
|
@@ -4627,7 +4752,7 @@ function checkBonus(dir) {
|
|
|
4627
4752
|
let hasClaudeHooks = false;
|
|
4628
4753
|
let hasPrecommit = false;
|
|
4629
4754
|
const hookSources = [];
|
|
4630
|
-
const settingsContent =
|
|
4755
|
+
const settingsContent = readFileOrNull2(join7(dir, ".claude", "settings.json"));
|
|
4631
4756
|
if (settingsContent) {
|
|
4632
4757
|
try {
|
|
4633
4758
|
const settings = JSON.parse(settingsContent);
|
|
@@ -4644,7 +4769,6 @@ function checkBonus(dir) {
|
|
|
4644
4769
|
hookSources.push("git pre-commit");
|
|
4645
4770
|
}
|
|
4646
4771
|
const hasHooks = hasClaudeHooks || hasPrecommit;
|
|
4647
|
-
const hookDetail = hasHooks ? hookSources.join(", ") : settingsContent ? "No hooks in settings.json" : "No hooks configured";
|
|
4648
4772
|
checks.push({
|
|
4649
4773
|
id: "hooks_configured",
|
|
4650
4774
|
name: "Hooks configured",
|
|
@@ -4652,10 +4776,15 @@ function checkBonus(dir) {
|
|
|
4652
4776
|
maxPoints: POINTS_HOOKS,
|
|
4653
4777
|
earnedPoints: hasHooks ? POINTS_HOOKS : 0,
|
|
4654
4778
|
passed: hasHooks,
|
|
4655
|
-
detail:
|
|
4656
|
-
suggestion: hasHooks ? void 0 : "Run `caliber hooks --install` for auto-refresh"
|
|
4779
|
+
detail: hasHooks ? hookSources.join(", ") : "No hooks configured",
|
|
4780
|
+
suggestion: hasHooks ? void 0 : "Run `caliber hooks --install` for auto-refresh",
|
|
4781
|
+
fix: hasHooks ? void 0 : {
|
|
4782
|
+
action: "install_hooks",
|
|
4783
|
+
data: {},
|
|
4784
|
+
instruction: "Install caliber hooks for automatic config refresh on commits."
|
|
4785
|
+
}
|
|
4657
4786
|
});
|
|
4658
|
-
const agentsMdExists =
|
|
4787
|
+
const agentsMdExists = existsSync5(join7(dir, "AGENTS.md"));
|
|
4659
4788
|
checks.push({
|
|
4660
4789
|
id: "agents_md_exists",
|
|
4661
4790
|
name: "AGENTS.md exists",
|
|
@@ -4664,16 +4793,21 @@ function checkBonus(dir) {
|
|
|
4664
4793
|
earnedPoints: agentsMdExists ? POINTS_AGENTS_MD : 0,
|
|
4665
4794
|
passed: agentsMdExists,
|
|
4666
4795
|
detail: agentsMdExists ? "Found at project root" : "Not found",
|
|
4667
|
-
suggestion: agentsMdExists ? void 0 : "Add AGENTS.md \u2014 the emerging cross-agent standard
|
|
4796
|
+
suggestion: agentsMdExists ? void 0 : "Add AGENTS.md \u2014 the emerging cross-agent standard",
|
|
4797
|
+
fix: agentsMdExists ? void 0 : {
|
|
4798
|
+
action: "create_file",
|
|
4799
|
+
data: { file: "AGENTS.md" },
|
|
4800
|
+
instruction: "Create AGENTS.md with project context for cross-agent compatibility."
|
|
4801
|
+
}
|
|
4668
4802
|
});
|
|
4669
4803
|
const skillsDir = join7(dir, ".claude", "skills");
|
|
4670
4804
|
let openSkillsCount = 0;
|
|
4671
4805
|
let totalSkillFiles = 0;
|
|
4672
4806
|
try {
|
|
4673
|
-
const entries =
|
|
4807
|
+
const entries = readdirSync3(skillsDir, { withFileTypes: true });
|
|
4674
4808
|
for (const entry of entries) {
|
|
4675
4809
|
if (entry.isDirectory()) {
|
|
4676
|
-
const skillMd =
|
|
4810
|
+
const skillMd = readFileOrNull2(join7(skillsDir, entry.name, "SKILL.md"));
|
|
4677
4811
|
if (skillMd) {
|
|
4678
4812
|
totalSkillFiles++;
|
|
4679
4813
|
if (skillMd.trimStart().startsWith("---")) {
|
|
@@ -4695,7 +4829,12 @@ function checkBonus(dir) {
|
|
|
4695
4829
|
earnedPoints: allOpenSkills ? POINTS_OPEN_SKILLS_FORMAT : 0,
|
|
4696
4830
|
passed: allOpenSkills,
|
|
4697
4831
|
detail: totalSkillFiles === 0 ? "No skills to check" : allOpenSkills ? `All ${totalSkillFiles} skill${totalSkillFiles === 1 ? "" : "s"} use SKILL.md with frontmatter` : `${openSkillsCount}/${totalSkillFiles} use OpenSkills format`,
|
|
4698
|
-
suggestion: totalSkillFiles > 0 && !allOpenSkills ? "Migrate skills to .claude/skills/{name}/SKILL.md with YAML frontmatter
|
|
4832
|
+
suggestion: totalSkillFiles > 0 && !allOpenSkills ? "Migrate skills to .claude/skills/{name}/SKILL.md with YAML frontmatter" : void 0,
|
|
4833
|
+
fix: totalSkillFiles > 0 && !allOpenSkills ? {
|
|
4834
|
+
action: "migrate_skills",
|
|
4835
|
+
data: { openSkills: openSkillsCount, total: totalSkillFiles },
|
|
4836
|
+
instruction: "Migrate flat skill files to .claude/skills/{name}/SKILL.md with YAML frontmatter."
|
|
4837
|
+
} : void 0
|
|
4699
4838
|
});
|
|
4700
4839
|
return checks;
|
|
4701
4840
|
}
|
|
@@ -4737,14 +4876,15 @@ function filterChecksForTarget(checks, target) {
|
|
|
4737
4876
|
if (CURSOR_ONLY_CHECKS.has(c.id)) return target.includes("cursor");
|
|
4738
4877
|
if (CODEX_ONLY_CHECKS.has(c.id)) return target.includes("codex");
|
|
4739
4878
|
if (BOTH_ONLY_CHECKS.has(c.id)) return target.includes("claude") && target.includes("cursor");
|
|
4879
|
+
if (NON_CODEX_CHECKS.has(c.id)) return !target.includes("codex");
|
|
4740
4880
|
return true;
|
|
4741
4881
|
});
|
|
4742
4882
|
}
|
|
4743
4883
|
function detectTargetAgent(dir) {
|
|
4744
4884
|
const agents = [];
|
|
4745
|
-
if (
|
|
4746
|
-
if (
|
|
4747
|
-
if (
|
|
4885
|
+
if (existsSync6(join8(dir, "CLAUDE.md")) || existsSync6(join8(dir, ".claude", "skills"))) agents.push("claude");
|
|
4886
|
+
if (existsSync6(join8(dir, ".cursorrules")) || existsSync6(join8(dir, ".cursor", "rules"))) agents.push("cursor");
|
|
4887
|
+
if (existsSync6(join8(dir, ".codex")) || existsSync6(join8(dir, ".agents", "skills"))) agents.push("codex");
|
|
4748
4888
|
return agents.length > 0 ? agents : ["claude"];
|
|
4749
4889
|
}
|
|
4750
4890
|
function computeLocalScore(dir, targetAgent) {
|
|
@@ -4752,7 +4892,7 @@ function computeLocalScore(dir, targetAgent) {
|
|
|
4752
4892
|
const allChecks = [
|
|
4753
4893
|
...checkExistence(dir),
|
|
4754
4894
|
...checkQuality(dir),
|
|
4755
|
-
...
|
|
4895
|
+
...checkGrounding(dir),
|
|
4756
4896
|
...checkAccuracy(dir),
|
|
4757
4897
|
...checkFreshness(dir),
|
|
4758
4898
|
...checkBonus(dir)
|
|
@@ -4770,7 +4910,7 @@ function computeLocalScore(dir, targetAgent) {
|
|
|
4770
4910
|
categories: {
|
|
4771
4911
|
existence: sumCategory(checks, "existence"),
|
|
4772
4912
|
quality: sumCategory(checks, "quality"),
|
|
4773
|
-
|
|
4913
|
+
grounding: sumCategory(checks, "grounding"),
|
|
4774
4914
|
accuracy: sumCategory(checks, "accuracy"),
|
|
4775
4915
|
freshness: sumCategory(checks, "freshness"),
|
|
4776
4916
|
bonus: sumCategory(checks, "bonus")
|
|
@@ -4790,12 +4930,12 @@ var AGENT_DISPLAY_NAMES = {
|
|
|
4790
4930
|
var CATEGORY_LABELS = {
|
|
4791
4931
|
existence: "FILES & SETUP",
|
|
4792
4932
|
quality: "QUALITY",
|
|
4793
|
-
|
|
4933
|
+
grounding: "GROUNDING",
|
|
4794
4934
|
accuracy: "ACCURACY",
|
|
4795
4935
|
freshness: "FRESHNESS & SAFETY",
|
|
4796
4936
|
bonus: "BONUS"
|
|
4797
4937
|
};
|
|
4798
|
-
var CATEGORY_ORDER = ["existence", "quality", "
|
|
4938
|
+
var CATEGORY_ORDER = ["existence", "quality", "grounding", "accuracy", "freshness", "bonus"];
|
|
4799
4939
|
function gradeColor(grade) {
|
|
4800
4940
|
switch (grade) {
|
|
4801
4941
|
case "A":
|
|
@@ -4908,7 +5048,7 @@ function displayScoreDelta(before, after) {
|
|
|
4908
5048
|
import chalk7 from "chalk";
|
|
4909
5049
|
import ora from "ora";
|
|
4910
5050
|
import select4 from "@inquirer/select";
|
|
4911
|
-
import { mkdirSync, readFileSync as
|
|
5051
|
+
import { mkdirSync, readFileSync as readFileSync4, readdirSync as readdirSync4, existsSync as existsSync7, writeFileSync } from "fs";
|
|
4912
5052
|
import { join as join9, dirname as dirname2 } from "path";
|
|
4913
5053
|
|
|
4914
5054
|
// src/scanner/index.ts
|
|
@@ -5067,7 +5207,7 @@ import fs22 from "fs";
|
|
|
5067
5207
|
import path17 from "path";
|
|
5068
5208
|
import os3 from "os";
|
|
5069
5209
|
import crypto3 from "crypto";
|
|
5070
|
-
import { execSync as
|
|
5210
|
+
import { execSync as execSync11 } from "child_process";
|
|
5071
5211
|
var CONFIG_DIR2 = path17.join(os3.homedir(), ".caliber");
|
|
5072
5212
|
var CONFIG_FILE2 = path17.join(CONFIG_DIR2, "config.json");
|
|
5073
5213
|
var runtimeDisabled = false;
|
|
@@ -5094,7 +5234,7 @@ function getMachineId() {
|
|
|
5094
5234
|
}
|
|
5095
5235
|
function getGitEmailHash() {
|
|
5096
5236
|
try {
|
|
5097
|
-
const email =
|
|
5237
|
+
const email = execSync11("git config user.email", { encoding: "utf-8" }).trim();
|
|
5098
5238
|
if (!email) return void 0;
|
|
5099
5239
|
return crypto3.createHash("sha256").update(email).digest("hex");
|
|
5100
5240
|
} catch {
|
|
@@ -5244,7 +5384,7 @@ function getInstalledSkills() {
|
|
|
5244
5384
|
];
|
|
5245
5385
|
for (const dir of dirs) {
|
|
5246
5386
|
try {
|
|
5247
|
-
const entries =
|
|
5387
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
5248
5388
|
for (const entry of entries) {
|
|
5249
5389
|
if (entry.isDirectory()) {
|
|
5250
5390
|
installed.add(entry.name.toLowerCase());
|
|
@@ -5409,9 +5549,9 @@ Already installed skills: ${Array.from(installed).join(", ")}`);
|
|
|
5409
5549
|
}
|
|
5410
5550
|
function extractTopDeps() {
|
|
5411
5551
|
const pkgPath = join9(process.cwd(), "package.json");
|
|
5412
|
-
if (!
|
|
5552
|
+
if (!existsSync7(pkgPath)) return [];
|
|
5413
5553
|
try {
|
|
5414
|
-
const pkg3 = JSON.parse(
|
|
5554
|
+
const pkg3 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
5415
5555
|
const deps = Object.keys(pkg3.dependencies ?? {});
|
|
5416
5556
|
const trivial = /* @__PURE__ */ new Set([
|
|
5417
5557
|
"typescript",
|
|
@@ -6004,7 +6144,7 @@ async function initCommand(options) {
|
|
|
6004
6144
|
let passingChecks;
|
|
6005
6145
|
let currentScore;
|
|
6006
6146
|
if (hasExistingConfig && baselineScore.score >= 95 && !options.force) {
|
|
6007
|
-
failingChecks = llmFixableChecks.map((c) => ({ name: c.name, suggestion: c.suggestion }));
|
|
6147
|
+
failingChecks = llmFixableChecks.map((c) => ({ name: c.name, suggestion: c.suggestion, fix: c.fix }));
|
|
6008
6148
|
passingChecks = baselineScore.checks.filter((c) => c.passed).map((c) => ({ name: c.name }));
|
|
6009
6149
|
currentScore = baselineScore.score;
|
|
6010
6150
|
if (failingChecks.length > 0) {
|
|
@@ -6102,7 +6242,7 @@ async function initCommand(options) {
|
|
|
6102
6242
|
}, onComplete: () => {
|
|
6103
6243
|
}, onError: () => {
|
|
6104
6244
|
} },
|
|
6105
|
-
inlineFailingChecks.map((c) => ({ name: c.name, suggestion: c.suggestion })),
|
|
6245
|
+
inlineFailingChecks.map((c) => ({ name: c.name, suggestion: c.suggestion, fix: c.fix })),
|
|
6106
6246
|
inlineScore.score,
|
|
6107
6247
|
inlineScore.checks.filter((c) => c.passed).map((c) => ({ name: c.name })),
|
|
6108
6248
|
{ skipSkills: true, forceTargetedFix: true }
|
|
@@ -6196,7 +6336,9 @@ async function initCommand(options) {
|
|
|
6196
6336
|
if (agentsStub) {
|
|
6197
6337
|
const setup = generatedSetup;
|
|
6198
6338
|
setup.codex = { agentsMd: agentsStub.content };
|
|
6199
|
-
if (!setup.targetAgent
|
|
6339
|
+
if (!setup.targetAgent) {
|
|
6340
|
+
setup.targetAgent = ["codex"];
|
|
6341
|
+
} else if (Array.isArray(setup.targetAgent) && !setup.targetAgent.includes("codex")) {
|
|
6200
6342
|
setup.targetAgent.push("codex");
|
|
6201
6343
|
}
|
|
6202
6344
|
}
|
|
@@ -6923,7 +7065,7 @@ import chalk13 from "chalk";
|
|
|
6923
7065
|
import ora5 from "ora";
|
|
6924
7066
|
|
|
6925
7067
|
// src/lib/git-diff.ts
|
|
6926
|
-
import { execSync as
|
|
7068
|
+
import { execSync as execSync12 } from "child_process";
|
|
6927
7069
|
var MAX_DIFF_BYTES = 1e5;
|
|
6928
7070
|
var DOC_PATTERNS = [
|
|
6929
7071
|
"CLAUDE.md",
|
|
@@ -6937,7 +7079,7 @@ function excludeArgs() {
|
|
|
6937
7079
|
}
|
|
6938
7080
|
function safeExec(cmd) {
|
|
6939
7081
|
try {
|
|
6940
|
-
return
|
|
7082
|
+
return execSync12(cmd, {
|
|
6941
7083
|
encoding: "utf-8",
|
|
6942
7084
|
stdio: ["pipe", "pipe", "pipe"],
|
|
6943
7085
|
maxBuffer: 10 * 1024 * 1024
|
|
@@ -7864,7 +8006,7 @@ learn.command("status").description("Show learning system status").action(tracke
|
|
|
7864
8006
|
import fs32 from "fs";
|
|
7865
8007
|
import path26 from "path";
|
|
7866
8008
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7867
|
-
import { execSync as
|
|
8009
|
+
import { execSync as execSync13 } from "child_process";
|
|
7868
8010
|
import chalk17 from "chalk";
|
|
7869
8011
|
import ora6 from "ora";
|
|
7870
8012
|
import confirm from "@inquirer/confirm";
|
|
@@ -7874,7 +8016,7 @@ var pkg2 = JSON.parse(
|
|
|
7874
8016
|
);
|
|
7875
8017
|
function getInstalledVersion() {
|
|
7876
8018
|
try {
|
|
7877
|
-
const globalRoot =
|
|
8019
|
+
const globalRoot = execSync13("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7878
8020
|
const pkgPath = path26.join(globalRoot, "@rely-ai", "caliber", "package.json");
|
|
7879
8021
|
return JSON.parse(fs32.readFileSync(pkgPath, "utf-8")).version;
|
|
7880
8022
|
} catch {
|
|
@@ -7919,7 +8061,7 @@ Update available: ${current} -> ${latest}`)
|
|
|
7919
8061
|
}
|
|
7920
8062
|
const spinner = ora6("Updating caliber...").start();
|
|
7921
8063
|
try {
|
|
7922
|
-
|
|
8064
|
+
execSync13(`npm install -g @rely-ai/caliber@${latest}`, {
|
|
7923
8065
|
stdio: "pipe",
|
|
7924
8066
|
timeout: 12e4,
|
|
7925
8067
|
env: { ...process.env, npm_config_fund: "false", npm_config_audit: "false" }
|
|
@@ -7936,7 +8078,7 @@ Update available: ${current} -> ${latest}`)
|
|
|
7936
8078
|
console.log(chalk17.dim(`
|
|
7937
8079
|
Restarting: caliber ${args.join(" ")}
|
|
7938
8080
|
`));
|
|
7939
|
-
|
|
8081
|
+
execSync13(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
|
|
7940
8082
|
stdio: "inherit",
|
|
7941
8083
|
env: { ...process.env, CALIBER_SKIP_UPDATE_CHECK: "1" }
|
|
7942
8084
|
});
|