Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ A CLI tool to synchronize AI coding assistant rule files between a central repos

## What is this tool for?

Many AI coding assistants (Claude Code, Gemini CLI, OpenCode, Codex CLI) read a local rules file to understand context, coding standards, and specific instructions.
Many AI coding assistants (Claude Code, Gemini CLI, OpenCode, Codex CLI, GitHub Copilot) read local instruction files to understand context, coding standards, and specific instructions.

Managing these rules across numerous projects can be tedious. `sync-rules` lets you maintain a **single, centralized directory** of rules and automatically synchronize the relevant subsets into your projects using a single standard file.

## Key Features

- **Centralized Rule Management:** Keep all your AI guidelines in one place.
- **Project-Specific Configuration:** Use flexible glob patterns to define exactly which rules apply to which projects.
- **Single Standard File:** Always generates `AGENTS.md` with all selected rules and writes a `CLAUDE.md` file containing `@AGENTS.md` for Claude Code.
- **Single Standard File:** Always generates `AGENTS.md` with all selected rules and writes a `CLAUDE.md` file containing `@AGENTS.md` for Claude Code. Copilot CLI reads the synced `AGENTS.md` directly.
// Seamless integration via shell: chain with your tool, e.g. `sync-rules && claude --chat`.

## Installation
Expand Down Expand Up @@ -63,7 +63,8 @@ Edit the `config.json` file to define your setup.
"global": ["global-rules/*.md"],
"globalOverrides": {
"claude": ["claude-specific/*.md"],
"codex": ["codex-specific/*.md"]
"codex": ["codex-specific/*.md"],
"copilot": ["copilot-specific/*.md"]
},
"projects": [
{
Expand All @@ -79,8 +80,8 @@ Edit the `config.json` file to define your setup.
```

- `rulesSource`: The central directory where you store your rule files (e.g., Markdown files). If omitted, it defaults to the system's data directory.
- `global`: Optional POSIX globs for rules that are combined and written to built-in global target files for supported tools (e.g., `~/.claude/CLAUDE.md`, `~/.gemini/AGENTS.md`, `~/.config/opencode/AGENTS.md`, `~/.codex/AGENTS.md`).
- `globalOverrides`: Optional per-harness override globs. Each key is a harness name (`claude`, `gemini`, `opencode`, `codex`) with its own glob patterns. Override rules are appended after the shared `global` rules for that harness only. A rule file must not appear in both `global` and an override for the same harness.
- `global`: Optional POSIX globs for rules that are combined and written to built-in global target files for supported tools (e.g., `~/.claude/CLAUDE.md`, `~/.gemini/AGENTS.md`, `~/.config/opencode/AGENTS.md`, `~/.codex/AGENTS.md`, `~/.copilot/copilot-instructions.md`).
- `globalOverrides`: Optional per-harness override globs. Each key is a harness name (`claude`, `gemini`, `opencode`, `codex`, `copilot`) with its own glob patterns. Override rules are appended after the shared `global` rules for that harness only. A rule file must not appear in both `global` and an override for the same harness.
- `projects`: Optional array defining each project. Can be omitted for a globals-only config.
- `path`: The root directory of the project (supports `~` for home directory).
- `rules`: POSIX-style glob patterns to select files from `rulesSource`. Supports negation (`!`).
Expand Down Expand Up @@ -125,9 +126,11 @@ Tip: define a small shell function to forward args cleanly:

## Output Files

- `AGENTS.md`: Canonical rules file read by Codex CLI, Gemini CLI, and OpenCode.
- `AGENTS.md`: Canonical rules file read by Codex CLI, Gemini CLI, OpenCode, and GitHub Copilot CLI.
- `CLAUDE.md`: A tiny include file with `@AGENTS.md` (Claude Code supported syntax).

> **Note:** The built-in global `copilot` target writes Copilot CLI's user-level instructions file at `~/.copilot/copilot-instructions.md`.

## Pipeline Examples

The CLI follows Unix philosophy—silent success, machine-readable output modes, and composability with standard tools.
Expand Down
2 changes: 2 additions & 0 deletions src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ describe("config", () => {
gemini: ["gemini/*.md"],
opencode: ["opencode/*.md"],
codex: ["codex/*.md"],
copilot: ["copilot/*.md"],
},
});
const config = parseConfig(json);
Expand All @@ -158,6 +159,7 @@ describe("config", () => {
"gemini",
"opencode",
"codex",
"copilot",
]);
});
});
Expand Down
1 change: 1 addition & 0 deletions src/config/loader.createSampleConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe("createSampleConfig", () => {
expect(content).toContain('"globalOverrides"');
expect(content).toContain('"claude"');
expect(content).toContain('"codex"');
expect(content).toContain('"copilot"');
expect(content).toContain('"projects"');
});

Expand Down
1 change: 1 addition & 0 deletions src/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const SAMPLE_CONFIG = {
globalOverrides: {
claude: ["claude-specific/*.md"],
codex: ["codex-specific/*.md"],
copilot: ["copilot-specific/*.md"],
},
projects: [
{
Expand Down
8 changes: 7 additions & 1 deletion src/core/harness-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
* The registry is the single source of truth for valid harness names and target paths.
*/

export type HarnessName = "claude" | "gemini" | "opencode" | "codex";
export type HarnessName =
| "claude"
| "gemini"
| "opencode"
| "codex"
| "copilot";

type HarnessEntry = {
readonly target: string;
Expand All @@ -16,6 +21,7 @@ export const HARNESS_REGISTRY: Record<HarnessName, HarnessEntry> = {
gemini: { target: "~/.gemini/AGENTS.md" },
opencode: { target: "~/.config/opencode/AGENTS.md" },
codex: { target: "~/.codex/AGENTS.md" },
copilot: { target: "~/.copilot/copilot-instructions.md" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Info: Verified that ~/.copilot/copilot-instructions.md is the correct path for Copilot CLI user-level instructions per GitHub docs. Good.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect COPILOT_HOME for Copilot global instructions

The new Copilot harness hard-codes ~/.copilot/copilot-instructions.md, so global sync writes to the wrong location whenever users run Copilot CLI with a non-default config directory (for example by setting COPILOT_HOME, which GitHub’s CLI docs describe as overriding ~/.copilot). In that setup, synced instructions are silently ignored by Copilot CLI even though sync reports the Copilot target was handled.

Useful? React with 👍 / 👎.

};

export const HARNESS_NAMES = Object.keys(HARNESS_REGISTRY) as HarnessName[];
3 changes: 3 additions & 0 deletions src/core/sync-global.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ describe("sync-global", () => {
const paths = actionsArgument.map((action) => action.path);
expect(paths.some((p) => p.endsWith("/.claude/CLAUDE.md"))).toBe(true);
expect(paths.some((p) => p.endsWith("/.codex/AGENTS.md"))).toBe(true);
expect(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Info: Good addition. The test now verifies that all five harnesses (including copilot) receive write actions when global patterns are configured. This catches any future regression where a new harness is added to the registry but accidentally excluded from the sync loop.

paths.some((p) => p.endsWith("/.copilot/copilot-instructions.md")),
).toBe(true);
expect(
actionsArgument.every(
(action) => action.content === "# G1\nA\n\n# G2\nB",
Expand Down
3 changes: 3 additions & 0 deletions src/core/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ describe("sync", () => {

beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fsPromises.lstat).mockRejectedValue(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Info: This beforeEach lstat mock reset is a good defensive addition. vi.clearAllMocks() clears call history but technically preserves mock implementations — however, re-establishing the default explicitly here makes the test setup self-documenting and guards against any mockResolvedValueOnce queue leakage between tests. Nice housekeeping bundled with this PR.

Object.assign(new Error("ENOENT"), { code: "ENOENT" }),
);
});

it("writes AGENTS.md with concatenated rules", async () => {
Expand Down
Loading