docs: presentation-deck README with SVG logo, banner, and architecture diagram

This commit is contained in:
2026-05-23 10:43:29 +01:00
parent 9f0a7b776e
commit 0dfccbc48f
4 changed files with 497 additions and 56 deletions
+219 -56
View File
@@ -1,85 +1,181 @@
# harness-sync
<p align="center">
<img src="docs/assets/banner.svg" alt="harness-sync — one config, every harness" width="900">
</p>
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.
<p align="center">
<strong>One canonical source of truth</strong> for skills, agents, MCP servers, LLM endpoints, and global instructions across every LLM harness on your machine.
</p>
## Supported harnesses
<p align="center">
<a href="#install">Install</a> ·
<a href="#first-run">First run</a> ·
<a href="#architecture">Architecture</a> ·
<a href="#supported-harnesses">Harnesses</a> ·
<a href="#commands">Commands</a> ·
<a href="#design">Design</a>
</p>
| 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/<name>/` 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/<name>.yaml │
│ • skills/<name>/SKILL.md │
│ • agents/<name>.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.
<p align="center">
<img src="docs/assets/architecture.svg" alt="harness-sync architecture diagram" width="900">
</p>
---
## 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/<harness>/`
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/<name>.md # optional override
├── skills/<name>/SKILL.md # markdown + frontmatter; symlinked into harnesses that accept it
├── agents/<name>.md # markdown + frontmatter
├── mcp.yaml # canonical MCP server registry
├── state/<harness>/... # 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/<name>/`
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 <name> 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 <path>` 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/<harness>/`; "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 `<file>.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/<harness>/<path> (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 `<file>.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/<harness>/ # 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.
<p align="center">
<img src="docs/assets/logo.svg" alt="harness-sync" width="120">
</p>
+147
View File
@@ -0,0 +1,147 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 560" width="1200" height="560" role="img" aria-label="harness-sync architecture diagram">
<title>harness-sync architecture</title>
<desc>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.</desc>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#0ea5e9"/>
</marker>
<marker id="arrowGrey" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#64748b"/>
</marker>
<style>
.box {
fill: #f8fafc;
stroke: #cbd5e1;
stroke-width: 1.5;
rx: 8;
}
.box-accent {
fill: #e0f2fe;
stroke: #0ea5e9;
stroke-width: 2;
}
.box-state {
fill: #fef3c7;
stroke: #f59e0b;
stroke-width: 1.5;
}
.label {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 14px;
fill: #0f172a;
}
.label-bold {
font-weight: 600;
}
.mono {
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
font-size: 12px;
fill: #334155;
}
.caption {
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: 12px;
fill: #64748b;
}
</style>
</defs>
<!-- Canonical box (left) -->
<g>
<rect class="box-accent" x="40" y="60" width="280" height="440" rx="8"/>
<text class="label label-bold" x="180" y="92" text-anchor="middle">canonical (git repo)</text>
<text class="caption" x="180" y="110" text-anchor="middle">~/.config/harness-sync/</text>
<g class="mono">
<text x="60" y="155">config.yaml</text>
<text x="60" y="180">profiles/</text>
<text x="80" y="200">home.yaml</text>
<text x="80" y="220">work.yaml</text>
<text x="60" y="250">instructions/global.md</text>
<text x="60" y="280">skills/&lt;name&gt;/SKILL.md</text>
<text x="60" y="305">agents/&lt;name&gt;.md</text>
<text x="60" y="330">mcp.yaml</text>
</g>
<rect class="box-state" x="60" y="370" width="240" height="105" rx="6"/>
<text class="label label-bold" x="180" y="395" text-anchor="middle">state/</text>
<text class="caption" x="180" y="415" text-anchor="middle">last-rendered snapshots</text>
<text class="caption" x="180" y="435" text-anchor="middle">(git HEAD)</text>
<text class="caption" x="180" y="460" text-anchor="middle">used as 3-way merge base</text>
</g>
<!-- Adapter box (middle) -->
<g>
<rect class="box" x="450" y="180" width="300" height="200" rx="8"/>
<text class="label label-bold" x="600" y="210" text-anchor="middle">adapter.Render(bundle)</text>
<text class="caption" x="600" y="228" text-anchor="middle">internal/adapters/&lt;name&gt;/</text>
<g class="mono">
<text x="470" y="265">→ canonical types</text>
<text x="470" y="288">→ native schema</text>
<text x="470" y="311">→ FileSet (symlink | rendered)</text>
<text x="470" y="345">common.BuildProviders()</text>
<text x="470" y="362">common.BuildMCPMapStyled()</text>
</g>
</g>
<!-- 3-way merge box (top right inflow) -->
<g>
<rect class="box" x="820" y="60" width="320" height="160" rx="8"/>
<text class="label label-bold" x="980" y="90" text-anchor="middle">3-way merge</text>
<text class="caption" x="980" y="108" text-anchor="middle">git merge-file</text>
<g class="mono">
<text x="840" y="140">base = state/&lt;harness&gt;/&lt;path&gt;</text>
<text x="840" y="160">ours = rendered_new</text>
<text x="840" y="180">theirs = target on disk</text>
<text x="840" y="205" fill="#dc2626">conflict → write &lt;file&gt;.rej</text>
</g>
</g>
<!-- Target harnesses (right side, stacked) -->
<g>
<rect class="box" x="820" y="260" width="320" height="240" rx="8"/>
<text class="label label-bold" x="980" y="290" text-anchor="middle">harness target</text>
<text class="caption" x="980" y="308" text-anchor="middle">filesystem write + symlink</text>
<g class="mono">
<text x="840" y="340">~/.claude/ (claude-code)</text>
<text x="840" y="362">~/.config/crush/ (crush)</text>
<text x="840" y="384">~/.config/kilo/ (kilo)</text>
<text x="840" y="406">~/.config/opencode/ (opencode)</text>
<text x="840" y="428">~/.config/goose/ (goose)</text>
<text x="840" y="450">~/.config/cagent/ (cagent)</text>
<text x="840" y="472">~/.config/zed/ (zed)</text>
</g>
</g>
<!-- Arrows -->
<!-- canonical -> adapter -->
<path d="M 320 280 L 450 280" fill="none" stroke="#0ea5e9" stroke-width="2" marker-end="url(#arrow)"/>
<!-- adapter -> 3-way merge -->
<path d="M 750 230 L 820 160" fill="none" stroke="#0ea5e9" stroke-width="2" marker-end="url(#arrow)"/>
<!-- state -> 3-way merge -->
<path d="M 300 410 C 600 410, 820 200, 820 180" fill="none" stroke="#64748b" stroke-width="1.5" stroke-dasharray="5 4" marker-end="url(#arrowGrey)"/>
<!-- 3-way merge -> target -->
<path d="M 980 220 L 980 260" fill="none" stroke="#0ea5e9" stroke-width="2" marker-end="url(#arrow)"/>
<!-- target -> state (snapshot write-back) -->
<path d="M 820 380 C 600 380, 300 380, 300 360" fill="none" stroke="#94a3b8" stroke-width="1.5" stroke-dasharray="3 3" marker-end="url(#arrowGrey)"/>
<!-- Legend / labels along arrows -->
<g class="caption">
<text x="385" y="270" text-anchor="middle">load</text>
<text x="780" y="190">render</text>
<text x="985" y="245" text-anchor="middle">write</text>
<text x="560" y="375">snapshot back to state/</text>
</g>
<!-- Bottom strip: apply pipeline -->
<g>
<rect class="box" x="40" y="525" width="1100" height="0" rx="8" opacity="0"/>
<text class="caption" x="600" y="540" text-anchor="middle">
apply: for each detected adapter → Render → 3-way merge against state → write target → commit new state in canonical repo
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

+78
View File
@@ -0,0 +1,78 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 320" width="1200" height="320" role="img" aria-label="harness-sync banner">
<title>harness-sync</title>
<desc>harness-sync banner: a sync hub connecting seven LLM harnesses with a wordmark and tagline.</desc>
<defs>
<linearGradient id="bgGradient" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#0f172a"/>
<stop offset="100%" stop-color="#1e293b"/>
</linearGradient>
<linearGradient id="hubGradient2" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#38bdf8"/>
<stop offset="100%" stop-color="#0284c7"/>
</linearGradient>
<linearGradient id="textGradient" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#f0f9ff"/>
<stop offset="100%" stop-color="#7dd3fc"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="1200" height="320" fill="url(#bgGradient)" rx="16"/>
<!-- Subtle grid -->
<g stroke="#1e3a5f" stroke-width="1" opacity="0.4">
<line x1="0" y1="80" x2="1200" y2="80"/>
<line x1="0" y1="160" x2="1200" y2="160"/>
<line x1="0" y1="240" x2="1200" y2="240"/>
</g>
<!-- Logo group: hub + 7 nodes, centered around (200, 160), radius 110 -->
<g transform="translate(200,160)">
<!-- Connections -->
<g stroke="#38bdf8" stroke-width="1.5" stroke-opacity="0.5" stroke-dasharray="4 3" fill="none">
<line x1="0" y1="0" x2="0" y2="-110"/>
<line x1="0" y1="0" x2="85.99" y2="-68.59"/>
<line x1="0" y1="0" x2="107.24" y2="24.48"/>
<line x1="0" y1="0" x2="47.73" y2="99.1"/>
<line x1="0" y1="0" x2="-47.73" y2="99.1"/>
<line x1="0" y1="0" x2="-107.24" y2="24.48"/>
<line x1="0" y1="0" x2="-85.99" y2="-68.59"/>
</g>
<!-- Satellite nodes -->
<g fill="#0f172a" stroke="#7dd3fc" stroke-width="2">
<circle cx="0" cy="-110" r="12"/>
<circle cx="85.99" cy="-68.59" r="12"/>
<circle cx="107.24" cy="24.48" r="12"/>
<circle cx="47.73" cy="99.1" r="12"/>
<circle cx="-47.73" cy="99.1" r="12"/>
<circle cx="-107.24" cy="24.48" r="12"/>
<circle cx="-85.99" cy="-68.59" r="12"/>
</g>
<!-- Hub -->
<circle cx="0" cy="0" r="38" fill="url(#hubGradient2)" stroke="#0c4a6e" stroke-width="2"/>
<!-- Sync arrows -->
<g fill="none" stroke="#f0f9ff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<path d="M -16 -6 A 15 15 0 0 1 12 -10"/>
<polyline points="8,-17 13,-10 5,-7"/>
<path d="M 16 6 A 15 15 0 0 1 -12 10"/>
<polyline points="-8,17 -13,10 -5,7"/>
</g>
</g>
<!-- Wordmark -->
<g font-family="ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, monospace">
<text x="400" y="155" font-size="72" font-weight="700" fill="url(#textGradient)" letter-spacing="-2">
harness-sync
</text>
<text x="400" y="200" font-size="22" font-weight="400" fill="#94a3b8" letter-spacing="1">
one config, every harness
</text>
<text x="400" y="240" font-size="14" font-weight="400" fill="#64748b" font-family="ui-sans-serif, system-ui, sans-serif">
claude-code · crush · kilo · opencode · goose · cagent · zed
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+53
View File
@@ -0,0 +1,53 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 320" width="320" height="320" role="img" aria-label="harness-sync logo">
<title>harness-sync</title>
<desc>A central hub connected to seven satellite nodes representing supported LLM harnesses.</desc>
<defs>
<linearGradient id="hubGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0ea5e9"/>
<stop offset="100%" stop-color="#0369a1"/>
</linearGradient>
<filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Connections from hub to each node -->
<g stroke="#38bdf8" stroke-width="1.5" stroke-opacity="0.55" stroke-dasharray="4 3" fill="none">
<line x1="160" y1="160" x2="160" y2="30"/>
<line x1="160" y1="160" x2="261.6" y2="78.9"/>
<line x1="160" y1="160" x2="286.7" y2="188.9"/>
<line x1="160" y1="160" x2="216.4" y2="277.1"/>
<line x1="160" y1="160" x2="103.6" y2="277.1"/>
<line x1="160" y1="160" x2="33.3" y2="188.9"/>
<line x1="160" y1="160" x2="58.4" y2="78.9"/>
</g>
<!-- Satellite nodes -->
<g fill="#f1f5f9" stroke="#0ea5e9" stroke-width="2">
<circle cx="160" cy="30" r="14"/>
<circle cx="261.6" cy="78.9" r="14"/>
<circle cx="286.7" cy="188.9" r="14"/>
<circle cx="216.4" cy="277.1" r="14"/>
<circle cx="103.6" cy="277.1" r="14"/>
<circle cx="33.3" cy="188.9" r="14"/>
<circle cx="58.4" cy="78.9" r="14"/>
</g>
<!-- Hub -->
<g filter="url(#softGlow)">
<circle cx="160" cy="160" r="46" fill="url(#hubGradient)" stroke="#0c4a6e" stroke-width="2"/>
</g>
<!-- Sync arrows inside hub -->
<g fill="none" stroke="#f0f9ff" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M 140 152 A 18 18 0 0 1 174 148"/>
<polyline points="170,140 175,148 167,151"/>
<path d="M 180 168 A 18 18 0 0 1 146 172"/>
<polyline points="150,180 145,172 153,169"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB