diff --git a/.claude/agent-memory/rs-agents-code-reviewer/MEMORY.md b/.claude/agent-memory/rs-agents-code-reviewer/MEMORY.md
new file mode 100644
index 0000000..5707051
--- /dev/null
+++ b/.claude/agent-memory/rs-agents-code-reviewer/MEMORY.md
@@ -0,0 +1,3 @@
+# Memory Index
+
+- [Hermes/MCP onboarding scripts — review notes](project_hermes_mcp_onboarding.md) — connect-hermes-mcp.sh conventions, daemon-down false-success pattern, docker mcp output formats
diff --git a/.claude/agent-memory/rs-agents-code-reviewer/project_hermes_mcp_onboarding.md b/.claude/agent-memory/rs-agents-code-reviewer/project_hermes_mcp_onboarding.md
new file mode 100644
index 0000000..f2a6787
--- /dev/null
+++ b/.claude/agent-memory/rs-agents-code-reviewer/project_hermes_mcp_onboarding.md
@@ -0,0 +1,19 @@
+---
+name: hermes-mcp-onboarding
+description: Review observations for onboarding_bin Hermes/MCP wiring scripts — conventions, a verified false-success edge case, and real docker mcp output shapes
+metadata:
+ type: project
+---
+
+Onboarding bash scripts in `onboarding_bin/` that wire Hermes Agent to the Docker MCP Toolkit gateway (WR-18897 branch).
+
+**Convention baseline** (all onboarding_bin scripts follow this): `set -euo pipefail`, `export PATH="$HOME/.local/bin:/opt/homebrew/bin:..."` for launchd, `bold()/step()/warn()` printf helpers, `--soft` semantics = missing prereq warns + exit 0 so the install never fails. Modeled on `mac-mini-agent/sync-rs-agents-to-hermes.sh`.
+
+**Verified bug pattern — daemon-down false success** (connect-hermes-mcp.sh): when `docker info` fails, `DAEMON_UP=0` and the live-verify block is skipped, but `VERIFY_OK` stays initialized to 1, so the script prints "✔ Hermes is connected" even though it earlier warned "Live verification is skipped." **Why:** `VERIFY_OK` is only ever set to 0 inside checks that are gated behind `DAEMON_UP=1`. **How to apply:** when reviewing verify-gate scripts, check that the summary/success message is gated on the same condition as the checks that would justify it.
+
+**docker mcp CLI output shapes (verified live 2026-07):**
+- `docker mcp oauth ls` → `name | authorized|not authorized` table; awk `-F'|' /not authorized/` is a safe filter (no false-positive from "authorized" rows).
+- `docker mcp profile show
` → server count = `grep -c '^ - type:'` (4-space indent); nested `snapshot.server.type:` lines are more deeply indented so they do NOT inflate the count. Cross-checks against `docker mcp profile server ls`.
+- `docker mcp profile server ls` → `PROFILE | TYPE | IDENTIFIER` table, all profiles at once.
+
+**Non-findings to NOT re-raise:** quoted heredoc `<<'PYEOF'` means MCP_PROFILE reaches Python as pure argv data (no injection); `|| true` after `grep -c` neutralizes pipefail-on-zero-match; `if not _save_mcp_server(...)` relies on a truthy return from a private Hermes API (nit at most).
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index ef5a6de..38ae758 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -24,7 +24,62 @@
"Bash(brew tap *)",
"Bash(/opt/homebrew/bin/hermes --version)",
"Read(//Users/andyb/.local/bin/**)",
- "Read(//Users/andyb/.hermes/**)"
+ "Read(//Users/andyb/.hermes/**)",
+ "Bash(brew list *)",
+ "WebFetch(domain:hermes-agent.nousresearch.com)",
+ "Bash(hermes --help)",
+ "Bash(python3 -c \"import yaml,sys; d=yaml.safe_load\\(open\\('meta/configs/hermes.yml'\\)\\); print\\(yaml.dump\\(d\\)\\)\")",
+ "Bash(yamllint -d default meta/configs/hermes.yml)",
+ "Bash(yamllint meta/configs/homebrew_base.yml)",
+ "Bash(awk '{ if \\(length > 80\\) print FILENAME\":\"NR\": \"length\" cols\" }' meta/configs/homebrew_base.yml meta/configs/security.yml meta/configs/version_control.yml)",
+ "Bash(ruby -ryaml -e 'p YAML.load_file\\(\"meta/configs/hermes.yml\"\\)')",
+ "Bash(yamllint -f parsable .)",
+ "Bash(grep -oE '\\\\\\(\\(document-start|comments-indentation|[a-z-]+\\)\\\\\\)$')",
+ "Read(//Users/andyb/.claude/**)",
+ "Read(//Users/andyb/Retail-Success/repos/**)",
+ "Bash(scutil --get LocalHostName)",
+ "Bash(command -v hermes)",
+ "Bash(hermes --version)",
+ "Bash(command -v ollama)",
+ "Bash(ollama --version)",
+ "Bash(ollama list *)",
+ "Bash(hermes dashboard *)",
+ "Bash(lsof -nP -iTCP:9119 -sTCP:LISTEN)",
+ "Bash(hermes model *)",
+ "Bash(kill %1)",
+ "Bash(command -v devtunnel)",
+ "Bash(devtunnel list *)",
+ "Bash(ps -p 31628 -o pid,etime,command)",
+ "Bash(brew services *)",
+ "Bash(lsof -nP -iTCP:11434 -sTCP:LISTEN)",
+ "Bash(ollama show *)",
+ "Read(//Users/andyb/**)",
+ "Bash(OLLAMA_HOST=http://localhost:11434 ollama pull gemma3:4b)",
+ "Bash(ollama run *)",
+ "Bash(pkill -f \"ollama serve\")",
+ "Bash(export OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0 OLLAMA_CONTEXT_LENGTH=65536)",
+ "Bash(/opt/homebrew/bin/ollama serve *)",
+ "Bash(awk '{print} /Plugin discovery complete/{exit}')",
+ "Bash(ollama ps *)",
+ "Bash(sysctl -n vm.swapusage)",
+ "Bash(memory_pressure)",
+ "Bash(chmod +x mac-mini-agent/verify-ollama-hermes.sh)",
+ "Bash(break)",
+ "Bash(bash mac-mini-agent/verify-ollama-hermes.sh)",
+ "Bash(hermes skills *)",
+ "Bash(hermes plugins *)",
+ "Bash(bash -n mac-mini-agent/sync-rs-agents-to-hermes.sh)",
+ "Bash(bash -n onboarding_bin/install-hermes-agent.sh)",
+ "Bash(bash mac-mini-agent/sync-rs-agents-to-hermes.sh)",
+ "Bash(HOME=/tmp/nohome RS_AGENTS_SKILLS_SRC=/tmp/nonexistent bash mac-mini-agent/sync-rs-agents-to-hermes.sh --soft)",
+ "Bash(sed 's/\\\\x1b\\\\[[0-9;]*m//g')",
+ "Bash(hermes prompt-size *)",
+ "Bash(:)",
+ "Bash(launchctl start *)",
+ "Bash(sed 's/\\\\x1b\\\\[[0-9;]*m//g' /tmp/rs-agents-sync.out.log)",
+ "Bash(docker mcp *)",
+ "Bash(grep -iA2 profile)",
+ "Bash(docker info *)"
]
}
}
diff --git a/.yamllint.yml b/.yamllint.yml
index 9d52b14..9d92aa3 100644
--- a/.yamllint.yml
+++ b/.yamllint.yml
@@ -1,9 +1,15 @@
extends: default
-indentation: 2
-line-length: 120
+# NOTE: yamllint rules must live under `rules:`. `indentation`, `line-length`,
+# and `min-spaces-from-content` were previously at the top level / under the
+# wrong key, which made yamllint error out with "invalid config" on every file.
rules:
- min-spaces-from-content: 1
+ # Character-limit rule turned off — no maximum line length is enforced.
+ line-length: disable
+ indentation:
+ spaces: 2
+ comments:
+ min-spaces-from-content: 1
truthy:
check-keys: false
document-start:
@@ -14,3 +20,7 @@ ignore: |
/node_modules/*.yml
/node_modules/*.yamllint
/bin/
+ # Vendored dotbot submodules — third-party YAML, not ours to lint.
+ meta/dotbot/
+ meta/dotbot-brew/
+ meta/dotbot-vscode/
diff --git a/CLAUDE.md b/CLAUDE.md
index c4dc43f..d506227 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -15,7 +15,7 @@ Read this before editing anything — the flow spans several files:
- `./install-standalone ` — runs `meta/base.yml` then the named configs only. No brew plugin dir.
- ⚠️ The README references `./install`; **that script does not exist**. Use the two above.
-2. **Profiles** — `meta/profiles/` is a plain newline-delimited list of config names (no extension). Existing: `base`, `devops`, `react`, `ruby`. A profile = the ordered set of configs for a stack.
+2. **Profiles** — `meta/profiles/` is a plain newline-delimited list of config names (no extension). Existing: `base`, `devops`, `react`, `ruby`, `ai`. A profile = the ordered set of configs for a stack. (`ai` runs the `hermes` config, which installs the Hermes Agent via its own first-party installer — `onboarding_bin/install-hermes-agent.sh` — *not* Homebrew, because Hermes self-updates via `hermes update`.)
3. **Configs** — `meta/configs/.yml` are dotbot directive files (`link`, `shell`, `clean`, and brew bundles). `meta/base.yml` always runs first (sets link defaults, inits submodules, cleans `~` / `~/.config`).
diff --git a/README.md b/README.md
index 8a9a70a..b71f5f7 100755
--- a/README.md
+++ b/README.md
@@ -159,7 +159,7 @@ submodules are already declared in `.gitmodules` and are fetched automatically b
`install-profile` (do NOT run `git submodule add` — it will error):
```bash
-./install-profile base # then: ./install-profile react | ruby | devops
+./install-profile base # then: ./install-profile react | ruby | devops | ai
```
> If you hit Homebrew permission errors, fix ownership of the brew prefix with
diff --git a/ai/plans/MCP-PROFILE-SYNC-PLAN.md b/ai/plans/MCP-PROFILE-SYNC-PLAN.md
new file mode 100644
index 0000000..8f6e624
--- /dev/null
+++ b/ai/plans/MCP-PROFILE-SYNC-PLAN.md
@@ -0,0 +1,436 @@
+# Hermes ⇄ Docker MCP Toolkit — Connect & Stay-in-Sync Plan
+
+**Author:** Claude (Fable / Opus 4.8) for @andyb · **Date:** 2026-07-02
+**Ticket:** WR-18897 (local AI coding harness)
+**Status:** EXECUTED (same-machine spine, 2026-07-03) — see the Execution log below.
+Refines the earlier draft of this file and the Hermes onboarding shipped in
+`559303f` / `dcf059b` / `355e83f`.
+
+---
+
+## Execution log (2026-07-03)
+
+The §1–§4 same-machine spine is built, run, and verified on this machine:
+
+- **`onboarding_bin/connect-hermes-mcp.sh`** — new; idempotent, `--soft`/`--dry-run`,
+ full-docker-path entry, verify gate, OAuth manual-gate listing, rollback hint.
+- **`onboarding_bin/install-hermes-agent.sh`** — chained as best-effort step 3
+ (after the rs-agents skills sync), same `--soft`-non-fatal pattern.
+- **Live result:** `MCP_DOCKER` inserted into `~/.hermes/config.yaml`
+ (diff vs backup = exactly the 9-line `mcp_servers` block; comments intact);
+ `hermes mcp test MCP_DOCKER` connected in 21 s and discovered **289 tools**
+ across the 18 servers of the `default` profile; re-run → `UNCHANGED` no-op;
+ shellcheck clean.
+- **One deliberate deviation from §4:** the upsert uses **Hermes' own config API**
+ (`hermes_cli.mcp_config._save_mcp_server` via the venv Python Hermes ships) instead
+ of `yq` — the plan's own named alternative. Rationale: `yq` is not installed and not
+ in `Brewfile.base`, while the Hermes API is guaranteed present whenever Hermes is
+ (zero new dependency), and it runs the same security validation, config lock,
+ default-stripping, and atomic write as `hermes mcp add`. The `yq` preflight in §4 is
+ therefore superseded.
+- **Open items resolved:**
+ - **§5.2 — resolved.** `cmd_mcp_add` writes only `command`/`args`/`env`; there is no
+ per-server tool-filter key (filtering is agent-level `enabled_toolsets`/
+ `disabled_toolsets`). A bare entry registered all 289 gateway tools, as designed.
+ - **O1 — half-resolved (the Hermes half).** Hermes *does* implement dynamic tool
+ discovery for `notifications/tools/list_changed`
+ (`tools/mcp_tool.py:1511–1584`, `tools/registry.py:368`). Unverified half: whether
+ the docker gateway *propagates* the notification when `--watch` reconfigures it.
+ Until tested (add a Toolkit server while a Hermes session is open), the posture
+ stays reconnect-on-demand; the mini-only launchd timer (Layer B.2) remains
+ **not built** per the plan's own gate.
+ - **O3 — moot for automation** (the script never uses `hermes mcp add`); left as a
+ doc caveat for the manual convenience path only.
+ - **D1 = same-machine shipped now; §7 deferred** to a follow-up ticket.
+ **D2 = `default` profile** (env-overridable via `MCP_PROFILE=`).
+ **D3 = Docker stays a documented prerequisite** (no change to `meta/profiles/ai`).
+- **Code review (rs-agents code-reviewer): approved with warnings — both fixed.**
+ (1) daemon-down path no longer prints a false "connected" success (message is now
+ conditioned on the daemon being up); (2) the live verify is capped by
+ `run_with_timeout` (`timeout`/`gtimeout`/perl-alarm fallback, 180 s default,
+ `HERMES_TEST_TIMEOUT` override) so a wedged gateway can never stall a piped
+ `./install-profile ai`; (3) the private-API coupling to
+ `hermes_cli.mcp_config._save_mcp_server` is now called out in a comment for
+ post-`hermes update` diagnosability. Re-verified after the fixes: shellcheck clean,
+ re-run `UNCHANGED`, live connect 20.8 s / 289 tools.
+- **Not done here:** §5.6 (fold into the `local-development-setup` skill) — that skill
+ lives in Wayroo.tools and ships via its own Jira → PR → `reload plugins` flow.
+
+**Goal (SO THAT):** wire the Hermes AI harness to the machine's Docker Desktop
+**MCP Toolkit** profile — and keep that connection correct as MCP servers are added,
+updated, or removed in the Toolkit — **SO THAT** any local model driven by Hermes gets
+the same live tool plane Claude Code already has, on every engineer's machine, with
+zero hand-copying and no secrets in git.
+
+**Scope note.** The primary target is the *same-machine* case the request describes:
+Hermes talks to the Toolkit on whatever box runs it. §7 generalizes the same mechanism
+to provisioning a *fresh* machine (mac-mini / teammate), which is a strictly larger
+problem (secrets, OAuth) and is kept separate on purpose.
+
+---
+
+## 0. What we verified on this machine (2026-07-02)
+
+These are the load-bearing facts the design rests on — all confirmed, not assumed.
+
+### Hermes DOES consume MCP servers (this unblocks the whole plan)
+- `hermes mcp` is a full CLI subcommand: `add` / `remove` / `list` / `test` /
+ `configure` / `login` / `reauth` / `install` / `serve`.
+ (`~/.hermes/hermes-agent/hermes_cli/subcommands/mcp.py`)
+- MCP servers are stored in **`~/.hermes/config.yaml` under a top-level `mcp_servers:`
+ dict** — one entry per server. CRUD writes go there via `_save_mcp_server()`
+ (`hermes_cli/mcp_config.py`). There is **no** separate `mcp.json` or DB table.
+ *(Note: the live `config.yaml` on this box has no `mcp_servers:` key yet — Hermes is
+ installed but has never had an MCP server added. That is exactly the gap this plan
+ closes.)*
+- Transports supported: **stdio** (`command` + `args` + `env`), **HTTP/streamable**
+ (`url` + `headers`), and **SSE** (`url` + `transport: sse`). A `url` key routes to
+ HTTP; otherwise stdio (`tools/mcp_tool.py:_is_http()`).
+- Each stdio server is spawned as its **own long-lived background subprocess** at
+ session start, and its tool list is registered **at connect** (`_connect_server()`,
+ `tools/mcp_tool.py`). → **A change to the Toolkit's server set is not guaranteed to
+ appear in a running Hermes session** without a reconnect/restart. See §3.
+
+**Consequence:** `docker mcp gateway run` slots into Hermes as a single **stdio**
+`mcp_servers` entry — exactly the shape Claude Code uses for `MCP_DOCKER`. One entry,
+all Toolkit tools multiplexed behind it. No gateway-awareness code needed in Hermes.
+
+### The `docker mcp` CLI (verified via `--help` + live `profile show`)
+1. **`docker mcp gateway run`** is local **stdio** — no url, no auth token to configure
+ (that is confirmed already in `mac-mini-mcp-bootstrap.sh`). This is what Hermes spawns.
+2. **`docker mcp gateway run --profile `** pins the gateway to a specific profile
+ (`--profile` is mutually exclusive with `--servers`/`--enable-all-servers`). Pinning
+ makes Hermes' toolset explicit and versioned instead of "whatever `default` is."
+3. **`docker mcp gateway run --watch`** is **on by default** — the gateway "watches for
+ changes and reconfigures" itself. So when you add/remove a server in the Toolkit UI
+ or CLI, the *gateway* picks it up live. (What a *client* sees is the §3 nuance.)
+4. **`docker mcp gateway run --secrets docker-desktop`** (default) — the gateway reads
+ secrets from **Docker Desktop's Keychain**. The client (Hermes/Claude Code) never
+ handles secret values. A `.env` path is an escape hatch (plaintext at rest — avoid).
+5. **`docker mcp profile export/import`** — export is YAML with the server list, pinned
+ image digests, per-server config, and secret **names only (never values)** →
+ git-safe. **`profile push/pull`** move a profile via an OCI registry (e.g. GHCR).
+6. **`docker mcp secret`** has only `set` / `ls` / `rm` — **no get/export.** Secret
+ *values* cannot be read back out. (Only matters for §7 fresh-machine provisioning;
+ irrelevant to the same-machine case, where the values are already in the Keychain.)
+7. Profiles present on this box: `default` (18 servers), `dev_workflow`,
+ `terminal_control`. Per-server config in `~/.docker/mcp/config.yaml` carries identity
+ (`andyb@retailsuccess.com`, `filesystem.paths`) — must be templated for §7 reuse.
+
+---
+
+## 1. Design — the same-machine spine (this is the whole answer for the request)
+
+```
+ ~/.docker/mcp/ ── profile "rs-agent" (or default) ──┐
+ registry.yaml (servers) config.yaml (identity) │
+ Keychain (secret VALUES) │
+ ▼
+ docker mcp gateway run --profile rs-agent (stdio, --watch, --secrets docker-desktop)
+ │
+ ┌─────────────────────────────────┴───────────────────────────┐
+ ▼ ▼
+ Claude Code ~/.claude.json MCP_DOCKER Hermes ~/.hermes/config.yaml
+ "docker mcp gateway run …" mcp_servers.MCP_DOCKER:
+ (already wired) command: docker
+ args: [mcp, gateway, run, --profile, rs-agent]
+```
+
+**Principle:** Hermes points at the **gateway**, not at individual servers. The Toolkit
+(registry + Keychain) is the single source of truth for *what tools exist* and *their
+secrets*. `--watch` keeps the gateway current. The engineer's own Docker Desktop supplies
+servers; the engineer's own Keychain supplies secrets. This is **per-machine-correct by
+construction** — identical wiring on every box, nothing machine-specific committed.
+
+**The exact Hermes entry** (idempotently written by the onboarding script — see §4):
+
+```yaml
+# ~/.hermes/config.yaml (top-level mcp_servers dict)
+mcp_servers:
+ MCP_DOCKER:
+ command: docker # or full path — see §4 launchd note
+ args: [mcp, gateway, run, --profile, default] # profile: D2 (lead with `default`)
+ env: {} # no secrets here — gateway owns them
+ # NB: deliberately NO tool-filter key → all gateway tools enabled (the gateway already
+ # gates the server set; we don't want Hermes second-guessing it). Confirm in §5.2.
+```
+
+**Do NOT provision this with `hermes mcp add`.** Per the verified `add` flow, that command
+is **interactive** — it runs a security probe and then presents a *tool-selection
+checklist* on the tty. Under `./install-profile ai` (dotbot, piped, no tty, `--soft`),
+an interactive `add` would hang, fail, or silently default the tool set — breaking the
+zero-touch / idempotent invariant we inherit from WR-18897. So:
+- **Primary path (automation): a structure-safe YAML upsert** of the block above into
+ `~/.hermes/config.yaml`. This also matches the existing pattern — `sync-rs-agents-to-
+ hermes.sh` writes Hermes state directly and never invokes an interactive `hermes`
+ command. `config.yaml` is a large structured file (`_config_version: 31`, many
+ sections), so a naive append corrupts it: use `yq` (or Hermes' config API) for an
+ idempotent, structure-preserving `mcp_servers.MCP_DOCKER` set. §4 details this.
+- **Manual convenience only:** `hermes mcp add MCP_DOCKER --command docker --args …`
+ when a human is at a terminal and wants the interactive tool picker.
+
+**Why name it `MCP_DOCKER`:** matches the Claude Code entry name, so the two harnesses
+present the same server identity and the mental model is one-to-one.
+
+**Profile choice (decision D2, §8):** for the literal ask — connect to *"my profile"* —
+lead with the machine's existing **`default`** profile (the 18-server set already in use;
+zero extra setup; it *is* "my profile"). Offer a pinned dedicated **`rs-agent`** profile
+as a versioning enhancement (explicit, reproducible, reviewable) — not the default choice.
+
+---
+
+## 2. Secrets — nothing to build for the same-machine case
+
+Because Hermes talks to the gateway and the gateway reads `--secrets docker-desktop`:
+- **Secret values never touch Hermes, never touch this repo, never touch git.** They stay
+ in the macOS Keychain where `docker mcp secret set` already put them.
+- Adding the Hermes entry grants Hermes exactly the tools the Keychain already unlocks —
+ no token plumbing, no vault, no `.env`.
+- **OAuth servers** (`atlassian-remote`, `sentry-remote`, GitHub OAuth) are already
+ authorized on this machine for Claude Code's use of the same gateway; Hermes inherits
+ that through the shared gateway. Nothing to re-authorize for the same-machine case.
+
+The vault / token-injection problem the earlier draft treated as *blocking* only exists
+when standing up a **fresh** machine — see §7. It does **not** block this request.
+
+---
+
+## 3. Staying in sync — detection + resync (the second half of the ask)
+
+Two layers move independently. Be precise about which one `--watch` covers.
+
+### Layer A — the gateway ↔ the Toolkit ✅ automatic, nothing to build
+`docker mcp gateway run --watch` (default on) reconfigures the running gateway when a
+server is added / updated / removed in the Toolkit (via Docker Desktop UI or
+`docker mcp profile server add|rm`). No script, no hook, no launchd unit — it is **free**.
+Precision: `--watch` keeps the *gateway* current; it does **not** by itself refresh a
+*running Hermes session's* view of the tools (that is Layer B / O1). What always picks up
+a change on the Hermes side is a fresh connect — every new session gets the current set;
+a live session needs the O1 answer.
+
+### Layer B — a running Hermes session ↔ the gateway's current tool set ⚠️ the one caveat
+Hermes registers a stdio server's tools **at connect** (§0). Whether a *live* Hermes
+session sees a mid-session tool change depends on whether Hermes honors the MCP
+`tools/list_changed` notification or caches the list until reconnect. Evidence (long-
+lived task, tools registered at connect) points to **cached-until-reconnect**, but this
+was not confirmed by running the binary (blocked in this environment).
+
+**Conservative, low-cost resync design (works regardless of which is true):**
+1. **Default posture — reconnect on demand.** Adding/removing a Toolkit server is rare
+ and deliberate. After such a change, the next new Hermes session already sees the new
+ set (fresh connect). For a long-running session, `hermes mcp test MCP_DOCKER` (or
+ remove+re-add, or restart the session) forces a reconnect. Document this one-liner;
+ don't build machinery for a rare manual action.
+2. **Always-on box only (mac-mini) — bounce on drift.** Reuse the *existing* proven
+ launchd pattern (`com.retailsuccess.rs-agents-sync`, hourly, from `355e83f`). Add a
+ sibling timer `com.retailsuccess.mcp-toolkit-watch` that:
+ - hashes the Toolkit server set — **prefer a hash of `~/.docker/mcp/registry.yaml`
+ over the `servers:` names from `docker mcp profile show`**: the registry hash also
+ catches an *in-place* server update (same name, new pinned image digest / config),
+ which a names-only hash would miss,
+ - compares to the last-seen hash,
+ - on change, forces the always-on Hermes session to reconnect so the new tool list is
+ picked up. **Mechanism to confirm before building (O2):** the plan does *not* assume
+ a `hermes gateway restart` command — that is not among the verified `hermes mcp`
+ subcommands in §0 and was not confirmed (binary blocked here). The concrete,
+ already-verified fallback is to **reload the always-on launchd unit**
+ (`launchctl kickstart -k …`), which respawns Hermes and thus re-connects the gateway
+ from scratch. Config-only; touches no secrets and no git.
+ This is *optional* and *mini-only* — a 16 GB laptop does not need it (same call the
+ team already made for the Ollama plist, which is intentionally not loaded on laptops).
+
+**Verify `tools/list_changed` before building Layer B.2 — it's a two-link chain, test
+both.** Live zero-touch sync requires: (a) the docker gateway **propagates** a
+`tools/list_changed` notification downstream when `--watch` reconfigures it, AND (b)
+Hermes **honors** that notification and re-lists mid-session. One cheap check on a box
+where `hermes` runs: add a server to the Toolkit while a Hermes session is open and see
+if the new tool appears without a restart. If *both* links hold, Layer B.2 collapses to
+*nothing* and `--watch` alone covers everything. If *either* fails, the restart-on-change
+default (B.1/B.2) covers it. Do not overclaim "Hermes honors list_changed" alone buys
+live sync — the gateway must propagate it too. **Open item O1.**
+
+### What is explicitly NOT built (avoid over-engineering)
+- No per-server sync into Hermes. Hermes holds **one** entry (the gateway); the server
+ *set* lives in the Toolkit, not duplicated in Hermes config.
+- No polling of individual servers, no secret re-injection loop on the same machine.
+- No hiding of failures: do **not** use `hermes -z` (silences all errors,
+ `oneshot.py:148`) or `docker mcp gateway run --secrets` to a plaintext `.env` to
+ paper over a locked Keychain.
+
+---
+
+## 4. Integration with the onboarding flow (dotbot + WR-18897 zero-touch)
+
+Mirror how rs-agents skills were wired into Hermes in `dcf059b`/`355e83f`: a best-effort,
+idempotent step hung off the Hermes install, gated on the tools existing.
+
+### New script — `onboarding_bin/connect-hermes-mcp.sh` (idempotent, `--dry-run`)
+Modeled on `sync-rs-agents-to-hermes.sh` (same `set -euo pipefail`, `PATH` export for
+launchd, `bold/step/warn` helpers, `--soft` mode that never fails the install):
+1. **Preflight (soft):** need `docker`, `docker mcp`, `hermes` on PATH, **and `yq`**
+ (the structure-safe upsert tool — step 3). Any missing → warn + exit 0 under `--soft`
+ (Hermes install still succeeds; run later).
+ - **Runtime dependency to name, not to block on:** the wiring is written even if Docker
+ Desktop is *installed but not running* — but `docker mcp gateway run` (and therefore
+ every live Hermes session and the §4.4 verify gate) only works once the **daemon is
+ up**. This is a session-time dependency, not an install-time one. Mirror
+ `mac-mini-mcp-bootstrap.sh` step 1: if `docker info` fails, warn "start Docker
+ Desktop, wait for green, then the gateway connects" (and `open -a Docker` on the
+ mini). The entry is still written; it simply activates when the daemon comes up.
+ The verify gate surfaces a down daemon loudly (not silenced, per §6).
+2. **Ensure the profile exists (only if using a pinned `rs-agent` profile, D2):**
+ `docker mcp profile show rs-agent` or create/import from the committed profile YAML
+ (§7). For the plain `default` case, skip.
+3. **Wire Hermes idempotently — via structure-safe YAML upsert, NOT `hermes mcp add`**
+ (which is interactive; see §1). Use `yq` (or Hermes' config API) to set
+ `mcp_servers.MCP_DOCKER` in `~/.hermes/config.yaml`: absent → insert; present and
+ identical → no-op; present and stale (wrong args/profile) → update in place. Must
+ preserve every other section of the large `config.yaml` (`_config_version`, etc.) and
+ never clobber unrelated `mcp_servers` entries. Fail loudly if `yq` is unavailable
+ rather than hand-editing YAML with `sed`/`echo`.
+4. **Verify gate** (same philosophy as `verify-ollama-hermes.sh` layers):
+ `hermes mcp list` shows `MCP_DOCKER` · `hermes mcp test MCP_DOCKER` connects · the
+ gateway tool count matches `docker mcp profile show`. Under `--soft`, verify is
+ best-effort.
+5. **Print manual gates** (only what's genuinely interactive): first-run OAuth
+ `docker mcp oauth authorize ` if any OAuth server isn't yet authorized on this
+ box. (Same-machine: usually already done for Claude Code.)
+
+### Hook it into the Hermes installer
+In `onboarding_bin/install-hermes-agent.sh`, after the rs-agents skills sync block, add
+a third best-effort step calling `connect-hermes-mcp.sh --soft` — same pattern, same
+"non-fatal" guard. `./install-profile ai` then stands up the *whole* harness: Ollama →
+Hermes → rs-agents skills → **Toolkit tool plane**.
+
+### Profile / config file touch-list
+- **`onboarding_bin/connect-hermes-mcp.sh`** — new.
+- **`onboarding_bin/install-hermes-agent.sh`** — add the one best-effort call.
+- **`meta/profiles/ai`** — currently just `hermes`. Optionally add a `docker` config so
+ `./install-profile ai` also guarantees Docker Desktop is present (or keep Docker a
+ documented prerequisite, matching `mac-mini-mcp-bootstrap.sh` step 0). Decision D3.
+- **`meta/configs/hermes.yml`** — unchanged (it already invokes the installer, which now
+ chains the MCP connect).
+- **`mac-mini-agent/provision-coding-harness.sh`** — add the optional Layer B.2 launchd
+ timer for the always-on box only (mirrors the `rs-agents-sync` timer it already
+ installs). **Not** loaded on laptops.
+
+### Rollback / unwiring (one-step revert, per repo release discipline)
+The change is additive but must be reversible in one step:
+- **Remove the Hermes wiring:** `hermes mcp remove MCP_DOCKER` (or delete the
+ `mcp_servers.MCP_DOCKER` block from `~/.hermes/config.yaml` via `yq`). Hermes then runs
+ exactly as before — this touches nothing else.
+- **Stop chaining it on install:** revert the one best-effort call added to
+ `install-hermes-agent.sh`.
+- **Mini-only:** unload + remove the `com.retailsuccess.mcp-toolkit-watch` launchd unit
+ (`launchctl bootout …` + delete the plist) — same teardown as the existing
+ `rs-agents-sync` timer.
+Since Claude Code's `MCP_DOCKER` wiring is entirely independent, reverting the Hermes side
+never affects Claude Code's access to the same gateway.
+
+### Failure mode to design for: the spawned `docker` subprocess environment
+The entry's `env: {}` is correct for secrets (the gateway owns them). But when Hermes
+runs **always-on under launchd on the mini**, the `docker mcp gateway run` subprocess
+inherits launchd's **minimal environment** — `docker` may not be on PATH and the Docker
+context/`DOCKER_HOST` may not resolve. This is the exact minimal-env problem that forced
+the explicit `PATH` export in `sync-rs-agents-to-hermes.sh`. Mitigate at §1's entry:
+either use a **full path** in `command` (e.g. `/usr/local/bin/docker` /
+`/opt/homebrew/bin/docker`) or set a minimal `env` (`PATH`, and `DOCKER_HOST` if the
+socket isn't default). The §4.4 verify gate (`hermes mcp test MCP_DOCKER`) surfaces this
+loudly at connect — a broken connection fails visibly, it is not silenced. Same-machine
+laptop (interactive login shell) is not affected; this is a mini/launchd concern.
+
+### Idempotency & zero-touch invariants (match the WR-18897 bar)
+- Re-runnable: every step checks-then-acts; second run is a no-op.
+- `--soft` on the onboarding path: a missing Docker/Toolkit never fails a Hermes install.
+- No secrets written anywhere by this flow (gateway owns them).
+- Source of truth stays the Toolkit (same-machine) / the committed profile YAML (§7),
+ never `~/.hermes/config.yaml` (which is a generated consumer, like `~/.hermes/skills`).
+
+---
+
+## 5. Sequencing
+
+1. **Confirm O1** (both links: gateway propagates + live Hermes honors
+ `tools/list_changed`?) on a box where `hermes` runs. Decides whether Layer B.2 is
+ needed at all. *(cheap, 5 min)*
+2. **§5.2 — Confirm the no-tool-filter default enables all gateway tools.** Write the
+ bare `{command, args, env}` entry (no tool-filter key), start a session, and check
+ `hermes mcp test MCP_DOCKER` / the tool count matches `docker mcp profile show`. If
+ Hermes defaults a *bare* entry to zero/partial tools, the upsert must write the
+ tool-enable field too. *(cheap, 5 min — do alongside step 1)*
+3. Build `connect-hermes-mcp.sh` (+`--dry-run`); rehearse against the machine's `default`
+ profile first, then a scratch `rs-agent-test` profile. *(the main build — small)*
+4. Chain it into `install-hermes-agent.sh --soft`; dry-run `./install-profile ai`. *(small)*
+5. **Only if O1 = not-live:** add the `mcp-toolkit-watch` launchd timer to
+ `provision-coding-harness.sh` (mini-only). *(small, optional)*
+6. Fold the working recipe into the `local-development-setup` skill (per DRY-RUN §8):
+ "connect the local AI host to the Docker MCP tool plane."
+
+---
+
+## 6. Security guardrails
+- Same-machine: **no secret ever leaves the Keychain**; nothing secret is written by this
+ flow; `~/.hermes/config.yaml` holds only the gateway command, not tokens.
+- Do **not** default the gateway to `--secrets ` (plaintext at rest). Only if a
+ box is truly headless with a locked Keychain, and then the file is `chmod 600` outside
+ the repo — an explicit decision, never a drift.
+- Do **not** silence failures: no `hermes -z`, no swallowing gateway connect errors. The
+ verify gate must surface a broken connection loudly.
+- Per-machine OAuth is a feature: a copied profile YAML grants no access on its own.
+
+---
+
+## 7. Generalizing to a fresh machine (mac-mini / teammate) — SECONDARY, larger scope
+
+The same-machine plan above needs none of this. It applies **only** when provisioning a
+box that has *no* Toolkit servers/secrets yet — the earlier draft's abarrows→mac-mini
+replication problem. Kept separate because it introduces the two hard parts the
+same-machine case avoids: the server-set transport and the secret values.
+
+- **Transport of the server set:** commit a **sanitized** `docker mcp profile export`
+ (server list + config with identity templated from `.envrc`, secret *names* only, a
+ grep gate proving zero secret *values*) to
+ `mac-mini-agent/mcp/profile-rs-agent.yaml`; `docker mcp profile import` on the target.
+ Fallback: `docker mcp profile push/pull` via GHCR if the YAML grows large.
+- **Secret values:** the CLI can't export them (§0.6). A vault (1Password `op` or the
+ company standard) is the only source of truth: `op read … | docker mcp secret set …`
+ per required secret name. Missing vault item → fall back to the interactive
+ `set_secret` prompt already in `mac-mini-mcp-bootstrap.sh` (never worse than today).
+- **OAuth:** per-machine by design — the apply step ends by printing the exact
+ `docker mcp oauth authorize ` commands to run interactively on the target.
+- **24/7 survival:** keep `mac-mini-mcp-bootstrap.sh` §5 (auto-login, Docker autostart,
+ `pmset` no-sleep, Keychain auto-unlock).
+- **Housekeeping:** `ai/workflows/mac-mini-agent/` holds a stale rev-1 copy of
+ DRY-RUN-PLAN.md + a duplicate bootstrap script — delete the copies (or symlink) so
+ `mac-mini-agent/` is canonical. Retire the manual server-list/secret-prompt parts of
+ `mac-mini-mcp-bootstrap.sh` once the export/apply flow covers them; keep its §5.
+
+---
+
+## 8. Open decisions & items
+
+- **O1 (blocks only Layer B.2):** Two-link chain for live sync — does the docker gateway
+ *propagate* `tools/list_changed` on `--watch` reconfigure, AND does a running Hermes
+ session *honor* it (vs. caching tools until reconnect)? Test both empirically (§5.1).
+ Only if both hold does `--watch` alone become the entire sync story with no launchd
+ timer. Either failing → keep the restart-on-change default.
+- **O2 (blocks only Layer B.2 mechanism):** Confirm how to force a live Hermes reconnect
+ — is there a `hermes gateway restart` (unverified; not in the §0 subcommand list)?
+ If not, the launchd-unit reload (`launchctl kickstart -k`) is the verified fallback.
+- **O3 (blocks only the CLI convenience path):** Verify that
+ `hermes mcp add MCP_DOCKER --command docker --args mcp gateway run --profile default`
+ passes `--profile`/trailing args through to the gateway subprocess rather than having
+ Hermes' own parser consume them (a `--` separator or quoting may be required). The
+ automation path uses the unambiguous YAML upsert and is unaffected; this only gates the
+ documented manual convenience command in §1.
+- **D1:** Same-machine only (this request) vs. also build the §7 fresh-machine path now?
+ Recommend: ship the same-machine spine first (small, complete answer), do §7 as its
+ own follow-up ticket.
+- **D2:** For the literal "connect to *my* profile" ask, lead with the machine's existing
+ **`default`** profile (zero extra setup; it *is* "my profile"). A pinned dedicated
+ **`rs-agent`** profile is the versioning enhancement (explicit/reproducible) — same
+ choice as pinning `--profile` on the Claude Code side.
+- **D3:** Make `docker`/Docker-Desktop a `meta/profiles/ai` dependency, or keep it a
+ documented prerequisite (matches today's bootstrap step 0)?
+- **D4 (only if §7 is taken):** vault choice (1Password `op` vs company standard);
+ agent-box-scoped tokens vs reusing andyb's.
diff --git a/engineering/ide/.vscode/extensions.txt b/engineering/ide/.vscode/extensions.txt
index 44fe5ca..de67b3a 100755
--- a/engineering/ide/.vscode/extensions.txt
+++ b/engineering/ide/.vscode/extensions.txt
@@ -24,8 +24,6 @@ firsttris.vscode-jest-runner
fnando.linter
foxundermoon.shell-format
github.codespaces
-github.copilot
-github.copilot-chat
github.vscode-github-actions
github.vscode-pull-request-github
gluca.log-view-l4n
diff --git a/engineering/ide/.vscode/settings.json b/engineering/ide/.vscode/settings.json
index 2b1af03..e4c4e01 100755
--- a/engineering/ide/.vscode/settings.json
+++ b/engineering/ide/.vscode/settings.json
@@ -4,150 +4,150 @@
// },
"[css]": {
"editor.codeActionsOnSave": {
- "source.fixAll.stylelint": "explicit"
- }
+ "source.fixAll.stylelint": "explicit",
+ },
},
"[dockerfile]": {
"editor.codeActionsOnSave": {
- "source.fixAll.shellcheck": "explicit"
+ "source.fixAll.shellcheck": "explicit",
},
- "editor.defaultFormatter": "ms-azuretools.vscode-docker"
+ "editor.defaultFormatter": "ms-azuretools.vscode-docker",
},
"[dockerfile][dotenv][shellscript]": {
"editor.codeActionsOnSave": {
"source.fixAll.shellcheck": "explicit",
- "source.fixAll.stylelint": "never"
- }
+ "source.fixAll.stylelint": "never",
+ },
},
"[dotenv]": {
"editor.codeActionsOnSave": {
"source.fixAll.shellcheck": "explicit",
- "source.fixAll.stylelint": "never"
+ "source.fixAll.stylelint": "never",
},
- "editor.defaultFormatter": "foxundermoon.shell-format"
+ "editor.defaultFormatter": "foxundermoon.shell-format",
},
"[dotenv][shellscript]": {
"editor.codeActionsOnSave": {
"source.fixAll.shellcheck": "explicit",
- "source.fixAll.stylelint": "never"
- }
+ "source.fixAll.stylelint": "never",
+ },
},
"[gitignore]": {
"editor.codeActionsOnSave": {
- "source.fixAll.stylelint": "never"
+ "source.fixAll.stylelint": "never",
},
- "editor.defaultFormatter": "timonwong.shellcheck"
+ "editor.defaultFormatter": "timonwong.shellcheck",
},
"[gitignore][ruby][sql][xml]": {
"editor.codeActionsOnSave": {
- "source.fixAll.stylelint": "never"
- }
+ "source.fixAll.stylelint": "never",
+ },
},
"[gitignore][sql][xml]": {
"editor.codeActionsOnSave": {
- "source.fixAll.stylelint": "never"
- }
+ "source.fixAll.stylelint": "never",
+ },
},
"[html]": {
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
- "tidyHtml.formatOnSave": "explicit"
+ "tidyHtml.formatOnSave": "explicit",
},
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
},
"[ignore]": {
- "editor.defaultFormatter": "foxundermoon.shell-format"
+ "editor.defaultFormatter": "foxundermoon.shell-format",
},
"[javascript][javascriptreact][typescript][typescriptreact]": {
"editor.codeActionsOnSave": {
- "source.fixAll.eslint": "explicit"
+ "source.fixAll.eslint": "explicit",
},
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
},
"[json]": {
"editor.codeActionsOnSave": {
"editor.formatOnSave": "explicit",
"source.fixAll": "explicit",
- "source.fixAll.sort-json": "always"
- }
+ "source.fixAll.sort-json": "always",
+ },
// "editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.codeActionsOnSave": {
"editor.formatOnSave": "explicit",
- "source.fixAll": "explicit"
- }
+ "source.fixAll": "explicit",
+ },
},
"[less]": {
"editor.codeActionsOnSave": {
- "source.fixAll.stylelint": "explicit"
+ "source.fixAll.stylelint": "explicit",
},
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
},
"[less][scss]": {
"editor.codeActionsOnSave": {
- "source.fixAll.stylelint": "explicit"
- }
+ "source.fixAll.stylelint": "explicit",
+ },
},
"[markdown]": {
- "editor.formatOnType": false
+ "editor.formatOnType": false,
},
"[powershell]": {
- "editor.defaultFormatter": "foxundermoon.shell-format"
+ "editor.defaultFormatter": "foxundermoon.shell-format",
},
// "[prisma]": {
// "editor.defaultFormatter": "Prisma.prisma"
// },
"[properties]": {
- "editor.defaultFormatter": "foxundermoon.shell-format"
+ "editor.defaultFormatter": "foxundermoon.shell-format",
},
"[rails][erb]": {
"editor.defaultFormatter": "manuelpuyol.erb-linter",
- "editor.formatOnSave": true
+ "editor.formatOnSave": true,
},
"[ruby]": {
"editor.codeActionsOnSave": {
- "source.fixAll.stylelint": "never"
- }
+ "source.fixAll.stylelint": "never",
+ },
// "editor.defaultFormatter": "castwide.solargraph"
},
"[scminput]": {
"editor.rulers": [50, 72],
- "editor.wordWrap": "off"
+ "editor.wordWrap": "off",
},
"[scss]": {
"editor.codeActionsOnSave": {
- "source.fixAll.stylelint": "explicit"
+ "source.fixAll.stylelint": "explicit",
},
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
},
"[shellscript]": {
"editor.codeActionsOnSave": {
"source.fixAll.shellcheck": "explicit",
- "source.fixAll.stylelint": "never"
+ "source.fixAll.stylelint": "never",
},
- "editor.defaultFormatter": "foxundermoon.shell-format"
+ "editor.defaultFormatter": "foxundermoon.shell-format",
},
"[sql]": {
"editor.codeActionsOnSave": {
- "source.fixAll.stylelint": "never"
- }
+ "source.fixAll.stylelint": "never",
+ },
},
"[xml]": {
"editor.codeActionsOnSave": {
- "source.fixAll.stylelint": "never"
+ "source.fixAll.stylelint": "never",
},
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
},
"yaml.format.singleQuote": false,
"yaml.format.proseWrap": "never",
"yaml.schemaStore.enable": true,
"yaml.trace.server": "verbose",
"[yaml]": {
- "editor.defaultFormatter": "redhat.vscode-yaml"
+ "editor.defaultFormatter": "redhat.vscode-yaml",
},
"accessibility.signals.terminalBell": {
- "sound": "on"
+ "sound": "on",
},
"accessibility.voice.keywordActivation": "chatInContext",
"atlascode.bitbucket.enabled": false,
@@ -165,8 +165,8 @@
"enabled": true,
"monitor": true,
"query": "assignee = currentUser() AND (resolution = Unresolved AND status != Abandoned) AND (status != \"PO Acceptance\")",
- "siteId": "31a389db-299e-4351-a43b-20f6a9aa378c"
- }
+ "siteId": "31a389db-299e-4351-a43b-20f6a9aa378c",
+ },
],
"atlascode.jira.startWorkBranchTemplate.customPrefixes": [
"bug",
@@ -175,7 +175,7 @@
"maintenance",
"release",
"research",
- "bugfix"
+ "bugfix",
],
"atlascode.jira.startWorkBranchTemplate.customTemplate": "{{{prefix}}}/{{{issueKey}}}-{{{summary}}}",
"atlascode.jira.statusbar.showUser": false,
@@ -184,7 +184,7 @@
"FIXME:",
"ISSUE:",
"TODO -",
- "TODO:"
+ "TODO:",
],
"html.autoClosingTags": true,
"javascript.autoClosingTags": true,
@@ -266,7 +266,7 @@
"sql": true,
"vue": true,
"xml": true,
- "xsl": true
+ "xsl": true,
},
"cSpell.ignorePaths": [
"**/.git/objects/**",
@@ -275,7 +275,7 @@
"**/vscode-extension/**",
".vscode",
"coverage/**",
- "**/**/swagger.json"
+ "**/**/swagger.json",
],
"cSpell.language": "en,en-GB",
"cSpell.maxDuplicateProblems": 2,
@@ -695,7 +695,7 @@
"woff",
"workhorse",
"yamllint",
- "zindex"
+ "zindex",
],
"css.validate": false,
"cssModules.pathAlias": {},
@@ -705,8 +705,8 @@
"skipFiles": [
"**/node_modules/**",
"/**",
- "storybook-static/**"
- ]
+ "storybook-static/**",
+ ],
},
"debug.openExplorerOnEnd": false,
"debug.terminal.clearBeforeReusing": true,
@@ -723,7 +723,7 @@
"editor.bracketPairColorization.enabled": true,
"editor.bracketPairColorization.independentColorPoolPerBracketType": true,
"editor.codeActionsOnSave": {
- "source.fixAll": "explicit"
+ "source.fixAll": "explicit",
},
"editor.pasteAs.preferences": ["uri.path.absolute", "text.updateImports"],
"editor.cursorBlinking": "expand",
@@ -750,7 +750,7 @@
"editor.quickSuggestions": {
"comments": "off",
"other": "on",
- "strings": "off"
+ "strings": "off",
},
"editor.scrollBeyondLastLine": true,
"editor.suggestSelection": "first",
@@ -789,7 +789,7 @@
"javascript": "javascriptreact",
"sass": "css",
"scss": "css",
- "typescript": "typescriptreact"
+ "typescript": "typescriptreact",
},
"emmet.showAbbreviationSuggestions": false,
"emmet.triggerExpansionOnTab": true,
@@ -802,7 +802,7 @@
"javascriptreact",
"json",
"typescript",
- "typescriptreact"
+ "typescriptreact",
],
"explorer.confirmDelete": false,
"explorer.confirmDragAndDrop": false,
@@ -832,7 +832,7 @@
"**/log/**.log": true,
"**/public/assets**": true,
"**/public/packs**": true,
- "**/var/folders/**": true
+ "**/var/folders/**": true,
},
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
@@ -844,7 +844,7 @@
"**/node_modules/*/**": true,
"**/dist/**": true,
"**/.git/**": true,
- "**/storybook-static/**": true
+ "**/storybook-static/**": true,
},
"gistpad.playgrounds.showConsole": true,
"gistpad.playgrounds.stylesheetLanguage": "scss",
@@ -867,17 +867,17 @@
// "github.copilot.nextEditSuggestions.enabled": true,
"github.copilot.chat.generateTests.codeLens": true,
"github.copilot.chat.testGeneration.instructions": [
- { "file": ".github/copilot-instructions-automated-testing.md" }
+ { "file": ".github/copilot-instructions-automated-testing.md" },
],
"github.copilot.chat.commitMessageGeneration.instructions": [
- { "file": ".github/copilot-instructions-commit-message.md" }
+ { "file": ".github/copilot-instructions-commit-message.md" },
],
"github.copilot.enableCodeActions": true,
"github.copilot.enable": {
"*": true,
"markdown": true,
"plaintext": false,
- "scminput": false
+ "scminput": false,
},
"github.customPullRequestDescription": "gitEditor",
"github.customPullRequestTitle": true,
@@ -892,29 +892,29 @@
"githubPullRequests.queries": [
{
"label": "Local Pull Request Branches",
- "query": "default"
+ "query": "default",
},
{
"label": "Waiting For My Review",
- "query": "repo:${owner}/${repository} is:open review-requested:${user}"
+ "query": "repo:${owner}/${repository} is:open review-requested:${user}",
},
{
"label": "Assigned To Me",
- "query": "repo:${owner}/${repository} is:open assignee:${user}"
+ "query": "repo:${owner}/${repository} is:open assignee:${user}",
},
{
"label": "Created By Me",
- "query": "repo:${owner}/${repository} is:open author:${user}"
+ "query": "repo:${owner}/${repository} is:open author:${user}",
},
{
"label": "All Open",
- "query": "default"
- }
+ "query": "default",
+ },
],
"gitlens.advanced.messages": {
"suppressCommitHasNoPreviousCommitWarning": true,
"suppressCommitNotFoundWarning": true,
- "suppressFileNotUnderSourceControlWarning": true
+ "suppressFileNotUnderSourceControlWarning": true,
},
"gitlens.worktrees.defaultLocation": "",
"gitlens.advanced.repositorySearchDepth": 2,
@@ -927,7 +927,7 @@
"fetch:command",
"push:command",
"stash-push:command",
- "switch:command"
+ "switch:command",
],
"gitlens.sortBranchesBy": "date:desc",
"gitlens.statusBar.enabled": false,
@@ -992,7 +992,7 @@
"ruby.useLanguageServer": true,
"rubyLsp.featuresConfiguration": {},
"rubyLsp.rubyVersionManager": {
- "identifier": "rbenv"
+ "identifier": "rbenv",
},
"scm.alwaysShowActions": true,
"scm.diffDecorations": "gutter",
@@ -1012,7 +1012,7 @@
"**/tmp": true,
"**/yarn-error.log": true,
"**trace.zip": true,
- "src/design-system-package/dist/**": true
+ "src/design-system-package/dist/**": true,
},
"search.globalFindClipboard": true,
"search.quickOpen.includeHistory": false,
@@ -1035,7 +1035,7 @@
"jvmoptions",
"properties",
"shellscript",
- "spring-boot-properties"
+ "spring-boot-properties",
],
"solargraph.autoformat": true,
"solargraph.diagnostics": true,
@@ -1079,7 +1079,7 @@
"title",
"tools",
"type",
- "version"
+ "version",
],
"sortLines.filterBlankLines": true,
"sortLines.sortEntireFile": true,
@@ -1098,7 +1098,7 @@
"svelte",
"vue",
"vue-html",
- "vue-postcss"
+ "vue-postcss",
],
"terminal.external.linuxExec": "",
"terminal.external.osxExec": "Warp.app",
@@ -1120,52 +1120,52 @@
"terminal.integrated.minimumContrastRatio": 5,
"terminal.integrated.profiles.linux": {
"ash": {
- "path": "/bin/ash"
+ "path": "/bin/ash",
},
"bash": {
"icon": "terminal-bash",
- "path": "bash"
+ "path": "bash",
},
"fish": {
- "path": "fish"
+ "path": "fish",
},
"pwsh": {
"icon": "terminal-powershell",
- "path": "pwsh"
+ "path": "pwsh",
},
"sh": {
- "path": "/bin/sh"
+ "path": "/bin/sh",
},
"tmux": {
"icon": "terminal-tmux",
- "path": "tmux"
+ "path": "tmux",
},
"zsh": {
- "path": "zsh"
- }
+ "path": "zsh",
+ },
},
"terminal.integrated.profiles.osx": {
"bash": {
"args": ["-l"],
"icon": "terminal-bash",
- "path": "bash"
+ "path": "bash",
},
"fish": {
"args": ["-l"],
- "path": "fish"
+ "path": "fish",
},
"pwsh": {
"icon": "terminal-powershell",
- "path": "pwsh"
+ "path": "pwsh",
},
"tmux": {
"icon": "terminal-tmux",
- "path": "tmux"
+ "path": "tmux",
},
"zsh": {
"args": ["-l"],
- "path": "zsh"
- }
+ "path": "zsh",
+ },
},
"terminal.integrated.rightClickBehavior": "default",
"terminal.integrated.shellIntegration.enabled": true,
@@ -1183,7 +1183,7 @@
"editorBracketHighlight.foreground4": "#B084EB",
"editorBracketHighlight.foreground5": "#45A9F9",
"editorBracketHighlight.foreground6": "#FFB86C",
- "editorBracketHighlight.unexpectedBracket.foreground": "#FF2C6D"
+ "editorBracketHighlight.unexpectedBracket.foreground": "#FF2C6D",
},
"workbench.editor.highlightModifiedTabs": true,
"workbench.editor.openPositioning": "last",
@@ -1209,10 +1209,10 @@
"contributors": false,
"repositories": true,
"searchAndCompare": true,
- "launchpad": false
+ "launchpad": false,
},
"rubyLsp.featureFlags": {
- "all": true
+ "all": true,
},
"chat.editing.alwaysSaveWithGeneratedChanges": true,
"scm.diffDecorationsIgnoreTrimWhitespace": "true",
@@ -1245,12 +1245,12 @@
"editor.quickSuggestions": {
"other": true,
"comments": false,
- "strings": true
+ "strings": true,
},
- "editor.defaultFormatter": "redhat.vscode-yaml"
+ "editor.defaultFormatter": "redhat.vscode-yaml",
},
"[github-actions-workflow]": {
- "editor.defaultFormatter": "redhat.vscode-yaml"
+ "editor.defaultFormatter": "redhat.vscode-yaml",
},
"chat.mcp.gallery.enabled": true,
"atlascode.rovodev.showEntitlementNotifications": false,
@@ -1267,16 +1267,17 @@
".claude/settings.json": true,
"~/.copilot/hooks": true,
"~/.claude/settings.json": true,
- "~/.agents/hooks": true
+ "~/.agents/hooks": true,
},
"window.newWindowProfile": "Default",
"atlascode.rovodev.enabled": true,
"atlascode.jira.enabled": true,
"chat.tools.terminal.autoApprove": {
"npm run lint:errors-only": true,
- "npx vitest": true
+ "npx vitest": true,
},
"chat.viewSessions.orientation": "stacked",
"claudeCode.preferredLocation": "panel",
- "terminal.integrated.initialHint": false
+ "terminal.integrated.initialHint": false,
+ "chat.useClaudeHooks": true,
}
diff --git a/mac-mini-agent/DRY-RUN-PLAN.md b/mac-mini-agent/DRY-RUN-PLAN.md
new file mode 100644
index 0000000..50dcd91
--- /dev/null
+++ b/mac-mini-agent/DRY-RUN-PLAN.md
@@ -0,0 +1,224 @@
+# Local AI Coding Harness — Provisioning Plan
+
+**Author:** Claude (Opus 4.8) for @andyb · **Date:** 2026‑06‑28 (rev 2)
+**Stand‑up target:** `rss-mbp-5` (your MacBook) first → **promote to the mac‑mini** (24/7 box)
+**Status:** PLAN — nothing executed. The only thing I'd run before you approve is the ~10‑min GO/NO‑GO spike in §3, and only if you say go.
+
+> **What you actually want (clarified 06‑28):** *"Stand up the Hermes agent harness, connect Ollama to it,
+> then talk to Hermes via Microsoft Teams and/or a dashboard that can be hit externally."* Provisioned by
+> the **dotfiles**, so it's repeatable on the mini. And — **critically** — **rs‑agents must be accessible to
+> whatever model is selected, at all times.** "Dry run" = a real first working stand‑up (not throwaway).
+>
+> **The good news:** Hermes Agent (Nous Research) has native machinery for almost all of this — a web
+> `dashboard`, `model` selection against any OpenAI‑compatible provider (Ollama fits), `skills tap`/`install`
+> + `bundles` to load skills under a `/slash` command for **any** model, a launchd `gateway` for always‑on,
+> and `webhook` for event‑driven activation. The **one non‑native piece is MS Teams** (bridge required).
+>
+> **The one risk that governs everything:** I've confirmed these commands *exist* (via `--help`), not that
+> the integration *works end‑to‑end on a small local model*. So §3 is a single GO/NO‑GO test, and the rest
+> of the plan is explicitly conditional on it.
+
+---
+
+## 1. Objectives (recovered from repeated mentions + 06‑28 clarification)
+
+Marked **[R]** = recovered from repeated mentions across sessions; **[C]** = explicit 06‑28 clarification;
+**[rec]** = my recommendation filling a gap you didn't specify.
+
+| # | Objective | Source |
+|---|-----------|--------|
+| **O1** | mac‑mini as a **24/7 always‑on agent box** running your full AI tooling. | **[R]** 06‑23 (repeated), 06‑28 |
+| **O2** | **Stand up the Hermes harness + connect Ollama** (local models) as the coding harness. | **[C]** 06‑28; **[R]** Brewfile `hermes-agent`/`ollama` |
+| **O3** | Talk to Hermes via **MS Teams** and/or an **externally‑hittable dashboard**. | **[C]** 06‑28 |
+| **O4** | **The dotfiles repo provisions the whole coding harness** (hermes, ollama, local models, a way to reach /rs‑agents) — "so I don't have to think about it." | **[C]** 06‑28; **[R]** O2 hardening |
+| **O5** | **rs‑agents accessible to whatever model is selected, at all times** — *"our company's core tailored AI tooling."* | **[C]** 06‑28 (flagged *very important*) |
+| **O6** | Bake the **Ollama service start** (with perf env vars) into provisioning. | **[C]** 06‑28 (brew caveat) |
+| **O7** | **Fold learnings back into rs‑agents** skills (`local-development-setup` + `developer-onboarding`). | **[R]** 06‑28 |
+| **O8** | Idempotent / re‑runnable provisioning. | **[R]** onboarding hardening |
+
+Resolved earlier‑session unknown: **"hermes" = Hermes Agent by Nous Research** — a self‑improving agent that
+selects any model/provider, runs skills, exposes a dashboard, and can run always‑on.
+
+---
+
+## 2. Architecture
+
+```
+ MS Teams ──(webhook bridge + bot — NON-NATIVE, phase 2)──┐
+ ▼
+ External ──(tunnel + auth)──> Hermes dashboard ──> ┌─────────────────────────┐
+ (hermes dashboard) │ HERMES AGENT HARNESS │
+ │ model picker + fallback │
+ You (CLI/Teams/dashboard) ─────────────────────────> │ skills + /rs-agents │
+ └───────┬──────────┬───────┘
+ local inference ◀──────┘ └──────▶ rs-agents skills
+ ┌────────────────────┐ (hermes skills tap → on-demand)
+ │ Ollama (service) │
+ │ FLASH_ATTENTION=1 │ models: llama3.2:3b (smoke),
+ │ KV_CACHE_TYPE=q8_0│ qwen2.5-coder:7b (coding)
+ └────────────────────┘
+ ── all installed + configured by dotfiles: Brewfile + provision-coding-harness.sh ──
+```
+
+- **Hermes = the harness / front door.** Picks the model (`hermes model`), holds the skills, serves the
+ dashboard, runs always‑on via its launchd `gateway`.
+- **Ollama = local model server** (OpenAI‑compatible `http://localhost:11434/v1`). Hermes points at it.
+- **rs‑agents = skills made available to Hermes** so they ride with whatever model is selected (§5).
+- **dotfiles = provisioning** — `Brewfile.base` already *installs* everything; a new
+ `provision-coding-harness.sh` *configures* it (§4).
+
+---
+
+## 3. ⭐ STEP 1 — GO/NO‑GO smoke test (the spine; ~10 min; the only thing I'd run pre‑approval)
+
+**The single question that validates the whole architecture:**
+*Can I talk to Hermes, have it route to a **local** Ollama model, and have that model invoke **one** rs‑agents skill — end to end?*
+
+```bash
+# a) Ollama up with the perf flags you were told to use, + the model you'll actually code with
+OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0 /opt/homebrew/opt/ollama/bin/ollama serve &>/tmp/ollama.log &
+ollama pull qwen2.5-coder:7b # the real coding model (test the weak link, not just a toy)
+ollama pull llama3.2:3b # fast fallback for the sanity ping
+
+# b) Point Hermes at Ollama (interactive picker; choose a custom OpenAI-compatible provider)
+hermes model # provider → OpenAI-compatible, base url http://localhost:11434/v1, model qwen2.5-coder:7b
+
+# c) Make ONE rs-agents skill available to Hermes.
+# PRIMARY: add the repo as a skill tap
+hermes skills tap add Retail-Success/Wayroo.tools # ⚠️ may not discover ai/skills//SKILL.md (layout, see note)
+# FALLBACK if the tap finds nothing: install a single skill straight from its raw SKILL.md URL
+hermes skills install https://raw.githubusercontent.com/Retail-Success/Wayroo.tools//ai/skills/commit-conventions/SKILL.md --name commit-conventions
+hermes skills list # confirm the skill is present
+
+# d) End-to-end: local model + must use the skill
+: > /tmp/ollama.log
+hermes -z "Using our commit-conventions skill, write a conventional commit message for: fixed the .env ordering bug in onboard.sh (ticket WR-18897)."
+grep -iE "chat/completions|qwen2.5-coder" /tmp/ollama.log # proof inference was LOCAL
+```
+
+**✅ GO** if: the reply is correct *and shaped by the skill* (e.g. a `fix(...)` line with the WR‑18897 prefix),
+**and** `/tmp/ollama.log` shows the request hit the local model. → proceed to §4.
+
+**🛑 NO‑GO branches (decide here, cheaply):**
+- *Local model ignores/garbles the skill* → small‑model tool/skill‑following is the limiter. Options: use a
+ bigger local model, gate `/rs-agents` behind an explicit slash invocation, or keep rs‑agents on Claude
+ Code and use Ollama only for cheap/offline tasks. **This finding reshapes §5 — better to learn it now.**
+- *`hermes skills tap` doesn't discover the skills* → confirms the **layout mismatch** below; the real work
+ becomes a thin export (next bullet), not the harness.
+
+> **Layout note (the likely real friction):** `hermes skills install` uses an `/skills/`
+> identifier — i.e. skills at a repo's **top‑level `skills/`**. rs‑agents skills live at **`ai/skills//`**.
+> So a naive tap of the repo may find nothing. Fallbacks, cheapest first: (1) install individual skills by
+> raw URL (works regardless of layout); (2) a thin **export** that mirrors `ai/skills/**` → a top‑level
+> `skills/**` on a branch/dedicated repo the tap understands; (3) `hermes mcp` against an rs‑agents MCP
+> server (Tier 2). The spike tells us which we need.
+
+---
+
+## 4. The provisioning script (dotfiles) — **conditional on §3 = GO**
+
+New idempotent `mac-mini-agent/provision-coding-harness.sh`, callable standalone or from `onboard.sh`,
+with a real `--dry-run` (print actions, change nothing — the bootstrap has none today). Stages:
+
+1. **Ollama as a service with perf flags baked in (O6).** ⚠️ `brew services start ollama` does **not** set
+ the env vars. Capture them properly via a launchd agent:
+ `~/Library/LaunchAgents/com.retailsuccess.ollama.plist` with
+ `EnvironmentVariables = { OLLAMA_FLASH_ATTENTION = "1"; OLLAMA_KV_CACHE_TYPE = "q8_0"; }`,
+ `ProgramArguments = [/opt/homebrew/opt/ollama/bin/ollama, serve]`, `RunAtLoad`, `KeepAlive`.
+ (Simple alternative, no perf flags: `brew services start ollama`.)
+2. **Pre‑pull models:** `llama3.2:3b` (smoke) + `qwen2.5-coder:7b` (coding). **[rec]** — sized to RAM (≤ ~60%).
+3. **Hermes config (scriptable):** `hermes setup --non-interactive`; default model/provider → Ollama; add a
+ cloud **fallback** chain (`hermes fallback add`) so it degrades to cloud when local can't cope.
+4. **rs‑agents available to Hermes (§5, Tier 1):** `hermes skills tap add …` (or the export from §3) so
+ skills are discoverable on‑demand by any selected model.
+5. **Dashboard:** `hermes dashboard` on `127.0.0.1:9119` (external exposure handled in §6, mini).
+6. **Health check:** `hermes doctor` + `hermes status` as the post‑provision gate.
+
+---
+
+## 5. ⭐ rs‑agents → whatever model is selected (O5 — the critical requirement)
+
+Hermes loads skills into the agent context independently of which model `hermes model` picks — so skills
+made available to Hermes ride along with **every** model. Two tiers; **pick the tier explicitly** (this is a
+real decision, not an implementation detail):
+
+### Tier 1 — skills + knowledge on‑demand *(recommended start; verified by §3)*
+- rs‑agents **skills** (59) and **knowledge** are standard `SKILL.md` / markdown → exposed to Hermes via
+ **`hermes skills tap`** so any model can pull the relevant one **on demand**.
+- **Do NOT force‑load all 59** into one `/rs-agents` bundle — that can blow a local model's context window.
+ At most a **small curated bundle** (e.g. the always‑on engineering‑operating‑rules + a router skill that
+ indexes the rest). Tap = discovery; bundle = a tiny core.
+- **Live orchestration** (orchestrator → specialists, workflows) **stays on Claude Code**, which is just one
+ selectable model in the harness. Non‑Claude models get the *content/standards*, not the sub‑agent spawning.
+- **Sync:** rs‑agents repo stays the source of truth; `hermes skills update` (or Hermes' `curator`) refreshes.
+
+### Tier 2 — port agents/workflows as personas *(large; defer until you decide it's needed)*
+- Export each agent's system prompt + each workflow as a Hermes skill/persona so a **local** model can
+ *replicate the team* (not just read the rules). This is a real build (export pipeline + sync + the
+ small‑model reliability question from §3), and may not reproduce true delegation on a 7B model.
+- **Recommendation:** start Tier 1; only commit to Tier 2 if the §3 spike shows local models follow skills
+ well *and* you specifically need offline/non‑Claude orchestration. **Your call — flag it.**
+
+---
+
+## 6. Access surfaces (O3)
+
+- **Dashboard (native, testable now):** `hermes dashboard`. Default bind `127.0.0.1` — local immediately.
+ **External:** Hermes' June‑2026 hardening **requires an auth provider (password/OAuth) on any public bind**
+ and recommends **bind localhost + a tunnel**. So external = `hermes dashboard` + **Tailscale Funnel /
+ Cloudflare Tunnel** + auth — a **mini** concern (no tailscale CLI here yet).
+- **MS Teams (the one non‑native piece — phase 2 / mini):** Hermes' gateway covers Telegram/Discord/
+ WhatsApp/Weixin/Slack, **not Teams**. Bridge via **`hermes webhook subscribe`** (inbound, event‑driven
+ activation) behind a small **Teams bot / Outgoing‑Webhook connector** (Azure Bot or a Teams incoming/
+ outgoing webhook → POSTs to the Hermes webhook route; replies via `hermes send`). Don't let this block the
+ local stand‑up.
+
+---
+
+## 7. Always‑on + promote to the mini (O1)
+
+- **Hermes always‑on:** `hermes gateway install` (installs a launchd background service) — native, no custom
+ units needed. `hermes gateway start/status`.
+- **Ollama always‑on:** the launchd plist from §4.1 (`RunAtLoad`/`KeepAlive`).
+- **Headless survival (from `mac-mini-mcp-bootstrap.sh`):** auto‑login, Docker Desktop autostart,
+ `pmset`/`systemsetup` no‑sleep, Keychain auto‑unlock, MCP secrets in Keychain.
+- **External access:** add the mini to `~/.ssh/config` (absent today); Tailscale for the tunnel + dashboard.
+- **MCP tools:** the existing `mac-mini-mcp-bootstrap.sh` (17 servers) remains the tool plane; optionally
+ `hermes mcp add MCP_DOCKER` so Hermes shares the same tools as Claude Code.
+
+---
+
+## 8. Fold learnings back into rs‑agents (O7 — decided: extend the two existing skills, no new skill)
+Author in native dirs → `node ai/scripts/generate-index.js` → bump plugin `version` (per `ai/CLAUDE.md`).
+- **`local-development-setup`** — add a "Local AI agent host" section: the proven §3/§4 recipe (Ollama
+ service + env vars, Hermes→Ollama, the rs‑agents‑to‑Hermes mechanism that actually worked) + §7 promotion.
+ Widen its `description:` to mention Mac provisioning + local‑model setup.
+- **`developer-onboarding`** — pointer to the dotfiles one‑liner + "your machine can run local models" note
+ linking to the above.
+- **dotfiles:** add `--dry-run`/`DRY_RUN` to `mac-mini-mcp-bootstrap.sh` and ship `provision-coding-harness.sh`.
+
+---
+
+## 9. Risks & guardrails
+- **Architecture rests on §3** — local model actually following an rs‑agents skill. Validate before building.
+- **Small‑model reliability** — qwen2.5‑coder:7b may follow skills inconsistently; that's the real limiter on "any model."
+- **Context window** — don't force‑load 59 skills into one bundle; tap on‑demand (§5).
+- **Layout mismatch** — `hermes skills tap` likely won't see `ai/skills/**`; expect a thin export (§3 note).
+- **External exposure** — dashboard public bind requires auth; use a tunnel, never a raw public bind. Secrets in Keychain, not the repo.
+- **PATH shadowing** — `~/.local/bin/hermes` (pip) shadows brew `hermes-agent`; pin one in the dotfiles.
+- **Daily driver** — this is a *real* stand‑up (not throwaway), but on the mini for always‑on; keep the laptop service stoppable (`hermes gateway stop`, `launchctl unload`).
+
+---
+
+## 10. Decisions & the one bounded ask
+**Decided:** plan‑as‑deliverable (no full execution yet) · models `llama3.2:3b` + `qwen2.5-coder:7b` ·
+capture by extending the two existing rs‑agents skills.
+
+**Decided 06‑28 (rev 2):** run the §3 GO/NO‑GO spike now · **Teams bridge deferred to the mini phase**
+(local harness + dashboard first).
+
+**Still open (the spike informs these):**
+- **rs‑agents tier:** Tier 1 (skills on‑demand, recommended) vs. commit to Tier 2 (persona export) — decide after §3.
+
+### §3 spike results — _(appended below as the test runs)_
+```
diff --git a/mac-mini-agent/OLLAMA-HERMES-RUNBOOK.md b/mac-mini-agent/OLLAMA-HERMES-RUNBOOK.md
new file mode 100644
index 0000000..e99adee
--- /dev/null
+++ b/mac-mini-agent/OLLAMA-HERMES-RUNBOOK.md
@@ -0,0 +1,139 @@
+# Standing up Ollama + Hermes — Runbook
+
+> Live log of the exact steps required to get Ollama serving local models **through** the
+> Hermes Agent harness. Captured as we execute on `rss-mbp-5` so it can be folded into a
+> dotfiles provisioning script (DRY-RUN-PLAN.md §4 / O4) and replayed on the mac-mini.
+>
+> **Machine:** `rss-mbp-5` (MacBook, Apple M4, 16 GB) · **Hermes:** v0.17.0 (pip, `~/.local/bin/hermes`) · **Ollama:** 0.30.11 (brew) · **Date:** 2026-06-29
+
+---
+
+## TL;DR of what we discovered
+
+1. Hermes was **already configured** to use Ollama during the 2026-06-28 spike
+ (`provider: custom`, `base_url: http://localhost:11434/v1`) — but the spike **never reached GO**.
+2. **The blocker:** Hermes refuses any *primary* model whose context window is **< 64,000 tokens**.
+ `qwen2.5-coder:7b` reports only **32K** → rejected. This is why the spike stalled
+ (evidence: `~/.hermes/config.yaml.pre-spike-bak` + the empty `§3 results` block in DRY-RUN-PLAN.md).
+3. **Second, hidden issue:** Ollama itself only *serves* 4096 tokens by default
+ (`default_num_ctx=4096` in its log) unless `OLLAMA_CONTEXT_LENGTH` is raised.
+4. **Fix / model choice:** use **`gemma3:4b` (Gemma 3 4B, 128K native context)** as the general
+ default — it clears the 64K floor with **no override hack**. Keep `qwen2.5-coder:7b` as the
+ explicit *coding* model (its 32K is handled separately when selected).
+
+---
+
+## The steps
+
+### Step 0 — Ollama server up with performance flags ✅ DONE
+```bash
+OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0 ollama serve
+```
+Verify (log should show `OLLAMA_FLASH_ATTENTION:true`, `OLLAMA_KV_CACHE_TYPE:q8_0`, `Listening on 127.0.0.1:11434`):
+```bash
+curl -s http://localhost:11434/api/tags >/dev/null && echo "ollama UP"
+```
+> ⚠️ `brew services start ollama` does **NOT** set these env vars — see Step 6 for the durable launchd unit.
+
+### Step 1 — Pull the models ✅ DONE
+```bash
+ollama pull gemma3:4b # general default — 128K context, clears Hermes' 64K floor
+# already present from the 06-28 spike:
+# qwen2.5-coder:7b (coding model, 32K ctx)
+# llama3.2:3b (128K ctx, fast/smoke)
+ollama list
+```
+Confirmed: `gemma3:4b` (4.3B, **131072** ctx, 3.3 GB), `qwen2.5-coder:7b`, `llama3.2:3b` all present.
+
+### Step 2 — Point Hermes at the chosen model ✅ DONE (decision below)
+**Decision (2026-06-29):** default = **`llama3.1:8b`** (tools + native 128K, real 64K+ headroom,
+no override hacks); keep **`qwen2.5-coder:7b`** for coding (select with `hermes -m qwen2.5-coder:7b`).
+gemma3:4b dropped from the agent role (no tools).
+
+`~/.hermes/config.yaml` (backed up to `config.yaml.bak-gemma`):
+```yaml
+model:
+ default: llama3.1:8b
+ provider: custom
+ base_url: http://localhost:11434/v1
+ context_length: 65536 # cap displayed ctx → clears 64K floor; memory-safe on 16 GB
+ ollama_num_ctx: 65536 # request 64K runtime ctx from Ollama (required for 32K-native qwen)
+```
+
+### Step 3 — Make Ollama actually *serve* enough context ✅ DONE (manual; launchd still pending → Step 6)
+Default is 4096; raised so long agent sessions aren't silently truncated. Serve restarted with:
+```bash
+OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0 OLLAMA_CONTEXT_LENGTH=65536 ollama serve
+```
+**Verified the Ollama side works end to end:** a direct call to the OpenAI-compat endpoint
+returns correct output, and `ollama ps` shows `gemma3:4b` loaded at **65536 ctx, 100% GPU, ~2.9 GB**:
+```bash
+curl -s http://localhost:11434/v1/chat/completions -H 'Content-Type: application/json' \
+ -d '{"model":"gemma3:4b","messages":[{"role":"user","content":"Reply with: DIRECT_OK"}],"max_tokens":20}'
+# -> "DIRECT_OK"
+```
+
+### Step 4 — Smoke test: prove Hermes routes to the LOCAL model ✅ GO (plumbing proven)
+`hermes -z "What is 17 + 25? Reply with just the number."` → **`42`**, exit 0, with
+`qwen2.5-coder:7b` loaded 100% on GPU. Hermes ↔ Ollama agent path works end to end.
+
+---
+
+## 🔑 Root-cause writeup: why `hermes -z` "hung"/failed (RESOLVED)
+
+It wasn't a hang. `hermes_cli/oneshot.py:148` does `logging.disable(logging.CRITICAL)` and wraps the
+whole run in `redirect_stdout/stderr(devnull)` — so **all errors are silenced** in one-shot mode.
+oneshot returns **exit 2** when the agent turn comes back `failed`/`partial` with empty text
+(`oneshot.py:223`). The real error was only visible by calling `_run_agent()` directly with logging
+on. It was:
+
+```
+HTTP 400: registry.ollama.ai/library/gemma3:4b does not support tools
+```
+
+**Two hard requirements for ANY local model used as a Hermes _primary_:**
+1. **Tool-calling support** — Hermes sends tool definitions every request. Models without `tools`
+ capability get a 400 and the turn fails.
+2. **≥64K context** — Hermes' floor for reliable tool use, checked twice: the *displayed* context
+ (static gate) AND the *runtime* context Ollama actually loads.
+
+### Capability matrix (from `ollama show`)
+| Model | tools | context (native) | Verdict as Hermes default |
+|---|---|---|---|
+| `gemma3:4b` | ❌ (vision only) | 128K | ❌ unusable — no tools (this was the blocker) |
+| `llama3.2:3b` | ✅ | 128K | ⚠️ works, but 3B is too weak — emitted a bogus tool call, ~94 s |
+| `qwen2.5-coder:7b` | ✅ | 32K | ✅ works with both context knobs; strong; effective ctx ~32K |
+
+### The two context knobs (needed for 32K-native models like qwen)
+```yaml
+model:
+ context_length: 65536 # overrides the DISPLAYED context → clears the static 64K gate
+ ollama_num_ctx: 65536 # makes Hermes REQUEST 64K runtime ctx from Ollama
+```
+⚠️ Ollama still clamps qwen2.5-coder:7b to its trained **32K** at load (`ollama ps` shows
+CONTEXT 32768). The knobs satisfy Hermes' gate; true 64K+ requires a natively-large model
+(e.g. `llama3.1:8b`, 128K + tools) or a yarn-rope Modelfile.
+
+### ⚠️ Latency / tuning (open)
+First one-shot call: **94–148 s**. Dominated by (a) cold model load, (b) evaluating a large system
+prompt — **44 enabled tools + a 71 KB `AGENTS.md` (truncated to 20K)**. Levers: trim/disable unused
+tools (`-t`/plugin disable), shrink `AGENTS.md`, keep the model warm (`OLLAMA_KEEP_ALIVE`).
+
+### Step 5 — Wire rs-agents skills to whatever model is selected (O5) ⬜ PENDING
+
+### Step 6 — Durable services (launchd) + fold into dotfiles (O4/O6) ⬜ PENDING
+
+---
+
+## How to test it works (layered)
+
+| Layer | What it proves | How |
+|---|---|---|
+| **A. Ollama alone** | local inference works | `ollama run gemma3:4b "say hi"` |
+| **B. Hermes → Ollama** | the harness routes to the local model | `hermes -z "Reply with exactly: LOCAL_OK"` **while** tailing `~/.ollama` / serve log for a `chat/completions` hit |
+| **C. Model identity** | the *right* model answered | `ollama ps` shows `gemma3:4b` loaded during the call |
+| **D. Skill-following (O5)** | a local model uses an rs-agents skill | `hermes -z "Using the commit-conventions skill, write a commit message for: fix .env ordering bug (WR-18897)"` → output must be *shaped by the skill* |
+| **E. Dashboard / tunnel** | external surface works | open the dev-tunnel URL, chat, confirm reply |
+
+**GO** = layers A–D pass. **NO-GO branch** = if a local model can't follow the skill (D),
+keep rs-agents on Claude and use Ollama for cheap/offline tasks only (DRY-RUN-PLAN §3 / §9).
diff --git a/mac-mini-agent/com.retailsuccess.ollama.plist b/mac-mini-agent/com.retailsuccess.ollama.plist
new file mode 100644
index 0000000..13a8776
--- /dev/null
+++ b/mac-mini-agent/com.retailsuccess.ollama.plist
@@ -0,0 +1,46 @@
+
+
+
+
+ Label
+ com.retailsuccess.ollama
+
+ ProgramArguments
+
+ /opt/homebrew/bin/ollama
+ serve
+
+
+ EnvironmentVariables
+
+ OLLAMA_FLASH_ATTENTION
+ 1
+ OLLAMA_KV_CACHE_TYPE
+ q8_0
+ OLLAMA_CONTEXT_LENGTH
+ 65536
+
+
+ RunAtLoad
+
+ KeepAlive
+
+
+ StandardOutPath
+ /tmp/ollama.out.log
+ StandardErrorPath
+ /tmp/ollama.err.log
+
+
diff --git a/mac-mini-agent/provision-coding-harness.sh b/mac-mini-agent/provision-coding-harness.sh
new file mode 100755
index 0000000..41d04cc
--- /dev/null
+++ b/mac-mini-agent/provision-coding-harness.sh
@@ -0,0 +1,152 @@
+#!/usr/bin/env bash
+# ─────────────────────────────────────────────────────────────────────────────
+# provision-coding-harness.sh
+# Stand up the local AI coding harness: Ollama (local models) behind Hermes,
+# with rs-agents skills synced in. Idempotent + re-runnable.
+#
+# bash provision-coding-harness.sh [--dry-run] [--enable-service]
+#
+# --dry-run print what would happen, change nothing
+# --enable-service install the always-on launchd Ollama unit even off a mini
+#
+# Model is chosen by RAM (both need tool-calling + >=64K context for Hermes):
+# >=32 GB (mac-mini / always-on) -> llama3.1:8b (native 128K, true 64K)
+# <32 GB (laptop) -> qwen2.5-coder:7b (32K, lifted via knobs)
+#
+# The always-on launchd service is installed only on a >=32 GB box (or with
+# --enable-service): a KeepAlive model resident 24/7 would starve a 16 GB laptop.
+# See OLLAMA-HERMES-RUNBOOK.md for the why behind every step.
+# ─────────────────────────────────────────────────────────────────────────────
+set -euo pipefail
+
+# Onboarding + launchd runs often lack Homebrew on PATH; make brew-installed and
+# user-local tools (ollama, hermes) resolve on both Apple-silicon and Intel Macs.
+export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
+
+DRY=0; ENABLE_SERVICE=0
+for a in "$@"; do
+ case "$a" in
+ --dry-run) DRY=1 ;;
+ --enable-service) ENABLE_SERVICE=1 ;;
+ *) echo "unknown arg: $a"; exit 2 ;;
+ esac
+done
+
+bold() { printf '\033[1m%s\033[0m\n' "$*"; }
+step() { printf '\n\033[1;36m▶ %s\033[0m\n' "$*"; }
+warn() { printf '\033[1;33m! %s\033[0m\n' "$*"; }
+run() { if [[ $DRY -eq 1 ]]; then printf ' [dry-run] %s\n' "$*"; else eval "$*"; fi; }
+
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+OLLAMA_BIN="${OLLAMA_BIN:-$(command -v ollama || true)}"
+HERMES_PY="${HERMES_PY:-$HOME/.hermes/hermes-agent/venv/bin/python}"
+RAM_GB=$(( $(sysctl -n hw.memsize) / 1073741824 ))
+
+# ── 0. Prereqs ───────────────────────────────────────────────────────────────
+step "0. Prerequisites (RAM=${RAM_GB} GB)"
+[[ -n "$OLLAMA_BIN" && -x "$OLLAMA_BIN" ]] || { warn "ollama not installed — 'brew install ollama' (or run the Brewfile)."; exit 1; }
+command -v hermes >/dev/null || { warn "hermes not installed — run onboarding_bin/install-hermes-agent.sh first."; exit 1; }
+[[ -x "$HERMES_PY" ]] || { warn "Hermes venv python not found at $HERMES_PY — re-run onboarding_bin/install-hermes-agent.sh (or set HERMES_PY)."; exit 1; }
+bold " ollama: $("$OLLAMA_BIN" --version 2>/dev/null | head -1)"
+bold " hermes: $(hermes --version 2>/dev/null | head -1)"
+
+if (( RAM_GB >= 32 )); then MODEL="llama3.1:8b"; else MODEL="qwen2.5-coder:7b"; fi
+SMOKE="llama3.2:3b"
+bold " chosen default model: ${MODEL} (smoke: ${SMOKE})"
+
+# ── 1. Ollama service ────────────────────────────────────────────────────────
+step "1. Ollama server"
+PLIST="$HERE/com.retailsuccess.ollama.plist"
+DEST_PLIST="$HOME/Library/LaunchAgents/com.retailsuccess.ollama.plist"
+if (( RAM_GB >= 32 || ENABLE_SERVICE == 1 )); then
+ bold " installing always-on launchd unit (perf env vars baked in)"
+ run "cp '$PLIST' '$DEST_PLIST'"
+ run "launchctl unload '$DEST_PLIST' 2>/dev/null || true"
+ run "launchctl load -w '$DEST_PLIST'"
+else
+ warn " laptop (<32 GB): skipping always-on service. Starting Ollama on-demand for this run."
+ if ! curl -s --max-time 3 http://localhost:11434/api/tags >/dev/null 2>&1; then
+ run "OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0 OLLAMA_CONTEXT_LENGTH=65536 nohup '$OLLAMA_BIN' serve >/tmp/ollama.out.log 2>&1 &"
+ [[ $DRY -eq 0 ]] && sleep 4
+ fi
+fi
+[[ $DRY -eq 0 ]] && { curl -s --max-time 5 http://localhost:11434/api/tags >/dev/null && bold " Ollama API up ✔" || warn " Ollama API not responding yet"; }
+
+# ── 2. Pull models ───────────────────────────────────────────────────────────
+step "2. Pull models (sized to RAM)"
+for m in "$MODEL" "$SMOKE"; do
+ if [[ $DRY -eq 0 ]] && "$OLLAMA_BIN" list 2>/dev/null | grep -q "^${m}[[:space:]]"; then
+ bold " ${m} already present"
+ else
+ run "'$OLLAMA_BIN' pull '$m'"
+ fi
+done
+
+# ── 3. Configure Hermes -> Ollama (idempotent, preserves comments) ───────────
+step "3. Point Hermes at Ollama (model + 64K context knobs)"
+run "cp -p '$HOME/.hermes/config.yaml' '$HOME/.hermes/config.yaml.bak-provision' 2>/dev/null || true"
+if [[ $DRY -eq 1 ]]; then
+ printf ' [dry-run] set model.default=%s, provider=custom, base_url=localhost:11434/v1, context_length=65536, ollama_num_ctx=65536\n' "$MODEL"
+else
+ "$HERMES_PY" - "$MODEL" <<'PY'
+import sys, os
+model = sys.argv[1]
+p = os.path.expanduser("~/.hermes/config.yaml")
+block = {"default": model, "provider": "custom",
+ "base_url": "http://localhost:11434/v1",
+ "context_length": 65536, "ollama_num_ctx": 65536}
+try:
+ from ruamel.yaml import YAML # preserves comments
+ yaml = YAML()
+ with open(p) as f: data = yaml.load(f)
+ data["model"] = block
+ with open(p, "w") as f: yaml.dump(data, f)
+ print(" config.yaml updated (ruamel, comments preserved)")
+except Exception:
+ import yaml as pyyaml # fallback: comments lost (backup exists)
+ with open(p) as f: data = pyyaml.safe_load(f)
+ data["model"] = block
+ with open(p, "w") as f: pyyaml.safe_dump(data, f, sort_keys=False)
+ print(" config.yaml updated (pyyaml; comments not preserved — see .bak-provision)")
+PY
+fi
+
+# ── 4. Ensure the rs-agents Claude Code plugin (the sync SOURCE) ─────────────
+step "4. Ensure rs-agents Claude Code plugin (source-of-truth channel)"
+run "bash '$HERE/../onboarding_bin/install-claude-plugins.sh'"
+
+# ── 5. Sync rs-agents skills into Hermes (the bridge) ────────────────────────
+step "5. Sync rs-agents skills into Hermes"
+run "bash '$HERE/sync-rs-agents-to-hermes.sh' --soft"
+
+# ── 6. Keep Hermes in sync — launchd timer re-runs the sync (login + hourly) ─
+# The clean-rebuild sync mirrors the plugin cache, so add/modify/DELETE all
+# propagate. Cheap + idempotent, so it runs on both laptop and mini.
+step "6. Install the rs-agents -> Hermes auto-resync timer"
+SYNC_PLIST="$HOME/Library/LaunchAgents/com.retailsuccess.rs-agents-sync.plist"
+if [[ $DRY -eq 1 ]]; then
+ printf ' [dry-run] write + load %s (RunAtLoad + hourly: bash %s --soft)\n' "$SYNC_PLIST" "$HERE/sync-rs-agents-to-hermes.sh"
+else
+ cat > "$SYNC_PLIST" <
+
+ Labelcom.retailsuccess.rs-agents-sync
+ ProgramArguments
+ /bin/bash$HERE/sync-rs-agents-to-hermes.sh--soft
+
+ RunAtLoad
+ StartInterval3600
+ StandardOutPath/tmp/rs-agents-sync.out.log
+ StandardErrorPath/tmp/rs-agents-sync.err.log
+
+PLIST
+ launchctl unload "$SYNC_PLIST" 2>/dev/null || true
+ launchctl load -w "$SYNC_PLIST"
+ bold " auto-resync installed (hourly + on login). Disable: launchctl unload $SYNC_PLIST"
+fi
+
+# ── 7. Health check ──────────────────────────────────────────────────────────
+step "7. Verify the harness"
+run "bash '$HERE/verify-ollama-hermes.sh' '$MODEL'"
+
+printf '\n'; bold "✔ Coding harness provisioned. Default model: ${MODEL}. Talk to it with: hermes -z \"...\""
diff --git a/mac-mini-agent/sync-rs-agents-to-hermes.sh b/mac-mini-agent/sync-rs-agents-to-hermes.sh
new file mode 100755
index 0000000..9ec1fb0
--- /dev/null
+++ b/mac-mini-agent/sync-rs-agents-to-hermes.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+# ─────────────────────────────────────────────────────────────────────────────
+# sync-rs-agents-to-hermes.sh
+# Mirror the rs-agents skills from the INSTALLED Claude Code plugin into Hermes.
+#
+# This is the Hermes equivalent of "reload plugins": Claude Code consumes the
+# rs-agents plugin natively, but Hermes has its own skills store and cannot read
+# the plugin layout (`ai/skills//`). This script copies the skills from the
+# *released* plugin cache into `~/.hermes/skills/rs-agents/` so the local model
+# can discover them on-demand.
+#
+# ⚠️ READ-ONLY CONSUMER. The source of truth is the Wayroo.tools repo; new/
+# changed skills are authored there and shipped via the plugin (Jira → PR →
+# merge → `reload plugins`). NEVER author a skill in ~/.hermes/skills — it is
+# overwritten on every sync.
+#
+# Safe to sync all skills: Hermes injects only a ~8 KB skills *index* (name +
+# description) into the prompt and loads a full skill on-demand via its tool, so
+# the per-skill prompt cost is one index line, not the whole body.
+# ─────────────────────────────────────────────────────────────────────────────
+set -euo pipefail
+
+# launchd runs with a minimal PATH; make sure user-local tools (hermes) resolve.
+export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
+
+bold() { printf '\033[1m%s\033[0m\n' "$*"; }
+step() { printf '\n\033[1;36m▶ %s\033[0m\n' "$*"; }
+warn() { printf '\033[1;33m! %s\033[0m\n' "$*"; }
+
+HERMES_SKILLS_DIR="${HERMES_SKILLS_DIR:-$HOME/.hermes/skills/rs-agents}"
+
+# --soft: for onboarding — if no source is found, warn and exit 0 (don't fail the install).
+SOFT=0
+[[ "${1:-}" == "--soft" ]] && SOFT=1
+
+step "Locating rs-agents skills (source of truth: the released plugin)"
+# 1) Prefer the installed Claude Code plugin cache — exactly what `reload plugins` gave.
+# (sort -V picks the newest version within a marketplace; if rs-agents ever ships
+# from more than one marketplace, sort on the version segment instead — the
+# marketplace name sorts before the version here.)
+SRC="$(ls -d "$HOME"/.claude/plugins/cache/*/rs-agents/*/skills 2>/dev/null | sort -V | tail -1 || true)"
+# 2) Fallback: a local Wayroo.tools clone (RS_AGENTS_SKILLS_SRC overrides). Same
+# /SKILL.md layout, so the copy below works for either source.
+if [[ -z "${SRC}" || ! -d "${SRC}" ]]; then
+ for cand in "${RS_AGENTS_SKILLS_SRC:-}" \
+ "$HOME/Retail-Success/repos/development-team/Wayroo.tools/ai/skills"; do
+ [[ -n "$cand" && -d "$cand" ]] && { SRC="$cand"; break; }
+ done
+fi
+if [[ -z "${SRC}" || ! -d "${SRC}" ]]; then
+ warn "rs-agents skills not found (no plugin cache, no Wayroo.tools clone)."
+ warn "Install the rs-agents plugin in Claude Code (add the wayroo marketplace + 'reload plugins'),"
+ warn "or set RS_AGENTS_SKILLS_SRC=/path/to/Wayroo.tools/ai/skills, then re-run this script."
+ [[ $SOFT -eq 1 ]] && { warn "(--soft) skipping rs-agents sync; Hermes install continues."; exit 0; }
+ exit 1
+fi
+bold " source: ${SRC}"
+
+step "Mirroring skills into Hermes"
+# Clean rebuild so removed/renamed skills don't linger (idempotent) — but stage
+# into a temp dir and swap only on a non-empty mirror: this sync runs hourly via
+# launchd, so a restructured/empty source must never wipe a working skill set.
+mkdir -p "$(dirname "${HERMES_SKILLS_DIR}")"
+STAGE="$(mktemp -d "${HERMES_SKILLS_DIR}.stage.XXXXXX")"
+trap 'rm -rf "${STAGE}"' EXIT
+count=0
+for d in "${SRC}"/*/; do
+ [[ -f "${d}SKILL.md" ]] || continue
+ name="$(basename "$d")"
+ mkdir -p "${STAGE}/${name}"
+ cp "${d}SKILL.md" "${STAGE}/${name}/SKILL.md"
+ count=$((count + 1))
+done
+if (( count == 0 )); then
+ warn "source '${SRC}' contains no /SKILL.md dirs — leaving existing Hermes skills untouched."
+ warn "The plugin layout may have changed; update the source lookup in this script."
+ [[ $SOFT -eq 1 ]] && { warn "(--soft) skipping rs-agents sync; Hermes install continues."; exit 0; }
+ exit 1
+fi
+rm -rf "${HERMES_SKILLS_DIR}"
+mv "${STAGE}" "${HERMES_SKILLS_DIR}"
+trap - EXIT
+bold " synced ${count} rs-agents skills -> ${HERMES_SKILLS_DIR}"
+
+step "Verifying Hermes sees them"
+if command -v hermes >/dev/null 2>&1; then
+ # grep -c exits 1 on zero matches; don't let pipefail abort after a good sync.
+ hermes skills list 2>/dev/null | grep -c "rs-agents" | xargs -I{} echo " hermes reports {} skills in the 'rs-agents' category" || true
+else
+ warn "hermes not on PATH — skipped verification"
+fi
+bold "✔ Sync complete. Local models can now discover rs-agents skills on-demand."
diff --git a/mac-mini-agent/verify-ollama-hermes.sh b/mac-mini-agent/verify-ollama-hermes.sh
new file mode 100755
index 0000000..b1669bf
--- /dev/null
+++ b/mac-mini-agent/verify-ollama-hermes.sh
@@ -0,0 +1,133 @@
+#!/usr/bin/env bash
+# verify-ollama-hermes.sh — prove the Ollama + Hermes local-agent harness works CORRECTLY.
+#
+# Tests each link in the chain with explicit PASS/FAIL, proves inference is actually
+# LOCAL (not a silent cloud fallback), and measures memory-fit + latency (the thing that
+# made llama3.1:8b unusable on a 16 GB box). Repeatable + model-agnostic, so the same
+# script validates a fresh Mac or the 24/7 mini.
+#
+# Usage: ./verify-ollama-hermes.sh [model] (default: hermes config model.default)
+#
+set -uo pipefail
+
+# launchd/onboarding runs may lack Homebrew on PATH; make ollama + hermes resolve
+# on both Apple-silicon and Intel Macs.
+export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
+
+OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}"
+CFG="$HOME/.hermes/config.yaml"
+# model.default may be quoted in YAML (model names contain ':'), so strip quotes.
+MODEL="${1:-$(awk '/^model:/{m=1;next} m&&/default:/{gsub(/["'\'']/,"",$2); print $2; exit}' "$CFG" 2>/dev/null)}"
+
+if [ -z "$MODEL" ]; then
+ echo "Error: no default model could be derived." >&2
+ echo " Checked Hermes config: $CFG" >&2
+ echo " Expected a 'model:' section with a 'default:' entry, or a model argument:" >&2
+ echo " $0 " >&2
+ exit 1
+fi
+
+MARK="===VERIFY $(date +%H%M%S)==="
+# Where `ollama serve` request lines actually land: the launchd unit sends stderr
+# (the request/access log) to ollama.err.log; the on-demand start in
+# provision-coding-harness.sh merges both streams into ollama.out.log. Mark and
+# scan whichever exists to prove a local hit (override with OLLAMA_LOGS).
+SLOGS="${OLLAMA_LOGS:-/tmp/ollama.err.log /tmp/ollama.out.log}"
+FAILED=0
+
+g(){ printf " \033[32mPASS\033[0m %s\n" "$1"; }
+r(){ printf " \033[31mFAIL\033[0m %s\n" "$1"; FAILED=1; }
+i(){ printf " ···· %s\n" "$1"; }
+
+echo "════════════════════════════════════════════════════════════"
+echo " Verifying Ollama + Hermes harness model=$MODEL"
+echo "════════════════════════════════════════════════════════════"
+
+# L1 — Ollama server reachable
+echo "[L1] Ollama server"
+curl -s --max-time 5 "$OLLAMA_URL/api/tags" >/dev/null \
+ && g "Ollama API responding ($OLLAMA_URL)" \
+ || { r "Ollama API not responding — start it (ollama serve)"; echo "ABORT"; exit 1; }
+
+# L2 — model pulled + the two hard requirements (tools, >=64K context)
+echo "[L2] Model capabilities (Hermes needs BOTH: tools + >=64K context)"
+if ollama show "$MODEL" >/dev/null 2>&1; then
+ g "model '$MODEL' present"
+ CAPS=$(ollama show "$MODEL" 2>/dev/null | awk '/Capabilities/{c=1;next} /Parameters|Projector|System|License/{c=0} c&&NF{print $1}' | tr '\n' ' ')
+ CTX=$(ollama show "$MODEL" 2>/dev/null | awk '/context length/{print $NF}')
+ case " $CAPS " in *" tools "*) g "tool-calling supported";; *) r "NO tool-calling — unusable as a Hermes primary (caps: $CAPS )";; esac
+ if [ "${CTX:-0}" -ge 64000 ]; then g "native context ${CTX} >= 64000"; else i "native context ${CTX} < 64000 — relies on config context_length/ollama_num_ctx override"; fi
+else
+ r "model '$MODEL' not pulled (ollama pull $MODEL)"
+fi
+
+# L3 — raw model works via the OpenAI-compat endpoint Hermes uses (isolates model from Hermes)
+echo "[L3] Direct inference (OpenAI-compat endpoint, no Hermes)"
+t0=$(date +%s)
+DIRECT=$(curl -s --max-time 180 "$OLLAMA_URL/v1/chat/completions" -H 'Content-Type: application/json' \
+ -d "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly: PING_OK\"}],\"max_tokens\":16}" \
+ | python3 -c 'import sys,json;print(json.load(sys.stdin)["choices"][0]["message"]["content"].strip())' 2>/dev/null)
+t1=$(date +%s)
+echo "$DIRECT" | grep -q "PING_OK" && g "direct inference correct ($((t1-t0))s)" || r "direct inference wrong/empty: '$DIRECT'"
+
+# snapshot swap to detect memory thrash across the heavy Hermes call
+sw(){ sysctl -n vm.swapusage | sed -E 's/.*used = ([0-9.]+M).*/\1/'; }
+SWAP0=$(sw)
+for f in $SLOGS; do [ -e "$f" ] && printf '\n%s\n' "$MARK" >> "$f"; done
+
+# L4 — Hermes end-to-end AND proof it routed LOCALLY (the subtle correctness check)
+echo "[L4] Hermes -z end-to-end + local-routing proof"
+t0=$(date +%s)
+OUT=$(hermes -z "What is 17 + 25? Reply with just the number as plain text." 2>/dev/null); RC=$?
+t1=$(date +%s); LAT=$((t1-t0))
+{ [ $RC -eq 0 ] && echo "$OUT" | grep -q "42"; } \
+ && g "correct answer (exit 0, ${LAT}s): '$OUT'" \
+ || r "Hermes failed (exit $RC, ${LAT}s): '$OUT'"
+# local proof #1: Ollama logged a chat request during the window
+LOCAL_HIT=0
+for f in $SLOGS; do
+ awk -v m="$MARK" '$0~m{f=1} f' "$f" 2>/dev/null | grep -qiE "chat/completions|POST" && { LOCAL_HIT=1; break; }
+done
+if [ "$LOCAL_HIT" -eq 1 ]; then
+ g "request hit the LOCAL Ollama server (not cloud)"
+else
+ i "no local request line in ollama log (check proof #2)"
+fi
+# local proof #2: the model is loaded in ollama ps. If BOTH proofs come up empty,
+# the "inference is actually LOCAL" guarantee is unproven — that is a FAIL, not info.
+PS=$(ollama ps 2>/dev/null | sed -n '2p')
+if [ -n "$PS" ]; then
+ g "model loaded locally: $PS"
+elif [ "$LOCAL_HIT" -eq 1 ]; then
+ i "ollama ps shows nothing loaded (may have unloaded)"
+else
+ r "no local-routing evidence (no ollama log hit, nothing loaded) — possible cloud fallback"
+fi
+
+# FIT — memory + latency (why llama3.1:8b@64K failed on 16 GB)
+echo "[FIT] Memory + latency"
+i "latency ${LAT}s swap used ${SWAP0} -> $(sw)"
+[ "${LAT:-9999}" -le 300 ] && g "latency within 300s budget" \
+ || r "latency ${LAT}s > 300s — likely memory thrash; model too big for this machine"
+
+# L5 — the REAL-USE test: does the local model actually follow an rs-agents skill? (O5)
+echo "[L5] rs-agents skill-following (the real-use bar)"
+if hermes skills list 2>/dev/null | grep -qi "commit-conventions"; then
+ SK=$(hermes -z "Using the commit-conventions skill, write ONLY a conventional commit subject line for: fixed the .env ordering bug in onboard.sh (ticket WR-18897)." 2>/dev/null)
+ # Pass = the skill's CONTENT shaped the output: a type(scope): pattern + the ticket key,
+ # found ANYWHERE (a local 7B often wraps it in a raw tool-call rather than clean text).
+ if echo "$SK" | grep -qiE "[a-z]+\([^)]+\):" && echo "$SK" | grep -qiE "WR-18897"; then
+ g "skill content applied: '$SK'"
+ echo "$SK" | grep -q "{" && i "note: local model wrapped it in a raw tool-call — content correct, agentic mechanics rough on a 7B"
+ else
+ r "skill not followed (weak model or not loaded): '$SK'"
+ fi
+else
+ i "SKIPPED — no rs-agents skill installed yet (Step C: hermes skills install )"
+fi
+
+echo "────────────────────────────────────────────────────────────"
+[ "$FAILED" -eq 0 ] && echo " RESULT: ✅ ALL CHECKS PASSED — $MODEL" \
+ || echo " RESULT: ❌ FAILED — $MODEL (see above)"
+echo "────────────────────────────────────────────────────────────"
+exit $FAILED
diff --git a/meta/configs/hermes.yml b/meta/configs/hermes.yml
new file mode 100644
index 0000000..803c095
--- /dev/null
+++ b/meta/configs/hermes.yml
@@ -0,0 +1,11 @@
+---
+# hermes - Installs the Hermes Agent (Nous Research) AI coding harness via its
+# official installer and opens the interactive setup wizard. Hermes self-updates
+# (`hermes update`) and lives under ~/.hermes, so it is installed from its own
+# installer, NOT via Homebrew. Requires ~/.local/bin on PATH (set up by base).
+
+- shell:
+ - description: 'Installing Hermes Agent (AI) and opening its setup wizard...'
+ command: './onboarding_bin/install-hermes-agent.sh'
+ stdout: true
+ stderr: true
diff --git a/meta/profiles/ai b/meta/profiles/ai
new file mode 100644
index 0000000..6081c5e
--- /dev/null
+++ b/meta/profiles/ai
@@ -0,0 +1 @@
+hermes
diff --git a/meta/profiles/base b/meta/profiles/base
index bf60dc4..20d9fbc 100755
--- a/meta/profiles/base
+++ b/meta/profiles/base
@@ -7,4 +7,5 @@ command_line
version_control
nvm
ide
+hermes
cleanup
diff --git a/onboarding_bin/connect-hermes-mcp.sh b/onboarding_bin/connect-hermes-mcp.sh
new file mode 100755
index 0000000..b0c81ea
--- /dev/null
+++ b/onboarding_bin/connect-hermes-mcp.sh
@@ -0,0 +1,208 @@
+#!/usr/bin/env bash
+# ─────────────────────────────────────────────────────────────────────────────
+# connect-hermes-mcp.sh
+# Wire the Hermes Agent to the Docker Desktop MCP Toolkit gateway.
+#
+# Adds ONE stdio MCP server entry — MCP_DOCKER — to ~/.hermes/config.yaml that
+# spawns `docker mcp gateway run --profile `. That single entry
+# multiplexes every MCP server enabled in the machine's Docker MCP Toolkit,
+# the same shape (and name) Claude Code already uses for its MCP_DOCKER entry.
+#
+# Why the gateway and not per-server wiring: the Toolkit (registry + Keychain)
+# stays the single source of truth for WHICH servers exist and for their
+# secrets. `docker mcp gateway run` watches for Toolkit changes by default
+# (--watch) and reads secret values from Docker Desktop's Keychain store
+# (--secrets docker-desktop) — secret values never touch Hermes config, this
+# repo, or git. Add/remove a server in the Toolkit and the gateway picks it up;
+# a Hermes session sees the new set on its next connect (Hermes also honors
+# MCP tools/list_changed notifications for live sessions).
+#
+# Why NOT `hermes mcp add`: that command is interactive (security probe + a
+# tty tool-selection checklist) and would hang a piped, zero-touch
+# `./install-profile ai`. Instead the entry is upserted through Hermes' own
+# config API (hermes_cli.mcp_config._save_mcp_server) via the Python
+# interpreter Hermes ships — same validation, locking, and atomic write as the
+# CLI, no interactivity, no extra dependency (no yq required).
+#
+# Idempotent: identical entry → no-op; missing/stale → insert/update in place.
+# Rollback: `hermes mcp remove MCP_DOCKER` — touches nothing else.
+#
+# Usage: connect-hermes-mcp.sh [--soft] [--dry-run]
+# --soft onboarding mode: a missing prerequisite warns and exits 0 so the
+# Hermes install never fails on this step; re-run later.
+# --dry-run show what would change without writing anything.
+# MCP_PROFILE= pin the gateway to a Toolkit profile (default: default —
+# the profile Docker Desktop manages; see `docker mcp profile ls`).
+# HERMES_TEST_TIMEOUT= cap on the live gateway connect test (default:
+# 180). A hung gateway must never stall a zero-touch install.
+# ─────────────────────────────────────────────────────────────────────────────
+set -euo pipefail
+
+# launchd / piped installs run with a minimal PATH; resolve user-local tools.
+export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
+
+bold() { printf '\033[1m%s\033[0m\n' "$*"; }
+step() { printf '\n\033[1;36m▶ %s\033[0m\n' "$*"; }
+warn() { printf '\033[1;33m! %s\033[0m\n' "$*"; }
+
+# macOS has no GNU timeout by default; perl (always present) is the fallback.
+run_with_timeout() {
+ local secs="$1"; shift
+ if command -v timeout >/dev/null 2>&1; then timeout "$secs" "$@"
+ elif command -v gtimeout >/dev/null 2>&1; then gtimeout "$secs" "$@"
+ else perl -e 'alarm shift; exec @ARGV' "$secs" "$@"
+ fi
+}
+
+SERVER_NAME="MCP_DOCKER"
+MCP_PROFILE="${MCP_PROFILE:-default}"
+HERMES_AGENT_DIR="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
+
+SOFT=0
+DRY_RUN=0
+for arg in "$@"; do
+ case "$arg" in
+ --soft) SOFT=1 ;;
+ --dry-run) DRY_RUN=1 ;;
+ *) warn "unknown flag: $arg (usage: connect-hermes-mcp.sh [--soft] [--dry-run])"; exit 2 ;;
+ esac
+done
+
+soft_fail() {
+ warn "$1"
+ [[ $SOFT -eq 1 ]] && { warn "(--soft) skipping Hermes⇄MCP-Toolkit wiring; install continues."; exit 0; }
+ exit 1
+}
+
+step "Preflight: docker + MCP Toolkit + hermes"
+command -v docker >/dev/null 2>&1 \
+ || soft_fail "docker CLI not found — install Docker Desktop, then re-run this script."
+DOCKER_BIN="$(command -v docker)"
+docker mcp --version >/dev/null 2>&1 \
+ || soft_fail "'docker mcp' CLI plugin missing — enable the MCP Toolkit in Docker Desktop settings."
+command -v hermes >/dev/null 2>&1 \
+ || soft_fail "hermes not on PATH — run install-hermes-agent.sh first."
+HERMES_PY="${HERMES_AGENT_DIR}/venv/bin/python"
+[[ -x "$HERMES_PY" ]] \
+ || soft_fail "Hermes venv python not found at ${HERMES_PY} — is the Hermes install complete?"
+
+# The wiring is written even when the Docker daemon is down — the entry simply
+# activates on the next Hermes session once Docker Desktop is running. Only the
+# live verification below needs the daemon.
+DAEMON_UP=1
+if ! docker info >/dev/null 2>&1; then
+ DAEMON_UP=0
+ warn "Docker Desktop is not running. Writing the wiring anyway — start Docker"
+ warn "Desktop (wait for the green whale), and the gateway connects on the next"
+ warn "Hermes session. Live verification is skipped for now."
+fi
+
+if [[ "$MCP_PROFILE" != "default" ]]; then
+ docker mcp profile show "$MCP_PROFILE" >/dev/null 2>&1 \
+ || soft_fail "MCP Toolkit profile '${MCP_PROFILE}' does not exist (see: docker mcp profile ls)."
+fi
+
+# Full docker path in `command`: Hermes may run under launchd (mac-mini) whose
+# minimal environment has no /usr/local or /opt/homebrew on PATH — a bare
+# `docker` would fail to spawn there. An absolute path works in both worlds.
+bold " server : ${SERVER_NAME}"
+bold " spawns : ${DOCKER_BIN} mcp gateway run --profile ${MCP_PROFILE}"
+
+step "Upserting ${SERVER_NAME} into ~/.hermes/config.yaml (via Hermes' own config API)"
+UPSERT_RESULT="$(cd "$HERMES_AGENT_DIR" && "$HERMES_PY" - "$SERVER_NAME" "$DOCKER_BIN" "$MCP_PROFILE" "$DRY_RUN" <<'PYEOF'
+import sys
+
+name, docker_bin, profile = sys.argv[1], sys.argv[2], sys.argv[3]
+dry_run = sys.argv[4] == "1"
+# Same key set `hermes mcp add` writes for a stdio server (env omitted when
+# empty). No tool-filter key exists at this level — a bare entry registers
+# every tool the gateway exposes, which is what we want: the gateway already
+# gates the server set.
+desired = {
+ "command": docker_bin,
+ "args": ["mcp", "gateway", "run", "--profile", profile],
+}
+
+# Underscore-private Hermes internals: a `hermes update` could rename these.
+# If this step starts failing after an update, check hermes_cli/mcp_config.py
+# for the current add/save entry points.
+from hermes_cli.mcp_config import _get_mcp_servers, _save_mcp_server
+
+existing = _get_mcp_servers().get(name)
+if existing == desired:
+ print("UNCHANGED — already wired exactly like this; nothing written.")
+elif dry_run:
+ action = "UPDATE (stale entry)" if existing else "INSERT"
+ print(f"DRY-RUN — would {action}: mcp_servers.{name} = {desired}")
+else:
+ # _save_mcp_server runs Hermes' own security validation, takes the config
+ # lock, and writes atomically — identical code path to `hermes mcp add`.
+ if not _save_mcp_server(name, desired):
+ sys.exit(3)
+ print(("UPDATED (was stale)" if existing else "INSERTED") + f" — mcp_servers.{name}")
+PYEOF
+)" || soft_fail "Hermes config upsert failed (rejected or errored — see output above)."
+bold " ${UPSERT_RESULT}"
+
+if [[ $DRY_RUN -eq 1 ]]; then
+ bold "✔ Dry run complete — nothing written."
+ exit 0
+fi
+
+# ── Verify gate ──────────────────────────────────────────────────────────────
+# Best-effort under --soft; loud otherwise. Never silence a broken connection
+# (no `hermes -z`): a failure here must be visible.
+VERIFY_OK=1
+
+step "Verifying: Hermes lists the server"
+if hermes mcp list 2>/dev/null | grep -q "$SERVER_NAME"; then
+ bold " hermes mcp list: ${SERVER_NAME} present"
+else
+ warn "hermes mcp list does not show ${SERVER_NAME}"
+ VERIFY_OK=0
+fi
+
+if [[ $DAEMON_UP -eq 1 ]]; then
+ step "Verifying: live gateway connect (hermes mcp test ${SERVER_NAME})"
+ # Capped: --soft protects against failure but not against hanging, and an
+ # unbounded stall here would freeze a piped `./install-profile ai`.
+ if run_with_timeout "${HERMES_TEST_TIMEOUT:-180}" hermes mcp test "$SERVER_NAME"; then
+ SERVER_COUNT="$(docker mcp profile show "$MCP_PROFILE" 2>/dev/null | grep -c '^ - type:' || true)"
+ bold " gateway OK — Toolkit profile '${MCP_PROFILE}' currently has ${SERVER_COUNT:-?} servers behind it"
+ else
+ RC=$?
+ if [[ $RC -eq 124 || $RC -eq 142 ]]; then
+ warn "hermes mcp test ${SERVER_NAME} timed out after ${HERMES_TEST_TIMEOUT:-180}s — entry written, gateway not confirmed."
+ else
+ warn "hermes mcp test ${SERVER_NAME} failed — the entry is written but the gateway did not connect."
+ fi
+ VERIFY_OK=0
+ fi
+
+ # OAuth-backed Toolkit servers are authorized per machine, by design. List
+ # any that still need a one-time interactive grant (the only manual gate).
+ UNAUTH="$(docker mcp oauth ls 2>/dev/null | awk -F'|' '/not authorized/ {gsub(/ /,"",$1); print $1}' || true)"
+ if [[ -n "$UNAUTH" ]]; then
+ step "Manual gate: OAuth servers not yet authorized on this machine"
+ while IFS= read -r srv; do
+ warn " docker mcp oauth authorize ${srv}"
+ done <<< "$UNAUTH"
+ warn "(Only needed if you use those servers' tools — everything else works now.)"
+ fi
+fi
+
+if [[ $VERIFY_OK -eq 1 && $DAEMON_UP -eq 1 ]]; then
+ bold "✔ Hermes is connected to the Docker MCP Toolkit (profile: ${MCP_PROFILE})."
+ bold " Toolkit changes flow through automatically; a running Hermes session"
+ bold " picks them up on reconnect (or: hermes mcp test ${SERVER_NAME})."
+ bold " Rollback: hermes mcp remove ${SERVER_NAME}"
+elif [[ $VERIFY_OK -eq 1 ]]; then
+ # Daemon down: the wiring is in place but nothing has actually connected yet —
+ # don't claim it has.
+ bold "✔ Wiring written (profile: ${MCP_PROFILE}) — live verification deferred."
+ bold " Start Docker Desktop, then confirm with: hermes mcp test ${SERVER_NAME}"
+ bold " Rollback: hermes mcp remove ${SERVER_NAME}"
+else
+ [[ $SOFT -eq 1 ]] && { warn "(--soft) verification incomplete — re-run this script once Docker/Hermes are healthy."; exit 0; }
+ soft_fail "verification failed — see warnings above."
+fi
diff --git a/onboarding_bin/install-claude-plugins.sh b/onboarding_bin/install-claude-plugins.sh
new file mode 100755
index 0000000..fa97960
--- /dev/null
+++ b/onboarding_bin/install-claude-plugins.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+# ─────────────────────────────────────────────────────────────────────────────
+# install-claude-plugins.sh — register the Retail Success plugin marketplace and
+# install the rs-agents plugin into Claude Code (non-interactive, idempotent).
+#
+# This is the SOURCE OF TRUTH channel for local machines: Claude Code auto-updates
+# rs-agents on startup (marketplace `autoUpdate`), and the Hermes skills-sync
+# mirrors from the resulting plugin cache. Run this BEFORE sync-rs-agents-to-hermes.sh.
+#
+# Note: the marketplace's declared name has been both `retailsuccess` (current
+# marketplace.json) and `wayroo` (older / already on some machines), so we try
+# both plugin identifiers. The Hermes sync glob is name-agnostic either way.
+# ─────────────────────────────────────────────────────────────────────────────
+set -uo pipefail
+
+if ! command -v claude >/dev/null 2>&1; then
+ echo "! claude CLI not installed — skipping rs-agents plugin install (install Claude Code first)."
+ exit 0
+fi
+
+# CLAUDECODE= clears the in-session guard so these run from a plain shell.
+echo "▶ Registering the Retail Success plugin marketplace (Retail-Success/Wayroo.tools)..."
+CLAUDECODE= claude plugin marketplace add Retail-Success/Wayroo.tools 2>&1 | tail -2 || true
+
+echo "▶ Installing/enabling the rs-agents plugin..."
+if CLAUDECODE= claude plugin install rs-agents@retailsuccess 2>/dev/null; then
+ echo " installed rs-agents@retailsuccess"
+elif CLAUDECODE= claude plugin install rs-agents@wayroo 2>/dev/null; then
+ echo " installed rs-agents@wayroo"
+else
+ echo "! Could not auto-install rs-agents — open Claude Code and run '/plugin' to add it,"
+ echo " or check the marketplace name with 'claude plugin marketplace list'."
+ exit 0
+fi
+
+echo "✔ rs-agents installed. Claude Code auto-updates it on startup (marketplace autoUpdate:true)."
+echo " → downstream: run mac-mini-agent/sync-rs-agents-to-hermes.sh to mirror skills into Hermes."
diff --git a/onboarding_bin/install-hermes-agent.sh b/onboarding_bin/install-hermes-agent.sh
new file mode 100755
index 0000000..8ed467e
--- /dev/null
+++ b/onboarding_bin/install-hermes-agent.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+
+# install-hermes-agent.sh — Install the Hermes Agent (Nous Research) AI coding
+# harness via its official first-party installer, then open its interactive
+# setup wizard ("the installer").
+#
+# Why not Homebrew: Hermes is a self-updating, self-learning agent. It installs
+# under ~/.hermes, links a `hermes` shim into ~/.local/bin, and upgrades itself
+# via `hermes update`. A Homebrew keg would fight that self-update (and shadow
+# the shim on PATH), so Hermes is installed from its own installer instead.
+# ~/.local/bin is placed on PATH by the dotfiles (shell/.zprofile, shell/.zshrc).
+#
+# Idempotent: if `hermes` is already installed, just (re)open the setup wizard
+# instead of reinstalling.
+
+if command -v hermes >/dev/null 2>&1; then
+ echo "Hermes Agent IS already installed: $(hermes --version 2>/dev/null | head -1)"
+ echo "Opening the Hermes setup wizard to (re)configure..."
+ hermes setup
+else
+ echo "Hermes Agent is NOT installed. Downloading and installing now..."
+ # The installer downloads Hermes to ~/.hermes, links ~/.local/bin/hermes, and
+ # auto-opens the interactive setup wizard at the end. The wizard reads from
+ # /dev/tty, so it still prompts even though the script is piped from curl.
+ # With no terminal available it skips the wizard and prints a reminder to
+ # "Run 'hermes setup' after install".
+ curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
+fi
+
+# ── Connect rs-agents into Hermes (via SKILLS, not `hermes plugins`) ──────────
+# Hermes cannot consume the rs-agents *Claude Code plugin* directly — `hermes
+# plugins` expects Python provider plugins, a different format. rs-agents reaches
+# Hermes through its SKILLS: mirror the released plugin's SKILL.md files into
+# ~/.hermes/skills/rs-agents so any selected local model can discover them
+# on-demand. Best-effort (--soft): if the source isn't present yet (plugin not
+# installed / repo not cloned), the Hermes install still succeeds — run the sync
+# later once the rs-agents plugin is available.
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+# 1) Ensure the rs-agents Claude Code plugin is installed — that's the SOURCE the
+# Hermes sync mirrors from (and Claude Code auto-updates it on startup).
+PLUGINS="${SCRIPT_DIR}/install-claude-plugins.sh"
+if [ -f "$PLUGINS" ]; then
+ echo ""
+ echo "Ensuring the rs-agents Claude Code plugin is installed..."
+ bash "$PLUGINS" || echo "rs-agents plugin install skipped (non-fatal)."
+fi
+
+# 2) Mirror the plugin's skills into Hermes (best-effort; soft-skips if no source).
+SYNC="${SCRIPT_DIR}/../mac-mini-agent/sync-rs-agents-to-hermes.sh"
+if [ -f "$SYNC" ]; then
+ echo ""
+ echo "Connecting rs-agents skills into Hermes (via skills sync)..."
+ bash "$SYNC" --soft || echo "rs-agents sync skipped (non-fatal)."
+fi
+
+# 3) Wire Hermes to the Docker Desktop MCP Toolkit gateway (best-effort;
+# soft-skips if Docker/the Toolkit isn't installed yet). One MCP_DOCKER
+# entry gives any local model the same tool plane Claude Code uses; the
+# Toolkit + Keychain stay the source of truth for servers and secrets.
+MCP_CONNECT="${SCRIPT_DIR}/connect-hermes-mcp.sh"
+if [ -f "$MCP_CONNECT" ]; then
+ echo ""
+ echo "Connecting Hermes to the Docker MCP Toolkit gateway..."
+ bash "$MCP_CONNECT" --soft || echo "MCP Toolkit wiring skipped (non-fatal)."
+fi
diff --git a/version-control/.git-template-directory/.gitignore b/version-control/.git-template-directory/.gitignore
index da04562..8463048 100755
--- a/version-control/.git-template-directory/.gitignore
+++ b/version-control/.git-template-directory/.gitignore
@@ -200,3 +200,5 @@ atlassian-ide-plugin.xml
connections.xml
server_instances.xml
ruby/.gem*
+
+**/.claude/settings.local.json