@rely-ai/caliber 1.38.0 → 1.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +333 -96
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -1309,6 +1309,19 @@ var REFRESH_LAST_ERROR_FILE = path2.join(CALIBER_DIR, "last-refresh-error.json")
1309
1309
  var MIN_SESSIONS_FOR_COMPARISON = 3;
1310
1310
 
1311
1311
  // src/fingerprint/existing-config.ts
1312
+ var CALIBER_MANAGED_PREFIX = "caliber-";
1313
+ var INCLUDABLE_DOC_PATTERNS = [
1314
+ "ARCHITECTURE.md",
1315
+ "CONTRIBUTING.md",
1316
+ "DEVELOPMENT.md",
1317
+ "SETUP.md",
1318
+ "docs/ARCHITECTURE.md",
1319
+ "docs/CONTRIBUTING.md",
1320
+ "docs/DEVELOPMENT.md",
1321
+ "docs/API.md",
1322
+ "docs/GUIDE.md",
1323
+ "docs/SETUP.md"
1324
+ ];
1312
1325
  function readSkillsFromDir(skillsDir) {
1313
1326
  if (!fs2.existsSync(skillsDir)) return void 0;
1314
1327
  try {
@@ -1371,6 +1384,18 @@ function readExistingConfigs(dir) {
1371
1384
  } catch {
1372
1385
  }
1373
1386
  }
1387
+ const claudeRulesDir = path3.join(dir, ".claude", "rules");
1388
+ if (fs2.existsSync(claudeRulesDir)) {
1389
+ try {
1390
+ const files = fs2.readdirSync(claudeRulesDir).filter((f) => f.endsWith(".md"));
1391
+ const rules = files.map((f) => ({
1392
+ filename: f,
1393
+ content: fs2.readFileSync(path3.join(claudeRulesDir, f), "utf-8")
1394
+ }));
1395
+ if (rules.length > 0) configs.claudeRules = rules;
1396
+ } catch {
1397
+ }
1398
+ }
1374
1399
  const cursorrulesPath = path3.join(dir, ".cursorrules");
1375
1400
  if (fs2.existsSync(cursorrulesPath)) {
1376
1401
  configs.cursorrules = fs2.readFileSync(cursorrulesPath, "utf-8");
@@ -1434,6 +1459,8 @@ function readExistingConfigs(dir) {
1434
1459
  } catch {
1435
1460
  }
1436
1461
  }
1462
+ const found = INCLUDABLE_DOC_PATTERNS.filter((p) => fs2.existsSync(path3.join(dir, p)));
1463
+ if (found.length > 0) configs.includableDocs = found;
1437
1464
  return configs;
1438
1465
  }
1439
1466
 
@@ -2708,15 +2735,16 @@ var CLAUDE_CLI_BIN = "claude";
2708
2735
  var DEFAULT_TIMEOUT_MS2 = 10 * 60 * 1e3;
2709
2736
  var IS_WINDOWS2 = process.platform === "win32";
2710
2737
  function spawnClaude(args) {
2738
+ const env = { ...process.env, CLAUDE_CODE_SIMPLE: "1" };
2711
2739
  return IS_WINDOWS2 ? spawn2([CLAUDE_CLI_BIN, ...args].join(" "), {
2712
2740
  cwd: process.cwd(),
2713
2741
  stdio: ["pipe", "pipe", "pipe"],
2714
- env: process.env,
2742
+ env,
2715
2743
  shell: true
2716
2744
  }) : spawn2(CLAUDE_CLI_BIN, args, {
2717
2745
  cwd: process.cwd(),
2718
2746
  stdio: ["pipe", "pipe", "pipe"],
2719
- env: process.env
2747
+ env
2720
2748
  });
2721
2749
  }
2722
2750
  var ClaudeCliProvider = class {
@@ -3113,6 +3141,7 @@ var CONFIG_FILE_TYPES = `You understand these config files:
3113
3141
  - .agents/skills/{name}/SKILL.md: Same OpenSkills format for Codex skills (Codex scans .agents/skills/ for skills).
3114
3142
  - .opencode/skills/{name}/SKILL.md: Same OpenSkills format for OpenCode skills (OpenCode scans .opencode/skills/ for skills).
3115
3143
  - .cursor/skills/{name}/SKILL.md: Same OpenSkills format for Cursor skills.
3144
+ - .claude/rules/*.md: Path-scoped rules for Claude Code with YAML frontmatter. Each rule file contains a \`paths:\` field with glob patterns \u2014 Claude Code only loads the rule when the user works on matching files. Use for domain-specific conventions (e.g., API patterns, test conventions, database rules). Always-apply rules omit the \`paths:\` field.
3116
3145
  - .cursorrules: Coding rules for Cursor (deprecated legacy format \u2014 do NOT generate this).
3117
3146
  - .cursor/rules/*.mdc: Modern Cursor rules with frontmatter (description, globs, alwaysApply).
3118
3147
  - .github/copilot-instructions.md: Always-on repository-wide instructions for GitHub Copilot \u2014 same purpose as CLAUDE.md but for Copilot. Plain markdown, no frontmatter.
@@ -3152,6 +3181,7 @@ Skill field requirements:
3152
3181
  - "name" MUST NOT be any of these reserved names (they are managed by Caliber automatically): "setup-caliber", "find-skills", "save-learning". Do NOT generate skills with these names.
3153
3182
  - "description": MUST include WHAT it does + WHEN to use it with specific trigger phrases. Example: "Manages database migrations. Use when user says 'run migration', 'create migration', 'db schema change', or modifies files in db/migrations/."
3154
3183
  - "content": markdown body only \u2014 do NOT include YAML frontmatter, it is generated from name+description.
3184
+ - "paths" (optional): array of glob patterns. When provided, the skill is only loaded when the user works on matching files. Use for skills tied to specific file types or directories. Example: ["src/api/**", "src/routes/**"] for an API skill, or ["Dockerfile*", "docker-compose*"] for a Docker skill. Omit for general-purpose skills.
3155
3185
 
3156
3186
  Skill content structure \u2014 follow this template:
3157
3187
  1. A heading with the skill name
@@ -3160,6 +3190,31 @@ Skill content structure \u2014 follow this template:
3160
3190
  4. "## Troubleshooting" (optional) \u2014 common errors and how to fix them
3161
3191
 
3162
3192
  Keep skill content under 200 lines. Focus on actionable instructions, not documentation prose.`;
3193
+ var CLAUDE_RULES_FORMAT = `CLAUDE RULES FORMAT \u2014 .claude/rules/*.md files:
3194
+ Claude Code loads .claude/rules/*.md files as path-scoped instructions. Generate 2-4 rules that extract domain-specific conventions from CLAUDE.md into focused, contextually-loaded files.
3195
+
3196
+ Each rule file uses YAML frontmatter with an optional \`paths:\` field:
3197
+ \`\`\`markdown
3198
+ ---
3199
+ paths:
3200
+ - src/api/**
3201
+ - src/routes/**
3202
+ ---
3203
+
3204
+ # API Conventions
3205
+
3206
+ - All endpoints return \`{ data, error }\` envelope
3207
+ - Use \`validateRequest(schema)\` middleware before handlers
3208
+ \`\`\`
3209
+
3210
+ Rules without \`paths:\` apply globally (use sparingly \u2014 prefer path-scoped rules).
3211
+
3212
+ Guidelines:
3213
+ - Extract patterns from the codebase that apply to specific file types or directories
3214
+ - Keep each rule under 50 lines \u2014 these are loaded into context, conciseness matters
3215
+ - Good candidates: testing patterns, API conventions, database query patterns, component structure
3216
+ - Do NOT duplicate content from CLAUDE.md \u2014 rules supplement it with path-specific detail
3217
+ - filename must be kebab-case ending in .md (e.g. \`testing-patterns.md\`, \`api-conventions.md\`)`;
3163
3218
  var SCORING_CRITERIA = `SCORING CRITERIA \u2014 your output is scored deterministically against the actual filesystem. Optimize for 100/100:
3164
3219
 
3165
3220
  Existence (25 pts):
@@ -3202,7 +3257,13 @@ For command sections, use code blocks with one command per line.
3202
3257
 
3203
3258
  - Each skill content: max 150 lines. Focus on patterns and examples, not exhaustive docs.
3204
3259
  - Cursor rules: max 5 .mdc files.
3205
- - If the project is large, prioritize depth on the 3-4 most critical tools over breadth across everything.`;
3260
+ - If the project is large, prioritize depth on the 3-4 most critical tools over breadth across everything.
3261
+
3262
+ @include directives (Claude Code only):
3263
+ - If the project has existing documentation files (ARCHITECTURE.md, CONTRIBUTING.md, API docs, etc.), reference them in CLAUDE.md using \`@./path\` instead of summarizing their content. Claude Code will inline them automatically.
3264
+ - Example: Instead of writing an architecture summary, add \`@./ARCHITECTURE.md\` in the relevant section.
3265
+ - This keeps CLAUDE.md compact while giving the agent access to detailed docs.
3266
+ - Only use @include for files that actually exist in the provided file tree.`;
3206
3267
  var GENERATION_SYSTEM_PROMPT = `${ROLE_AND_CONTEXT}
3207
3268
 
3208
3269
  ${CONFIG_FILE_TYPES}
@@ -3224,18 +3285,19 @@ AgentSetup schema:
3224
3285
  ],
3225
3286
  "claude": {
3226
3287
  "claudeMd": "string (markdown content for CLAUDE.md)",
3227
- "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)" }]
3288
+ "rules": [{ "filename": "string.md (kebab-case, e.g. api-conventions.md)", "content": "string (markdown with YAML frontmatter containing paths: glob)" }],
3289
+ "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)", "paths": ["optional array of glob patterns \u2014 omit for general-purpose skills"] }]
3228
3290
  },
3229
3291
  "codex": {
3230
3292
  "agentsMd": "string (markdown content for AGENTS.md \u2014 the primary Codex instructions file, same quality/structure as CLAUDE.md)",
3231
- "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)" }]
3293
+ "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)", "paths": ["optional array of glob patterns \u2014 omit for general-purpose skills"] }]
3232
3294
  },
3233
3295
  "opencode": {
3234
3296
  "agentsMd": "string (markdown content for AGENTS.md \u2014 reuse codex.agentsMd if codex is also targeted, otherwise generate fresh)",
3235
- "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)" }]
3297
+ "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)", "paths": ["optional array of glob patterns \u2014 omit for general-purpose skills"] }]
3236
3298
  },
3237
3299
  "cursor": {
3238
- "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)" }],
3300
+ "skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)", "paths": ["optional array of glob patterns \u2014 omit for general-purpose skills"] }],
3239
3301
  "rules": [{ "filename": "string.mdc", "content": "string (with frontmatter)" }]
3240
3302
  },
3241
3303
  "copilot": {
@@ -3248,12 +3310,15 @@ NOTE: If both "codex" and "opencode" are targeted, set opencode.agentsMd to the
3248
3310
 
3249
3311
  ${SKILL_FORMAT_RULES}
3250
3312
 
3313
+ ${CLAUDE_RULES_FORMAT}
3314
+
3251
3315
  ${FILE_DESCRIPTIONS_RULES}
3252
3316
 
3253
3317
  ${SCORING_CRITERIA}
3254
3318
 
3255
3319
  ${OUTPUT_SIZE_CONSTRAINTS}
3256
- - Skills: generate 3-6 skills per target platform based on project complexity. Each skill should cover a distinct tool, workflow, or domain \u2014 don't pad with generic skills.`;
3320
+ - Skills: generate 3-6 skills per target platform based on project complexity. Each skill should cover a distinct tool, workflow, or domain \u2014 don't pad with generic skills.
3321
+ - Claude rules: generate 2-4 .claude/rules/*.md files when claude is targeted. Each rule should extract domain-specific patterns from the codebase.`;
3257
3322
  var CORE_GENERATION_PROMPT = `${ROLE_AND_CONTEXT}
3258
3323
 
3259
3324
  ${CONFIG_FILE_TYPES}
@@ -3275,6 +3340,7 @@ CoreSetup schema:
3275
3340
  ],
3276
3341
  "claude": {
3277
3342
  "claudeMd": "string (markdown content for CLAUDE.md)",
3343
+ "rules": [{ "filename": "string.md (kebab-case, e.g. api-conventions.md)", "content": "string (markdown with YAML frontmatter containing paths: glob)" }],
3278
3344
  "skillTopics": [{ "name": "string (kebab-case)", "description": "string (what this skill does and WHEN to use it \u2014 include trigger phrases)" }]
3279
3345
  },
3280
3346
  "codex": {
@@ -3308,12 +3374,15 @@ Skill topic description MUST follow this formula: [What it does] + [When to use
3308
3374
  Include specific trigger phrases users would actually say. Also include negative triggers to prevent over-triggering.
3309
3375
  Example: "Creates a new API endpoint following the project's route pattern. Handles request validation, error responses, and DB queries. Use when user says 'add endpoint', 'new route', 'create API', or adds files to src/routes/. Do NOT use for modifying existing routes."
3310
3376
 
3377
+ ${CLAUDE_RULES_FORMAT}
3378
+
3311
3379
  ${FILE_DESCRIPTIONS_RULES}
3312
3380
 
3313
3381
  ${SCORING_CRITERIA}
3314
3382
 
3315
3383
  ${OUTPUT_SIZE_CONSTRAINTS}
3316
- - Skill topics: 3-6 per platform based on project complexity (name + description only, no content).`;
3384
+ - Skill topics: 3-6 per platform based on project complexity (name + description only, no content).
3385
+ - Claude rules: generate 2-4 .claude/rules/*.md files when claude is targeted.`;
3317
3386
  var SKILL_GENERATION_PROMPT = `You generate a single skill file for a coding agent (Claude Code, Cursor, Codex, or OpenCode).
3318
3387
 
3319
3388
  Given project context and a skill topic, produce a focused SKILL.md body.
@@ -3343,7 +3412,7 @@ Rules:
3343
3412
  Description field formula: [What it does] + [When to use it with trigger phrases] + [Key capabilities]. Include negative triggers ("Do NOT use for X") to prevent over-triggering.
3344
3413
 
3345
3414
  Return ONLY a JSON object:
3346
- {"name": "string (kebab-case)", "description": "string (what + when + capabilities + negative triggers)", "content": "string (markdown body)"}`;
3415
+ {"name": "string (kebab-case)", "description": "string (what + when + capabilities + negative triggers)", "content": "string (markdown body)", "paths": ["optional glob patterns \u2014 include when the skill applies to specific file types or directories, omit for general-purpose skills"]}`;
3347
3416
  var REFINE_SYSTEM_PROMPT = `You are an expert at modifying coding agent configurations (Claude Code, Cursor, Codex, OpenCode, and GitHub Copilot).
3348
3417
 
3349
3418
  You will receive the current AgentSetup JSON and a user request describing what to change.
@@ -3400,7 +3469,7 @@ var REFRESH_SYSTEM_PROMPT = `You are an expert at maintaining coding project doc
3400
3469
 
3401
3470
  You will receive:
3402
3471
  1. Git diffs showing what code changed
3403
- 2. Current contents of documentation files (CLAUDE.md, README.md, skills, cursor rules, copilot instructions)
3472
+ 2. Current contents of documentation files (CLAUDE.md, .claude/rules/*.md, README.md, skills, cursor rules, copilot instructions)
3404
3473
  3. Project context (languages, frameworks, file tree)
3405
3474
 
3406
3475
  CONSERVATIVE UPDATE means:
@@ -3419,6 +3488,12 @@ Quality constraints (the output is scored deterministically):
3419
3488
  - ONLY reference file paths that exist in the provided file tree \u2014 do NOT invent paths
3420
3489
  - Preserve the existing structure (headings, bullet style, formatting)
3421
3490
 
3491
+ Claude rules (.claude/rules/*.md):
3492
+ - If the diff affects code in a domain covered by an existing rule, update that rule
3493
+ - If the diff introduces a new domain pattern (e.g., new API conventions, new test patterns), create a new rule file
3494
+ - Rules with paths: frontmatter should only contain conventions relevant to those paths
3495
+ - Keep rules under 50 lines \u2014 they load into context alongside other instructions
3496
+
3422
3497
  Cross-agent sync:
3423
3498
  - When a code change affects documentation, update ALL provided platform configs together.
3424
3499
  - A renamed command, moved file, or changed convention must be reflected in every config (CLAUDE.md, AGENTS.md, copilot instructions, skills across all platforms).
@@ -3435,6 +3510,7 @@ Return a JSON object with this exact shape:
3435
3510
  "updatedDocs": {
3436
3511
  "agentsMd": "<updated content or null>",
3437
3512
  "claudeMd": "<updated content or null>",
3513
+ "claudeRules": [{"filename": "name.md", "content": "full content with frontmatter"}] or null,
3438
3514
  "readmeMd": "<updated content or null>",
3439
3515
  "cursorrules": "<updated content or null>",
3440
3516
  "cursorRules": [{"filename": "name.mdc", "content": "..."}] or null,
@@ -4028,6 +4104,55 @@ function removeHook() {
4028
4104
  writeSettings(settings);
4029
4105
  return { removed: true, notFound: false };
4030
4106
  }
4107
+ function createScriptHook(config) {
4108
+ const { eventName, scriptPath, scriptContent, description } = config;
4109
+ const hasHook = (matchers) => matchers.some((entry) => entry.hooks?.some((h) => h.description === description));
4110
+ function isInstalled() {
4111
+ const settings = readSettings();
4112
+ const matchers = settings.hooks?.[eventName];
4113
+ return Array.isArray(matchers) && hasHook(matchers);
4114
+ }
4115
+ function install() {
4116
+ const settings = readSettings();
4117
+ if (!settings.hooks) settings.hooks = {};
4118
+ const matchers = settings.hooks[eventName];
4119
+ if (Array.isArray(matchers) && hasHook(matchers)) {
4120
+ return { installed: false, alreadyInstalled: true };
4121
+ }
4122
+ const scriptDir = path10.dirname(scriptPath);
4123
+ if (!fs10.existsSync(scriptDir)) fs10.mkdirSync(scriptDir, { recursive: true });
4124
+ fs10.writeFileSync(scriptPath, scriptContent);
4125
+ fs10.chmodSync(scriptPath, 493);
4126
+ if (!Array.isArray(settings.hooks[eventName])) {
4127
+ settings.hooks[eventName] = [];
4128
+ }
4129
+ settings.hooks[eventName].push({
4130
+ matcher: "",
4131
+ hooks: [{ type: "command", command: scriptPath, description }]
4132
+ });
4133
+ writeSettings(settings);
4134
+ return { installed: true, alreadyInstalled: false };
4135
+ }
4136
+ function remove() {
4137
+ const settings = readSettings();
4138
+ const matchers = settings.hooks?.[eventName];
4139
+ if (!Array.isArray(matchers)) return { removed: false, notFound: true };
4140
+ const idx = matchers.findIndex(
4141
+ (entry) => entry.hooks?.some((h) => h.description === description)
4142
+ );
4143
+ if (idx === -1) return { removed: false, notFound: true };
4144
+ matchers.splice(idx, 1);
4145
+ if (matchers.length === 0) delete settings.hooks[eventName];
4146
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
4147
+ writeSettings(settings);
4148
+ try {
4149
+ fs10.unlinkSync(scriptPath);
4150
+ } catch {
4151
+ }
4152
+ return { removed: true, notFound: false };
4153
+ }
4154
+ return { isInstalled, install, remove };
4155
+ }
4031
4156
  var STOP_HOOK_SCRIPT_CONTENT = `#!/bin/sh
4032
4157
  if grep -q "caliber" .git/hooks/pre-commit 2>/dev/null; then
4033
4158
  exit 0
@@ -4040,58 +4165,44 @@ fi
4040
4165
  touch "$FLAG"
4041
4166
  printf '{"decision":"block","reason":"Caliber agent config sync is not set up on this machine. Tell the user: This project uses Caliber to keep agent configs in sync \u2014 it is not set up yet. Want me to run /setup-caliber? (~30 seconds)"}'
4042
4167
  `;
4043
- var STOP_HOOK_SCRIPT_PATH = path10.join(".claude", "hooks", "caliber-check-sync.sh");
4044
- var STOP_HOOK_DESCRIPTION = "Caliber: offer setup if not configured";
4045
- function hasStopHook(matchers) {
4046
- return matchers.some(
4047
- (entry) => entry.hooks?.some((h) => h.description === STOP_HOOK_DESCRIPTION)
4048
- );
4049
- }
4050
- function installStopHook() {
4051
- const settings = readSettings();
4052
- if (!settings.hooks) settings.hooks = {};
4053
- const stop = settings.hooks.Stop;
4054
- if (Array.isArray(stop) && hasStopHook(stop)) {
4055
- return { installed: false, alreadyInstalled: true };
4056
- }
4057
- const scriptDir = path10.dirname(STOP_HOOK_SCRIPT_PATH);
4058
- if (!fs10.existsSync(scriptDir)) fs10.mkdirSync(scriptDir, { recursive: true });
4059
- fs10.writeFileSync(STOP_HOOK_SCRIPT_PATH, STOP_HOOK_SCRIPT_CONTENT);
4060
- fs10.chmodSync(STOP_HOOK_SCRIPT_PATH, 493);
4061
- if (!Array.isArray(settings.hooks.Stop)) {
4062
- settings.hooks.Stop = [];
4063
- }
4064
- settings.hooks.Stop.push({
4065
- matcher: "",
4066
- hooks: [
4067
- {
4068
- type: "command",
4069
- command: STOP_HOOK_SCRIPT_PATH,
4070
- description: STOP_HOOK_DESCRIPTION
4071
- }
4072
- ]
4073
- });
4074
- writeSettings(settings);
4075
- return { installed: true, alreadyInstalled: false };
4076
- }
4077
- function removeStopHook() {
4078
- const settings = readSettings();
4079
- const stop = settings.hooks?.Stop;
4080
- if (!Array.isArray(stop)) return { removed: false, notFound: true };
4081
- const idx = stop.findIndex(
4082
- (entry) => entry.hooks?.some((h) => h.description === STOP_HOOK_DESCRIPTION)
4083
- );
4084
- if (idx === -1) return { removed: false, notFound: true };
4085
- stop.splice(idx, 1);
4086
- if (stop.length === 0) delete settings.hooks.Stop;
4087
- if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
4088
- writeSettings(settings);
4089
- try {
4090
- fs10.unlinkSync(STOP_HOOK_SCRIPT_PATH);
4091
- } catch {
4092
- }
4093
- return { removed: true, notFound: false };
4094
- }
4168
+ var stopHook = createScriptHook({
4169
+ eventName: "Stop",
4170
+ scriptPath: path10.join(".claude", "hooks", "caliber-check-sync.sh"),
4171
+ scriptContent: STOP_HOOK_SCRIPT_CONTENT,
4172
+ description: "Caliber: offer setup if not configured"
4173
+ });
4174
+ var installStopHook = stopHook.install;
4175
+ var removeStopHook = stopHook.remove;
4176
+ var FRESHNESS_SCRIPT = `#!/bin/sh
4177
+ STATE_FILE=".caliber/.caliber-state.json"
4178
+ [ ! -f "$STATE_FILE" ] && exit 0
4179
+ LAST_SHA=$(grep -o '"lastRefreshSha":"[^"]*"' "$STATE_FILE" 2>/dev/null | cut -d'"' -f4)
4180
+ [ -z "$LAST_SHA" ] && exit 0
4181
+ CURRENT_SHA=$(git rev-parse HEAD 2>/dev/null)
4182
+ [ "$LAST_SHA" = "$CURRENT_SHA" ] && exit 0
4183
+ COMMITS_BEHIND=$(git rev-list --count "$LAST_SHA".."$CURRENT_SHA" 2>/dev/null || echo 0)
4184
+ if [ "$COMMITS_BEHIND" -gt 15 ]; then
4185
+ printf '{"systemMessage":"Caliber: agent configs are %s commits behind. Run caliber refresh to sync."}' "$COMMITS_BEHIND"
4186
+ fi
4187
+ `;
4188
+ var sessionStartHook = createScriptHook({
4189
+ eventName: "SessionStart",
4190
+ scriptPath: path10.join(".claude", "hooks", "caliber-session-freshness.sh"),
4191
+ scriptContent: FRESHNESS_SCRIPT,
4192
+ description: "Caliber: check config freshness on session start"
4193
+ });
4194
+ var isSessionStartHookInstalled = sessionStartHook.isInstalled;
4195
+ var installSessionStartHook = sessionStartHook.install;
4196
+ var removeSessionStartHook = sessionStartHook.remove;
4197
+ var notificationHook = createScriptHook({
4198
+ eventName: "Notification",
4199
+ scriptPath: path10.join(".claude", "hooks", "caliber-freshness-notify.sh"),
4200
+ scriptContent: FRESHNESS_SCRIPT,
4201
+ description: "Caliber: warn when agent configs are stale"
4202
+ });
4203
+ var isNotificationHookInstalled = notificationHook.isInstalled;
4204
+ var installNotificationHook = notificationHook.install;
4205
+ var removeNotificationHook = notificationHook.remove;
4095
4206
  var PRECOMMIT_START = "# caliber:pre-commit:start";
4096
4207
  var PRECOMMIT_END = "# caliber:pre-commit:end";
4097
4208
  function getPrecommitBlock() {
@@ -4972,7 +5083,8 @@ Generate the skill content following the instructions in the system prompt.`;
4972
5083
  return {
4973
5084
  name: result.name || topic.name,
4974
5085
  description: result.description || topic.description,
4975
- content
5086
+ content,
5087
+ ...result.paths?.length ? { paths: result.paths } : {}
4976
5088
  };
4977
5089
  }
4978
5090
  async function streamGeneration(config) {
@@ -5188,7 +5300,7 @@ function sampleFileTree(fileTree, codeAnalysisPaths, limit) {
5188
5300
  function buildGeneratePrompt(fingerprint, targetAgent, prompt, failingChecks, currentScore, passingChecks) {
5189
5301
  const parts = [];
5190
5302
  const existing = fingerprint.existingConfigs;
5191
- const hasExistingConfigs = !!(existing.claudeMd || existing.claudeSettings || existing.claudeSkills?.length || existing.readmeMd || existing.agentsMd || existing.cursorrules || existing.cursorRules?.length);
5303
+ const hasExistingConfigs = !!(existing.claudeMd || existing.claudeSettings || existing.claudeSkills?.length || existing.claudeRules?.length || existing.readmeMd || existing.agentsMd || existing.cursorrules || existing.cursorRules?.length);
5192
5304
  const isTargetedFix = failingChecks && failingChecks.length > 0 && currentScore !== void 0 && currentScore >= 95;
5193
5305
  if (isTargetedFix) {
5194
5306
  parts.push(`TARGETED FIX MODE \u2014 current score: ${currentScore}/100, target: ${targetAgent}`);
@@ -5280,6 +5392,16 @@ ${truncate(skill.content, LIMITS.SKILL_CHARS)}`
5280
5392
  (${existing.claudeSkills.length - LIMITS.SKILLS_MAX} more skills omitted)`);
5281
5393
  }
5282
5394
  }
5395
+ if (existing.claudeRules?.length) {
5396
+ parts.push("\n--- Existing Claude Rules ---");
5397
+ for (const rule of existing.claudeRules.slice(0, LIMITS.RULES_MAX)) {
5398
+ parts.push(
5399
+ `
5400
+ [.claude/rules/${rule.filename}]
5401
+ ${truncate(rule.content, LIMITS.SKILL_CHARS)}`
5402
+ );
5403
+ }
5404
+ }
5283
5405
  if (existing.cursorrules)
5284
5406
  parts.push(
5285
5407
  `
@@ -5327,6 +5449,13 @@ ${existing.personalLearnings}`
5327
5449
  Project dependencies (${allDeps.length}):`);
5328
5450
  parts.push(allDeps.join(", "));
5329
5451
  }
5452
+ if (existing.includableDocs?.length) {
5453
+ parts.push("\n--- Existing Documentation Files (use @include) ---");
5454
+ parts.push("These files exist and can be referenced in CLAUDE.md using @./path:");
5455
+ for (const doc of existing.includableDocs) {
5456
+ parts.push(`- ${doc}`);
5457
+ }
5458
+ }
5330
5459
  if (prompt) parts.push(`
5331
5460
  User instructions: ${prompt}`);
5332
5461
  if (fingerprint.codeAnalysis) {
@@ -5390,18 +5519,29 @@ function writeClaudeConfig(config) {
5390
5519
  appendSyncBlock(appendLearningsBlock(appendPreCommitBlock(config.claudeMd)))
5391
5520
  );
5392
5521
  written.push("CLAUDE.md");
5522
+ if (config.rules?.length) {
5523
+ const rulesDir = path12.join(".claude", "rules");
5524
+ if (!fs12.existsSync(rulesDir)) fs12.mkdirSync(rulesDir, { recursive: true });
5525
+ for (const rule of config.rules) {
5526
+ const rulePath = path12.join(rulesDir, rule.filename);
5527
+ fs12.writeFileSync(rulePath, rule.content);
5528
+ written.push(rulePath);
5529
+ }
5530
+ }
5393
5531
  if (config.skills?.length) {
5394
5532
  for (const skill of config.skills) {
5395
5533
  const skillDir = path12.join(".claude", "skills", skill.name);
5396
5534
  if (!fs12.existsSync(skillDir)) fs12.mkdirSync(skillDir, { recursive: true });
5397
5535
  const skillPath = path12.join(skillDir, "SKILL.md");
5398
- const frontmatter = [
5399
- "---",
5400
- `name: ${skill.name}`,
5401
- `description: ${skill.description}`,
5402
- "---",
5403
- ""
5404
- ].join("\n");
5536
+ const frontmatterLines = ["---", `name: ${skill.name}`, `description: ${skill.description}`];
5537
+ if (skill.paths?.length) {
5538
+ frontmatterLines.push("paths:");
5539
+ for (const p of skill.paths) {
5540
+ frontmatterLines.push(` - ${p}`);
5541
+ }
5542
+ }
5543
+ frontmatterLines.push("---", "");
5544
+ const frontmatter = frontmatterLines.join("\n");
5405
5545
  fs12.writeFileSync(skillPath, frontmatter + skill.content);
5406
5546
  written.push(skillPath);
5407
5547
  }
@@ -5688,6 +5828,9 @@ function getFilesToWrite(setup) {
5688
5828
  if (setup.targetAgent.includes("claude") && setup.claude) {
5689
5829
  files.push("CLAUDE.md");
5690
5830
  if (setup.claude.mcpServers) files.push(".mcp.json");
5831
+ if (setup.claude.rules) {
5832
+ for (const r of setup.claude.rules) files.push(`.claude/rules/${r.filename}`);
5833
+ }
5691
5834
  if (setup.claude.skills) {
5692
5835
  for (const s of setup.claude.skills) {
5693
5836
  files.push(`.claude/skills/${s.name.replace(/[^a-z0-9-]/gi, "-").toLowerCase()}/SKILL.md`);
@@ -6230,6 +6373,7 @@ var POINTS_SKILLS_BONUS_PER_EXTRA = 1;
6230
6373
  var POINTS_SKILLS_BONUS_CAP = 2;
6231
6374
  var POINTS_CURSOR_MDC_RULES = 3;
6232
6375
  var POINTS_MCP_SERVERS = 3;
6376
+ var POINTS_CLAUDE_RULES = 3;
6233
6377
  var POINTS_CROSS_PLATFORM_PARITY = 2;
6234
6378
  var POINTS_EXECUTABLE_CONTENT = 8;
6235
6379
  var POINTS_CONCISE_CONFIG = 6;
@@ -6299,7 +6443,11 @@ var SECRET_PLACEHOLDER_PATTERNS = [
6299
6443
  /<[^>]+>/
6300
6444
  ];
6301
6445
  var CURSOR_ONLY_CHECKS = /* @__PURE__ */ new Set(["cursor_rules_exist", "cursor_mdc_rules"]);
6302
- var CLAUDE_ONLY_CHECKS = /* @__PURE__ */ new Set(["claude_md_exists", "claude_md_freshness"]);
6446
+ var CLAUDE_ONLY_CHECKS = /* @__PURE__ */ new Set([
6447
+ "claude_md_exists",
6448
+ "claude_md_freshness",
6449
+ "claude_rules_exist"
6450
+ ]);
6303
6451
  var BOTH_ONLY_CHECKS = /* @__PURE__ */ new Set(["cross_platform_parity", "no_duplicate_content"]);
6304
6452
  var CODEX_ONLY_CHECKS = /* @__PURE__ */ new Set(["codex_agents_md_exists"]);
6305
6453
  var COPILOT_ONLY_CHECKS = /* @__PURE__ */ new Set(["copilot_instructions_exists"]);
@@ -6368,6 +6516,32 @@ function checkExistence(dir) {
6368
6516
  instruction: "Create CLAUDE.md with project context, commands, architecture, and conventions."
6369
6517
  }
6370
6518
  });
6519
+ const claudeRulesDir = join3(dir, ".claude", "rules");
6520
+ let claudeRuleFiles = [];
6521
+ if (existsSync3(claudeRulesDir)) {
6522
+ try {
6523
+ claudeRuleFiles = readdirSync2(claudeRulesDir).filter(
6524
+ (f) => f.endsWith(".md") && !f.startsWith(CALIBER_MANAGED_PREFIX)
6525
+ );
6526
+ } catch {
6527
+ }
6528
+ }
6529
+ const hasClaudeRules = claudeRuleFiles.length > 0;
6530
+ checks.push({
6531
+ id: "claude_rules_exist",
6532
+ name: "Claude rules exist (.claude/rules/)",
6533
+ category: "existence",
6534
+ maxPoints: POINTS_CLAUDE_RULES,
6535
+ earnedPoints: hasClaudeRules ? POINTS_CLAUDE_RULES : 0,
6536
+ passed: hasClaudeRules,
6537
+ detail: hasClaudeRules ? `${claudeRuleFiles.length} rule${claudeRuleFiles.length === 1 ? "" : "s"} found` : "No .claude/rules/*.md files",
6538
+ suggestion: hasClaudeRules ? void 0 : "Add .claude/rules/*.md with path-scoped conventions for better context efficiency",
6539
+ fix: hasClaudeRules ? void 0 : {
6540
+ action: "create_file",
6541
+ data: { file: ".claude/rules/" },
6542
+ instruction: "Create .claude/rules/ with path-scoped markdown rules (e.g., testing-patterns.md, api-conventions.md)."
6543
+ }
6544
+ });
6371
6545
  const hasCursorrules = existsSync3(join3(dir, ".cursorrules"));
6372
6546
  const cursorRulesDir = existsSync3(join3(dir, ".cursor", "rules"));
6373
6547
  const cursorRulesExist = hasCursorrules || cursorRulesDir;
@@ -10056,6 +10230,8 @@ async function initCommand(options) {
10056
10230
  }
10057
10231
  installStopHook();
10058
10232
  console.log(` ${chalk14.green("\u2713")} Onboarding hook \u2014 nudges new team members to set up`);
10233
+ installSessionStartHook();
10234
+ console.log(` ${chalk14.green("\u2713")} Freshness hook \u2014 warns when configs are stale`);
10059
10235
  const { ensureBuiltinSkills: ensureBuiltinSkills2 } = await Promise.resolve().then(() => (init_builtin_skills(), builtin_skills_exports));
10060
10236
  for (const agent of targetAgent) {
10061
10237
  if (agent === "claude" && !fs34.existsSync(".claude"))
@@ -11169,6 +11345,14 @@ function scopeDiffToDir(diff, dir, allConfigDirs) {
11169
11345
  init_pre_commit_block();
11170
11346
  import fs37 from "fs";
11171
11347
  import path30 from "path";
11348
+ function writeFileGroup(groupDir, files) {
11349
+ fs37.mkdirSync(groupDir, { recursive: true });
11350
+ return files.map((file) => {
11351
+ const filePath = path30.join(groupDir, file.filename);
11352
+ fs37.writeFileSync(filePath, file.content);
11353
+ return filePath.replace(/\\/g, "/");
11354
+ });
11355
+ }
11172
11356
  function writeRefreshDocs(docs, dir = ".") {
11173
11357
  const written = [];
11174
11358
  const p = (relPath) => (dir === "." ? relPath : path30.join(dir, relPath)).replace(/\\/g, "/");
@@ -11188,6 +11372,9 @@ function writeRefreshDocs(docs, dir = ".") {
11188
11372
  fs37.writeFileSync(filePath, appendManagedBlocks(docs.claudeMd));
11189
11373
  written.push(filePath);
11190
11374
  }
11375
+ if (docs.claudeRules) {
11376
+ written.push(...writeFileGroup(p(path30.join(".claude", "rules")), docs.claudeRules));
11377
+ }
11191
11378
  if (docs.readmeMd) {
11192
11379
  const filePath = p("README.md");
11193
11380
  ensureParent(filePath);
@@ -11201,12 +11388,7 @@ function writeRefreshDocs(docs, dir = ".") {
11201
11388
  written.push(filePath);
11202
11389
  }
11203
11390
  if (docs.cursorRules) {
11204
- const rulesDir = p(path30.join(".cursor", "rules"));
11205
- if (!fs37.existsSync(rulesDir)) fs37.mkdirSync(rulesDir, { recursive: true });
11206
- for (const rule of docs.cursorRules) {
11207
- fs37.writeFileSync(path30.join(rulesDir, rule.filename), rule.content);
11208
- written.push(p(path30.join(".cursor", "rules", rule.filename)));
11209
- }
11391
+ written.push(...writeFileGroup(p(path30.join(".cursor", "rules")), docs.cursorRules));
11210
11392
  }
11211
11393
  if (docs.copilotInstructions) {
11212
11394
  const filePath = p(path30.join(".github", "copilot-instructions.md"));
@@ -11215,12 +11397,9 @@ function writeRefreshDocs(docs, dir = ".") {
11215
11397
  written.push(filePath);
11216
11398
  }
11217
11399
  if (docs.copilotInstructionFiles) {
11218
- const instructionsDir = p(path30.join(".github", "instructions"));
11219
- if (!fs37.existsSync(instructionsDir)) fs37.mkdirSync(instructionsDir, { recursive: true });
11220
- for (const file of docs.copilotInstructionFiles) {
11221
- fs37.writeFileSync(path30.join(instructionsDir, file.filename), file.content);
11222
- written.push(p(path30.join(".github", "instructions", file.filename)));
11223
- }
11400
+ written.push(
11401
+ ...writeFileGroup(p(path30.join(".github", "instructions")), docs.copilotInstructionFiles)
11402
+ );
11224
11403
  }
11225
11404
  return written;
11226
11405
  }
@@ -11307,9 +11486,17 @@ Changed files: ${diff.changedFiles.join(", ")}`);
11307
11486
  parts.push(skill.content);
11308
11487
  }
11309
11488
  }
11489
+ if (existingDocs.claudeRules?.length) {
11490
+ for (const rule of existingDocs.claudeRules) {
11491
+ if (rule.filename.startsWith(CALIBER_MANAGED_PREFIX)) continue;
11492
+ parts.push(`
11493
+ [.claude/rules/${rule.filename}]`);
11494
+ parts.push(rule.content);
11495
+ }
11496
+ }
11310
11497
  if (existingDocs.cursorRules?.length) {
11311
11498
  for (const rule of existingDocs.cursorRules) {
11312
- if (rule.filename.startsWith("caliber-")) continue;
11499
+ if (rule.filename.startsWith(CALIBER_MANAGED_PREFIX)) continue;
11313
11500
  parts.push(`
11314
11501
  [.cursor/rules/${rule.filename}]`);
11315
11502
  parts.push(rule.content);
@@ -11326,6 +11513,16 @@ Changed files: ${diff.changedFiles.join(", ")}`);
11326
11513
  parts.push(file.content);
11327
11514
  }
11328
11515
  }
11516
+ if (existingDocs.includableDocs?.length) {
11517
+ parts.push(`
11518
+ --- Existing Documentation Files (use @include) ---`);
11519
+ parts.push(
11520
+ "These files exist in the project and can be referenced in CLAUDE.md using @./path:"
11521
+ );
11522
+ for (const doc of existingDocs.includableDocs) {
11523
+ parts.push(`- ${doc}`);
11524
+ }
11525
+ }
11329
11526
  if (learnedSection) {
11330
11527
  parts.push("\n--- Learned Patterns (from session learning) ---");
11331
11528
  parts.push("Consider these accumulated learnings when deciding what to update:");
@@ -11669,6 +11866,10 @@ function collectFilesToWrite(updatedDocs, dir = ".") {
11669
11866
  const p = (relPath) => (dir === "." ? relPath : path34.join(dir, relPath)).replace(/\\/g, "/");
11670
11867
  if (updatedDocs.agentsMd) files.push(p("AGENTS.md"));
11671
11868
  if (updatedDocs.claudeMd) files.push(p("CLAUDE.md"));
11869
+ if (Array.isArray(updatedDocs.claudeRules)) {
11870
+ for (const r of updatedDocs.claudeRules)
11871
+ files.push(p(`.claude/rules/${r.filename}`));
11872
+ }
11672
11873
  if (updatedDocs.readmeMd) files.push(p("README.md"));
11673
11874
  if (updatedDocs.cursorrules) files.push(p(".cursorrules"));
11674
11875
  if (Array.isArray(updatedDocs.cursorRules)) {
@@ -11964,6 +12165,22 @@ var HOOKS = [
11964
12165
  isInstalled: isPreCommitHookInstalled,
11965
12166
  install: installPreCommitHook,
11966
12167
  remove: removePreCommitHook
12168
+ },
12169
+ {
12170
+ id: "session-start",
12171
+ label: "Claude Code SessionStart",
12172
+ description: "Check config freshness when a session starts",
12173
+ isInstalled: isSessionStartHookInstalled,
12174
+ install: installSessionStartHook,
12175
+ remove: removeSessionStartHook
12176
+ },
12177
+ {
12178
+ id: "notification",
12179
+ label: "Claude Code Notification",
12180
+ description: "Warn when agent configs are stale (>15 commits behind)",
12181
+ isInstalled: isNotificationHookInstalled,
12182
+ install: installNotificationHook,
12183
+ remove: removeNotificationHook
11967
12184
  }
11968
12185
  ];
11969
12186
  function printStatus() {
@@ -11979,8 +12196,12 @@ function printStatus() {
11979
12196
  }
11980
12197
  async function hooksCommand(options) {
11981
12198
  if (!options.install && !options.remove) {
11982
- console.log(chalk20.dim("\n Note: caliber now adds refresh instructions directly to config files."));
11983
- console.log(chalk20.dim(" These hooks are available for non-agent workflows (manual commits).\n"));
12199
+ console.log(
12200
+ chalk20.dim("\n Note: caliber now adds refresh instructions directly to config files.")
12201
+ );
12202
+ console.log(
12203
+ chalk20.dim(" These hooks are available for non-agent workflows (manual commits).\n")
12204
+ );
11984
12205
  }
11985
12206
  if (options.install) {
11986
12207
  for (const hook of HOOKS) {
@@ -13611,12 +13832,13 @@ var MANAGED_DOC_FILES = [
13611
13832
  ];
13612
13833
  var SKILL_DIRS = PLATFORM_CONFIGS.map((c) => c.skillsDir);
13613
13834
  var CURSOR_RULES_DIR = path41.join(".cursor", "rules");
13614
- function removeCaliberCursorRules() {
13835
+ var CLAUDE_RULES_DIR = path41.join(".claude", "rules");
13836
+ function removeCaliberManagedFiles(dir, extension) {
13615
13837
  const removed = [];
13616
- if (!fs50.existsSync(CURSOR_RULES_DIR)) return removed;
13617
- for (const file of fs50.readdirSync(CURSOR_RULES_DIR)) {
13618
- if (file.startsWith("caliber-") && file.endsWith(".mdc")) {
13619
- const fullPath = path41.join(CURSOR_RULES_DIR, file);
13838
+ if (!fs50.existsSync(dir)) return removed;
13839
+ for (const file of fs50.readdirSync(dir)) {
13840
+ if (file.startsWith(CALIBER_MANAGED_PREFIX) && file.endsWith(extension)) {
13841
+ const fullPath = path41.join(dir, file);
13620
13842
  fs50.unlinkSync(fullPath);
13621
13843
  removed.push(fullPath);
13622
13844
  }
@@ -13689,6 +13911,16 @@ async function uninstallCommand(options) {
13689
13911
  console.log(` ${chalk28.red("\u2717")} Onboarding hook removed`);
13690
13912
  actions.push("onboarding hook");
13691
13913
  }
13914
+ const notificationHookResult = removeNotificationHook();
13915
+ if (notificationHookResult.removed) {
13916
+ console.log(` ${chalk28.red("\u2717")} Notification hook removed`);
13917
+ actions.push("notification hook");
13918
+ }
13919
+ const sessionStartResult = removeSessionStartHook();
13920
+ if (sessionStartResult.removed) {
13921
+ console.log(` ${chalk28.red("\u2717")} SessionStart hook removed`);
13922
+ actions.push("session-start hook");
13923
+ }
13692
13924
  const learnResult = removeLearningHooks();
13693
13925
  if (learnResult.removed) {
13694
13926
  console.log(` ${chalk28.red("\u2717")} Claude Code learning hooks removed`);
@@ -13704,11 +13936,16 @@ async function uninstallCommand(options) {
13704
13936
  console.log(` ${chalk28.yellow("~")} ${file} \u2014 managed blocks removed`);
13705
13937
  actions.push(file);
13706
13938
  }
13707
- const removedRules = removeCaliberCursorRules();
13708
- for (const rule of removedRules) {
13939
+ const removedCursorRules = removeCaliberManagedFiles(CURSOR_RULES_DIR, ".mdc");
13940
+ for (const rule of removedCursorRules) {
13941
+ console.log(` ${chalk28.red("\u2717")} ${rule}`);
13942
+ }
13943
+ if (removedCursorRules.length > 0) actions.push("cursor rules");
13944
+ const removedClaudeRules = removeCaliberManagedFiles(CLAUDE_RULES_DIR, ".md");
13945
+ for (const rule of removedClaudeRules) {
13709
13946
  console.log(` ${chalk28.red("\u2717")} ${rule}`);
13710
13947
  }
13711
- if (removedRules.length > 0) actions.push("cursor rules");
13948
+ if (removedClaudeRules.length > 0) actions.push("claude rules");
13712
13949
  const removedSkills = removeBuiltinSkills();
13713
13950
  for (const skill of removedSkills) {
13714
13951
  console.log(` ${chalk28.red("\u2717")} ${skill}/`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rely-ai/caliber",
3
- "version": "1.38.0",
3
+ "version": "1.40.0",
4
4
  "description": "AI context infrastructure for coding agents — keeps CLAUDE.md, Cursor rules, and skills in sync as your codebase evolves",
5
5
  "type": "module",
6
6
  "bin": {