@chamba/core 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -420,6 +420,40 @@ declare class VaultWriter {
420
420
  write(input: WriteNoteInput): Promise<WriteNoteResult>;
421
421
  }
422
422
 
423
+ interface RuleConvention {
424
+ editor: string;
425
+ /** Path relative to a repo root. */
426
+ path: string;
427
+ kind: 'file' | 'dir';
428
+ }
429
+ declare const RULE_SOURCES: readonly RuleConvention[];
430
+ /** A concrete rule file found in a repo. */
431
+ interface RuleSource {
432
+ /** Repo path relative to the workspace root (`.` for the root itself). */
433
+ repo: string;
434
+ editor: string;
435
+ /** Rule file path relative to the workspace root. */
436
+ path: string;
437
+ }
438
+ interface RuleExcerpt {
439
+ source: RuleSource;
440
+ excerpt: string;
441
+ }
442
+ /**
443
+ * Discover coding-rule files across the workspace root and each repo, for every
444
+ * editor convention in the catalog. Non-exclusive: a repo can match several.
445
+ * `repoDirs` are paths relative to the workspace root (`.` for the root).
446
+ */
447
+ declare function detectRuleSources(fs: FilesystemPort, workspaceRoot: string, repoDirs: string[]): Promise<RuleSource[]>;
448
+ /**
449
+ * Read each rule file fresh and return a clamped excerpt, bounded by a total
450
+ * budget so rules never crowd out the rest of the context.
451
+ */
452
+ declare function readRuleExcerpts(fs: FilesystemPort, workspaceRoot: string, sources: RuleSource[], opts?: {
453
+ maxCharsPerRule?: number;
454
+ totalBudget?: number;
455
+ }): Promise<RuleExcerpt[]>;
456
+
423
457
  /** Relative path (from workspace root) of the chamba workspace file. */
424
458
  declare const WORKSPACE_DIR = ".chamba";
425
459
  declare const WORKSPACE_FILE = "workspace.md";
@@ -438,6 +472,8 @@ interface Workspace {
438
472
  framework?: string;
439
473
  conventions: string[];
440
474
  projects: ProjectRef[];
475
+ /** Coding-rule files found across repos (Cursor, Claude, Trae, …). */
476
+ ruleSources: RuleSource[];
441
477
  /** Top-level directory names (without trailing slash), sorted. */
442
478
  folderMap: string[];
443
479
  }
@@ -588,6 +624,8 @@ interface ContextBuildInput {
588
624
  task: string;
589
625
  /** When set, search this Obsidian vault for notes relevant to the task. */
590
626
  vaultPath?: string;
627
+ /** Include a section with each repo's coding rules (default true). */
628
+ includeRules?: boolean;
591
629
  /** Soft cap on the produced context, in estimated tokens (~4 chars/token). */
592
630
  maxTokens?: number;
593
631
  }
@@ -604,6 +642,8 @@ declare class ContextBuilder {
604
642
  private readonly fs;
605
643
  constructor(fs: FilesystemPort);
606
644
  build(input: ContextBuildInput): Promise<BuiltContext>;
645
+ /** Each repo's coding rules (read fresh, clamped), non-exclusive across editors. */
646
+ private codingRulesSection;
607
647
  private workspaceSection;
608
648
  private notesSection;
609
649
  private searchNotes;
@@ -847,4 +887,4 @@ declare class MultiRepoWorktreeManager {
847
887
  private git;
848
888
  }
849
889
 
850
- export { AGENT_ROLES, type AgentConfig, type AgentRole, type BranchNameInput, type BuiltContext, type ChambaConfig, type CleanupMultiResult, type CleanupResult, type ClockPort, ConfigError, type ConfigFile, type ConfigSource, type ConfigSourceKind, type ContextBuildInput, ContextBuilder, type CreateMultiInput, type CreateWorktreeInput, DEFAULT_CONFIG, DEFAULT_WORKTREE_CONFIG, type DetectOptions, type DirEntry, EFFORT_LEVELS, type Effort, FakeProcess, FilesystemMemoryStore, type FilesystemPort, type GeneratePlanInput, GitDetector, type Issue, type IssueSeverity, type ListedWorktree, type LoadConfigOptions, type LoadConfigResult, MEMORY_DIR, MODEL_CATALOG, type Memory, MemoryFilesystem, type MemoryStore, type ModelInfo, type ModelProvider, MultiRepoWorktreeManager, type MultiRepoWorktreeResult, type NoteFields, ObsidianDetector, type ParseResult, type PartialWorktreeConfig, type PlanReview, type PlanWorktreesInput, type ProcessExecOptions, type ProcessHandler, type ProcessPort, type ProcessResult, type ProjectRef, REASONING_PRIORITIES, ROLE_DESCRIPTIONS, type ReasoningPriority, type RecordedCall, type RelevantNote, type RememberInput, type ResolvedConfig, type ReviewInput, Reviewer, type SubtaskSpec, VAULT_NOTES_DIR, type ValidatePlanInput, type ValidationResult, type VaultDetection, VaultWriter, WORKSPACE_DIR, WORKSPACE_FILE, WORKSPACE_RELATIVE_PATH, type WorkerKind, type Workspace, WorkspaceScanner, type WorktreeConfig, WorktreeError, type WorktreeHandle, type WorktreeLayout, WorktreeManager, type WorktreePlanItem, type WorktreeStatus, type WriteNoteInput, type WriteNoteResult, basename, buildBranchName, buildHint, buildTicketBranch, configFileSchema, copyEnvFiles, detectGitRepos, diffLines, dirname, editorWorkspaceContent, editorWorkspaceDir, extname, generatePlanTemplate, getModel, joinPath, listVaultNotes, loadConfig, modelsByProvider, normalizeVaultPath, parseChambaConfig, planWorktrees, renderNote, renderWorkspaceMarkdown, resolveEffort, resolveRole, resolveWorktreeConfig, safeTicket, slugify, slugifyForGit, suggestFilesLikelyTouched, suggestSubtasks, textsEqual, validatePlan, worktreeConfigSchema, worktreePathFor, worktreeRelativePath, writeEditorWorkspace };
890
+ export { AGENT_ROLES, type AgentConfig, type AgentRole, type BranchNameInput, type BuiltContext, type ChambaConfig, type CleanupMultiResult, type CleanupResult, type ClockPort, ConfigError, type ConfigFile, type ConfigSource, type ConfigSourceKind, type ContextBuildInput, ContextBuilder, type CreateMultiInput, type CreateWorktreeInput, DEFAULT_CONFIG, DEFAULT_WORKTREE_CONFIG, type DetectOptions, type DirEntry, EFFORT_LEVELS, type Effort, FakeProcess, FilesystemMemoryStore, type FilesystemPort, type GeneratePlanInput, GitDetector, type Issue, type IssueSeverity, type ListedWorktree, type LoadConfigOptions, type LoadConfigResult, MEMORY_DIR, MODEL_CATALOG, type Memory, MemoryFilesystem, type MemoryStore, type ModelInfo, type ModelProvider, MultiRepoWorktreeManager, type MultiRepoWorktreeResult, type NoteFields, ObsidianDetector, type ParseResult, type PartialWorktreeConfig, type PlanReview, type PlanWorktreesInput, type ProcessExecOptions, type ProcessHandler, type ProcessPort, type ProcessResult, type ProjectRef, REASONING_PRIORITIES, ROLE_DESCRIPTIONS, RULE_SOURCES, type ReasoningPriority, type RecordedCall, type RelevantNote, type RememberInput, type ResolvedConfig, type ReviewInput, Reviewer, type RuleConvention, type RuleExcerpt, type RuleSource, type SubtaskSpec, VAULT_NOTES_DIR, type ValidatePlanInput, type ValidationResult, type VaultDetection, VaultWriter, WORKSPACE_DIR, WORKSPACE_FILE, WORKSPACE_RELATIVE_PATH, type WorkerKind, type Workspace, WorkspaceScanner, type WorktreeConfig, WorktreeError, type WorktreeHandle, type WorktreeLayout, WorktreeManager, type WorktreePlanItem, type WorktreeStatus, type WriteNoteInput, type WriteNoteResult, basename, buildBranchName, buildHint, buildTicketBranch, configFileSchema, copyEnvFiles, detectGitRepos, detectRuleSources, diffLines, dirname, editorWorkspaceContent, editorWorkspaceDir, extname, generatePlanTemplate, getModel, joinPath, listVaultNotes, loadConfig, modelsByProvider, normalizeVaultPath, parseChambaConfig, planWorktrees, readRuleExcerpts, renderNote, renderWorkspaceMarkdown, resolveEffort, resolveRole, resolveWorktreeConfig, safeTicket, slugify, slugifyForGit, suggestFilesLikelyTouched, suggestSubtasks, textsEqual, validatePlan, worktreeConfigSchema, worktreePathFor, worktreeRelativePath, writeEditorWorkspace };
package/dist/index.js CHANGED
@@ -415,6 +415,19 @@ function renderWorkspaceMarkdown(ws) {
415
415
  lines.push("_None detected._");
416
416
  }
417
417
  lines.push("");
418
+ lines.push("## Coding rules");
419
+ lines.push("");
420
+ lines.push("> Rule files found per repo (read non-exclusively across editors). chamba");
421
+ lines.push("> loads their content into context at task time.");
422
+ lines.push("");
423
+ if (ws.ruleSources.length > 0) {
424
+ for (const r of ws.ruleSources) {
425
+ lines.push(`- \`${r.path}\` \u2014 ${r.editor} (${r.repo})`);
426
+ }
427
+ } else {
428
+ lines.push("_None detected._");
429
+ }
430
+ lines.push("");
418
431
  lines.push("## Active projects");
419
432
  lines.push("");
420
433
  if (ws.projects.length > 0) {
@@ -904,12 +917,12 @@ var MemoryFilesystem = class {
904
917
  const prefix = norm === "/" ? "/" : `${norm}/`;
905
918
  const seen = /* @__PURE__ */ new Set();
906
919
  const entries = [];
907
- const pushChild = (full, isDir) => {
920
+ const pushChild = (full, isDir2) => {
908
921
  if (!full.startsWith(prefix) || full === norm) return;
909
922
  const rest = full.slice(prefix.length);
910
923
  const slash = rest.indexOf("/");
911
924
  const name = slash === -1 ? rest : rest.slice(0, slash);
912
- const childIsDir = slash !== -1 || isDir;
925
+ const childIsDir = slash !== -1 || isDir2;
913
926
  if (name.length === 0 || seen.has(name)) return;
914
927
  seen.add(name);
915
928
  entries.push({ name, isDirectory: childIsDir, isFile: !childIsDir });
@@ -920,6 +933,90 @@ var MemoryFilesystem = class {
920
933
  }
921
934
  };
922
935
 
936
+ // src/workspace/rules.ts
937
+ var RULE_SOURCES = [
938
+ { editor: "Cursor", path: ".cursor/rules", kind: "dir" },
939
+ { editor: "Cursor", path: ".cursorrules", kind: "file" },
940
+ { editor: "Claude Code", path: "CLAUDE.md", kind: "file" },
941
+ { editor: "Claude Code", path: ".claude/rules", kind: "dir" },
942
+ { editor: "Windsurf", path: ".windsurfrules", kind: "file" },
943
+ { editor: "Windsurf", path: ".windsurf/rules", kind: "dir" },
944
+ { editor: "Trae", path: ".trae/rules", kind: "dir" },
945
+ { editor: "Trae", path: ".trae/project_rules.md", kind: "file" },
946
+ { editor: "GitHub Copilot", path: ".github/copilot-instructions.md", kind: "file" },
947
+ { editor: "Cline", path: ".clinerules", kind: "file" },
948
+ { editor: "Agents", path: "AGENTS.md", kind: "file" }
949
+ ];
950
+ function isRuleFile(name) {
951
+ const lower = name.toLowerCase();
952
+ return lower.endsWith(".md") || lower.endsWith(".mdc");
953
+ }
954
+ async function isDir(fs, path) {
955
+ try {
956
+ await fs.readDir(path);
957
+ return true;
958
+ } catch {
959
+ return false;
960
+ }
961
+ }
962
+ async function detectRuleSources(fs, workspaceRoot, repoDirs) {
963
+ const found = [];
964
+ const seen = /* @__PURE__ */ new Set();
965
+ const add = (repo, editor, relPath) => {
966
+ if (seen.has(relPath)) return;
967
+ seen.add(relPath);
968
+ found.push({ repo, editor, path: relPath });
969
+ };
970
+ for (const repo of repoDirs) {
971
+ const repoAbs = repo === "." ? workspaceRoot : joinPath(workspaceRoot, repo);
972
+ for (const conv of RULE_SOURCES) {
973
+ const abs = joinPath(repoAbs, conv.path);
974
+ const rel = repo === "." ? conv.path : joinPath(repo, conv.path);
975
+ if (conv.kind === "file") {
976
+ if (await fs.exists(abs)) add(repo, conv.editor, rel);
977
+ continue;
978
+ }
979
+ if (!await isDir(fs, abs)) continue;
980
+ let entries;
981
+ try {
982
+ entries = await fs.readDir(abs);
983
+ } catch {
984
+ continue;
985
+ }
986
+ for (const entry of entries) {
987
+ if (entry.isFile && isRuleFile(entry.name)) {
988
+ add(repo, conv.editor, joinPath(rel, entry.name));
989
+ }
990
+ }
991
+ }
992
+ }
993
+ return found;
994
+ }
995
+ async function readRuleExcerpts(fs, workspaceRoot, sources, opts = {}) {
996
+ const maxPer = opts.maxCharsPerRule ?? 400;
997
+ const total = opts.totalBudget ?? 1500;
998
+ const out = [];
999
+ let used = 0;
1000
+ for (const source of sources) {
1001
+ if (used >= total) break;
1002
+ let text;
1003
+ try {
1004
+ text = await fs.readFile(joinPath(workspaceRoot, source.path));
1005
+ } catch {
1006
+ continue;
1007
+ }
1008
+ const budget = Math.min(maxPer, total - used);
1009
+ const excerpt = clampExcerpt(text.trim(), budget);
1010
+ used += excerpt.length;
1011
+ out.push({ source, excerpt });
1012
+ }
1013
+ return out;
1014
+ }
1015
+ function clampExcerpt(text, maxChars) {
1016
+ if (text.length <= maxChars) return text;
1017
+ return `${text.slice(0, Math.max(0, maxChars - 1))}\u2026`;
1018
+ }
1019
+
923
1020
  // src/workspace/context-builder.ts
924
1021
  var DEFAULT_MAX_TOKENS = 2e3;
925
1022
  var NOTE_SCAN_MAX_DEPTH = 8;
@@ -955,6 +1052,9 @@ var ContextBuilder = class {
955
1052
  async build(input) {
956
1053
  const sections2 = [this.workspaceSection(input.workspace)];
957
1054
  let relevantNotes = [];
1055
+ if (input.includeRules !== false && input.workspace.ruleSources.length > 0) {
1056
+ sections2.push(await this.codingRulesSection(input.workspace));
1057
+ }
958
1058
  if (input.vaultPath) {
959
1059
  const notes = await this.searchNotes(input.vaultPath, input.task);
960
1060
  relevantNotes = notes.map((n) => n.path);
@@ -964,6 +1064,22 @@ var ContextBuilder = class {
964
1064
  const context = clamp(sections2.join("\n\n"), maxChars);
965
1065
  return { context, relevantNotes };
966
1066
  }
1067
+ /** Each repo's coding rules (read fresh, clamped), non-exclusive across editors. */
1068
+ async codingRulesSection(ws) {
1069
+ const excerpts = await readRuleExcerpts(this.fs, ws.root, ws.ruleSources);
1070
+ if (excerpts.length === 0) return "## Coding rules\n\nNo readable rule files.";
1071
+ const lines = [
1072
+ "## Coding rules",
1073
+ "",
1074
+ "Follow these per-repo rules (any editor). Read the full file for details:",
1075
+ ""
1076
+ ];
1077
+ for (const { source, excerpt } of excerpts) {
1078
+ lines.push(`### \`${source.path}\` \u2014 ${source.editor} (${source.repo})`);
1079
+ lines.push("", excerpt, "");
1080
+ }
1081
+ return lines.join("\n").trimEnd();
1082
+ }
967
1083
  workspaceSection(ws) {
968
1084
  const lines = ["## Workspace context", "", ws.description.trim()];
969
1085
  if (ws.languages.length > 0) lines.push("", `Languages: ${ws.languages.join(", ")}`);
@@ -1161,6 +1277,30 @@ var ObsidianDetector = class {
1161
1277
  }
1162
1278
  };
1163
1279
 
1280
+ // src/worktree/git-repo-detector.ts
1281
+ async function hasGitDir(fs, dir) {
1282
+ try {
1283
+ const entries = await fs.readDir(dir);
1284
+ return entries.some((e) => e.name === ".git" && e.isDirectory);
1285
+ } catch {
1286
+ return false;
1287
+ }
1288
+ }
1289
+ async function detectGitRepos(fs, workspaceRoot) {
1290
+ let entries;
1291
+ try {
1292
+ entries = await fs.readDir(workspaceRoot);
1293
+ } catch {
1294
+ return [];
1295
+ }
1296
+ const repos = [];
1297
+ for (const entry of entries) {
1298
+ if (!entry.isDirectory) continue;
1299
+ if (await hasGitDir(fs, joinPath(workspaceRoot, entry.name))) repos.push(entry.name);
1300
+ }
1301
+ return repos.sort();
1302
+ }
1303
+
1164
1304
  // src/workspace/scanner.ts
1165
1305
  var MAX_DEPTH = 6;
1166
1306
  var DEFAULT_IGNORES = [
@@ -1230,6 +1370,10 @@ var WorkspaceScanner = class {
1230
1370
  const framework = rootProject?.framework ?? projects.find((p) => p.framework)?.framework;
1231
1371
  const conventions = await this.detectConventions(root);
1232
1372
  const description = await this.detectDescription(root, rootProject, framework, languages);
1373
+ const repoDirs = [
1374
+ .../* @__PURE__ */ new Set([".", ...await detectGitRepos(this.fs, root), ...projects.map((p) => p.path)])
1375
+ ];
1376
+ const ruleSources = await detectRuleSources(this.fs, root, repoDirs);
1233
1377
  return {
1234
1378
  root,
1235
1379
  description,
@@ -1237,6 +1381,7 @@ var WorkspaceScanner = class {
1237
1381
  framework,
1238
1382
  conventions,
1239
1383
  projects,
1384
+ ruleSources,
1240
1385
  folderMap: [...acc.topDirs].sort()
1241
1386
  };
1242
1387
  }
@@ -1396,10 +1541,10 @@ function globToRegExp(glob) {
1396
1541
  }
1397
1542
  return out;
1398
1543
  }
1399
- function isIgnored(rules, relPath, isDir) {
1544
+ function isIgnored(rules, relPath, isDir2) {
1400
1545
  const base = basename(relPath);
1401
1546
  for (const rule of rules) {
1402
- if (rule.dirOnly && !isDir) continue;
1547
+ if (rule.dirOnly && !isDir2) continue;
1403
1548
  const target = rule.anchored ? relPath : base;
1404
1549
  if (rule.re.test(target)) return true;
1405
1550
  }
@@ -1563,30 +1708,6 @@ var GitDetector = class {
1563
1708
  }
1564
1709
  };
1565
1710
 
1566
- // src/worktree/git-repo-detector.ts
1567
- async function hasGitDir(fs, dir) {
1568
- try {
1569
- const entries = await fs.readDir(dir);
1570
- return entries.some((e) => e.name === ".git" && e.isDirectory);
1571
- } catch {
1572
- return false;
1573
- }
1574
- }
1575
- async function detectGitRepos(fs, workspaceRoot) {
1576
- let entries;
1577
- try {
1578
- entries = await fs.readDir(workspaceRoot);
1579
- } catch {
1580
- return [];
1581
- }
1582
- const repos = [];
1583
- for (const entry of entries) {
1584
- if (!entry.isDirectory) continue;
1585
- if (await hasGitDir(fs, joinPath(workspaceRoot, entry.name))) repos.push(entry.name);
1586
- }
1587
- return repos.sort();
1588
- }
1589
-
1590
1711
  // src/worktree/manager.ts
1591
1712
  var WorktreeError = class extends Error {
1592
1713
  name = "WorktreeError";
@@ -1789,6 +1910,7 @@ export {
1789
1910
  ObsidianDetector,
1790
1911
  REASONING_PRIORITIES,
1791
1912
  ROLE_DESCRIPTIONS,
1913
+ RULE_SOURCES,
1792
1914
  Reviewer,
1793
1915
  VAULT_NOTES_DIR,
1794
1916
  VaultWriter,
@@ -1805,6 +1927,7 @@ export {
1805
1927
  configFileSchema,
1806
1928
  copyEnvFiles,
1807
1929
  detectGitRepos,
1930
+ detectRuleSources,
1808
1931
  diffLines,
1809
1932
  dirname,
1810
1933
  editorWorkspaceContent,
@@ -1819,6 +1942,7 @@ export {
1819
1942
  normalizeVaultPath,
1820
1943
  parseChambaConfig,
1821
1944
  planWorktrees,
1945
+ readRuleExcerpts,
1822
1946
  renderNote,
1823
1947
  renderWorkspaceMarkdown,
1824
1948
  resolveEffort,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chamba/core",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Core logic for chamba: workspace scanner, plan + heuristic reviewer, git worktrees, Obsidian, memory — no Node APIs, no LLM",
5
5
  "license": "MIT",
6
6
  "type": "module",