From 0dfccbc48fffff8a7378ae0468627c306bfd44ef Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sat, 23 May 2026 10:43:29 +0100 Subject: [PATCH] docs: presentation-deck README with SVG logo, banner, and architecture diagram --- README.md | 275 ++++++++++++++++++++++++++++------- docs/assets/architecture.svg | 147 +++++++++++++++++++ docs/assets/banner.svg | 78 ++++++++++ docs/assets/logo.svg | 53 +++++++ 4 files changed, 497 insertions(+), 56 deletions(-) create mode 100644 docs/assets/architecture.svg create mode 100644 docs/assets/banner.svg create mode 100644 docs/assets/logo.svg diff --git a/README.md b/README.md index d4b872d..cf781fe 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,181 @@ -# harness-sync +

+ harness-sync — one config, every harness +

-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. +

-## Supported harnesses +

+ Install · + First run · + Architecture · + Harnesses · + Commands · + Design +

-| 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. + +

+ harness-sync architecture diagram +

+ +--- ## 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. + +

+ harness-sync +

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 @@ + + harness-sync architecture + Flow diagram: canonical config in a git repository renders through per-harness adapters via a three-way merge into each harness's native config layout. + + + + + + + + + + + + + + + canonical (git repo) + ~/.config/harness-sync/ + + + config.yaml + profiles/ + home.yaml + work.yaml + instructions/global.md + skills/<name>/SKILL.md + agents/<name>.md + mcp.yaml + + + + state/ + last-rendered snapshots + (git HEAD) + used as 3-way merge base + + + + + + adapter.Render(bundle) + internal/adapters/<name>/ + + + → canonical types + → native schema + → FileSet (symlink | rendered) + common.BuildProviders() + common.BuildMCPMapStyled() + + + + + + + 3-way merge + git merge-file + + + base = state/<harness>/<path> + ours = rendered_new + theirs = target on disk + conflict → write <file>.rej + + + + + + + harness target + filesystem write + symlink + + + ~/.claude/ (claude-code) + ~/.config/crush/ (crush) + ~/.config/kilo/ (kilo) + ~/.config/opencode/ (opencode) + ~/.config/goose/ (goose) + ~/.config/cagent/ (cagent) + ~/.config/zed/ (zed) + + + + + + + + + + + + + + + + + + load + render + write + snapshot back to state/ + + + + + + + apply: for each detected adapter → Render → 3-way merge against state → write target → commit new state in canonical repo + + + 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 @@ + + harness-sync + harness-sync banner: a sync hub connecting seven LLM harnesses with a wordmark and tagline. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + harness-sync + + + one config, every harness + + + claude-code · crush · kilo · opencode · goose · cagent · zed + + + 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 @@ + + harness-sync + A central hub connected to seven satellite nodes representing supported LLM harnesses. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +