@rely-ai/caliber 1.12.14 → 1.12.16

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.
Files changed (2) hide show
  1. package/dist/bin.js +702 -560
  2. 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
- parts.push(`- ${check.name}${check.suggestion ? `: ${check.suggestion}` : ""}`);
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 "Documented commands exist": DELETE the specific invalid commands listed in the suggestion. Do NOT replace them with other commands unless you are 100% certain they exist.
2464
- - For "Documented paths exist": DELETE the specific non-existent paths listed in the suggestion. Do NOT replace them with guessed paths.
2465
- - For "Concise context files": Remove the least important lines to get under the line limit. Do NOT add new content.`);
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 existsSync8 } from "fs";
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 existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
3736
- import { join as join3 } from "path";
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 POINTS_HAS_COMMANDS = 8;
3748
- var POINTS_NOT_BLOATED = 6;
3749
- var POINTS_NO_VAGUE = 4;
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 POINTS_NO_CONTRADICTIONS = 2;
3753
- var POINTS_DEP_COVERAGE = 10;
3754
- var POINTS_SERVICE_COVERAGE = 6;
3755
- var POINTS_MCP_COVERAGE = 4;
3756
- var POINTS_COMMANDS_VALID = 6;
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 BLOAT_THRESHOLDS = [
3766
- { maxLines: 150, points: 6 },
3767
- { maxLines: 200, points: 4 },
3768
- { maxLines: 300, points: 3 },
3769
- { maxLines: 500, points: 1 }
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 FRESHNESS_THRESHOLDS = [
3772
- { maxDaysOld: 7, points: 4 },
3773
- { maxDaysOld: 14, points: 3 },
3774
- { maxDaysOld: 30, points: 2 },
3775
- { maxDaysOld: 60, points: 1 }
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 VAGUE_PATTERNS = [
3778
- /follow\s+best\s+practices/i,
3779
- /write\s+clean\s+code/i,
3780
- /ensure\s+quality/i,
3781
- /be\s+consistent/i,
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 COMMAND_PATTERNS = [
3788
- /(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?(?:dev|build|test|lint|start|format)/i,
3789
- /(?:make|cargo|go)\s+(?:build|test|run|lint|vet|fmt)/i,
3790
- /(?:vitest|jest|pytest|mocha|ava)\b/i,
3791
- /(?:eslint|prettier|biome|ruff)\b/i,
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 CONTRADICTION_PAIRS = [
3804
- { a: /\buse\s+npm\b/i, b: /\buse\s+(?:pnpm|yarn|bun)\b/i },
3805
- { a: /\buse\s+pnpm\b/i, b: /\buse\s+(?:npm|yarn|bun)\b/i },
3806
- { a: /\buse\s+yarn\b/i, b: /\buse\s+(?:npm|pnpm|bun)\b/i },
3807
- { a: /\buse\s+tabs\b/i, b: /\buse\s+spaces\b/i },
3808
- { a: /\bsemicolons?\b.*\balways\b/i, b: /\bno\s+semicolons?\b/i }
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 readdirSync2(dir, { recursive: true }).map(String).filter((f) => pattern.test(f));
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 = readFileSync3(join3(dir, rel), "utf-8");
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 = existsSync3(join3(dir, "CLAUDE.md"));
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 = existsSync3(join3(dir, ".cursorrules"));
4061
- const cursorRulesDir = existsSync3(join3(dir, ".cursor", "rules"));
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 = existsSync3(join3(dir, "AGENTS.md"));
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(join3(dir, ".claude", "skills"), /\.(md|SKILL\.md)$/);
4085
- const codexSkills = countFiles(join3(dir, ".agents", "skills"), /SKILL\.md$/);
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 (SkillsBench research)" : void 0
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(join3(dir, ".cursor", "rules"), /\.mdc$/);
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: mcpPassed ? POINTS_MCP_SERVERS : 0,
4122
- passed: mcpPassed,
4123
- detail: mcp.count > 0 ? `${mcp.count} server${mcp.count === 1 ? "" : "s"} in ${mcp.sources.join(", ")}` : hasServices ? "No MCP servers (external services detected)" : "No MCP servers needed (no external services detected)",
4124
- suggestion: !mcpPassed ? "Configure MCP servers in .mcp.json for detected external services" : void 0
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
- function readFileOrNull3(path27) {
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 readFileSync4(path27, "utf-8");
4021
+ return readFileSync3(filePath, "utf-8");
4148
4022
  } catch {
4149
4023
  return null;
4150
4024
  }
4151
4025
  }
4152
- function countLines(content) {
4153
- return content.split("\n").length;
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 = readFileOrNull3(join4(dir, "CLAUDE.md"));
4158
- const cursorrules = readFileOrNull3(join4(dir, ".cursorrules"));
4159
- const agentsMd = readFileOrNull3(join4(dir, "AGENTS.md"));
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 hasCommands = primaryInstructions ? COMMAND_PATTERNS.some((p) => p.test(primaryInstructions)) : false;
4164
- const matchedCommands = primaryInstructions ? COMMAND_PATTERNS.filter((p) => p.test(primaryInstructions)).map((p) => {
4165
- const m = primaryInstructions.match(p);
4166
- return m ? m[0] : "";
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: "has_commands",
4170
- name: "Build/test/lint commands",
4222
+ id: "has_executable_content",
4223
+ name: "Executable content (code blocks)",
4171
4224
  category: "quality",
4172
- maxPoints: POINTS_HAS_COMMANDS,
4173
- earnedPoints: hasCommands ? POINTS_HAS_COMMANDS : 0,
4174
- passed: hasCommands,
4175
- detail: hasCommands ? `Found: ${matchedCommands.slice(0, 3).join(", ")}` : primaryInstructions ? "No build/test/lint commands detected" : "No instructions file to check",
4176
- suggestion: hasCommands ? void 0 : "Add build, test, and lint commands to your instructions file"
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 primaryFile = claudeMd ?? agentsMd ?? cursorrules;
4179
- const primaryName = claudeMd ? "CLAUDE.md" : agentsMd ? "AGENTS.md" : cursorrules ? ".cursorrules" : null;
4180
- let bloatPoints = 0;
4181
- let lineCount = 0;
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: "not_bloated",
4191
- name: "Concise context files",
4241
+ id: "concise_config",
4242
+ name: "Concise config (token budget)",
4192
4243
  category: "quality",
4193
- maxPoints: POINTS_NOT_BLOATED,
4194
- earnedPoints: bloatPoints,
4195
- passed: bloatPoints >= 6,
4196
- detail: primaryName ? `${primaryName}: ${lineCount} lines` : "No context files to measure",
4197
- suggestion: bloatPoints < POINTS_NOT_BLOATED && primaryName ? `${primaryName} is ${lineCount} lines \u2014 must be \u2264150 lines for full points (currently losing ${POINTS_NOT_BLOATED - bloatPoints} pts)` : void 0
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
- const vagueMatches = [];
4200
- if (combinedContent) {
4201
- const lines = combinedContent.split("\n");
4202
- for (let i = 0; i < lines.length; i++) {
4203
- for (const pattern of VAGUE_PATTERNS) {
4204
- if (pattern.test(lines[i])) {
4205
- vagueMatches.push({ pattern: lines[i].trim(), line: i + 1 });
4206
- break;
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: "no_vague_instructions",
4213
- name: "No vague instructions",
4282
+ id: "concreteness",
4283
+ name: "Concrete instructions",
4214
4284
  category: "quality",
4215
- maxPoints: POINTS_NO_VAGUE,
4216
- earnedPoints: vagueMatches.length === 0 ? POINTS_NO_VAGUE : 0,
4217
- passed: vagueMatches.length === 0,
4218
- detail: vagueMatches.length === 0 ? "All instructions are specific and actionable" : `${vagueMatches.length} vague instruction${vagueMatches.length === 1 ? "" : "s"} found`,
4219
- suggestion: vagueMatches.length > 0 ? `Replace "${vagueMatches[0].pattern.slice(0, 50)}" (line ${vagueMatches[0].line}) with specific, measurable guidance` : void 0
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 contradictions = [];
4267
- if (allContent.length >= 2) {
4268
- for (const pair of CONTRADICTION_PAIRS) {
4269
- const fileA = allContent.find((c) => pair.a.test(c));
4270
- const fileB = allContent.find((c) => pair.b.test(c));
4271
- if (fileA && fileB && fileA !== fileB) {
4272
- contradictions.push(`"${pair.a.source}" vs "${pair.b.source}"`);
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
- for (const content of allContent) {
4277
- for (const pair of CONTRADICTION_PAIRS) {
4278
- if (pair.a.test(content) && pair.b.test(content)) {
4279
- contradictions.push(`Same file contains "${pair.a.source}" and "${pair.b.source}"`);
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 hasContradictions = contradictions.length > 0;
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: "no_contradictions",
4286
- name: "No contradictions",
4287
- category: "quality",
4288
- maxPoints: POINTS_NO_CONTRADICTIONS,
4289
- earnedPoints: hasContradictions ? 0 : POINTS_NO_CONTRADICTIONS,
4290
- passed: !hasContradictions,
4291
- detail: hasContradictions ? `${contradictions.length} contradiction${contradictions.length === 1 ? "" : "s"} found` : "No conflicting instructions detected",
4292
- suggestion: hasContradictions ? `Contradiction: ${contradictions[0]}. Remove or rephrase one of the conflicting statements.` : void 0
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 existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync } from "fs";
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 readFileOrNull4(path27) {
4301
- try {
4302
- return readFileSync5(path27, "utf-8");
4303
- } catch {
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 cmdPattern = /(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?([a-zA-Z0-9_:@./-]+)/g;
4328
- const seen = /* @__PURE__ */ new Set();
4329
- let match;
4330
- while ((match = cmdPattern.exec(claudeMd)) !== null) {
4331
- const scriptName = match[1].replace(/[.,;:!?)]+$/, "");
4332
- if (seen.has(scriptName)) continue;
4333
- seen.add(scriptName);
4334
- const builtins = /* @__PURE__ */ new Set(["install", "ci", "test", "start", "init", "publish", "pack", "link", "uninstall"]);
4335
- if (builtins.has(scriptName)) {
4336
- if ((scriptName === "test" || scriptName === "start") && !scripts.has(scriptName)) {
4337
- invalid.push(`${match[0]} (no "${scriptName}" script in package.json)`);
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
- invalid.push(`${match[0]} (no "${scriptName}" script in package.json)`);
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(`${match[0]} (no "${target}" target in Makefile)`);
4493
+ invalid.push(ref);
4374
4494
  }
4375
4495
  }
4376
4496
  }
4377
4497
  return { valid, invalid, total: valid.length + invalid.length };
4378
4498
  }
4379
- function validateDocumentedPaths(dir) {
4380
- const claudeMd = readFileOrNull4(join5(dir, "CLAUDE.md"));
4381
- if (!claudeMd) return { valid: [], invalid: [], total: 0 };
4382
- const valid = [];
4383
- const invalid = [];
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
- return { valid, invalid, total: valid.length + invalid.length };
4399
- }
4400
- function detectConfigDrift(dir) {
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 files = readdirSync3(fullPath, { recursive: true }).map(String).filter((f) => /\.(ts|js|tsx|jsx|py|go|rs|java|rb)$/.test(f));
4408
- for (const file of files.slice(0, 100)) {
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
- const stat = statSync(join5(fullPath, file));
4411
- if (stat.mtime.getTime() > latestSrcMtime) {
4412
- latestSrcMtime = stat.mtime.getTime();
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
- const configFiles = ["CLAUDE.md", ".cursorrules"];
4421
- let latestConfigMtime = 0;
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
- if (latestSrcMtime === 0 || latestConfigMtime === 0) {
4432
- return { driftDays: 0, srcLastModified: null, configLastModified: null };
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 cmds = validateDocumentedCommands(dir);
4445
- const cmdRatio = cmds.total > 0 ? cmds.valid.length / cmds.total : 1;
4446
- const cmdPoints = cmds.total === 0 ? POINTS_COMMANDS_VALID : Math.round(cmdRatio * POINTS_COMMANDS_VALID);
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: "paths_valid",
4462
- name: "Documented paths exist",
4557
+ id: "references_valid",
4558
+ name: "References point to real files",
4463
4559
  category: "accuracy",
4464
- maxPoints: POINTS_PATHS_VALID,
4465
- earnedPoints: pathPoints,
4466
- passed: pathRatio >= 0.8,
4467
- detail: paths.total === 0 ? "No file paths documented" : `${paths.valid.length}/${paths.total} paths verified`,
4468
- suggestion: paths.invalid.length > 0 ? `Remove these non-existent paths from CLAUDE.md: ${paths.invalid.join("; ")}` : void 0
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 = detectConfigDrift(dir);
4575
+ const drift = detectGitDrift(dir);
4471
4576
  let driftPoints = POINTS_CONFIG_DRIFT;
4472
- if (drift.driftDays > 30) driftPoints = 0;
4473
- else if (drift.driftDays > 14) driftPoints = Math.round(POINTS_CONFIG_DRIFT * 0.25);
4474
- else if (drift.driftDays > 7) driftPoints = Math.round(POINTS_CONFIG_DRIFT * 0.5);
4475
- else if (drift.driftDays > 3) driftPoints = Math.round(POINTS_CONFIG_DRIFT * 0.75);
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.driftDays <= 7,
4483
- detail: drift.srcLastModified && drift.configLastModified ? drift.driftDays === 0 ? "Config is up to date with code changes" : `Code changed ${drift.driftDays} day${drift.driftDays === 1 ? "" : "s"} after last config update` : "Could not determine drift",
4484
- suggestion: drift.driftDays > 7 ? `Code has changed since last config update \u2014 run \`caliber refresh\` to sync` : void 0
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 { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
4609
+ import { execSync as execSync9 } from "child_process";
4491
4610
  import { join as join6 } from "path";
4492
- function readFileOrNull5(path27) {
4493
- try {
4494
- return readFileSync6(path27, "utf-8");
4495
- } catch {
4496
- return null;
4497
- }
4498
- }
4499
- function daysSinceModified(filePath) {
4500
- try {
4501
- const stat = statSync2(filePath);
4502
- const now = Date.now();
4503
- const mtime = stat.mtime.getTime();
4504
- return Math.floor((now - mtime) / (1e3 * 60 * 60 * 24));
4505
- } catch {
4506
- return null;
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 claudeMdPath = join6(dir, "CLAUDE.md");
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 (daysOld === null) {
4519
- freshnessDetail = "No instructions file to check";
4636
+ if (commitsSince === null) {
4637
+ freshnessDetail = "Config files not tracked in git";
4638
+ freshnessPoints = 0;
4520
4639
  } else {
4521
- const threshold = FRESHNESS_THRESHOLDS.find((t) => daysOld <= t.maxDaysOld);
4640
+ const threshold = FRESHNESS_COMMIT_THRESHOLDS.find((t) => commitsSince <= t.maxCommits);
4522
4641
  freshnessPoints = threshold ? threshold.points : 0;
4523
- freshnessDetail = daysOld === 0 ? "Modified today" : daysOld === 1 ? "Modified yesterday" : `Modified ${daysOld} days ago`;
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: `${primaryName} freshness`,
4646
+ name: "Config freshness",
4528
4647
  category: "freshness",
4529
4648
  maxPoints: POINTS_FRESHNESS,
4530
4649
  earnedPoints: freshnessPoints,
4531
- passed: freshnessPoints >= 4,
4650
+ passed: freshnessPoints >= 3,
4532
4651
  detail: freshnessDetail,
4533
- suggestion: daysOld !== null && freshnessPoints < 4 ? `${primaryName} is ${daysOld} days old \u2014 run \`caliber refresh\` to update it` : void 0
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 = readFileOrNull5(join6(dir, rel));
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
- secretFindings.push({
4553
- file: rel,
4554
- line: i + 1,
4555
- pattern: pattern.source.slice(0, 20) + "..."
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 = readFileOrNull5(settingsPath);
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 existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
4606
- import { execSync as execSync8 } from "child_process";
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 = execSync8("git rev-parse --git-dir", { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
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 = readFileOrNull6(hookPath);
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 = readFileOrNull6(join7(dir, ".claude", "settings.json"));
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: hookDetail,
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 = existsSync7(join7(dir, "AGENTS.md"));
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 (60k+ repos)"
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 = readdirSync4(skillsDir, { withFileTypes: true });
4807
+ const entries = readdirSync3(skillsDir, { withFileTypes: true });
4674
4808
  for (const entry of entries) {
4675
4809
  if (entry.isDirectory()) {
4676
- const skillMd = readFileOrNull6(join7(skillsDir, entry.name, "SKILL.md"));
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 (SkillsBench: +16.2pp improvement)" : void 0
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 (existsSync8(join8(dir, "CLAUDE.md")) || existsSync8(join8(dir, ".claude", "skills"))) agents.push("claude");
4746
- if (existsSync8(join8(dir, ".cursorrules")) || existsSync8(join8(dir, ".cursor", "rules"))) agents.push("cursor");
4747
- if (existsSync8(join8(dir, ".codex")) || existsSync8(join8(dir, ".agents", "skills"))) agents.push("codex");
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
- ...checkCoverage(dir),
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
- coverage: sumCategory(checks, "coverage"),
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
- coverage: "COVERAGE",
4933
+ grounding: "GROUNDING",
4794
4934
  accuracy: "ACCURACY",
4795
4935
  freshness: "FRESHNESS & SAFETY",
4796
4936
  bonus: "BONUS"
4797
4937
  };
4798
- var CATEGORY_ORDER = ["existence", "quality", "coverage", "accuracy", "freshness", "bonus"];
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 readFileSync8, readdirSync as readdirSync5, existsSync as existsSync9, writeFileSync } from "fs";
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 execSync9 } from "child_process";
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 = execSync9("git config user.email", { encoding: "utf-8" }).trim();
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 = readdirSync5(dir, { withFileTypes: true });
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 (!existsSync9(pkgPath)) return [];
5552
+ if (!existsSync7(pkgPath)) return [];
5413
5553
  try {
5414
- const pkg3 = JSON.parse(readFileSync8(pkgPath, "utf-8"));
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 || !setup.targetAgent.includes("codex")) {
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 execSync10 } from "child_process";
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 execSync10(cmd, {
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 execSync11 } from "child_process";
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 = execSync11("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
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
- execSync11(`npm install -g @rely-ai/caliber@${latest}`, {
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
- execSync11(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
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
  });