Skip to main content

Keep equivalent AI-harness asset files converged on the latest change.

Project description


id: dedd4778-ae3b-5a1f-af39-9f386dfb8857

syncmd

Keep your AI-harness instruction and skill files in sync — automatically, from git.


The problem

Every agentic tool reads its own instruction/context/skill files, but they almost always mean the same thing:

Harness Instruction file
Claude Code CLAUDE.md
Cross-tool standard AGENTS.md
GitHub Copilot .github/copilot-instructions.md
Cursor .cursorrules, .cursor/rules/project.mdc
Gemini GEMINI.md
Windsurf .windsurf/rules/rules.md
Cline .clinerules
Roo Code .roo/rules/rules.md
JetBrains Junie .junie/guidelines.md
Amazon Q .amazonq/rules/project.md
Kiro .kiro/steering/project.md
Goose .goosehints
Warp WARP.md
Qwen Code QWEN.md
Aider CONVENTIONS.md
OpenHands .openhands/microagents/repo.md
Augment .augment-guidelines
Replit replit.md

Run syncmd formats to list every built-in harness label and its mount points. By default plan/sync cover all formats; restrict with a comma-separated list:

syncmd sync --formats=claude,agents,cursor

A teammate edits CLAUDE.md. Now AGENTS.md, .cursorrules, and the rest silently drift out of date — and whoever opens the repo with a different tool gets stale guidance. The same happens to skills (SKILL.md under .claude/, .agents/, .github/) and specs.

syncmd treats a group of equivalent files as one logical asset with several native mount points, decides which copy is the latest authoritative version using git history plus your working tree, and propagates it to the rest. Edit whichever file your tool uses; run syncmd; everything converges.

It is a compiler, not a copier: it figures out which member actually changed since the group last agreed, so a concurrent edit is reported as a conflict instead of being silently clobbered.


Install

# CLI
cargo install syncmd              # provides the `syncmd` binary
npm install -g syncmd             # downloads the matching prebuilt CLI

# Python SDK
pip install syncmd                # provides the `syncmd` module

Building from source:

cargo build --release             # CLI at target/release/syncmd
maturin develop -m crates/syncmd-py/pyproject.toml   # Python module into the active venv

syncmd requires the target path to be inside a git repository — the whole "align to the latest" decision is derived from history. It never initializes a repo or commits on your behalf.


Usage

The basic unit is syncmd sync <path>, where <path> is a file or directory inside a git repo:

syncmd sync .                     # sync every known group in the repo
syncmd sync CLAUDE.md             # sync just the group CLAUDE.md belongs to
syncmd sync .claude/skills/foo/   # sync the `foo` skill across all roots

Because the unit is a path, it composes upward — point it at a project, or (later) a whole home directory of projects, and each repo is handled independently.

What "latest" means

For each member syncmd computes a recency:

  1. Uncommitted / untracked edits rank as now — your live edit to CLAUDE.md beats a week-old committed AGENTS.md.
  2. Otherwise, the committer date of the last commit that touched the file.
  3. A file that doesn't exist yet is only ever a creation target.

It finds the baseline — the most recent commit where the group last agreed — and looks at what changed since:

  • One member changed → unambiguous, propagate it.
  • Several changed independently → a real conflict; resolved per --strategy, and losers are always backed up and reported.

Common commands

# Preview only — compute the plan, write nothing
syncmd plan .
syncmd sync . --dry-run

# Choose how divergence is resolved
syncmd sync . --strategy newest        # default: newest edit wins (losers backed up)
syncmd sync . --strategy error         # refuse to write, report the conflict, exit 1
syncmd sync . --strategy interactive   # show diffs, you pick the winner

# Machine-readable output
syncmd sync . --json

# Safety toggles
syncmd sync . --no-backup              # don't write <member>.syncmd.bak
syncmd sync . --allow-delete           # propagate a deletion across the group

Worked examples

Situation Result
A. Live edit — you edit CLAUDE.md (uncommitted), AGENTS.md already synced CLAUDE.md wins; AGENTS.md rewritten; re-run is a no-op
B. Bootstrap — only CLAUDE.md exists the other members are created from it
C. DivergenceAGENTS.md and CLAUDE.md edited in separate commits newest wins & backs up the other; error writes nothing and exits 1
D. Converged — all members byte-identical no-op, exit 0
E. Skillfoo exists only under .claude/ created under .agents/ and .github/

Exit codes

Code Meaning
0 All groups in sync or successfully propagated
1 At least one unresolved conflict (--strategy error/interactive abort)
2 Precondition failure (not a git repo, bad path, bad config)
3 I/O or git error

Configuration

syncmd ships sensible built-in defaults (instructions, skills, agents, specs) — zero config needed. To extend or override, drop a syncmd.toml at the repo root:

# A fixed list of equivalent files (order = canonical preference, used only to break ties)
[[rule]]
group = "instructions"
type = "claude_md"
layout = "file"
strategy = "newest"
mounts = [
  { harness = "agents",  pattern = "AGENTS.md" },
  { harness = "claude",  pattern = "CLAUDE.md" },
  { harness = "copilot", pattern = ".github/copilot-instructions.md" },
  { harness = "cursor",  pattern = ".cursorrules" },
  { harness = "gemini",  pattern = "GEMINI.md" },
]

# A templated, folder-backed group — each {name} becomes its own asset
[[rule]]
group = "skill:{name}"
type = "skill"
layout = "folder"
strategy = "newest"
mounts = [
  { harness = "claude", pattern = ".claude/skills/{name}/SKILL.md" },
  { harness = "agents", pattern = ".agents/skills/{name}/SKILL.md" },
  { harness = "github", pattern = ".github/skills/{name}/SKILL.md" },
]

SDK (Python)

The Rust core is exposed as a Python module. sync() and plan() return a report object whose to_dict() matches the flowpad-sdk record shape (type/id first, snake_case, nulls omitted), so it drops straight into flow-sdk tooling.

import json
import syncmd

# Preview — no writes
report = syncmd.plan(".")

for group in report.groups:
    print(group.name, group.decision, group.winner_oid)

# Apply, choosing a strategy
report = syncmd.sync(".", strategy="newest", backup=True)

print(report.summary.written, "files updated")

# flow-sdk-shaped dict / JSON
print(json.dumps(report.to_dict(), indent=2))
print(report.to_json())
# Round-trips into flow-sdk records
from flow_sdk.fs_store import FSRecord
rec = FSRecord.from_dict(report.to_dict())

plan(path, *, recursive=False) and sync(path, *, strategy="newest", dry_run=False, backup=True, create_missing=True, allow_delete=False, recursive=False) mirror the CLI exactly — the CLI and the SDK are both thin wrappers over the same core functions.


How it works (one paragraph)

The blob object id (OID) is the only content primitive: two files are "in sync" when they point at the same blob. Equality, "changed since the baseline", and group agreement are all OID comparisons — syncmd never diffs bytes until it writes the single winning file. The decision is made once per group (who wins); the actions are applied per member (write / create / skip / back up). All writes are atomic (temp file + rename) and overwritten files are backed up to <member>.syncmd.bak first.

See docs/sync.md for the full algorithm specification. See docs/releasing.md for the multi-registry release flow.


Status

v1 in development. In scope: markdown instruction/skill/agent/spec groups, git-history-based recency, propagation, divergence detection. Out of scope (for now): non-markdown/binary assets, the adapter-compiler layer (rendering one canonical source into harness-specific manifests like MCP config.toml), networked/multi-repo sync, and auto-commit.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

syncmd-0.1.0.tar.gz (52.4 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

syncmd-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.0 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

syncmd-0.1.0-cp39-abi3-macosx_11_0_arm64.whl (1.8 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

File details

Details for the file syncmd-0.1.0.tar.gz.

File metadata

  • Download URL: syncmd-0.1.0.tar.gz
  • Upload date:
  • Size: 52.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for syncmd-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4e9a5e572fa3dcebaea9eae2113d91bc85a0f66a6e3b386be66fe5000859126a
MD5 e80a3b08725e532be1985065f6d7232d
BLAKE2b-256 8e7603d182ac770bac1aa8e1cd54a44e47366c9d8960fec7a858aef27a23aaea

See more details on using hashes here.

File details

Details for the file syncmd-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for syncmd-0.1.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 5b42ca3bfee12fb4d4d04182225268fa1f7ecfc21bb423ac8e10c06338e58fac
MD5 ec276c18c4f5c68b661cc3614eca8386
BLAKE2b-256 bc8aa5a071dcbce32a4cf4dee670c0d0a2de9a8dd11b88dbba99263351e0b766

See more details on using hashes here.

File details

Details for the file syncmd-0.1.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for syncmd-0.1.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ab58afde4deb40fb001c3d607ea177d02cc97c5935113d3ecbd3ad44bdc7f362
MD5 cbb1ac6a4691b459c7813678f20d8b4f
BLAKE2b-256 d41a00e24ce4f466431e58a1da452a8c9d09316385548cce22c636984a128ca5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page