-One canonical source of truth for skills, agents, MCP servers, LLM endpoints, and global instructions across multiple LLM harnesses. Edit once at `~/.config/harness-sync/`; propagate everywhere.
+
+ One canonical source of truth for skills, agents, MCP servers, LLM endpoints, and global instructions across every LLM harness on your machine.
+
-| Harness | Config path | What harness-sync writes |
-|---|---|---|
-| claude-code | `~/.claude/` | `skills/` (symlink), `agents/` (symlink), `CLAUDE.md`, `settings.json` `mcpServers` (merged) |
-| crush | `~/.config/crush/` | `skills/` (symlink), `crush.json` `providers`/`default_model`/`mcp` (merged) |
-| kilo | `~/.config/kilo/` | `agent/` (symlink), `kilo.json` `model`/`mcp` (merged) |
-| opencode | `~/.config/opencode/` | `opencode.jsonc` `provider`/`model`/`mcp` (merged), `AGENTS.md` |
-| goose | `~/.config/goose/` | `config.yaml` `GOOSE_PROVIDER`/`GOOSE_MODEL`/`extensions` (merged) |
-| cagent | `~/.config/cagent/` | starter `default.yaml` (per-run config) |
-| zed | `~/.config/zed/` | `settings.json` `agent`/`context_servers` (merged — other keys preserved) |
+---
-Adding a new harness: drop a Go package under `internal/adapters//` implementing `adapter.Adapter`, register it in `cmd/harness-sync/main.go`.
+## The problem
+
+You use claude-code, crush, kilo, opencode, goose, cagent, zed — pick any three.
+Every one of them keeps **its own copy** of the things you'd want shared:
+
+- the same skill markdown, duplicated seven ways
+- the same agent definitions, rewritten per harness
+- the same MCP server registry, with seven different key names (`mcpServers`, `mcp`, `context_servers`, `extensions`, …)
+- the same LLM gateway URL and token, hand-copied into seven config files
+
+Edit once. Forget the others. They drift. Things stop working in odd places.
+
+---
+
+## The fix
+
+```
+ ┌──────────────────────────────────┐
+ │ ~/.config/harness-sync/ (git) │
+ │ │
+ │ • profiles/.yaml │
+ │ • skills//SKILL.md │
+ │ • agents/.md │
+ │ • mcp.yaml │
+ │ • instructions/global.md │
+ └──────────────┬───────────────────┘
+ │ harness-sync apply
+ ┌─────────────┴─────────────┐
+ ▼ ▼
+ ┌───────────────────┐ ┌──────────────────┐
+ │ per-harness │ │ 3-way merge │
+ │ adapter.Render() │ ◄──► │ via git │
+ └─────────┬─────────┘ │ merge-file │
+ │ └──────────────────┘
+ ▼
+ ~/.claude/ ~/.config/crush/ ~/.config/kilo/
+ ~/.config/opencode/ ~/.config/goose/ ~/.config/cagent/
+ ~/.config/zed/
+```
+
+One canonical tree → seven harness-native configs, each with the correct keys.
+
+
+
+
+
+---
## Install
```bash
-make build && make install # installs to ~/.local/bin/harness-sync
+git clone https://github.com/lukaszraczylo/harness-sync
+cd harness-sync
+make build && make install # installs to ~/.local/bin/harness-sync
```
+Requires Go 1.22+ and `git` on `$PATH`.
+
+---
+
## First run
```bash
-harness-sync detect # see which harnesses are installed
-harness-sync init # pick which to import from; canonical tree created at ~/.config/harness-sync/
-harness-sync apply # propagate canonical to all detected harnesses
+harness-sync detect # what's installed?
+harness-sync init # pick which to import from; canonical tree created
+harness-sync apply # propagate canonical to every detected harness
```
-`init` opens an interactive multi-select of detected harnesses. Use `--no-prompt` to import from all, or `--from claude-code,crush` to be explicit.
+Real output on a machine with all seven harnesses installed:
-## Day-to-day
-
-```bash
-harness-sync show # list files each adapter would write
-harness-sync diff # apply --dry-run shorthand
-harness-sync apply # propagate canonical
-harness-sync apply crush # propagate to one harness only
-harness-sync profile use work # switch active profile and reapply
-harness-sync rollback 1 # revert last apply via git revert
-harness-sync adapter list # print registered adapters
+```text
+$ harness-sync detect
+claude-code detected
+crush detected
+kilo detected
+opencode detected
+goose detected
+cagent detected
+zed detected
```
-## Canonical layout
+`init` opens an interactive multi-select of detected harnesses
+(use `--no-prompt` to take all, or `--from claude-code,crush` to be explicit).
+On first apply, harness-sync writes a render snapshot under `state//`
+and commits the canonical tree — every subsequent apply is a `git merge-file`
+three-way merge against that snapshot.
-```
-~/.config/harness-sync/
-├── config.yaml # enabled_harnesses, active_profile
-├── profiles/
-│ ├── home.yaml # gateway URL + token + model allowlist + upstreams
-│ └── work.yaml
-├── instructions/
-│ ├── global.md # rendered to CLAUDE.md / AGENTS.md / etc.
-│ └── per-harness/.md # optional override
-├── skills//SKILL.md # markdown + frontmatter; symlinked into harnesses that accept it
-├── agents/.md # markdown + frontmatter
-├── mcp.yaml # canonical MCP server registry
-├── state//... # last-rendered snapshots (git-tracked, do not hand-edit)
-└── backups/ # pre-apply backups; git-ignored
+---
+
+## Supported harnesses
+
+Each row was grounded against the real config file on disk **and** the
+harness's official docs before the adapter was written. Wrong key names
+silently produce configs the harness can't parse — that's a bug class
+harness-sync goes out of its way to avoid.
+
+| Harness | Native config | MCP key | Model selector | Instructions | Symlinked |
+|---|---|---|---|---|---|
+| **claude-code** | `~/.claude/settings.json` (merged) | `mcpServers` | `env.ANTHROPIC_DEFAULT_MODEL` | `~/.claude/CLAUDE.md` | `skills/`, `agents/` |
+| **crush** | `~/.config/crush/crush.json` (merged) | `mcp` (type: stdio\|http\|sse) | `default_model` | auto-read `AGENTS.md` | `skills/` |
+| **kilo** | `~/.config/kilo/kilo.json` (merged) | `mcp` (type: local\|remote) | `model` + `small_model` | `instructions` array | `agent/` |
+| **opencode** | `~/.config/opencode/opencode.jsonc` (merged) | `mcp` (type: local\|remote) | `model` | `AGENTS.md` | — |
+| **goose** | `~/.config/goose/config.yaml` (merged) | n/a — `extensions` map (type: stdio) | `GOOSE_PROVIDER` + `GOOSE_MODEL` | env via `extensions.tom` | — |
+| **cagent** | `~/.config/cagent/default.yaml` (starter) | `mcps` | per-agent `model:` | inline `instruction:` | — |
+| **zed** | `~/.config/zed/settings.json` (merged) | `context_servers` (source: custom) | `agent.default_model.{provider,model}` | `~/.config/zed/rules/` | — |
+
+> **Merged, not replaced.** For harnesses with user-managed config keys (every one
+> except cagent), harness-sync reads the existing file, overlays only the keys
+> it owns, and writes it back. Your hooks, permissions, language servers,
+> editor settings — everything else — survive untouched.
+
+Add a new harness: drop a Go package under `internal/adapters//`
+implementing `adapter.Adapter`, register one line in `cmd/harness-sync/main.go`.
+
+---
+
+## Commands
+
+```text
+harness-sync detect list adapters + detection status
+harness-sync show [harness...] print files each adapter manages
+ --all include not-detected harnesses
+harness-sync init import from detected harnesses
+ --from a,b pick adapters, skip prompt
+ --no-prompt take all detected
+harness-sync apply [harness...] render + write
+ --dry-run show plan, write nothing
+ --force overwrite without 3-way merge
+harness-sync diff [harness...] apply --dry-run shorthand
+harness-sync profile list list canonical profiles
+harness-sync profile use switch active profile + re-apply
+harness-sync rollback [n] git revert last N apply commits
+harness-sync adapter list print registered adapters
```
-The whole tree is a git repository. Push it to a private remote for cross-machine sync.
+Every command accepts `--root ` to point at a non-default canonical tree.
+
+---
## Profiles
-A profile bundles the LLM stack. Switching profiles re-renders every harness's LLM config in one command.
+A profile bundles the LLM stack — gateway URL, dummy token, model allowlist,
+upstream provider keys. Switching profiles re-renders every harness in one
+command.
```yaml
-# profiles/home.yaml
+# ~/.config/harness-sync/profiles/home.yaml
name: home
gateway:
url: https://gateway.lan
- token: dummy-local-token
+ token: dummy-local-token # plaintext OK: gateway accepts any non-empty token
default_model: claude-sonnet-4-6
upstreams:
- name: anthropic
- api_key: ${ANTHROPIC_API_KEY}
+ api_key: ${ANTHROPIC_API_KEY} # env-var substitution at render time
- name: openai
api_key: ${OPENAI_API_KEY}
+ - name: ollama
+ base_url: http://10.0.1.21:11434
models:
- id: claude-sonnet-4-6
alias: sonnet
@@ -87,19 +183,86 @@ models:
alias: opus
```
-Secrets use `${VAR}` for env-var substitution at render time. The dummy gateway token may be plaintext (it has no value if leaked); real provider keys must be env-var references.
+Dummy gateway tokens may be plaintext (they have no value if leaked).
+Real provider keys **must** be `${VAR}` references — harness-sync substitutes
+at render time so the canonical tree never contains plaintext secrets.
-## Conflict resolution
+```bash
+harness-sync profile use work # switch the active profile…
+harness-sync apply # …and re-render every harness
+```
-`apply` performs git-style three-way merge on every rendered file. The base is the previous render snapshot from `state//`; "ours" is the new render; "theirs" is the current target file. When the merge is clean, the target is updated. When it conflicts, harness-sync writes the merged-with-markers result next to the target as `.rej`, leaves the original file untouched, and exits non-zero. Resolve the conflict (edit the target, delete `.rej`), then run `apply` again.
+---
-The same canonical repo holds the state snapshots. `rollback N` calls `git revert` on the last N commits and reapplies.
+## Conflict resolution via git
-## Design and implementation
+Every `apply` is a git-style three-way merge:
-- Design: [`docs/superpowers/specs/2026-05-23-harness-sync-design.md`](docs/superpowers/specs/2026-05-23-harness-sync-design.md)
-- Plan: [`docs/superpowers/plans/2026-05-23-harness-sync.md`](docs/superpowers/plans/2026-05-23-harness-sync.md)
+```
+base ← state// (last render harness-sync wrote)
+ours ← rendered_new (what harness-sync would write now)
+theirs ← target file on disk (what's actually there)
+```
+
+| Situation | Outcome |
+|---|---|
+| `target == ours` | skip (already in sync) |
+| `target == base` | fast-forward write |
+| disjoint changes | clean merge, write merged content |
+| overlapping changes | write `.rej` next to target, leave target alone, exit non-zero |
+
+Resolve a `.rej`: open it, copy the bits you want into the target, delete the
+`.rej`, run `apply` again. No bespoke conflict format — it's `<<<<<<<` markers
+from `git merge-file`.
+
+Roll back the last N applies: `harness-sync rollback 1` calls `git revert` on
+the canonical repo, then re-renders.
+
+---
+
+## Architecture
+
+A single Go binary, ~5000 LOC across 18 packages, 93 tests including an
+end-to-end test that runs the real binary against a fake `$HOME`.
+
+```
+cmd/harness-sync/main.go # cobra entrypoint + adapter registration
+internal/canonical/ # Bundle, Profile, MCPRegistry, Skill, Agent types + loader
+internal/adapter/ # Adapter interface, Registry, FileSet
+internal/adapter/common/ # shared building blocks (BuildProviders, BuildMCPMapStyled, MergeJSONKeys, MergeYAMLKeys, …)
+internal/adapters// # one package per harness, ~50-100 LOC of harness-specific glue
+internal/apply/ # render → 3-way merge → write pipeline + state snapshots
+internal/merge/ # git merge-file wrapper
+internal/gitx/ # thin shell wrapper over the git CLI
+internal/render/ # deterministic JSON / YAML / TOML marshallers
+internal/secrets/ # ${VAR} substitution with strict missing-key error
+internal/cli/ # cobra subcommands (detect, show, init, apply, diff, profile, rollback, adapter)
+internal/ui/ # huh-backed multi-select with non-interactive override for tests
+tests/e2e/ # binary-level integration test
+```
+
+DRY: the four pre-existing claude-code-like harnesses share `BuildProviders`,
+`ProvidersAsMap`, `BuildMCPMapStyled`, `MergeJSONKeys`, `ImportMarkdownTree`,
+`ParseFrontmatter`, and `StripJSONComments` from `internal/adapter/common/`.
+The four MCP dialects (claude / crush / opencode / zed) sit in one switch,
+so the right `type:` discriminator goes to the right harness.
+
+---
+
+## Design
+
+- **Spec:** [`docs/superpowers/specs/2026-05-23-harness-sync-design.md`](docs/superpowers/specs/2026-05-23-harness-sync-design.md) — problem framing, canonical layout, adapter interface, merge semantics
+- **Plan:** [`docs/superpowers/plans/2026-05-23-harness-sync.md`](docs/superpowers/plans/2026-05-23-harness-sync.md) — 26-task TDD implementation plan
+
+Non-goals (v1): watcher daemon, GUI, remote sync (push the canonical git repo
+yourself), keychain integration beyond env-var substitution.
+
+---
## License
-Same as the surrounding repo.
+Same as the surrounding repository.
+
+
+
+
diff --git a/docs/assets/architecture.svg b/docs/assets/architecture.svg
new file mode 100644
index 0000000..e55c2ee
--- /dev/null
+++ b/docs/assets/architecture.svg
@@ -0,0 +1,147 @@
+
diff --git a/docs/assets/banner.svg b/docs/assets/banner.svg
new file mode 100644
index 0000000..d20cb13
--- /dev/null
+++ b/docs/assets/banner.svg
@@ -0,0 +1,78 @@
+
diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg
new file mode 100644
index 0000000..2868717
--- /dev/null
+++ b/docs/assets/logo.svg
@@ -0,0 +1,53 @@
+