compose-agentsmd 3.5.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Compose AGENTS.md
2
2
 
3
- This repository contains CLI tooling for composing per-project `AGENTS.md` files from modular rule sets.
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 `AGENTS.md` from the shared GitHub source.
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
- The tool prepends a small "Tool Rules" block to every generated `AGENTS.md` so agents know how to regenerate or update rules.
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
- When the output file is `AGENTS.md`, the CLI also prints a unified diff for `AGENTS.md` when it changes (and prints `AGENTS.md unchanged.` when it does not). This works even when the project is not under git. `--quiet` and `--json` suppress this output.
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 `AGENTS.md` immediately.
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 `AGENTS.md` by refreshing the cache. If your `source` is a local path, `edit-rules` points to the local workspace and `apply-rules` skips the push.
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
- // Output file name.
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): include `rules/global` (defaults to true). Omit this unless you want to disable globals.
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 output file(s) after `init`
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 AGENTS.md ruleset",
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",
@@ -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 { primaryOutputPath, companionOutputPath: configuredCompanionPath };
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 = normalizePath(path.relative(rootDir, primaryOutputPath));
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 resolvedRules = [];
557
- const seenRules = new Set();
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), resolvedRules, seenRules);
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), resolvedRules, seenRules);
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, resolvedRules, seenRules);
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 = resolvedRules.filter((p) => normalizePath(p).startsWith(`${normalizedGlobalRoot}/`));
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 parts = resolvedRules.map((rulePath) => {
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 lintHeader = "<!-- markdownlint-disable MD025 -->";
592
- const toolRules = normalizeTrailingWhitespace(TOOL_RULES);
593
- const primaryOutputContent = `${lintHeader}\n${[toolRules, ...parts].join("\n\n")}\n`;
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: composedOutputPath,
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: normalizePath(path.relative(rootDir, companionOutputPath)),
605
- content: buildClaudeCompanionContent(primaryOutputPath, companionOutputPath)
678
+ relativePath: companionDisplayPath,
679
+ content: buildClaudeCompanionContent(primaryOutputPath, companionOutputPath),
680
+ scope: "repository"
606
681
  });
607
682
  }
608
- let agentsMdDiff;
609
- if (options.emitAgentsMdDiff && path.basename(primaryOutputPath) === DEFAULT_OUTPUT) {
610
- const before = fs.existsSync(primaryOutputPath)
611
- ? fs.readFileSync(primaryOutputPath, "utf8")
612
- : "";
613
- if (before === primaryOutputContent) {
614
- agentsMdDiff = { status: "unchanged" };
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
- else {
617
- agentsMdDiff = {
618
- status: "updated",
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: composedFiles.map((file) => file.relativePath),
632
- agentsMdDiff,
711
+ outputs: [...repositoryOutputs, ...globalOutputs],
712
+ repositoryOutputs,
713
+ globalOutputs,
714
+ outputDiffs,
633
715
  budgetResult
634
716
  };
635
717
  };
636
- const printAgentsMdDiffIfPresent = (result) => {
637
- if (!result.agentsMdDiff) {
638
- return;
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
- if (result.agentsMdDiff.status === "unchanged") {
641
- process.stdout.write("AGENTS.md unchanged.\n");
642
- return;
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
- process.stdout.write("AGENTS.md updated. ACTION (agent): refresh rule recognition from the diff below.\n");
645
- process.stdout.write("--- BEGIN DIFF ---\n");
646
- if (result.agentsMdDiff.patch) {
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
- process.stdout.write("--- END DIFF ---\n");
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(" // Include rules/global from the source.");
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 = normalizePath(path.relative(rootDir, item.path));
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 = [outputPaths.primaryOutputPath];
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: normalizePath(path.relative(rootDir, item.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
- emitAgentsMdDiff: !args.quiet && !args.json
917
+ emitDiffs: !args.quiet && !args.json
817
918
  });
818
919
  }
819
920
  if (args.json) {
820
921
  process.stdout.write(JSON.stringify({
821
- initialized: [normalizePath(path.relative(rootDir, rulesetPath))],
822
- localRules: extraToWrite.map((filePath) => normalizePath(path.relative(rootDir, 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- ${normalizePath(path.relative(rootDir, rulesetPath))}\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) => `- ${normalizePath(path.relative(rootDir, filePath))}`)
935
+ .map((filePath) => `- ${toDisplayPath(rootDir, filePath)}`)
833
936
  .join("\n")}\n`);
834
937
  }
835
938
  if (composedOutput) {
836
- process.stdout.write(`Composed AGENTS.md:\n${composedOutput.outputs.map((filePath) => `- ${filePath}`).join("\n")}\n`);
837
- printAgentsMdDiffIfPresent(composedOutput);
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 AGENTS.md."
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
- emitAgentsMdDiff: !args.quiet && !args.json
1027
+ emitDiffs: !args.quiet && !args.json
925
1028
  });
926
1029
  if (args.json) {
927
- process.stdout.write(JSON.stringify({ composed: output.outputs, dryRun: !!args.dryRun, budget: output.budgetResult }, null, 2) + "\n");
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(`Composed AGENTS.md:\n${output.outputs.map((filePath) => `- ${filePath}`).join("\n")}\n`);
931
- printAgentsMdDiffIfPresent(output);
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
- emitAgentsMdDiff: !args.quiet && !args.json
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(`Composed AGENTS.md:\n${outputs
956
- .flatMap((result) => result.outputs)
957
- .map((filePath) => `- ${filePath}`)
958
- .join("\n")}\n`);
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
- printAgentsMdDiffIfPresent(result);
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": "3.5.1",
4
- "description": "CLI tools for composing per-project AGENTS.md files from modular rule sets",
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.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> Output filename for init (default: AGENTS.md)
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 output file(s) after init
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