diff --git a/README.md b/README.md index f94ce2c..d4b872d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,105 @@ # harness-sync -Sync skills, agents, MCP, and LLM endpoints across multiple LLM harnesses -(claude-code, crush, kilo, opencode, goose, cagent, zed). +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. -Canonical config lives in `~/.config/harness-sync/` (git-tracked). Run -`harness-sync apply` to propagate. +## Supported harnesses -See `docs/superpowers/specs/2026-05-23-harness-sync-design.md` for 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) | -## Build +Adding a new harness: drop a Go package under `internal/adapters//` implementing `adapter.Adapter`, register it in `cmd/harness-sync/main.go`. - make build && make install +## Install + +```bash +make build && make install # installs to ~/.local/bin/harness-sync +``` + +## 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 +``` + +`init` opens an interactive multi-select of detected harnesses. Use `--no-prompt` to import from all, or `--from claude-code,crush` to be explicit. + +## 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 +``` + +## Canonical layout + +``` +~/.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 +``` + +The whole tree is a git repository. Push it to a private remote for cross-machine sync. + +## Profiles + +A profile bundles the LLM stack. Switching profiles re-renders every harness's LLM config in one command. + +```yaml +# profiles/home.yaml +name: home +gateway: + url: https://gateway.lan + token: dummy-local-token + default_model: claude-sonnet-4-6 +upstreams: + - name: anthropic + api_key: ${ANTHROPIC_API_KEY} + - name: openai + api_key: ${OPENAI_API_KEY} +models: + - id: claude-sonnet-4-6 + alias: sonnet + - id: claude-opus-4-7 + 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. + +## Conflict resolution + +`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. + +## Design and implementation + +- 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) + +## License + +Same as the surrounding repo.