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 +5 -0
- package/agent-ruleset.schema.json +2 -2
- package/dist/compose-agents.js +64 -38
- package/dist/git-fallback.js +7 -0
- package/package.json +3 -2
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.
|
package/dist/compose-agents.js
CHANGED
|
@@ -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
|
|
26
|
-
|
|
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
|
-
|
|
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?.
|
|
638
|
-
const moduleBudget = projectRuleset.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
|
|
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
|
|
662
|
-
const
|
|
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
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
:
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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.
|
|
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.
|
|
942
|
-
|
|
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.
|
|
1042
|
-
process.stderr.write(
|
|
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.
|
|
1079
|
-
process.stderr.write(
|
|
1104
|
+
if (result.budgetResult.totalExceeded || result.budgetResult.moduleReviewTriggered) {
|
|
1105
|
+
process.stderr.write(formatBudgetReport(result.budgetResult));
|
|
1080
1106
|
}
|
|
1081
1107
|
}
|
|
1082
1108
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compose-agentsmd",
|
|
3
|
-
"version": "
|
|
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",
|