compose-agentsmd 4.0.0 → 6.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): hard total token budget for the composed global instruction output (defaults to `8000`). Exceeding this is reported as a budget violation.
120
+ - `budget.moduleTokens` (optional): per-module advisory threshold for each composed global rule section (defaults to `800`). Crossing this is **not** a violation; it triggers a review prompt to check whether the listed modules contain procedural content that should move to skills (procedures belong in skills, not rules).
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 the total budget, the CLI emits a `⚠ Global rules budget exceeded` warning to `stderr`. When any module crosses the per-module advisory threshold, the CLI emits a separate `ℹ Modules over per-module review threshold` advisory to `stderr`. Both can be suppressed with `--quiet`. The machine-readable `--json` output includes `budget.totalExceeded`, `budget.moduleReviewTriggered`, the tokenizer name, total token count, and any over-threshold 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,18 @@ 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
+ // Token budgets for the composed global rules.
29
+ // - DEFAULT_TOTAL_BUDGET: hard budget for the always-loaded global rules.
30
+ // Sized to accommodate realistic invariant density (~80–120 invariants ×
31
+ // ~30–50 tokens each ≈ 5–6k tokens) plus structural margin and growth
32
+ // headroom, while staying a small fraction of the smallest target model's
33
+ // effective system-prompt window. Total exceedance is a budget violation.
34
+ // - DEFAULT_MODULE_BUDGET: per-module advisory threshold, NOT a violation.
35
+ // Crossing it triggers a review prompt to check whether the module is
36
+ // leaking procedural content (procedures belong in skills, not rules).
37
+ const DEFAULT_TOTAL_BUDGET = 8000;
38
+ const DEFAULT_MODULE_BUDGET = 800;
27
39
  const LINT_HEADER = "<!-- markdownlint-disable MD025 -->";
28
40
  const readValueArg = (remaining, index, flag) => {
29
41
  const value = remaining[index + 1];
@@ -441,7 +453,7 @@ const cloneAtRef = (repoUrl, ref, destination) => {
441
453
  execGit(["clone", "--depth", "1", "--branch", ref, repoUrl, destination]);
442
454
  };
443
455
  const fetchCommit = (repoUrl, commitHash, destination) => {
444
- ensureDir(destination);
456
+ prepareGitFallbackDestination(destination);
445
457
  execGit(["init"], destination);
446
458
  execGit(["remote", "add", "origin", repoUrl], destination);
447
459
  execGit(["fetch", "--depth", "1", "origin", commitHash], destination);
@@ -584,6 +596,12 @@ const buildInstructionContent = (parts, includeToolRules) => {
584
596
  }
585
597
  return `${LINT_HEADER}\n${sections.join("\n\n")}\n`;
586
598
  };
599
+ const countBudgetTokens = (content) => {
600
+ if (content.length === 0) {
601
+ return 0;
602
+ }
603
+ return countTokens(content);
604
+ };
587
605
  const buildScopeDiff = (scope, targetPaths, desiredContent, rootDir) => {
588
606
  if (targetPaths.length === 0) {
589
607
  return undefined;
@@ -634,32 +652,37 @@ const composeRuleset = (rulesetPath, rootDir, options) => {
634
652
  const extraRules = Array.isArray(projectRuleset.extra) ? projectRuleset.extra : [];
635
653
  const directRulePaths = extraRules.map((rulePath) => resolveFrom(rulesetDir, rulePath));
636
654
  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
- };
655
+ const totalBudget = projectRuleset.budget?.totalTokens ?? DEFAULT_TOTAL_BUDGET;
656
+ const moduleBudget = projectRuleset.budget?.moduleTokens ?? DEFAULT_MODULE_BUDGET;
654
657
  const buildRuleParts = (rulePaths) => rulePaths.map((rulePath) => {
655
658
  const body = normalizeTrailingWhitespace(fs.readFileSync(rulePath, "utf8"));
656
659
  const sourcePath = formatRuleSourcePath(rulePath, rulesRoot, rulesetDir, projectRuleset.source, resolvedRef);
657
- return `Source: ${sourcePath}\n\n${body}`;
660
+ return {
661
+ name: path.basename(rulePath),
662
+ content: `Source: ${sourcePath}\n\n${body}`
663
+ };
658
664
  });
659
665
  const repositoryParts = buildRuleParts(resolvedRepositoryRules);
660
666
  const globalParts = buildRuleParts(resolvedGlobalRules);
661
- const primaryOutputContent = buildInstructionContent(repositoryParts, true);
662
- const globalOutputContent = buildInstructionContent(globalParts, false);
667
+ const repositoryContentParts = repositoryParts.map((part) => part.content);
668
+ const globalContentParts = globalParts.map((part) => part.content);
669
+ const primaryOutputContent = buildInstructionContent(repositoryContentParts, true);
670
+ const globalOutputContent = buildInstructionContent(globalContentParts, false);
671
+ const moduleTokenCounts = globalParts.map((part) => ({
672
+ name: part.name,
673
+ tokens: countBudgetTokens(part.content)
674
+ }));
675
+ const totalTokens = countBudgetTokens(globalOutputContent);
676
+ const overBudgetModules = moduleTokenCounts.filter((module) => module.tokens > moduleBudget);
677
+ const budgetResult = {
678
+ tokenizer: BUDGET_TOKENIZER,
679
+ totalTokens,
680
+ totalBudget,
681
+ moduleBudget,
682
+ overBudgetModules,
683
+ totalExceeded: totalTokens > totalBudget,
684
+ moduleReviewTriggered: overBudgetModules.length > 0
685
+ };
663
686
  const repositoryOutputs = [toDisplayPath(rootDir, primaryOutputPath)];
664
687
  const globalOutputs = globalOutputPaths.map((filePath) => toDisplayPath(rootDir, filePath));
665
688
  const composedFiles = [
@@ -746,18 +769,20 @@ const formatComposedOutputs = (result) => {
746
769
  }
747
770
  return `${lines.join("\n")}\n`;
748
771
  };
749
- const formatBudgetWarning = (result) => {
750
- const totalInfo = result.totalLines > result.totalBudget
751
- ? `: ${result.totalLines}/${result.totalBudget} lines`
752
- : "";
753
- const lines = [`⚠ Global rules budget exceeded${totalInfo}`];
754
- if (result.overBudgetModules.length > 0) {
755
- lines.push(` Over-budget modules (>${result.moduleBudget} lines):`);
772
+ const formatBudgetReport = (result) => {
773
+ const lines = [];
774
+ if (result.totalExceeded) {
775
+ lines.push(`⚠ Global rules budget exceeded (${result.tokenizer}): ` +
776
+ `${result.totalTokens}/${result.totalBudget} tokens`);
777
+ }
778
+ if (result.moduleReviewTriggered) {
779
+ lines.push(`ℹ Modules over per-module review threshold (> ${result.moduleBudget} tokens, advisory):`);
756
780
  for (const mod of result.overBudgetModules) {
757
- lines.push(` ${mod.name}: ${mod.lines} lines`);
781
+ lines.push(` ${mod.name}: ${mod.tokens} tokens`);
758
782
  }
783
+ lines.push(" Review whether listed modules contain procedural content that should move to skills.");
759
784
  }
760
- return `${lines.join("\n")}\n`;
785
+ return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
761
786
  };
762
787
  const LOCAL_RULES_TEMPLATE = "# Local Rules\n\n- Add project-specific instructions here.\n";
763
788
  const buildInitRuleset = (args) => {
@@ -938,8 +963,9 @@ const initProject = async (args, rootDir, rulesetName) => {
938
963
  if (composedOutput) {
939
964
  process.stdout.write(formatComposedOutputs(composedOutput));
940
965
  printOutputDiffs(composedOutput);
941
- if (composedOutput.budgetResult.exceeded) {
942
- process.stderr.write(formatBudgetWarning(composedOutput.budgetResult));
966
+ if (composedOutput.budgetResult.totalExceeded ||
967
+ composedOutput.budgetResult.moduleReviewTriggered) {
968
+ process.stderr.write(formatBudgetReport(composedOutput.budgetResult));
943
969
  }
944
970
  }
945
971
  }
@@ -1038,8 +1064,8 @@ const main = async () => {
1038
1064
  else if (!args.quiet) {
1039
1065
  process.stdout.write(formatComposedOutputs(output));
1040
1066
  printOutputDiffs(output);
1041
- if (output.budgetResult.exceeded) {
1042
- process.stderr.write(formatBudgetWarning(output.budgetResult));
1067
+ if (output.budgetResult.totalExceeded || output.budgetResult.moduleReviewTriggered) {
1068
+ process.stderr.write(formatBudgetReport(output.budgetResult));
1043
1069
  }
1044
1070
  }
1045
1071
  return;
@@ -1075,8 +1101,8 @@ const main = async () => {
1075
1101
  printOutputDiffs(result);
1076
1102
  }
1077
1103
  for (const result of outputs) {
1078
- if (result.budgetResult.exceeded) {
1079
- process.stderr.write(formatBudgetWarning(result.budgetResult));
1104
+ if (result.budgetResult.totalExceeded || result.budgetResult.moduleReviewTriggered) {
1105
+ process.stderr.write(formatBudgetReport(result.budgetResult));
1080
1106
  }
1081
1107
  }
1082
1108
  }
@@ -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": "6.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",