compose-agentsmd 3.5.0 → 4.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 +25 -17
- package/agent-ruleset.schema.json +3 -2
- package/dist/compose-agents.js +189 -74
- package/package.json +4 -3
- 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,13 @@ 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
|
-
- `claude` (optional): companion settings for Claude Code.
|
|
118
|
+
- `claude` (optional): repository companion settings for Claude Code.
|
|
111
119
|
- `claude.enabled` (optional): enable/disable companion generation (defaults to `true`).
|
|
112
120
|
- `claude.output` (optional): companion file path (defaults to `CLAUDE.md`).
|
|
113
|
-
- `output` (optional): output file name (defaults to `AGENTS.md`).
|
|
121
|
+
- `output` (optional): repository output file name (defaults to `AGENTS.md`).
|
|
114
122
|
|
|
115
123
|
### Ruleset schema validation
|
|
116
124
|
|
|
@@ -132,11 +140,11 @@ Remote sources are cached under `~/.agentsmd/cache/<owner>/<repo>/<ref>/`. Use `
|
|
|
132
140
|
- `--source <source>`: rules source for `init`
|
|
133
141
|
- `--domains <list>`: comma-separated domains for `init`
|
|
134
142
|
- `--extra <list>`: comma-separated extra rules for `init`
|
|
135
|
-
- `--output <file>`: output filename for `init`
|
|
143
|
+
- `--output <file>`: repository output filename for `init`
|
|
136
144
|
- `--no-domains`: initialize with no domains
|
|
137
145
|
- `--no-extra`: initialize without extra rule files
|
|
138
|
-
- `--no-global`: initialize without global rules
|
|
139
|
-
- `--compose`: compose
|
|
146
|
+
- `--no-global`: initialize without user-global rules
|
|
147
|
+
- `--compose`: compose repository and user-global instruction files after `init`
|
|
140
148
|
- `--dry-run`: show init plan without writing files
|
|
141
149
|
- `--yes`: skip init confirmation prompt
|
|
142
150
|
- `--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",
|
package/dist/compose-agents.js
CHANGED
|
@@ -9,6 +9,10 @@ import { createTwoFilesPatch } from "diff";
|
|
|
9
9
|
const DEFAULT_RULESET_NAME = "agent-ruleset.json";
|
|
10
10
|
const DEFAULT_OUTPUT = "AGENTS.md";
|
|
11
11
|
const DEFAULT_CLAUDE_OUTPUT = "CLAUDE.md";
|
|
12
|
+
const DEFAULT_CODEX_GLOBAL_OUTPUT = path.join(os.homedir(), ".codex", "AGENTS.md");
|
|
13
|
+
const DEFAULT_CLAUDE_GLOBAL_OUTPUT = path.join(os.homedir(), ".claude", "CLAUDE.md");
|
|
14
|
+
const DEFAULT_GEMINI_GLOBAL_OUTPUT = path.join(os.homedir(), ".gemini", "GEMINI.md");
|
|
15
|
+
const DEFAULT_COPILOT_GLOBAL_OUTPUT = path.join(os.homedir(), ".copilot", "copilot-instructions.md");
|
|
12
16
|
const DEFAULT_CACHE_ROOT = path.join(os.homedir(), ".agentsmd", "cache");
|
|
13
17
|
const DEFAULT_WORKSPACE_ROOT = path.join(os.homedir(), ".agentsmd", "workspace");
|
|
14
18
|
const DEFAULT_INIT_SOURCE = "github:owner/repo@latest";
|
|
@@ -20,6 +24,7 @@ const TOOL_RULES_PATH = new URL("../tools/tool-rules.md", import.meta.url);
|
|
|
20
24
|
const USAGE_PATH = new URL("../tools/usage.txt", import.meta.url);
|
|
21
25
|
const DEFAULT_TOTAL_BUDGET = 350;
|
|
22
26
|
const DEFAULT_MODULE_BUDGET = 30;
|
|
27
|
+
const LINT_HEADER = "<!-- markdownlint-disable MD025 -->";
|
|
23
28
|
const readValueArg = (remaining, index, flag) => {
|
|
24
29
|
const value = remaining[index + 1];
|
|
25
30
|
if (!value) {
|
|
@@ -182,6 +187,22 @@ const resolveFrom = (baseDir, targetPath) => {
|
|
|
182
187
|
}
|
|
183
188
|
return path.resolve(baseDir, targetPath);
|
|
184
189
|
};
|
|
190
|
+
const isSubPath = (baseDir, targetPath) => {
|
|
191
|
+
const relativePath = path.relative(path.resolve(baseDir), path.resolve(targetPath));
|
|
192
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
193
|
+
};
|
|
194
|
+
const toDisplayPath = (rootDir, filePath) => {
|
|
195
|
+
if (isSubPath(rootDir, filePath)) {
|
|
196
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
197
|
+
return normalizePath(relativePath || path.basename(filePath));
|
|
198
|
+
}
|
|
199
|
+
const homeDir = os.homedir();
|
|
200
|
+
if (isSubPath(homeDir, filePath)) {
|
|
201
|
+
const relativeToHome = normalizePath(path.relative(homeDir, filePath));
|
|
202
|
+
return relativeToHome ? `~/${relativeToHome}` : "~";
|
|
203
|
+
}
|
|
204
|
+
return normalizePath(path.resolve(filePath));
|
|
205
|
+
};
|
|
185
206
|
const ensureDir = (dirPath) => {
|
|
186
207
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
187
208
|
};
|
|
@@ -530,46 +551,93 @@ const formatRuleSourcePath = (rulePath, rulesRoot, rulesetDir, source, resolvedR
|
|
|
530
551
|
const result = normalizePath(path.relative(rulesetDir, rulePath));
|
|
531
552
|
return result;
|
|
532
553
|
};
|
|
554
|
+
const getGlobalOutputPaths = () => [
|
|
555
|
+
DEFAULT_CODEX_GLOBAL_OUTPUT,
|
|
556
|
+
DEFAULT_CLAUDE_GLOBAL_OUTPUT,
|
|
557
|
+
DEFAULT_GEMINI_GLOBAL_OUTPUT,
|
|
558
|
+
DEFAULT_COPILOT_GLOBAL_OUTPUT
|
|
559
|
+
];
|
|
533
560
|
const resolveOutputPaths = (rulesetDir, projectRuleset) => {
|
|
534
561
|
const primaryOutputPath = resolveFrom(rulesetDir, projectRuleset.output ?? DEFAULT_OUTPUT);
|
|
535
562
|
const claude = projectRuleset.claude ?? {};
|
|
536
563
|
const companionEnabled = claude.enabled !== false;
|
|
537
564
|
const configuredCompanionPath = resolveFrom(rulesetDir, claude.output ?? DEFAULT_CLAUDE_OUTPUT);
|
|
565
|
+
const globalOutputPaths = projectRuleset.global === false ? [] : getGlobalOutputPaths();
|
|
538
566
|
if (!companionEnabled ||
|
|
539
567
|
path.resolve(primaryOutputPath) === path.resolve(configuredCompanionPath)) {
|
|
540
|
-
return { primaryOutputPath };
|
|
568
|
+
return { primaryOutputPath, globalOutputPaths };
|
|
541
569
|
}
|
|
542
|
-
return {
|
|
570
|
+
return {
|
|
571
|
+
primaryOutputPath,
|
|
572
|
+
companionOutputPath: configuredCompanionPath,
|
|
573
|
+
globalOutputPaths
|
|
574
|
+
};
|
|
543
575
|
};
|
|
544
576
|
const buildClaudeCompanionContent = (primaryOutputPath, companionOutputPath) => {
|
|
545
577
|
const relativeImportPath = normalizePath(path.relative(path.dirname(companionOutputPath), primaryOutputPath));
|
|
546
578
|
return `@${relativeImportPath}\n`;
|
|
547
579
|
};
|
|
580
|
+
const buildInstructionContent = (parts, includeToolRules) => {
|
|
581
|
+
const sections = includeToolRules ? [normalizeTrailingWhitespace(TOOL_RULES), ...parts] : parts;
|
|
582
|
+
if (sections.length === 0) {
|
|
583
|
+
return "";
|
|
584
|
+
}
|
|
585
|
+
return `${LINT_HEADER}\n${sections.join("\n\n")}\n`;
|
|
586
|
+
};
|
|
587
|
+
const buildScopeDiff = (scope, targetPaths, desiredContent, rootDir) => {
|
|
588
|
+
if (targetPaths.length === 0) {
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
const displayTargets = targetPaths.map((filePath) => toDisplayPath(rootDir, filePath));
|
|
592
|
+
const changedTargetPath = targetPaths.find((filePath) => {
|
|
593
|
+
if (!fs.existsSync(filePath)) {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
return fs.readFileSync(filePath, "utf8") !== desiredContent;
|
|
597
|
+
});
|
|
598
|
+
if (!changedTargetPath) {
|
|
599
|
+
return {
|
|
600
|
+
scope,
|
|
601
|
+
targets: displayTargets,
|
|
602
|
+
status: "unchanged"
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const before = fs.existsSync(changedTargetPath) ? fs.readFileSync(changedTargetPath, "utf8") : "";
|
|
606
|
+
const displayPath = toDisplayPath(rootDir, changedTargetPath);
|
|
607
|
+
return {
|
|
608
|
+
scope,
|
|
609
|
+
targets: displayTargets,
|
|
610
|
+
status: "updated",
|
|
611
|
+
patch: createTwoFilesPatch(`a/${displayPath}`, `b/${displayPath}`, before, desiredContent, "", "", { context: 3 })
|
|
612
|
+
};
|
|
613
|
+
};
|
|
548
614
|
const composeRuleset = (rulesetPath, rootDir, options) => {
|
|
549
615
|
const rulesetDir = path.dirname(rulesetPath);
|
|
550
616
|
const projectRuleset = readProjectRuleset(rulesetPath);
|
|
551
|
-
const { primaryOutputPath, companionOutputPath } = resolveOutputPaths(rulesetDir, projectRuleset);
|
|
552
|
-
const composedOutputPath =
|
|
617
|
+
const { primaryOutputPath, companionOutputPath, globalOutputPaths } = resolveOutputPaths(rulesetDir, projectRuleset);
|
|
618
|
+
const composedOutputPath = toDisplayPath(rootDir, primaryOutputPath);
|
|
553
619
|
const { rulesRoot, resolvedRef } = resolveRulesRoot(rulesetDir, projectRuleset.source, options.refresh ?? false);
|
|
554
620
|
const globalRoot = path.join(rulesRoot, "global");
|
|
555
621
|
const domainsRoot = path.join(rulesRoot, "domains");
|
|
556
|
-
const
|
|
557
|
-
const
|
|
622
|
+
const resolvedGlobalRules = [];
|
|
623
|
+
const seenGlobalRules = new Set();
|
|
624
|
+
const resolvedRepositoryRules = [];
|
|
625
|
+
const seenRepositoryRules = new Set();
|
|
558
626
|
if (projectRuleset.global !== false) {
|
|
559
|
-
addRulePaths(collectMarkdownFiles(globalRoot),
|
|
627
|
+
addRulePaths(collectMarkdownFiles(globalRoot), resolvedGlobalRules, seenGlobalRules);
|
|
560
628
|
}
|
|
561
629
|
const domains = Array.isArray(projectRuleset.domains) ? projectRuleset.domains : [];
|
|
562
630
|
for (const domain of domains) {
|
|
563
631
|
const domainRoot = path.resolve(domainsRoot, domain);
|
|
564
|
-
addRulePaths(collectMarkdownFiles(domainRoot),
|
|
632
|
+
addRulePaths(collectMarkdownFiles(domainRoot), resolvedRepositoryRules, seenRepositoryRules);
|
|
565
633
|
}
|
|
566
634
|
const extraRules = Array.isArray(projectRuleset.extra) ? projectRuleset.extra : [];
|
|
567
635
|
const directRulePaths = extraRules.map((rulePath) => resolveFrom(rulesetDir, rulePath));
|
|
568
|
-
addRulePaths(directRulePaths,
|
|
636
|
+
addRulePaths(directRulePaths, resolvedRepositoryRules, seenRepositoryRules);
|
|
569
637
|
const totalBudget = projectRuleset.budget?.totalLines ?? DEFAULT_TOTAL_BUDGET;
|
|
570
638
|
const moduleBudget = projectRuleset.budget?.moduleLines ?? DEFAULT_MODULE_BUDGET;
|
|
571
639
|
const normalizedGlobalRoot = normalizePath(path.resolve(globalRoot));
|
|
572
|
-
const globalRuleFiles =
|
|
640
|
+
const globalRuleFiles = resolvedGlobalRules.filter((p) => normalizePath(p).startsWith(`${normalizedGlobalRoot}/`));
|
|
573
641
|
const moduleLineCounts = globalRuleFiles.map((filePath) => {
|
|
574
642
|
const content = fs.readFileSync(filePath, "utf8");
|
|
575
643
|
return { name: path.basename(filePath), lines: content.split("\n").length };
|
|
@@ -583,41 +651,53 @@ const composeRuleset = (rulesetPath, rootDir, options) => {
|
|
|
583
651
|
overBudgetModules,
|
|
584
652
|
exceeded: totalLines > totalBudget || overBudgetModules.length > 0
|
|
585
653
|
};
|
|
586
|
-
const
|
|
654
|
+
const buildRuleParts = (rulePaths) => rulePaths.map((rulePath) => {
|
|
587
655
|
const body = normalizeTrailingWhitespace(fs.readFileSync(rulePath, "utf8"));
|
|
588
656
|
const sourcePath = formatRuleSourcePath(rulePath, rulesRoot, rulesetDir, projectRuleset.source, resolvedRef);
|
|
589
657
|
return `Source: ${sourcePath}\n\n${body}`;
|
|
590
658
|
});
|
|
591
|
-
const
|
|
592
|
-
const
|
|
593
|
-
const primaryOutputContent =
|
|
659
|
+
const repositoryParts = buildRuleParts(resolvedRepositoryRules);
|
|
660
|
+
const globalParts = buildRuleParts(resolvedGlobalRules);
|
|
661
|
+
const primaryOutputContent = buildInstructionContent(repositoryParts, true);
|
|
662
|
+
const globalOutputContent = buildInstructionContent(globalParts, false);
|
|
663
|
+
const repositoryOutputs = [toDisplayPath(rootDir, primaryOutputPath)];
|
|
664
|
+
const globalOutputs = globalOutputPaths.map((filePath) => toDisplayPath(rootDir, filePath));
|
|
594
665
|
const composedFiles = [
|
|
595
666
|
{
|
|
596
667
|
absolutePath: primaryOutputPath,
|
|
597
|
-
relativePath:
|
|
598
|
-
content: primaryOutputContent
|
|
668
|
+
relativePath: toDisplayPath(rootDir, primaryOutputPath),
|
|
669
|
+
content: primaryOutputContent,
|
|
670
|
+
scope: "repository"
|
|
599
671
|
}
|
|
600
672
|
];
|
|
601
673
|
if (companionOutputPath) {
|
|
674
|
+
const companionDisplayPath = toDisplayPath(rootDir, companionOutputPath);
|
|
675
|
+
repositoryOutputs.push(companionDisplayPath);
|
|
602
676
|
composedFiles.push({
|
|
603
677
|
absolutePath: companionOutputPath,
|
|
604
|
-
relativePath:
|
|
605
|
-
content: buildClaudeCompanionContent(primaryOutputPath, companionOutputPath)
|
|
678
|
+
relativePath: companionDisplayPath,
|
|
679
|
+
content: buildClaudeCompanionContent(primaryOutputPath, companionOutputPath),
|
|
680
|
+
scope: "repository"
|
|
606
681
|
});
|
|
607
682
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
:
|
|
613
|
-
|
|
614
|
-
|
|
683
|
+
for (const globalOutputPath of globalOutputPaths) {
|
|
684
|
+
composedFiles.push({
|
|
685
|
+
absolutePath: globalOutputPath,
|
|
686
|
+
relativePath: toDisplayPath(rootDir, globalOutputPath),
|
|
687
|
+
content: globalOutputContent,
|
|
688
|
+
scope: "global"
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
const outputDiffs = [];
|
|
692
|
+
if (options.emitDiffs) {
|
|
693
|
+
const repositoryDiff = buildScopeDiff("repository", [primaryOutputPath], primaryOutputContent, rootDir);
|
|
694
|
+
if (repositoryDiff) {
|
|
695
|
+
repositoryDiff.targets = repositoryOutputs;
|
|
696
|
+
outputDiffs.push(repositoryDiff);
|
|
615
697
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
patch: createTwoFilesPatch(`a/${composedOutputPath}`, `b/${composedOutputPath}`, before, primaryOutputContent, "", "", { context: 3 })
|
|
620
|
-
};
|
|
698
|
+
const globalDiff = buildScopeDiff("global", globalOutputPaths, globalOutputContent, rootDir);
|
|
699
|
+
if (globalDiff) {
|
|
700
|
+
outputDiffs.push(globalDiff);
|
|
621
701
|
}
|
|
622
702
|
}
|
|
623
703
|
if (!options.dryRun) {
|
|
@@ -628,28 +708,43 @@ const composeRuleset = (rulesetPath, rootDir, options) => {
|
|
|
628
708
|
}
|
|
629
709
|
return {
|
|
630
710
|
output: composedOutputPath,
|
|
631
|
-
outputs:
|
|
632
|
-
|
|
711
|
+
outputs: [...repositoryOutputs, ...globalOutputs],
|
|
712
|
+
repositoryOutputs,
|
|
713
|
+
globalOutputs,
|
|
714
|
+
outputDiffs,
|
|
633
715
|
budgetResult
|
|
634
716
|
};
|
|
635
717
|
};
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
718
|
+
const printOutputDiffs = (result) => {
|
|
719
|
+
for (const diff of result.outputDiffs) {
|
|
720
|
+
const scopeLabel = diff.scope === "global" ? "Global outputs" : "Repository outputs";
|
|
721
|
+
if (diff.status === "unchanged") {
|
|
722
|
+
process.stdout.write(`${scopeLabel} unchanged.\n`);
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
process.stdout.write(`${scopeLabel} updated. ACTION (agent): refresh rule recognition from the diff below.\n`);
|
|
726
|
+
process.stdout.write(`Targets:\n${diff.targets.map((target) => `- ${target}`).join("\n")}\n`);
|
|
727
|
+
process.stdout.write(`--- BEGIN ${diff.scope.toUpperCase()} DIFF ---\n`);
|
|
728
|
+
if (diff.patch) {
|
|
729
|
+
process.stdout.write(diff.patch);
|
|
730
|
+
if (!diff.patch.endsWith("\n")) {
|
|
731
|
+
process.stdout.write("\n");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
process.stdout.write(`--- END ${diff.scope.toUpperCase()} DIFF ---\n`);
|
|
639
735
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
736
|
+
};
|
|
737
|
+
const formatComposedOutputs = (result) => {
|
|
738
|
+
const lines = ["Composed instruction files:"];
|
|
739
|
+
if (result.repositoryOutputs.length > 0) {
|
|
740
|
+
lines.push("Repository:");
|
|
741
|
+
lines.push(...result.repositoryOutputs.map((filePath) => `- ${filePath}`));
|
|
643
742
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
process.stdout.write(result.agentsMdDiff.patch);
|
|
648
|
-
if (!result.agentsMdDiff.patch.endsWith("\n")) {
|
|
649
|
-
process.stdout.write("\n");
|
|
650
|
-
}
|
|
743
|
+
if (result.globalOutputs.length > 0) {
|
|
744
|
+
lines.push("Global:");
|
|
745
|
+
lines.push(...result.globalOutputs.map((filePath) => `- ${filePath}`));
|
|
651
746
|
}
|
|
652
|
-
|
|
747
|
+
return `${lines.join("\n")}\n`;
|
|
653
748
|
};
|
|
654
749
|
const formatBudgetWarning = (result) => {
|
|
655
750
|
const totalInfo = result.totalLines > result.totalBudget
|
|
@@ -704,7 +799,7 @@ const formatInitRuleset = (ruleset) => {
|
|
|
704
799
|
` "extra": ${extraValue},`
|
|
705
800
|
];
|
|
706
801
|
if (ruleset.global === false) {
|
|
707
|
-
lines.push(" //
|
|
802
|
+
lines.push(" // Write shared global rules to user-level instruction files.");
|
|
708
803
|
lines.push(' "global": false,');
|
|
709
804
|
}
|
|
710
805
|
lines.push(" // Claude Code companion output settings.");
|
|
@@ -720,7 +815,7 @@ const formatInitRuleset = (ruleset) => {
|
|
|
720
815
|
const formatPlan = (items, rootDir) => {
|
|
721
816
|
const lines = items.map((item) => {
|
|
722
817
|
const verb = item.action === "overwrite" ? "Overwrite" : "Create";
|
|
723
|
-
const relative =
|
|
818
|
+
const relative = toDisplayPath(rootDir, item.path);
|
|
724
819
|
return `- ${verb}: ${relative}`;
|
|
725
820
|
});
|
|
726
821
|
return `Init plan:\n${lines.join("\n")}\n`;
|
|
@@ -768,19 +863,25 @@ const initProject = async (args, rootDir, rulesetName) => {
|
|
|
768
863
|
extraToWrite.push(extraPath);
|
|
769
864
|
}
|
|
770
865
|
if (args.compose) {
|
|
771
|
-
const composedTargets = [
|
|
866
|
+
const composedTargets = [
|
|
867
|
+
{ path: outputPaths.primaryOutputPath, requireForce: true },
|
|
868
|
+
...outputPaths.globalOutputPaths.map((outputPath) => ({
|
|
869
|
+
path: outputPath,
|
|
870
|
+
requireForce: false
|
|
871
|
+
}))
|
|
872
|
+
];
|
|
772
873
|
if (outputPaths.companionOutputPath) {
|
|
773
|
-
composedTargets.push(outputPaths.companionOutputPath);
|
|
874
|
+
composedTargets.push({ path: outputPaths.companionOutputPath, requireForce: true });
|
|
774
875
|
}
|
|
775
876
|
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)`);
|
|
877
|
+
if (fs.existsSync(composedTarget.path)) {
|
|
878
|
+
if (composedTarget.requireForce && !args.force) {
|
|
879
|
+
throw new Error(`Output already exists: ${normalizePath(composedTarget.path)} (use --force to overwrite)`);
|
|
779
880
|
}
|
|
780
|
-
plan.push({ action: "overwrite", path: composedTarget });
|
|
881
|
+
plan.push({ action: "overwrite", path: composedTarget.path });
|
|
781
882
|
}
|
|
782
883
|
else {
|
|
783
|
-
plan.push({ action: "create", path: composedTarget });
|
|
884
|
+
plan.push({ action: "create", path: composedTarget.path });
|
|
784
885
|
}
|
|
785
886
|
}
|
|
786
887
|
}
|
|
@@ -793,7 +894,7 @@ const initProject = async (args, rootDir, rulesetName) => {
|
|
|
793
894
|
dryRun: true,
|
|
794
895
|
plan: plan.map((item) => ({
|
|
795
896
|
action: item.action,
|
|
796
|
-
path:
|
|
897
|
+
path: toDisplayPath(rootDir, item.path)
|
|
797
898
|
}))
|
|
798
899
|
}, null, 2) + "\n");
|
|
799
900
|
}
|
|
@@ -813,28 +914,30 @@ const initProject = async (args, rootDir, rulesetName) => {
|
|
|
813
914
|
if (args.compose) {
|
|
814
915
|
composedOutput = composeRuleset(rulesetPath, rootDir, {
|
|
815
916
|
refresh: args.refresh ?? false,
|
|
816
|
-
|
|
917
|
+
emitDiffs: !args.quiet && !args.json
|
|
817
918
|
});
|
|
818
919
|
}
|
|
819
920
|
if (args.json) {
|
|
820
921
|
process.stdout.write(JSON.stringify({
|
|
821
|
-
initialized: [
|
|
822
|
-
localRules: extraToWrite.map((filePath) =>
|
|
922
|
+
initialized: [toDisplayPath(rootDir, rulesetPath)],
|
|
923
|
+
localRules: extraToWrite.map((filePath) => toDisplayPath(rootDir, filePath)),
|
|
823
924
|
composed: composedOutput ? composedOutput.outputs : [],
|
|
925
|
+
repositoryOutputs: composedOutput ? composedOutput.repositoryOutputs : [],
|
|
926
|
+
globalOutputs: composedOutput ? composedOutput.globalOutputs : [],
|
|
824
927
|
dryRun: false,
|
|
825
928
|
...(composedOutput ? { budget: composedOutput.budgetResult } : {})
|
|
826
929
|
}, null, 2) + "\n");
|
|
827
930
|
}
|
|
828
931
|
else if (!args.quiet) {
|
|
829
|
-
process.stdout.write(`Initialized ruleset:\n- ${
|
|
932
|
+
process.stdout.write(`Initialized ruleset:\n- ${toDisplayPath(rootDir, rulesetPath)}\n`);
|
|
830
933
|
if (extraToWrite.length > 0) {
|
|
831
934
|
process.stdout.write(`Initialized local rules:\n${extraToWrite
|
|
832
|
-
.map((filePath) => `- ${
|
|
935
|
+
.map((filePath) => `- ${toDisplayPath(rootDir, filePath)}`)
|
|
833
936
|
.join("\n")}\n`);
|
|
834
937
|
}
|
|
835
938
|
if (composedOutput) {
|
|
836
|
-
process.stdout.write(
|
|
837
|
-
|
|
939
|
+
process.stdout.write(formatComposedOutputs(composedOutput));
|
|
940
|
+
printOutputDiffs(composedOutput);
|
|
838
941
|
if (composedOutput.budgetResult.exceeded) {
|
|
839
942
|
process.stderr.write(formatBudgetWarning(composedOutput.budgetResult));
|
|
840
943
|
}
|
|
@@ -905,7 +1008,7 @@ const main = async () => {
|
|
|
905
1008
|
"Next steps:",
|
|
906
1009
|
`- Edit rule files under: ${rulesDirectory}`,
|
|
907
1010
|
"- 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
|
|
1011
|
+
"- Run compose-agentsmd apply-rules from your project root to apply updates and regenerate instruction files."
|
|
909
1012
|
].join("\n") + "\n");
|
|
910
1013
|
return;
|
|
911
1014
|
}
|
|
@@ -921,14 +1024,20 @@ const main = async () => {
|
|
|
921
1024
|
const output = composeRuleset(rulesetPath, rootDir, {
|
|
922
1025
|
refresh: true,
|
|
923
1026
|
dryRun: args.dryRun,
|
|
924
|
-
|
|
1027
|
+
emitDiffs: !args.quiet && !args.json
|
|
925
1028
|
});
|
|
926
1029
|
if (args.json) {
|
|
927
|
-
process.stdout.write(JSON.stringify({
|
|
1030
|
+
process.stdout.write(JSON.stringify({
|
|
1031
|
+
composed: output.outputs,
|
|
1032
|
+
repositoryOutputs: output.repositoryOutputs,
|
|
1033
|
+
globalOutputs: output.globalOutputs,
|
|
1034
|
+
dryRun: !!args.dryRun,
|
|
1035
|
+
budget: output.budgetResult
|
|
1036
|
+
}, null, 2) + "\n");
|
|
928
1037
|
}
|
|
929
1038
|
else if (!args.quiet) {
|
|
930
|
-
process.stdout.write(
|
|
931
|
-
|
|
1039
|
+
process.stdout.write(formatComposedOutputs(output));
|
|
1040
|
+
printOutputDiffs(output);
|
|
932
1041
|
if (output.budgetResult.exceeded) {
|
|
933
1042
|
process.stderr.write(formatBudgetWarning(output.budgetResult));
|
|
934
1043
|
}
|
|
@@ -942,22 +1051,28 @@ const main = async () => {
|
|
|
942
1051
|
const outputs = rulesetFiles.sort().map((rulesetPath) => composeRuleset(rulesetPath, rootDir, {
|
|
943
1052
|
refresh: args.refresh,
|
|
944
1053
|
dryRun: args.dryRun,
|
|
945
|
-
|
|
1054
|
+
emitDiffs: !args.quiet && !args.json
|
|
946
1055
|
}));
|
|
947
1056
|
if (args.json) {
|
|
948
1057
|
process.stdout.write(JSON.stringify({
|
|
949
1058
|
composed: outputs.flatMap((result) => result.outputs),
|
|
1059
|
+
repositoryOutputs: outputs.flatMap((result) => result.repositoryOutputs),
|
|
1060
|
+
globalOutputs: outputs.flatMap((result) => result.globalOutputs),
|
|
950
1061
|
dryRun: !!args.dryRun,
|
|
951
1062
|
budget: outputs[0].budgetResult
|
|
952
1063
|
}, null, 2) + "\n");
|
|
953
1064
|
}
|
|
954
1065
|
else if (!args.quiet) {
|
|
955
|
-
process.stdout.write(
|
|
956
|
-
|
|
957
|
-
.
|
|
958
|
-
.
|
|
1066
|
+
process.stdout.write(formatComposedOutputs({
|
|
1067
|
+
output: outputs[0].output,
|
|
1068
|
+
outputs: outputs.flatMap((result) => result.outputs),
|
|
1069
|
+
repositoryOutputs: outputs.flatMap((result) => result.repositoryOutputs),
|
|
1070
|
+
globalOutputs: outputs.flatMap((result) => result.globalOutputs),
|
|
1071
|
+
outputDiffs: [],
|
|
1072
|
+
budgetResult: outputs[0].budgetResult
|
|
1073
|
+
}));
|
|
959
1074
|
for (const result of outputs) {
|
|
960
|
-
|
|
1075
|
+
printOutputDiffs(result);
|
|
961
1076
|
}
|
|
962
1077
|
for (const result of outputs) {
|
|
963
1078
|
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": "4.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",
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
"diff": "^8.0.3"
|
|
66
66
|
},
|
|
67
67
|
"overrides": {
|
|
68
|
-
"minimatch": "^10.2.
|
|
68
|
+
"minimatch": "^10.2.4",
|
|
69
|
+
"rollup": "^4.59.0"
|
|
69
70
|
},
|
|
70
71
|
"lint-staged": {
|
|
71
72
|
"**/*.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
|