compose-agentsmd 4.0.0 → 5.0.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/README.md CHANGED
@@ -115,11 +115,16 @@ Ruleset keys:
115
115
  - `global` (optional): write `rules/global` to user-global instruction files (defaults to true). Set `false` to skip global writes.
116
116
  - `domains` (optional): domain folders under `rules/domains/<domain>`.
117
117
  - `extra` (optional): additional local rule files to append.
118
+ - `budget` (optional): global-rule budget thresholds in `o200k_base` tokens.
119
+ - `budget.totalTokens` (optional): total token budget for the composed global instruction output (defaults to `4500`).
120
+ - `budget.moduleTokens` (optional): per-module token budget for each composed global rule section (defaults to `400`).
118
121
  - `claude` (optional): repository companion settings for Claude Code.
119
122
  - `claude.enabled` (optional): enable/disable companion generation (defaults to `true`).
120
123
  - `claude.output` (optional): companion file path (defaults to `CLAUDE.md`).
121
124
  - `output` (optional): repository output file name (defaults to `AGENTS.md`).
122
125
 
126
+ When the composed global instruction output exceeds either budget, the CLI emits a warning to `stderr`. The machine-readable `--json` output includes the tokenizer name, total token count, and any over-budget modules.
127
+
123
128
  ### Ruleset schema validation
124
129
 
125
130
  `compose-agentsmd` validates rulesets against `agent-ruleset.schema.json` on every run. If the ruleset does not conform to the schema, the tool exits with a schema error.
@@ -48,11 +48,11 @@
48
48
  "type": "object",
49
49
  "additionalProperties": false,
50
50
  "properties": {
51
- "totalLines": {
51
+ "totalTokens": {
52
52
  "type": "integer",
53
53
  "minimum": 1
54
54
  },
55
- "moduleLines": {
55
+ "moduleTokens": {
56
56
  "type": "integer",
57
57
  "minimum": 1
58
58
  }
@@ -6,6 +6,8 @@ import { execFileSync } from "node:child_process";
6
6
  import readline from "node:readline";
7
7
  import { Ajv } from "ajv";
8
8
  import { createTwoFilesPatch } from "diff";
9
+ import { countTokens } from "gpt-tokenizer";
10
+ import { prepareGitFallbackDestination } from "./git-fallback.js";
9
11
  const DEFAULT_RULESET_NAME = "agent-ruleset.json";
10
12
  const DEFAULT_OUTPUT = "AGENTS.md";
11
13
  const DEFAULT_CLAUDE_OUTPUT = "CLAUDE.md";
@@ -22,8 +24,9 @@ const RULESET_SCHEMA_PATH = new URL("../agent-ruleset.schema.json", import.meta.
22
24
  const PACKAGE_JSON_PATH = new URL("../package.json", import.meta.url);
23
25
  const TOOL_RULES_PATH = new URL("../tools/tool-rules.md", import.meta.url);
24
26
  const USAGE_PATH = new URL("../tools/usage.txt", import.meta.url);
25
- const DEFAULT_TOTAL_BUDGET = 350;
26
- const DEFAULT_MODULE_BUDGET = 30;
27
+ const BUDGET_TOKENIZER = "o200k_base";
28
+ const DEFAULT_TOTAL_BUDGET = 4500;
29
+ const DEFAULT_MODULE_BUDGET = 400;
27
30
  const LINT_HEADER = "<!-- markdownlint-disable MD025 -->";
28
31
  const readValueArg = (remaining, index, flag) => {
29
32
  const value = remaining[index + 1];
@@ -441,7 +444,7 @@ const cloneAtRef = (repoUrl, ref, destination) => {
441
444
  execGit(["clone", "--depth", "1", "--branch", ref, repoUrl, destination]);
442
445
  };
443
446
  const fetchCommit = (repoUrl, commitHash, destination) => {
444
- ensureDir(destination);
447
+ prepareGitFallbackDestination(destination);
445
448
  execGit(["init"], destination);
446
449
  execGit(["remote", "add", "origin", repoUrl], destination);
447
450
  execGit(["fetch", "--depth", "1", "origin", commitHash], destination);
@@ -584,6 +587,12 @@ const buildInstructionContent = (parts, includeToolRules) => {
584
587
  }
585
588
  return `${LINT_HEADER}\n${sections.join("\n\n")}\n`;
586
589
  };
590
+ const countBudgetTokens = (content) => {
591
+ if (content.length === 0) {
592
+ return 0;
593
+ }
594
+ return countTokens(content);
595
+ };
587
596
  const buildScopeDiff = (scope, targetPaths, desiredContent, rootDir) => {
588
597
  if (targetPaths.length === 0) {
589
598
  return undefined;
@@ -634,32 +643,36 @@ const composeRuleset = (rulesetPath, rootDir, options) => {
634
643
  const extraRules = Array.isArray(projectRuleset.extra) ? projectRuleset.extra : [];
635
644
  const directRulePaths = extraRules.map((rulePath) => resolveFrom(rulesetDir, rulePath));
636
645
  addRulePaths(directRulePaths, resolvedRepositoryRules, seenRepositoryRules);
637
- const totalBudget = projectRuleset.budget?.totalLines ?? DEFAULT_TOTAL_BUDGET;
638
- const moduleBudget = projectRuleset.budget?.moduleLines ?? DEFAULT_MODULE_BUDGET;
639
- const normalizedGlobalRoot = normalizePath(path.resolve(globalRoot));
640
- const globalRuleFiles = resolvedGlobalRules.filter((p) => normalizePath(p).startsWith(`${normalizedGlobalRoot}/`));
641
- const moduleLineCounts = globalRuleFiles.map((filePath) => {
642
- const content = fs.readFileSync(filePath, "utf8");
643
- return { name: path.basename(filePath), lines: content.split("\n").length };
644
- });
645
- const totalLines = moduleLineCounts.reduce((sum, m) => sum + m.lines, 0);
646
- const overBudgetModules = moduleLineCounts.filter((m) => m.lines > moduleBudget);
647
- const budgetResult = {
648
- totalLines,
649
- totalBudget,
650
- moduleBudget,
651
- overBudgetModules,
652
- exceeded: totalLines > totalBudget || overBudgetModules.length > 0
653
- };
646
+ const totalBudget = projectRuleset.budget?.totalTokens ?? DEFAULT_TOTAL_BUDGET;
647
+ const moduleBudget = projectRuleset.budget?.moduleTokens ?? DEFAULT_MODULE_BUDGET;
654
648
  const buildRuleParts = (rulePaths) => rulePaths.map((rulePath) => {
655
649
  const body = normalizeTrailingWhitespace(fs.readFileSync(rulePath, "utf8"));
656
650
  const sourcePath = formatRuleSourcePath(rulePath, rulesRoot, rulesetDir, projectRuleset.source, resolvedRef);
657
- return `Source: ${sourcePath}\n\n${body}`;
651
+ return {
652
+ name: path.basename(rulePath),
653
+ content: `Source: ${sourcePath}\n\n${body}`
654
+ };
658
655
  });
659
656
  const repositoryParts = buildRuleParts(resolvedRepositoryRules);
660
657
  const globalParts = buildRuleParts(resolvedGlobalRules);
661
- const primaryOutputContent = buildInstructionContent(repositoryParts, true);
662
- const globalOutputContent = buildInstructionContent(globalParts, false);
658
+ const repositoryContentParts = repositoryParts.map((part) => part.content);
659
+ const globalContentParts = globalParts.map((part) => part.content);
660
+ const primaryOutputContent = buildInstructionContent(repositoryContentParts, true);
661
+ const globalOutputContent = buildInstructionContent(globalContentParts, false);
662
+ const moduleTokenCounts = globalParts.map((part) => ({
663
+ name: part.name,
664
+ tokens: countBudgetTokens(part.content)
665
+ }));
666
+ const totalTokens = countBudgetTokens(globalOutputContent);
667
+ const overBudgetModules = moduleTokenCounts.filter((module) => module.tokens > moduleBudget);
668
+ const budgetResult = {
669
+ tokenizer: BUDGET_TOKENIZER,
670
+ totalTokens,
671
+ totalBudget,
672
+ moduleBudget,
673
+ overBudgetModules,
674
+ exceeded: totalTokens > totalBudget || overBudgetModules.length > 0
675
+ };
663
676
  const repositoryOutputs = [toDisplayPath(rootDir, primaryOutputPath)];
664
677
  const globalOutputs = globalOutputPaths.map((filePath) => toDisplayPath(rootDir, filePath));
665
678
  const composedFiles = [
@@ -747,14 +760,14 @@ const formatComposedOutputs = (result) => {
747
760
  return `${lines.join("\n")}\n`;
748
761
  };
749
762
  const formatBudgetWarning = (result) => {
750
- const totalInfo = result.totalLines > result.totalBudget
751
- ? `: ${result.totalLines}/${result.totalBudget} lines`
763
+ const totalInfo = result.totalTokens > result.totalBudget
764
+ ? `: ${result.totalTokens}/${result.totalBudget} tokens`
752
765
  : "";
753
- const lines = [`⚠ Global rules budget exceeded${totalInfo}`];
766
+ const lines = [`⚠ Global rules budget exceeded (${result.tokenizer})${totalInfo}`];
754
767
  if (result.overBudgetModules.length > 0) {
755
- lines.push(` Over-budget modules (>${result.moduleBudget} lines):`);
768
+ lines.push(` Over-budget modules (> ${result.moduleBudget} tokens):`);
756
769
  for (const mod of result.overBudgetModules) {
757
- lines.push(` ${mod.name}: ${mod.lines} lines`);
770
+ lines.push(` ${mod.name}: ${mod.tokens} tokens`);
758
771
  }
759
772
  }
760
773
  return `${lines.join("\n")}\n`;
@@ -0,0 +1,7 @@
1
+ import fs from "node:fs";
2
+ export const prepareGitFallbackDestination = (destination) => {
3
+ if (fs.existsSync(destination)) {
4
+ fs.rmSync(destination, { recursive: true, force: true });
5
+ }
6
+ fs.mkdirSync(destination, { recursive: true });
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compose-agentsmd",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "CLI tools for composing repository-local and user-global agent instruction files from modular rule sets",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -62,7 +62,8 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "ajv": "^8.17.1",
65
- "diff": "^8.0.3"
65
+ "diff": "^8.0.3",
66
+ "gpt-tokenizer": "^3.4.0"
66
67
  },
67
68
  "overrides": {
68
69
  "minimatch": "^10.2.4",