compose-agentsmd 3.5.1 → 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 +30 -17
- package/agent-ruleset.schema.json +5 -4
- package/dist/compose-agents.js +224 -96
- package/dist/git-fallback.js +7 -0
- package/package.json +6 -4
- package/tools/usage.txt +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Compose AGENTS.md
|
|
2
2
|
|
|
3
|
-
This repository contains CLI tooling for composing
|
|
3
|
+
This repository contains CLI tooling for composing repository-local and user-global agent instruction files from modular rule sets.
|
|
4
4
|
|
|
5
5
|
## Compatibility
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ This provides the `compose-agentsmd` command.
|
|
|
24
24
|
|
|
25
25
|
## Rules setup (this repository)
|
|
26
26
|
|
|
27
|
-
The default ruleset for this repository is `agent-ruleset.json` and currently composes the `node` domain into
|
|
27
|
+
The default ruleset for this repository is `agent-ruleset.json` and currently composes the `node` domain into repository-local instructions from the shared GitHub source.
|
|
28
28
|
|
|
29
29
|
## Compose
|
|
30
30
|
|
|
@@ -34,14 +34,22 @@ From each project root, run:
|
|
|
34
34
|
compose-agentsmd
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
The tool reads `agent-ruleset.json` from the given root directory (default: current working directory), and writes the output file specified by the ruleset. If `output` is omitted, it defaults to `AGENTS.md`.
|
|
37
|
+
The tool reads `agent-ruleset.json` from the given root directory (default: current working directory), and writes the repository-local output file specified by the ruleset. If `output` is omitted, it defaults to `AGENTS.md`.
|
|
38
38
|
|
|
39
39
|
By default, compose also writes a `CLAUDE.md` companion file containing an `@...` import pointing to the primary output file. You can disable this with `claude.enabled: false` in the ruleset.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
Each composed rule section is also prefixed with the source file path that produced it.
|
|
41
|
+
By default, compose writes `rules/global` to these user-global instruction files with the same composed content:
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
- `~/.codex/AGENTS.md`
|
|
44
|
+
- `~/.claude/CLAUDE.md`
|
|
45
|
+
- `~/.gemini/GEMINI.md`
|
|
46
|
+
- `~/.copilot/copilot-instructions.md`
|
|
47
|
+
|
|
48
|
+
Repository-local `AGENTS.md` contains the tool rules plus only the repository-facing rules (`domains` + `extra`). Global rules are no longer embedded into each repository output.
|
|
49
|
+
|
|
50
|
+
Each composed rule section is prefixed with the source file path that produced it.
|
|
51
|
+
|
|
52
|
+
When compose changes files, the CLI prints diffs for both repository outputs and global outputs. This works even when the project is not under git. `--quiet` and `--json` suppress this output.
|
|
45
53
|
|
|
46
54
|
## Setup (init)
|
|
47
55
|
|
|
@@ -56,11 +64,11 @@ Defaults:
|
|
|
56
64
|
- `source`: `github:owner/repo@latest`
|
|
57
65
|
- `domains`: empty
|
|
58
66
|
- `extra`: empty
|
|
59
|
-
- `global`: omitted (defaults to `true
|
|
67
|
+
- `global`: omitted (defaults to `true`, meaning write user-global instruction files)
|
|
60
68
|
- `claude`: `{ "enabled": true, "output": "CLAUDE.md" }`
|
|
61
69
|
- `output`: `AGENTS.md`
|
|
62
70
|
|
|
63
|
-
Use `--dry-run` to preview actions, `--force` to overwrite existing files, and `--compose` to generate
|
|
71
|
+
Use `--dry-run` to preview actions, `--force` to overwrite existing repository output files, and `--compose` to generate instruction files immediately.
|
|
64
72
|
|
|
65
73
|
## Updating shared rules
|
|
66
74
|
|
|
@@ -77,7 +85,7 @@ compose-agentsmd edit-rules
|
|
|
77
85
|
compose-agentsmd apply-rules
|
|
78
86
|
```
|
|
79
87
|
|
|
80
|
-
`edit-rules` clones the GitHub source into the workspace (or reuses it), then prints the workspace path, rules directory, and next steps. `apply-rules` pushes the workspace (if clean) and regenerates
|
|
88
|
+
`edit-rules` clones the GitHub source into the workspace (or reuses it), then prints the workspace path, rules directory, and next steps. `apply-rules` pushes the workspace (if clean) and regenerates repository/global instruction files by refreshing the cache. If your `source` is a local path, `edit-rules` points to the local workspace and `apply-rules` skips the push.
|
|
81
89
|
|
|
82
90
|
## Project ruleset format
|
|
83
91
|
|
|
@@ -91,12 +99,12 @@ Ruleset files accept JSON with `//` or `/* */` comments.
|
|
|
91
99
|
"domains": ["node", "unreal"],
|
|
92
100
|
// Additional local rule files to append.
|
|
93
101
|
"extra": ["agent-rules-local/custom.md"],
|
|
94
|
-
// Optional Claude Code companion output.
|
|
102
|
+
// Optional Claude Code repository companion output.
|
|
95
103
|
"claude": {
|
|
96
104
|
"enabled": true,
|
|
97
105
|
"output": "CLAUDE.md"
|
|
98
106
|
},
|
|
99
|
-
//
|
|
107
|
+
// Repository output file name.
|
|
100
108
|
"output": "AGENTS.md"
|
|
101
109
|
}
|
|
102
110
|
```
|
|
@@ -104,13 +112,18 @@ Ruleset files accept JSON with `//` or `/* */` comments.
|
|
|
104
112
|
Ruleset keys:
|
|
105
113
|
|
|
106
114
|
- `source` (required): rules source. Use `github:owner/repo@ref` or a local path.
|
|
107
|
-
- `global` (optional):
|
|
115
|
+
- `global` (optional): write `rules/global` to user-global instruction files (defaults to true). Set `false` to skip global writes.
|
|
108
116
|
- `domains` (optional): domain folders under `rules/domains/<domain>`.
|
|
109
117
|
- `extra` (optional): additional local rule files to append.
|
|
110
|
-
- `
|
|
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`).
|
|
121
|
+
- `claude` (optional): repository companion settings for Claude Code.
|
|
111
122
|
- `claude.enabled` (optional): enable/disable companion generation (defaults to `true`).
|
|
112
123
|
- `claude.output` (optional): companion file path (defaults to `CLAUDE.md`).
|
|
113
|
-
- `output` (optional): output file name (defaults to `AGENTS.md`).
|
|
124
|
+
- `output` (optional): repository output file name (defaults to `AGENTS.md`).
|
|
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.
|
|
114
127
|
|
|
115
128
|
### Ruleset schema validation
|
|
116
129
|
|
|
@@ -132,11 +145,11 @@ Remote sources are cached under `~/.agentsmd/cache/<owner>/<repo>/<ref>/`. Use `
|
|
|
132
145
|
- `--source <source>`: rules source for `init`
|
|
133
146
|
- `--domains <list>`: comma-separated domains for `init`
|
|
134
147
|
- `--extra <list>`: comma-separated extra rules for `init`
|
|
135
|
-
- `--output <file>`: output filename for `init`
|
|
148
|
+
- `--output <file>`: repository output filename for `init`
|
|
136
149
|
- `--no-domains`: initialize with no domains
|
|
137
150
|
- `--no-extra`: initialize without extra rule files
|
|
138
|
-
- `--no-global`: initialize without global rules
|
|
139
|
-
- `--compose`: compose
|
|
151
|
+
- `--no-global`: initialize without user-global rules
|
|
152
|
+
- `--compose`: compose repository and user-global instruction files after `init`
|
|
140
153
|
- `--dry-run`: show init plan without writing files
|
|
141
154
|
- `--yes`: skip init confirmation prompt
|
|
142
155
|
- `--force`: overwrite existing files during init
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"title": "Compose
|
|
3
|
+
"title": "Compose agent instruction files ruleset",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"additionalProperties": false,
|
|
6
6
|
"required": ["source"],
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"minLength": 1
|
|
11
11
|
},
|
|
12
12
|
"global": {
|
|
13
|
-
"type": "boolean"
|
|
13
|
+
"type": "boolean",
|
|
14
|
+
"description": "Write rules/global to user-global instruction files."
|
|
14
15
|
},
|
|
15
16
|
"output": {
|
|
16
17
|
"type": "string",
|
|
@@ -47,11 +48,11 @@
|
|
|
47
48
|
"type": "object",
|
|
48
49
|
"additionalProperties": false,
|
|
49
50
|
"properties": {
|
|
50
|
-
"
|
|
51
|
+
"totalTokens": {
|
|
51
52
|
"type": "integer",
|
|
52
53
|
"minimum": 1
|
|
53
54
|
},
|
|
54
|
-
"
|
|
55
|
+
"moduleTokens": {
|
|
55
56
|
"type": "integer",
|
|
56
57
|
"minimum": 1
|
|
57
58
|
}
|
package/dist/compose-agents.js
CHANGED
|
@@ -6,9 +6,15 @@ 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";
|
|
14
|
+
const DEFAULT_CODEX_GLOBAL_OUTPUT = path.join(os.homedir(), ".codex", "AGENTS.md");
|
|
15
|
+
const DEFAULT_CLAUDE_GLOBAL_OUTPUT = path.join(os.homedir(), ".claude", "CLAUDE.md");
|
|
16
|
+
const DEFAULT_GEMINI_GLOBAL_OUTPUT = path.join(os.homedir(), ".gemini", "GEMINI.md");
|
|
17
|
+
const DEFAULT_COPILOT_GLOBAL_OUTPUT = path.join(os.homedir(), ".copilot", "copilot-instructions.md");
|
|
12
18
|
const DEFAULT_CACHE_ROOT = path.join(os.homedir(), ".agentsmd", "cache");
|
|
13
19
|
const DEFAULT_WORKSPACE_ROOT = path.join(os.homedir(), ".agentsmd", "workspace");
|
|
14
20
|
const DEFAULT_INIT_SOURCE = "github:owner/repo@latest";
|
|
@@ -18,8 +24,10 @@ const RULESET_SCHEMA_PATH = new URL("../agent-ruleset.schema.json", import.meta.
|
|
|
18
24
|
const PACKAGE_JSON_PATH = new URL("../package.json", import.meta.url);
|
|
19
25
|
const TOOL_RULES_PATH = new URL("../tools/tool-rules.md", import.meta.url);
|
|
20
26
|
const USAGE_PATH = new URL("../tools/usage.txt", import.meta.url);
|
|
21
|
-
const
|
|
22
|
-
const
|
|
27
|
+
const BUDGET_TOKENIZER = "o200k_base";
|
|
28
|
+
const DEFAULT_TOTAL_BUDGET = 4500;
|
|
29
|
+
const DEFAULT_MODULE_BUDGET = 400;
|
|
30
|
+
const LINT_HEADER = "<!-- markdownlint-disable MD025 -->";
|
|
23
31
|
const readValueArg = (remaining, index, flag) => {
|
|
24
32
|
const value = remaining[index + 1];
|
|
25
33
|
if (!value) {
|
|
@@ -182,6 +190,22 @@ const resolveFrom = (baseDir, targetPath) => {
|
|
|
182
190
|
}
|
|
183
191
|
return path.resolve(baseDir, targetPath);
|
|
184
192
|
};
|
|
193
|
+
const isSubPath = (baseDir, targetPath) => {
|
|
194
|
+
const relativePath = path.relative(path.resolve(baseDir), path.resolve(targetPath));
|
|
195
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
196
|
+
};
|
|
197
|
+
const toDisplayPath = (rootDir, filePath) => {
|
|
198
|
+
if (isSubPath(rootDir, filePath)) {
|
|
199
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
200
|
+
return normalizePath(relativePath || path.basename(filePath));
|
|
201
|
+
}
|
|
202
|
+
const homeDir = os.homedir();
|
|
203
|
+
if (isSubPath(homeDir, filePath)) {
|
|
204
|
+
const relativeToHome = normalizePath(path.relative(homeDir, filePath));
|
|
205
|
+
return relativeToHome ? `~/${relativeToHome}` : "~";
|
|
206
|
+
}
|
|
207
|
+
return normalizePath(path.resolve(filePath));
|
|
208
|
+
};
|
|
185
209
|
const ensureDir = (dirPath) => {
|
|
186
210
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
187
211
|
};
|
|
@@ -420,7 +444,7 @@ const cloneAtRef = (repoUrl, ref, destination) => {
|
|
|
420
444
|
execGit(["clone", "--depth", "1", "--branch", ref, repoUrl, destination]);
|
|
421
445
|
};
|
|
422
446
|
const fetchCommit = (repoUrl, commitHash, destination) => {
|
|
423
|
-
|
|
447
|
+
prepareGitFallbackDestination(destination);
|
|
424
448
|
execGit(["init"], destination);
|
|
425
449
|
execGit(["remote", "add", "origin", repoUrl], destination);
|
|
426
450
|
execGit(["fetch", "--depth", "1", "origin", commitHash], destination);
|
|
@@ -530,94 +554,163 @@ const formatRuleSourcePath = (rulePath, rulesRoot, rulesetDir, source, resolvedR
|
|
|
530
554
|
const result = normalizePath(path.relative(rulesetDir, rulePath));
|
|
531
555
|
return result;
|
|
532
556
|
};
|
|
557
|
+
const getGlobalOutputPaths = () => [
|
|
558
|
+
DEFAULT_CODEX_GLOBAL_OUTPUT,
|
|
559
|
+
DEFAULT_CLAUDE_GLOBAL_OUTPUT,
|
|
560
|
+
DEFAULT_GEMINI_GLOBAL_OUTPUT,
|
|
561
|
+
DEFAULT_COPILOT_GLOBAL_OUTPUT
|
|
562
|
+
];
|
|
533
563
|
const resolveOutputPaths = (rulesetDir, projectRuleset) => {
|
|
534
564
|
const primaryOutputPath = resolveFrom(rulesetDir, projectRuleset.output ?? DEFAULT_OUTPUT);
|
|
535
565
|
const claude = projectRuleset.claude ?? {};
|
|
536
566
|
const companionEnabled = claude.enabled !== false;
|
|
537
567
|
const configuredCompanionPath = resolveFrom(rulesetDir, claude.output ?? DEFAULT_CLAUDE_OUTPUT);
|
|
568
|
+
const globalOutputPaths = projectRuleset.global === false ? [] : getGlobalOutputPaths();
|
|
538
569
|
if (!companionEnabled ||
|
|
539
570
|
path.resolve(primaryOutputPath) === path.resolve(configuredCompanionPath)) {
|
|
540
|
-
return { primaryOutputPath };
|
|
571
|
+
return { primaryOutputPath, globalOutputPaths };
|
|
541
572
|
}
|
|
542
|
-
return {
|
|
573
|
+
return {
|
|
574
|
+
primaryOutputPath,
|
|
575
|
+
companionOutputPath: configuredCompanionPath,
|
|
576
|
+
globalOutputPaths
|
|
577
|
+
};
|
|
543
578
|
};
|
|
544
579
|
const buildClaudeCompanionContent = (primaryOutputPath, companionOutputPath) => {
|
|
545
580
|
const relativeImportPath = normalizePath(path.relative(path.dirname(companionOutputPath), primaryOutputPath));
|
|
546
581
|
return `@${relativeImportPath}\n`;
|
|
547
582
|
};
|
|
583
|
+
const buildInstructionContent = (parts, includeToolRules) => {
|
|
584
|
+
const sections = includeToolRules ? [normalizeTrailingWhitespace(TOOL_RULES), ...parts] : parts;
|
|
585
|
+
if (sections.length === 0) {
|
|
586
|
+
return "";
|
|
587
|
+
}
|
|
588
|
+
return `${LINT_HEADER}\n${sections.join("\n\n")}\n`;
|
|
589
|
+
};
|
|
590
|
+
const countBudgetTokens = (content) => {
|
|
591
|
+
if (content.length === 0) {
|
|
592
|
+
return 0;
|
|
593
|
+
}
|
|
594
|
+
return countTokens(content);
|
|
595
|
+
};
|
|
596
|
+
const buildScopeDiff = (scope, targetPaths, desiredContent, rootDir) => {
|
|
597
|
+
if (targetPaths.length === 0) {
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
const displayTargets = targetPaths.map((filePath) => toDisplayPath(rootDir, filePath));
|
|
601
|
+
const changedTargetPath = targetPaths.find((filePath) => {
|
|
602
|
+
if (!fs.existsSync(filePath)) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
return fs.readFileSync(filePath, "utf8") !== desiredContent;
|
|
606
|
+
});
|
|
607
|
+
if (!changedTargetPath) {
|
|
608
|
+
return {
|
|
609
|
+
scope,
|
|
610
|
+
targets: displayTargets,
|
|
611
|
+
status: "unchanged"
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const before = fs.existsSync(changedTargetPath) ? fs.readFileSync(changedTargetPath, "utf8") : "";
|
|
615
|
+
const displayPath = toDisplayPath(rootDir, changedTargetPath);
|
|
616
|
+
return {
|
|
617
|
+
scope,
|
|
618
|
+
targets: displayTargets,
|
|
619
|
+
status: "updated",
|
|
620
|
+
patch: createTwoFilesPatch(`a/${displayPath}`, `b/${displayPath}`, before, desiredContent, "", "", { context: 3 })
|
|
621
|
+
};
|
|
622
|
+
};
|
|
548
623
|
const composeRuleset = (rulesetPath, rootDir, options) => {
|
|
549
624
|
const rulesetDir = path.dirname(rulesetPath);
|
|
550
625
|
const projectRuleset = readProjectRuleset(rulesetPath);
|
|
551
|
-
const { primaryOutputPath, companionOutputPath } = resolveOutputPaths(rulesetDir, projectRuleset);
|
|
552
|
-
const composedOutputPath =
|
|
626
|
+
const { primaryOutputPath, companionOutputPath, globalOutputPaths } = resolveOutputPaths(rulesetDir, projectRuleset);
|
|
627
|
+
const composedOutputPath = toDisplayPath(rootDir, primaryOutputPath);
|
|
553
628
|
const { rulesRoot, resolvedRef } = resolveRulesRoot(rulesetDir, projectRuleset.source, options.refresh ?? false);
|
|
554
629
|
const globalRoot = path.join(rulesRoot, "global");
|
|
555
630
|
const domainsRoot = path.join(rulesRoot, "domains");
|
|
556
|
-
const
|
|
557
|
-
const
|
|
631
|
+
const resolvedGlobalRules = [];
|
|
632
|
+
const seenGlobalRules = new Set();
|
|
633
|
+
const resolvedRepositoryRules = [];
|
|
634
|
+
const seenRepositoryRules = new Set();
|
|
558
635
|
if (projectRuleset.global !== false) {
|
|
559
|
-
addRulePaths(collectMarkdownFiles(globalRoot),
|
|
636
|
+
addRulePaths(collectMarkdownFiles(globalRoot), resolvedGlobalRules, seenGlobalRules);
|
|
560
637
|
}
|
|
561
638
|
const domains = Array.isArray(projectRuleset.domains) ? projectRuleset.domains : [];
|
|
562
639
|
for (const domain of domains) {
|
|
563
640
|
const domainRoot = path.resolve(domainsRoot, domain);
|
|
564
|
-
addRulePaths(collectMarkdownFiles(domainRoot),
|
|
641
|
+
addRulePaths(collectMarkdownFiles(domainRoot), resolvedRepositoryRules, seenRepositoryRules);
|
|
565
642
|
}
|
|
566
643
|
const extraRules = Array.isArray(projectRuleset.extra) ? projectRuleset.extra : [];
|
|
567
644
|
const directRulePaths = extraRules.map((rulePath) => resolveFrom(rulesetDir, rulePath));
|
|
568
|
-
addRulePaths(directRulePaths,
|
|
569
|
-
const totalBudget = projectRuleset.budget?.
|
|
570
|
-
const moduleBudget = projectRuleset.budget?.
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
645
|
+
addRulePaths(directRulePaths, resolvedRepositoryRules, seenRepositoryRules);
|
|
646
|
+
const totalBudget = projectRuleset.budget?.totalTokens ?? DEFAULT_TOTAL_BUDGET;
|
|
647
|
+
const moduleBudget = projectRuleset.budget?.moduleTokens ?? DEFAULT_MODULE_BUDGET;
|
|
648
|
+
const buildRuleParts = (rulePaths) => rulePaths.map((rulePath) => {
|
|
649
|
+
const body = normalizeTrailingWhitespace(fs.readFileSync(rulePath, "utf8"));
|
|
650
|
+
const sourcePath = formatRuleSourcePath(rulePath, rulesRoot, rulesetDir, projectRuleset.source, resolvedRef);
|
|
651
|
+
return {
|
|
652
|
+
name: path.basename(rulePath),
|
|
653
|
+
content: `Source: ${sourcePath}\n\n${body}`
|
|
654
|
+
};
|
|
576
655
|
});
|
|
577
|
-
const
|
|
578
|
-
const
|
|
656
|
+
const repositoryParts = buildRuleParts(resolvedRepositoryRules);
|
|
657
|
+
const globalParts = buildRuleParts(resolvedGlobalRules);
|
|
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);
|
|
579
668
|
const budgetResult = {
|
|
580
|
-
|
|
669
|
+
tokenizer: BUDGET_TOKENIZER,
|
|
670
|
+
totalTokens,
|
|
581
671
|
totalBudget,
|
|
582
672
|
moduleBudget,
|
|
583
673
|
overBudgetModules,
|
|
584
|
-
exceeded:
|
|
674
|
+
exceeded: totalTokens > totalBudget || overBudgetModules.length > 0
|
|
585
675
|
};
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
const sourcePath = formatRuleSourcePath(rulePath, rulesRoot, rulesetDir, projectRuleset.source, resolvedRef);
|
|
589
|
-
return `Source: ${sourcePath}\n\n${body}`;
|
|
590
|
-
});
|
|
591
|
-
const lintHeader = "<!-- markdownlint-disable MD025 -->";
|
|
592
|
-
const toolRules = normalizeTrailingWhitespace(TOOL_RULES);
|
|
593
|
-
const primaryOutputContent = `${lintHeader}\n${[toolRules, ...parts].join("\n\n")}\n`;
|
|
676
|
+
const repositoryOutputs = [toDisplayPath(rootDir, primaryOutputPath)];
|
|
677
|
+
const globalOutputs = globalOutputPaths.map((filePath) => toDisplayPath(rootDir, filePath));
|
|
594
678
|
const composedFiles = [
|
|
595
679
|
{
|
|
596
680
|
absolutePath: primaryOutputPath,
|
|
597
|
-
relativePath:
|
|
598
|
-
content: primaryOutputContent
|
|
681
|
+
relativePath: toDisplayPath(rootDir, primaryOutputPath),
|
|
682
|
+
content: primaryOutputContent,
|
|
683
|
+
scope: "repository"
|
|
599
684
|
}
|
|
600
685
|
];
|
|
601
686
|
if (companionOutputPath) {
|
|
687
|
+
const companionDisplayPath = toDisplayPath(rootDir, companionOutputPath);
|
|
688
|
+
repositoryOutputs.push(companionDisplayPath);
|
|
602
689
|
composedFiles.push({
|
|
603
690
|
absolutePath: companionOutputPath,
|
|
604
|
-
relativePath:
|
|
605
|
-
content: buildClaudeCompanionContent(primaryOutputPath, companionOutputPath)
|
|
691
|
+
relativePath: companionDisplayPath,
|
|
692
|
+
content: buildClaudeCompanionContent(primaryOutputPath, companionOutputPath),
|
|
693
|
+
scope: "repository"
|
|
606
694
|
});
|
|
607
695
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
:
|
|
613
|
-
|
|
614
|
-
|
|
696
|
+
for (const globalOutputPath of globalOutputPaths) {
|
|
697
|
+
composedFiles.push({
|
|
698
|
+
absolutePath: globalOutputPath,
|
|
699
|
+
relativePath: toDisplayPath(rootDir, globalOutputPath),
|
|
700
|
+
content: globalOutputContent,
|
|
701
|
+
scope: "global"
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
const outputDiffs = [];
|
|
705
|
+
if (options.emitDiffs) {
|
|
706
|
+
const repositoryDiff = buildScopeDiff("repository", [primaryOutputPath], primaryOutputContent, rootDir);
|
|
707
|
+
if (repositoryDiff) {
|
|
708
|
+
repositoryDiff.targets = repositoryOutputs;
|
|
709
|
+
outputDiffs.push(repositoryDiff);
|
|
615
710
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
patch: createTwoFilesPatch(`a/${composedOutputPath}`, `b/${composedOutputPath}`, before, primaryOutputContent, "", "", { context: 3 })
|
|
620
|
-
};
|
|
711
|
+
const globalDiff = buildScopeDiff("global", globalOutputPaths, globalOutputContent, rootDir);
|
|
712
|
+
if (globalDiff) {
|
|
713
|
+
outputDiffs.push(globalDiff);
|
|
621
714
|
}
|
|
622
715
|
}
|
|
623
716
|
if (!options.dryRun) {
|
|
@@ -628,38 +721,53 @@ const composeRuleset = (rulesetPath, rootDir, options) => {
|
|
|
628
721
|
}
|
|
629
722
|
return {
|
|
630
723
|
output: composedOutputPath,
|
|
631
|
-
outputs:
|
|
632
|
-
|
|
724
|
+
outputs: [...repositoryOutputs, ...globalOutputs],
|
|
725
|
+
repositoryOutputs,
|
|
726
|
+
globalOutputs,
|
|
727
|
+
outputDiffs,
|
|
633
728
|
budgetResult
|
|
634
729
|
};
|
|
635
730
|
};
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
731
|
+
const printOutputDiffs = (result) => {
|
|
732
|
+
for (const diff of result.outputDiffs) {
|
|
733
|
+
const scopeLabel = diff.scope === "global" ? "Global outputs" : "Repository outputs";
|
|
734
|
+
if (diff.status === "unchanged") {
|
|
735
|
+
process.stdout.write(`${scopeLabel} unchanged.\n`);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
process.stdout.write(`${scopeLabel} updated. ACTION (agent): refresh rule recognition from the diff below.\n`);
|
|
739
|
+
process.stdout.write(`Targets:\n${diff.targets.map((target) => `- ${target}`).join("\n")}\n`);
|
|
740
|
+
process.stdout.write(`--- BEGIN ${diff.scope.toUpperCase()} DIFF ---\n`);
|
|
741
|
+
if (diff.patch) {
|
|
742
|
+
process.stdout.write(diff.patch);
|
|
743
|
+
if (!diff.patch.endsWith("\n")) {
|
|
744
|
+
process.stdout.write("\n");
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
process.stdout.write(`--- END ${diff.scope.toUpperCase()} DIFF ---\n`);
|
|
639
748
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
749
|
+
};
|
|
750
|
+
const formatComposedOutputs = (result) => {
|
|
751
|
+
const lines = ["Composed instruction files:"];
|
|
752
|
+
if (result.repositoryOutputs.length > 0) {
|
|
753
|
+
lines.push("Repository:");
|
|
754
|
+
lines.push(...result.repositoryOutputs.map((filePath) => `- ${filePath}`));
|
|
643
755
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
process.stdout.write(result.agentsMdDiff.patch);
|
|
648
|
-
if (!result.agentsMdDiff.patch.endsWith("\n")) {
|
|
649
|
-
process.stdout.write("\n");
|
|
650
|
-
}
|
|
756
|
+
if (result.globalOutputs.length > 0) {
|
|
757
|
+
lines.push("Global:");
|
|
758
|
+
lines.push(...result.globalOutputs.map((filePath) => `- ${filePath}`));
|
|
651
759
|
}
|
|
652
|
-
|
|
760
|
+
return `${lines.join("\n")}\n`;
|
|
653
761
|
};
|
|
654
762
|
const formatBudgetWarning = (result) => {
|
|
655
|
-
const totalInfo = result.
|
|
656
|
-
? `: ${result.
|
|
763
|
+
const totalInfo = result.totalTokens > result.totalBudget
|
|
764
|
+
? `: ${result.totalTokens}/${result.totalBudget} tokens`
|
|
657
765
|
: "";
|
|
658
|
-
const lines = [`⚠ Global rules budget exceeded${totalInfo}`];
|
|
766
|
+
const lines = [`⚠ Global rules budget exceeded (${result.tokenizer})${totalInfo}`];
|
|
659
767
|
if (result.overBudgetModules.length > 0) {
|
|
660
|
-
lines.push(` Over-budget modules (
|
|
768
|
+
lines.push(` Over-budget modules (> ${result.moduleBudget} tokens):`);
|
|
661
769
|
for (const mod of result.overBudgetModules) {
|
|
662
|
-
lines.push(` ${mod.name}: ${mod.
|
|
770
|
+
lines.push(` ${mod.name}: ${mod.tokens} tokens`);
|
|
663
771
|
}
|
|
664
772
|
}
|
|
665
773
|
return `${lines.join("\n")}\n`;
|
|
@@ -704,7 +812,7 @@ const formatInitRuleset = (ruleset) => {
|
|
|
704
812
|
` "extra": ${extraValue},`
|
|
705
813
|
];
|
|
706
814
|
if (ruleset.global === false) {
|
|
707
|
-
lines.push(" //
|
|
815
|
+
lines.push(" // Write shared global rules to user-level instruction files.");
|
|
708
816
|
lines.push(' "global": false,');
|
|
709
817
|
}
|
|
710
818
|
lines.push(" // Claude Code companion output settings.");
|
|
@@ -720,7 +828,7 @@ const formatInitRuleset = (ruleset) => {
|
|
|
720
828
|
const formatPlan = (items, rootDir) => {
|
|
721
829
|
const lines = items.map((item) => {
|
|
722
830
|
const verb = item.action === "overwrite" ? "Overwrite" : "Create";
|
|
723
|
-
const relative =
|
|
831
|
+
const relative = toDisplayPath(rootDir, item.path);
|
|
724
832
|
return `- ${verb}: ${relative}`;
|
|
725
833
|
});
|
|
726
834
|
return `Init plan:\n${lines.join("\n")}\n`;
|
|
@@ -768,19 +876,25 @@ const initProject = async (args, rootDir, rulesetName) => {
|
|
|
768
876
|
extraToWrite.push(extraPath);
|
|
769
877
|
}
|
|
770
878
|
if (args.compose) {
|
|
771
|
-
const composedTargets = [
|
|
879
|
+
const composedTargets = [
|
|
880
|
+
{ path: outputPaths.primaryOutputPath, requireForce: true },
|
|
881
|
+
...outputPaths.globalOutputPaths.map((outputPath) => ({
|
|
882
|
+
path: outputPath,
|
|
883
|
+
requireForce: false
|
|
884
|
+
}))
|
|
885
|
+
];
|
|
772
886
|
if (outputPaths.companionOutputPath) {
|
|
773
|
-
composedTargets.push(outputPaths.companionOutputPath);
|
|
887
|
+
composedTargets.push({ path: outputPaths.companionOutputPath, requireForce: true });
|
|
774
888
|
}
|
|
775
889
|
for (const composedTarget of composedTargets) {
|
|
776
|
-
if (fs.existsSync(composedTarget)) {
|
|
777
|
-
if (!args.force) {
|
|
778
|
-
throw new Error(`Output already exists: ${normalizePath(composedTarget)} (use --force to overwrite)`);
|
|
890
|
+
if (fs.existsSync(composedTarget.path)) {
|
|
891
|
+
if (composedTarget.requireForce && !args.force) {
|
|
892
|
+
throw new Error(`Output already exists: ${normalizePath(composedTarget.path)} (use --force to overwrite)`);
|
|
779
893
|
}
|
|
780
|
-
plan.push({ action: "overwrite", path: composedTarget });
|
|
894
|
+
plan.push({ action: "overwrite", path: composedTarget.path });
|
|
781
895
|
}
|
|
782
896
|
else {
|
|
783
|
-
plan.push({ action: "create", path: composedTarget });
|
|
897
|
+
plan.push({ action: "create", path: composedTarget.path });
|
|
784
898
|
}
|
|
785
899
|
}
|
|
786
900
|
}
|
|
@@ -793,7 +907,7 @@ const initProject = async (args, rootDir, rulesetName) => {
|
|
|
793
907
|
dryRun: true,
|
|
794
908
|
plan: plan.map((item) => ({
|
|
795
909
|
action: item.action,
|
|
796
|
-
path:
|
|
910
|
+
path: toDisplayPath(rootDir, item.path)
|
|
797
911
|
}))
|
|
798
912
|
}, null, 2) + "\n");
|
|
799
913
|
}
|
|
@@ -813,28 +927,30 @@ const initProject = async (args, rootDir, rulesetName) => {
|
|
|
813
927
|
if (args.compose) {
|
|
814
928
|
composedOutput = composeRuleset(rulesetPath, rootDir, {
|
|
815
929
|
refresh: args.refresh ?? false,
|
|
816
|
-
|
|
930
|
+
emitDiffs: !args.quiet && !args.json
|
|
817
931
|
});
|
|
818
932
|
}
|
|
819
933
|
if (args.json) {
|
|
820
934
|
process.stdout.write(JSON.stringify({
|
|
821
|
-
initialized: [
|
|
822
|
-
localRules: extraToWrite.map((filePath) =>
|
|
935
|
+
initialized: [toDisplayPath(rootDir, rulesetPath)],
|
|
936
|
+
localRules: extraToWrite.map((filePath) => toDisplayPath(rootDir, filePath)),
|
|
823
937
|
composed: composedOutput ? composedOutput.outputs : [],
|
|
938
|
+
repositoryOutputs: composedOutput ? composedOutput.repositoryOutputs : [],
|
|
939
|
+
globalOutputs: composedOutput ? composedOutput.globalOutputs : [],
|
|
824
940
|
dryRun: false,
|
|
825
941
|
...(composedOutput ? { budget: composedOutput.budgetResult } : {})
|
|
826
942
|
}, null, 2) + "\n");
|
|
827
943
|
}
|
|
828
944
|
else if (!args.quiet) {
|
|
829
|
-
process.stdout.write(`Initialized ruleset:\n- ${
|
|
945
|
+
process.stdout.write(`Initialized ruleset:\n- ${toDisplayPath(rootDir, rulesetPath)}\n`);
|
|
830
946
|
if (extraToWrite.length > 0) {
|
|
831
947
|
process.stdout.write(`Initialized local rules:\n${extraToWrite
|
|
832
|
-
.map((filePath) => `- ${
|
|
948
|
+
.map((filePath) => `- ${toDisplayPath(rootDir, filePath)}`)
|
|
833
949
|
.join("\n")}\n`);
|
|
834
950
|
}
|
|
835
951
|
if (composedOutput) {
|
|
836
|
-
process.stdout.write(
|
|
837
|
-
|
|
952
|
+
process.stdout.write(formatComposedOutputs(composedOutput));
|
|
953
|
+
printOutputDiffs(composedOutput);
|
|
838
954
|
if (composedOutput.budgetResult.exceeded) {
|
|
839
955
|
process.stderr.write(formatBudgetWarning(composedOutput.budgetResult));
|
|
840
956
|
}
|
|
@@ -905,7 +1021,7 @@ const main = async () => {
|
|
|
905
1021
|
"Next steps:",
|
|
906
1022
|
`- Edit rule files under: ${rulesDirectory}`,
|
|
907
1023
|
"- If this source is GitHub, commit and push the workspace changes before apply-rules.",
|
|
908
|
-
"- Run compose-agentsmd apply-rules from your project root to apply updates and regenerate
|
|
1024
|
+
"- Run compose-agentsmd apply-rules from your project root to apply updates and regenerate instruction files."
|
|
909
1025
|
].join("\n") + "\n");
|
|
910
1026
|
return;
|
|
911
1027
|
}
|
|
@@ -921,14 +1037,20 @@ const main = async () => {
|
|
|
921
1037
|
const output = composeRuleset(rulesetPath, rootDir, {
|
|
922
1038
|
refresh: true,
|
|
923
1039
|
dryRun: args.dryRun,
|
|
924
|
-
|
|
1040
|
+
emitDiffs: !args.quiet && !args.json
|
|
925
1041
|
});
|
|
926
1042
|
if (args.json) {
|
|
927
|
-
process.stdout.write(JSON.stringify({
|
|
1043
|
+
process.stdout.write(JSON.stringify({
|
|
1044
|
+
composed: output.outputs,
|
|
1045
|
+
repositoryOutputs: output.repositoryOutputs,
|
|
1046
|
+
globalOutputs: output.globalOutputs,
|
|
1047
|
+
dryRun: !!args.dryRun,
|
|
1048
|
+
budget: output.budgetResult
|
|
1049
|
+
}, null, 2) + "\n");
|
|
928
1050
|
}
|
|
929
1051
|
else if (!args.quiet) {
|
|
930
|
-
process.stdout.write(
|
|
931
|
-
|
|
1052
|
+
process.stdout.write(formatComposedOutputs(output));
|
|
1053
|
+
printOutputDiffs(output);
|
|
932
1054
|
if (output.budgetResult.exceeded) {
|
|
933
1055
|
process.stderr.write(formatBudgetWarning(output.budgetResult));
|
|
934
1056
|
}
|
|
@@ -942,22 +1064,28 @@ const main = async () => {
|
|
|
942
1064
|
const outputs = rulesetFiles.sort().map((rulesetPath) => composeRuleset(rulesetPath, rootDir, {
|
|
943
1065
|
refresh: args.refresh,
|
|
944
1066
|
dryRun: args.dryRun,
|
|
945
|
-
|
|
1067
|
+
emitDiffs: !args.quiet && !args.json
|
|
946
1068
|
}));
|
|
947
1069
|
if (args.json) {
|
|
948
1070
|
process.stdout.write(JSON.stringify({
|
|
949
1071
|
composed: outputs.flatMap((result) => result.outputs),
|
|
1072
|
+
repositoryOutputs: outputs.flatMap((result) => result.repositoryOutputs),
|
|
1073
|
+
globalOutputs: outputs.flatMap((result) => result.globalOutputs),
|
|
950
1074
|
dryRun: !!args.dryRun,
|
|
951
1075
|
budget: outputs[0].budgetResult
|
|
952
1076
|
}, null, 2) + "\n");
|
|
953
1077
|
}
|
|
954
1078
|
else if (!args.quiet) {
|
|
955
|
-
process.stdout.write(
|
|
956
|
-
|
|
957
|
-
.
|
|
958
|
-
.
|
|
1079
|
+
process.stdout.write(formatComposedOutputs({
|
|
1080
|
+
output: outputs[0].output,
|
|
1081
|
+
outputs: outputs.flatMap((result) => result.outputs),
|
|
1082
|
+
repositoryOutputs: outputs.flatMap((result) => result.repositoryOutputs),
|
|
1083
|
+
globalOutputs: outputs.flatMap((result) => result.globalOutputs),
|
|
1084
|
+
outputDiffs: [],
|
|
1085
|
+
budgetResult: outputs[0].budgetResult
|
|
1086
|
+
}));
|
|
959
1087
|
for (const result of outputs) {
|
|
960
|
-
|
|
1088
|
+
printOutputDiffs(result);
|
|
961
1089
|
}
|
|
962
1090
|
for (const result of outputs) {
|
|
963
1091
|
if (result.budgetResult.exceeded) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compose-agentsmd",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI tools for composing
|
|
3
|
+
"version": "5.0.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -62,10 +62,12 @@
|
|
|
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
|
-
"minimatch": "^10.2.
|
|
69
|
+
"minimatch": "^10.2.4",
|
|
70
|
+
"rollup": "^4.59.0"
|
|
69
71
|
},
|
|
70
72
|
"lint-staged": {
|
|
71
73
|
"**/*.ts": [
|
package/tools/usage.txt
CHANGED
|
@@ -12,11 +12,11 @@ Options:
|
|
|
12
12
|
--source <source> Rules source for init (default: github:owner/repo@latest)
|
|
13
13
|
--domains <list> Comma-separated domains for init (default: none)
|
|
14
14
|
--extra <list> Comma-separated extra rules for init
|
|
15
|
-
--output <file>
|
|
15
|
+
--output <file> Repository output filename for init (default: AGENTS.md)
|
|
16
16
|
--no-domains Initialize with no domains
|
|
17
17
|
--no-extra Initialize without extra rule files
|
|
18
|
-
--no-global Initialize without global rules
|
|
19
|
-
--compose Compose
|
|
18
|
+
--no-global Initialize without user-global rules
|
|
19
|
+
--compose Compose repository and user-global instruction files after init
|
|
20
20
|
--dry-run Show init plan without writing files
|
|
21
21
|
--yes Skip init confirmation prompt
|
|
22
22
|
--force Overwrite existing files during init
|