@rely-ai/caliber 1.24.1-dev.1773872765 → 1.25.0-dev.1773915517

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 +1754 -888
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -20,6 +20,41 @@ var __copyProps = (to, from, except, desc) => {
20
20
  };
21
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
22
 
23
+ // src/constants.ts
24
+ var constants_exports = {};
25
+ __export(constants_exports, {
26
+ AUTH_DIR: () => AUTH_DIR,
27
+ BACKUPS_DIR: () => BACKUPS_DIR,
28
+ CALIBER_DIR: () => CALIBER_DIR,
29
+ LEARNING_DIR: () => LEARNING_DIR,
30
+ LEARNING_MAX_EVENTS: () => LEARNING_MAX_EVENTS,
31
+ LEARNING_ROI_FILE: () => LEARNING_ROI_FILE,
32
+ LEARNING_SESSION_FILE: () => LEARNING_SESSION_FILE,
33
+ LEARNING_STATE_FILE: () => LEARNING_STATE_FILE,
34
+ MANIFEST_FILE: () => MANIFEST_FILE,
35
+ MIN_SESSIONS_FOR_COMPARISON: () => MIN_SESSIONS_FOR_COMPARISON,
36
+ PERSONAL_LEARNINGS_FILE: () => PERSONAL_LEARNINGS_FILE
37
+ });
38
+ import path2 from "path";
39
+ import os from "os";
40
+ var AUTH_DIR, CALIBER_DIR, MANIFEST_FILE, BACKUPS_DIR, LEARNING_DIR, LEARNING_SESSION_FILE, LEARNING_STATE_FILE, LEARNING_MAX_EVENTS, LEARNING_ROI_FILE, PERSONAL_LEARNINGS_FILE, MIN_SESSIONS_FOR_COMPARISON;
41
+ var init_constants = __esm({
42
+ "src/constants.ts"() {
43
+ "use strict";
44
+ AUTH_DIR = path2.join(os.homedir(), ".caliber");
45
+ CALIBER_DIR = ".caliber";
46
+ MANIFEST_FILE = path2.join(CALIBER_DIR, "manifest.json");
47
+ BACKUPS_DIR = path2.join(CALIBER_DIR, "backups");
48
+ LEARNING_DIR = path2.join(CALIBER_DIR, "learning");
49
+ LEARNING_SESSION_FILE = "current-session.jsonl";
50
+ LEARNING_STATE_FILE = "state.json";
51
+ LEARNING_MAX_EVENTS = 500;
52
+ LEARNING_ROI_FILE = "roi-stats.json";
53
+ PERSONAL_LEARNINGS_FILE = path2.join(AUTH_DIR, "personal-learnings.md");
54
+ MIN_SESSIONS_FOR_COMPARISON = 3;
55
+ }
56
+ });
57
+
23
58
  // src/llm/config.ts
24
59
  var config_exports = {};
25
60
  __export(config_exports, {
@@ -36,8 +71,8 @@ __export(config_exports, {
36
71
  writeConfigFile: () => writeConfigFile
37
72
  });
38
73
  import fs4 from "fs";
39
- import path4 from "path";
40
- import os from "os";
74
+ import path5 from "path";
75
+ import os2 from "os";
41
76
  function getMaxPromptTokens() {
42
77
  const config = loadConfig();
43
78
  const model = process.env.CALIBER_MODEL || config?.model;
@@ -136,8 +171,8 @@ var CONFIG_DIR, CONFIG_FILE, DEFAULT_MODELS, MODEL_CONTEXT_WINDOWS, DEFAULT_CONT
136
171
  var init_config = __esm({
137
172
  "src/llm/config.ts"() {
138
173
  "use strict";
139
- CONFIG_DIR = path4.join(os.homedir(), ".caliber");
140
- CONFIG_FILE = path4.join(CONFIG_DIR, "config.json");
174
+ CONFIG_DIR = path5.join(os2.homedir(), ".caliber");
175
+ CONFIG_FILE = path5.join(CONFIG_DIR, "config.json");
141
176
  DEFAULT_MODELS = {
142
177
  anthropic: "claude-sonnet-4-6",
143
178
  vertex: "claude-sonnet-4-6",
@@ -185,44 +220,13 @@ var init_types = __esm({
185
220
  }
186
221
  });
187
222
 
188
- // src/constants.ts
189
- var constants_exports = {};
190
- __export(constants_exports, {
191
- AUTH_DIR: () => AUTH_DIR,
192
- BACKUPS_DIR: () => BACKUPS_DIR,
193
- CALIBER_DIR: () => CALIBER_DIR,
194
- LEARNING_DIR: () => LEARNING_DIR,
195
- LEARNING_MAX_EVENTS: () => LEARNING_MAX_EVENTS,
196
- LEARNING_ROI_FILE: () => LEARNING_ROI_FILE,
197
- LEARNING_SESSION_FILE: () => LEARNING_SESSION_FILE,
198
- LEARNING_STATE_FILE: () => LEARNING_STATE_FILE,
199
- MANIFEST_FILE: () => MANIFEST_FILE
200
- });
201
- import path10 from "path";
202
- import os3 from "os";
203
- var AUTH_DIR, CALIBER_DIR, MANIFEST_FILE, BACKUPS_DIR, LEARNING_DIR, LEARNING_SESSION_FILE, LEARNING_STATE_FILE, LEARNING_MAX_EVENTS, LEARNING_ROI_FILE;
204
- var init_constants = __esm({
205
- "src/constants.ts"() {
206
- "use strict";
207
- AUTH_DIR = path10.join(os3.homedir(), ".caliber");
208
- CALIBER_DIR = ".caliber";
209
- MANIFEST_FILE = path10.join(CALIBER_DIR, "manifest.json");
210
- BACKUPS_DIR = path10.join(CALIBER_DIR, "backups");
211
- LEARNING_DIR = path10.join(CALIBER_DIR, "learning");
212
- LEARNING_SESSION_FILE = "current-session.jsonl";
213
- LEARNING_STATE_FILE = "state.json";
214
- LEARNING_MAX_EVENTS = 500;
215
- LEARNING_ROI_FILE = "roi-stats.json";
216
- }
217
- });
218
-
219
223
  // src/lib/resolve-caliber.ts
220
224
  var resolve_caliber_exports = {};
221
225
  __export(resolve_caliber_exports, {
222
226
  isCaliberCommand: () => isCaliberCommand,
223
227
  resolveCaliber: () => resolveCaliber
224
228
  });
225
- import fs16 from "fs";
229
+ import fs18 from "fs";
226
230
  import { execSync as execSync6 } from "child_process";
227
231
  function resolveCaliber() {
228
232
  if (_resolved) return _resolved;
@@ -244,7 +248,7 @@ function resolveCaliber() {
244
248
  } catch {
245
249
  }
246
250
  const binPath = process.argv[1];
247
- if (binPath && fs16.existsSync(binPath)) {
251
+ if (binPath && fs18.existsSync(binPath)) {
248
252
  _resolved = binPath;
249
253
  return _resolved;
250
254
  }
@@ -268,13 +272,13 @@ var init_resolve_caliber = __esm({
268
272
 
269
273
  // src/utils/editor.ts
270
274
  import { execSync as execSync13, spawn as spawn3 } from "child_process";
271
- import fs24 from "fs";
272
- import path20 from "path";
275
+ import fs26 from "fs";
276
+ import path22 from "path";
273
277
  import os5 from "os";
274
278
  function getEmptyFilePath(proposedPath) {
275
- fs24.mkdirSync(DIFF_TEMP_DIR, { recursive: true });
276
- const tempPath = path20.join(DIFF_TEMP_DIR, path20.basename(proposedPath));
277
- fs24.writeFileSync(tempPath, "");
279
+ fs26.mkdirSync(DIFF_TEMP_DIR, { recursive: true });
280
+ const tempPath = path22.join(DIFF_TEMP_DIR, path22.basename(proposedPath));
281
+ fs26.writeFileSync(tempPath, "");
278
282
  return tempPath;
279
283
  }
280
284
  function commandExists(cmd) {
@@ -314,7 +318,7 @@ var init_editor = __esm({
314
318
  "src/utils/editor.ts"() {
315
319
  "use strict";
316
320
  IS_WINDOWS3 = process.platform === "win32";
317
- DIFF_TEMP_DIR = path20.join(os5.tmpdir(), "caliber-diff");
321
+ DIFF_TEMP_DIR = path22.join(os5.tmpdir(), "caliber-diff");
318
322
  }
319
323
  });
320
324
 
@@ -326,7 +330,7 @@ __export(review_exports, {
326
330
  promptWantsReview: () => promptWantsReview
327
331
  });
328
332
  import chalk10 from "chalk";
329
- import fs25 from "fs";
333
+ import fs27 from "fs";
330
334
  import select4 from "@inquirer/select";
331
335
  import { createTwoFilesPatch } from "diff";
332
336
  async function promptWantsReview() {
@@ -363,8 +367,8 @@ async function openReview(method, stagedFiles) {
363
367
  return;
364
368
  }
365
369
  const fileInfos = stagedFiles.map((file) => {
366
- const proposed = fs25.readFileSync(file.proposedPath, "utf-8");
367
- const current = file.currentPath ? fs25.readFileSync(file.currentPath, "utf-8") : "";
370
+ const proposed = fs27.readFileSync(file.proposedPath, "utf-8");
371
+ const current = file.currentPath ? fs27.readFileSync(file.currentPath, "utf-8") : "";
368
372
  const patch = createTwoFilesPatch(
369
373
  file.isNew ? "/dev/null" : file.relativePath,
370
374
  file.relativePath,
@@ -539,13 +543,13 @@ __export(lock_exports, {
539
543
  isCaliberRunning: () => isCaliberRunning,
540
544
  releaseLock: () => releaseLock
541
545
  });
542
- import fs34 from "fs";
543
- import path26 from "path";
546
+ import fs36 from "fs";
547
+ import path28 from "path";
544
548
  import os7 from "os";
545
549
  function isCaliberRunning() {
546
550
  try {
547
- if (!fs34.existsSync(LOCK_FILE)) return false;
548
- const raw = fs34.readFileSync(LOCK_FILE, "utf-8").trim();
551
+ if (!fs36.existsSync(LOCK_FILE)) return false;
552
+ const raw = fs36.readFileSync(LOCK_FILE, "utf-8").trim();
549
553
  const { pid, ts } = JSON.parse(raw);
550
554
  if (Date.now() - ts > STALE_MS) return false;
551
555
  try {
@@ -560,13 +564,13 @@ function isCaliberRunning() {
560
564
  }
561
565
  function acquireLock() {
562
566
  try {
563
- fs34.writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid, ts: Date.now() }));
567
+ fs36.writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid, ts: Date.now() }));
564
568
  } catch {
565
569
  }
566
570
  }
567
571
  function releaseLock() {
568
572
  try {
569
- if (fs34.existsSync(LOCK_FILE)) fs34.unlinkSync(LOCK_FILE);
573
+ if (fs36.existsSync(LOCK_FILE)) fs36.unlinkSync(LOCK_FILE);
570
574
  } catch {
571
575
  }
572
576
  }
@@ -574,25 +578,25 @@ var LOCK_FILE, STALE_MS;
574
578
  var init_lock = __esm({
575
579
  "src/lib/lock.ts"() {
576
580
  "use strict";
577
- LOCK_FILE = path26.join(os7.tmpdir(), ".caliber.lock");
581
+ LOCK_FILE = path28.join(os7.tmpdir(), ".caliber.lock");
578
582
  STALE_MS = 10 * 60 * 1e3;
579
583
  }
580
584
  });
581
585
 
582
586
  // src/cli.ts
583
587
  import { Command } from "commander";
584
- import fs41 from "fs";
585
- import path31 from "path";
588
+ import fs45 from "fs";
589
+ import path35 from "path";
586
590
  import { fileURLToPath } from "url";
587
591
 
588
592
  // src/commands/init.ts
589
- import path22 from "path";
593
+ import path24 from "path";
590
594
  import chalk14 from "chalk";
591
- import fs29 from "fs";
595
+ import fs31 from "fs";
592
596
 
593
597
  // src/fingerprint/index.ts
594
598
  import fs7 from "fs";
595
- import path6 from "path";
599
+ import path7 from "path";
596
600
 
597
601
  // src/fingerprint/git.ts
598
602
  import { execSync } from "child_process";
@@ -607,10 +611,14 @@ function isInsideGitRepo() {
607
611
  dir = parent;
608
612
  }
609
613
  }
610
- function getGitRemoteUrl() {
611
- if (!isInsideGitRepo()) return void 0;
614
+ function getGitRemoteUrl(cwd) {
615
+ if (!cwd && !isInsideGitRepo()) return void 0;
612
616
  try {
613
- return execSync("git remote get-url origin", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
617
+ return execSync("git remote get-url origin", {
618
+ encoding: "utf-8",
619
+ stdio: ["pipe", "pipe", "pipe"],
620
+ ...cwd ? { cwd, timeout: 3e3 } : {}
621
+ }).trim();
614
622
  } catch {
615
623
  return void 0;
616
624
  }
@@ -696,37 +704,38 @@ function scan(base, rel, depth, maxDepth, result) {
696
704
  }
697
705
 
698
706
  // src/fingerprint/existing-config.ts
707
+ init_constants();
699
708
  import fs2 from "fs";
700
- import path2 from "path";
709
+ import path3 from "path";
701
710
  function readExistingConfigs(dir) {
702
711
  const configs = {};
703
- const readmeMdPath = path2.join(dir, "README.md");
712
+ const readmeMdPath = path3.join(dir, "README.md");
704
713
  if (fs2.existsSync(readmeMdPath)) {
705
714
  configs.readmeMd = fs2.readFileSync(readmeMdPath, "utf-8");
706
715
  }
707
- const agentsMdPath = path2.join(dir, "AGENTS.md");
716
+ const agentsMdPath = path3.join(dir, "AGENTS.md");
708
717
  if (fs2.existsSync(agentsMdPath)) {
709
718
  configs.agentsMd = fs2.readFileSync(agentsMdPath, "utf-8");
710
719
  }
711
- const claudeMdPath = path2.join(dir, "CLAUDE.md");
720
+ const claudeMdPath = path3.join(dir, "CLAUDE.md");
712
721
  if (fs2.existsSync(claudeMdPath)) {
713
722
  configs.claudeMd = fs2.readFileSync(claudeMdPath, "utf-8");
714
723
  }
715
- const claudeSettingsPath = path2.join(dir, ".claude", "settings.json");
724
+ const claudeSettingsPath = path3.join(dir, ".claude", "settings.json");
716
725
  if (fs2.existsSync(claudeSettingsPath)) {
717
726
  try {
718
727
  configs.claudeSettings = JSON.parse(fs2.readFileSync(claudeSettingsPath, "utf-8"));
719
728
  } catch {
720
729
  }
721
730
  }
722
- const skillsDir = path2.join(dir, ".claude", "skills");
731
+ const skillsDir = path3.join(dir, ".claude", "skills");
723
732
  if (fs2.existsSync(skillsDir)) {
724
733
  try {
725
734
  const entries = fs2.readdirSync(skillsDir);
726
735
  const skills = [];
727
736
  for (const entry of entries) {
728
- const entryPath = path2.join(skillsDir, entry);
729
- const skillMdPath = path2.join(entryPath, "SKILL.md");
737
+ const entryPath = path3.join(skillsDir, entry);
738
+ const skillMdPath = path3.join(entryPath, "SKILL.md");
730
739
  if (fs2.statSync(entryPath).isDirectory() && fs2.existsSync(skillMdPath)) {
731
740
  skills.push({ filename: `${entry}/SKILL.md`, content: fs2.readFileSync(skillMdPath, "utf-8") });
732
741
  } else if (entry.endsWith(".md")) {
@@ -737,36 +746,36 @@ function readExistingConfigs(dir) {
737
746
  } catch {
738
747
  }
739
748
  }
740
- const cursorrulesPath = path2.join(dir, ".cursorrules");
749
+ const cursorrulesPath = path3.join(dir, ".cursorrules");
741
750
  if (fs2.existsSync(cursorrulesPath)) {
742
751
  configs.cursorrules = fs2.readFileSync(cursorrulesPath, "utf-8");
743
752
  }
744
- const cursorRulesDir = path2.join(dir, ".cursor", "rules");
753
+ const cursorRulesDir = path3.join(dir, ".cursor", "rules");
745
754
  if (fs2.existsSync(cursorRulesDir)) {
746
755
  try {
747
756
  const files = fs2.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"));
748
757
  configs.cursorRules = files.map((f) => ({
749
758
  filename: f,
750
- content: fs2.readFileSync(path2.join(cursorRulesDir, f), "utf-8")
759
+ content: fs2.readFileSync(path3.join(cursorRulesDir, f), "utf-8")
751
760
  }));
752
761
  } catch {
753
762
  }
754
763
  }
755
- const cursorSkillsDir = path2.join(dir, ".cursor", "skills");
764
+ const cursorSkillsDir = path3.join(dir, ".cursor", "skills");
756
765
  if (fs2.existsSync(cursorSkillsDir)) {
757
766
  try {
758
767
  const slugs = fs2.readdirSync(cursorSkillsDir).filter((f) => {
759
- return fs2.statSync(path2.join(cursorSkillsDir, f)).isDirectory();
768
+ return fs2.statSync(path3.join(cursorSkillsDir, f)).isDirectory();
760
769
  });
761
- configs.cursorSkills = slugs.filter((slug) => fs2.existsSync(path2.join(cursorSkillsDir, slug, "SKILL.md"))).map((name) => ({
770
+ configs.cursorSkills = slugs.filter((slug) => fs2.existsSync(path3.join(cursorSkillsDir, slug, "SKILL.md"))).map((name) => ({
762
771
  name,
763
772
  filename: "SKILL.md",
764
- content: fs2.readFileSync(path2.join(cursorSkillsDir, name, "SKILL.md"), "utf-8")
773
+ content: fs2.readFileSync(path3.join(cursorSkillsDir, name, "SKILL.md"), "utf-8")
765
774
  }));
766
775
  } catch {
767
776
  }
768
777
  }
769
- const mcpJsonPath = path2.join(dir, ".mcp.json");
778
+ const mcpJsonPath = path3.join(dir, ".mcp.json");
770
779
  if (fs2.existsSync(mcpJsonPath)) {
771
780
  try {
772
781
  const mcpJson = JSON.parse(fs2.readFileSync(mcpJsonPath, "utf-8"));
@@ -776,7 +785,7 @@ function readExistingConfigs(dir) {
776
785
  } catch {
777
786
  }
778
787
  }
779
- const cursorMcpPath = path2.join(dir, ".cursor", "mcp.json");
788
+ const cursorMcpPath = path3.join(dir, ".cursor", "mcp.json");
780
789
  if (fs2.existsSync(cursorMcpPath)) {
781
790
  try {
782
791
  const cursorMcpJson = JSON.parse(fs2.readFileSync(cursorMcpPath, "utf-8"));
@@ -786,12 +795,20 @@ function readExistingConfigs(dir) {
786
795
  } catch {
787
796
  }
788
797
  }
798
+ if (fs2.existsSync(PERSONAL_LEARNINGS_FILE)) {
799
+ try {
800
+ const content = fs2.readFileSync(PERSONAL_LEARNINGS_FILE, "utf-8");
801
+ const bullets = content.split("\n").filter((l) => l.startsWith("- ")).join("\n");
802
+ if (bullets) configs.personalLearnings = bullets;
803
+ } catch {
804
+ }
805
+ }
789
806
  return configs;
790
807
  }
791
808
 
792
809
  // src/fingerprint/code-analysis.ts
793
810
  import fs3 from "fs";
794
- import path3 from "path";
811
+ import path4 from "path";
795
812
  import { execSync as execSync2 } from "child_process";
796
813
  var IGNORE_DIRS2 = /* @__PURE__ */ new Set([
797
814
  "node_modules",
@@ -1096,18 +1113,18 @@ function extractSkeletonIndentBased(lines, ext) {
1096
1113
  }
1097
1114
  function extractImports(content, filePath) {
1098
1115
  const imports = [];
1099
- const dir = path3.dirname(filePath);
1116
+ const dir = path4.dirname(filePath);
1100
1117
  for (const line of content.split("\n")) {
1101
1118
  const trimmed = line.trim();
1102
1119
  const jsMatch = trimmed.match(/(?:from|require\()\s*['"]([^'"]+)['"]/);
1103
1120
  if (jsMatch && jsMatch[1].startsWith(".")) {
1104
- imports.push(path3.normalize(path3.join(dir, jsMatch[1])));
1121
+ imports.push(path4.normalize(path4.join(dir, jsMatch[1])));
1105
1122
  continue;
1106
1123
  }
1107
1124
  const pyMatch = trimmed.match(/^from\s+(\.[.\w]*)\s+import/);
1108
1125
  if (pyMatch) {
1109
1126
  const modulePath = pyMatch[1].replace(/\./g, "/");
1110
- imports.push(path3.normalize(path3.join(dir, modulePath)));
1127
+ imports.push(path4.normalize(path4.join(dir, modulePath)));
1111
1128
  continue;
1112
1129
  }
1113
1130
  const goMatch = trimmed.match(/^\s*"([^"]+)"/);
@@ -1120,7 +1137,7 @@ function extractImports(content, filePath) {
1120
1137
  function buildImportCounts(files) {
1121
1138
  const counts = /* @__PURE__ */ new Map();
1122
1139
  for (const [filePath, content] of files) {
1123
- const ext = path3.extname(filePath).toLowerCase();
1140
+ const ext = path4.extname(filePath).toLowerCase();
1124
1141
  if (!SOURCE_EXTENSIONS.has(ext)) continue;
1125
1142
  const imports = extractImports(content, filePath);
1126
1143
  for (const imp of imports) {
@@ -1154,7 +1171,7 @@ function getGitFrequency(dir) {
1154
1171
  function groupByDirectory(files) {
1155
1172
  const groups = /* @__PURE__ */ new Map();
1156
1173
  for (const f of files) {
1157
- const dir = path3.dirname(f.path);
1174
+ const dir = path4.dirname(f.path);
1158
1175
  const group = groups.get(dir) || [];
1159
1176
  group.push(f);
1160
1177
  groups.set(dir, group);
@@ -1175,14 +1192,14 @@ function analyzeCode(dir) {
1175
1192
  let totalChars = 0;
1176
1193
  for (const relPath of allPaths) {
1177
1194
  try {
1178
- totalChars += fs3.statSync(path3.join(dir, relPath)).size;
1195
+ totalChars += fs3.statSync(path4.join(dir, relPath)).size;
1179
1196
  } catch {
1180
1197
  }
1181
1198
  }
1182
1199
  const fileContents = /* @__PURE__ */ new Map();
1183
1200
  for (const relPath of allPaths) {
1184
1201
  try {
1185
- const content = fs3.readFileSync(path3.join(dir, relPath), "utf-8");
1202
+ const content = fs3.readFileSync(path4.join(dir, relPath), "utf-8");
1186
1203
  if (content.split("\n").length <= 500) fileContents.set(relPath, content);
1187
1204
  } catch {
1188
1205
  }
@@ -1192,7 +1209,7 @@ function analyzeCode(dir) {
1192
1209
  const scored = [];
1193
1210
  let compressedChars = 0;
1194
1211
  for (const [relPath, rawContent] of fileContents) {
1195
- const ext = path3.extname(relPath).toLowerCase();
1212
+ const ext = path4.extname(relPath).toLowerCase();
1196
1213
  const compressed = compressContent(rawContent, ext);
1197
1214
  const skeleton = extractSkeleton(compressed, ext);
1198
1215
  compressedChars += compressed.length;
@@ -1221,7 +1238,7 @@ function analyzeCode(dir) {
1221
1238
  }
1222
1239
  if (similar.length > 0) {
1223
1240
  dupGroups++;
1224
- const names = similar.map((f) => path3.basename(f.path));
1241
+ const names = similar.map((f) => path4.basename(f.path));
1225
1242
  const summary = `(${similar.length} similar file${similar.length === 1 ? "" : "s"} in ${dirPath}/: ${names.join(", ")})`;
1226
1243
  const summarySize = summary.length + 30;
1227
1244
  if (includedChars + summarySize <= CHAR_BUDGET) {
@@ -1258,7 +1275,7 @@ function analyzeCode(dir) {
1258
1275
  }
1259
1276
  function walkDir(base, rel, depth, maxDepth, files) {
1260
1277
  if (depth > maxDepth) return;
1261
- const fullPath = path3.join(base, rel);
1278
+ const fullPath = path4.join(base, rel);
1262
1279
  let entries;
1263
1280
  try {
1264
1281
  entries = fs3.readdirSync(fullPath, { withFileTypes: true });
@@ -1274,7 +1291,7 @@ function walkDir(base, rel, depth, maxDepth, files) {
1274
1291
  } else if (entry.isFile()) {
1275
1292
  if (SKIP_FILES.has(entry.name)) continue;
1276
1293
  if (SKIP_PATTERNS.some((p) => p.test(entry.name))) continue;
1277
- const ext = path3.extname(entry.name).toLowerCase();
1294
+ const ext = path4.extname(entry.name).toLowerCase();
1278
1295
  if (TEXT_EXTENSIONS.has(ext) || depth === 0 && !ext && !entry.name.startsWith(".")) {
1279
1296
  files.push(relPath);
1280
1297
  }
@@ -1282,7 +1299,7 @@ function walkDir(base, rel, depth, maxDepth, files) {
1282
1299
  }
1283
1300
  }
1284
1301
  function filePriority(filePath) {
1285
- const base = path3.basename(filePath);
1302
+ const base = path4.basename(filePath);
1286
1303
  const entryPoints = /* @__PURE__ */ new Set([
1287
1304
  "index.ts",
1288
1305
  "index.js",
@@ -1589,7 +1606,7 @@ var OpenAICompatProvider = class {
1589
1606
 
1590
1607
  // src/llm/cursor-acp.ts
1591
1608
  import { spawn, execSync as execSync3 } from "child_process";
1592
- import os2 from "os";
1609
+ import os3 from "os";
1593
1610
 
1594
1611
  // src/llm/seat-based-errors.ts
1595
1612
  var ERROR_PATTERNS = [
@@ -1663,7 +1680,7 @@ var CursorAcpProvider = class {
1663
1680
  });
1664
1681
  }
1665
1682
  buildArgs(model, streaming) {
1666
- const args = ["--print", "--trust", "--workspace", os2.tmpdir()];
1683
+ const args = ["--print", "--trust", "--workspace", os3.tmpdir()];
1667
1684
  if (model && model !== "auto" && model !== "default") {
1668
1685
  args.push("--model", model);
1669
1686
  }
@@ -2302,7 +2319,7 @@ async function validateModel(options) {
2302
2319
  init_config();
2303
2320
 
2304
2321
  // src/ai/prompts.ts
2305
- var ROLE_AND_CONTEXT = `You are an expert auditor for coding agent configurations (Claude Code, Cursor, and Codex).
2322
+ var ROLE_AND_CONTEXT = `You are an expert auditor for coding agent configurations (Claude Code, Cursor, Codex, and GitHub Copilot).
2306
2323
 
2307
2324
  Your job depends on context:
2308
2325
  - If no existing configs exist \u2192 generate an initial setup from scratch.
@@ -2314,7 +2331,9 @@ var CONFIG_FILE_TYPES = `You understand these config files:
2314
2331
  - .agents/skills/{name}/SKILL.md: Same OpenSkills format for Codex skills (Codex scans .agents/skills/ for skills).
2315
2332
  - .cursor/skills/{name}/SKILL.md: Same OpenSkills format for Cursor skills.
2316
2333
  - .cursorrules: Coding rules for Cursor (deprecated legacy format \u2014 do NOT generate this).
2317
- - .cursor/rules/*.mdc: Modern Cursor rules with frontmatter (description, globs, alwaysApply).`;
2334
+ - .cursor/rules/*.mdc: Modern Cursor rules with frontmatter (description, globs, alwaysApply).
2335
+ - .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.
2336
+ - .github/instructions/*.instructions.md: Path-specific instruction files for GitHub Copilot with YAML frontmatter containing an \`applyTo\` glob pattern (e.g. \`applyTo: "**/*.ts,**/*.tsx"\`). Only loaded when Copilot is working on matching files.`;
2318
2337
  var EXCLUSIONS = `Do NOT generate .claude/settings.json, .claude/settings.local.json, or mcpServers \u2014 those are managed separately.`;
2319
2338
  var OUTPUT_FORMAT = `Your output MUST follow this exact format (no markdown fences):
2320
2339
 
@@ -2356,6 +2375,7 @@ var SCORING_CRITERIA = `SCORING CRITERIA \u2014 your output is scored determinis
2356
2375
  Existence (25 pts):
2357
2376
  - CLAUDE.md exists (6 pts) \u2014 always generate for claude targets
2358
2377
  - AGENTS.md exists (6 pts) \u2014 always generate for codex target
2378
+ - copilot-instructions.md exists (6 pts) \u2014 always generate for github-copilot target
2359
2379
  - Skills configured (8 pts) \u2014 generate 3+ skills for full points
2360
2380
  - MCP servers referenced (3 pts) \u2014 mention detected MCP integrations in your config text
2361
2381
  - When cursor is targeted: Cursor rules exist (3+3 pts), cross-platform parity (2 pts)
@@ -2407,7 +2427,7 @@ ${OUTPUT_FORMAT}
2407
2427
 
2408
2428
  AgentSetup schema:
2409
2429
  {
2410
- "targetAgent": ["claude", "cursor", "codex"] (array of selected agents),
2430
+ "targetAgent": ["claude", "cursor", "codex", "github-copilot"] (array of selected agents),
2411
2431
  "fileDescriptions": {
2412
2432
  "<file-path>": "reason for this change (max 80 chars)"
2413
2433
  },
@@ -2425,6 +2445,10 @@ AgentSetup schema:
2425
2445
  "cursor": {
2426
2446
  "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)" }],
2427
2447
  "rules": [{ "filename": "string.mdc", "content": "string (with frontmatter)" }]
2448
+ },
2449
+ "copilot": {
2450
+ "instructions": "string (markdown content for .github/copilot-instructions.md \u2014 same quality/structure as CLAUDE.md)",
2451
+ "instructionFiles": [{ "filename": "string.instructions.md", "content": "string (with applyTo YAML frontmatter, e.g. ---\\napplyTo: \\"**/*.ts,**/*.tsx\\"\\n---\\n\\nInstructions here)" }]
2428
2452
  }
2429
2453
  }
2430
2454
 
@@ -2446,7 +2470,7 @@ ${OUTPUT_FORMAT}
2446
2470
 
2447
2471
  CoreSetup schema:
2448
2472
  {
2449
- "targetAgent": ["claude", "cursor", "codex"] (array of selected agents),
2473
+ "targetAgent": ["claude", "cursor", "codex", "github-copilot"] (array of selected agents),
2450
2474
  "fileDescriptions": {
2451
2475
  "<file-path>": "reason for this change (max 80 chars)"
2452
2476
  },
@@ -2464,6 +2488,10 @@ CoreSetup schema:
2464
2488
  "cursor": {
2465
2489
  "skillTopics": [{ "name": "string (kebab-case)", "description": "string" }],
2466
2490
  "rules": [{ "filename": "string.mdc", "content": "string (with frontmatter)" }]
2491
+ },
2492
+ "copilot": {
2493
+ "instructions": "string (markdown content for .github/copilot-instructions.md \u2014 same quality/structure as CLAUDE.md)",
2494
+ "instructionFiles": [{ "filename": "string.instructions.md", "content": "string (with applyTo YAML frontmatter)" }]
2467
2495
  }
2468
2496
  }
2469
2497
 
@@ -2515,7 +2543,7 @@ Description field formula: [What it does] + [When to use it with trigger phrases
2515
2543
 
2516
2544
  Return ONLY a JSON object:
2517
2545
  {"name": "string (kebab-case)", "description": "string (what + when + capabilities + negative triggers)", "content": "string (markdown body)"}`;
2518
- var REFINE_SYSTEM_PROMPT = `You are an expert at modifying coding agent configurations (Claude Code, Cursor, and Codex).
2546
+ var REFINE_SYSTEM_PROMPT = `You are an expert at modifying coding agent configurations (Claude Code, Cursor, Codex, and GitHub Copilot).
2519
2547
 
2520
2548
  You will receive the current AgentSetup JSON and a user request describing what to change.
2521
2549
 
@@ -2523,7 +2551,7 @@ Apply the requested changes to the setup and return the complete updated AgentSe
2523
2551
 
2524
2552
  AgentSetup schema:
2525
2553
  {
2526
- "targetAgent": ["claude", "cursor", "codex"] (array of selected agents),
2554
+ "targetAgent": ["claude", "cursor", "codex", "github-copilot"] (array of selected agents),
2527
2555
  "fileDescriptions": {
2528
2556
  "<file-path>": "reason for this change (max 80 chars)"
2529
2557
  },
@@ -2541,6 +2569,10 @@ AgentSetup schema:
2541
2569
  "cursor": {
2542
2570
  "skills": [{ "name": "string (kebab-case)", "description": "string", "content": "string (markdown body, no frontmatter)" }],
2543
2571
  "rules": [{ "filename": "string.mdc", "content": "string (with frontmatter)" }]
2572
+ },
2573
+ "copilot": {
2574
+ "instructions": "string (markdown content for .github/copilot-instructions.md)",
2575
+ "instructionFiles": [{ "filename": "string.instructions.md", "content": "string (with applyTo YAML frontmatter)" }]
2544
2576
  }
2545
2577
  }
2546
2578
 
@@ -2586,7 +2618,9 @@ Return a JSON object with this exact shape:
2586
2618
  "readmeMd": "<updated content or null>",
2587
2619
  "cursorrules": "<updated content or null>",
2588
2620
  "cursorRules": [{"filename": "name.mdc", "content": "..."}] or null,
2589
- "claudeSkills": [{"filename": "name.md", "content": "..."}] or null
2621
+ "claudeSkills": [{"filename": "name.md", "content": "..."}] or null,
2622
+ "copilotInstructions": "<updated content or null>",
2623
+ "copilotInstructionFiles": [{"filename": "name.instructions.md", "content": "..."}] or null
2590
2624
  },
2591
2625
  "changesSummary": "<1-2 sentence summary of what was updated and why>",
2592
2626
  "docsUpdated": ["CLAUDE.md", "README.md"]
@@ -2628,6 +2662,12 @@ Each bullet MUST be prefixed with an observation type in bold brackets. Valid ty
2628
2662
  - **[env]** \u2014 an environment or configuration requirement
2629
2663
  - **[convention]** \u2014 a project-specific rule or naming convention
2630
2664
 
2665
+ Each type can optionally include a scope suffix to indicate whether the learning is project-specific or personal to the developer:
2666
+ - **[type:project]** \u2014 specific to this codebase (DEFAULT \u2014 use when in doubt)
2667
+ - **[type:personal]** \u2014 specific to the developer's workflow or personal preferences
2668
+
2669
+ Use :personal ONLY for user corrections that are clearly personal preferences rather than project requirements. For example, "use bun not npm" is :personal if the project's lockfile doesn't enforce it. "Always run tsc before tsup" is :project because it's about the project's build process. When in doubt, default to :project (or omit the scope entirely, which means :project).
2670
+
2631
2671
  Good examples:
2632
2672
  - "**[correction]** Files in \`src/generated/\` are auto-generated \u2014 never edit them directly"
2633
2673
  - "**[correction]** Use \`pnpm\` not \`npm\` \u2014 the lockfile is pnpm-lock.yaml and npm creates conflicts"
@@ -2675,6 +2715,7 @@ Analyze the provided file tree and file extension distribution. Return a JSON ob
2675
2715
  - "languages": array of programming languages used, ordered by prominence in the project (most files first)
2676
2716
  - "frameworks": array of frameworks and key libraries detected, ordered by prominence
2677
2717
  - "tools": array of external tools, services, and platforms the project integrates with, ordered by prominence
2718
+ - "workspaces": array of relative paths to sub-projects, workspaces, or modules detected from workspace config files (e.g. pnpm-workspace.yaml, Cargo.toml [workspace], go.work, settings.gradle, BUILD files). Empty array if not a multi-project repo.
2678
2719
 
2679
2720
  Use the file extension distribution to determine the ordering \u2014 technologies with more files should appear first.
2680
2721
 
@@ -2711,7 +2752,8 @@ async function detectProjectStack(fileTree, suffixCounts) {
2711
2752
  return {
2712
2753
  languages: Array.isArray(result.languages) ? result.languages : [],
2713
2754
  frameworks: Array.isArray(result.frameworks) ? result.frameworks : [],
2714
- tools: Array.isArray(result.tools) ? result.tools : []
2755
+ tools: Array.isArray(result.tools) ? result.tools : [],
2756
+ workspaces: Array.isArray(result.workspaces) ? result.workspaces : []
2715
2757
  };
2716
2758
  }
2717
2759
 
@@ -2720,14 +2762,14 @@ init_config();
2720
2762
 
2721
2763
  // src/fingerprint/cache.ts
2722
2764
  import fs6 from "fs";
2723
- import path5 from "path";
2765
+ import path6 from "path";
2724
2766
  import crypto from "crypto";
2725
2767
  import { execSync as execSync5 } from "child_process";
2726
2768
  var CACHE_VERSION = 1;
2727
2769
  var CACHE_DIR = ".caliber/cache";
2728
2770
  var CACHE_FILE = "fingerprint.json";
2729
2771
  function getCachePath(dir) {
2730
- return path5.join(dir, CACHE_DIR, CACHE_FILE);
2772
+ return path6.join(dir, CACHE_DIR, CACHE_FILE);
2731
2773
  }
2732
2774
  function getGitHead(dir) {
2733
2775
  try {
@@ -2776,16 +2818,17 @@ function loadFingerprintCache(dir, fileTree) {
2776
2818
  codeAnalysis: cache.codeAnalysis,
2777
2819
  languages: cache.languages,
2778
2820
  frameworks: cache.frameworks,
2779
- tools: cache.tools
2821
+ tools: cache.tools,
2822
+ workspaces: cache.workspaces
2780
2823
  };
2781
2824
  } catch {
2782
2825
  return null;
2783
2826
  }
2784
2827
  }
2785
- function saveFingerprintCache(dir, fileTree, codeAnalysis, languages, frameworks, tools) {
2828
+ function saveFingerprintCache(dir, fileTree, codeAnalysis, languages, frameworks, tools, workspaces) {
2786
2829
  const cachePath = getCachePath(dir);
2787
2830
  try {
2788
- const cacheDir = path5.dirname(cachePath);
2831
+ const cacheDir = path6.dirname(cachePath);
2789
2832
  if (!fs6.existsSync(cacheDir)) {
2790
2833
  fs6.mkdirSync(cacheDir, { recursive: true });
2791
2834
  }
@@ -2796,12 +2839,24 @@ function saveFingerprintCache(dir, fileTree, codeAnalysis, languages, frameworks
2796
2839
  codeAnalysis,
2797
2840
  languages,
2798
2841
  frameworks,
2799
- tools
2842
+ tools,
2843
+ workspaces
2800
2844
  };
2801
2845
  fs6.writeFileSync(cachePath, JSON.stringify(cache), "utf-8");
2802
2846
  } catch {
2803
2847
  }
2804
2848
  }
2849
+ function getDetectedWorkspaces(dir) {
2850
+ const cachePath = getCachePath(dir);
2851
+ try {
2852
+ if (!fs6.existsSync(cachePath)) return [];
2853
+ const raw = fs6.readFileSync(cachePath, "utf-8");
2854
+ const cache = JSON.parse(raw);
2855
+ return cache.workspaces ?? [];
2856
+ } catch {
2857
+ return [];
2858
+ }
2859
+ }
2805
2860
 
2806
2861
  // src/fingerprint/index.ts
2807
2862
  async function collectFingerprint(dir) {
@@ -2833,20 +2888,21 @@ async function collectFingerprint(dir) {
2833
2888
  existingConfigs,
2834
2889
  codeAnalysis
2835
2890
  };
2836
- await enrichWithLLM(fingerprint);
2891
+ const workspaces = await enrichWithLLM(fingerprint);
2837
2892
  saveFingerprintCache(
2838
2893
  dir,
2839
2894
  fileTree,
2840
2895
  codeAnalysis,
2841
2896
  fingerprint.languages,
2842
2897
  fingerprint.frameworks,
2843
- fingerprint.tools
2898
+ fingerprint.tools,
2899
+ workspaces
2844
2900
  );
2845
2901
  return fingerprint;
2846
2902
  }
2847
2903
  function readPackageName(dir) {
2848
2904
  try {
2849
- const pkgPath = path6.join(dir, "package.json");
2905
+ const pkgPath = path7.join(dir, "package.json");
2850
2906
  if (!fs7.existsSync(pkgPath)) return void 0;
2851
2907
  const pkg3 = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
2852
2908
  return pkg3.name;
@@ -2857,12 +2913,12 @@ function readPackageName(dir) {
2857
2913
  async function enrichWithLLM(fingerprint) {
2858
2914
  try {
2859
2915
  const config = loadConfig();
2860
- if (!config) return;
2861
- if (fingerprint.fileTree.length === 0) return;
2916
+ if (!config) return [];
2917
+ if (fingerprint.fileTree.length === 0) return [];
2862
2918
  const suffixCounts = {};
2863
2919
  for (const entry of fingerprint.fileTree) {
2864
2920
  if (entry.endsWith("/")) continue;
2865
- const ext = path6.extname(entry).toLowerCase();
2921
+ const ext = path7.extname(entry).toLowerCase();
2866
2922
  if (ext) {
2867
2923
  suffixCounts[ext] = (suffixCounts[ext] || 0) + 1;
2868
2924
  }
@@ -2871,99 +2927,574 @@ async function enrichWithLLM(fingerprint) {
2871
2927
  if (result.languages?.length) fingerprint.languages = result.languages;
2872
2928
  if (result.frameworks?.length) fingerprint.frameworks = result.frameworks;
2873
2929
  if (result.tools?.length) fingerprint.tools = result.tools;
2930
+ return result.workspaces ?? [];
2874
2931
  } catch {
2932
+ return [];
2875
2933
  }
2876
2934
  }
2877
2935
 
2878
- // src/ai/generate.ts
2879
- init_config();
2936
+ // src/fingerprint/sources.ts
2937
+ import fs8 from "fs";
2938
+ import path8 from "path";
2880
2939
 
2881
- // src/utils/dependencies.ts
2882
- import { readFileSync } from "fs";
2883
- import { join } from "path";
2884
- function readFileOrNull(path33) {
2940
+ // src/scoring/utils.ts
2941
+ import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
2942
+ import { join, relative } from "path";
2943
+ function readFileOrNull(filePath) {
2885
2944
  try {
2886
- return readFileSync(path33, "utf-8");
2945
+ return readFileSync(filePath, "utf-8");
2887
2946
  } catch {
2888
2947
  return null;
2889
2948
  }
2890
2949
  }
2891
- function readJsonOrNull(path33) {
2892
- const content = readFileOrNull(path33);
2893
- if (!content) return null;
2894
- try {
2895
- return JSON.parse(content);
2896
- } catch {
2897
- return null;
2950
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2951
+ "node_modules",
2952
+ ".git",
2953
+ "dist",
2954
+ "build",
2955
+ "out",
2956
+ ".next",
2957
+ ".nuxt",
2958
+ "__pycache__",
2959
+ ".venv",
2960
+ "venv",
2961
+ "env",
2962
+ ".env",
2963
+ "target",
2964
+ "vendor",
2965
+ ".cache",
2966
+ ".parcel-cache",
2967
+ "coverage",
2968
+ ".nyc_output",
2969
+ ".turbo",
2970
+ ".caliber",
2971
+ ".claude",
2972
+ ".cursor",
2973
+ ".agents",
2974
+ ".codex"
2975
+ ]);
2976
+ var IGNORED_FILES = /* @__PURE__ */ new Set([
2977
+ ".DS_Store",
2978
+ "Thumbs.db",
2979
+ ".gitignore",
2980
+ ".editorconfig",
2981
+ ".prettierrc",
2982
+ ".prettierignore",
2983
+ ".eslintignore",
2984
+ "package-lock.json",
2985
+ "yarn.lock",
2986
+ "pnpm-lock.yaml",
2987
+ "bun.lockb"
2988
+ ]);
2989
+ function collectProjectStructure(dir, maxDepth = 2) {
2990
+ const dirs = [];
2991
+ const files = [];
2992
+ function walk(currentDir, depth) {
2993
+ if (depth > maxDepth) return;
2994
+ try {
2995
+ const entries = readdirSync(currentDir, { withFileTypes: true });
2996
+ for (const entry of entries) {
2997
+ const name = entry.name;
2998
+ if (name.startsWith(".") && IGNORED_DIRS.has(name)) continue;
2999
+ if (IGNORED_FILES.has(name)) continue;
3000
+ const rel = relative(dir, join(currentDir, name));
3001
+ if (entry.isDirectory()) {
3002
+ if (IGNORED_DIRS.has(name)) continue;
3003
+ dirs.push(rel);
3004
+ walk(join(currentDir, name), depth + 1);
3005
+ } else if (entry.isFile()) {
3006
+ files.push(rel);
3007
+ }
3008
+ }
3009
+ } catch {
3010
+ }
2898
3011
  }
3012
+ walk(dir, 0);
3013
+ return { dirs, files };
2899
3014
  }
2900
- function extractNpmDeps(dir) {
2901
- const pkg3 = readJsonOrNull(join(dir, "package.json"));
2902
- if (!pkg3) return [];
2903
- const deps = {
2904
- ...pkg3.dependencies,
2905
- ...pkg3.devDependencies
2906
- };
2907
- const trivial = /* @__PURE__ */ new Set([
2908
- "typescript",
2909
- "@types/node",
2910
- "tslib",
2911
- "ts-node",
2912
- "tsx",
2913
- "prettier",
2914
- "eslint",
2915
- "@eslint/js",
2916
- "rimraf",
2917
- "cross-env",
2918
- "dotenv",
2919
- "nodemon",
2920
- "husky",
2921
- "lint-staged",
2922
- "commitlint",
2923
- "@commitlint/cli",
2924
- "@commitlint/config-conventional"
2925
- ]);
2926
- const trivialPatterns = [
2927
- /^@rely-ai\//,
2928
- /^@caliber-ai\//,
2929
- /^eslint-/,
2930
- /^@eslint\//,
2931
- /^prettier-/,
2932
- /^@typescript-eslint\//
2933
- ];
2934
- return Object.keys(deps).filter((d) => !trivial.has(d) && !d.startsWith("@types/") && !trivialPatterns.some((p) => p.test(d))).slice(0, 30);
2935
- }
2936
- function extractPythonDeps(dir) {
2937
- const reqTxt = readFileOrNull(join(dir, "requirements.txt"));
2938
- if (reqTxt) {
2939
- return reqTxt.split("\n").map((l) => l.trim().split(/[=<>!~\[]/)[0].trim()).filter((l) => l && !l.startsWith("#")).slice(0, 30);
3015
+ function collectPrimaryConfigContent(dir) {
3016
+ const parts = [];
3017
+ for (const file of ["CLAUDE.md", ".cursorrules", "AGENTS.md"]) {
3018
+ const content = readFileOrNull(join(dir, file));
3019
+ if (content) parts.push(content);
2940
3020
  }
2941
- const pyproject = readFileOrNull(join(dir, "pyproject.toml"));
2942
- if (pyproject) {
2943
- const depMatch = pyproject.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
2944
- if (depMatch) {
2945
- return depMatch[1].split("\n").map((l) => l.trim().replace(/["',]/g, "").split(/[=<>!~\[]/)[0].trim()).filter((l) => l.length > 0).slice(0, 30);
3021
+ try {
3022
+ const rulesDir = join(dir, ".cursor", "rules");
3023
+ const mdcFiles = readdirSync(rulesDir).filter((f) => f.endsWith(".mdc"));
3024
+ for (const f of mdcFiles) {
3025
+ const content = readFileOrNull(join(rulesDir, f));
3026
+ if (content) parts.push(content);
2946
3027
  }
3028
+ } catch {
2947
3029
  }
2948
- return [];
3030
+ return parts.join("\n");
2949
3031
  }
2950
- function extractGoDeps(dir) {
2951
- const goMod = readFileOrNull(join(dir, "go.mod"));
2952
- if (!goMod) return [];
2953
- const requireBlock = goMod.match(/require\s*\(([\s\S]*?)\)/);
2954
- if (!requireBlock) return [];
2955
- return requireBlock[1].split("\n").map((l) => l.trim().split(/\s/)[0]).filter((l) => l && !l.startsWith("//")).map((l) => l.split("/").pop() || l).slice(0, 30);
3032
+ function collectAllConfigContent(dir) {
3033
+ const parts = [collectPrimaryConfigContent(dir)];
3034
+ for (const skillsDir of [join(dir, ".claude", "skills"), join(dir, ".agents", "skills")]) {
3035
+ try {
3036
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
3037
+ for (const entry of entries) {
3038
+ if (entry.isDirectory()) {
3039
+ const skill = readFileOrNull(join(skillsDir, entry.name, "SKILL.md"));
3040
+ if (skill) parts.push(skill);
3041
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
3042
+ const content = readFileOrNull(join(skillsDir, entry.name));
3043
+ if (content) parts.push(content);
3044
+ }
3045
+ }
3046
+ } catch {
3047
+ }
3048
+ }
3049
+ return parts.join("\n");
2956
3050
  }
2957
- function extractRustDeps(dir) {
2958
- const cargo = readFileOrNull(join(dir, "Cargo.toml"));
2959
- if (!cargo) return [];
2960
- const depSection = cargo.match(/\[dependencies\]([\s\S]*?)(?:\[|$)/);
2961
- if (!depSection) return [];
2962
- return depSection[1].split("\n").map((l) => l.trim().split(/\s*=/)[0].trim()).filter((l) => l.length > 0 && !l.startsWith("#")).slice(0, 30);
3051
+ function estimateTokens2(text) {
3052
+ return Math.ceil(text.length / 4);
2963
3053
  }
2964
- function extractAllDeps(dir) {
2965
- return [
2966
- ...extractNpmDeps(dir),
3054
+ function analyzeMarkdownStructure(content) {
3055
+ const lines = content.split("\n");
3056
+ let headingCount = 0;
3057
+ let h2Count = 0;
3058
+ let h3Count = 0;
3059
+ let codeBlockCount = 0;
3060
+ let codeBlockLines = 0;
3061
+ let listItemCount = 0;
3062
+ let inlineCodeCount = 0;
3063
+ let inCodeBlock = false;
3064
+ for (const line of lines) {
3065
+ const trimmed = line.trim();
3066
+ if (trimmed.startsWith("```")) {
3067
+ if (!inCodeBlock) codeBlockCount++;
3068
+ inCodeBlock = !inCodeBlock;
3069
+ continue;
3070
+ }
3071
+ if (inCodeBlock) {
3072
+ codeBlockLines++;
3073
+ continue;
3074
+ }
3075
+ if (trimmed.startsWith("## ") && !trimmed.startsWith("### ")) h2Count++;
3076
+ if (trimmed.startsWith("### ")) h3Count++;
3077
+ if (trimmed.startsWith("#")) headingCount++;
3078
+ if (/^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) listItemCount++;
3079
+ const inlineMatches = trimmed.match(/`[^`]+`/g);
3080
+ if (inlineMatches) inlineCodeCount += inlineMatches.length;
3081
+ }
3082
+ return {
3083
+ headingCount,
3084
+ h2Count,
3085
+ h3Count,
3086
+ codeBlockCount,
3087
+ codeBlockLines,
3088
+ listItemCount,
3089
+ inlineCodeCount,
3090
+ totalLines: lines.length,
3091
+ nonEmptyLines: lines.filter((l) => l.trim().length > 0).length
3092
+ };
3093
+ }
3094
+ function extractReferences(content) {
3095
+ const refs = /* @__PURE__ */ new Set();
3096
+ const backtickPattern = /`([^`]+)`/g;
3097
+ let match;
3098
+ while ((match = backtickPattern.exec(content)) !== null) {
3099
+ const term = match[1].trim();
3100
+ if ((term.includes("/") || /\.\w{1,5}$/.test(term)) && !term.startsWith("-") && term.length < 200) {
3101
+ if (term.startsWith("@") && (term.match(/\//g) || []).length === 1) continue;
3102
+ if (term.includes(" ")) continue;
3103
+ if (/^\d+\.\d+/.test(term)) continue;
3104
+ if (term.includes("/") && !/\.\w{1,5}$/.test(term)) {
3105
+ if (term !== term.toLowerCase() && !/^[a-z]/.test(term)) continue;
3106
+ const segments = term.split("/");
3107
+ if (segments.every((s) => s.length <= 3)) continue;
3108
+ }
3109
+ const cleaned = term.replace(/[,;:!?)]+$/, "");
3110
+ if (cleaned.length > 1) refs.add(cleaned);
3111
+ }
3112
+ }
3113
+ const pathPattern = /(?:^|\s)((?:[a-zA-Z0-9_@.-]+\/)+[a-zA-Z0-9_.*-]+\.[a-zA-Z]{1,5})/gm;
3114
+ while ((match = pathPattern.exec(content)) !== null) {
3115
+ const term = match[1].trim();
3116
+ if (term.length > 2 && term.length < 200) {
3117
+ if (term.startsWith("@") && (term.match(/\//g) || []).length === 1) continue;
3118
+ const cleaned = term.replace(/[,;:!?)]+$/, "");
3119
+ if (cleaned.length > 1) refs.add(cleaned);
3120
+ }
3121
+ }
3122
+ return Array.from(refs);
3123
+ }
3124
+ function validateFileReferences(content, dir, checkExists = existsSync2) {
3125
+ const refs = extractReferences(content);
3126
+ const valid = [];
3127
+ const invalid = [];
3128
+ for (const ref of refs) {
3129
+ if (/^https?:\/\//.test(ref)) continue;
3130
+ if (/^\d+\.\d+/.test(ref)) continue;
3131
+ if (ref.startsWith("#") || ref.startsWith("@")) continue;
3132
+ if (ref.includes("*") || ref.includes("..")) continue;
3133
+ if (!ref.includes("/") && !ref.includes(".")) continue;
3134
+ const fullPath = join(dir, ref);
3135
+ if (checkExists(fullPath)) {
3136
+ valid.push(ref);
3137
+ } else {
3138
+ const withoutTrailing = ref.replace(/\/+$/, "");
3139
+ if (withoutTrailing !== ref && checkExists(join(dir, withoutTrailing))) {
3140
+ valid.push(ref);
3141
+ } else {
3142
+ invalid.push(ref);
3143
+ }
3144
+ }
3145
+ }
3146
+ return { valid, invalid, total: valid.length + invalid.length };
3147
+ }
3148
+ function countConcreteness(content) {
3149
+ let concrete = 0;
3150
+ let abstract = 0;
3151
+ let inCodeBlock = false;
3152
+ for (const line of content.split("\n")) {
3153
+ if (line.trim().startsWith("```")) {
3154
+ inCodeBlock = !inCodeBlock;
3155
+ continue;
3156
+ }
3157
+ const classification = classifyLine(line, inCodeBlock);
3158
+ if (classification === "concrete") concrete++;
3159
+ else if (classification === "abstract") abstract++;
3160
+ }
3161
+ return { concrete, abstract };
3162
+ }
3163
+ function countTreeLines(content) {
3164
+ const treeLinePattern = /[├└│─┬]/;
3165
+ let count = 0;
3166
+ let inCodeBlock = false;
3167
+ for (const line of content.split("\n")) {
3168
+ if (line.trim().startsWith("```")) {
3169
+ inCodeBlock = !inCodeBlock;
3170
+ continue;
3171
+ }
3172
+ if (inCodeBlock && treeLinePattern.test(line)) count++;
3173
+ }
3174
+ return count;
3175
+ }
3176
+ function calculateDuplicatePercent(content1, content2) {
3177
+ const lines1 = new Set(content1.split("\n").map((l) => l.trim()).filter((l) => l.length > 10));
3178
+ const lines2 = content2.split("\n").map((l) => l.trim()).filter((l) => l.length > 10);
3179
+ const overlapping = lines2.filter((l) => lines1.has(l)).length;
3180
+ return lines2.length > 0 ? Math.round(overlapping / lines2.length * 100) : 0;
3181
+ }
3182
+ function calculateDensityPoints(density, maxPoints) {
3183
+ if (density >= 40) return maxPoints;
3184
+ if (density >= 25) return Math.round(maxPoints * 0.75);
3185
+ if (density >= 15) return Math.round(maxPoints * 0.5);
3186
+ if (density >= 5) return Math.round(maxPoints * 0.25);
3187
+ return 0;
3188
+ }
3189
+ function isEntryMentioned(entry, contentLower) {
3190
+ const entryLower = entry.toLowerCase();
3191
+ const variants = [entryLower, entryLower.replace(/\\/g, "/")];
3192
+ const lastSegment = entry.split("/").pop()?.toLowerCase();
3193
+ if (lastSegment && lastSegment.length > 3) variants.push(lastSegment);
3194
+ return variants.some((v) => {
3195
+ const escaped = v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3196
+ return new RegExp(`(?:^|[\\s\`/"'\\.,(])${escaped}(?:[\\s\`/"'.,;:!?)\\\\]|$)`, "i").test(contentLower);
3197
+ });
3198
+ }
3199
+ function classifyLine(line, inCodeBlock) {
3200
+ if (inCodeBlock) return "concrete";
3201
+ const trimmed = line.trim();
3202
+ if (trimmed.length === 0) return "neutral";
3203
+ if (trimmed.startsWith("#")) return "neutral";
3204
+ if (/`[^`]+`/.test(trimmed)) return "concrete";
3205
+ if (/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+\.[a-zA-Z]{1,5}/.test(trimmed)) return "concrete";
3206
+ if (/[a-zA-Z0-9_]{4,}\/[a-zA-Z0-9_.-]/.test(trimmed)) return "concrete";
3207
+ 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";
3208
+ return "abstract";
3209
+ }
3210
+
3211
+ // src/fingerprint/sources.ts
3212
+ var MAX_SOURCES_IN_PROMPT = 5;
3213
+ var SOURCE_CONTENT_LIMIT = 2e3;
3214
+ var README_CONTENT_LIMIT = 1e3;
3215
+ var ORIGIN_PRIORITY = { cli: 0, config: 1, workspace: 2 };
3216
+ function loadSourcesConfig(dir) {
3217
+ const configPath = path8.join(dir, ".caliber", "sources.json");
3218
+ const content = readFileOrNull(configPath);
3219
+ if (!content) return [];
3220
+ try {
3221
+ const parsed = JSON.parse(content);
3222
+ if (!Array.isArray(parsed.sources)) {
3223
+ console.warn("Warning: .caliber/sources.json is malformed (missing sources array), skipping sources");
3224
+ return [];
3225
+ }
3226
+ return parsed.sources.filter(
3227
+ (s) => s.type && (s.path || s.url)
3228
+ );
3229
+ } catch {
3230
+ console.warn("Warning: .caliber/sources.json is malformed, skipping sources");
3231
+ return [];
3232
+ }
3233
+ }
3234
+ function writeSourcesConfig(dir, sources2) {
3235
+ const configDir = path8.join(dir, ".caliber");
3236
+ if (!fs8.existsSync(configDir)) {
3237
+ fs8.mkdirSync(configDir, { recursive: true });
3238
+ }
3239
+ const configPath = path8.join(configDir, "sources.json");
3240
+ fs8.writeFileSync(configPath, JSON.stringify({ sources: sources2 }, null, 2) + "\n", "utf-8");
3241
+ }
3242
+ function detectSourceType(absPath) {
3243
+ try {
3244
+ return fs8.statSync(absPath).isDirectory() ? "repo" : "file";
3245
+ } catch {
3246
+ return "file";
3247
+ }
3248
+ }
3249
+ function isInsideDir(childPath, parentDir) {
3250
+ const relative2 = path8.relative(parentDir, childPath);
3251
+ return !relative2.startsWith("..") && !path8.isAbsolute(relative2);
3252
+ }
3253
+ function resolveAllSources(dir, cliSources, workspaces) {
3254
+ const seen = /* @__PURE__ */ new Map();
3255
+ const projectRoot = path8.resolve(dir);
3256
+ for (const src of cliSources) {
3257
+ const absPath = path8.resolve(dir, src);
3258
+ if (seen.has(absPath)) continue;
3259
+ const type = detectSourceType(absPath);
3260
+ seen.set(absPath, {
3261
+ absPath,
3262
+ config: { type, path: src },
3263
+ origin: "cli"
3264
+ });
3265
+ }
3266
+ const configSources = loadSourcesConfig(dir);
3267
+ for (const cfg of configSources) {
3268
+ if (cfg.type === "url") continue;
3269
+ if (!cfg.path) continue;
3270
+ const absPath = path8.resolve(dir, cfg.path);
3271
+ if (seen.has(absPath)) continue;
3272
+ seen.set(absPath, { absPath, config: cfg, origin: "config" });
3273
+ }
3274
+ for (const ws of workspaces) {
3275
+ const absPath = path8.resolve(dir, ws);
3276
+ if (seen.has(absPath)) continue;
3277
+ if (!isInsideDir(absPath, projectRoot)) continue;
3278
+ seen.set(absPath, {
3279
+ absPath,
3280
+ config: { type: "repo", path: ws, role: "workspace-sibling" },
3281
+ origin: "workspace"
3282
+ });
3283
+ }
3284
+ const valid = [];
3285
+ for (const [absPath, resolved] of seen) {
3286
+ let stat;
3287
+ try {
3288
+ stat = fs8.statSync(absPath);
3289
+ } catch {
3290
+ console.warn(`Source ${resolved.config.path || absPath} not found, skipping`);
3291
+ continue;
3292
+ }
3293
+ if (isInsideDir(absPath, projectRoot) && resolved.origin !== "workspace") {
3294
+ if (absPath !== projectRoot) {
3295
+ console.warn(`Skipping ${resolved.config.path || absPath}: inside current project`);
3296
+ }
3297
+ continue;
3298
+ }
3299
+ if (resolved.config.type === "file" && !stat.isFile()) continue;
3300
+ if (resolved.config.type === "repo" && !stat.isDirectory()) continue;
3301
+ valid.push(resolved);
3302
+ }
3303
+ valid.sort((a, b) => {
3304
+ const pA = ORIGIN_PRIORITY[a.origin] ?? 2;
3305
+ const pB = ORIGIN_PRIORITY[b.origin] ?? 2;
3306
+ if (pA !== pB) return pA - pB;
3307
+ return a.absPath.length - b.absPath.length;
3308
+ });
3309
+ const capped = valid.slice(0, MAX_SOURCES_IN_PROMPT);
3310
+ return capped.map((r) => collectSourceSummary(r, dir));
3311
+ }
3312
+ function collectSourceSummary(resolved, projectDir) {
3313
+ const { config, origin, absPath } = resolved;
3314
+ if (config.type === "file") {
3315
+ return collectFileSummary(resolved, projectDir);
3316
+ }
3317
+ const summaryPath = path8.join(absPath, ".caliber", "summary.json");
3318
+ const summaryContent = readFileOrNull(summaryPath);
3319
+ if (summaryContent) {
3320
+ try {
3321
+ const published = JSON.parse(summaryContent);
3322
+ return {
3323
+ name: published.name || path8.basename(absPath),
3324
+ type: "repo",
3325
+ role: config.role || published.role || "related-repo",
3326
+ description: config.description || published.description || "",
3327
+ origin,
3328
+ topLevelDirs: Array.isArray(published.topLevelDirs) ? published.topLevelDirs : void 0,
3329
+ existingClaudeMd: typeof published.conventions === "string" ? published.conventions : void 0,
3330
+ packageName: typeof published.name === "string" ? published.name : void 0
3331
+ };
3332
+ } catch {
3333
+ }
3334
+ }
3335
+ return collectRepoSummary(resolved, projectDir);
3336
+ }
3337
+ function collectRepoSummary(resolved, projectDir) {
3338
+ const { config, origin, absPath } = resolved;
3339
+ const packageName = readPackageName(absPath);
3340
+ let topLevelDirs;
3341
+ let keyFiles;
3342
+ try {
3343
+ const entries = fs8.readdirSync(absPath, { withFileTypes: true });
3344
+ topLevelDirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).slice(0, 20);
3345
+ keyFiles = entries.filter((e) => e.isFile() && !e.name.startsWith(".")).map((e) => e.name).slice(0, 15);
3346
+ } catch {
3347
+ }
3348
+ const claudeMdContent = readFileOrNull(path8.join(absPath, "CLAUDE.md"));
3349
+ const existingClaudeMd = claudeMdContent ? claudeMdContent.slice(0, SOURCE_CONTENT_LIMIT) : void 0;
3350
+ const readmeContent = readFileOrNull(path8.join(absPath, "README.md"));
3351
+ const readmeExcerpt = readmeContent ? readmeContent.slice(0, README_CONTENT_LIMIT) : void 0;
3352
+ const gitRemoteUrl = getGitRemoteUrl(absPath);
3353
+ return {
3354
+ name: packageName || path8.basename(absPath),
3355
+ type: "repo",
3356
+ role: config.role || "related-repo",
3357
+ description: config.description || "",
3358
+ origin,
3359
+ gitRemoteUrl,
3360
+ topLevelDirs,
3361
+ keyFiles,
3362
+ existingClaudeMd,
3363
+ readmeExcerpt,
3364
+ packageName
3365
+ };
3366
+ }
3367
+ function collectFileSummary(resolved, projectDir) {
3368
+ const { config, origin, absPath } = resolved;
3369
+ const content = readFileOrNull(absPath);
3370
+ return {
3371
+ name: path8.basename(absPath),
3372
+ type: "file",
3373
+ role: config.role || "reference-doc",
3374
+ description: config.description || content?.slice(0, 100).split("\n")[0] || "",
3375
+ origin,
3376
+ existingClaudeMd: content ? content.slice(0, SOURCE_CONTENT_LIMIT) : void 0
3377
+ };
3378
+ }
3379
+ function formatSourcesForPrompt(sources2) {
3380
+ if (sources2.length === 0) return "";
3381
+ const parts = [
3382
+ "\n--- Related Sources ---",
3383
+ "This project works with these related sources. Reference them in the generated",
3384
+ "config where relevant \u2014 mention shared conventions, cross-repo workflows, and",
3385
+ "integration points.\n"
3386
+ ];
3387
+ for (const source of sources2) {
3388
+ parts.push(`[${source.name}] (${source.role})`);
3389
+ if (source.description) parts.push(source.description);
3390
+ if (source.topLevelDirs?.length) {
3391
+ parts.push(`Key dirs: ${source.topLevelDirs.join(", ")}`);
3392
+ }
3393
+ if (source.keyFiles?.length) {
3394
+ parts.push(`Key files: ${source.keyFiles.join(", ")}`);
3395
+ }
3396
+ if (source.existingClaudeMd) {
3397
+ parts.push(`Their CLAUDE.md:
3398
+ ${source.existingClaudeMd}`);
3399
+ } else if (source.readmeExcerpt) {
3400
+ parts.push(`Their README:
3401
+ ${source.readmeExcerpt}`);
3402
+ }
3403
+ parts.push("");
3404
+ }
3405
+ parts.push("--- End Related Sources ---");
3406
+ return parts.join("\n");
3407
+ }
3408
+
3409
+ // src/ai/generate.ts
3410
+ init_config();
3411
+
3412
+ // src/utils/dependencies.ts
3413
+ import { readFileSync as readFileSync2 } from "fs";
3414
+ import { join as join2 } from "path";
3415
+ function readFileOrNull2(path37) {
3416
+ try {
3417
+ return readFileSync2(path37, "utf-8");
3418
+ } catch {
3419
+ return null;
3420
+ }
3421
+ }
3422
+ function readJsonOrNull(path37) {
3423
+ const content = readFileOrNull2(path37);
3424
+ if (!content) return null;
3425
+ try {
3426
+ return JSON.parse(content);
3427
+ } catch {
3428
+ return null;
3429
+ }
3430
+ }
3431
+ function extractNpmDeps(dir) {
3432
+ const pkg3 = readJsonOrNull(join2(dir, "package.json"));
3433
+ if (!pkg3) return [];
3434
+ const deps = {
3435
+ ...pkg3.dependencies,
3436
+ ...pkg3.devDependencies
3437
+ };
3438
+ const trivial = /* @__PURE__ */ new Set([
3439
+ "typescript",
3440
+ "@types/node",
3441
+ "tslib",
3442
+ "ts-node",
3443
+ "tsx",
3444
+ "prettier",
3445
+ "eslint",
3446
+ "@eslint/js",
3447
+ "rimraf",
3448
+ "cross-env",
3449
+ "dotenv",
3450
+ "nodemon",
3451
+ "husky",
3452
+ "lint-staged",
3453
+ "commitlint",
3454
+ "@commitlint/cli",
3455
+ "@commitlint/config-conventional"
3456
+ ]);
3457
+ const trivialPatterns = [
3458
+ /^@rely-ai\//,
3459
+ /^@caliber-ai\//,
3460
+ /^eslint-/,
3461
+ /^@eslint\//,
3462
+ /^prettier-/,
3463
+ /^@typescript-eslint\//
3464
+ ];
3465
+ return Object.keys(deps).filter((d) => !trivial.has(d) && !d.startsWith("@types/") && !trivialPatterns.some((p) => p.test(d))).slice(0, 30);
3466
+ }
3467
+ function extractPythonDeps(dir) {
3468
+ const reqTxt = readFileOrNull2(join2(dir, "requirements.txt"));
3469
+ if (reqTxt) {
3470
+ return reqTxt.split("\n").map((l) => l.trim().split(/[=<>!~\[]/)[0].trim()).filter((l) => l && !l.startsWith("#")).slice(0, 30);
3471
+ }
3472
+ const pyproject = readFileOrNull2(join2(dir, "pyproject.toml"));
3473
+ if (pyproject) {
3474
+ const depMatch = pyproject.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
3475
+ if (depMatch) {
3476
+ return depMatch[1].split("\n").map((l) => l.trim().replace(/["',]/g, "").split(/[=<>!~\[]/)[0].trim()).filter((l) => l.length > 0).slice(0, 30);
3477
+ }
3478
+ }
3479
+ return [];
3480
+ }
3481
+ function extractGoDeps(dir) {
3482
+ const goMod = readFileOrNull2(join2(dir, "go.mod"));
3483
+ if (!goMod) return [];
3484
+ const requireBlock = goMod.match(/require\s*\(([\s\S]*?)\)/);
3485
+ if (!requireBlock) return [];
3486
+ return requireBlock[1].split("\n").map((l) => l.trim().split(/\s/)[0]).filter((l) => l && !l.startsWith("//")).map((l) => l.split("/").pop() || l).slice(0, 30);
3487
+ }
3488
+ function extractRustDeps(dir) {
3489
+ const cargo = readFileOrNull2(join2(dir, "Cargo.toml"));
3490
+ if (!cargo) return [];
3491
+ const depSection = cargo.match(/\[dependencies\]([\s\S]*?)(?:\[|$)/);
3492
+ if (!depSection) return [];
3493
+ return depSection[1].split("\n").map((l) => l.trim().split(/\s*=/)[0].trim()).filter((l) => l.length > 0 && !l.startsWith("#")).slice(0, 30);
3494
+ }
3495
+ function extractAllDeps(dir) {
3496
+ return [
3497
+ ...extractNpmDeps(dir),
2967
3498
  ...extractPythonDeps(dir),
2968
3499
  ...extractGoDeps(dir),
2969
3500
  ...extractRustDeps(dir)
@@ -3094,6 +3625,12 @@ ${fingerprint.fileTree.slice(0, 50).join("\n")}`);
3094
3625
  parts.push(`
3095
3626
  Dependencies: ${allDeps.join(", ")}`);
3096
3627
  }
3628
+ if (fingerprint.sources?.length) {
3629
+ parts.push("\nRelated Sources:");
3630
+ for (const s of fingerprint.sources) {
3631
+ parts.push(`- ${s.name} (${s.role}): ${s.description || "related project"}`);
3632
+ }
3633
+ }
3097
3634
  return parts.join("\n");
3098
3635
  }
3099
3636
  async function generateSkill(context, topic, model) {
@@ -3409,6 +3946,11 @@ ${truncate(skill.content, LIMITS.SKILL_CHARS)}`);
3409
3946
  (${existing.cursorSkills.length - LIMITS.SKILLS_MAX} more skills omitted)`);
3410
3947
  }
3411
3948
  }
3949
+ if (existing.personalLearnings) {
3950
+ parts.push(`
3951
+ --- Personal Learnings (developer-specific, include in generated configs) ---
3952
+ ${existing.personalLearnings}`);
3953
+ }
3412
3954
  const allDeps = extractAllDeps(process.cwd());
3413
3955
  if (allDeps.length > 0) {
3414
3956
  parts.push(`
@@ -3458,24 +4000,27 @@ ${f.content}
3458
4000
  parts.push(header);
3459
4001
  parts.push(codeLines.join("\n"));
3460
4002
  }
4003
+ if (fingerprint.sources?.length) {
4004
+ parts.push(formatSourcesForPrompt(fingerprint.sources));
4005
+ }
3461
4006
  return parts.join("\n");
3462
4007
  }
3463
4008
 
3464
4009
  // src/writers/index.ts
3465
- import fs13 from "fs";
4010
+ import fs15 from "fs";
3466
4011
 
3467
4012
  // src/writers/claude/index.ts
3468
- import fs8 from "fs";
3469
- import path7 from "path";
4013
+ import fs9 from "fs";
4014
+ import path9 from "path";
3470
4015
  function writeClaudeConfig(config) {
3471
4016
  const written = [];
3472
- fs8.writeFileSync("CLAUDE.md", config.claudeMd);
4017
+ fs9.writeFileSync("CLAUDE.md", config.claudeMd);
3473
4018
  written.push("CLAUDE.md");
3474
4019
  if (config.skills?.length) {
3475
4020
  for (const skill of config.skills) {
3476
- const skillDir = path7.join(".claude", "skills", skill.name);
3477
- if (!fs8.existsSync(skillDir)) fs8.mkdirSync(skillDir, { recursive: true });
3478
- const skillPath = path7.join(skillDir, "SKILL.md");
4021
+ const skillDir = path9.join(".claude", "skills", skill.name);
4022
+ if (!fs9.existsSync(skillDir)) fs9.mkdirSync(skillDir, { recursive: true });
4023
+ const skillPath = path9.join(skillDir, "SKILL.md");
3479
4024
  const frontmatter = [
3480
4025
  "---",
3481
4026
  `name: ${skill.name}`,
@@ -3483,49 +4028,49 @@ function writeClaudeConfig(config) {
3483
4028
  "---",
3484
4029
  ""
3485
4030
  ].join("\n");
3486
- fs8.writeFileSync(skillPath, frontmatter + skill.content);
4031
+ fs9.writeFileSync(skillPath, frontmatter + skill.content);
3487
4032
  written.push(skillPath);
3488
4033
  }
3489
4034
  }
3490
4035
  if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
3491
4036
  let existingServers = {};
3492
4037
  try {
3493
- if (fs8.existsSync(".mcp.json")) {
3494
- const existing = JSON.parse(fs8.readFileSync(".mcp.json", "utf-8"));
4038
+ if (fs9.existsSync(".mcp.json")) {
4039
+ const existing = JSON.parse(fs9.readFileSync(".mcp.json", "utf-8"));
3495
4040
  if (existing.mcpServers) existingServers = existing.mcpServers;
3496
4041
  }
3497
4042
  } catch {
3498
4043
  }
3499
4044
  const mergedServers = { ...existingServers, ...config.mcpServers };
3500
- fs8.writeFileSync(".mcp.json", JSON.stringify({ mcpServers: mergedServers }, null, 2));
4045
+ fs9.writeFileSync(".mcp.json", JSON.stringify({ mcpServers: mergedServers }, null, 2));
3501
4046
  written.push(".mcp.json");
3502
4047
  }
3503
4048
  return written;
3504
4049
  }
3505
4050
 
3506
4051
  // src/writers/cursor/index.ts
3507
- import fs9 from "fs";
3508
- import path8 from "path";
4052
+ import fs10 from "fs";
4053
+ import path10 from "path";
3509
4054
  function writeCursorConfig(config) {
3510
4055
  const written = [];
3511
4056
  if (config.cursorrules) {
3512
- fs9.writeFileSync(".cursorrules", config.cursorrules);
4057
+ fs10.writeFileSync(".cursorrules", config.cursorrules);
3513
4058
  written.push(".cursorrules");
3514
4059
  }
3515
4060
  if (config.rules?.length) {
3516
- const rulesDir = path8.join(".cursor", "rules");
3517
- if (!fs9.existsSync(rulesDir)) fs9.mkdirSync(rulesDir, { recursive: true });
4061
+ const rulesDir = path10.join(".cursor", "rules");
4062
+ if (!fs10.existsSync(rulesDir)) fs10.mkdirSync(rulesDir, { recursive: true });
3518
4063
  for (const rule of config.rules) {
3519
- const rulePath = path8.join(rulesDir, rule.filename);
3520
- fs9.writeFileSync(rulePath, rule.content);
4064
+ const rulePath = path10.join(rulesDir, rule.filename);
4065
+ fs10.writeFileSync(rulePath, rule.content);
3521
4066
  written.push(rulePath);
3522
4067
  }
3523
4068
  }
3524
4069
  if (config.skills?.length) {
3525
4070
  for (const skill of config.skills) {
3526
- const skillDir = path8.join(".cursor", "skills", skill.name);
3527
- if (!fs9.existsSync(skillDir)) fs9.mkdirSync(skillDir, { recursive: true });
3528
- const skillPath = path8.join(skillDir, "SKILL.md");
4071
+ const skillDir = path10.join(".cursor", "skills", skill.name);
4072
+ if (!fs10.existsSync(skillDir)) fs10.mkdirSync(skillDir, { recursive: true });
4073
+ const skillPath = path10.join(skillDir, "SKILL.md");
3529
4074
  const frontmatter = [
3530
4075
  "---",
3531
4076
  `name: ${skill.name}`,
@@ -3533,41 +4078,41 @@ function writeCursorConfig(config) {
3533
4078
  "---",
3534
4079
  ""
3535
4080
  ].join("\n");
3536
- fs9.writeFileSync(skillPath, frontmatter + skill.content);
4081
+ fs10.writeFileSync(skillPath, frontmatter + skill.content);
3537
4082
  written.push(skillPath);
3538
4083
  }
3539
4084
  }
3540
4085
  if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
3541
4086
  const cursorDir = ".cursor";
3542
- if (!fs9.existsSync(cursorDir)) fs9.mkdirSync(cursorDir, { recursive: true });
3543
- const mcpPath = path8.join(cursorDir, "mcp.json");
4087
+ if (!fs10.existsSync(cursorDir)) fs10.mkdirSync(cursorDir, { recursive: true });
4088
+ const mcpPath = path10.join(cursorDir, "mcp.json");
3544
4089
  let existingServers = {};
3545
4090
  try {
3546
- if (fs9.existsSync(mcpPath)) {
3547
- const existing = JSON.parse(fs9.readFileSync(mcpPath, "utf-8"));
4091
+ if (fs10.existsSync(mcpPath)) {
4092
+ const existing = JSON.parse(fs10.readFileSync(mcpPath, "utf-8"));
3548
4093
  if (existing.mcpServers) existingServers = existing.mcpServers;
3549
4094
  }
3550
4095
  } catch {
3551
4096
  }
3552
4097
  const mergedServers = { ...existingServers, ...config.mcpServers };
3553
- fs9.writeFileSync(mcpPath, JSON.stringify({ mcpServers: mergedServers }, null, 2));
4098
+ fs10.writeFileSync(mcpPath, JSON.stringify({ mcpServers: mergedServers }, null, 2));
3554
4099
  written.push(mcpPath);
3555
4100
  }
3556
4101
  return written;
3557
4102
  }
3558
4103
 
3559
4104
  // src/writers/codex/index.ts
3560
- import fs10 from "fs";
3561
- import path9 from "path";
4105
+ import fs11 from "fs";
4106
+ import path11 from "path";
3562
4107
  function writeCodexConfig(config) {
3563
4108
  const written = [];
3564
- fs10.writeFileSync("AGENTS.md", config.agentsMd);
4109
+ fs11.writeFileSync("AGENTS.md", config.agentsMd);
3565
4110
  written.push("AGENTS.md");
3566
4111
  if (config.skills?.length) {
3567
4112
  for (const skill of config.skills) {
3568
- const skillDir = path9.join(".agents", "skills", skill.name);
3569
- if (!fs10.existsSync(skillDir)) fs10.mkdirSync(skillDir, { recursive: true });
3570
- const skillPath = path9.join(skillDir, "SKILL.md");
4113
+ const skillDir = path11.join(".agents", "skills", skill.name);
4114
+ if (!fs11.existsSync(skillDir)) fs11.mkdirSync(skillDir, { recursive: true });
4115
+ const skillPath = path11.join(skillDir, "SKILL.md");
3571
4116
  const frontmatter = [
3572
4117
  "---",
3573
4118
  `name: ${skill.name}`,
@@ -3575,71 +4120,92 @@ function writeCodexConfig(config) {
3575
4120
  "---",
3576
4121
  ""
3577
4122
  ].join("\n");
3578
- fs10.writeFileSync(skillPath, frontmatter + skill.content);
4123
+ fs11.writeFileSync(skillPath, frontmatter + skill.content);
3579
4124
  written.push(skillPath);
3580
4125
  }
3581
4126
  }
3582
4127
  return written;
3583
4128
  }
3584
4129
 
4130
+ // src/writers/github-copilot/index.ts
4131
+ import fs12 from "fs";
4132
+ import path12 from "path";
4133
+ function writeGithubCopilotConfig(config) {
4134
+ const written = [];
4135
+ if (config.instructions) {
4136
+ fs12.mkdirSync(".github", { recursive: true });
4137
+ fs12.writeFileSync(path12.join(".github", "copilot-instructions.md"), config.instructions);
4138
+ written.push(".github/copilot-instructions.md");
4139
+ }
4140
+ if (config.instructionFiles?.length) {
4141
+ const instructionsDir = path12.join(".github", "instructions");
4142
+ fs12.mkdirSync(instructionsDir, { recursive: true });
4143
+ for (const file of config.instructionFiles) {
4144
+ fs12.writeFileSync(path12.join(instructionsDir, file.filename), file.content);
4145
+ written.push(`.github/instructions/${file.filename}`);
4146
+ }
4147
+ }
4148
+ return written;
4149
+ }
4150
+
3585
4151
  // src/writers/backup.ts
3586
4152
  init_constants();
3587
- import fs11 from "fs";
3588
- import path11 from "path";
4153
+ import fs13 from "fs";
4154
+ import path13 from "path";
3589
4155
  function createBackup(files) {
3590
4156
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3591
- const backupDir = path11.join(BACKUPS_DIR, timestamp);
4157
+ const backupDir = path13.join(BACKUPS_DIR, timestamp);
3592
4158
  for (const file of files) {
3593
- if (!fs11.existsSync(file)) continue;
3594
- const dest = path11.join(backupDir, file);
3595
- const destDir = path11.dirname(dest);
3596
- if (!fs11.existsSync(destDir)) {
3597
- fs11.mkdirSync(destDir, { recursive: true });
4159
+ if (!fs13.existsSync(file)) continue;
4160
+ const dest = path13.join(backupDir, file);
4161
+ const destDir = path13.dirname(dest);
4162
+ if (!fs13.existsSync(destDir)) {
4163
+ fs13.mkdirSync(destDir, { recursive: true });
3598
4164
  }
3599
- fs11.copyFileSync(file, dest);
4165
+ fs13.copyFileSync(file, dest);
3600
4166
  }
3601
4167
  return backupDir;
3602
4168
  }
3603
4169
  function restoreBackup(backupDir, file) {
3604
- const backupFile = path11.join(backupDir, file);
3605
- if (!fs11.existsSync(backupFile)) return false;
3606
- const destDir = path11.dirname(file);
3607
- if (!fs11.existsSync(destDir)) {
3608
- fs11.mkdirSync(destDir, { recursive: true });
4170
+ const backupFile = path13.join(backupDir, file);
4171
+ if (!fs13.existsSync(backupFile)) return false;
4172
+ const destDir = path13.dirname(file);
4173
+ if (!fs13.existsSync(destDir)) {
4174
+ fs13.mkdirSync(destDir, { recursive: true });
3609
4175
  }
3610
- fs11.copyFileSync(backupFile, file);
4176
+ fs13.copyFileSync(backupFile, file);
3611
4177
  return true;
3612
4178
  }
3613
4179
 
3614
4180
  // src/writers/manifest.ts
3615
4181
  init_constants();
3616
- import fs12 from "fs";
4182
+ import fs14 from "fs";
3617
4183
  import crypto2 from "crypto";
3618
4184
  function readManifest() {
3619
4185
  try {
3620
- if (!fs12.existsSync(MANIFEST_FILE)) return null;
3621
- return JSON.parse(fs12.readFileSync(MANIFEST_FILE, "utf-8"));
4186
+ if (!fs14.existsSync(MANIFEST_FILE)) return null;
4187
+ return JSON.parse(fs14.readFileSync(MANIFEST_FILE, "utf-8"));
3622
4188
  } catch {
3623
4189
  return null;
3624
4190
  }
3625
4191
  }
3626
4192
  function writeManifest(manifest) {
3627
- if (!fs12.existsSync(CALIBER_DIR)) {
3628
- fs12.mkdirSync(CALIBER_DIR, { recursive: true });
4193
+ if (!fs14.existsSync(CALIBER_DIR)) {
4194
+ fs14.mkdirSync(CALIBER_DIR, { recursive: true });
3629
4195
  }
3630
- fs12.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
4196
+ fs14.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
3631
4197
  }
3632
4198
  function fileChecksum(filePath) {
3633
- const content = fs12.readFileSync(filePath);
4199
+ const content = fs14.readFileSync(filePath);
3634
4200
  return crypto2.createHash("sha256").update(content).digest("hex");
3635
4201
  }
3636
4202
 
3637
4203
  // src/writers/index.ts
3638
4204
  function writeSetup(setup) {
3639
4205
  const filesToWrite = getFilesToWrite(setup);
3640
- const filesToDelete = (setup.deletions || []).map((d) => d.filePath).filter((f) => fs13.existsSync(f));
4206
+ const filesToDelete = (setup.deletions || []).map((d) => d.filePath).filter((f) => fs15.existsSync(f));
3641
4207
  const existingFiles = [
3642
- ...filesToWrite.filter((f) => fs13.existsSync(f)),
4208
+ ...filesToWrite.filter((f) => fs15.existsSync(f)),
3643
4209
  ...filesToDelete
3644
4210
  ];
3645
4211
  const backupDir = existingFiles.length > 0 ? createBackup(existingFiles) : void 0;
@@ -3653,9 +4219,12 @@ function writeSetup(setup) {
3653
4219
  if (setup.targetAgent.includes("codex") && setup.codex) {
3654
4220
  written.push(...writeCodexConfig(setup.codex));
3655
4221
  }
4222
+ if (setup.targetAgent.includes("github-copilot") && setup.copilot) {
4223
+ written.push(...writeGithubCopilotConfig(setup.copilot));
4224
+ }
3656
4225
  const deleted = [];
3657
4226
  for (const filePath of filesToDelete) {
3658
- fs13.unlinkSync(filePath);
4227
+ fs15.unlinkSync(filePath);
3659
4228
  deleted.push(filePath);
3660
4229
  }
3661
4230
  ensureGitignore();
@@ -3685,8 +4254,8 @@ function undoSetup() {
3685
4254
  const removed = [];
3686
4255
  for (const entry of manifest.entries) {
3687
4256
  if (entry.action === "created") {
3688
- if (fs13.existsSync(entry.path)) {
3689
- fs13.unlinkSync(entry.path);
4257
+ if (fs15.existsSync(entry.path)) {
4258
+ fs15.unlinkSync(entry.path);
3690
4259
  removed.push(entry.path);
3691
4260
  }
3692
4261
  } else if ((entry.action === "modified" || entry.action === "deleted") && manifest.backupDir) {
@@ -3696,8 +4265,8 @@ function undoSetup() {
3696
4265
  }
3697
4266
  }
3698
4267
  const { MANIFEST_FILE: MANIFEST_FILE2 } = (init_constants(), __toCommonJS(constants_exports));
3699
- if (fs13.existsSync(MANIFEST_FILE2)) {
3700
- fs13.unlinkSync(MANIFEST_FILE2);
4268
+ if (fs15.existsSync(MANIFEST_FILE2)) {
4269
+ fs15.unlinkSync(MANIFEST_FILE2);
3701
4270
  }
3702
4271
  return { restored, removed };
3703
4272
  }
@@ -3728,27 +4297,33 @@ function getFilesToWrite(setup) {
3728
4297
  for (const s of setup.codex.skills) files.push(`.agents/skills/${s.name}/SKILL.md`);
3729
4298
  }
3730
4299
  }
4300
+ if (setup.targetAgent.includes("github-copilot") && setup.copilot) {
4301
+ if (setup.copilot.instructions) files.push(".github/copilot-instructions.md");
4302
+ if (setup.copilot.instructionFiles) {
4303
+ for (const f of setup.copilot.instructionFiles) files.push(`.github/instructions/${f.filename}`);
4304
+ }
4305
+ }
3731
4306
  return files;
3732
4307
  }
3733
4308
  function ensureGitignore() {
3734
4309
  const gitignorePath = ".gitignore";
3735
- if (fs13.existsSync(gitignorePath)) {
3736
- const content = fs13.readFileSync(gitignorePath, "utf-8");
4310
+ if (fs15.existsSync(gitignorePath)) {
4311
+ const content = fs15.readFileSync(gitignorePath, "utf-8");
3737
4312
  if (!content.includes(".caliber/")) {
3738
- fs13.appendFileSync(gitignorePath, "\n# Caliber local state\n.caliber/\n");
4313
+ fs15.appendFileSync(gitignorePath, "\n# Caliber local state\n.caliber/\n");
3739
4314
  }
3740
4315
  } else {
3741
- fs13.writeFileSync(gitignorePath, "# Caliber local state\n.caliber/\n");
4316
+ fs15.writeFileSync(gitignorePath, "# Caliber local state\n.caliber/\n");
3742
4317
  }
3743
4318
  }
3744
4319
 
3745
4320
  // src/writers/staging.ts
3746
4321
  init_constants();
3747
- import fs14 from "fs";
3748
- import path12 from "path";
3749
- var STAGED_DIR = path12.join(CALIBER_DIR, "staged");
3750
- var PROPOSED_DIR = path12.join(STAGED_DIR, "proposed");
3751
- var CURRENT_DIR = path12.join(STAGED_DIR, "current");
4322
+ import fs16 from "fs";
4323
+ import path14 from "path";
4324
+ var STAGED_DIR = path14.join(CALIBER_DIR, "staged");
4325
+ var PROPOSED_DIR = path14.join(STAGED_DIR, "proposed");
4326
+ var CURRENT_DIR = path14.join(STAGED_DIR, "current");
3752
4327
  function normalizeContent(content) {
3753
4328
  return content.split("\n").map((line) => line.trimEnd()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
3754
4329
  }
@@ -3758,20 +4333,20 @@ function stageFiles(files, projectDir) {
3758
4333
  let modifiedFiles = 0;
3759
4334
  const stagedFiles = [];
3760
4335
  for (const file of files) {
3761
- const originalPath = path12.join(projectDir, file.path);
3762
- if (fs14.existsSync(originalPath)) {
3763
- const existing = fs14.readFileSync(originalPath, "utf-8");
4336
+ const originalPath = path14.join(projectDir, file.path);
4337
+ if (fs16.existsSync(originalPath)) {
4338
+ const existing = fs16.readFileSync(originalPath, "utf-8");
3764
4339
  if (normalizeContent(existing) === normalizeContent(file.content)) {
3765
4340
  continue;
3766
4341
  }
3767
4342
  }
3768
- const proposedPath = path12.join(PROPOSED_DIR, file.path);
3769
- fs14.mkdirSync(path12.dirname(proposedPath), { recursive: true });
3770
- fs14.writeFileSync(proposedPath, file.content);
3771
- if (fs14.existsSync(originalPath)) {
3772
- const currentPath = path12.join(CURRENT_DIR, file.path);
3773
- fs14.mkdirSync(path12.dirname(currentPath), { recursive: true });
3774
- fs14.copyFileSync(originalPath, currentPath);
4343
+ const proposedPath = path14.join(PROPOSED_DIR, file.path);
4344
+ fs16.mkdirSync(path14.dirname(proposedPath), { recursive: true });
4345
+ fs16.writeFileSync(proposedPath, file.content);
4346
+ if (fs16.existsSync(originalPath)) {
4347
+ const currentPath = path14.join(CURRENT_DIR, file.path);
4348
+ fs16.mkdirSync(path14.dirname(currentPath), { recursive: true });
4349
+ fs16.copyFileSync(originalPath, currentPath);
3775
4350
  modifiedFiles++;
3776
4351
  stagedFiles.push({ relativePath: file.path, proposedPath, currentPath, originalPath, isNew: false });
3777
4352
  } else {
@@ -3782,13 +4357,13 @@ function stageFiles(files, projectDir) {
3782
4357
  return { newFiles, modifiedFiles, stagedFiles };
3783
4358
  }
3784
4359
  function cleanupStaging() {
3785
- if (fs14.existsSync(STAGED_DIR)) {
3786
- fs14.rmSync(STAGED_DIR, { recursive: true, force: true });
4360
+ if (fs16.existsSync(STAGED_DIR)) {
4361
+ fs16.rmSync(STAGED_DIR, { recursive: true, force: true });
3787
4362
  }
3788
4363
  }
3789
4364
 
3790
4365
  // src/commands/setup-files.ts
3791
- import fs15 from "fs";
4366
+ import fs17 from "fs";
3792
4367
  function buildSkillContent(skill) {
3793
4368
  const frontmatter = `---
3794
4369
  name: ${skill.name}
@@ -3836,8 +4411,18 @@ function collectSetupFiles(setup, targetAgent) {
3836
4411
  }
3837
4412
  }
3838
4413
  }
4414
+ const copilot = setup.copilot;
4415
+ if (copilot) {
4416
+ if (copilot.instructions) files.push({ path: ".github/copilot-instructions.md", content: copilot.instructions });
4417
+ const instructionFiles = copilot.instructionFiles;
4418
+ if (Array.isArray(instructionFiles)) {
4419
+ for (const file of instructionFiles) {
4420
+ files.push({ path: `.github/instructions/${file.filename}`, content: file.content });
4421
+ }
4422
+ }
4423
+ }
3839
4424
  const codexTargeted = targetAgent ? targetAgent.includes("codex") : false;
3840
- if (codexTargeted && !fs15.existsSync("AGENTS.md") && !(codex && codex.agentsMd)) {
4425
+ if (codexTargeted && !fs17.existsSync("AGENTS.md") && !(codex && codex.agentsMd)) {
3841
4426
  const agentRefs = [];
3842
4427
  if (claude) agentRefs.push("See `CLAUDE.md` for Claude Code configuration.");
3843
4428
  if (cursor) agentRefs.push("See `.cursor/rules/` for Cursor rules.");
@@ -3855,27 +4440,27 @@ ${agentRefs.join(" ")}
3855
4440
 
3856
4441
  // src/lib/hooks.ts
3857
4442
  init_resolve_caliber();
3858
- import fs17 from "fs";
3859
- import path13 from "path";
4443
+ import fs19 from "fs";
4444
+ import path15 from "path";
3860
4445
  import { execSync as execSync7 } from "child_process";
3861
- var SETTINGS_PATH = path13.join(".claude", "settings.json");
4446
+ var SETTINGS_PATH = path15.join(".claude", "settings.json");
3862
4447
  var REFRESH_TAIL = "refresh --quiet";
3863
4448
  var HOOK_DESCRIPTION = "Caliber: auto-refreshing docs based on code changes";
3864
4449
  function getHookCommand() {
3865
4450
  return `${resolveCaliber()} ${REFRESH_TAIL}`;
3866
4451
  }
3867
4452
  function readSettings() {
3868
- if (!fs17.existsSync(SETTINGS_PATH)) return {};
4453
+ if (!fs19.existsSync(SETTINGS_PATH)) return {};
3869
4454
  try {
3870
- return JSON.parse(fs17.readFileSync(SETTINGS_PATH, "utf-8"));
4455
+ return JSON.parse(fs19.readFileSync(SETTINGS_PATH, "utf-8"));
3871
4456
  } catch {
3872
4457
  return {};
3873
4458
  }
3874
4459
  }
3875
4460
  function writeSettings(settings) {
3876
- const dir = path13.dirname(SETTINGS_PATH);
3877
- if (!fs17.existsSync(dir)) fs17.mkdirSync(dir, { recursive: true });
3878
- fs17.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
4461
+ const dir = path15.dirname(SETTINGS_PATH);
4462
+ if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
4463
+ fs19.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
3879
4464
  }
3880
4465
  function findHookIndex(sessionEnd) {
3881
4466
  return sessionEnd.findIndex(
@@ -3938,19 +4523,19 @@ ${PRECOMMIT_END}`;
3938
4523
  function getGitHooksDir() {
3939
4524
  try {
3940
4525
  const gitDir = execSync7("git rev-parse --git-dir", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3941
- return path13.join(gitDir, "hooks");
4526
+ return path15.join(gitDir, "hooks");
3942
4527
  } catch {
3943
4528
  return null;
3944
4529
  }
3945
4530
  }
3946
4531
  function getPreCommitPath() {
3947
4532
  const hooksDir = getGitHooksDir();
3948
- return hooksDir ? path13.join(hooksDir, "pre-commit") : null;
4533
+ return hooksDir ? path15.join(hooksDir, "pre-commit") : null;
3949
4534
  }
3950
4535
  function isPreCommitHookInstalled() {
3951
4536
  const hookPath = getPreCommitPath();
3952
- if (!hookPath || !fs17.existsSync(hookPath)) return false;
3953
- const content = fs17.readFileSync(hookPath, "utf-8");
4537
+ if (!hookPath || !fs19.existsSync(hookPath)) return false;
4538
+ const content = fs19.readFileSync(hookPath, "utf-8");
3954
4539
  return content.includes(PRECOMMIT_START);
3955
4540
  }
3956
4541
  function installPreCommitHook() {
@@ -3959,44 +4544,44 @@ function installPreCommitHook() {
3959
4544
  }
3960
4545
  const hookPath = getPreCommitPath();
3961
4546
  if (!hookPath) return { installed: false, alreadyInstalled: false };
3962
- const hooksDir = path13.dirname(hookPath);
3963
- if (!fs17.existsSync(hooksDir)) fs17.mkdirSync(hooksDir, { recursive: true });
4547
+ const hooksDir = path15.dirname(hookPath);
4548
+ if (!fs19.existsSync(hooksDir)) fs19.mkdirSync(hooksDir, { recursive: true });
3964
4549
  let content = "";
3965
- if (fs17.existsSync(hookPath)) {
3966
- content = fs17.readFileSync(hookPath, "utf-8");
4550
+ if (fs19.existsSync(hookPath)) {
4551
+ content = fs19.readFileSync(hookPath, "utf-8");
3967
4552
  if (!content.endsWith("\n")) content += "\n";
3968
4553
  content += "\n" + getPrecommitBlock() + "\n";
3969
4554
  } else {
3970
4555
  content = "#!/bin/sh\n\n" + getPrecommitBlock() + "\n";
3971
4556
  }
3972
- fs17.writeFileSync(hookPath, content);
3973
- fs17.chmodSync(hookPath, 493);
4557
+ fs19.writeFileSync(hookPath, content);
4558
+ fs19.chmodSync(hookPath, 493);
3974
4559
  return { installed: true, alreadyInstalled: false };
3975
4560
  }
3976
4561
  function removePreCommitHook() {
3977
4562
  const hookPath = getPreCommitPath();
3978
- if (!hookPath || !fs17.existsSync(hookPath)) {
4563
+ if (!hookPath || !fs19.existsSync(hookPath)) {
3979
4564
  return { removed: false, notFound: true };
3980
4565
  }
3981
- let content = fs17.readFileSync(hookPath, "utf-8");
4566
+ let content = fs19.readFileSync(hookPath, "utf-8");
3982
4567
  if (!content.includes(PRECOMMIT_START)) {
3983
4568
  return { removed: false, notFound: true };
3984
4569
  }
3985
4570
  const regex = new RegExp(`\\n?${PRECOMMIT_START}[\\s\\S]*?${PRECOMMIT_END}\\n?`);
3986
4571
  content = content.replace(regex, "\n");
3987
4572
  if (content.trim() === "#!/bin/sh" || content.trim() === "") {
3988
- fs17.unlinkSync(hookPath);
4573
+ fs19.unlinkSync(hookPath);
3989
4574
  } else {
3990
- fs17.writeFileSync(hookPath, content);
4575
+ fs19.writeFileSync(hookPath, content);
3991
4576
  }
3992
4577
  return { removed: true, notFound: false };
3993
4578
  }
3994
4579
 
3995
4580
  // src/lib/learning-hooks.ts
3996
4581
  init_resolve_caliber();
3997
- import fs18 from "fs";
3998
- import path14 from "path";
3999
- var SETTINGS_PATH2 = path14.join(".claude", "settings.json");
4582
+ import fs20 from "fs";
4583
+ import path16 from "path";
4584
+ var SETTINGS_PATH2 = path16.join(".claude", "settings.json");
4000
4585
  var HOOK_TAILS = [
4001
4586
  { event: "PostToolUse", tail: "learn observe", description: "Caliber: recording tool usage for session learning" },
4002
4587
  { event: "PostToolUseFailure", tail: "learn observe --failure", description: "Caliber: recording tool failure for session learning" },
@@ -4013,17 +4598,17 @@ function getHookConfigs() {
4013
4598
  }));
4014
4599
  }
4015
4600
  function readSettings2() {
4016
- if (!fs18.existsSync(SETTINGS_PATH2)) return {};
4601
+ if (!fs20.existsSync(SETTINGS_PATH2)) return {};
4017
4602
  try {
4018
- return JSON.parse(fs18.readFileSync(SETTINGS_PATH2, "utf-8"));
4603
+ return JSON.parse(fs20.readFileSync(SETTINGS_PATH2, "utf-8"));
4019
4604
  } catch {
4020
4605
  return {};
4021
4606
  }
4022
4607
  }
4023
4608
  function writeSettings2(settings) {
4024
- const dir = path14.dirname(SETTINGS_PATH2);
4025
- if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
4026
- fs18.writeFileSync(SETTINGS_PATH2, JSON.stringify(settings, null, 2));
4609
+ const dir = path16.dirname(SETTINGS_PATH2);
4610
+ if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
4611
+ fs20.writeFileSync(SETTINGS_PATH2, JSON.stringify(settings, null, 2));
4027
4612
  }
4028
4613
  function hasLearningHook(matchers, tail) {
4029
4614
  return matchers.some((entry) => entry.hooks?.some((h) => isCaliberCommand(h.command, tail)));
@@ -4057,7 +4642,7 @@ function installLearningHooks() {
4057
4642
  writeSettings2(settings);
4058
4643
  return { installed: true, alreadyInstalled: false };
4059
4644
  }
4060
- var CURSOR_HOOKS_PATH = path14.join(".cursor", "hooks.json");
4645
+ var CURSOR_HOOKS_PATH = path16.join(".cursor", "hooks.json");
4061
4646
  var CURSOR_HOOK_EVENTS = [
4062
4647
  { event: "postToolUse", tail: "learn observe" },
4063
4648
  { event: "postToolUseFailure", tail: "learn observe --failure" },
@@ -4065,17 +4650,17 @@ var CURSOR_HOOK_EVENTS = [
4065
4650
  { event: "sessionEnd", tail: "learn finalize --auto" }
4066
4651
  ];
4067
4652
  function readCursorHooks() {
4068
- if (!fs18.existsSync(CURSOR_HOOKS_PATH)) return { version: 1, hooks: {} };
4653
+ if (!fs20.existsSync(CURSOR_HOOKS_PATH)) return { version: 1, hooks: {} };
4069
4654
  try {
4070
- return JSON.parse(fs18.readFileSync(CURSOR_HOOKS_PATH, "utf-8"));
4655
+ return JSON.parse(fs20.readFileSync(CURSOR_HOOKS_PATH, "utf-8"));
4071
4656
  } catch {
4072
4657
  return { version: 1, hooks: {} };
4073
4658
  }
4074
4659
  }
4075
4660
  function writeCursorHooks(config) {
4076
- const dir = path14.dirname(CURSOR_HOOKS_PATH);
4077
- if (!fs18.existsSync(dir)) fs18.mkdirSync(dir, { recursive: true });
4078
- fs18.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(config, null, 2));
4661
+ const dir = path16.dirname(CURSOR_HOOKS_PATH);
4662
+ if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
4663
+ fs20.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(config, null, 2));
4079
4664
  }
4080
4665
  function hasCursorHook(entries, tail) {
4081
4666
  return entries.some((e) => isCaliberCommand(e.command, tail));
@@ -4145,22 +4730,22 @@ function removeLearningHooks() {
4145
4730
 
4146
4731
  // src/lib/state.ts
4147
4732
  init_constants();
4148
- import fs19 from "fs";
4149
- import path15 from "path";
4733
+ import fs21 from "fs";
4734
+ import path17 from "path";
4150
4735
  import { execSync as execSync8 } from "child_process";
4151
- var STATE_FILE = path15.join(CALIBER_DIR, ".caliber-state.json");
4736
+ var STATE_FILE = path17.join(CALIBER_DIR, ".caliber-state.json");
4152
4737
  function normalizeTargetAgent(value) {
4153
4738
  if (Array.isArray(value)) return value;
4154
4739
  if (typeof value === "string") {
4155
4740
  if (value === "both") return ["claude", "cursor"];
4156
- if (["claude", "cursor", "codex"].includes(value)) return [value];
4741
+ if (["claude", "cursor", "codex", "github-copilot"].includes(value)) return [value];
4157
4742
  }
4158
4743
  return void 0;
4159
4744
  }
4160
4745
  function readState() {
4161
4746
  try {
4162
- if (!fs19.existsSync(STATE_FILE)) return null;
4163
- const raw = JSON.parse(fs19.readFileSync(STATE_FILE, "utf-8"));
4747
+ if (!fs21.existsSync(STATE_FILE)) return null;
4748
+ const raw = JSON.parse(fs21.readFileSync(STATE_FILE, "utf-8"));
4164
4749
  if (raw.targetAgent) raw.targetAgent = normalizeTargetAgent(raw.targetAgent);
4165
4750
  return raw;
4166
4751
  } catch {
@@ -4168,10 +4753,10 @@ function readState() {
4168
4753
  }
4169
4754
  }
4170
4755
  function writeState(state) {
4171
- if (!fs19.existsSync(CALIBER_DIR)) {
4172
- fs19.mkdirSync(CALIBER_DIR, { recursive: true });
4756
+ if (!fs21.existsSync(CALIBER_DIR)) {
4757
+ fs21.mkdirSync(CALIBER_DIR, { recursive: true });
4173
4758
  }
4174
- fs19.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
4759
+ fs21.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
4175
4760
  }
4176
4761
  function getCurrentHeadSha() {
4177
4762
  try {
@@ -4287,11 +4872,11 @@ async function runInteractiveProviderSetup(options) {
4287
4872
 
4288
4873
  // src/scoring/index.ts
4289
4874
  import { existsSync as existsSync7 } from "fs";
4290
- import { join as join8 } from "path";
4875
+ import { join as join9 } from "path";
4291
4876
 
4292
4877
  // src/scoring/checks/existence.ts
4293
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
4294
- import { join as join2 } from "path";
4878
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
4879
+ import { join as join3 } from "path";
4295
4880
 
4296
4881
  // src/scoring/constants.ts
4297
4882
  var POINTS_CLAUDE_MD_EXISTS = 6;
@@ -4319,6 +4904,8 @@ var POINTS_HOOKS = 2;
4319
4904
  var POINTS_AGENTS_MD = 1;
4320
4905
  var POINTS_OPEN_SKILLS_FORMAT = 2;
4321
4906
  var POINTS_LEARNED_CONTENT = 2;
4907
+ var POINTS_SOURCES_CONFIGURED = 3;
4908
+ var POINTS_SOURCES_REFERENCED = 3;
4322
4909
  var TOKEN_BUDGET_THRESHOLDS = [
4323
4910
  { maxTokens: 2e3, points: 6 },
4324
4911
  { maxTokens: 3500, points: 5 },
@@ -4382,6 +4969,9 @@ var BOTH_ONLY_CHECKS = /* @__PURE__ */ new Set([
4382
4969
  var CODEX_ONLY_CHECKS = /* @__PURE__ */ new Set([
4383
4970
  "codex_agents_md_exists"
4384
4971
  ]);
4972
+ var COPILOT_ONLY_CHECKS = /* @__PURE__ */ new Set([
4973
+ "copilot_instructions_exists"
4974
+ ]);
4385
4975
  var NON_CODEX_CHECKS = /* @__PURE__ */ new Set([
4386
4976
  "agents_md_exists"
4387
4977
  ]);
@@ -4402,13 +4992,13 @@ function computeGrade(score) {
4402
4992
  // src/scoring/checks/existence.ts
4403
4993
  function countFiles(dir, pattern) {
4404
4994
  try {
4405
- return readdirSync(dir, { recursive: true }).map(String).filter((f) => pattern.test(f));
4995
+ return readdirSync2(dir, { recursive: true }).map(String).filter((f) => pattern.test(f));
4406
4996
  } catch {
4407
4997
  return [];
4408
4998
  }
4409
4999
  }
4410
5000
  function hasMcpServers(dir) {
4411
- const sources = [];
5001
+ const sources2 = [];
4412
5002
  let count = 0;
4413
5003
  const mcpFiles = [
4414
5004
  ".mcp.json",
@@ -4418,21 +5008,21 @@ function hasMcpServers(dir) {
4418
5008
  ];
4419
5009
  for (const rel of mcpFiles) {
4420
5010
  try {
4421
- const content = readFileSync2(join2(dir, rel), "utf-8");
5011
+ const content = readFileSync3(join3(dir, rel), "utf-8");
4422
5012
  const parsed = JSON.parse(content);
4423
5013
  const servers = parsed.mcpServers;
4424
5014
  if (servers && Object.keys(servers).length > 0) {
4425
5015
  count += Object.keys(servers).length;
4426
- sources.push(rel);
5016
+ sources2.push(rel);
4427
5017
  }
4428
5018
  } catch {
4429
5019
  }
4430
5020
  }
4431
- return { count, sources };
5021
+ return { count, sources: sources2 };
4432
5022
  }
4433
5023
  function checkExistence(dir) {
4434
5024
  const checks = [];
4435
- const claudeMdExists = existsSync2(join2(dir, "CLAUDE.md"));
5025
+ const claudeMdExists = existsSync3(join3(dir, "CLAUDE.md"));
4436
5026
  checks.push({
4437
5027
  id: "claude_md_exists",
4438
5028
  name: "CLAUDE.md exists",
@@ -4448,8 +5038,8 @@ function checkExistence(dir) {
4448
5038
  instruction: "Create CLAUDE.md with project context, commands, architecture, and conventions."
4449
5039
  }
4450
5040
  });
4451
- const hasCursorrules = existsSync2(join2(dir, ".cursorrules"));
4452
- const cursorRulesDir = existsSync2(join2(dir, ".cursor", "rules"));
5041
+ const hasCursorrules = existsSync3(join3(dir, ".cursorrules"));
5042
+ const cursorRulesDir = existsSync3(join3(dir, ".cursor", "rules"));
4453
5043
  const cursorRulesExist = hasCursorrules || cursorRulesDir;
4454
5044
  checks.push({
4455
5045
  id: "cursor_rules_exist",
@@ -4466,7 +5056,7 @@ function checkExistence(dir) {
4466
5056
  instruction: "Create .cursor/rules/ with project-specific Cursor rules."
4467
5057
  }
4468
5058
  });
4469
- const agentsMdExists = existsSync2(join2(dir, "AGENTS.md"));
5059
+ const agentsMdExists = existsSync3(join3(dir, "AGENTS.md"));
4470
5060
  checks.push({
4471
5061
  id: "codex_agents_md_exists",
4472
5062
  name: "AGENTS.md exists",
@@ -4478,12 +5068,28 @@ function checkExistence(dir) {
4478
5068
  suggestion: agentsMdExists ? void 0 : "Create AGENTS.md with project context for Codex",
4479
5069
  fix: agentsMdExists ? void 0 : {
4480
5070
  action: "create_file",
4481
- data: { file: "AGENTS.md" },
4482
- instruction: "Create AGENTS.md with project context for Codex."
5071
+ data: { file: "AGENTS.md" },
5072
+ instruction: "Create AGENTS.md with project context for Codex."
5073
+ }
5074
+ });
5075
+ const copilotInstructionsExists = existsSync3(join3(dir, ".github", "copilot-instructions.md"));
5076
+ checks.push({
5077
+ id: "copilot_instructions_exists",
5078
+ name: "Copilot instructions exist",
5079
+ category: "existence",
5080
+ maxPoints: POINTS_CLAUDE_MD_EXISTS,
5081
+ earnedPoints: copilotInstructionsExists ? POINTS_CLAUDE_MD_EXISTS : 0,
5082
+ passed: copilotInstructionsExists,
5083
+ detail: copilotInstructionsExists ? "Found at .github/copilot-instructions.md" : "Not found",
5084
+ suggestion: copilotInstructionsExists ? void 0 : "Create .github/copilot-instructions.md with project context for GitHub Copilot",
5085
+ fix: copilotInstructionsExists ? void 0 : {
5086
+ action: "create_file",
5087
+ data: { file: ".github/copilot-instructions.md" },
5088
+ instruction: "Create .github/copilot-instructions.md with project context for GitHub Copilot."
4483
5089
  }
4484
5090
  });
4485
- const claudeSkills = countFiles(join2(dir, ".claude", "skills"), /\.(md|SKILL\.md)$/);
4486
- const codexSkills = countFiles(join2(dir, ".agents", "skills"), /SKILL\.md$/);
5091
+ const claudeSkills = countFiles(join3(dir, ".claude", "skills"), /\.(md|SKILL\.md)$/);
5092
+ const codexSkills = countFiles(join3(dir, ".agents", "skills"), /SKILL\.md$/);
4487
5093
  const skillCount = claudeSkills.length + codexSkills.length;
4488
5094
  const skillBase = skillCount >= 1 ? POINTS_SKILLS_EXIST : 0;
4489
5095
  const skillBonus = Math.min((skillCount - 1) * POINTS_SKILLS_BONUS_PER_EXTRA, POINTS_SKILLS_BONUS_CAP);
@@ -4504,7 +5110,7 @@ function checkExistence(dir) {
4504
5110
  instruction: "Create .claude/skills/ with 2-3 project-specific workflow skills."
4505
5111
  } : void 0
4506
5112
  });
4507
- const mdcFiles = countFiles(join2(dir, ".cursor", "rules"), /\.mdc$/);
5113
+ const mdcFiles = countFiles(join3(dir, ".cursor", "rules"), /\.mdc$/);
4508
5114
  const mdcCount = mdcFiles.length;
4509
5115
  checks.push({
4510
5116
  id: "cursor_mdc_rules",
@@ -4547,297 +5153,24 @@ function checkExistence(dir) {
4547
5153
  maxPoints: POINTS_CROSS_PLATFORM_PARITY,
4548
5154
  earnedPoints: hasParity ? POINTS_CROSS_PLATFORM_PARITY : 0,
4549
5155
  passed: hasParity,
4550
- 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",
4551
- suggestion: hasParity ? void 0 : "Add configs for both platforms so all teammates get context",
4552
- fix: hasParity ? void 0 : {
4553
- action: "add_platform",
4554
- data: { hasClaude: hasClaudeConfigs, hasCursor: hasCursorConfigs },
4555
- instruction: hasClaudeConfigs ? "Add Cursor rules (.cursor/rules/) for cross-platform support." : "Add CLAUDE.md for cross-platform support."
4556
- }
4557
- });
4558
- return checks;
4559
- }
4560
-
4561
- // src/scoring/checks/quality.ts
4562
- import { join as join4 } from "path";
4563
-
4564
- // src/scoring/utils.ts
4565
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
4566
- import { join as join3, relative } from "path";
4567
- function readFileOrNull2(filePath) {
4568
- try {
4569
- return readFileSync3(filePath, "utf-8");
4570
- } catch {
4571
- return null;
4572
- }
4573
- }
4574
- var IGNORED_DIRS = /* @__PURE__ */ new Set([
4575
- "node_modules",
4576
- ".git",
4577
- "dist",
4578
- "build",
4579
- "out",
4580
- ".next",
4581
- ".nuxt",
4582
- "__pycache__",
4583
- ".venv",
4584
- "venv",
4585
- "env",
4586
- ".env",
4587
- "target",
4588
- "vendor",
4589
- ".cache",
4590
- ".parcel-cache",
4591
- "coverage",
4592
- ".nyc_output",
4593
- ".turbo",
4594
- ".caliber",
4595
- ".claude",
4596
- ".cursor",
4597
- ".agents",
4598
- ".codex"
4599
- ]);
4600
- var IGNORED_FILES = /* @__PURE__ */ new Set([
4601
- ".DS_Store",
4602
- "Thumbs.db",
4603
- ".gitignore",
4604
- ".editorconfig",
4605
- ".prettierrc",
4606
- ".prettierignore",
4607
- ".eslintignore",
4608
- "package-lock.json",
4609
- "yarn.lock",
4610
- "pnpm-lock.yaml",
4611
- "bun.lockb"
4612
- ]);
4613
- function collectProjectStructure(dir, maxDepth = 2) {
4614
- const dirs = [];
4615
- const files = [];
4616
- function walk(currentDir, depth) {
4617
- if (depth > maxDepth) return;
4618
- try {
4619
- const entries = readdirSync2(currentDir, { withFileTypes: true });
4620
- for (const entry of entries) {
4621
- const name = entry.name;
4622
- if (name.startsWith(".") && IGNORED_DIRS.has(name)) continue;
4623
- if (IGNORED_FILES.has(name)) continue;
4624
- const rel = relative(dir, join3(currentDir, name));
4625
- if (entry.isDirectory()) {
4626
- if (IGNORED_DIRS.has(name)) continue;
4627
- dirs.push(rel);
4628
- walk(join3(currentDir, name), depth + 1);
4629
- } else if (entry.isFile()) {
4630
- files.push(rel);
4631
- }
4632
- }
4633
- } catch {
4634
- }
4635
- }
4636
- walk(dir, 0);
4637
- return { dirs, files };
4638
- }
4639
- function collectPrimaryConfigContent(dir) {
4640
- const parts = [];
4641
- for (const file of ["CLAUDE.md", ".cursorrules", "AGENTS.md"]) {
4642
- const content = readFileOrNull2(join3(dir, file));
4643
- if (content) parts.push(content);
4644
- }
4645
- try {
4646
- const rulesDir = join3(dir, ".cursor", "rules");
4647
- const mdcFiles = readdirSync2(rulesDir).filter((f) => f.endsWith(".mdc"));
4648
- for (const f of mdcFiles) {
4649
- const content = readFileOrNull2(join3(rulesDir, f));
4650
- if (content) parts.push(content);
4651
- }
4652
- } catch {
4653
- }
4654
- return parts.join("\n");
4655
- }
4656
- function collectAllConfigContent(dir) {
4657
- const parts = [collectPrimaryConfigContent(dir)];
4658
- for (const skillsDir of [join3(dir, ".claude", "skills"), join3(dir, ".agents", "skills")]) {
4659
- try {
4660
- const entries = readdirSync2(skillsDir, { withFileTypes: true });
4661
- for (const entry of entries) {
4662
- if (entry.isDirectory()) {
4663
- const skill = readFileOrNull2(join3(skillsDir, entry.name, "SKILL.md"));
4664
- if (skill) parts.push(skill);
4665
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
4666
- const content = readFileOrNull2(join3(skillsDir, entry.name));
4667
- if (content) parts.push(content);
4668
- }
4669
- }
4670
- } catch {
4671
- }
4672
- }
4673
- return parts.join("\n");
4674
- }
4675
- function estimateTokens2(text) {
4676
- return Math.ceil(text.length / 4);
4677
- }
4678
- function analyzeMarkdownStructure(content) {
4679
- const lines = content.split("\n");
4680
- let headingCount = 0;
4681
- let h2Count = 0;
4682
- let h3Count = 0;
4683
- let codeBlockCount = 0;
4684
- let codeBlockLines = 0;
4685
- let listItemCount = 0;
4686
- let inlineCodeCount = 0;
4687
- let inCodeBlock = false;
4688
- for (const line of lines) {
4689
- const trimmed = line.trim();
4690
- if (trimmed.startsWith("```")) {
4691
- if (!inCodeBlock) codeBlockCount++;
4692
- inCodeBlock = !inCodeBlock;
4693
- continue;
4694
- }
4695
- if (inCodeBlock) {
4696
- codeBlockLines++;
4697
- continue;
4698
- }
4699
- if (trimmed.startsWith("## ") && !trimmed.startsWith("### ")) h2Count++;
4700
- if (trimmed.startsWith("### ")) h3Count++;
4701
- if (trimmed.startsWith("#")) headingCount++;
4702
- if (/^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) listItemCount++;
4703
- const inlineMatches = trimmed.match(/`[^`]+`/g);
4704
- if (inlineMatches) inlineCodeCount += inlineMatches.length;
4705
- }
4706
- return {
4707
- headingCount,
4708
- h2Count,
4709
- h3Count,
4710
- codeBlockCount,
4711
- codeBlockLines,
4712
- listItemCount,
4713
- inlineCodeCount,
4714
- totalLines: lines.length,
4715
- nonEmptyLines: lines.filter((l) => l.trim().length > 0).length
4716
- };
4717
- }
4718
- function extractReferences(content) {
4719
- const refs = /* @__PURE__ */ new Set();
4720
- const backtickPattern = /`([^`]+)`/g;
4721
- let match;
4722
- while ((match = backtickPattern.exec(content)) !== null) {
4723
- const term = match[1].trim();
4724
- if ((term.includes("/") || /\.\w{1,5}$/.test(term)) && !term.startsWith("-") && term.length < 200) {
4725
- if (term.startsWith("@") && (term.match(/\//g) || []).length === 1) continue;
4726
- if (term.includes(" ")) continue;
4727
- if (/^\d+\.\d+/.test(term)) continue;
4728
- if (term.includes("/") && !/\.\w{1,5}$/.test(term)) {
4729
- if (term !== term.toLowerCase() && !/^[a-z]/.test(term)) continue;
4730
- const segments = term.split("/");
4731
- if (segments.every((s) => s.length <= 3)) continue;
4732
- }
4733
- const cleaned = term.replace(/[,;:!?)]+$/, "");
4734
- if (cleaned.length > 1) refs.add(cleaned);
4735
- }
4736
- }
4737
- const pathPattern = /(?:^|\s)((?:[a-zA-Z0-9_@.-]+\/)+[a-zA-Z0-9_.*-]+\.[a-zA-Z]{1,5})/gm;
4738
- while ((match = pathPattern.exec(content)) !== null) {
4739
- const term = match[1].trim();
4740
- if (term.length > 2 && term.length < 200) {
4741
- if (term.startsWith("@") && (term.match(/\//g) || []).length === 1) continue;
4742
- const cleaned = term.replace(/[,;:!?)]+$/, "");
4743
- if (cleaned.length > 1) refs.add(cleaned);
4744
- }
4745
- }
4746
- return Array.from(refs);
4747
- }
4748
- function validateFileReferences(content, dir, checkExists = existsSync3) {
4749
- const refs = extractReferences(content);
4750
- const valid = [];
4751
- const invalid = [];
4752
- for (const ref of refs) {
4753
- if (/^https?:\/\//.test(ref)) continue;
4754
- if (/^\d+\.\d+/.test(ref)) continue;
4755
- if (ref.startsWith("#") || ref.startsWith("@")) continue;
4756
- if (ref.includes("*") || ref.includes("..")) continue;
4757
- if (!ref.includes("/") && !ref.includes(".")) continue;
4758
- const fullPath = join3(dir, ref);
4759
- if (checkExists(fullPath)) {
4760
- valid.push(ref);
4761
- } else {
4762
- const withoutTrailing = ref.replace(/\/+$/, "");
4763
- if (withoutTrailing !== ref && checkExists(join3(dir, withoutTrailing))) {
4764
- valid.push(ref);
4765
- } else {
4766
- invalid.push(ref);
4767
- }
4768
- }
4769
- }
4770
- return { valid, invalid, total: valid.length + invalid.length };
4771
- }
4772
- function countConcreteness(content) {
4773
- let concrete = 0;
4774
- let abstract = 0;
4775
- let inCodeBlock = false;
4776
- for (const line of content.split("\n")) {
4777
- if (line.trim().startsWith("```")) {
4778
- inCodeBlock = !inCodeBlock;
4779
- continue;
4780
- }
4781
- const classification = classifyLine(line, inCodeBlock);
4782
- if (classification === "concrete") concrete++;
4783
- else if (classification === "abstract") abstract++;
4784
- }
4785
- return { concrete, abstract };
4786
- }
4787
- function countTreeLines(content) {
4788
- const treeLinePattern = /[├└│─┬]/;
4789
- let count = 0;
4790
- let inCodeBlock = false;
4791
- for (const line of content.split("\n")) {
4792
- if (line.trim().startsWith("```")) {
4793
- inCodeBlock = !inCodeBlock;
4794
- continue;
4795
- }
4796
- if (inCodeBlock && treeLinePattern.test(line)) count++;
4797
- }
4798
- return count;
4799
- }
4800
- function calculateDuplicatePercent(content1, content2) {
4801
- const lines1 = new Set(content1.split("\n").map((l) => l.trim()).filter((l) => l.length > 10));
4802
- const lines2 = content2.split("\n").map((l) => l.trim()).filter((l) => l.length > 10);
4803
- const overlapping = lines2.filter((l) => lines1.has(l)).length;
4804
- return lines2.length > 0 ? Math.round(overlapping / lines2.length * 100) : 0;
4805
- }
4806
- function calculateDensityPoints(density, maxPoints) {
4807
- if (density >= 40) return maxPoints;
4808
- if (density >= 25) return Math.round(maxPoints * 0.75);
4809
- if (density >= 15) return Math.round(maxPoints * 0.5);
4810
- if (density >= 5) return Math.round(maxPoints * 0.25);
4811
- return 0;
4812
- }
4813
- function isEntryMentioned(entry, contentLower) {
4814
- const entryLower = entry.toLowerCase();
4815
- const variants = [entryLower, entryLower.replace(/\\/g, "/")];
4816
- const lastSegment = entry.split("/").pop()?.toLowerCase();
4817
- if (lastSegment && lastSegment.length > 3) variants.push(lastSegment);
4818
- return variants.some((v) => {
4819
- const escaped = v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4820
- return new RegExp(`(?:^|[\\s\`/"'\\.,(])${escaped}(?:[\\s\`/"'.,;:!?)\\\\]|$)`, "i").test(contentLower);
5156
+ 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",
5157
+ suggestion: hasParity ? void 0 : "Add configs for both platforms so all teammates get context",
5158
+ fix: hasParity ? void 0 : {
5159
+ action: "add_platform",
5160
+ data: { hasClaude: hasClaudeConfigs, hasCursor: hasCursorConfigs },
5161
+ instruction: hasClaudeConfigs ? "Add Cursor rules (.cursor/rules/) for cross-platform support." : "Add CLAUDE.md for cross-platform support."
5162
+ }
4821
5163
  });
4822
- }
4823
- function classifyLine(line, inCodeBlock) {
4824
- if (inCodeBlock) return "concrete";
4825
- const trimmed = line.trim();
4826
- if (trimmed.length === 0) return "neutral";
4827
- if (trimmed.startsWith("#")) return "neutral";
4828
- if (/`[^`]+`/.test(trimmed)) return "concrete";
4829
- if (/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+\.[a-zA-Z]{1,5}/.test(trimmed)) return "concrete";
4830
- if (/[a-zA-Z0-9_]{4,}\/[a-zA-Z0-9_.-]/.test(trimmed)) return "concrete";
4831
- 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";
4832
- return "abstract";
5164
+ return checks;
4833
5165
  }
4834
5166
 
4835
5167
  // src/scoring/checks/quality.ts
5168
+ import { join as join4 } from "path";
4836
5169
  function checkQuality(dir) {
4837
5170
  const checks = [];
4838
- const claudeMd = readFileOrNull2(join4(dir, "CLAUDE.md"));
4839
- const cursorrules = readFileOrNull2(join4(dir, ".cursorrules"));
4840
- const agentsMd = readFileOrNull2(join4(dir, "AGENTS.md"));
5171
+ const claudeMd = readFileOrNull(join4(dir, "CLAUDE.md"));
5172
+ const cursorrules = readFileOrNull(join4(dir, ".cursorrules"));
5173
+ const agentsMd = readFileOrNull(join4(dir, "AGENTS.md"));
4841
5174
  const allContent = [claudeMd, cursorrules, agentsMd].filter(Boolean);
4842
5175
  const combinedContent = allContent.join("\n");
4843
5176
  const primaryInstructions = claudeMd ?? agentsMd ?? cursorrules;
@@ -5253,7 +5586,7 @@ function checkFreshness(dir) {
5253
5586
  ];
5254
5587
  const secretFindings = [];
5255
5588
  for (const rel of filesToScan) {
5256
- const content = readFileOrNull2(join6(dir, rel));
5589
+ const content = readFileOrNull(join6(dir, rel));
5257
5590
  if (!content) continue;
5258
5591
  const lines = content.split("\n");
5259
5592
  for (let i = 0; i < lines.length; i++) {
@@ -5287,7 +5620,7 @@ function checkFreshness(dir) {
5287
5620
  const settingsPath = join6(dir, ".claude", "settings.json");
5288
5621
  let hasPermissions = false;
5289
5622
  let permissionDetail = "";
5290
- const settingsContent = readFileOrNull2(settingsPath);
5623
+ const settingsContent = readFileOrNull(settingsPath);
5291
5624
  if (settingsContent) {
5292
5625
  try {
5293
5626
  const settings = JSON.parse(settingsContent);
@@ -5327,7 +5660,7 @@ function hasPreCommitHook(dir) {
5327
5660
  try {
5328
5661
  const gitDir = execSync11("git rev-parse --git-dir", { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
5329
5662
  const hookPath = join7(gitDir, "hooks", "pre-commit");
5330
- const content = readFileOrNull2(hookPath);
5663
+ const content = readFileOrNull(hookPath);
5331
5664
  return content ? content.includes("caliber") : false;
5332
5665
  } catch {
5333
5666
  return false;
@@ -5338,7 +5671,7 @@ function checkBonus(dir) {
5338
5671
  let hasClaudeHooks = false;
5339
5672
  let hasPrecommit = false;
5340
5673
  const hookSources = [];
5341
- const settingsContent = readFileOrNull2(join7(dir, ".claude", "settings.json"));
5674
+ const settingsContent = readFileOrNull(join7(dir, ".claude", "settings.json"));
5342
5675
  if (settingsContent) {
5343
5676
  try {
5344
5677
  const settings = JSON.parse(settingsContent);
@@ -5393,7 +5726,7 @@ function checkBonus(dir) {
5393
5726
  const entries = readdirSync3(skillsDir, { withFileTypes: true });
5394
5727
  for (const entry of entries) {
5395
5728
  if (entry.isDirectory()) {
5396
- const skillMd = readFileOrNull2(join7(skillsDir, entry.name, "SKILL.md"));
5729
+ const skillMd = readFileOrNull(join7(skillsDir, entry.name, "SKILL.md"));
5397
5730
  if (skillMd) {
5398
5731
  totalSkillFiles++;
5399
5732
  if (skillMd.trimStart().startsWith("---")) {
@@ -5422,7 +5755,7 @@ function checkBonus(dir) {
5422
5755
  instruction: "Migrate flat skill files to .claude/skills/{name}/SKILL.md with YAML frontmatter."
5423
5756
  } : void 0
5424
5757
  });
5425
- const learningsContent = readFileOrNull2(join7(dir, "CALIBER_LEARNINGS.md"));
5758
+ const learningsContent = readFileOrNull(join7(dir, "CALIBER_LEARNINGS.md"));
5426
5759
  const hasLearned = learningsContent ? learningsContent.split("\n").filter((l) => l.startsWith("- ")).length > 0 : false;
5427
5760
  checks.push({
5428
5761
  id: "learned_content",
@@ -5437,24 +5770,62 @@ function checkBonus(dir) {
5437
5770
  return checks;
5438
5771
  }
5439
5772
 
5773
+ // src/scoring/checks/sources.ts
5774
+ import { join as join8 } from "path";
5775
+ function checkSources(dir) {
5776
+ const checks = [];
5777
+ const configSources = loadSourcesConfig(dir);
5778
+ const hasSources = configSources.length > 0;
5779
+ checks.push({
5780
+ id: "sources_configured",
5781
+ name: "External sources configured",
5782
+ category: "bonus",
5783
+ maxPoints: hasSources ? POINTS_SOURCES_CONFIGURED : 0,
5784
+ earnedPoints: hasSources ? POINTS_SOURCES_CONFIGURED : 0,
5785
+ passed: hasSources,
5786
+ detail: hasSources ? `${configSources.length} source${configSources.length === 1 ? "" : "s"} configured` : "No external sources configured",
5787
+ suggestion: hasSources ? void 0 : "Run `caliber sources add <path>` to add related repos or docs"
5788
+ });
5789
+ if (hasSources) {
5790
+ const claudeMd = readFileOrNull(join8(dir, "CLAUDE.md"));
5791
+ const content = claudeMd?.toLowerCase() || "";
5792
+ const referenced = configSources.some((source) => {
5793
+ if (!source.path) return false;
5794
+ const name = source.path.split("/").pop()?.toLowerCase() || "";
5795
+ return name.length > 2 && content.includes(name);
5796
+ });
5797
+ checks.push({
5798
+ id: "sources_referenced",
5799
+ name: "Sources referenced in config",
5800
+ category: "grounding",
5801
+ maxPoints: POINTS_SOURCES_REFERENCED,
5802
+ earnedPoints: referenced ? POINTS_SOURCES_REFERENCED : 0,
5803
+ passed: referenced,
5804
+ detail: referenced ? "At least one source is referenced in CLAUDE.md" : "No configured sources are mentioned in CLAUDE.md",
5805
+ suggestion: referenced ? void 0 : "Regenerate with `caliber init` to include source context in your config"
5806
+ });
5807
+ }
5808
+ return checks;
5809
+ }
5810
+
5440
5811
  // src/scoring/dismissed.ts
5441
5812
  init_constants();
5442
- import fs20 from "fs";
5443
- import path16 from "path";
5444
- var DISMISSED_FILE = path16.join(CALIBER_DIR, "dismissed-checks.json");
5813
+ import fs22 from "fs";
5814
+ import path18 from "path";
5815
+ var DISMISSED_FILE = path18.join(CALIBER_DIR, "dismissed-checks.json");
5445
5816
  function readDismissedChecks() {
5446
5817
  try {
5447
- if (!fs20.existsSync(DISMISSED_FILE)) return [];
5448
- return JSON.parse(fs20.readFileSync(DISMISSED_FILE, "utf-8"));
5818
+ if (!fs22.existsSync(DISMISSED_FILE)) return [];
5819
+ return JSON.parse(fs22.readFileSync(DISMISSED_FILE, "utf-8"));
5449
5820
  } catch {
5450
5821
  return [];
5451
5822
  }
5452
5823
  }
5453
5824
  function writeDismissedChecks(checks) {
5454
- if (!fs20.existsSync(CALIBER_DIR)) {
5455
- fs20.mkdirSync(CALIBER_DIR, { recursive: true });
5825
+ if (!fs22.existsSync(CALIBER_DIR)) {
5826
+ fs22.mkdirSync(CALIBER_DIR, { recursive: true });
5456
5827
  }
5457
- fs20.writeFileSync(DISMISSED_FILE, JSON.stringify(checks, null, 2) + "\n");
5828
+ fs22.writeFileSync(DISMISSED_FILE, JSON.stringify(checks, null, 2) + "\n");
5458
5829
  }
5459
5830
  function getDismissedIds() {
5460
5831
  return new Set(readDismissedChecks().map((c) => c.id));
@@ -5473,6 +5844,7 @@ function filterChecksForTarget(checks, target) {
5473
5844
  if (CLAUDE_ONLY_CHECKS.has(c.id)) return target.includes("claude");
5474
5845
  if (CURSOR_ONLY_CHECKS.has(c.id)) return target.includes("cursor");
5475
5846
  if (CODEX_ONLY_CHECKS.has(c.id)) return target.includes("codex");
5847
+ if (COPILOT_ONLY_CHECKS.has(c.id)) return target.includes("github-copilot");
5476
5848
  if (BOTH_ONLY_CHECKS.has(c.id)) return target.includes("claude") && target.includes("cursor");
5477
5849
  if (NON_CODEX_CHECKS.has(c.id)) return !target.includes("codex");
5478
5850
  return true;
@@ -5480,9 +5852,10 @@ function filterChecksForTarget(checks, target) {
5480
5852
  }
5481
5853
  function detectTargetAgent(dir) {
5482
5854
  const agents = [];
5483
- if (existsSync7(join8(dir, "CLAUDE.md")) || existsSync7(join8(dir, ".claude", "skills"))) agents.push("claude");
5484
- if (existsSync7(join8(dir, ".cursorrules")) || existsSync7(join8(dir, ".cursor", "rules"))) agents.push("cursor");
5485
- if (existsSync7(join8(dir, ".codex")) || existsSync7(join8(dir, ".agents", "skills"))) agents.push("codex");
5855
+ if (existsSync7(join9(dir, "CLAUDE.md")) || existsSync7(join9(dir, ".claude", "skills"))) agents.push("claude");
5856
+ if (existsSync7(join9(dir, ".cursorrules")) || existsSync7(join9(dir, ".cursor", "rules"))) agents.push("cursor");
5857
+ if (existsSync7(join9(dir, ".codex")) || existsSync7(join9(dir, ".agents", "skills"))) agents.push("codex");
5858
+ if (existsSync7(join9(dir, ".github", "copilot-instructions.md"))) agents.push("github-copilot");
5486
5859
  return agents.length > 0 ? agents : ["claude"];
5487
5860
  }
5488
5861
  function computeLocalScore(dir, targetAgent) {
@@ -5493,7 +5866,8 @@ function computeLocalScore(dir, targetAgent) {
5493
5866
  ...checkGrounding(dir),
5494
5867
  ...checkAccuracy(dir),
5495
5868
  ...checkFreshness(dir),
5496
- ...checkBonus(dir)
5869
+ ...checkBonus(dir),
5870
+ ...checkSources(dir)
5497
5871
  ];
5498
5872
  const dismissed = getDismissedIds();
5499
5873
  const checks = filterChecksForTarget(allChecks, target).filter((c) => !dismissed.has(c.id));
@@ -5696,16 +6070,16 @@ import chalk6 from "chalk";
5696
6070
  import ora from "ora";
5697
6071
  import select3 from "@inquirer/select";
5698
6072
  import { mkdirSync, readFileSync as readFileSync4, readdirSync as readdirSync4, existsSync as existsSync8, writeFileSync } from "fs";
5699
- import { join as join9, dirname as dirname2 } from "path";
6073
+ import { join as join10, dirname as dirname2 } from "path";
5700
6074
 
5701
6075
  // src/scanner/index.ts
5702
- import fs21 from "fs";
5703
- import path17 from "path";
6076
+ import fs23 from "fs";
6077
+ import path19 from "path";
5704
6078
  import crypto3 from "crypto";
5705
6079
  function scanLocalState(dir) {
5706
6080
  const items = [];
5707
- const claudeMdPath = path17.join(dir, "CLAUDE.md");
5708
- if (fs21.existsSync(claudeMdPath)) {
6081
+ const claudeMdPath = path19.join(dir, "CLAUDE.md");
6082
+ if (fs23.existsSync(claudeMdPath)) {
5709
6083
  items.push({
5710
6084
  type: "rule",
5711
6085
  platform: "claude",
@@ -5714,10 +6088,10 @@ function scanLocalState(dir) {
5714
6088
  path: claudeMdPath
5715
6089
  });
5716
6090
  }
5717
- const skillsDir = path17.join(dir, ".claude", "skills");
5718
- if (fs21.existsSync(skillsDir)) {
5719
- for (const file of fs21.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
5720
- const filePath = path17.join(skillsDir, file);
6091
+ const skillsDir = path19.join(dir, ".claude", "skills");
6092
+ if (fs23.existsSync(skillsDir)) {
6093
+ for (const file of fs23.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
6094
+ const filePath = path19.join(skillsDir, file);
5721
6095
  items.push({
5722
6096
  type: "skill",
5723
6097
  platform: "claude",
@@ -5727,10 +6101,10 @@ function scanLocalState(dir) {
5727
6101
  });
5728
6102
  }
5729
6103
  }
5730
- const mcpJsonPath = path17.join(dir, ".mcp.json");
5731
- if (fs21.existsSync(mcpJsonPath)) {
6104
+ const mcpJsonPath = path19.join(dir, ".mcp.json");
6105
+ if (fs23.existsSync(mcpJsonPath)) {
5732
6106
  try {
5733
- const mcpJson = JSON.parse(fs21.readFileSync(mcpJsonPath, "utf-8"));
6107
+ const mcpJson = JSON.parse(fs23.readFileSync(mcpJsonPath, "utf-8"));
5734
6108
  if (mcpJson.mcpServers) {
5735
6109
  for (const name of Object.keys(mcpJson.mcpServers)) {
5736
6110
  items.push({
@@ -5745,8 +6119,8 @@ function scanLocalState(dir) {
5745
6119
  } catch {
5746
6120
  }
5747
6121
  }
5748
- const agentsMdPath = path17.join(dir, "AGENTS.md");
5749
- if (fs21.existsSync(agentsMdPath)) {
6122
+ const agentsMdPath = path19.join(dir, "AGENTS.md");
6123
+ if (fs23.existsSync(agentsMdPath)) {
5750
6124
  items.push({
5751
6125
  type: "rule",
5752
6126
  platform: "codex",
@@ -5755,12 +6129,12 @@ function scanLocalState(dir) {
5755
6129
  path: agentsMdPath
5756
6130
  });
5757
6131
  }
5758
- const codexSkillsDir = path17.join(dir, ".agents", "skills");
5759
- if (fs21.existsSync(codexSkillsDir)) {
6132
+ const codexSkillsDir = path19.join(dir, ".agents", "skills");
6133
+ if (fs23.existsSync(codexSkillsDir)) {
5760
6134
  try {
5761
- for (const name of fs21.readdirSync(codexSkillsDir)) {
5762
- const skillFile = path17.join(codexSkillsDir, name, "SKILL.md");
5763
- if (fs21.existsSync(skillFile)) {
6135
+ for (const name of fs23.readdirSync(codexSkillsDir)) {
6136
+ const skillFile = path19.join(codexSkillsDir, name, "SKILL.md");
6137
+ if (fs23.existsSync(skillFile)) {
5764
6138
  items.push({
5765
6139
  type: "skill",
5766
6140
  platform: "codex",
@@ -5773,8 +6147,8 @@ function scanLocalState(dir) {
5773
6147
  } catch {
5774
6148
  }
5775
6149
  }
5776
- const cursorrulesPath = path17.join(dir, ".cursorrules");
5777
- if (fs21.existsSync(cursorrulesPath)) {
6150
+ const cursorrulesPath = path19.join(dir, ".cursorrules");
6151
+ if (fs23.existsSync(cursorrulesPath)) {
5778
6152
  items.push({
5779
6153
  type: "rule",
5780
6154
  platform: "cursor",
@@ -5783,10 +6157,10 @@ function scanLocalState(dir) {
5783
6157
  path: cursorrulesPath
5784
6158
  });
5785
6159
  }
5786
- const cursorRulesDir = path17.join(dir, ".cursor", "rules");
5787
- if (fs21.existsSync(cursorRulesDir)) {
5788
- for (const file of fs21.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"))) {
5789
- const filePath = path17.join(cursorRulesDir, file);
6160
+ const cursorRulesDir = path19.join(dir, ".cursor", "rules");
6161
+ if (fs23.existsSync(cursorRulesDir)) {
6162
+ for (const file of fs23.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"))) {
6163
+ const filePath = path19.join(cursorRulesDir, file);
5790
6164
  items.push({
5791
6165
  type: "rule",
5792
6166
  platform: "cursor",
@@ -5796,12 +6170,12 @@ function scanLocalState(dir) {
5796
6170
  });
5797
6171
  }
5798
6172
  }
5799
- const cursorSkillsDir = path17.join(dir, ".cursor", "skills");
5800
- if (fs21.existsSync(cursorSkillsDir)) {
6173
+ const cursorSkillsDir = path19.join(dir, ".cursor", "skills");
6174
+ if (fs23.existsSync(cursorSkillsDir)) {
5801
6175
  try {
5802
- for (const name of fs21.readdirSync(cursorSkillsDir)) {
5803
- const skillFile = path17.join(cursorSkillsDir, name, "SKILL.md");
5804
- if (fs21.existsSync(skillFile)) {
6176
+ for (const name of fs23.readdirSync(cursorSkillsDir)) {
6177
+ const skillFile = path19.join(cursorSkillsDir, name, "SKILL.md");
6178
+ if (fs23.existsSync(skillFile)) {
5805
6179
  items.push({
5806
6180
  type: "skill",
5807
6181
  platform: "cursor",
@@ -5814,10 +6188,10 @@ function scanLocalState(dir) {
5814
6188
  } catch {
5815
6189
  }
5816
6190
  }
5817
- const cursorMcpPath = path17.join(dir, ".cursor", "mcp.json");
5818
- if (fs21.existsSync(cursorMcpPath)) {
6191
+ const cursorMcpPath = path19.join(dir, ".cursor", "mcp.json");
6192
+ if (fs23.existsSync(cursorMcpPath)) {
5819
6193
  try {
5820
- const mcpJson = JSON.parse(fs21.readFileSync(cursorMcpPath, "utf-8"));
6194
+ const mcpJson = JSON.parse(fs23.readFileSync(cursorMcpPath, "utf-8"));
5821
6195
  if (mcpJson.mcpServers) {
5822
6196
  for (const name of Object.keys(mcpJson.mcpServers)) {
5823
6197
  items.push({
@@ -5835,7 +6209,7 @@ function scanLocalState(dir) {
5835
6209
  return items;
5836
6210
  }
5837
6211
  function hashFile(filePath) {
5838
- const text = fs21.readFileSync(filePath, "utf-8");
6212
+ const text = fs23.readFileSync(filePath, "utf-8");
5839
6213
  return crypto3.createHash("sha256").update(JSON.stringify({ text })).digest("hex");
5840
6214
  }
5841
6215
  function hashJson(obj) {
@@ -5850,27 +6224,27 @@ import { PostHog } from "posthog-node";
5850
6224
  import chalk5 from "chalk";
5851
6225
 
5852
6226
  // src/telemetry/config.ts
5853
- import fs22 from "fs";
5854
- import path18 from "path";
6227
+ import fs24 from "fs";
6228
+ import path20 from "path";
5855
6229
  import os4 from "os";
5856
6230
  import crypto4 from "crypto";
5857
6231
  import { execSync as execSync12 } from "child_process";
5858
- var CONFIG_DIR2 = path18.join(os4.homedir(), ".caliber");
5859
- var CONFIG_FILE2 = path18.join(CONFIG_DIR2, "config.json");
6232
+ var CONFIG_DIR2 = path20.join(os4.homedir(), ".caliber");
6233
+ var CONFIG_FILE2 = path20.join(CONFIG_DIR2, "config.json");
5860
6234
  var runtimeDisabled = false;
5861
6235
  function readConfig() {
5862
6236
  try {
5863
- if (!fs22.existsSync(CONFIG_FILE2)) return {};
5864
- return JSON.parse(fs22.readFileSync(CONFIG_FILE2, "utf-8"));
6237
+ if (!fs24.existsSync(CONFIG_FILE2)) return {};
6238
+ return JSON.parse(fs24.readFileSync(CONFIG_FILE2, "utf-8"));
5865
6239
  } catch {
5866
6240
  return {};
5867
6241
  }
5868
6242
  }
5869
6243
  function writeConfig(config) {
5870
- if (!fs22.existsSync(CONFIG_DIR2)) {
5871
- fs22.mkdirSync(CONFIG_DIR2, { recursive: true });
6244
+ if (!fs24.existsSync(CONFIG_DIR2)) {
6245
+ fs24.mkdirSync(CONFIG_DIR2, { recursive: true });
5872
6246
  }
5873
- fs22.writeFileSync(CONFIG_FILE2, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
6247
+ fs24.writeFileSync(CONFIG_FILE2, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
5874
6248
  }
5875
6249
  function getMachineId() {
5876
6250
  const config = readConfig();
@@ -6054,17 +6428,17 @@ function detectLocalPlatforms() {
6054
6428
  }
6055
6429
  function getSkillPath(platform, slug) {
6056
6430
  if (platform === "cursor") {
6057
- return join9(".cursor", "skills", slug, "SKILL.md");
6431
+ return join10(".cursor", "skills", slug, "SKILL.md");
6058
6432
  }
6059
6433
  if (platform === "codex") {
6060
- return join9(".agents", "skills", slug, "SKILL.md");
6434
+ return join10(".agents", "skills", slug, "SKILL.md");
6061
6435
  }
6062
- return join9(".claude", "skills", slug, "SKILL.md");
6436
+ return join10(".claude", "skills", slug, "SKILL.md");
6063
6437
  }
6064
6438
  function getSkillDir(platform) {
6065
- if (platform === "cursor") return join9(process.cwd(), ".cursor", "skills");
6066
- if (platform === "codex") return join9(process.cwd(), ".agents", "skills");
6067
- return join9(process.cwd(), ".claude", "skills");
6439
+ if (platform === "cursor") return join10(process.cwd(), ".cursor", "skills");
6440
+ if (platform === "codex") return join10(process.cwd(), ".agents", "skills");
6441
+ return join10(process.cwd(), ".claude", "skills");
6068
6442
  }
6069
6443
  function getInstalledSkills(platforms) {
6070
6444
  const installed = /* @__PURE__ */ new Set();
@@ -6235,7 +6609,7 @@ Already installed skills: ${Array.from(installed).join(", ")}`);
6235
6609
  return parts.join("\n");
6236
6610
  }
6237
6611
  function extractTopDeps() {
6238
- const pkgPath = join9(process.cwd(), "package.json");
6612
+ const pkgPath = join10(process.cwd(), "package.json");
6239
6613
  if (!existsSync8(pkgPath)) return [];
6240
6614
  try {
6241
6615
  const pkg3 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
@@ -6574,7 +6948,7 @@ async function installSkills(recs, platforms, contentMap) {
6574
6948
  if (!content) continue;
6575
6949
  for (const platform of platforms) {
6576
6950
  const skillPath = getSkillPath(platform, rec.slug);
6577
- const fullPath = join9(process.cwd(), skillPath);
6951
+ const fullPath = join10(process.cwd(), skillPath);
6578
6952
  mkdirSync(dirname2(fullPath), { recursive: true });
6579
6953
  writeFileSync(fullPath, content, "utf-8");
6580
6954
  installed.push(`[${platform}] ${skillPath}`);
@@ -6829,11 +7203,11 @@ function countIssuePoints(issues) {
6829
7203
  }
6830
7204
  async function scoreAndRefine(setup, dir, sessionHistory, callbacks) {
6831
7205
  const existsCache = /* @__PURE__ */ new Map();
6832
- const cachedExists = (path33) => {
6833
- const cached = existsCache.get(path33);
7206
+ const cachedExists = (path37) => {
7207
+ const cached = existsCache.get(path37);
6834
7208
  if (cached !== void 0) return cached;
6835
- const result = existsSync9(path33);
6836
- existsCache.set(path33, result);
7209
+ const result = existsSync9(path37);
7210
+ existsCache.set(path37, result);
6837
7211
  return result;
6838
7212
  };
6839
7213
  const projectStructure = collectProjectStructure(dir);
@@ -6972,8 +7346,8 @@ async function runScoreRefineWithSpinner(setup, dir, sessionHistory) {
6972
7346
  }
6973
7347
 
6974
7348
  // src/lib/debug-report.ts
6975
- import fs23 from "fs";
6976
- import path19 from "path";
7349
+ import fs25 from "fs";
7350
+ import path21 from "path";
6977
7351
  var DebugReport = class {
6978
7352
  sections = [];
6979
7353
  startTime;
@@ -7042,11 +7416,11 @@ var DebugReport = class {
7042
7416
  lines.push(`| **Total** | **${formatMs(totalMs)}** |`);
7043
7417
  lines.push("");
7044
7418
  }
7045
- const dir = path19.dirname(outputPath);
7046
- if (!fs23.existsSync(dir)) {
7047
- fs23.mkdirSync(dir, { recursive: true });
7419
+ const dir = path21.dirname(outputPath);
7420
+ if (!fs25.existsSync(dir)) {
7421
+ fs25.mkdirSync(dir, { recursive: true });
7048
7422
  }
7049
- fs23.writeFileSync(outputPath, lines.join("\n"));
7423
+ fs25.writeFileSync(outputPath, lines.join("\n"));
7050
7424
  }
7051
7425
  };
7052
7426
  function formatMs(ms) {
@@ -7447,7 +7821,7 @@ import chalk11 from "chalk";
7447
7821
  import ora3 from "ora";
7448
7822
  import select5 from "@inquirer/select";
7449
7823
  import checkbox from "@inquirer/checkbox";
7450
- import fs26 from "fs";
7824
+ import fs28 from "fs";
7451
7825
 
7452
7826
  // src/ai/refine.ts
7453
7827
  async function refineSetup(currentSetup, message, conversationHistory, callbacks) {
@@ -7588,16 +7962,18 @@ init_config();
7588
7962
  init_review();
7589
7963
  function detectAgents(dir) {
7590
7964
  const agents = [];
7591
- if (fs26.existsSync(`${dir}/.claude`)) agents.push("claude");
7592
- if (fs26.existsSync(`${dir}/.cursor`)) agents.push("cursor");
7593
- if (fs26.existsSync(`${dir}/.agents`) || fs26.existsSync(`${dir}/AGENTS.md`)) agents.push("codex");
7965
+ if (fs28.existsSync(`${dir}/.claude`)) agents.push("claude");
7966
+ if (fs28.existsSync(`${dir}/.cursor`)) agents.push("cursor");
7967
+ if (fs28.existsSync(`${dir}/.agents`) || fs28.existsSync(`${dir}/AGENTS.md`)) agents.push("codex");
7968
+ if (fs28.existsSync(`${dir}/.github/copilot-instructions.md`)) agents.push("github-copilot");
7594
7969
  return agents;
7595
7970
  }
7596
7971
  async function promptAgent(detected) {
7597
7972
  const choices = [
7598
7973
  { name: "Claude Code", value: "claude", checked: detected?.includes("claude") ?? false },
7599
7974
  { name: "Cursor", value: "cursor", checked: detected?.includes("cursor") ?? false },
7600
- { name: "Codex (OpenAI)", value: "codex", checked: detected?.includes("codex") ?? false }
7975
+ { name: "Codex (OpenAI)", value: "codex", checked: detected?.includes("codex") ?? false },
7976
+ { name: "GitHub Copilot", value: "github-copilot", checked: detected?.includes("github-copilot") ?? false }
7601
7977
  ];
7602
7978
  const hasDefaults = detected && detected.length > 0;
7603
7979
  const message = hasDefaults ? "Detected agents (press Enter to confirm, or toggle with space)" : "Which coding agents do you use? (toggle with space)";
@@ -7671,7 +8047,7 @@ async function classifyRefineIntent(message) {
7671
8047
  const fastModel = getFastModel();
7672
8048
  try {
7673
8049
  const result = await llmJsonCall({
7674
- system: `You classify whether a user message is a valid request to modify AI agent config files (CLAUDE.md, .cursorrules, skills).
8050
+ system: `You classify whether a user message is a valid request to modify AI agent config files (CLAUDE.md, .cursorrules, copilot-instructions.md, skills).
7675
8051
  Valid: requests to add, remove, change, or restructure config content. Examples: "add testing commands", "remove the terraform section", "make CLAUDE.md shorter".
7676
8052
  Invalid: questions, requests to show/display something, general chat, or anything that isn't a concrete config change.
7677
8053
  Return {"valid": true} or {"valid": false}. Nothing else.`,
@@ -7725,18 +8101,18 @@ async function refineLoop(currentSetup, sessionHistory, summarizeSetup2, printSu
7725
8101
 
7726
8102
  // src/commands/init-display.ts
7727
8103
  import chalk12 from "chalk";
7728
- import fs27 from "fs";
8104
+ import fs29 from "fs";
7729
8105
  function formatWhatChanged(setup) {
7730
8106
  const lines = [];
7731
8107
  const claude = setup.claude;
7732
8108
  const codex = setup.codex;
7733
8109
  const cursor = setup.cursor;
7734
8110
  if (claude?.claudeMd) {
7735
- const action = fs27.existsSync("CLAUDE.md") ? "Updated" : "Created";
8111
+ const action = fs29.existsSync("CLAUDE.md") ? "Updated" : "Created";
7736
8112
  lines.push(`${action} CLAUDE.md`);
7737
8113
  }
7738
8114
  if (codex?.agentsMd) {
7739
- const action = fs27.existsSync("AGENTS.md") ? "Updated" : "Created";
8115
+ const action = fs29.existsSync("AGENTS.md") ? "Updated" : "Created";
7740
8116
  lines.push(`${action} AGENTS.md`);
7741
8117
  }
7742
8118
  const allSkills = [];
@@ -7773,7 +8149,7 @@ function printSetupSummary(setup) {
7773
8149
  };
7774
8150
  if (claude) {
7775
8151
  if (claude.claudeMd) {
7776
- const icon = fs27.existsSync("CLAUDE.md") ? chalk12.yellow("~") : chalk12.green("+");
8152
+ const icon = fs29.existsSync("CLAUDE.md") ? chalk12.yellow("~") : chalk12.green("+");
7777
8153
  const desc = getDescription("CLAUDE.md");
7778
8154
  console.log(` ${icon} ${chalk12.bold("CLAUDE.md")}`);
7779
8155
  if (desc) console.log(chalk12.dim(` ${desc}`));
@@ -7783,7 +8159,7 @@ function printSetupSummary(setup) {
7783
8159
  if (Array.isArray(skills) && skills.length > 0) {
7784
8160
  for (const skill of skills) {
7785
8161
  const skillPath = `.claude/skills/${skill.name}/SKILL.md`;
7786
- const icon = fs27.existsSync(skillPath) ? chalk12.yellow("~") : chalk12.green("+");
8162
+ const icon = fs29.existsSync(skillPath) ? chalk12.yellow("~") : chalk12.green("+");
7787
8163
  const desc = getDescription(skillPath);
7788
8164
  console.log(` ${icon} ${chalk12.bold(skillPath)}`);
7789
8165
  console.log(chalk12.dim(` ${desc || skill.description || skill.name}`));
@@ -7794,7 +8170,7 @@ function printSetupSummary(setup) {
7794
8170
  const codex = setup.codex;
7795
8171
  if (codex) {
7796
8172
  if (codex.agentsMd) {
7797
- const icon = fs27.existsSync("AGENTS.md") ? chalk12.yellow("~") : chalk12.green("+");
8173
+ const icon = fs29.existsSync("AGENTS.md") ? chalk12.yellow("~") : chalk12.green("+");
7798
8174
  const desc = getDescription("AGENTS.md");
7799
8175
  console.log(` ${icon} ${chalk12.bold("AGENTS.md")}`);
7800
8176
  if (desc) console.log(chalk12.dim(` ${desc}`));
@@ -7804,7 +8180,7 @@ function printSetupSummary(setup) {
7804
8180
  if (Array.isArray(codexSkills) && codexSkills.length > 0) {
7805
8181
  for (const skill of codexSkills) {
7806
8182
  const skillPath = `.agents/skills/${skill.name}/SKILL.md`;
7807
- const icon = fs27.existsSync(skillPath) ? chalk12.yellow("~") : chalk12.green("+");
8183
+ const icon = fs29.existsSync(skillPath) ? chalk12.yellow("~") : chalk12.green("+");
7808
8184
  const desc = getDescription(skillPath);
7809
8185
  console.log(` ${icon} ${chalk12.bold(skillPath)}`);
7810
8186
  console.log(chalk12.dim(` ${desc || skill.description || skill.name}`));
@@ -7814,7 +8190,7 @@ function printSetupSummary(setup) {
7814
8190
  }
7815
8191
  if (cursor) {
7816
8192
  if (cursor.cursorrules) {
7817
- const icon = fs27.existsSync(".cursorrules") ? chalk12.yellow("~") : chalk12.green("+");
8193
+ const icon = fs29.existsSync(".cursorrules") ? chalk12.yellow("~") : chalk12.green("+");
7818
8194
  const desc = getDescription(".cursorrules");
7819
8195
  console.log(` ${icon} ${chalk12.bold(".cursorrules")}`);
7820
8196
  if (desc) console.log(chalk12.dim(` ${desc}`));
@@ -7824,7 +8200,7 @@ function printSetupSummary(setup) {
7824
8200
  if (Array.isArray(cursorSkills) && cursorSkills.length > 0) {
7825
8201
  for (const skill of cursorSkills) {
7826
8202
  const skillPath = `.cursor/skills/${skill.name}/SKILL.md`;
7827
- const icon = fs27.existsSync(skillPath) ? chalk12.yellow("~") : chalk12.green("+");
8203
+ const icon = fs29.existsSync(skillPath) ? chalk12.yellow("~") : chalk12.green("+");
7828
8204
  const desc = getDescription(skillPath);
7829
8205
  console.log(` ${icon} ${chalk12.bold(skillPath)}`);
7830
8206
  console.log(chalk12.dim(` ${desc || skill.description || skill.name}`));
@@ -7835,7 +8211,7 @@ function printSetupSummary(setup) {
7835
8211
  if (Array.isArray(rulesArr) && rulesArr.length > 0) {
7836
8212
  for (const rule of rulesArr) {
7837
8213
  const rulePath = `.cursor/rules/${rule.filename}`;
7838
- const icon = fs27.existsSync(rulePath) ? chalk12.yellow("~") : chalk12.green("+");
8214
+ const icon = fs29.existsSync(rulePath) ? chalk12.yellow("~") : chalk12.green("+");
7839
8215
  const desc = getDescription(rulePath);
7840
8216
  console.log(` ${icon} ${chalk12.bold(rulePath)}`);
7841
8217
  if (desc) {
@@ -7882,12 +8258,12 @@ function displayTokenUsage() {
7882
8258
  // src/commands/init-helpers.ts
7883
8259
  init_config();
7884
8260
  import chalk13 from "chalk";
7885
- import fs28 from "fs";
7886
- import path21 from "path";
8261
+ import fs30 from "fs";
8262
+ import path23 from "path";
7887
8263
  function isFirstRun(dir) {
7888
- const caliberDir = path21.join(dir, ".caliber");
8264
+ const caliberDir = path23.join(dir, ".caliber");
7889
8265
  try {
7890
- const stat = fs28.statSync(caliberDir);
8266
+ const stat = fs30.statSync(caliberDir);
7891
8267
  return !stat.isDirectory();
7892
8268
  } catch {
7893
8269
  return true;
@@ -7940,8 +8316,8 @@ function ensurePermissions(fingerprint) {
7940
8316
  const settingsPath = ".claude/settings.json";
7941
8317
  let settings = {};
7942
8318
  try {
7943
- if (fs28.existsSync(settingsPath)) {
7944
- settings = JSON.parse(fs28.readFileSync(settingsPath, "utf-8"));
8319
+ if (fs30.existsSync(settingsPath)) {
8320
+ settings = JSON.parse(fs30.readFileSync(settingsPath, "utf-8"));
7945
8321
  }
7946
8322
  } catch {
7947
8323
  }
@@ -7950,12 +8326,12 @@ function ensurePermissions(fingerprint) {
7950
8326
  if (Array.isArray(allow) && allow.length > 0) return;
7951
8327
  permissions.allow = derivePermissions(fingerprint);
7952
8328
  settings.permissions = permissions;
7953
- if (!fs28.existsSync(".claude")) fs28.mkdirSync(".claude", { recursive: true });
7954
- fs28.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
8329
+ if (!fs30.existsSync(".claude")) fs30.mkdirSync(".claude", { recursive: true });
8330
+ fs30.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
7955
8331
  }
7956
8332
  function writeErrorLog(config, rawOutput, error, stopReason) {
7957
8333
  try {
7958
- const logPath = path21.join(process.cwd(), ".caliber", "error-log.md");
8334
+ const logPath = path23.join(process.cwd(), ".caliber", "error-log.md");
7959
8335
  const lines = [
7960
8336
  `# Generation Error \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`,
7961
8337
  "",
@@ -7968,8 +8344,8 @@ function writeErrorLog(config, rawOutput, error, stopReason) {
7968
8344
  lines.push("## Error", "```", error, "```", "");
7969
8345
  }
7970
8346
  lines.push("## Raw LLM Output", "```", rawOutput || "(empty)", "```");
7971
- fs28.mkdirSync(path21.join(process.cwd(), ".caliber"), { recursive: true });
7972
- fs28.writeFileSync(logPath, lines.join("\n"));
8347
+ fs30.mkdirSync(path23.join(process.cwd(), ".caliber"), { recursive: true });
8348
+ fs30.writeFileSync(logPath, lines.join("\n"));
7973
8349
  console.log(chalk13.dim(`
7974
8350
  Error log written to .caliber/error-log.md`));
7975
8351
  } catch {
@@ -8036,7 +8412,7 @@ async function initCommand(options) {
8036
8412
  \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
8037
8413
  `));
8038
8414
  console.log(chalk14.dim(" Scan your project and generate tailored config files for"));
8039
- console.log(chalk14.dim(" Claude Code, Cursor, and Codex.\n"));
8415
+ console.log(chalk14.dim(" Claude Code, Cursor, Codex, and GitHub Copilot.\n"));
8040
8416
  console.log(title.bold(" How it works:\n"));
8041
8417
  console.log(chalk14.dim(" 1. Setup Connect your LLM provider and select your agents"));
8042
8418
  console.log(chalk14.dim(" 2. Engine Detect stack, generate configs & skills in parallel"));
@@ -8165,6 +8541,13 @@ async function initCommand(options) {
8165
8541
  display.update(TASK_STACK, "done", stackSummary + largeRepoNote);
8166
8542
  trackInitProjectDiscovered(fingerprint.languages.length, fingerprint.frameworks.length, fingerprint.fileTree.length);
8167
8543
  log(options.verbose, `Fingerprint: ${fingerprint.languages.length} languages, ${fingerprint.frameworks.length} frameworks, ${fingerprint.fileTree.length} files`);
8544
+ const cliSources = options.source || [];
8545
+ const workspaces = getDetectedWorkspaces(process.cwd());
8546
+ const sources2 = resolveAllSources(process.cwd(), cliSources, workspaces);
8547
+ if (sources2.length > 0) {
8548
+ fingerprint.sources = sources2;
8549
+ log(options.verbose, `Sources: ${sources2.length} resolved (${sources2.map((s) => s.name).join(", ")})`);
8550
+ }
8168
8551
  if (report) {
8169
8552
  report.addJson("Fingerprint: Git", { remote: fingerprint.gitRemoteUrl, packageName: fingerprint.packageName });
8170
8553
  report.addCodeBlock("Fingerprint: File Tree", fingerprint.fileTree.join("\n"));
@@ -8397,10 +8780,10 @@ async function initCommand(options) {
8397
8780
  console.log(JSON.stringify(generatedSetup, null, 2));
8398
8781
  return;
8399
8782
  }
8400
- const { default: ora8 } = await import("ora");
8401
- const writeSpinner = ora8("Writing config files...").start();
8783
+ const { default: ora9 } = await import("ora");
8784
+ const writeSpinner = ora9("Writing config files...").start();
8402
8785
  try {
8403
- if (targetAgent.includes("codex") && !fs29.existsSync("AGENTS.md") && !generatedSetup.codex) {
8786
+ if (targetAgent.includes("codex") && !fs31.existsSync("AGENTS.md") && !generatedSetup.codex) {
8404
8787
  const claude = generatedSetup.claude;
8405
8788
  const cursor = generatedSetup.cursor;
8406
8789
  const agentRefs = [];
@@ -8613,9 +8996,9 @@ ${agentRefs.join(" ")}
8613
8996
  }
8614
8997
  if (report) {
8615
8998
  report.markStep("Finished");
8616
- const reportPath = path22.join(process.cwd(), ".caliber", "debug-report.md");
8999
+ const reportPath = path24.join(process.cwd(), ".caliber", "debug-report.md");
8617
9000
  report.write(reportPath);
8618
- console.log(chalk14.dim(` Debug report written to ${path22.relative(process.cwd(), reportPath)}
9001
+ console.log(chalk14.dim(` Debug report written to ${path24.relative(process.cwd(), reportPath)}
8619
9002
  `));
8620
9003
  }
8621
9004
  }
@@ -8654,7 +9037,7 @@ function undoCommand() {
8654
9037
 
8655
9038
  // src/commands/status.ts
8656
9039
  import chalk16 from "chalk";
8657
- import fs30 from "fs";
9040
+ import fs32 from "fs";
8658
9041
  init_config();
8659
9042
  async function statusCommand(options) {
8660
9043
  const config = loadConfig();
@@ -8681,7 +9064,7 @@ async function statusCommand(options) {
8681
9064
  }
8682
9065
  console.log(` Files managed: ${chalk16.cyan(manifest.entries.length.toString())}`);
8683
9066
  for (const entry of manifest.entries) {
8684
- const exists = fs30.existsSync(entry.path);
9067
+ const exists = fs32.existsSync(entry.path);
8685
9068
  const icon = exists ? chalk16.green("\u2713") : chalk16.red("\u2717");
8686
9069
  console.log(` ${icon} ${entry.path} (${entry.action})`);
8687
9070
  }
@@ -8836,21 +9219,21 @@ async function regenerateCommand(options) {
8836
9219
  }
8837
9220
 
8838
9221
  // src/commands/score.ts
8839
- import fs31 from "fs";
9222
+ import fs33 from "fs";
8840
9223
  import os6 from "os";
8841
- import path23 from "path";
9224
+ import path25 from "path";
8842
9225
  import { execFileSync } from "child_process";
8843
9226
  import chalk18 from "chalk";
8844
9227
  var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".cursorrules", "CALIBER_LEARNINGS.md"];
8845
9228
  var CONFIG_DIRS = [".claude", ".cursor"];
8846
9229
  function scoreBaseRef(ref, target) {
8847
9230
  if (!/^[\w.\-\/~^@{}]+$/.test(ref)) return null;
8848
- const tmpDir = fs31.mkdtempSync(path23.join(os6.tmpdir(), "caliber-compare-"));
9231
+ const tmpDir = fs33.mkdtempSync(path25.join(os6.tmpdir(), "caliber-compare-"));
8849
9232
  try {
8850
9233
  for (const file of CONFIG_FILES) {
8851
9234
  try {
8852
9235
  const content = execFileSync("git", ["show", `${ref}:${file}`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
8853
- fs31.writeFileSync(path23.join(tmpDir, file), content);
9236
+ fs33.writeFileSync(path25.join(tmpDir, file), content);
8854
9237
  } catch {
8855
9238
  }
8856
9239
  }
@@ -8858,10 +9241,10 @@ function scoreBaseRef(ref, target) {
8858
9241
  try {
8859
9242
  const files = execFileSync("git", ["ls-tree", "-r", "--name-only", ref, `${dir}/`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean);
8860
9243
  for (const file of files) {
8861
- const filePath = path23.join(tmpDir, file);
8862
- fs31.mkdirSync(path23.dirname(filePath), { recursive: true });
9244
+ const filePath = path25.join(tmpDir, file);
9245
+ fs33.mkdirSync(path25.dirname(filePath), { recursive: true });
8863
9246
  const content = execFileSync("git", ["show", `${ref}:${file}`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
8864
- fs31.writeFileSync(filePath, content);
9247
+ fs33.writeFileSync(filePath, content);
8865
9248
  }
8866
9249
  } catch {
8867
9250
  }
@@ -8871,7 +9254,7 @@ function scoreBaseRef(ref, target) {
8871
9254
  } catch {
8872
9255
  return null;
8873
9256
  } finally {
8874
- fs31.rmSync(tmpDir, { recursive: true, force: true });
9257
+ fs33.rmSync(tmpDir, { recursive: true, force: true });
8875
9258
  }
8876
9259
  }
8877
9260
  async function scoreCommand(options) {
@@ -8931,8 +9314,8 @@ async function scoreCommand(options) {
8931
9314
  }
8932
9315
 
8933
9316
  // src/commands/refresh.ts
8934
- import fs35 from "fs";
8935
- import path27 from "path";
9317
+ import fs37 from "fs";
9318
+ import path29 from "path";
8936
9319
  import chalk19 from "chalk";
8937
9320
  import ora6 from "ora";
8938
9321
 
@@ -9010,38 +9393,49 @@ function collectDiff(lastSha) {
9010
9393
  }
9011
9394
 
9012
9395
  // src/writers/refresh.ts
9013
- import fs32 from "fs";
9014
- import path24 from "path";
9396
+ import fs34 from "fs";
9397
+ import path26 from "path";
9015
9398
  function writeRefreshDocs(docs) {
9016
9399
  const written = [];
9017
9400
  if (docs.claudeMd) {
9018
- fs32.writeFileSync("CLAUDE.md", docs.claudeMd);
9401
+ fs34.writeFileSync("CLAUDE.md", docs.claudeMd);
9019
9402
  written.push("CLAUDE.md");
9020
9403
  }
9021
9404
  if (docs.readmeMd) {
9022
- fs32.writeFileSync("README.md", docs.readmeMd);
9405
+ fs34.writeFileSync("README.md", docs.readmeMd);
9023
9406
  written.push("README.md");
9024
9407
  }
9025
9408
  if (docs.cursorrules) {
9026
- fs32.writeFileSync(".cursorrules", docs.cursorrules);
9409
+ fs34.writeFileSync(".cursorrules", docs.cursorrules);
9027
9410
  written.push(".cursorrules");
9028
9411
  }
9029
9412
  if (docs.cursorRules) {
9030
- const rulesDir = path24.join(".cursor", "rules");
9031
- if (!fs32.existsSync(rulesDir)) fs32.mkdirSync(rulesDir, { recursive: true });
9413
+ const rulesDir = path26.join(".cursor", "rules");
9414
+ if (!fs34.existsSync(rulesDir)) fs34.mkdirSync(rulesDir, { recursive: true });
9032
9415
  for (const rule of docs.cursorRules) {
9033
- const filePath = path24.join(rulesDir, rule.filename);
9034
- fs32.writeFileSync(filePath, rule.content);
9035
- written.push(filePath);
9416
+ fs34.writeFileSync(path26.join(rulesDir, rule.filename), rule.content);
9417
+ written.push(`.cursor/rules/${rule.filename}`);
9036
9418
  }
9037
9419
  }
9038
9420
  if (docs.claudeSkills) {
9039
- const skillsDir = path24.join(".claude", "skills");
9040
- if (!fs32.existsSync(skillsDir)) fs32.mkdirSync(skillsDir, { recursive: true });
9421
+ const skillsDir = path26.join(".claude", "skills");
9422
+ if (!fs34.existsSync(skillsDir)) fs34.mkdirSync(skillsDir, { recursive: true });
9041
9423
  for (const skill of docs.claudeSkills) {
9042
- const filePath = path24.join(skillsDir, skill.filename);
9043
- fs32.writeFileSync(filePath, skill.content);
9044
- written.push(filePath);
9424
+ fs34.writeFileSync(path26.join(skillsDir, skill.filename), skill.content);
9425
+ written.push(`.claude/skills/${skill.filename}`);
9426
+ }
9427
+ }
9428
+ if (docs.copilotInstructions) {
9429
+ fs34.mkdirSync(".github", { recursive: true });
9430
+ fs34.writeFileSync(path26.join(".github", "copilot-instructions.md"), docs.copilotInstructions);
9431
+ written.push(".github/copilot-instructions.md");
9432
+ }
9433
+ if (docs.copilotInstructionFiles) {
9434
+ const instructionsDir = path26.join(".github", "instructions");
9435
+ fs34.mkdirSync(instructionsDir, { recursive: true });
9436
+ for (const file of docs.copilotInstructionFiles) {
9437
+ fs34.writeFileSync(path26.join(instructionsDir, file.filename), file.content);
9438
+ written.push(`.github/instructions/${file.filename}`);
9045
9439
  }
9046
9440
  }
9047
9441
  return written;
@@ -9049,8 +9443,8 @@ function writeRefreshDocs(docs) {
9049
9443
 
9050
9444
  // src/ai/refresh.ts
9051
9445
  init_config();
9052
- async function refreshDocs(diff, existingDocs, projectContext, learnedSection) {
9053
- const prompt = buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection);
9446
+ async function refreshDocs(diff, existingDocs, projectContext, learnedSection, sources2) {
9447
+ const prompt = buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection, sources2);
9054
9448
  const fastModel = getFastModel();
9055
9449
  const raw = await llmCall({
9056
9450
  system: REFRESH_SYSTEM_PROMPT,
@@ -9060,7 +9454,7 @@ async function refreshDocs(diff, existingDocs, projectContext, learnedSection) {
9060
9454
  });
9061
9455
  return parseJsonResponse(raw);
9062
9456
  }
9063
- function buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection) {
9457
+ function buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection, sources2) {
9064
9458
  const parts = [];
9065
9459
  parts.push("Update documentation based on the following code changes.\n");
9066
9460
  if (projectContext.packageName) parts.push(`Project: ${projectContext.packageName}`);
@@ -9113,12 +9507,15 @@ Changed files: ${diff.changedFiles.join(", ")}`);
9113
9507
  parts.push("Consider these accumulated learnings when deciding what to update:");
9114
9508
  parts.push(learnedSection);
9115
9509
  }
9510
+ if (sources2?.length) {
9511
+ parts.push(formatSourcesForPrompt(sources2));
9512
+ }
9116
9513
  return parts.join("\n");
9117
9514
  }
9118
9515
 
9119
9516
  // src/learner/writer.ts
9120
- import fs33 from "fs";
9121
- import path25 from "path";
9517
+ import fs35 from "fs";
9518
+ import path27 from "path";
9122
9519
 
9123
9520
  // src/learner/utils.ts
9124
9521
  var TYPE_PREFIX_RE = /^\*\*\[[^\]]+\]\*\*\s*/;
@@ -9128,6 +9525,13 @@ function normalizeBullet(bullet) {
9128
9525
  function hasTypePrefix(bullet) {
9129
9526
  return TYPE_PREFIX_RE.test(bullet.replace(/^- /, ""));
9130
9527
  }
9528
+ var SCOPE_RE = /^\*\*\[[^\]:]+:([^\]]+)\]\*\*/;
9529
+ function extractScope(bullet) {
9530
+ const clean = bullet.replace(/^- /, "");
9531
+ const match = clean.match(SCOPE_RE);
9532
+ if (match && match[1] === "personal") return "personal";
9533
+ return "project";
9534
+ }
9131
9535
  var SIMILARITY_THRESHOLD = 0.7;
9132
9536
  function isSimilarLearning(a, b) {
9133
9537
  const normA = normalizeBullet(a);
@@ -9140,12 +9544,19 @@ function isSimilarLearning(a, b) {
9140
9544
  }
9141
9545
 
9142
9546
  // src/learner/writer.ts
9547
+ init_constants();
9143
9548
  var LEARNINGS_FILE = "CALIBER_LEARNINGS.md";
9144
9549
  var LEARNINGS_HEADER = `# Caliber Learnings
9145
9550
 
9146
9551
  Accumulated patterns and anti-patterns from development sessions.
9147
9552
  Auto-managed by [caliber](https://github.com/rely-ai-org/caliber) \u2014 do not edit manually.
9148
9553
 
9554
+ `;
9555
+ var PERSONAL_LEARNINGS_HEADER = `# Personal Learnings
9556
+
9557
+ Developer-specific patterns and preferences.
9558
+ Auto-managed by [caliber](https://github.com/rely-ai-org/caliber) \u2014 do not edit manually.
9559
+
9149
9560
  `;
9150
9561
  var LEARNED_START = "<!-- caliber:learned -->";
9151
9562
  var LEARNED_END = "<!-- /caliber:learned -->";
@@ -9154,11 +9565,24 @@ function writeLearnedContent(update) {
9154
9565
  const written = [];
9155
9566
  let newItemCount = 0;
9156
9567
  let newItems = [];
9568
+ let personalItemCount = 0;
9569
+ let personalItems = [];
9157
9570
  if (update.claudeMdLearnedSection) {
9158
- const result = writeLearnedSection(update.claudeMdLearnedSection);
9159
- newItemCount = result.newCount;
9160
- newItems = result.newItems;
9161
- written.push(LEARNINGS_FILE);
9571
+ const bullets = parseBullets(update.claudeMdLearnedSection);
9572
+ const projectBullets = bullets.filter((b) => extractScope(b) === "project");
9573
+ const personalBullets = bullets.filter((b) => extractScope(b) === "personal");
9574
+ if (projectBullets.length > 0) {
9575
+ const result = writeLearnedSection(projectBullets.join("\n"));
9576
+ newItemCount = result.newCount;
9577
+ newItems = result.newItems;
9578
+ written.push(LEARNINGS_FILE);
9579
+ }
9580
+ if (personalBullets.length > 0) {
9581
+ const result = writePersonalLearnedSection(personalBullets.join("\n"));
9582
+ personalItemCount = result.newCount;
9583
+ personalItems = result.newItems;
9584
+ written.push(PERSONAL_LEARNINGS_FILE);
9585
+ }
9162
9586
  }
9163
9587
  if (update.skills?.length) {
9164
9588
  for (const skill of update.skills) {
@@ -9166,7 +9590,7 @@ function writeLearnedContent(update) {
9166
9590
  written.push(skillPath);
9167
9591
  }
9168
9592
  }
9169
- return { written, newItemCount, newItems };
9593
+ return { written, newItemCount, newItems, personalItemCount, personalItems };
9170
9594
  }
9171
9595
  function parseBullets(content) {
9172
9596
  const lines = content.split("\n");
@@ -9207,19 +9631,22 @@ function deduplicateLearnedItems(existing, incoming) {
9207
9631
  const capped = merged.length > MAX_LEARNED_ITEMS ? merged.slice(-MAX_LEARNED_ITEMS) : merged;
9208
9632
  return { merged: capped.join("\n"), newCount: newItems.length, newItems };
9209
9633
  }
9210
- function writeLearnedSection(content) {
9211
- const existingSection = readLearnedSection();
9212
- const { merged, newCount, newItems } = deduplicateLearnedItems(existingSection, content);
9213
- fs33.writeFileSync(LEARNINGS_FILE, LEARNINGS_HEADER + merged + "\n");
9634
+ function writeLearnedSectionTo(filePath, header, existing, incoming, mode) {
9635
+ const { merged, newCount, newItems } = deduplicateLearnedItems(existing, incoming);
9636
+ fs35.writeFileSync(filePath, header + merged + "\n");
9637
+ if (mode) fs35.chmodSync(filePath, mode);
9214
9638
  return { newCount, newItems };
9215
9639
  }
9640
+ function writeLearnedSection(content) {
9641
+ return writeLearnedSectionTo(LEARNINGS_FILE, LEARNINGS_HEADER, readLearnedSection(), content);
9642
+ }
9216
9643
  function writeLearnedSkill(skill) {
9217
- const skillDir = path25.join(".claude", "skills", skill.name);
9218
- if (!fs33.existsSync(skillDir)) fs33.mkdirSync(skillDir, { recursive: true });
9219
- const skillPath = path25.join(skillDir, "SKILL.md");
9220
- if (!skill.isNew && fs33.existsSync(skillPath)) {
9221
- const existing = fs33.readFileSync(skillPath, "utf-8");
9222
- fs33.writeFileSync(skillPath, existing.trimEnd() + "\n\n" + skill.content);
9644
+ const skillDir = path27.join(".claude", "skills", skill.name);
9645
+ if (!fs35.existsSync(skillDir)) fs35.mkdirSync(skillDir, { recursive: true });
9646
+ const skillPath = path27.join(skillDir, "SKILL.md");
9647
+ if (!skill.isNew && fs35.existsSync(skillPath)) {
9648
+ const existing = fs35.readFileSync(skillPath, "utf-8");
9649
+ fs35.writeFileSync(skillPath, existing.trimEnd() + "\n\n" + skill.content);
9223
9650
  } else {
9224
9651
  const frontmatter = [
9225
9652
  "---",
@@ -9228,37 +9655,47 @@ function writeLearnedSkill(skill) {
9228
9655
  "---",
9229
9656
  ""
9230
9657
  ].join("\n");
9231
- fs33.writeFileSync(skillPath, frontmatter + skill.content);
9658
+ fs35.writeFileSync(skillPath, frontmatter + skill.content);
9232
9659
  }
9233
9660
  return skillPath;
9234
9661
  }
9662
+ function writePersonalLearnedSection(content) {
9663
+ if (!fs35.existsSync(AUTH_DIR)) fs35.mkdirSync(AUTH_DIR, { recursive: true });
9664
+ return writeLearnedSectionTo(PERSONAL_LEARNINGS_FILE, PERSONAL_LEARNINGS_HEADER, readPersonalLearnings(), content, 384);
9665
+ }
9666
+ function readPersonalLearnings() {
9667
+ if (!fs35.existsSync(PERSONAL_LEARNINGS_FILE)) return null;
9668
+ const content = fs35.readFileSync(PERSONAL_LEARNINGS_FILE, "utf-8");
9669
+ const bullets = content.split("\n").filter((l) => l.startsWith("- ")).join("\n");
9670
+ return bullets || null;
9671
+ }
9235
9672
  function readLearnedSection() {
9236
- if (fs33.existsSync(LEARNINGS_FILE)) {
9237
- const content2 = fs33.readFileSync(LEARNINGS_FILE, "utf-8");
9673
+ if (fs35.existsSync(LEARNINGS_FILE)) {
9674
+ const content2 = fs35.readFileSync(LEARNINGS_FILE, "utf-8");
9238
9675
  const bullets = content2.split("\n").filter((l) => l.startsWith("- ")).join("\n");
9239
9676
  return bullets || null;
9240
9677
  }
9241
9678
  const claudeMdPath = "CLAUDE.md";
9242
- if (!fs33.existsSync(claudeMdPath)) return null;
9243
- const content = fs33.readFileSync(claudeMdPath, "utf-8");
9679
+ if (!fs35.existsSync(claudeMdPath)) return null;
9680
+ const content = fs35.readFileSync(claudeMdPath, "utf-8");
9244
9681
  const startIdx = content.indexOf(LEARNED_START);
9245
9682
  const endIdx = content.indexOf(LEARNED_END);
9246
9683
  if (startIdx === -1 || endIdx === -1) return null;
9247
9684
  return content.slice(startIdx + LEARNED_START.length, endIdx).trim() || null;
9248
9685
  }
9249
9686
  function migrateInlineLearnings() {
9250
- if (fs33.existsSync(LEARNINGS_FILE)) return false;
9687
+ if (fs35.existsSync(LEARNINGS_FILE)) return false;
9251
9688
  const claudeMdPath = "CLAUDE.md";
9252
- if (!fs33.existsSync(claudeMdPath)) return false;
9253
- const content = fs33.readFileSync(claudeMdPath, "utf-8");
9689
+ if (!fs35.existsSync(claudeMdPath)) return false;
9690
+ const content = fs35.readFileSync(claudeMdPath, "utf-8");
9254
9691
  const startIdx = content.indexOf(LEARNED_START);
9255
9692
  const endIdx = content.indexOf(LEARNED_END);
9256
9693
  if (startIdx === -1 || endIdx === -1) return false;
9257
9694
  const section = content.slice(startIdx + LEARNED_START.length, endIdx).trim();
9258
9695
  if (!section) return false;
9259
- fs33.writeFileSync(LEARNINGS_FILE, LEARNINGS_HEADER + section + "\n");
9696
+ fs35.writeFileSync(LEARNINGS_FILE, LEARNINGS_HEADER + section + "\n");
9260
9697
  const cleaned = content.slice(0, startIdx) + content.slice(endIdx + LEARNED_END.length);
9261
- fs33.writeFileSync(claudeMdPath, cleaned.replace(/\n{3,}/g, "\n\n").trim() + "\n");
9698
+ fs35.writeFileSync(claudeMdPath, cleaned.replace(/\n{3,}/g, "\n\n").trim() + "\n");
9262
9699
  return true;
9263
9700
  }
9264
9701
 
@@ -9270,11 +9707,11 @@ function log2(quiet, ...args) {
9270
9707
  function discoverGitRepos(parentDir) {
9271
9708
  const repos = [];
9272
9709
  try {
9273
- const entries = fs35.readdirSync(parentDir, { withFileTypes: true });
9710
+ const entries = fs37.readdirSync(parentDir, { withFileTypes: true });
9274
9711
  for (const entry of entries) {
9275
9712
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
9276
- const childPath = path27.join(parentDir, entry.name);
9277
- if (fs35.existsSync(path27.join(childPath, ".git"))) {
9713
+ const childPath = path29.join(parentDir, entry.name);
9714
+ if (fs37.existsSync(path29.join(childPath, ".git"))) {
9278
9715
  repos.push(childPath);
9279
9716
  }
9280
9717
  }
@@ -9305,6 +9742,8 @@ async function refreshSingleRepo(repoDir, options) {
9305
9742
  frameworks: fingerprint.frameworks,
9306
9743
  packageName: fingerprint.packageName
9307
9744
  };
9745
+ const workspaces = getDetectedWorkspaces(repoDir);
9746
+ const sources2 = resolveAllSources(repoDir, [], workspaces);
9308
9747
  const response = await refreshDocs(
9309
9748
  {
9310
9749
  committed: diff.committedDiff,
@@ -9315,7 +9754,8 @@ async function refreshSingleRepo(repoDir, options) {
9315
9754
  },
9316
9755
  existingDocs,
9317
9756
  projectContext,
9318
- learnedSection
9757
+ learnedSection,
9758
+ sources2.length > 0 ? sources2 : void 0
9319
9759
  );
9320
9760
  if (!response.docsUpdated || response.docsUpdated.length === 0) {
9321
9761
  spinner?.succeed(`${prefix}No doc updates needed`);
@@ -9377,7 +9817,7 @@ async function refreshCommand(options) {
9377
9817
  `));
9378
9818
  const originalDir = process.cwd();
9379
9819
  for (const repo of repos) {
9380
- const repoName = path27.basename(repo);
9820
+ const repoName = path29.basename(repo);
9381
9821
  try {
9382
9822
  process.chdir(repo);
9383
9823
  await refreshSingleRepo(repo, { ...options, label: repoName });
@@ -9398,7 +9838,7 @@ async function refreshCommand(options) {
9398
9838
 
9399
9839
  // src/commands/hooks.ts
9400
9840
  import chalk20 from "chalk";
9401
- import fs36 from "fs";
9841
+ import fs38 from "fs";
9402
9842
  var HOOKS = [
9403
9843
  {
9404
9844
  id: "session-end",
@@ -9438,11 +9878,11 @@ async function hooksCommand(options) {
9438
9878
  console.log(chalk20.green(" \u2713") + ` ${hook.label} enabled`);
9439
9879
  }
9440
9880
  }
9441
- if (fs36.existsSync(".claude")) {
9881
+ if (fs38.existsSync(".claude")) {
9442
9882
  const r = installLearningHooks();
9443
9883
  if (r.installed) console.log(chalk20.green(" \u2713") + " Claude Code learning hooks enabled");
9444
9884
  }
9445
- if (fs36.existsSync(".cursor")) {
9885
+ if (fs38.existsSync(".cursor")) {
9446
9886
  const r = installCursorLearningHooks();
9447
9887
  if (r.installed) console.log(chalk20.green(" \u2713") + " Cursor learning hooks enabled");
9448
9888
  }
@@ -9610,7 +10050,7 @@ async function configCommand() {
9610
10050
  }
9611
10051
 
9612
10052
  // src/commands/learn.ts
9613
- import fs40 from "fs";
10053
+ import fs42 from "fs";
9614
10054
  import chalk23 from "chalk";
9615
10055
 
9616
10056
  // src/learner/stdin.ts
@@ -9642,8 +10082,8 @@ function readStdin() {
9642
10082
 
9643
10083
  // src/learner/storage.ts
9644
10084
  init_constants();
9645
- import fs37 from "fs";
9646
- import path28 from "path";
10085
+ import fs39 from "fs";
10086
+ import path30 from "path";
9647
10087
  var MAX_RESPONSE_LENGTH = 2e3;
9648
10088
  var DEFAULT_STATE = {
9649
10089
  sessionId: null,
@@ -9652,48 +10092,48 @@ var DEFAULT_STATE = {
9652
10092
  lastAnalysisEventCount: 0
9653
10093
  };
9654
10094
  function ensureLearningDir() {
9655
- if (!fs37.existsSync(LEARNING_DIR)) {
9656
- fs37.mkdirSync(LEARNING_DIR, { recursive: true });
10095
+ if (!fs39.existsSync(LEARNING_DIR)) {
10096
+ fs39.mkdirSync(LEARNING_DIR, { recursive: true });
9657
10097
  }
9658
10098
  }
9659
10099
  function sessionFilePath() {
9660
- return path28.join(LEARNING_DIR, LEARNING_SESSION_FILE);
10100
+ return path30.join(LEARNING_DIR, LEARNING_SESSION_FILE);
9661
10101
  }
9662
10102
  function stateFilePath() {
9663
- return path28.join(LEARNING_DIR, LEARNING_STATE_FILE);
10103
+ return path30.join(LEARNING_DIR, LEARNING_STATE_FILE);
9664
10104
  }
9665
10105
  function truncateResponse(response) {
9666
10106
  const str = JSON.stringify(response);
9667
10107
  if (str.length <= MAX_RESPONSE_LENGTH) return response;
9668
10108
  return { _truncated: str.slice(0, MAX_RESPONSE_LENGTH) };
9669
10109
  }
10110
+ function trimSessionFileIfNeeded(filePath) {
10111
+ const state = readState2();
10112
+ if (state.eventCount + 1 > LEARNING_MAX_EVENTS) {
10113
+ const lines = fs39.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
10114
+ if (lines.length > LEARNING_MAX_EVENTS) {
10115
+ const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
10116
+ fs39.writeFileSync(filePath, kept.join("\n") + "\n");
10117
+ }
10118
+ }
10119
+ }
9670
10120
  function appendEvent(event) {
9671
10121
  ensureLearningDir();
9672
10122
  const truncated = { ...event, tool_response: truncateResponse(event.tool_response) };
9673
10123
  const filePath = sessionFilePath();
9674
- fs37.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
9675
- const count = getEventCount();
9676
- if (count > LEARNING_MAX_EVENTS) {
9677
- const lines = fs37.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
9678
- const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
9679
- fs37.writeFileSync(filePath, kept.join("\n") + "\n");
9680
- }
10124
+ fs39.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
10125
+ trimSessionFileIfNeeded(filePath);
9681
10126
  }
9682
10127
  function appendPromptEvent(event) {
9683
10128
  ensureLearningDir();
9684
10129
  const filePath = sessionFilePath();
9685
- fs37.appendFileSync(filePath, JSON.stringify(event) + "\n");
9686
- const count = getEventCount();
9687
- if (count > LEARNING_MAX_EVENTS) {
9688
- const lines = fs37.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
9689
- const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
9690
- fs37.writeFileSync(filePath, kept.join("\n") + "\n");
9691
- }
10130
+ fs39.appendFileSync(filePath, JSON.stringify(event) + "\n");
10131
+ trimSessionFileIfNeeded(filePath);
9692
10132
  }
9693
10133
  function readAllEvents() {
9694
10134
  const filePath = sessionFilePath();
9695
- if (!fs37.existsSync(filePath)) return [];
9696
- const lines = fs37.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
10135
+ if (!fs39.existsSync(filePath)) return [];
10136
+ const lines = fs39.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
9697
10137
  const events = [];
9698
10138
  for (const line of lines) {
9699
10139
  try {
@@ -9705,26 +10145,26 @@ function readAllEvents() {
9705
10145
  }
9706
10146
  function getEventCount() {
9707
10147
  const filePath = sessionFilePath();
9708
- if (!fs37.existsSync(filePath)) return 0;
9709
- const content = fs37.readFileSync(filePath, "utf-8");
10148
+ if (!fs39.existsSync(filePath)) return 0;
10149
+ const content = fs39.readFileSync(filePath, "utf-8");
9710
10150
  return content.split("\n").filter(Boolean).length;
9711
10151
  }
9712
10152
  function clearSession() {
9713
10153
  const filePath = sessionFilePath();
9714
- if (fs37.existsSync(filePath)) fs37.unlinkSync(filePath);
10154
+ if (fs39.existsSync(filePath)) fs39.unlinkSync(filePath);
9715
10155
  }
9716
10156
  function readState2() {
9717
10157
  const filePath = stateFilePath();
9718
- if (!fs37.existsSync(filePath)) return { ...DEFAULT_STATE };
10158
+ if (!fs39.existsSync(filePath)) return { ...DEFAULT_STATE };
9719
10159
  try {
9720
- return JSON.parse(fs37.readFileSync(filePath, "utf-8"));
10160
+ return JSON.parse(fs39.readFileSync(filePath, "utf-8"));
9721
10161
  } catch {
9722
10162
  return { ...DEFAULT_STATE };
9723
10163
  }
9724
10164
  }
9725
10165
  function writeState2(state) {
9726
10166
  ensureLearningDir();
9727
- fs37.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
10167
+ fs39.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
9728
10168
  }
9729
10169
  function resetState() {
9730
10170
  writeState2({ ...DEFAULT_STATE });
@@ -9732,14 +10172,14 @@ function resetState() {
9732
10172
  var LOCK_FILE2 = "finalize.lock";
9733
10173
  var LOCK_STALE_MS = 5 * 60 * 1e3;
9734
10174
  function lockFilePath() {
9735
- return path28.join(LEARNING_DIR, LOCK_FILE2);
10175
+ return path30.join(LEARNING_DIR, LOCK_FILE2);
9736
10176
  }
9737
10177
  function acquireFinalizeLock() {
9738
10178
  ensureLearningDir();
9739
10179
  const lockPath = lockFilePath();
9740
- if (fs37.existsSync(lockPath)) {
10180
+ if (fs39.existsSync(lockPath)) {
9741
10181
  try {
9742
- const stat = fs37.statSync(lockPath);
10182
+ const stat = fs39.statSync(lockPath);
9743
10183
  if (Date.now() - stat.mtimeMs < LOCK_STALE_MS) {
9744
10184
  return false;
9745
10185
  }
@@ -9747,7 +10187,7 @@ function acquireFinalizeLock() {
9747
10187
  }
9748
10188
  }
9749
10189
  try {
9750
- fs37.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
10190
+ fs39.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
9751
10191
  return true;
9752
10192
  } catch {
9753
10193
  return false;
@@ -9756,7 +10196,7 @@ function acquireFinalizeLock() {
9756
10196
  function releaseFinalizeLock() {
9757
10197
  const lockPath = lockFilePath();
9758
10198
  try {
9759
- if (fs37.existsSync(lockPath)) fs37.unlinkSync(lockPath);
10199
+ if (fs39.existsSync(lockPath)) fs39.unlinkSync(lockPath);
9760
10200
  } catch {
9761
10201
  }
9762
10202
  }
@@ -9802,22 +10242,22 @@ function sanitizeSecrets(text) {
9802
10242
 
9803
10243
  // src/lib/notifications.ts
9804
10244
  init_constants();
9805
- import fs38 from "fs";
9806
- import path29 from "path";
10245
+ import fs40 from "fs";
10246
+ import path31 from "path";
9807
10247
  import chalk22 from "chalk";
9808
- var NOTIFICATION_FILE = path29.join(LEARNING_DIR, "last-finalize-summary.json");
10248
+ var NOTIFICATION_FILE = path31.join(LEARNING_DIR, "last-finalize-summary.json");
9809
10249
  function writeFinalizeSummary(summary) {
9810
10250
  try {
9811
10251
  ensureLearningDir();
9812
- fs38.writeFileSync(NOTIFICATION_FILE, JSON.stringify(summary, null, 2));
10252
+ fs40.writeFileSync(NOTIFICATION_FILE, JSON.stringify(summary, null, 2));
9813
10253
  } catch {
9814
10254
  }
9815
10255
  }
9816
10256
  function checkPendingNotifications() {
9817
10257
  try {
9818
- if (!fs38.existsSync(NOTIFICATION_FILE)) return;
9819
- const raw = fs38.readFileSync(NOTIFICATION_FILE, "utf-8");
9820
- fs38.unlinkSync(NOTIFICATION_FILE);
10258
+ if (!fs40.existsSync(NOTIFICATION_FILE)) return;
10259
+ const raw = fs40.readFileSync(NOTIFICATION_FILE, "utf-8");
10260
+ fs40.unlinkSync(NOTIFICATION_FILE);
9821
10261
  const summary = JSON.parse(raw);
9822
10262
  if (!summary.newItemCount || summary.newItemCount === 0) return;
9823
10263
  const wasteLabel = summary.wasteTokens > 0 ? ` (~${summary.wasteTokens.toLocaleString()} wasted tokens captured)` : "";
@@ -9833,7 +10273,7 @@ function checkPendingNotifications() {
9833
10273
  console.log("");
9834
10274
  } catch {
9835
10275
  try {
9836
- fs38.unlinkSync(NOTIFICATION_FILE);
10276
+ fs40.unlinkSync(NOTIFICATION_FILE);
9837
10277
  } catch {
9838
10278
  }
9839
10279
  }
@@ -9888,7 +10328,7 @@ function parseAnalysisResponse(raw) {
9888
10328
  return { claudeMdLearnedSection: null, skills: null, explanations: ["LLM response contained invalid JSON."] };
9889
10329
  }
9890
10330
  }
9891
- async function analyzeEvents(events, existingClaudeMd, existingLearnedSection, existingSkills) {
10331
+ async function analyzeEvents(events, existingClaudeMd, existingLearnedSection, existingSkills, personalLearnings) {
9892
10332
  const fittedEvents = trimEventsToFit(events, MAX_PROMPT_TOKENS - 1e4);
9893
10333
  const eventsText = formatEventsForPrompt(fittedEvents);
9894
10334
  const contextParts = [];
@@ -9907,6 +10347,11 @@ ${existingLearnedSection}`);
9907
10347
  contextParts.push(`## Existing Skills
9908
10348
 
9909
10349
  ${skillsSummary}`);
10350
+ }
10351
+ if (personalLearnings) {
10352
+ contextParts.push(`## Personal Learnings (developer-specific, do NOT duplicate these)
10353
+
10354
+ ${personalLearnings}`);
9910
10355
  }
9911
10356
  contextParts.push(`## Task Segmentation & Attribution Instructions
9912
10357
 
@@ -9966,8 +10411,8 @@ init_config();
9966
10411
 
9967
10412
  // src/learner/roi.ts
9968
10413
  init_constants();
9969
- import fs39 from "fs";
9970
- import path30 from "path";
10414
+ import fs41 from "fs";
10415
+ import path32 from "path";
9971
10416
  var DEFAULT_TOTALS = {
9972
10417
  totalWasteTokens: 0,
9973
10418
  totalWasteSeconds: 0,
@@ -9981,22 +10426,28 @@ var DEFAULT_TOTALS = {
9981
10426
  lastSessionTimestamp: ""
9982
10427
  };
9983
10428
  function roiFilePath() {
9984
- return path30.join(LEARNING_DIR, LEARNING_ROI_FILE);
10429
+ return path32.join(LEARNING_DIR, LEARNING_ROI_FILE);
9985
10430
  }
9986
10431
  function readROIStats() {
9987
10432
  const filePath = roiFilePath();
9988
- if (!fs39.existsSync(filePath)) {
10433
+ if (!fs41.existsSync(filePath)) {
9989
10434
  return { learnings: [], sessions: [], totals: { ...DEFAULT_TOTALS } };
9990
10435
  }
9991
10436
  try {
9992
- return JSON.parse(fs39.readFileSync(filePath, "utf-8"));
10437
+ return JSON.parse(fs41.readFileSync(filePath, "utf-8"));
9993
10438
  } catch {
10439
+ try {
10440
+ const corruptPath = filePath + ".corrupt";
10441
+ fs41.renameSync(filePath, corruptPath);
10442
+ console.error(`caliber: roi-stats.json was corrupt \u2014 renamed to ${corruptPath}`);
10443
+ } catch {
10444
+ }
9994
10445
  return { learnings: [], sessions: [], totals: { ...DEFAULT_TOTALS } };
9995
10446
  }
9996
10447
  }
9997
10448
  function writeROIStats(stats) {
9998
10449
  ensureLearningDir();
9999
- fs39.writeFileSync(roiFilePath(), JSON.stringify(stats, null, 2));
10450
+ fs41.writeFileSync(roiFilePath(), JSON.stringify(stats, null, 2));
10000
10451
  }
10001
10452
  function recalculateTotals(stats) {
10002
10453
  const totals = stats.totals;
@@ -10016,8 +10467,24 @@ function recalculateTotals(stats) {
10016
10467
  totals.totalFailuresWithoutLearnings += s.failureCount;
10017
10468
  }
10018
10469
  }
10019
- totals.estimatedSavingsTokens = totals.totalWasteTokens * totals.totalSessionsWithLearnings;
10020
- totals.estimatedSavingsSeconds = totals.totalWasteSeconds * totals.totalSessionsWithLearnings;
10470
+ if (totals.totalSessionsWithLearnings >= MIN_SESSIONS_FOR_COMPARISON && totals.totalSessionsWithoutLearnings >= MIN_SESSIONS_FOR_COMPARISON) {
10471
+ const rateWithout = totals.totalFailuresWithoutLearnings / totals.totalSessionsWithoutLearnings;
10472
+ const rateWith = totals.totalFailuresWithLearnings / totals.totalSessionsWithLearnings;
10473
+ if (rateWithout > 0 && rateWith < rateWithout) {
10474
+ const reduction = (rateWithout - rateWith) / rateWithout;
10475
+ const totalSessions = totals.totalSessionsWithLearnings + totals.totalSessionsWithoutLearnings;
10476
+ const avgWastePerSession = totalSessions > 0 ? totals.totalWasteTokens / totalSessions : 0;
10477
+ totals.estimatedSavingsTokens = Math.round(reduction * avgWastePerSession * totals.totalSessionsWithLearnings);
10478
+ const avgWasteSecondsPerSession = totalSessions > 0 ? totals.totalWasteSeconds / totalSessions : 0;
10479
+ totals.estimatedSavingsSeconds = Math.round(reduction * avgWasteSecondsPerSession * totals.totalSessionsWithLearnings);
10480
+ } else {
10481
+ totals.estimatedSavingsTokens = 0;
10482
+ totals.estimatedSavingsSeconds = 0;
10483
+ }
10484
+ } else {
10485
+ totals.estimatedSavingsTokens = 0;
10486
+ totals.estimatedSavingsSeconds = 0;
10487
+ }
10021
10488
  if (stats.sessions.length > 0) {
10022
10489
  totals.firstSessionTimestamp = stats.sessions[0].timestamp;
10023
10490
  totals.lastSessionTimestamp = stats.sessions[stats.sessions.length - 1].timestamp;
@@ -10085,7 +10552,102 @@ function formatROISummary(stats) {
10085
10552
  return lines.join("\n");
10086
10553
  }
10087
10554
 
10555
+ // src/learner/attribution.ts
10556
+ init_config();
10557
+ function normalizeText(text) {
10558
+ return text.replace(/[^a-zA-Z0-9\s]/g, "").replace(/\s+/g, " ").toLowerCase().trim();
10559
+ }
10560
+ function preNormalizeLearning(summary) {
10561
+ const norm = normalizeText(summary);
10562
+ return { words: norm.split(" ").filter((w) => w.length > 3) };
10563
+ }
10564
+ function matchesNormalized(learning, normError) {
10565
+ if (learning.words.length === 0 || !normError) return false;
10566
+ const matchCount = learning.words.filter((w) => normError.includes(w)).length;
10567
+ return matchCount / learning.words.length >= 0.6;
10568
+ }
10569
+ function matchLearningsToFailures(learnings, failureEvents) {
10570
+ if (learnings.length === 0 || failureEvents.length === 0) {
10571
+ return { matchedIndices: [], unmatchedFailures: failureEvents.length };
10572
+ }
10573
+ const normalized = learnings.map((l) => preNormalizeLearning(l.summary));
10574
+ const matchedIndices = /* @__PURE__ */ new Set();
10575
+ let unmatchedFailures = 0;
10576
+ for (const event of failureEvents) {
10577
+ const errorText = typeof event.tool_response === "object" && "_truncated" in event.tool_response ? String(event.tool_response._truncated) : JSON.stringify(event.tool_response);
10578
+ const normError = normalizeText(errorText);
10579
+ let matched = false;
10580
+ for (let i = 0; i < learnings.length; i++) {
10581
+ if (matchesNormalized(normalized[i], normError)) {
10582
+ matchedIndices.add(i);
10583
+ matched = true;
10584
+ break;
10585
+ }
10586
+ }
10587
+ if (!matched) unmatchedFailures++;
10588
+ }
10589
+ return { matchedIndices: [...matchedIndices], unmatchedFailures };
10590
+ }
10591
+ async function semanticMatchFallback(learnings, failureEvents) {
10592
+ if (learnings.length === 0 || failureEvents.length === 0) {
10593
+ return { matchedIndices: [], unmatchedFailures: failureEvents.length };
10594
+ }
10595
+ const learningSummaries = learnings.map((l, i) => `${i}: ${l.summary}`).join("\n");
10596
+ const failureSummaries = failureEvents.slice(0, 10).map((e) => {
10597
+ const errorText = typeof e.tool_response === "object" && "_truncated" in e.tool_response ? String(e.tool_response._truncated).slice(0, 200) : JSON.stringify(e.tool_response).slice(0, 200);
10598
+ return `[${e.tool_name}] ${errorText}`;
10599
+ }).join("\n");
10600
+ const prompt = `Given these existing learnings (numbered):
10601
+ ${learningSummaries}
10602
+
10603
+ And these failure events from a session:
10604
+ ${failureSummaries}
10605
+
10606
+ Which learnings (by index number) are related to or could have prevented these failures?
10607
+ Return a JSON object: {"matchedIndices": [0, 2]} or {"matchedIndices": []} if none match.`;
10608
+ try {
10609
+ const fastModel = getFastModel();
10610
+ const raw = await llmCall({
10611
+ system: "You match failure patterns to existing learnings. Return only valid JSON.",
10612
+ prompt,
10613
+ maxTokens: 256,
10614
+ ...fastModel ? { model: fastModel } : {}
10615
+ });
10616
+ const json = extractJson(raw);
10617
+ if (json) {
10618
+ const parsed = JSON.parse(json);
10619
+ const indices = (parsed.matchedIndices || []).filter((i) => typeof i === "number" && i >= 0 && i < learnings.length);
10620
+ return { matchedIndices: indices, unmatchedFailures: failureEvents.length - indices.length };
10621
+ }
10622
+ } catch {
10623
+ }
10624
+ return { matchedIndices: [], unmatchedFailures: failureEvents.length };
10625
+ }
10626
+ function updateActivations(stats, matchedIndices) {
10627
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10628
+ for (const idx of matchedIndices) {
10629
+ if (idx < stats.learnings.length) {
10630
+ stats.learnings[idx].activationCount = (stats.learnings[idx].activationCount ?? 0) + 1;
10631
+ stats.learnings[idx].lastActivationTimestamp = now;
10632
+ }
10633
+ }
10634
+ }
10635
+ var DEFAULT_MIN_SESSIONS = 10;
10636
+ function findStaleLearnings(stats, minSessions = DEFAULT_MIN_SESSIONS) {
10637
+ if (stats.sessions.length < minSessions) return [];
10638
+ return stats.learnings.filter((l) => {
10639
+ const activations = l.activationCount ?? 0;
10640
+ if (activations > 0) return false;
10641
+ const createdAt = new Date(l.timestamp).getTime();
10642
+ const sessionsAfterCreation = stats.sessions.filter(
10643
+ (s) => new Date(s.timestamp).getTime() > createdAt
10644
+ ).length;
10645
+ return sessionsAfterCreation >= minSessions;
10646
+ });
10647
+ }
10648
+
10088
10649
  // src/commands/learn.ts
10650
+ init_constants();
10089
10651
  var MIN_EVENTS_FOR_ANALYSIS = 25;
10090
10652
  var MIN_EVENTS_AUTO = 10;
10091
10653
  var AUTO_SETTLE_MS = 200;
@@ -10169,25 +10731,34 @@ async function learnFinalizeCommand(options) {
10169
10731
  resetState();
10170
10732
  return;
10171
10733
  }
10172
- const events = readAllEvents();
10734
+ const allEvents = readAllEvents();
10173
10735
  const threshold = isAuto ? MIN_EVENTS_AUTO : MIN_EVENTS_FOR_ANALYSIS;
10174
- if (events.length < threshold) {
10175
- if (!isAuto) console.log(chalk23.dim(`caliber: ${events.length}/${threshold} events recorded \u2014 need more before analysis`));
10736
+ if (allEvents.length < threshold) {
10737
+ if (!isAuto) console.log(chalk23.dim(`caliber: ${allEvents.length}/${threshold} events recorded \u2014 need more before analysis`));
10176
10738
  return;
10177
10739
  }
10178
10740
  await validateModel({ fast: true });
10179
10741
  migrateInlineLearnings();
10742
+ const state = readState2();
10743
+ const analysisOffset = isIncremental ? state.lastAnalysisEventCount || 0 : 0;
10744
+ const events = analysisOffset > 0 ? allEvents.slice(analysisOffset) : allEvents;
10745
+ if (events.length < threshold) {
10746
+ if (!isAuto) console.log(chalk23.dim(`caliber: ${events.length}/${threshold} new events since last analysis \u2014 need more`));
10747
+ return;
10748
+ }
10180
10749
  const existingConfigs = readExistingConfigs(process.cwd());
10181
10750
  const existingLearnedSection = readLearnedSection();
10751
+ const existingPersonalLearnings = readPersonalLearnings();
10182
10752
  const existingSkills = existingConfigs.claudeSkills || [];
10183
10753
  const response = await analyzeEvents(
10184
10754
  events,
10185
10755
  existingConfigs.claudeMd || "",
10186
10756
  existingLearnedSection,
10187
- existingSkills
10757
+ existingSkills,
10758
+ existingPersonalLearnings
10188
10759
  );
10189
10760
  analyzed = true;
10190
- const waste = calculateSessionWaste(events);
10761
+ const waste = calculateSessionWaste(allEvents);
10191
10762
  const existingLearnedItems = existingLearnedSection ? existingLearnedSection.split("\n").filter((l) => l.startsWith("- ")).length : 0;
10192
10763
  const hadLearnings = existingLearnedItems > 0;
10193
10764
  let newLearningsProduced = 0;
@@ -10215,7 +10786,8 @@ async function learnFinalizeCommand(options) {
10215
10786
  }
10216
10787
  const wastePerLearning = Math.round(waste.totalWasteTokens / result.newItemCount);
10217
10788
  const TYPE_RE = /^\*\*\[([^\]]+)\]\*\*/;
10218
- const learningEntries = result.newItems.map((item) => {
10789
+ const explanations = response.explanations || [];
10790
+ const learningEntries = result.newItems.map((item, idx) => {
10219
10791
  const clean = item.replace(/^- /, "");
10220
10792
  const typeMatch = clean.match(TYPE_RE);
10221
10793
  return {
@@ -10223,7 +10795,8 @@ async function learnFinalizeCommand(options) {
10223
10795
  observationType: typeMatch ? typeMatch[1] : "unknown",
10224
10796
  summary: clean.replace(TYPE_RE, "").trim().slice(0, 80),
10225
10797
  wasteTokens: wastePerLearning,
10226
- sourceEventCount: events.length
10798
+ sourceEventCount: events.length,
10799
+ explanation: explanations[idx] || null
10227
10800
  };
10228
10801
  });
10229
10802
  for (const entry of learningEntries) {
@@ -10247,8 +10820,8 @@ async function learnFinalizeCommand(options) {
10247
10820
  }
10248
10821
  const sessionSummary = {
10249
10822
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10250
- sessionId: readState2().sessionId || "unknown",
10251
- eventCount: events.length,
10823
+ sessionId: state.sessionId || "unknown",
10824
+ eventCount: allEvents.length,
10252
10825
  failureCount: waste.failureCount,
10253
10826
  promptCount: waste.promptCount,
10254
10827
  wasteSeconds: Math.round(waste.totalWasteSeconds),
@@ -10261,8 +10834,21 @@ async function learnFinalizeCommand(options) {
10261
10834
  taskFailureCount: tasks.length > 0 ? taskFailureCount : void 0
10262
10835
  };
10263
10836
  const roiStats = recordSession(sessionSummary, roiLearningEntries);
10837
+ if (roiStats.learnings.length > 0 && waste.failureCount > 0) {
10838
+ const failureEvents = allEvents.filter(
10839
+ (e) => e.hook_event_name === "PostToolUseFailure"
10840
+ );
10841
+ let attribution = matchLearningsToFailures(roiStats.learnings, failureEvents);
10842
+ if (attribution.matchedIndices.length === 0 && failureEvents.length > 0) {
10843
+ attribution = await semanticMatchFallback(roiStats.learnings, failureEvents);
10844
+ }
10845
+ if (attribution.matchedIndices.length > 0) {
10846
+ updateActivations(roiStats, attribution.matchedIndices);
10847
+ writeROIStats(roiStats);
10848
+ }
10849
+ }
10264
10850
  trackLearnSessionAnalyzed({
10265
- eventCount: events.length,
10851
+ eventCount: allEvents.length,
10266
10852
  failureCount: waste.failureCount,
10267
10853
  correctionCount: waste.promptCount,
10268
10854
  hadLearningsAvailable: hadLearnings,
@@ -10285,6 +10871,12 @@ async function learnFinalizeCommand(options) {
10285
10871
  estimatedSavingsSeconds: t.estimatedSavingsSeconds,
10286
10872
  learningCount: roiStats.learnings.length
10287
10873
  });
10874
+ if (!isIncremental) {
10875
+ const staleLearnings = findStaleLearnings(roiStats);
10876
+ if (staleLearnings.length > 0 && !isAuto) {
10877
+ console.log(chalk23.yellow(`caliber: ${staleLearnings.length} learning${staleLearnings.length === 1 ? "" : "s"} never activated \u2014 run \`caliber learn list --verbose\` to review`));
10878
+ }
10879
+ }
10288
10880
  if (!isAuto && t.estimatedSavingsTokens > 0) {
10289
10881
  const totalLearnings = existingLearnedItems + newLearningsProduced;
10290
10882
  console.log(chalk23.dim(`caliber: ${totalLearnings} learnings active \u2014 est. ~${t.estimatedSavingsTokens.toLocaleString()} tokens saved across ${t.totalSessionsWithLearnings} sessions`));
@@ -10310,7 +10902,7 @@ async function learnFinalizeCommand(options) {
10310
10902
  }
10311
10903
  async function learnInstallCommand() {
10312
10904
  let anyInstalled = false;
10313
- if (fs40.existsSync(".claude")) {
10905
+ if (fs42.existsSync(".claude")) {
10314
10906
  const r = installLearningHooks();
10315
10907
  if (r.installed) {
10316
10908
  console.log(chalk23.green("\u2713") + " Claude Code learning hooks installed");
@@ -10319,7 +10911,7 @@ async function learnInstallCommand() {
10319
10911
  console.log(chalk23.dim(" Claude Code hooks already installed"));
10320
10912
  }
10321
10913
  }
10322
- if (fs40.existsSync(".cursor")) {
10914
+ if (fs42.existsSync(".cursor")) {
10323
10915
  const r = installCursorLearningHooks();
10324
10916
  if (r.installed) {
10325
10917
  console.log(chalk23.green("\u2713") + " Cursor learning hooks installed");
@@ -10328,7 +10920,7 @@ async function learnInstallCommand() {
10328
10920
  console.log(chalk23.dim(" Cursor hooks already installed"));
10329
10921
  }
10330
10922
  }
10331
- if (!fs40.existsSync(".claude") && !fs40.existsSync(".cursor")) {
10923
+ if (!fs42.existsSync(".claude") && !fs42.existsSync(".cursor")) {
10332
10924
  console.log(chalk23.yellow("No .claude/ or .cursor/ directory found."));
10333
10925
  console.log(chalk23.dim(" Run `caliber init` first, or create the directory manually."));
10334
10926
  return;
@@ -10398,16 +10990,116 @@ Learned items in CALIBER_LEARNINGS.md: ${chalk23.cyan(String(lineCount))}`);
10398
10990
  }
10399
10991
  }
10400
10992
  }
10993
+ function getAllLearnings() {
10994
+ const items = [];
10995
+ let idx = 0;
10996
+ const projectSection = readLearnedSection();
10997
+ if (projectSection) {
10998
+ for (const line of projectSection.split("\n").filter((l) => l.startsWith("- "))) {
10999
+ items.push({ text: line, source: "project", index: idx++ });
11000
+ }
11001
+ }
11002
+ const personalSection = readPersonalLearnings();
11003
+ if (personalSection) {
11004
+ for (const line of personalSection.split("\n").filter((l) => l.startsWith("- "))) {
11005
+ items.push({ text: line, source: "personal", index: idx++ });
11006
+ }
11007
+ }
11008
+ return items;
11009
+ }
11010
+ async function learnListCommand(options) {
11011
+ const items = getAllLearnings();
11012
+ if (items.length === 0) {
11013
+ console.log(chalk23.dim("No learnings yet. Run `caliber learn install` to start."));
11014
+ return;
11015
+ }
11016
+ const roiStats = options?.verbose ? readROIStats() : null;
11017
+ console.log(chalk23.bold(`
11018
+ Learnings (${items.length})
11019
+ `));
11020
+ for (const item of items) {
11021
+ const tag = item.source === "personal" ? chalk23.magenta("[personal]") : chalk23.blue("[project]");
11022
+ const display = item.text.replace(/^- /, "").slice(0, 100);
11023
+ console.log(` ${chalk23.dim(String(item.index + 1).padStart(2, " "))}. ${tag} ${display}`);
11024
+ if (options?.verbose && roiStats) {
11025
+ const match = roiStats.learnings.find((l) => display.includes(l.summary.slice(0, 40)));
11026
+ if (match) {
11027
+ const activations = match.activationCount ?? 0;
11028
+ const stale = activations === 0 && roiStats.sessions.length >= 10;
11029
+ const activationLabel = stale ? chalk23.yellow(`${activations} activations [stale]`) : chalk23.dim(`${activations} activation${activations === 1 ? "" : "s"}`);
11030
+ console.log(` ${activationLabel}`);
11031
+ if (match.explanation) {
11032
+ console.log(` ${chalk23.dim("Why: " + match.explanation.slice(0, 80))}`);
11033
+ }
11034
+ }
11035
+ }
11036
+ }
11037
+ console.log("");
11038
+ }
11039
+ async function learnDeleteCommand(indexStr) {
11040
+ const index = parseInt(indexStr, 10);
11041
+ if (isNaN(index) || index < 1) {
11042
+ console.log(chalk23.red(`Invalid index: "${indexStr}". Use a number from \`caliber learn list\`.`));
11043
+ return;
11044
+ }
11045
+ const items = getAllLearnings();
11046
+ const targetIdx = index - 1;
11047
+ if (targetIdx >= items.length) {
11048
+ console.log(chalk23.red(`Index ${index} is out of range. You have ${items.length} learnings.`));
11049
+ return;
11050
+ }
11051
+ const item = items[targetIdx];
11052
+ const filePath = item.source === "personal" ? PERSONAL_LEARNINGS_FILE : "CALIBER_LEARNINGS.md";
11053
+ if (!fs42.existsSync(filePath)) {
11054
+ console.log(chalk23.red("Learnings file not found."));
11055
+ return;
11056
+ }
11057
+ const content = fs42.readFileSync(filePath, "utf-8");
11058
+ const lines = content.split("\n");
11059
+ const bulletsOfSource = items.filter((i) => i.source === item.source);
11060
+ const posInFile = bulletsOfSource.indexOf(item);
11061
+ let bulletsSeen = 0;
11062
+ let lineToRemove = -1;
11063
+ for (let i = 0; i < lines.length; i++) {
11064
+ if (lines[i].startsWith("- ")) {
11065
+ if (bulletsSeen === posInFile) {
11066
+ lineToRemove = i;
11067
+ break;
11068
+ }
11069
+ bulletsSeen++;
11070
+ }
11071
+ }
11072
+ if (lineToRemove === -1) {
11073
+ console.log(chalk23.red("Could not locate learning in file."));
11074
+ return;
11075
+ }
11076
+ const bulletToRemove = lines[lineToRemove];
11077
+ const newLines = lines.filter((_, i) => i !== lineToRemove);
11078
+ fs42.writeFileSync(filePath, newLines.join("\n"));
11079
+ if (item.source === "personal") {
11080
+ fs42.chmodSync(filePath, 384);
11081
+ }
11082
+ const roiStats = readROIStats();
11083
+ const cleanText = bulletToRemove.replace(/^- /, "").replace(/^\*\*\[[^\]]+\]\*\*\s*/, "").trim();
11084
+ const roiIdx = roiStats.learnings.findIndex((l) => cleanText.includes(l.summary.slice(0, 30)));
11085
+ if (roiIdx !== -1) {
11086
+ roiStats.learnings.splice(roiIdx, 1);
11087
+ writeROIStats(roiStats);
11088
+ }
11089
+ console.log(chalk23.green("\u2713") + ` Removed: ${bulletToRemove.replace(/^- /, "").slice(0, 80)}`);
11090
+ }
10401
11091
 
10402
11092
  // src/commands/insights.ts
10403
11093
  import chalk24 from "chalk";
11094
+ init_constants();
10404
11095
  var MIN_SESSIONS_FULL = 20;
10405
11096
  function buildInsightsData(stats) {
10406
11097
  const t = stats.totals;
10407
11098
  const totalSessions = t.totalSessionsWithLearnings + t.totalSessionsWithoutLearnings;
10408
11099
  const failureRateWith = t.totalSessionsWithLearnings > 0 ? t.totalFailuresWithLearnings / t.totalSessionsWithLearnings : null;
10409
11100
  const failureRateWithout = t.totalSessionsWithoutLearnings > 0 ? t.totalFailuresWithoutLearnings / t.totalSessionsWithoutLearnings : null;
10410
- const failureRateImprovement = failureRateWith !== null && failureRateWithout !== null && failureRateWithout > 0 ? Math.round((1 - failureRateWith / failureRateWithout) * 100) : null;
11101
+ const hasSufficientCohorts = t.totalSessionsWithLearnings >= MIN_SESSIONS_FOR_COMPARISON && t.totalSessionsWithoutLearnings >= MIN_SESSIONS_FOR_COMPARISON;
11102
+ const failureRateImprovement = hasSufficientCohorts && failureRateWith !== null && failureRateWithout !== null && failureRateWithout > 0 ? Math.round((1 - failureRateWith / failureRateWithout) * 100) : null;
10411
11103
  let taskCount = 0;
10412
11104
  let taskSuccessCount = 0;
10413
11105
  let taskCorrectionCount = 0;
@@ -10462,6 +11154,8 @@ function displayEarlyData(data, score) {
10462
11154
  }
10463
11155
  if (data.failureRateImprovement !== null && data.failureRateImprovement > 0) {
10464
11156
  console.log(` Failure rate trend: ${chalk24.green(`${data.failureRateImprovement}% fewer`)} failures with learnings ${chalk24.dim("(early signal)")}`);
11157
+ } else if (data.totalSessions > 0 && data.failureRateImprovement === null) {
11158
+ console.log(` Failure rate trend: ${chalk24.dim("collecting data (need 3+ sessions in each group)")}`);
10465
11159
  }
10466
11160
  if (data.taskSuccessRate !== null) {
10467
11161
  console.log(` Task success rate: ${chalk24.cyan(`${data.taskSuccessRate}%`)} ${chalk24.dim(`(${data.taskCount} tasks)`)}`);
@@ -10486,6 +11180,8 @@ function displayFullInsights(data, score) {
10486
11180
  console.log(` Failure rate: ${chalk24.red(data.failureRateWithout.toFixed(1))}/session ${chalk24.dim("\u2192")} ${chalk24.green(data.failureRateWith.toFixed(1))}/session with learnings`);
10487
11181
  if (data.failureRateImprovement !== null && data.failureRateImprovement > 0) {
10488
11182
  console.log(` Improvement: ${chalk24.green(`${data.failureRateImprovement}%`)} fewer failures`);
11183
+ } else if (data.failureRateImprovement === null) {
11184
+ console.log(` Improvement: ${chalk24.dim("collecting data (need 3+ sessions in each group)")}`);
10489
11185
  }
10490
11186
  }
10491
11187
  if (data.totalWasteTokens > 0 || data.estimatedSavingsTokens > 0) {
@@ -10527,10 +11223,173 @@ async function insightsCommand(options) {
10527
11223
  }
10528
11224
  }
10529
11225
 
11226
+ // src/commands/sources.ts
11227
+ import fs43 from "fs";
11228
+ import path33 from "path";
11229
+ import chalk25 from "chalk";
11230
+ async function sourcesListCommand() {
11231
+ const dir = process.cwd();
11232
+ const configSources = loadSourcesConfig(dir);
11233
+ const workspaces = getDetectedWorkspaces(dir);
11234
+ if (configSources.length === 0 && workspaces.length === 0) {
11235
+ console.log(chalk25.dim("\n No sources configured.\n"));
11236
+ console.log(chalk25.dim(" Add a source: ") + chalk25.hex("#83D1EB")("caliber sources add <path>"));
11237
+ console.log(chalk25.dim(" Or add to .caliber/sources.json manually.\n"));
11238
+ return;
11239
+ }
11240
+ console.log(chalk25.bold("\n External Sources\n"));
11241
+ if (configSources.length > 0) {
11242
+ for (const source of configSources) {
11243
+ const sourcePath = source.path || source.url || "";
11244
+ const exists = source.path ? fs43.existsSync(path33.resolve(dir, source.path)) : false;
11245
+ const status = exists ? chalk25.green("reachable") : chalk25.red("not found");
11246
+ const hasSummary = source.path && fs43.existsSync(path33.join(path33.resolve(dir, source.path), ".caliber", "summary.json"));
11247
+ console.log(` ${chalk25.bold(source.role || source.type)} ${chalk25.dim(sourcePath)}`);
11248
+ console.log(` Type: ${source.type} Status: ${status}${hasSummary ? " " + chalk25.cyan("has summary.json") : ""}`);
11249
+ if (source.description) console.log(` ${chalk25.dim(source.description)}`);
11250
+ console.log("");
11251
+ }
11252
+ }
11253
+ if (workspaces.length > 0) {
11254
+ console.log(chalk25.dim(" Auto-detected workspaces:"));
11255
+ for (const ws of workspaces) {
11256
+ const exists = fs43.existsSync(path33.resolve(dir, ws));
11257
+ console.log(` ${exists ? chalk25.green("\u25CF") : chalk25.red("\u25CF")} ${ws}`);
11258
+ }
11259
+ console.log("");
11260
+ }
11261
+ }
11262
+ async function sourcesAddCommand(sourcePath) {
11263
+ const dir = process.cwd();
11264
+ const absPath = path33.resolve(dir, sourcePath);
11265
+ if (!fs43.existsSync(absPath)) {
11266
+ console.log(chalk25.red(`
11267
+ Path not found: ${sourcePath}
11268
+ `));
11269
+ throw new Error("__exit__");
11270
+ }
11271
+ const type = detectSourceType(absPath);
11272
+ if (isInsideDir(absPath, dir)) {
11273
+ console.log(chalk25.red(`
11274
+ Cannot add a path inside the current project as a source.
11275
+ `));
11276
+ throw new Error("__exit__");
11277
+ }
11278
+ const existing = loadSourcesConfig(dir);
11279
+ const alreadyConfigured = existing.some(
11280
+ (s) => s.path && path33.resolve(dir, s.path) === absPath
11281
+ );
11282
+ if (alreadyConfigured) {
11283
+ console.log(chalk25.yellow(`
11284
+ Already configured: ${sourcePath}
11285
+ `));
11286
+ return;
11287
+ }
11288
+ const defaultRole = type === "repo" ? "related-repo" : "reference-doc";
11289
+ const roleInput = await promptInput(`Role (e.g., shared-library, deployment) [${defaultRole}]:`);
11290
+ const role = roleInput || defaultRole;
11291
+ const description = await promptInput("Brief description (optional):");
11292
+ const newSource = {
11293
+ type,
11294
+ path: sourcePath,
11295
+ role,
11296
+ ...description ? { description } : {}
11297
+ };
11298
+ existing.push(newSource);
11299
+ writeSourcesConfig(dir, existing);
11300
+ console.log(chalk25.green(`
11301
+ \u2713 Added ${sourcePath} as ${type} source (${role})
11302
+ `));
11303
+ }
11304
+ async function sourcesRemoveCommand(name) {
11305
+ const dir = process.cwd();
11306
+ const existing = loadSourcesConfig(dir);
11307
+ const idx = existing.findIndex(
11308
+ (s) => s.path?.includes(name) || s.role === name
11309
+ );
11310
+ if (idx === -1) {
11311
+ console.log(chalk25.red(`
11312
+ Source not found: ${name}
11313
+ `));
11314
+ console.log(chalk25.dim(" Available sources:"));
11315
+ for (const s of existing) {
11316
+ console.log(chalk25.dim(` ${s.path || s.url} (${s.role || s.type})`));
11317
+ }
11318
+ throw new Error("__exit__");
11319
+ }
11320
+ const removed = existing.splice(idx, 1)[0];
11321
+ writeSourcesConfig(dir, existing);
11322
+ console.log(chalk25.green(`
11323
+ \u2713 Removed ${removed.path || removed.url} (${removed.role || removed.type})
11324
+ `));
11325
+ }
11326
+
11327
+ // src/commands/publish.ts
11328
+ import fs44 from "fs";
11329
+ import path34 from "path";
11330
+ import chalk26 from "chalk";
11331
+ import ora7 from "ora";
11332
+ init_config();
11333
+ async function publishCommand() {
11334
+ const dir = process.cwd();
11335
+ const config = loadConfig();
11336
+ if (!config) {
11337
+ console.log(chalk26.red("No LLM provider configured. Run ") + chalk26.hex("#83D1EB")("caliber config") + chalk26.red(" first."));
11338
+ throw new Error("__exit__");
11339
+ }
11340
+ const spinner = ora7("Generating project summary...").start();
11341
+ try {
11342
+ const fingerprint = await collectFingerprint(dir);
11343
+ const claudeMd = readFileOrNull(path34.join(dir, "CLAUDE.md"));
11344
+ const topLevelDirs = fingerprint.fileTree.filter((f) => f.endsWith("/") && !f.includes("/")).map((f) => f.replace(/\/$/, ""));
11345
+ const summary = {
11346
+ name: fingerprint.packageName || path34.basename(dir),
11347
+ version: "1.0.0",
11348
+ description: fingerprint.description || "",
11349
+ languages: fingerprint.languages,
11350
+ frameworks: fingerprint.frameworks,
11351
+ tools: fingerprint.tools,
11352
+ topLevelDirs
11353
+ };
11354
+ if (claudeMd) {
11355
+ summary.conventions = claudeMd.slice(0, 2e3);
11356
+ }
11357
+ try {
11358
+ const pkgContent = readFileOrNull(path34.join(dir, "package.json"));
11359
+ if (pkgContent) {
11360
+ const pkg3 = JSON.parse(pkgContent);
11361
+ if (pkg3.scripts) {
11362
+ const commands = {};
11363
+ for (const key of ["test", "build", "dev", "lint", "start"]) {
11364
+ if (pkg3.scripts[key]) commands[key] = `npm run ${key}`;
11365
+ }
11366
+ if (Object.keys(commands).length > 0) summary.commands = commands;
11367
+ }
11368
+ }
11369
+ } catch {
11370
+ }
11371
+ const outputDir = path34.join(dir, ".caliber");
11372
+ if (!fs44.existsSync(outputDir)) {
11373
+ fs44.mkdirSync(outputDir, { recursive: true });
11374
+ }
11375
+ const outputPath = path34.join(outputDir, "summary.json");
11376
+ fs44.writeFileSync(outputPath, JSON.stringify(summary, null, 2) + "\n", "utf-8");
11377
+ spinner.succeed("Project summary published");
11378
+ console.log(` ${chalk26.green("\u2713")} ${path34.relative(dir, outputPath)}`);
11379
+ console.log(chalk26.dim("\n Other projects can now reference this repo as a source."));
11380
+ console.log(chalk26.dim(" When they run `caliber init`, they'll read this summary automatically.\n"));
11381
+ } catch (err) {
11382
+ spinner.fail("Failed to generate summary");
11383
+ if (err instanceof Error && err.message === "__exit__") throw err;
11384
+ console.error(chalk26.red(err instanceof Error ? err.message : "Unknown error"));
11385
+ throw new Error("__exit__");
11386
+ }
11387
+ }
11388
+
10530
11389
  // src/cli.ts
10531
- var __dirname = path31.dirname(fileURLToPath(import.meta.url));
11390
+ var __dirname = path35.dirname(fileURLToPath(import.meta.url));
10532
11391
  var pkg = JSON.parse(
10533
- fs41.readFileSync(path31.resolve(__dirname, "..", "package.json"), "utf-8")
11392
+ fs45.readFileSync(path35.resolve(__dirname, "..", "package.json"), "utf-8")
10534
11393
  );
10535
11394
  var program = new Command();
10536
11395
  var displayVersion = process.env.CALIBER_LOCAL ? `${pkg.version}-local` : pkg.version;
@@ -10582,43 +11441,50 @@ program.hook("preAction", (thisCommand) => {
10582
11441
  });
10583
11442
  function parseAgentOption(value) {
10584
11443
  if (value === "both") return ["claude", "cursor"];
10585
- if (value === "all") return ["claude", "cursor", "codex"];
10586
- const valid = ["claude", "cursor", "codex"];
11444
+ if (value === "all") return ["claude", "cursor", "codex", "github-copilot"];
11445
+ const valid = ["claude", "cursor", "codex", "github-copilot"];
10587
11446
  const agents = [...new Set(value.split(",").map((s) => s.trim().toLowerCase()).filter((a) => valid.includes(a)))];
10588
11447
  if (agents.length === 0) {
10589
- console.error(`Invalid agent "${value}". Choose from: claude, cursor, codex (comma-separated for multiple)`);
11448
+ console.error(`Invalid agent "${value}". Choose from: claude, cursor, codex, github-copilot (comma-separated for multiple)`);
10590
11449
  process.exit(1);
10591
11450
  }
10592
11451
  return agents;
10593
11452
  }
10594
- program.command("init").description("Initialize your project for AI-assisted development").option("--agent <type>", "Target agents (comma-separated): claude, cursor, codex", parseAgentOption).option("--dry-run", "Preview changes without writing files").option("--force", "Overwrite existing setup without prompting").option("--debug-report", void 0, false).option("--show-tokens", "Show token usage summary at the end").option("--auto-approve", "Run without interactive prompts (auto-accept all)").option("--verbose", "Show detailed logs of each step").action(tracked("init", initCommand));
11453
+ program.command("init").description("Initialize your project for AI-assisted development").option("--agent <type>", "Target agents (comma-separated): claude, cursor, codex, github-copilot", parseAgentOption).option("--source <paths...>", "Related source paths to include as context").option("--dry-run", "Preview changes without writing files").option("--force", "Overwrite existing setup without prompting").option("--debug-report", void 0, false).option("--show-tokens", "Show token usage summary at the end").option("--auto-approve", "Run without interactive prompts (auto-accept all)").option("--verbose", "Show detailed logs of each step").action(tracked("init", initCommand));
10595
11454
  program.command("undo").description("Revert all config changes made by Caliber").action(tracked("undo", undoCommand));
10596
11455
  program.command("status").description("Show current Caliber setup status").option("--json", "Output as JSON").action(tracked("status", statusCommand));
10597
11456
  program.command("regenerate").alias("regen").alias("re").description("Re-analyze project and regenerate setup").option("--dry-run", "Preview changes without writing files").action(tracked("regenerate", regenerateCommand));
10598
11457
  program.command("config").description("Configure LLM provider, API key, and model").action(tracked("config", configCommand));
10599
11458
  program.command("skills").description("Discover and install community skills for your project").action(tracked("skills", recommendCommand));
10600
- program.command("score").description("Score your current agent config setup (deterministic, no network)").option("--json", "Output as JSON").option("--quiet", "One-line output for scripts/hooks").option("--agent <type>", "Target agents (comma-separated): claude, cursor, codex", parseAgentOption).option("--compare <ref>", "Compare score against a git ref (branch, tag, or SHA)").action(tracked("score", scoreCommand));
11459
+ program.command("score").description("Score your current agent config setup (deterministic, no network)").option("--json", "Output as JSON").option("--quiet", "One-line output for scripts/hooks").option("--agent <type>", "Target agents (comma-separated): claude, cursor, codex, github-copilot", parseAgentOption).option("--compare <ref>", "Compare score against a git ref (branch, tag, or SHA)").action(tracked("score", scoreCommand));
10601
11460
  program.command("refresh").description("Update docs based on recent code changes").option("--quiet", "Suppress output (for use in hooks)").option("--dry-run", "Preview changes without writing files").action(tracked("refresh", refreshCommand));
10602
11461
  program.command("hooks").description("Manage auto-refresh hooks (toggle interactively)").option("--install", "Enable all hooks non-interactively").option("--remove", "Disable all hooks non-interactively").action(tracked("hooks", hooksCommand));
10603
11462
  program.command("insights").description("Show agent performance insights and learning impact").option("--json", "Output as JSON").action(tracked("insights", insightsCommand));
11463
+ var sources = program.command("sources").description("Manage external context sources (related repos, docs)");
11464
+ sources.command("list").description("Show configured and auto-detected sources").action(tracked("sources:list", sourcesListCommand));
11465
+ sources.command("add").description("Add an external source").argument("<path>", "Path to repo directory or file").action(tracked("sources:add", sourcesAddCommand));
11466
+ sources.command("remove").description("Remove a configured source").argument("<name>", "Source path or role to remove").action(tracked("sources:remove", sourcesRemoveCommand));
11467
+ program.command("publish").description("Generate a machine-readable summary for other repos to consume").action(tracked("publish", publishCommand));
10604
11468
  var learn = program.command("learn", { hidden: true }).description("[dev] Session learning \u2014 observe tool usage and extract reusable instructions");
10605
11469
  learn.command("observe").description("Record a tool event from stdin (called by hooks)").option("--failure", "Mark event as a tool failure").option("--prompt", "Record a user prompt event").action(tracked("learn:observe", learnObserveCommand));
10606
11470
  learn.command("finalize").description("Analyze session events and update CALIBER_LEARNINGS.md (called on SessionEnd)").option("--force", "Skip the running-process check (for manual invocation)").option("--auto", "Silent mode for hooks (lower threshold, no interactive output)").option("--incremental", "Extract learnings mid-session without clearing events").action(tracked("learn:finalize", (opts) => learnFinalizeCommand(opts)));
10607
11471
  learn.command("install").description("Install learning hooks into .claude/settings.json").action(tracked("learn:install", learnInstallCommand));
10608
11472
  learn.command("remove").description("Remove learning hooks from .claude/settings.json").action(tracked("learn:remove", learnRemoveCommand));
10609
11473
  learn.command("status").description("Show learning system status").action(tracked("learn:status", learnStatusCommand));
11474
+ learn.command("list").description("List all learnings with their source and activation data").option("--verbose", "Show explanations and activation counts").action(tracked("learn:list", (opts) => learnListCommand(opts)));
11475
+ learn.command("delete <index>").description("Delete a learning by its index number (from `caliber learn list`)").action(tracked("learn:delete", (index) => learnDeleteCommand(index)));
10610
11476
 
10611
11477
  // src/utils/version-check.ts
10612
- import fs42 from "fs";
10613
- import path32 from "path";
11478
+ import fs46 from "fs";
11479
+ import path36 from "path";
10614
11480
  import { fileURLToPath as fileURLToPath2 } from "url";
10615
11481
  import { execSync as execSync15 } from "child_process";
10616
- import chalk25 from "chalk";
10617
- import ora7 from "ora";
11482
+ import chalk27 from "chalk";
11483
+ import ora8 from "ora";
10618
11484
  import confirm2 from "@inquirer/confirm";
10619
- var __dirname_vc = path32.dirname(fileURLToPath2(import.meta.url));
11485
+ var __dirname_vc = path36.dirname(fileURLToPath2(import.meta.url));
10620
11486
  var pkg2 = JSON.parse(
10621
- fs42.readFileSync(path32.resolve(__dirname_vc, "..", "package.json"), "utf-8")
11487
+ fs46.readFileSync(path36.resolve(__dirname_vc, "..", "package.json"), "utf-8")
10622
11488
  );
10623
11489
  function getChannel(version) {
10624
11490
  const match = version.match(/-(dev|next)\./);
@@ -10643,8 +11509,8 @@ function isNewer(registry, current) {
10643
11509
  function getInstalledVersion() {
10644
11510
  try {
10645
11511
  const globalRoot = execSync15("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
10646
- const pkgPath = path32.join(globalRoot, "@rely-ai", "caliber", "package.json");
10647
- return JSON.parse(fs42.readFileSync(pkgPath, "utf-8")).version;
11512
+ const pkgPath = path36.join(globalRoot, "@rely-ai", "caliber", "package.json");
11513
+ return JSON.parse(fs46.readFileSync(pkgPath, "utf-8")).version;
10648
11514
  } catch {
10649
11515
  return null;
10650
11516
  }
@@ -10669,17 +11535,17 @@ async function checkForUpdates() {
10669
11535
  if (!isInteractive) {
10670
11536
  const installTag = channel === "latest" ? "" : `@${channel}`;
10671
11537
  console.log(
10672
- chalk25.yellow(
11538
+ chalk27.yellow(
10673
11539
  `
10674
11540
  Update available: ${current} -> ${latest}
10675
- Run ${chalk25.bold(`npm install -g @rely-ai/caliber${installTag}`)} to upgrade.
11541
+ Run ${chalk27.bold(`npm install -g @rely-ai/caliber${installTag}`)} to upgrade.
10676
11542
  `
10677
11543
  )
10678
11544
  );
10679
11545
  return;
10680
11546
  }
10681
11547
  console.log(
10682
- chalk25.yellow(`
11548
+ chalk27.yellow(`
10683
11549
  Update available: ${current} -> ${latest}`)
10684
11550
  );
10685
11551
  const shouldUpdate = await confirm2({ message: "Would you like to update now? (Y/n)", default: true });
@@ -10688,7 +11554,7 @@ Update available: ${current} -> ${latest}`)
10688
11554
  return;
10689
11555
  }
10690
11556
  const tag = channel === "latest" ? latest : channel;
10691
- const spinner = ora7("Updating caliber...").start();
11557
+ const spinner = ora8("Updating caliber...").start();
10692
11558
  try {
10693
11559
  execSync15(`npm install -g @rely-ai/caliber@${tag}`, {
10694
11560
  stdio: "pipe",
@@ -10698,13 +11564,13 @@ Update available: ${current} -> ${latest}`)
10698
11564
  const installed = getInstalledVersion();
10699
11565
  if (installed !== latest) {
10700
11566
  spinner.fail(`Update incomplete \u2014 got ${installed ?? "unknown"}, expected ${latest}`);
10701
- console.log(chalk25.yellow(`Run ${chalk25.bold(`npm install -g @rely-ai/caliber@${tag}`)} manually.
11567
+ console.log(chalk27.yellow(`Run ${chalk27.bold(`npm install -g @rely-ai/caliber@${tag}`)} manually.
10702
11568
  `));
10703
11569
  return;
10704
11570
  }
10705
- spinner.succeed(chalk25.green(`Updated to ${latest}`));
11571
+ spinner.succeed(chalk27.green(`Updated to ${latest}`));
10706
11572
  const args = process.argv.slice(2);
10707
- console.log(chalk25.dim(`
11573
+ console.log(chalk27.dim(`
10708
11574
  Restarting: caliber ${args.join(" ")}
10709
11575
  `));
10710
11576
  execSync15(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
@@ -10717,11 +11583,11 @@ Restarting: caliber ${args.join(" ")}
10717
11583
  if (err instanceof Error) {
10718
11584
  const stderr = err.stderr;
10719
11585
  const errMsg = stderr ? String(stderr).trim().split("\n").pop() : err.message.split("\n")[0];
10720
- if (errMsg && !errMsg.includes("SIGTERM")) console.log(chalk25.dim(` ${errMsg}`));
11586
+ if (errMsg && !errMsg.includes("SIGTERM")) console.log(chalk27.dim(` ${errMsg}`));
10721
11587
  }
10722
11588
  console.log(
10723
- chalk25.yellow(
10724
- `Run ${chalk25.bold(`npm install -g @rely-ai/caliber@${tag}`)} manually to upgrade.
11589
+ chalk27.yellow(
11590
+ `Run ${chalk27.bold(`npm install -g @rely-ai/caliber@${tag}`)} manually to upgrade.
10725
11591
  `
10726
11592
  )
10727
11593
  );