From 470e0ec2ddbbabe1b6274511f156729a76044747 Mon Sep 17 00:00:00 2001 From: andyb Date: Sat, 27 Jun 2026 11:22:48 -0500 Subject: [PATCH 01/14] bugfix(onboarding): harden .envrc adoption, prune broken VS Code extensions, quiet system_profiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the Mac Mini provisioning review (WR-18897): - onboard.sh: add envrc_is_sourceable() so a malformed/old-template .envrc is backed up and regenerated instead of silently adopted — the bug that skipped the prompt and spammed "command not found" on every new shell. Prompt logic extracted into generate_envrc(). - extensions_base.txt: remove GitHub.copilot-chat (now a built-in extension, install conflicts), stkb.rewrap (fails marketplace signature verification), and ms-vscode-remote.remote-wsl (Windows-only, no-op on macOS). - starship.zsh: redirect system_profiler stderr to /dev/null so its hw.cpufamily diagnostic stops leaking into every new shell. Co-Authored-By: Claude Opus 4.8 (1M context) --- engineering/ide/.vscode/extensions_base.txt | 3 -- onboarding_bin/onboard.sh | 58 +++++++++++++++++---- shell/starship.zsh | 7 ++- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/engineering/ide/.vscode/extensions_base.txt b/engineering/ide/.vscode/extensions_base.txt index 4e92985..29bd867 100644 --- a/engineering/ide/.vscode/extensions_base.txt +++ b/engineering/ide/.vscode/extensions_base.txt @@ -9,7 +9,6 @@ esbenp.prettier-vscode foxundermoon.shell-format GitHub.codespaces GitHub.copilot -GitHub.copilot-chat github.vscode-github-actions GitHub.vscode-pull-request-github HTMLHint.vscode-htmlhint @@ -24,7 +23,6 @@ ms-playwright.playwright ms-vscode-remote.remote-containers ms-vscode-remote.remote-ssh ms-vscode-remote.remote-ssh-edit -ms-vscode-remote.remote-wsl ms-vscode-remote.vscode-remote-extensionpack ms-vscode.remote-explorer ms-vscode.remote-server @@ -36,7 +34,6 @@ richie5um2.vscode-sort-json ritwickdey.LiveServer ryu1kn.partial-diff steoates.autoimport -stkb.rewrap streetsidesoftware.code-spell-checker stylelint.vscode-stylelint timonwong.shellcheck diff --git a/onboarding_bin/onboard.sh b/onboarding_bin/onboard.sh index e8fc9b8..096a641 100755 --- a/onboarding_bin/onboard.sh +++ b/onboarding_bin/onboard.sh @@ -196,17 +196,23 @@ resolve_repo() { # ---------------------------------------------------------------------------- # Step 6 — Machine variables (.envrc). GATE only on first run (fill values). # ---------------------------------------------------------------------------- -ensure_envrc() { - step "Machine variables (.envrc)" - local target="$REPO_DIR/.envrc" +# True when $1 sources cleanly. A malformed old-template file (e.g. a line like +# `VAR=# Your key here` — `VAR=#` then `Your` is parsed as a command) emits +# "command not found" to stderr; empty stderr means the file is safe to adopt. +# Sourced in a throwaway subshell so it can't pollute our environment. +envrc_is_sourceable() { + [[ -f "$1" ]] || return 1 + local errout + errout="$(bash -c "source '$1'" 2>&1 >/dev/null)" || true + [[ -z "$errout" ]] +} + +# Prompt for machine variables and write a clean .envrc to $1. +generate_envrc() { + local target="$1" local pre="$REPO_DIR/onboarding_bin/pre-onboarding-script.sh" - if [[ -f "$target" ]]; then - ok ".envrc already present in repo" - elif [[ -f "$HOME/.envrc" ]]; then - mv "$HOME/.envrc" "$target" - ok "Moved ~/.envrc into the repo" - elif [[ -f "$pre" ]]; then + if [[ -f "$pre" ]]; then # Delegate to pre-onboarding-script.sh: it prompts for each value and writes # a clean, `export`-style ~/.envrc (no malformed placeholders). ONBOARD_ORCHESTRATED # tells it to skip its brew/clone tail (onboard.sh already did both). We feed @@ -217,11 +223,11 @@ ensure_envrc() { else ONBOARD_ORCHESTRATED=1 bash "$pre" || warn "pre-onboarding-script.sh reported errors (review above)" fi - if [[ -f "$HOME/.envrc" ]]; then + if [[ -f "$HOME/.envrc" ]] && envrc_is_sourceable "$HOME/.envrc"; then mv "$HOME/.envrc" "$target" ok ".envrc generated and saved into the repo" else - warn "~/.envrc was not created — falling back to the template" + warn "~/.envrc was not created cleanly — falling back to the template" cp "$REPO_DIR/.envrc.example" "$target" "${EDITOR:-open}" "$target" >/dev/null 2>&1 || open "$target" 2>/dev/null || true ask _ " Press Enter once you've saved your values in .envrc... " @@ -233,6 +239,36 @@ ensure_envrc() { ask _ " Press Enter once you've saved your values in .envrc... " ok ".envrc ready" fi +} + +ensure_envrc() { + step "Machine variables (.envrc)" + local target="$REPO_DIR/.envrc" + + # Only adopt an existing .envrc if it actually sources cleanly. Otherwise an + # old-template copy (malformed `VAR=# ...` lines, placeholder values) gets + # silently adopted, the prompt is skipped, and every new shell spews + # "command not found" while aliases resolve to bogus placeholder paths. + if [[ -f "$target" ]]; then + if envrc_is_sourceable "$target"; then + ok ".envrc already present in repo" + else + warn "Repo .envrc is malformed (old template?) — backing up to .envrc.malformed.bak and regenerating" + mv "$target" "$target.malformed.bak" + generate_envrc "$target" + fi + elif [[ -f "$HOME/.envrc" ]]; then + if envrc_is_sourceable "$HOME/.envrc"; then + mv "$HOME/.envrc" "$target" + ok "Moved ~/.envrc into the repo" + else + warn "Existing ~/.envrc is malformed (old template?) — backing up to ~/.envrc.malformed.bak and regenerating" + mv "$HOME/.envrc" "$HOME/.envrc.malformed.bak" + generate_envrc "$target" + fi + else + generate_envrc "$target" + fi # Load values into THIS session so later steps (company path, keys) see them. set -a; # shellcheck disable=SC1090 diff --git a/shell/starship.zsh b/shell/starship.zsh index 912e252..db62373 100644 --- a/shell/starship.zsh +++ b/shell/starship.zsh @@ -6,8 +6,11 @@ if [[ -f $LFILE ]]; then elif [[ -f $MFILE ]]; then _distro="macos" - # on mac os use the systemprofiler to determine the current model - _device=$(system_profiler SPHardwareDataType | awk '/Model Name/ {print $3,$4,$5,$6,$7}') + # on mac os use the systemprofiler to determine the current model. + # 2>/dev/null: on some Macs system_profiler prints a diagnostic line + # (e.g. "hw.cpufamily: 0x...") to stderr that would otherwise leak into + # every new shell. We only want stdout ("Model Name") for the icon match. + _device=$(system_profiler SPHardwareDataType 2>/dev/null | awk '/Model Name/ {print $3,$4,$5,$6,$7}') case $_device in *MacBook*) DEVICE="" ;; From 1a1555bb5089cf88ccfd84f9d49b0cafc9b2ed68 Mon Sep 17 00:00:00 2001 From: andyb Date: Sat, 27 Jun 2026 13:08:09 -0500 Subject: [PATCH 02/14] bugfix(onboarding): collect machine vars before clone to fix CURRENT_COMPANY path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolve_repo used CURRENT_COMPANY from the calling shell's environment to decide the clone path, then ensure_envrc prompted the user for the real value — too late. If the calling shell had a stale CURRENT_COMPANY (e.g. from a prior job), the repo landed in the wrong directory and every alias derived from it resolved to a corrupt path. Fix: introduce collect_machine_vars, which runs before resolve_repo. It sources any existing valid .envrc (fast no-op on re-runs) or prompts the user and writes ~/.envrc, then sources it so CURRENT_COMPANY is correct before any path decision is made. ensure_envrc (post-clone) then moves ~/.envrc into $REPO_DIR as before. Additional hardening: - CURRENT_COMPANY prompt default is hardcoded to "Retail-Success" (the correct value for this repo), so a stale env var can never silently win. - IDE_PATH defaults to "code" (VS Code CLI); stale "vscode" values are normalised at both init and post-prompt time. - generate_envrc checks ~/.envrc first to prevent double-prompting on the malformed-repo-.envrc edge case. - Generated .envrc now covers the full variable set from .envrc.example (adds JIRA_API_TOKEN placeholder and GIT_EMAIL_ADDRESS_PERSONAL). Co-Authored-By: Claude Sonnet 4.6 --- onboarding_bin/onboard.sh | 98 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/onboarding_bin/onboard.sh b/onboarding_bin/onboard.sh index 096a641..984cb7a 100755 --- a/onboarding_bin/onboard.sh +++ b/onboarding_bin/onboard.sh @@ -56,6 +56,96 @@ ask() { # ask [default] STACK="${1:-}" MANUAL_ACTIONS=() +# ---------------------------------------------------------------------------- +# Step 5a — Collect machine variables BEFORE resolve_repo so CURRENT_COMPANY +# is set correctly before the clone-path decision. On re-runs this +# is a no-op (sources the existing .envrc and returns immediately). +# Writes to ~/.envrc; ensure_envrc (post-clone) moves it into +# $REPO_DIR and sources it as the authoritative copy. +# ---------------------------------------------------------------------------- + +# Global scratch vars (NOT local) so ask()'s printf -v assignment propagates back. +_CMV_EMAIL="" _CMV_NAME="" _CMV_USER="" _CMV_GITHUB="" _CMV_COMPANY="" +_CMV_IDE="" _CMV_JIRA_URL="" _CMV_GPG="" _CMV_JIRA_EMAIL="" + +collect_machine_vars() { + # Running from inside the repo already? Source the repo's .envrc if valid. + local here; here="$(cd "$(dirname "${BASH_SOURCE[0]:-}")/.." 2>/dev/null && pwd)" + if [[ -n "$here" && -f "$here/install-profile" && -d "$here/meta" ]]; then + if [[ -f "$here/.envrc" ]] && envrc_is_sourceable "$here/.envrc"; then + set -a; . "$here/.envrc"; set +a + ok "Machine variables loaded from repo .envrc (re-run)" + return + fi + fi + + # ~/.envrc written by a prior bootstrap run? Adopt it. + if [[ -f "$HOME/.envrc" ]] && envrc_is_sourceable "$HOME/.envrc"; then + set -a; . "$HOME/.envrc"; set +a + ok "Machine variables loaded from existing ~/.envrc" + return + fi + + # First run: prompt, write ~/.envrc, source it so CURRENT_COMPANY is available + # to resolve_repo immediately below. + log "Collecting machine variables (needed before cloning the dotfiles repo)..." + + _CMV_EMAIL="${GIT_EMAIL_ADDRESS_PROFESSIONAL:-}" + _CMV_NAME="${CURRENT_NAME:-}" + _CMV_USER="${CURRENT_USER:-$USER}" + _CMV_GITHUB="${CURRENT_USER_GITHUB_URL:-https://github.com/$USER}" + # Hardcode the company default: ignoring any stale CURRENT_COMPANY in the calling + # shell is the whole point of this fix — "Retail-Success" is always the correct + # answer for this repo's onboarding. + _CMV_COMPANY="Retail-Success" + # Default to "code" (VS Code CLI); normalise any stale "vscode" value. + _CMV_IDE="${IDE_PATH:-code}" + [[ "$_CMV_IDE" == "vscode" ]] && _CMV_IDE="code" + _CMV_JIRA_URL="${JIRA_BASE_URL:-}" + _CMV_GPG="${CURRENT_USER_GPG_KEY:-}" + _CMV_JIRA_EMAIL="${JIRA_USER_EMAIL:-}" + + ask _CMV_EMAIL "Enter your git email [$_CMV_EMAIL]: " "$_CMV_EMAIL" + ask _CMV_NAME "Enter your name [$_CMV_NAME]: " "$_CMV_NAME" + ask _CMV_USER "Enter your username [$_CMV_USER]: " "$_CMV_USER" + ask _CMV_GITHUB "Enter your GitHub URL [$_CMV_GITHUB]: " "$_CMV_GITHUB" + ask _CMV_COMPANY "Enter your company name [$_CMV_COMPANY]: " "$_CMV_COMPANY" + ask _CMV_IDE "Enter your IDE command (e.g. 'code') [$_CMV_IDE]: " "$_CMV_IDE" + ask _CMV_JIRA_URL "Enter your JIRA Base URL [$_CMV_JIRA_URL]: " "$_CMV_JIRA_URL" + ask _CMV_GPG "Enter your GPG key id (blank = none) [$_CMV_GPG]: " "$_CMV_GPG" + ask _CMV_JIRA_EMAIL "Enter your JIRA user email [$_CMV_JIRA_EMAIL]: " "$_CMV_JIRA_EMAIL" + + # Normalise IDE_PATH in case the user typed "vscode". + [[ "$_CMV_IDE" == "vscode" ]] && _CMV_IDE="code" + + # Write ~/.envrc with the full variable set from .envrc.example (JIRA_API_TOKEN + # and GIT_EMAIL_ADDRESS_PERSONAL are intentionally left empty — set them manually + # after onboarding completes, or let the dotbot security step handle them). + cat > "$HOME/.envrc" < Date: Sun, 28 Jun 2026 11:35:34 -0500 Subject: [PATCH 03/14] chore(mac-mini): add Docker MCP Toolkit + Claude Code 24/7 bootstrap script Stands up the Docker MCP gateway (stdio, no url/auth) and wires Claude Code to it, replicating the default profile's enabled servers. Includes interactive secret setup and a reboot-survival checklist (auto-login, Docker autostart, sleep prevention, keychain). Co-Authored-By: Claude Opus 4.8 (1M context) --- mac-mini-agent/mac-mini-mcp-bootstrap.sh | 121 +++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100755 mac-mini-agent/mac-mini-mcp-bootstrap.sh diff --git a/mac-mini-agent/mac-mini-mcp-bootstrap.sh b/mac-mini-agent/mac-mini-mcp-bootstrap.sh new file mode 100755 index 0000000..f2fe1f0 --- /dev/null +++ b/mac-mini-agent/mac-mini-mcp-bootstrap.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# mac-mini-mcp-bootstrap.sh +# Stand up Docker MCP Toolkit + Claude Code on a 24/7 Mac mini. +# +# Replicates the "default" profile from andyb's primary machine. +# Run interactively the first time: bash mac-mini-mcp-bootstrap.sh +# +# Docker MCP Toolkit connects over LOCAL STDIO (`docker mcp gateway run`). +# There is NO url and NO auth token to configure — ignore any yaml template +# that asks for `url:` / `auth:`. That format is for remote HTTP/SSE servers. +# ───────────────────────────────────────────────────────────────────────────── +set -euo pipefail + +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' "$*"; } + +# ── 0. Prereqs ─────────────────────────────────────────────────────────────── +step "0. Checking prerequisites" +command -v docker >/dev/null || { warn "Docker Desktop not installed. Install it first: https://docs.docker.com/desktop/setup/install/mac-install/"; exit 1; } +command -v claude >/dev/null || { warn "Claude Code CLI not on PATH. Install: npm i -g @anthropic-ai/claude-code (or the official installer)"; exit 1; } +docker mcp --version >/dev/null 2>&1 || { warn "'docker mcp' unavailable — enable MCP Toolkit in Docker Desktop > Settings > Beta features."; exit 1; } +bold " docker: $(docker --version)" +bold " claude: $(claude --version 2>/dev/null || echo present)" + +# ── 1. Make sure Docker is actually running ────────────────────────────────── +step "1. Verifying Docker daemon is up" +if ! docker info >/dev/null 2>&1; then + warn "Docker isn't running. Start Docker Desktop, wait for it to go green, then re-run." + open -a Docker || true + exit 1 +fi +bold " Docker daemon reachable ✔" + +# ── 2. Enable the servers (default profile) ────────────────────────────────── +# Mirrors the primary machine. Edit this list to taste. +step "2. Enabling MCP servers on the 'default' profile" +DEFAULT_SERVERS=( + atlassian + awslabs-cloudwatch + awslabs-cloudwatch-appsignals + context7 + fetch + filesystem + gemini-api-docs + git + github-chat + github-official + markdownify + memory + npm-sentinel + playwright + playwright-mcp-server + sentry-remote + sequentialthinking + youtube_transcript +) +docker mcp profile server add default "${DEFAULT_SERVERS[@]}" +bold " Enabled: ${DEFAULT_SERVERS[*]}" +echo +docker mcp profile server ls + +# ── 3. Secrets — these do NOT transfer from the other Mac ──────────────────── +# Secrets live in the macOS Keychain on each machine. Set them here once. +# Re-run any line as needed; value is read from stdin so it won't hit shell history. +step "3. Secrets (set the ones you use; skip with Ctrl-D)" +warn "These are stored in this Mac mini's keychain. They were NOT copied from your laptop." +set_secret() { + local key="$1" label="$2" + read -r -s -p " ${label} (Enter to skip): " val; echo + if [[ -n "${val}" ]]; then + printf '%s' "${val}" | docker mcp secret set "${key}" + bold " set ${key}" + else + echo " skipped ${key}" + fi +} +set_secret github.personal_access_token "GitHub PAT" +set_secret github-chat.api_key "github-chat API key" +set_secret atlassian.jira.api_token "Jira API token" +set_secret atlassian.jira.personal_token "Jira personal token" +set_secret atlassian.confluence.api_token "Confluence API token" +set_secret atlassian.confluence.personal_token "Confluence personal token" +echo +warn "OAuth-based servers (atlassian-remote, sentry-remote, github official OAuth) must be" +warn "re-authorized interactively on THIS machine: docker mcp oauth authorize " + +# ── 4. Connect Claude Code to the gateway (the easy way) ───────────────────── +step "4. Wiring Claude Code to the Docker MCP gateway" +docker mcp client connect claude-code +# Equivalent manual form (stdio, no url/auth): +# claude mcp add MCP_DOCKER -- docker mcp gateway run +bold " Connected. Verify with: claude mcp list" + +# ── 5. Make it survive reboots (mostly manual / GUI) ───────────────────────── +step "5. 24/7 survival checklist (do these once, by hand)" +cat <<'EOF' + Docker Desktop must be running for the gateway to work, and that needs a + logged-in GUI session. SSH alone is not enough. Configure: + + a) AUTO-LOGIN after reboot: + System Settings > Users & Groups > Automatic login > + (Disable FileVault, or auto-login won't run until you type the disk password.) + + b) DOCKER DESKTOP autostart: + Docker Desktop > Settings > General > + [x] Start Docker Desktop when you sign in + [x] (optional) Open Docker Dashboard at startup -> leave OFF for headless + + c) PREVENT SLEEP (so the box stays reachable 24/7): + sudo pmset -a sleep 0 disablesleep 1 womp 1 + sudo systemsetup -setcomputersleep Never + + d) KEYCHAIN: auto-login keeps the login keychain unlocked, which is what lets + `docker mcp secret` read your tokens unattended. If you lock the screen + manually the keychain stays unlocked; a full logout will re-lock it. +EOF + +step "Done." +bold "Sanity check: claude mcp list (look for: MCP_DOCKER: docker mcp gateway run - Connected)" From 8014d1bc14cece175fc2989bc021cfa5797a58cf Mon Sep 17 00:00:00 2001 From: andyb Date: Sun, 28 Jun 2026 11:41:45 -0500 Subject: [PATCH 04/14] maintenance(Brewfile): [WR-18897] Add 'ollama' AI CLI tool for enhanced development support. --- Brewfile.base | 1 + 1 file changed, 1 insertion(+) diff --git a/Brewfile.base b/Brewfile.base index 0397cc4..52e7c13 100755 --- a/Brewfile.base +++ b/Brewfile.base @@ -16,6 +16,7 @@ brew 'icu4c' # REQUIRED - BASE/ROR/NODE/DEVOPS/PYTHON Compiled Library D brew 'gnupg' # REQUIRED - Needed for signing your commits with github. brew 'mongosh' # RECOMMENDED - Commonly used with nosql projects. brew 'nvm' # REQUIRED - BASE/ROR/NODE/DEVOPS/PYTHON Version manager for node +brew 'ollama' # RECOMMENDED - AI Ollama CLI tool brew 'openssl' # REQUIRED - ROR/NODE/DEVOPS/PYTHON/PG Network Utility for SSL requests brew 'pandoc' # OPTIONAL - ROR/NODE/DEVOPS/PYTHON/PG Allows for markdown to easily exported to word, pdfs, & html brew 'pipx' # REQUIRED - BASE/ROR/NODE/DEVOPS/PYTHON Recommended package manager for python From 4ad3a20c96ef5b87db4cdc39aea8c81e9cb49554 Mon Sep 17 00:00:00 2001 From: andyb Date: Sun, 28 Jun 2026 11:41:49 -0500 Subject: [PATCH 05/14] maintenance(dotfiles): [WR-18897] Ensure ~/.local/bin is on PATH for Hermes Agent support. --- shell/.zprofile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shell/.zprofile b/shell/.zprofile index 7365d50..96ebef1 100644 --- a/shell/.zprofile +++ b/shell/.zprofile @@ -21,3 +21,6 @@ fi # Created by `pipx` on 2024-09-29 16:51:26 # export PATH="$PATH:/Users/acbarrows/.local/bin" + +# Hermes Agent — ensure ~/.local/bin is on PATH +export PATH="$HOME/.local/bin:$PATH" From 1f94fc58a5e6e434b25494ea78176a9eb22e996c Mon Sep 17 00:00:00 2001 From: andyb Date: Sun, 28 Jun 2026 14:26:45 -0500 Subject: [PATCH 06/14] maintenance(onboarding): [WR-18897] Enhanced onboarding scripts and documentation for Mac Mini setup. --- .claude/settings.local.json | 14 +- Brewfile.base | 1 - HANDOFF.md => ai/plans/HANDOFF.md | 0 .../plans/ONBOARDING-PLAN.md | 0 ai/workflows/mac-mini-agent/DRY-RUN-PLAN.md | 236 ++++++++++++++++++ .../mac-mini-agent}/mac-mini-mcp-bootstrap.sh | 0 6 files changed, 249 insertions(+), 2 deletions(-) rename HANDOFF.md => ai/plans/HANDOFF.md (100%) rename ONBOARDING-PLAN.md => ai/plans/ONBOARDING-PLAN.md (100%) create mode 100644 ai/workflows/mac-mini-agent/DRY-RUN-PLAN.md rename {mac-mini-agent => ai/workflows/mac-mini-agent}/mac-mini-mcp-bootstrap.sh (100%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9c92205..ef5a6de 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -12,7 +12,19 @@ "Bash(set -e)", "mcp__claude_ai_Atlassian__getAccessibleAtlassianResources", "mcp__claude_ai_Atlassian__getJiraProjectIssueTypesMetadata", - "mcp__claude_ai_Atlassian__createJiraIssue" + "mcp__claude_ai_Atlassian__createJiraIssue", + "mcp__claude_ai_Atlassian__getVisibleJiraProjects", + "mcp__claude_ai_Atlassian__searchJiraIssuesUsingJql", + "Bash(awk '/^main\\\\\\(\\\\\\)|^# ===.*Main|^run\\\\\\(\\\\\\)|^# Orchestrat/{f=1} f' onboarding_bin/onboard.sh)", + "Bash(bash -n /Users/andyb/Retail-Success/repos/development-team/dotfiles/onboarding_bin/onboard.sh)", + "Bash(find \"/Users/andyb/Library/Application Support/Claude/local-agent-mode-sessions/skills-plugin\" -name \"SKILL.md\" 2>/dev/null | grep -E \"local-development|developer-onboard\" | head -10)", + "Bash(find \"/Users/andyb/Library/Application Support/Claude\" -name \"SKILL.md\" 2>/dev/null | xargs grep -l -E \"local.development|developer.onboard|onboard\" 2>/dev/null | head -20)", + "Bash(find ~/.claude/plugins/cache -name \"SKILL.md\" 2>/dev/null | xargs grep -l -i \"local.develop\\\\|developer.onboard\\\\|onboard\\\\|machine.setup\" 2>/dev/null)", + "Bash(brew info *)", + "Bash(brew tap *)", + "Bash(/opt/homebrew/bin/hermes --version)", + "Read(//Users/andyb/.local/bin/**)", + "Read(//Users/andyb/.hermes/**)" ] } } diff --git a/Brewfile.base b/Brewfile.base index 52e7c13..925a830 100755 --- a/Brewfile.base +++ b/Brewfile.base @@ -11,7 +11,6 @@ brew 'freetype' # REQUIRED - BASE/ROR/NODE CL brew 'gh' # REQUIRED - BASE/ROR/NODE/DEVOPS Github CLI brew 'git' # REQUIRED - ROR/NODE/DEVOPS/PYTHON Core version control brew 'hadolint' # REQUIRED - BASE/ROR/NODE/DEVOPS/PYTHON Used to lint Dockerfiles../ -brew 'hermes-agent' # REQUIRED - Coding harness and agent that self learns. brew 'icu4c' # REQUIRED - BASE/ROR/NODE/DEVOPS/PYTHON Compiled Library Dependency for unicode and globalization brew 'gnupg' # REQUIRED - Needed for signing your commits with github. brew 'mongosh' # RECOMMENDED - Commonly used with nosql projects. diff --git a/HANDOFF.md b/ai/plans/HANDOFF.md similarity index 100% rename from HANDOFF.md rename to ai/plans/HANDOFF.md diff --git a/ONBOARDING-PLAN.md b/ai/plans/ONBOARDING-PLAN.md similarity index 100% rename from ONBOARDING-PLAN.md rename to ai/plans/ONBOARDING-PLAN.md diff --git a/ai/workflows/mac-mini-agent/DRY-RUN-PLAN.md b/ai/workflows/mac-mini-agent/DRY-RUN-PLAN.md new file mode 100644 index 0000000..f23ed1f --- /dev/null +++ b/ai/workflows/mac-mini-agent/DRY-RUN-PLAN.md @@ -0,0 +1,236 @@ +# Local‑AI Agent Host — Dry‑Run Plan + +**Author:** Claude (Opus 4.8) for @andyb · **Date:** 2026‑06‑28 +**Dry‑run target:** `rss-mbp-5` (your MacBook — daily driver) · **Promotion target:** the mac‑mini (24/7 box) +**Status:** PLAN — nothing executed yet. Uncommitted. + +> **Thesis.** Across your 06‑19 → 06‑28 sessions you've been building toward one thing from +> several angles: **a mac‑mini that runs your full AI tooling as an always‑on agent.** The pieces +> already exist but were assembled in separate conversations and never wired together: +> the dotfiles `onboard.sh` *installs* everything, `mac-mini-mcp-bootstrap.sh` *wires the tools (MCP)*, +> the `wayroo-demo` work *proved the always‑on pattern (init‑check → launchd → health probe)*, and +> Ollama + Hermes are *installed but unconfigured*. **"Next level" = unify those fragments into one +> coherent, reversible flow.** **"Dry run" = rehearse that whole flow ephemerally on `rss-mbp-5` +> (stand up → validate → tear down) before it touches the mini or production `onboard.sh`.** + +--- + +## 1. Objectives — recovered from repeated mentions (with evidence) + +Each objective below is something you said **more than once, across more than one session**. Frequency +and a representative quote are cited so you can audit the recovery. Where the transcripts left a gap, +the choice is explicitly marked **[my recommendation, not your stated requirement]**. + +| # | Objective | Evidence (sessions) | +|---|-----------|---------------------| +| **O1** | **Provision the mac‑mini as a 24/7 always‑on agent box** running your full AI tooling + plugins. *(most‑repeated)* | `"We have a macmini that we are provisioning to be an always on bot that can leverage our full AI tooling, integrations, and those plugins."` — 06‑23 (repeated); 06‑28 `"the mac-mini that will be used for 24/7 agent use"` | +| **O2** | **A clean, idempotent, reversible one‑liner dotfiles onboarding** (`onboard.sh`). Hardened every session: `.env` prompts must run **before** dir creation; alias/`.envrc` pathing; VS Code symlinks; brew recipes. | 06‑19, 06‑22, 06‑26, 06‑28 (branch `feature/WR-18897-onboarding-dotfiles-hardening`) | +| **O3** | **Add Ollama + local models** to the stack. | 06‑28 `/compact`: `"add in using ollama and being able to use local models"` — installed via `Brewfile.base` line 19, never configured | +| **O4** | **Hermes Agent** as the self‑improving coding agent, routing between **local (Ollama)** and **cloud** models. | `Brewfile.base` line 14 `hermes-agent` "Coding harness and agent that self learns"; 06‑27 commit `4b88178`; raised on the mini 06‑28 | +| **O5** | **rs‑agents plugin as the brain** of the box — `init-check` gate → launchd always‑on → health/registry probe. | 06‑23 `wayroo-demo`: registry, `init-check`, launchd units (`RunAtLoad`/`KeepAlive`), health probe on :8765 | +| **O6** | **Fold learnings back into rs‑agents** skills/knowledge (`local-development-setup`, `developer-onboarding`) + maybe a new skill/workflow. | 06‑28 (verbatim): `"Use the /rs-agents:rs-agents and the /skill-creator to update /local-development-setup and/or the /developer-onboarding skill. From our learnings today, extract any wisdom we've gained..."` | +| **O7** | **Safety throughout** — idempotent, re‑runnable, reversible, dry‑run‑able. | `onboard.sh` "Re‑running is safe"; `ONBOARDING-PLAN.md` "Treat the Mini as the test bed" | + +**Resolved open question:** "hermes" — unclarified in the 06‑28 Luci session — is **Hermes Agent by Nous +Research** (`hermes-agent.nousresearch.com`): *"a self‑improving AI agent that creates skills from +experience."* Its CLI (`model` / `moa` / `fallback` / `proxy` / `mcp` / `skills`) is what makes O3+O4 one +architecture, not two. + +--- + +## 2. How the pieces fit (the architecture you're actually building) + +``` + ┌─────────────────────────────────────────────┐ + │ rs-agents plugin (the "team"/brain) │ + │ orchestrator → specialist sub-agents │ + │ gate: init-check · always-on: launchd │ + └───────────────┬─────────────────────────────┘ + │ uses + ┌────────────────────────────┼────────────────────────────┐ + │ MODEL/AGENT RUNTIME PLANE │ TOOL PLANE │ + ▼ ▼ ▼ + ┌──────────────┐ ┌────────────────────┐ ┌────────────────────┐ + │ Ollama │◀──/v1──────│ Hermes Agent │ │ Docker MCP Toolkit │ + │ local models │ OpenAI- │ routes local+cloud │ │ 17 MCP servers │ + │ :11434 │ compat │ fallback · moa │ │ gateway (stdio) │ + └──────────────┘ └────────────────────┘ └────────────────────┘ + ▲ ▲ ▲ + └─────────────── installed by ── dotfiles onboard.sh / Brewfile ──┘ +``` + +- **Ollama** = local model **server** (OpenAI‑compatible at `:11434/v1`). Already installed. +- **Hermes** = agent **runtime** that points at Ollama for local inference, with a **fallback chain** to + cloud when local can't cope, and self‑improving **skills**. Already installed (note: brew `hermes` is + PATH‑shadowed by `~/.local/bin/hermes` — the pip build wins; that's the featureful one). +- **Docker MCP Toolkit** = the **tool plane** — already wired by `mac-mini-mcp-bootstrap.sh`. +- **rs‑agents** = the Claude‑Code **agent team**; `init-check` + launchd + health probe = the always‑on + pattern from `wayroo-demo`. +- **dotfiles `onboard.sh`** = installs all of the above on a fresh Mac. + +The **only missing wire** is the model/agent runtime plane (Ollama ↔ Hermes) and its always‑on form. +This plan stands that wire up — safely, on the laptop, first. + +--- + +## 3. THE DRY RUN on `rss-mbp-5` (ephemeral · reversible · the core deliverable) + +**Golden rule (daily‑driver safety):** the dry run installs **nothing persistent** — **no launchd units, +no `brew services`, no auto‑login, no sleep changes**. Everything runs in the foreground or in a sandbox +dir and is torn down by one command block. The persistence belongs to the mini (§6). + +Run inside a sandbox so nothing leaks into your real config: + +```bash +export DRY=~/.local/state/local-ai-dryrun # sandbox for logs/config +mkdir -p "$DRY" && cd "$DRY" +``` + +Each step is **COMMAND → OBSERVABLE PASS CRITERION**. If a step's criterion isn't visibly met, stop. + +### Step 0 — Preflight snapshot *(read‑only)* +```bash +for t in ollama hermes docker brew; do printf "%-8s %s\n" "$t" "$(command -v $t)"; done +ollama --version; hermes --version; docker --version +sysctl -n hw.memsize | awk '{print "RAM:", $1/1073741824 "GB"}' +``` +**PASS:** all four resolve; you note your RAM (drives model choice below). + +### Step 1 — Bring up Ollama ephemerally + pull one small model +```bash +ollama serve >"$DRY/ollama.log" 2>&1 & echo $! > "$DRY/ollama.pid" # foreground daemon, captured PID +sleep 2 +ollama pull llama3.2:3b # ~2GB smoke model — fast, proves the path +curl -s http://localhost:11434/api/tags | jq '.models[].name' +curl -s http://localhost:11434/api/generate -d '{"model":"llama3.2:3b","prompt":"reply with the single word: ready","stream":false}' | jq -r .response +``` +**PASS:** `/api/tags` lists the model **and** `/api/generate` returns text (≈ "ready"). +**Model choice [my recommendation, not your stated requirement — no model was named in any transcript]:** +`llama3.2:3b` for the smoke test (small/fast); `qwen2.5-coder:7b` (~4.7GB) as the real coding model once +the path works. Rule of thumb: keep the model ≤ ~60% of RAM. + +### Step 2 — Confirm Hermes routes to the LOCAL model ⚠️ *(the step to verify, not assume)* +Hermes consumes OpenAI‑compatible `/v1` providers; Ollama serves `http://localhost:11434/v1`. Confirm +Hermes can be pointed there (via `hermes model` picker with a custom `--inference-url`, a config entry, or +`-m … --provider …`). Then run one prompt and confirm it hit the **local** server: +```bash +: > "$DRY/ollama.log" # clear log so the next hit is unambiguous +hermes -z "Say hello in 5 words." -m llama3.2:3b --provider # exact flag TBD in spike +grep -i "POST /v1/chat/completions\|/api/chat\|llama3.2" "$DRY/ollama.log" # proof the local model served it +``` +**PASS:** Hermes returns a completion **and** `ollama.log` shows the request — i.e. inference was local. +**FALLBACK if Hermes won't point at Ollama directly:** use **LM Studio** (also in your Brewfile, serves +OpenAI‑compat) or call Ollama's `/v1` endpoint directly; either still satisfies O3. Record what worked — +that becomes the documented recipe (§5). + +### Step 3 — MCP tool plane sanity *(lightweight — skip secrets in the dry run)* +```bash +docker mcp --version && docker mcp server ls 2>/dev/null | head +``` +**PASS:** `docker mcp` responds and lists servers. (Full secrets/keychain wiring is a mini concern, §6 — +don't load real PATs on the laptop for a dry run.) + +### Step 4 — rs‑agents gate: does the plugin load and register? +Reuse the **`init-check`** you built in `wayroo-demo` (the "FIRST GATE — near‑free, no gateway"). Intent: +confirm the rs‑agents plugin loads and the agents/skills register. +```bash +# from wherever your init-check lives (wayroo-demo); confirm path during the spike +npm run init-check # expect: AGENTS non-empty (~25 agents, ~59 skills) +``` +**PASS:** registry reports the agent/skill roster non‑empty. If empty → the plugin didn't load; fix +`RS_AGENTS_PATH` before going further. + +### Step 5 — Health/registry probe (foreground, NOT launchd) +Run the demo server/health probe in the foreground and hit it once: +```bash +# foreground only for the dry run — no launchd, no KeepAlive +curl -s localhost:8765/health | jq . # adjust to your wayroo-demo probe +``` +**PASS:** health returns `healthy` with `maxConcurrency`/`waiting` populated. + +### Step 6 — TEARDOWN (single block — leaves the laptop exactly as found) +```bash +kill "$(cat "$DRY/ollama.pid")" 2>/dev/null # stop the ephemeral Ollama daemon +ollama rm llama3.2:3b 2>/dev/null # optional: reclaim ~2GB +# stop any foreground demo server (Ctrl-C in its terminal) +rm -rf "$DRY" # remove sandbox logs/config +# NOTE: nothing was added to launchd / brew services / login items — nothing to undo there. +``` +**PASS:** `pgrep ollama` empty, sandbox gone, no launchd/login changes exist. + +**Dry‑run definition of done:** Steps 1, 2, 4, 5 each hit their PASS criterion, and Step 6 returns the +machine to its prior state. That's a complete, observable rehearsal of the always‑on stack — minus the +"always‑on." + +--- + +## 4. Run the dry run *through* the rs‑agents plugin (O5/O6) + +You asked to **leverage the company rs‑agents plugin** — so don't run §3 by hand, drive it through the +orchestrator (and let it use the right specialists): + +``` +/rs-agents → "Execute the Local-AI Agent Host dry run in mac-mini-agent/DRY-RUN-PLAN.md §3 on this + MacBook, ephemerally. devops-engineer owns the bootstrap/teardown; verify each step's + observable PASS criterion before proceeding; do NOT install launchd/brew-services/login + items; report a per-step pass/fail table." +``` +- **devops-engineer** — runs/edits the bootstrap + teardown, owns Step 1–3, 5–6. +- **debugger** — if Step 2 (Hermes↔Ollama) doesn't route locally. +- **tech-writer / skill author** — captures the result into rs‑agents (§5). + +--- + +## 5. Capture learnings → rs‑agents (O6 — the "fold it back in" you asked for 06‑28) + +**Decision (your call):** **extend the two existing skills** — do *not* add a new skill file. + +After the dry run proves the recipe, encode it (author in native dirs, then +`node ai/scripts/generate-index.js`, then bump the plugin `version` — per `ai/CLAUDE.md`): + +1. **Extend `local-development-setup`** (`ai/skills/local-development-setup/SKILL.md`) — generic today + (zero mention of dotfiles/mac‑mini/Ollama/Hermes/MCP). Add: the dotfiles `onboard.sh` one‑liner as the + Retail‑Success Mac‑provisioning path, and a "**Local AI agent host**" section = the proven §3 + Ollama + Hermes + MCP recipe (incl. the Hermes↔Ollama wiring that actually worked) and the §6 + promotion steps. Widen the `description:` frontmatter to mention Mac provisioning + local‑model setup. +2. **Extend `developer-onboarding`** (`ai/skills/developer-onboarding/SKILL.md`) — add a pointer to the + dotfiles one‑liner and a short "your machine can run local models" note that links to (1). +3. **Optional workflow** `ai/workflows/mac-mini-agent-bootstrap.md` — the deterministic always‑on runbook. +4. **Bootstrap upgrades** (dotfiles): add a real **`--dry-run`/`DRY_RUN`** mode to + `mac-mini-agent/mac-mini-mcp-bootstrap.sh` (it has none today), and **extend it with the Ollama+Hermes + section** the `/compact` note promised — gated so the laptop run is ephemeral. +5. *(Optional)* a `project`/`reference` memory recording that hermes = Nous Hermes Agent and the + Ollama↔Hermes wiring that actually worked. + +--- + +## 6. Promote to the mac‑mini — DOCUMENTED, **not** part of the laptop dry run (O1) + +These are the *persistent* pieces. They belong on the mini only, after §3 passes: +- **Ollama as a service:** `brew services start ollama` (or a launchd plist) + pre‑pull the chosen models. +- **Hermes** default model/provider set to local, cloud **fallback** configured. +- **Always‑on rs‑agents:** the `wayroo-demo` launchd units (`RunAtLoad`, `KeepAlive`), health probe on :8765. +- **Headless survival (from `mac-mini-mcp-bootstrap.sh`):** auto‑login, Docker Desktop autostart, + `pmset`/`systemsetup` no‑sleep, Keychain auto‑unlock, MCP secrets in Keychain. +- **Remote access:** add the mini to `~/.ssh/config` (not present today); Tailscale optional (no CLI yet). + +--- + +## 7. Risks & guardrails +- **Daily driver:** `rss-mbp-5` is your work machine — the dry run must stay ephemeral (§3 golden rule). +- **Disk:** ~179 GB free — fine for a few quantized models; `ollama rm` in teardown if tight. +- **Don't touch production `onboard.sh`** until the recipe is proven; keep bootstrap changes behind `--dry-run`. +- **Secrets:** no real PATs/tokens on the laptop dry run — that's a mini‑only step. +- **PATH shadowing:** `~/.local/bin/hermes` shadows brew `hermes-agent`; pick one deliberately and pin it. +- **Hermes↔Ollama routing is unverified** — §3 Step 2 is the spike that confirms it; LM Studio is the fallback. + +--- + +## 8. Decisions (made 2026‑06‑28) +1. **Plan is the deliverable** — no changes to `rss-mbp-5` yet. Review/iterate this doc; execute §3 later. +2. **Models:** `llama3.2:3b` (smoke) + `qwen2.5-coder:7b` (coding). ✔ (already in §3 Step 1) +3. **rs‑agents capture:** extend `local-development-setup` + `developer-onboarding`; **no new skill.** ✔ (see §5) + +### When you're ready to run it +Re‑open this file and either drive §3 through `/rs-agents` (§4) or run the steps directly. Sequence: +§3 (dry run) → §5 (fold back into the two skills + add `--dry-run` to the bootstrap) → §6 (promote to mini). diff --git a/mac-mini-agent/mac-mini-mcp-bootstrap.sh b/ai/workflows/mac-mini-agent/mac-mini-mcp-bootstrap.sh similarity index 100% rename from mac-mini-agent/mac-mini-mcp-bootstrap.sh rename to ai/workflows/mac-mini-agent/mac-mini-mcp-bootstrap.sh From 559303fc8fc3279d730c378ac21a939941b88389 Mon Sep 17 00:00:00 2001 From: andyb Date: Tue, 30 Jun 2026 18:15:59 -0500 Subject: [PATCH 07/14] maintenance(harness): [WR-18897] add local AI coding harness (Ollama + Hermes + rs-agents) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provision a local AI coding harness — Ollama local models behind the Hermes agent harness, with rs-agents skills synced in: - provision-coding-harness.sh: idempotent, --dry-run; RAM-sized model choice (qwen2.5-coder:7b on <32GB laptops, llama3.1:8b on the 32GB+ mini), Ollama service, model pulls, Hermes config (model + 64K context knobs), skill sync, health check. - sync-rs-agents-to-hermes.sh: mirror rs-agents skills from the installed Claude Code plugin into ~/.hermes/skills (Hermes' equivalent of `reload plugins`). - com.retailsuccess.ollama.plist: always-on launchd unit (perf env vars) for the 24/7 mini; intentionally not loaded on a 16GB laptop. - verify-ollama-hermes.sh: layered PASS/FAIL harness check (tools + 64K context + local routing + memory fit + skill-following). - meta/profiles/ai + meta/configs/hermes.yml + install-hermes-agent.sh: install Hermes via the `ai` profile. - DRY-RUN-PLAN.md / OLLAMA-HERMES-RUNBOOK.md: the plan + the validated runbook. - .yamllint.yml: fix rules nesting; CLAUDE.md/README: document the `ai` profile. Counterpart rs-agents skill guidance: Wayroo.tools#46 (WR-18915), epic WR-18831. Co-Authored-By: Claude Opus 4.8 (1M context) --- .yamllint.yml | 16 +- CLAUDE.md | 2 +- README.md | 2 +- mac-mini-agent/DRY-RUN-PLAN.md | 224 ++++++++++++++++++ mac-mini-agent/OLLAMA-HERMES-RUNBOOK.md | 139 +++++++++++ mac-mini-agent/com.retailsuccess.ollama.plist | 46 ++++ mac-mini-agent/provision-coding-harness.sh | 117 +++++++++ mac-mini-agent/sync-rs-agents-to-hermes.sh | 60 +++++ mac-mini-agent/verify-ollama-hermes.sh | 104 ++++++++ meta/configs/hermes.yml | 11 + meta/profiles/ai | 1 + onboarding_bin/install-hermes-agent.sh | 28 +++ 12 files changed, 745 insertions(+), 5 deletions(-) create mode 100644 mac-mini-agent/DRY-RUN-PLAN.md create mode 100644 mac-mini-agent/OLLAMA-HERMES-RUNBOOK.md create mode 100644 mac-mini-agent/com.retailsuccess.ollama.plist create mode 100755 mac-mini-agent/provision-coding-harness.sh create mode 100755 mac-mini-agent/sync-rs-agents-to-hermes.sh create mode 100755 mac-mini-agent/verify-ollama-hermes.sh create mode 100644 meta/configs/hermes.yml create mode 100644 meta/profiles/ai create mode 100755 onboarding_bin/install-hermes-agent.sh 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/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..4b7975c --- /dev/null +++ b/mac-mini-agent/provision-coding-harness.sh @@ -0,0 +1,117 @@ +#!/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 + +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="/opt/homebrew/bin/ollama" +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)" +command -v "$OLLAMA_BIN" >/dev/null || { 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; } +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 list 2>/dev/null | grep -q "^${m}[[:space:]]"; then + bold " ${m} already present" + else + run "ollama 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. Sync rs-agents skills into Hermes (the bridge) ──────────────────────── +step "4. Sync rs-agents skills into Hermes" +run "bash '$HERE/sync-rs-agents-to-hermes.sh'" + +# ── 5. Health check ────────────────────────────────────────────────────────── +step "5. 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..3134208 --- /dev/null +++ b/mac-mini-agent/sync-rs-agents-to-hermes.sh @@ -0,0 +1,60 @@ +#!/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 + +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}" + +step "Locating the installed rs-agents plugin (released version)" +# Prefer the Claude Code plugin cache (exactly what `reload plugins` installed). +# Pick the highest version dir if several are cached. +SRC="$(ls -d "$HOME"/.claude/plugins/cache/*/rs-agents/*/skills 2>/dev/null | sort -V | tail -1 || true)" +if [[ -z "${SRC}" || ! -d "${SRC}" ]]; then + warn "rs-agents plugin skills not found under ~/.claude/plugins/cache/*/rs-agents/*/skills" + warn "Install/refresh the plugin in Claude Code first (add the wayroo marketplace, then 'reload plugins')." + exit 1 +fi +bold " source: ${SRC}" + +step "Mirroring skills into Hermes" +# Clean rebuild so removed/renamed skills don't linger (idempotent). +rm -rf "${HERMES_SKILLS_DIR}" +mkdir -p "${HERMES_SKILLS_DIR}" +count=0 +for d in "${SRC}"/*/; do + [[ -f "${d}SKILL.md" ]] || continue + name="$(basename "$d")" + mkdir -p "${HERMES_SKILLS_DIR}/${name}" + cp "${d}SKILL.md" "${HERMES_SKILLS_DIR}/${name}/SKILL.md" + count=$((count + 1)) +done +bold " synced ${count} rs-agents skills -> ${HERMES_SKILLS_DIR}" + +step "Verifying Hermes sees them" +if command -v hermes >/dev/null 2>&1; then + hermes skills list 2>/dev/null | grep -c "rs-agents" | xargs -I{} echo " hermes reports {} skills in the 'rs-agents' category" +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..25d20b0 --- /dev/null +++ b/mac-mini-agent/verify-ollama-hermes.sh @@ -0,0 +1,104 @@ +#!/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 + +OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}" +CFG="$HOME/.hermes/config.yaml" +MODEL="${1:-$(awk '/^model:/{m=1;next} m&&/default:/{print $2; exit}' "$CFG")}" +MARK="===VERIFY $(date +%H%M%S)===" +SLOG=/tmp/ollama-spike.log # the ollama serve log (used to prove a local hit) +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) +printf '\n%s\n' "$MARK" >> "$SLOG" + +# 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 +if awk -v m="$MARK" '$0~m{f=1} f' "$SLOG" 2>/dev/null | grep -qiE "chat/completions|POST"; 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 +PS=$(ollama ps 2>/dev/null | sed -n '2p') +[ -n "$PS" ] && g "model loaded locally: $PS" || i "ollama ps shows nothing loaded (may have unloaded)" + +# 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/onboarding_bin/install-hermes-agent.sh b/onboarding_bin/install-hermes-agent.sh new file mode 100755 index 0000000..f3cfc92 --- /dev/null +++ b/onboarding_bin/install-hermes-agent.sh @@ -0,0 +1,28 @@ +#!/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 From dcf059b15e2ad54ce7e226911c5445af9cca9ad3 Mon Sep 17 00:00:00 2001 From: andyb Date: Thu, 2 Jul 2026 10:57:08 -0500 Subject: [PATCH 08/14] maintenance(harness): [WR-18897] wire rs-agents skills sync into Hermes onboarding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install-hermes-agent.sh now runs the rs-agents skills sync (best-effort, --soft) so a fresh Hermes install auto-connects rs-agents. Note: Hermes cannot consume the rs-agents Claude Code plugin via 'hermes plugins' (that expects Python provider plugins) — rs-agents reaches Hermes through its SKILLS. Sync gains a Wayroo.tools-clone fallback (RS_AGENTS_SKILLS_SRC) and a --soft mode that never fails the install. Co-Authored-By: Claude Opus 4.8 (1M context) --- mac-mini-agent/sync-rs-agents-to-hermes.sh | 23 +++++++++++++++++----- onboarding_bin/install-hermes-agent.sh | 16 +++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/mac-mini-agent/sync-rs-agents-to-hermes.sh b/mac-mini-agent/sync-rs-agents-to-hermes.sh index 3134208..e2c1850 100755 --- a/mac-mini-agent/sync-rs-agents-to-hermes.sh +++ b/mac-mini-agent/sync-rs-agents-to-hermes.sh @@ -26,13 +26,26 @@ warn() { printf '\033[1;33m! %s\033[0m\n' "$*"; } HERMES_SKILLS_DIR="${HERMES_SKILLS_DIR:-$HOME/.hermes/skills/rs-agents}" -step "Locating the installed rs-agents plugin (released version)" -# Prefer the Claude Code plugin cache (exactly what `reload plugins` installed). -# Pick the highest version dir if several are cached. +# --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. 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 plugin skills not found under ~/.claude/plugins/cache/*/rs-agents/*/skills" - warn "Install/refresh the plugin in Claude Code first (add the wayroo marketplace, then 'reload plugins')." + 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}" diff --git a/onboarding_bin/install-hermes-agent.sh b/onboarding_bin/install-hermes-agent.sh index f3cfc92..f889f34 100755 --- a/onboarding_bin/install-hermes-agent.sh +++ b/onboarding_bin/install-hermes-agent.sh @@ -26,3 +26,19 @@ else # "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)" +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 From 355e83f82e6cd1870f29103fe02acf3269d11192 Mon Sep 17 00:00:00 2001 From: andyb Date: Thu, 2 Jul 2026 11:57:41 -0500 Subject: [PATCH 09/14] maintenance(harness): [WR-18897] zero-touch rs-agents plugin install + auto-resync to Hermes Keep Hermes in lockstep with the rs-agents plugin as skills change upstream: - install-claude-plugins.sh: register the marketplace + install rs-agents (non-interactive, tries retailsuccess@ then wayroo@ marketplace names). Claude Code then auto-updates it on startup (marketplace autoUpdate). - install-hermes-agent.sh: onboarding now installs the plugin THEN mirrors its skills into Hermes. - provision-coding-harness.sh: adds a launchd timer (com.retailsuccess.rs-agents-sync, RunAtLoad + hourly) that re-runs the clean-rebuild sync, so add/modify/DELETE all propagate to Hermes hands-free. - sync: PATH export so it resolves hermes under launchd's minimal env. Only SKILLS port to Hermes (agents/workflows/memories are Claude Code constructs). Source of truth stays Wayroo.tools; requires the plugin version be bumped per release for autoUpdate to pull changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- mac-mini-agent/provision-coding-harness.sh | 40 +++++++++++++++++++--- mac-mini-agent/sync-rs-agents-to-hermes.sh | 3 ++ onboarding_bin/install-claude-plugins.sh | 37 ++++++++++++++++++++ onboarding_bin/install-hermes-agent.sh | 11 ++++++ 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100755 onboarding_bin/install-claude-plugins.sh diff --git a/mac-mini-agent/provision-coding-harness.sh b/mac-mini-agent/provision-coding-harness.sh index 4b7975c..57ea090 100755 --- a/mac-mini-agent/provision-coding-harness.sh +++ b/mac-mini-agent/provision-coding-harness.sh @@ -106,12 +106,42 @@ except Exception: PY fi -# ── 4. Sync rs-agents skills into Hermes (the bridge) ──────────────────────── -step "4. Sync rs-agents skills into Hermes" -run "bash '$HERE/sync-rs-agents-to-hermes.sh'" +# ── 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. Health check ────────────────────────────────────────────────────────── -step "5. Verify the harness" +# ── 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 index e2c1850..7bbaf48 100755 --- a/mac-mini-agent/sync-rs-agents-to-hermes.sh +++ b/mac-mini-agent/sync-rs-agents-to-hermes.sh @@ -20,6 +20,9 @@ # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail +# launchd runs with a minimal PATH; make sure user-local tools (hermes) resolve. +export PATH="$HOME/.local/bin:/opt/homebrew/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' "$*"; } 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 index f889f34..4d69dd3 100755 --- a/onboarding_bin/install-hermes-agent.sh +++ b/onboarding_bin/install-hermes-agent.sh @@ -36,6 +36,17 @@ fi # 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 "" From 6722e9535409641222988e6819fe3edeba15c1b0 Mon Sep 17 00:00:00 2001 From: andyb Date: Fri, 3 Jul 2026 12:38:06 -0500 Subject: [PATCH 10/14] maintenance(ide): Removed GitHub Copilot extensions and updated VSCode settings for code actions on save. maintenance(profiles): Added AI profile to base profiles. story(onboarding): Added script to connect Hermes to Docker MCP Toolkit gateway and updated installation script. maintenance(git): Updated .gitignore to exclude local Claude settings. --- .../rs-agents-code-reviewer/MEMORY.md | 3 + .../project_hermes_mcp_onboarding.md | 19 + ai/plans/MCP-PROFILE-SYNC-PLAN.md | 436 ++++++++++++++++++ engineering/ide/.vscode/extensions.txt | 2 - engineering/ide/.vscode/settings.json | 201 ++++---- meta/profiles/base | 1 + onboarding_bin/connect-hermes-mcp.sh | 208 +++++++++ onboarding_bin/install-hermes-agent.sh | 11 + .../.git-template-directory/.gitignore | 2 + 9 files changed, 781 insertions(+), 102 deletions(-) create mode 100644 .claude/agent-memory/rs-agents-code-reviewer/MEMORY.md create mode 100644 .claude/agent-memory/rs-agents-code-reviewer/project_hermes_mcp_onboarding.md create mode 100644 ai/plans/MCP-PROFILE-SYNC-PLAN.md create mode 100755 onboarding_bin/connect-hermes-mcp.sh 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/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/meta/profiles/base b/meta/profiles/base index bf60dc4..16f3943 100755 --- a/meta/profiles/base +++ b/meta/profiles/base @@ -7,4 +7,5 @@ command_line version_control nvm ide +ai 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-hermes-agent.sh b/onboarding_bin/install-hermes-agent.sh index 4d69dd3..8ed467e 100755 --- a/onboarding_bin/install-hermes-agent.sh +++ b/onboarding_bin/install-hermes-agent.sh @@ -53,3 +53,14 @@ if [ -f "$SYNC" ]; then 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 From 6f9b3c94ae5feabbe654cae567f615acc9aa807a Mon Sep 17 00:00:00 2001 From: andyb Date: Mon, 6 Jul 2026 13:38:56 -0500 Subject: [PATCH 11/14] maintenance(dotfiles): [WR-18897] Replaced 'ai' with 'hermes' in base profile --- meta/profiles/base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta/profiles/base b/meta/profiles/base index 16f3943..20d9fbc 100755 --- a/meta/profiles/base +++ b/meta/profiles/base @@ -7,5 +7,5 @@ command_line version_control nvm ide -ai +hermes cleanup From 649790f56fb0c404c60b6f4f1f001e0b9c6d1c16 Mon Sep 17 00:00:00 2001 From: Andrew Barrows Date: Mon, 6 Jul 2026 13:40:31 -0500 Subject: [PATCH 12/14] Update mac-mini-agent/verify-ollama-hermes.sh Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Signed-off-by: Andrew Barrows --- mac-mini-agent/verify-ollama-hermes.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mac-mini-agent/verify-ollama-hermes.sh b/mac-mini-agent/verify-ollama-hermes.sh index 25d20b0..603e67a 100755 --- a/mac-mini-agent/verify-ollama-hermes.sh +++ b/mac-mini-agent/verify-ollama-hermes.sh @@ -13,6 +13,15 @@ set -uo pipefail OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}" CFG="$HOME/.hermes/config.yaml" MODEL="${1:-$(awk '/^model:/{m=1;next} m&&/default:/{print $2; exit}' "$CFG")}" + +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)===" SLOG=/tmp/ollama-spike.log # the ollama serve log (used to prove a local hit) FAILED=0 From 34c0911eda868cdccc8b96f8e004f6e04587824e Mon Sep 17 00:00:00 2001 From: andyb Date: Mon, 6 Jul 2026 13:49:40 -0500 Subject: [PATCH 13/14] maintenance(dotfiles): [WR-18897] Enhanced settings.local.json with additional Bash commands and YAML validations for Hermes integration. --- .claude/settings.local.json | 57 ++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) 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 *)" ] } } From d459dc526c1ee4bd2bb609b2f4baf1a6d605764f Mon Sep 17 00:00:00 2001 From: andyb Date: Mon, 6 Jul 2026 14:16:14 -0500 Subject: [PATCH 14/14] maintenance(harness): [WR-18897] address PR #50 review callouts on mac-mini-agent scripts Validated all Sourcery callouts (rs-agents code-reviewer) and fixed the real ones: - provision-coding-harness.sh: export Homebrew-inclusive PATH (Apple-silicon + Intel), resolve OLLAMA_BIN via command -v (env-overridable), guard HERMES_PY existence, and call "$OLLAMA_BIN" consistently for list/pull. - verify-ollama-hermes.sh: point the local-routing proof at the logs Ollama actually writes (launchd err.log / on-demand out.log) instead of the never-written ollama-spike.log; hard-FAIL when neither local proof produces evidence; abort clearly on an empty MODEL; strip YAML quotes when deriving model.default. - sync-rs-agents-to-hermes.sh: stage the mirror in a temp dir and swap only on a non-empty copy, so a restructured/empty plugin source can never silently wipe the working Hermes skill set (the sync runs hourly via launchd); guard the zero-match grep -c pipeline under pipefail. Co-Authored-By: Claude Fable 5 --- mac-mini-agent/provision-coding-harness.sh | 17 ++++++++---- mac-mini-agent/sync-rs-agents-to-hermes.sh | 30 +++++++++++++++----- mac-mini-agent/verify-ollama-hermes.sh | 32 ++++++++++++++++++---- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/mac-mini-agent/provision-coding-harness.sh b/mac-mini-agent/provision-coding-harness.sh index 57ea090..41d04cc 100755 --- a/mac-mini-agent/provision-coding-harness.sh +++ b/mac-mini-agent/provision-coding-harness.sh @@ -19,6 +19,10 @@ # ───────────────────────────────────────────────────────────────────────────── 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 @@ -34,15 +38,16 @@ 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="/opt/homebrew/bin/ollama" -HERMES_PY="$HOME/.hermes/hermes-agent/venv/bin/python" +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)" -command -v "$OLLAMA_BIN" >/dev/null || { warn "ollama not installed — 'brew install ollama' (or run the Brewfile)."; exit 1; } +[[ -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; } -bold " ollama: $($OLLAMA_BIN --version 2>/dev/null | head -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 @@ -70,10 +75,10 @@ fi # ── 2. Pull models ─────────────────────────────────────────────────────────── step "2. Pull models (sized to RAM)" for m in "$MODEL" "$SMOKE"; do - if [[ $DRY -eq 0 ]] && ollama list 2>/dev/null | grep -q "^${m}[[:space:]]"; then + if [[ $DRY -eq 0 ]] && "$OLLAMA_BIN" list 2>/dev/null | grep -q "^${m}[[:space:]]"; then bold " ${m} already present" else - run "ollama pull '$m'" + run "'$OLLAMA_BIN' pull '$m'" fi done diff --git a/mac-mini-agent/sync-rs-agents-to-hermes.sh b/mac-mini-agent/sync-rs-agents-to-hermes.sh index 7bbaf48..9ec1fb0 100755 --- a/mac-mini-agent/sync-rs-agents-to-hermes.sh +++ b/mac-mini-agent/sync-rs-agents-to-hermes.sh @@ -21,7 +21,7 @@ set -euo pipefail # launchd runs with a minimal PATH; make sure user-local tools (hermes) resolve. -export PATH="$HOME/.local/bin:/opt/homebrew/bin:$PATH" +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' "$*"; } @@ -35,6 +35,9 @@ SOFT=0 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. @@ -54,22 +57,35 @@ fi bold " source: ${SRC}" step "Mirroring skills into Hermes" -# Clean rebuild so removed/renamed skills don't linger (idempotent). -rm -rf "${HERMES_SKILLS_DIR}" -mkdir -p "${HERMES_SKILLS_DIR}" +# 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 "${HERMES_SKILLS_DIR}/${name}" - cp "${d}SKILL.md" "${HERMES_SKILLS_DIR}/${name}/SKILL.md" + 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 - hermes skills list 2>/dev/null | grep -c "rs-agents" | xargs -I{} echo " hermes reports {} skills in the 'rs-agents' category" + # 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 diff --git a/mac-mini-agent/verify-ollama-hermes.sh b/mac-mini-agent/verify-ollama-hermes.sh index 603e67a..b1669bf 100755 --- a/mac-mini-agent/verify-ollama-hermes.sh +++ b/mac-mini-agent/verify-ollama-hermes.sh @@ -10,9 +10,14 @@ # 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="${1:-$(awk '/^model:/{m=1;next} m&&/default:/{print $2; exit}' "$CFG")}" +# 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 @@ -23,7 +28,11 @@ if [ -z "$MODEL" ]; then fi MARK="===VERIFY $(date +%H%M%S)===" -SLOG=/tmp/ollama-spike.log # the ollama serve log (used to prove a local hit) +# 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"; } @@ -64,7 +73,7 @@ echo "$DIRECT" | grep -q "PING_OK" && g "direct inference correct ($((t1-t0))s)" # 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) -printf '\n%s\n' "$MARK" >> "$SLOG" +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" @@ -75,14 +84,25 @@ t1=$(date +%s); LAT=$((t1-t0)) && 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 -if awk -v m="$MARK" '$0~m{f=1} f' "$SLOG" 2>/dev/null | grep -qiE "chat/completions|POST"; then +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 +# 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') -[ -n "$PS" ] && g "model loaded locally: $PS" || i "ollama ps shows nothing loaded (may have unloaded)" +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"