diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..a0814018 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,249 @@ +# ── Baked Devcontainer Image ────────────────────────────────────────── +# Pre-installs all heavy tooling so containers start in seconds, not minutes. +# Rebuild: docker build -t hermes-codespace:latest -f .devcontainer/Dockerfile . +# ──────────────────────────────────────────────────────────────────────── + +# ── Ollama version (only needed for the FROM instruction) ─────────────── +ARG OLLAMA_VERSION=0.32.5 + +# Use the official Ollama image to get the binary +FROM ollama/ollama:${OLLAMA_VERSION} AS ollama-bin + +# ── Node.js builder: ModelRelay + OmniRoute + Hermes Web UI + TUI ─────── +FROM node:24-slim AS node-builder + +ARG OMNIROUTE_VERSION +ARG HERMES_VERSION + +# Install git for GitHub-based npm installs and configure HTTPS +RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" + +# ── ModelRelay ──────────────────────────────────────────────────────── +RUN npm install github:gitricko/modelrelay -g --prefix /build/modelrelay + +# ── OmniRoute ───────────────────────────────────────────────────────── +RUN npm install omniroute@${OMNIROUTE_VERSION} -g --prefix /build/omniroute + +# ── OmniRoute dist/ dep repair (hollow deps workaround) ────────────── +RUN omni_root="/build/omniroute/lib/node_modules/omniroute" \ + && dist_nm="$omni_root/dist/node_modules" \ + && parent_nm="$omni_root/node_modules" \ + && if [ -d "$dist_nm" ]; then \ + for dst in $(find "$dist_nm" -mindepth 1 -maxdepth 1 -type d 2>/dev/null); do \ + rel="${dst#"$dist_nm"/}" \ + && src="$parent_nm/$rel" \ + && if [ -d "$src" ] && [ ! "$(find "$dst" \( -name '*.js' -o -name '*.mjs' -o -name '*.node' \) -type f 2>/dev/null | head -1)" ] \ + && [ "$(find "$src" \( -name '*.js' -o -name '*.mjs' -o -name '*.node' \) -type f 2>/dev/null | head -1)" ]; then \ + rm -rf "$dst" && cp -r "$src" "$dst" \ + && echo "Repaired hollow dep: $rel"; \ + fi; \ + done; \ + fi + +# ── Hermes Web UI ───────────────────────────────────────────────────── +# Build output lands at hermes_cli/web_dist relative to the repo root, +# NOT relative to the web/ directory. So from web/ it's ../hermes_cli/web_dist +RUN git clone --depth 1 --branch ${HERMES_VERSION} https://github.com/NousResearch/hermes-agent.git /tmp/hermes \ + && cd /tmp/hermes/web \ + && npm install --silent \ + && npm run build \ + && cp -r ../hermes_cli/web_dist /build/web_dist \ + && rm -rf /tmp/hermes + +# ── Hermes TUI ──────────────────────────────────────────────────────── +# Build the TUI bundle (dist/entry.js) and copy to hermes_cli/tui_dist +# so the dashboard's embedded chat tab can spawn the TUI without a full workspace +RUN git clone --depth 1 --branch ${HERMES_VERSION} https://github.com/NousResearch/hermes-agent.git /tmp/hermes-tui \ + && cd /tmp/hermes-tui/ui-tui \ + && npm install --silent \ + && npm run build \ + && mkdir -p /build/tui_dist \ + && cp dist/entry.js /build/tui_dist/ \ + && rm -rf /tmp/hermes-tui + +# ── Python builder: Hermes Agent venv ──────────────────────────────────── +# Using 3.11-slim because hermes-agent requires Python >=3.11. +# We copy the full Python 3.11 runtime into the final image so venv works. +FROM python:3.11-slim AS python-builder + +ARG HERMES_VERSION + +# Install git for cloning hermes-agent repo +RUN apt-get update && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* + +# Clone hermes-agent source (separate RUN so the prebuilt bundles can be +# injected into the tree BEFORE pip install below) +RUN git clone --depth 1 --branch ${HERMES_VERSION} https://github.com/NousResearch/hermes-agent.git /tmp/hermes + +# Inject the prebuilt TUI bundle into the source tree before `pip install .`. +# pyproject.toml declares tui_dist/**/* as package-data, so with the bundle +# present the wheel is built WITH it and the venv becomes self-contained — +# the dashboard's embedded chat PTY resolves hermes_cli/tui_dist/entry.js +# at runtime, and no last-minute copy into site-packages is needed. +COPY --from=node-builder /build/tui_dist/ /tmp/hermes/hermes_cli/tui_dist/ + +# Build Hermes Agent Python venv (non-editable so venv is self-contained) +# IMPORTANT: venv path must match COPY destination in final stage for shebangs to work +RUN mkdir -p /usr/local/lib/hermes-agent \ + && cd /tmp/hermes \ + && python -m venv /usr/local/lib/hermes-agent/venv \ + && /usr/local/lib/hermes-agent/venv/bin/pip install --no-cache-dir . \ + && /usr/local/lib/hermes-agent/venv/bin/pip install --no-cache-dir ".[acp]" \ + && /usr/local/lib/hermes-agent/venv/bin/hermes acp --check \ + && rm -rf /tmp/hermes + +# ── Final stage ──────────────────────────────────────────────────────── +FROM mcr.microsoft.com/devcontainers/base:ubuntu + +# Tool versions — passed from workflow via --build-arg (single source of truth) +ARG HERMES_VERSION +ARG OMNIROUTE_VERSION +ARG NODE_VERSION +ARG MNEMON_VERSION + +# Export as env so install scripts can see them +ENV OLLAMA_VERSION=${OLLAMA_VERSION} +ENV OLLAMA_NO_START=1 +ENV DEBIAN_FRONTEND=noninteractive + +# Copy the Ollama binary from the official image +COPY --from=ollama-bin --chown=1000:1000 /usr/bin/ollama /usr/local/bin/ollama + +# ── System packages + ALL heavy installs in ONE layer ──────────────────. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + zsh ripgrep jq curl git ca-certificates gnupg zstd sudo \ + python3 python3-pip nodejs npm \ + && rm -rf /var/lib/apt/lists/* \ + + # ── Ensure a non-root user with UID 1000 exists ────────────────────── + && UID1000_USER=$(getent passwd 1000 | cut -d: -f1) \ + && if [ -z "$UID1000_USER" ]; then \ + (getent group 1000 >/dev/null || groupadd --gid 1000 vscode) \ + && useradd --uid 1000 --gid 1000 -m -s /bin/bash vscode \ + && UID1000_USER=vscode; \ + fi \ + && echo "${UID1000_USER} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/vscode \ + && chmod 0440 /etc/sudoers.d/vscode \ + + # ── Ollama (direct binary install – reliable in Docker) ─────────── + && ollama --version \ + + # ── Bake Ollama embedding model into image ─────────────────────── + # Pull nomic-embed-text at build time so it's available instantly + # at runtime with zero RAM spike or download delay. (~300MB) + # Ensure full directory tree exists with proper permissions for vscode user + && mkdir -p /usr/share/ollama/.ollama/models \ + && chmod -R a+rwX /usr/share/ollama/.ollama \ + && chmod a+rx /usr/share/ollama \ + && OLLAMA_HOST=0.0.0.0 OLLAMA_MODELS=/usr/share/ollama/.ollama/models ollama serve & \ + for i in 1 2 3 4 5; do sleep 10; curl -sf http://localhost:11434/api/tags && break; done; \ + OLLAMA_MODELS=/usr/share/ollama/.ollama/models /usr/local/bin/ollama pull nomic-embed-text && \ + ls -la /usr/share/ollama/.ollama/models/ && \ + find /usr/share/ollama/.ollama/models -type f | head -20 && \ + OLLAMA_MODELS=/usr/share/ollama/.ollama/models ollama list && \ + kill %1 && wait 2>/dev/null || true \ + \ + # ── TailScale ───────────────────────────────────────────────────── + #&& mkdir -p /var/run/tailscale /var/lib/tailscale \ + #&& (curl -fsSL https://tailscale.com/install.sh | bash || echo "WARN: tailscale install failed, continuing") \ + #&& rm -rf /var/lib/apt/lists/* \ + + + # ── Mnemon ──────────────────────────────────────────────────────── + && ARCH=amd64 \ + && curl -sL "https://github.com/mnemon-dev/mnemon/releases/download/v${MNEMON_VERSION}/mnemon_${MNEMON_VERSION}_linux_${ARCH}.tar.gz" \ + -o /tmp/mnemon.tar.gz \ + && tar xzf /tmp/mnemon.tar.gz -C /tmp \ + && cp /tmp/mnemon /usr/local/bin/mnemon \ + && chmod +x /usr/local/bin/mnemon \ + && rm -rf /tmp/mnemon.tar.gz /tmp/mnemon \ + + + && curl -fsSL https://claude.ai/install.sh | bash \ + + + # ── Final cleanup ───────────────────────────────────────────────── + && apt-get autoremove -y \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /root/.npm /tmp/* /var/tmp/* \ + && rm -rf /root/.cache/pip 2>/dev/null || true \ + # Ensure Ollama model directory and all parent dirs are writable by vscode user + && mkdir -p /usr/share/ollama/.ollama/models \ + && chmod -R a+rwX /usr/share/ollama/.ollama \ + # Ensure /usr/share/ollama itself is traversable by vscode user + && chmod a+rx /usr/share/ollama + +# ── Copy Node.js packages from builder stage ────────────────────────── +COPY --from=node-builder /build/modelrelay/ /usr/local/lib/modelrelay/ +COPY --from=node-builder /build/omniroute/ /usr/local/lib/omniroute/ +# The venv's non-editable install puts hermes_cli in site-packages, so +# web_dist must go there for the dashboard --skip-build to find it. +COPY --from=node-builder /build/web_dist/ /usr/local/lib/hermes-agent/venv/lib/python3.11/site-packages/hermes_cli/web_dist/ + +# ── Copy Hermes Agent venv from python-builder ──────────────────────── +# Non-editable install: venv is self-contained, no source tree needed. +# Venv was built at /usr/local/lib/hermes-agent/venv — same path in final image. +COPY --from=python-builder /usr/local/lib/hermes-agent/venv/ /usr/local/lib/hermes-agent/venv/ + +# ── Install Python 3.11 (required by hermes-agent and the venv) ────── +# The venv was built in python:3.11-slim which puts python at /usr/local/bin/python3.11. +# deadsnakes PPA installs to /usr/bin/python3.11, so symlink for compatibility. +RUN apt-get update \ + && apt-get install -y --no-install-recommends software-properties-common \ + && add-apt-repository -y ppa:deadsnakes/ppa \ + && apt-get update \ + && apt-get install -y --no-install-recommends python3.11 python3.11-venv \ + && ln -sf /usr/bin/python3.11 /usr/local/bin/python3.11 \ + && ln -sf /usr/bin/python3.11 /usr/local/bin/python \ + && rm -rf /var/lib/apt/lists/* + +# ── Create symlinks for Node.js packages ────────────────────────────── +RUN ln -sf /usr/local/lib/modelrelay/bin/modelrelay /usr/local/bin/modelrelay \ + && ln -sf /usr/local/lib/omniroute/bin/omniroute /usr/local/bin/omniroute + +# ── Create hermes symlink from python-builder venv ──────────────────── +RUN ln -sf /usr/local/lib/hermes-agent/venv/bin/hermes /usr/local/bin/hermes + +# ── Ensure hermes venv is world-readable ─────────────────────────────── +# FHS root layout places the venv at /usr/local/lib/hermes-agent/venv/. +# Some sub-directories may be mode 700 (root-only). Make them traversable +# so the vscode user can exec hermes and its bundled Python. The venv is +# also made group/other-WRITABLE so the VS Code extension's ACP dependency +# repair flow (pip install into the venv as the vscode user) can succeed +# without sudo — this is a devcontainer, not a hardened prod image. +RUN chmod -R a+rX /usr/local/lib/hermes-agent 2>/dev/null || true \ + && chmod -R a+rwX /usr/local/lib/hermes-agent/venv 2>/dev/null || true \ + && chmod -R a+rX /usr/local/lib/nodejs 2>/dev/null || true + +# ── Make claude CLI accessible if installed to /root ─────────────────── +# The claude.ai installer may put the binary under $HOME/.local/bin/. +# Create a symlink in /usr/local/bin/ so all users can find it. +RUN CLAUDE_BIN="$(find /root/.local /usr/local -name claude -type f 2>/dev/null | head -1)" \ + && if [ -n "$CLAUDE_BIN" ] && [ ! -x "/usr/local/bin/claude" ]; then \ + ln -sf "$CLAUDE_BIN" /usr/local/bin/claude \ + && echo "claude CLI linked from $CLAUDE_BIN"; \ + fi + +# ── Copy config files ───────────────────────────────────────────────── +COPY .devcontainer/CLAUDE.md /usr/local/share/devcontainer-config/CLAUDE.md +COPY .devcontainer/claude-term-settings.json /usr/local/share/devcontainer-config/claude-term-settings.json +COPY .devcontainer/.claude.json /usr/local/share/devcontainer-config/.claude.json +COPY .devcontainer/skill-memory-automation.md /usr/local/share/devcontainer-config/skill-memory-automation.md +COPY .devcontainer/.hermes.md /usr/local/share/devcontainer-config/.hermes.md +COPY .devcontainer/cline-globalState.json /usr/local/share/devcontainer-config/cline-globalState.json +COPY .devcontainer/cline-secrets.json /usr/local/share/devcontainer-config/cline-secrets.json +COPY .devcontainer/self-check.sh /usr/local/bin/self-check.sh + +# ── Entrypoint: lightweight service start + config placement ────────── +COPY .devcontainer/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/self-check.sh + +# ── Switch to non-root user (by UID, works with any username) ──────────── +USER 1000 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["sleep", "infinity"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2026e81f..763ed81b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,10 @@ { "name": "Hermes-Coding-Agent", + "image": "ghcr.io/laouncle/hermes-codespace/devcontainer:pr-1", + "overrideCommand": false, + "runArgs": [ + "--name", "hermes-codespace" + ], "customizations": { "vscode": { "extensions": [ @@ -8,7 +13,5 @@ "saoudrizwan.claude-dev" ] } - }, - "postCreateCommand": "bash ./.devcontainer/post-create-cmd.sh >> /tmp/hermes-codespace.log 2>&1", - "postStartCommand": "bash ./.devcontainer/codespace-cleanup.sh >> /tmp/hermes-codespace.log 2>&1; bash ./.devcontainer/start-hermes.sh >> /tmp/hermes-codespace.log 2>&1" -} \ No newline at end of file + } +} diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh new file mode 100755 index 00000000..f7348aad --- /dev/null +++ b/.devcontainer/entrypoint.sh @@ -0,0 +1,243 @@ +#!/bin/bash +# ── Entrypoint: copies baked configs to $HOME and starts services ───── +# This replaces both post-create-cmd.sh and start-hermes.sh. +# Heavy installs are already in the image; this only does runtime setup. +set -e + +SCRIPT_NAME="entrypoint.sh" +echo "***** Hermes Codespace — Baked Image Entrypoint *****" + +# ── Locate hermes venv (FHS root layout vs legacy) ─────────────────── +HERMES_VENV="/usr/local/lib/hermes-agent/venv" +if [ ! -d "$HERMES_VENV" ]; then + HERMES_VENV="$HOME/.hermes/hermes-agent/venv" +fi +HERMES_PYTHON="$HERMES_VENV/bin/python" +HERMES_PIP="$HERMES_VENV/bin/pip" + +# ── Place config files into $HOME (only if not already customized) ─── +place_config() { + local src="$1" dst="$2" + if [ ! -f "$dst" ] || ! cmp -s "$src" "$dst" 2>/dev/null; then + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" + echo "[$SCRIPT_NAME] Placed $(basename "$dst")" + fi +} + +place_config /usr/local/share/devcontainer-config/CLAUDE.md "$HOME/.claude/CLAUDE.md" +place_config /usr/local/share/devcontainer-config/claude-term-settings.json "$HOME/.claude/settings.json" +place_config /usr/local/share/devcontainer-config/.claude.json "$HOME/.claude.json" +place_config /usr/local/share/devcontainer-config/.hermes.md "$HOME/.hermes.md" +place_config /usr/local/share/devcontainer-config/cline-globalState.json "$HOME/.cline/data/globalState.json" +place_config /usr/local/share/devcontainer-config/cline-secrets.json "$HOME/.cline/data/secrets.json" +mkdir -p "$HOME/.hermes/skills/memory-automation" +place_config /usr/local/share/devcontainer-config/skill-memory-automation.md "$HOME/.hermes/skills/memory-automation/SKILL.md" + +# ── Hermes config defaults (first session only) ────────────────────── +if command -v hermes &>/dev/null \ + && [ ! -f "$HOME/.hermes/config.yaml" ]; then + echo "[$SCRIPT_NAME] Setting up default Hermes config..." + hermes config set model.default auto-fastest + hermes config set model.provider omniroute + hermes config set providers.omniroute.base_url http://localhost:20128/v1 + hermes config set providers.omniroute.api_key no-key-needed + hermes config set providers.modelrelay.base_url http://localhost:7352/v1 + hermes config set providers.modelrelay.api_key no-key-needed + hermes config set fallback_providers.provider modelrelay + hermes config set fallback_providers.model auto-fastest + hermes config set auxiliary.title_generation.model auto-fastest + hermes config set auxiliary.title_generation.provider modelrelay + hermes config set auxiliary.vision.model auto-fastest + hermes config set auxiliary.vision.provider modelrelay + hermes config set auxiliary.compression.model auto-fastest + hermes config set auxiliary.compression.provider modelrelay + hermes config set approvals.mode off + hermes config set memory.memory_enabled true + hermes config set memory.user_profile_enabled true + hermes config set memory.provider mnemon + hermes config set agent.max_turns 120 + hermes config set kanban.failure_limit 3 +fi + +# ── Mnemon USER.md ─────────────────────────────────────────────────── +if [ ! -f "$HOME/.hermes/memories/USER.md" ]; then + mkdir -p "$HOME/.hermes/memories" + cat > "$HOME/.hermes/memories/USER.md" <<'USEREOF' +Always use Mnemon (mnemon_remember / mnemon_recall) as primary memory provider instead of the standard memory() tool. Mnemon has no char limit. Only fall back to memory() for structured preference data (target=user or memory). +USEREOF + echo "[$SCRIPT_NAME] Created USER.md for Mnemon" +fi + +# ── Service throttling helpers ─────────────────────────────────────── + +# Wait for CPU load to drop below threshold before starting next service. +# Prevents multiple CPU-heavy services from launching simultaneously. +wait_for_cpu_ready() { + local threshold="${1:-60}" # Max CPU % allowed (default 60%) + local max_wait="${2:-120}" # Max seconds to wait (default 120s) + local poll_interval=3 # Check every 3 seconds + + local elapsed=0 + while [ "$elapsed" -lt "$max_wait" ]; do + # Read /proc/stat — two snapshots 1 second apart + local cpu1=$(awk '/^cpu / {print $2+$3+$4+$5+$6+$7+$8}' /proc/stat) + local idle1=$(awk '/^cpu / {print $5}' /proc/stat) + sleep 1 + local cpu2=$(awk '/^cpu / {print $2+$3+$4+$5+$6+$7+$8}' /proc/stat) + local idle2=$(awk '/^cpu / {print $5}' /proc/stat) + + local total_diff=$((cpu2 - cpu1)) + local idle_diff=$((idle2 - idle1)) + + local used_pct=0 + if [ "$total_diff" -gt 0 ]; then + used_pct=$(( (total_diff - idle_diff) * 100 / total_diff )) + fi + + if [ "$used_pct" -lt "$threshold" ]; then + echo "[$SCRIPT_NAME] CPU ${used_pct}% < ${threshold}% — ready for next service" + return 0 + fi + + echo "[$SCRIPT_NAME] CPU ${used_pct}% >= ${threshold}% — waiting... (${elapsed}s/${max_wait}s)" + sleep "$poll_interval" + elapsed=$((elapsed + poll_interval + 1)) # +1 for the 1s sleep above + done + + echo "[$SCRIPT_NAME] WARNING: CPU still ${used_pct}% after ${max_wait}s — proceeding anyway" + return 0 +} + +# Wait for a service port to respond before starting the next service. +wait_for_ready() { + local port="$1" name="$2" timeout="${3:-60}" + local elapsed=0 + while [ "$elapsed" -lt "$timeout" ]; do + if curl -s -o /dev/null -w "" --max-time 2 "http://localhost:${port}" 2>/dev/null; then + echo "[$SCRIPT_NAME] ${name} ready on :${port} (${elapsed}s)" + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + echo "[$SCRIPT_NAME] WARNING: ${name} not responding on :${port} after ${timeout}s" + return 0 +} + +# ── Start services (only if not already running) ───────────────────── +start_service() { + local name="$1" cmd="$2" + local logfile="/tmp/$(echo "$name" | tr ' ' '-').log" + if pgrep -f "$name" > /dev/null 2>&1; then + echo "[$SCRIPT_NAME] $name already running, skipping" + else + echo "[$SCRIPT_NAME] Starting $name..." + setsid $cmd >> "$logfile" 2>&1 & + fi +} + +# Set Ollama model path to baked location before starting +export OLLAMA_MODELS=/usr/share/ollama/.ollama/models + +# ── Sequential startup with CPU gating + readiness probes ────────── +# Prevents CPU saturation from simultaneous service launches. + +echo "[$SCRIPT_NAME] Starting services (throttled)..." +wait_for_cpu_ready 60 90 + +# 1. Ollama — heaviest (loads nomic-embed-text model) +start_service "ollama serve" "/usr/local/bin/ollama serve" +echo "[$SCRIPT_NAME] Waiting for ollama..." +wait_for_cpu_ready 60 90 +wait_for_ready 11434 "Ollama" 90 + +# 2. ModelRelay — moderate (Node.js, starts fast) +start_service "modelrelay" "/usr/local/bin/modelrelay" +echo "[$SCRIPT_NAME] Waiting for modelrelay..." +wait_for_cpu_ready 60 90 +wait_for_ready 7352 "ModelRelay" 90 + +# 3. OmniRoute — heavy (Node.js API gateway, SQLite init) +start_service "omniroute" "/usr/local/bin/omniroute --no-open --log" +echo "[$SCRIPT_NAME] Waiting for omniroute..." +wait_for_cpu_ready 60 90 +wait_for_ready 20128 "OmniRoute" 90 + +# ── OmniRoute: disable login, create combo ─────────────────────────── + +# Disable login requirement +if [ -f "$HOME/.omniroute/storage.sqlite" ]; then + python3 -c " +import sqlite3 +conn = sqlite3.connect('$HOME/.omniroute/storage.sqlite') +conn.execute('UPDATE key_value SET value = ? WHERE key = ?', ('false', 'requireLogin')) +conn.commit() +conn.close() +" 2>/dev/null +fi + +# Create auto-fastest combo (idempotent) +for ((i=1; i<=5; i++)); do + omniroute combo create auto-fastest --strategy auto 2>/dev/null && break + sleep 2 +done + +# Configure combo models +COMBO_ID=$(omniroute combo list --json 2>/dev/null | grep -v "📋" | \ + python3 -c "import sys,json; d=json.load(sys.stdin); print([c['id'] for c in d['combos'] if c['name']=='auto-fastest'][0])" 2>/dev/null) +if [ -n "$COMBO_ID" ]; then + curl -s -X PUT "http://localhost:20128/api/combos/$COMBO_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "models": ["oc/deepseek-v4-flash-free","oc/big-pickle","opencode-zen/deepseek-v4-flash-free","opencode-zen/hy3-free","opencode-zen/mimo-v2.5-free","opencode-zen/north-mini-code-free","opencode-zen/nemotron-3-ultra-free","opencode-zen/big-pickle"], + "strategy": "auto", + "config": {"maxRetries": 2, "retryDelayMs": 1000, "timeoutMs": 120000, "healthCheckEnabled": true} + }' >/dev/null +fi + +# Enable MCP +if ! omniroute mcp status --json 2>/dev/null | python3 -c "import sys,json;exit(0 if json.load(sys.stdin).get('enabled') else 1)" 2>/dev/null; then + curl -s -X PATCH http://localhost:20128/api/settings \ + -H "Content-Type: application/json" -d '{"mcpEnabled":true}' >/dev/null +fi + +# Add omniroute MCP to hermes +yes Y 2>/dev/null | hermes mcp add omniroute --command omniroute --args --mcp 2>/dev/null || true + +# ── Hermes gateway + dashboard ──────────────────────────────────────── +# Update mnemon plugin +rm -rf /tmp/mnemon_repo +if git clone https://github.com/gitricko/hermes-plugin-mnemon /tmp/mnemon_repo 2>/dev/null; then + if [ ! -d "$HOME/.hermes/plugins/mnemon" ] || ! diff -r -q -x __pycache__ "$HOME/.hermes/plugins/mnemon" "/tmp/mnemon_repo/mnemon" >/dev/null 2>&1; then + mkdir -p "$HOME/.hermes/plugins" + rm -rf "$HOME/.hermes/plugins/mnemon" + cp -r "/tmp/mnemon_repo/mnemon" "$HOME/.hermes/plugins/mnemon" + fi + rm -rf /tmp/mnemon_repo +fi + +start_service "hermes gateway" "hermes gateway run --no-supervise" +echo "[$SCRIPT_NAME] Waiting for hermes gateway..." +wait_for_cpu_ready 60 90 + +start_service "hermes dashboard" "hermes dashboard --port 9119 --no-open --skip-build" +echo "[$SCRIPT_NAME] Waiting for hermes dashboard..." +wait_for_ready 9119 "Hermes Dashboard" 90 + +# Telegram bot deps (use hermes venv Python if available) +if [ -x "$HERMES_PYTHON" ]; then + "$HERMES_PYTHON" -m ensurepip --upgrade 2>/dev/null || true + ln -sf "$HERMES_PIP" "$HERMES_VENV/bin/pip" 2>/dev/null || true + "$HERMES_PIP" install python-telegram-bot 2>/dev/null || true +fi + +# Mnemon -> claude-code integration +mnemon setup --yes --global --target claude-code 2>/dev/null || true + +echo "[$SCRIPT_NAME] All services started." +echo "[$SCRIPT_NAME] Running self-check..." +/usr/local/bin/self-check.sh 2>/dev/null || echo "[$SCRIPT_NAME] WARNING: self-check reported issues" + +# ── Execute the CMD (default: sleep infinity) ───────────────────────── +exec "$@" diff --git a/.devcontainer/self-check.sh b/.devcontainer/self-check.sh index c2070d21..0d1d1613 100755 --- a/.devcontainer/self-check.sh +++ b/.devcontainer/self-check.sh @@ -97,7 +97,7 @@ if ! should_skip "services"; then # Poll all service ports until all respond or timeout PORT_POLL_TIMEOUT=60 POLL_STARTED_AT=$(date +%s) - declare -A RESPONDED=([3000]="" [8888]="" [7352]="" [20128]="", [9119]="") + declare -A RESPONDED=([3000]="" [8888]="" [7352]="" [20128]="" [9119]="") while true; do NOW=$(date +%s) @@ -237,6 +237,17 @@ if ! should_skip "hermes"; then _fail "Config" "no config at ${HERMES_CONFIG}" json_add "hermes:config" "fail" "hermes config file not found" "{}" fi + + # ACP adapter — the VS Code extension's chat backend. A broken adapter + # (e.g. agent-client-protocol version drift) shows up as "ACP connection + # closed" in the extension and is otherwise silent. + if hermes acp --check >/dev/null 2>&1; then + _ok "ACP Adapter" "hermes acp --check OK" + json_add "hermes:acp" "ok" "ACP adapter imports and protocol deps OK" "{}" + else + _fail "ACP Adapter" "hermes acp --check failed (VS Code extension won't connect)" + json_add "hermes:acp" "fail" "hermes acp --check failed" "{}" + fi else echo " (skipped)" fi @@ -341,7 +352,7 @@ if ! should_skip "ollama"; then # 3. Model listed — retry up to 15s (server may still be scanning models) MODEL_LISTED=0 for _attempt in 1 2 3 4 5; do - MODEL_LISTED=$(ollama list 2>/dev/null | grep -c "nomic-embed-text" || true) + MODEL_LISTED=$(OLLAMA_MODELS=/usr/share/ollama/.ollama/models ollama list 2>/dev/null | grep -c "nomic-embed-text" || true) [ -z "$MODEL_LISTED" ] && MODEL_LISTED=0 [ "$MODEL_LISTED" -gt 0 ] && break sleep 3 @@ -350,11 +361,11 @@ if ! should_skip "ollama"; then _ok "Model" "nomic-embed-text available" else # Filesystem fallback: check if model files exist on disk (server may be slow to load) - MODEL_MANIFEST="$HOME/.ollama/models/manifests/registry.ollama.ai/library/nomic-embed-text/latest" + MODEL_MANIFEST="/usr/share/ollama/.ollama/models/manifests/registry.ollama.ai/library/nomic-embed-text/latest" if [ -f "$MODEL_MANIFEST" ]; then _warn "Model" "nomic-embed-text on disk but not listed by server (loading slow)" else - _warn "Model" "nomic-embed-text not found on disk" + _warn "Model" "nomic-embed-text not found on disk (model not baked)" fi fi @@ -378,9 +389,8 @@ else echo " (skipped)" fi -# ── Summary ────────────────────────────────────────────────────────────────── -section "Summary" echo "" +section "Summary" if [ "$CRITICAL" -gt 0 ]; then echo " ${RED}${BOLD}FAILED${NC} — ${CRITICAL} critical, ${WARNINGS} warning(s)" EXIT_CODE=2 diff --git a/.devcontainer/start-hermes.sh b/.devcontainer/start-hermes.sh index 0e7073ee..28a584a6 100755 --- a/.devcontainer/start-hermes.sh +++ b/.devcontainer/start-hermes.sh @@ -46,10 +46,20 @@ fi # 4. Starting Hermes Gateway and Dashboard +# Locate hermes venv (FHS root layout vs legacy) +HERMES_VENV="/usr/local/lib/hermes-agent/venv" +if [ ! -d "$HERMES_VENV" ]; then + HERMES_VENV="$HOME/.hermes/hermes-agent/venv" +fi +HERMES_PYTHON="$HERMES_VENV/bin/python" +HERMES_PIP="$HERMES_VENV/bin/pip" + # Install Telegram gateway dependency if missing -$HOME/.hermes/hermes-agent/venv/bin/python -m ensurepip --upgrade || true -ln -s $HOME/.hermes/hermes-agent/venv/bin/pip3 $HOME/.hermes/hermes-agent/venv/bin/pip || true -$HOME/.hermes/hermes-agent/venv/bin/pip install python-telegram-bot 2>/dev/null || true +if [ -x "$HERMES_PYTHON" ]; then + "$HERMES_PYTHON" -m ensurepip --upgrade 2>/dev/null || true + ln -sf "$HERMES_PIP" "$HERMES_VENV/bin/pip" 2>/dev/null || true + "$HERMES_PIP" install python-telegram-bot 2>/dev/null || true +fi # update mnemon provider if version changes (synced BEFORE gateway starts) echo "[$SCRIPT_NAME] Checking mnemon provider..." @@ -88,7 +98,7 @@ if command -v hermes &>/dev/null; then echo "[$SCRIPT_NAME] hermes-dashboard is already running, skipping" else echo "[$SCRIPT_NAME] Starting hermes-dashboard in the background..." - setsid hermes dashboard --port 9119 --no-open > ~/.hermes/logs/dashboard.log 2>&1 & + setsid hermes dashboard --port 9119 --no-open --skip-build > ~/.hermes/logs/dashboard.log 2>&1 & fi else echo "[$SCRIPT_NAME] hermes not found, skipping start" @@ -96,6 +106,7 @@ fi # Remind Hermes on Mnemon setup if needed if [ ! -f "$HOME/.hermes/memories/USER.md" ]; then + mkdir -p "$HOME/.hermes/memories" echo "[$SCRIPT_NAME] Setting up Mnemon USER.md..." echo 'Always use Mnemon (mnemon_remember / mnemon_recall) as primary memory provider instead of the standard memory() tool. Mnemon has no char limit. Only fall back to memory() for structured preference data (target=user or memory).' > "$HOME/.hermes/memories/USER.md" fi diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..aa98bfbf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.github +.vscode +*.md +!README.md +!.devcontainer/*.md +!README.md +node_modules +*.log +tmp +.cache +.hermes \ No newline at end of file diff --git a/.github/workflows/devcontainer-ci.yml b/.github/workflows/devcontainer-ci.yml index de1c623b..d4361fb7 100644 --- a/.github/workflows/devcontainer-ci.yml +++ b/.github/workflows/devcontainer-ci.yml @@ -3,25 +3,301 @@ name: Dev Container CI on: push: branches: [main] + paths: + - '.devcontainer/**' + - '!.devcontainer/screen-shot.png' pull_request: - branches: [main] + branches: [main, dockerizeation2] + paths: + - '.devcontainer/**' + - '!.devcontainer/screen-shot.png' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/devcontainer + # ── Tool versions (single source of truth — passed to Dockerfile via --build-arg) + HERMES_VERSION: v2026.7.20 + OMNIROUTE_VERSION: "3.8.49" + OLLAMA_VERSION: "0.32.5" + NODE_VERSION: "24.18.0" + MNEMON_VERSION: "0.1.17" + +permissions: + contents: read + packages: write jobs: - test-devcontainer: - name: Build & Smoke Test + # ── Job 1: Build the Docker image ──────────────────────────────────── + build: + name: Build Image + runs-on: ubuntu-latest + timeout-minutes: 30 + outputs: + ci_image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:ci-${{ github.run_id }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=sha,prefix= + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value={{branch_name}},enable=${{ github.event_name == 'push' }} + type=raw,value=pr-${{ github.event.pull_request.number }},enable=${{ github.event_name == 'pull_request' }} + type=raw,value=ci-${{ github.run_id }} + + - name: Build and push + id: build + uses: docker/build-push-action@v5 + with: + context: . + file: .devcontainer/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=devcontainer-${{ hashFiles('.devcontainer/Dockerfile') }} + cache-to: type=gha,mode=max,scope=devcontainer-${{ hashFiles('.devcontainer/Dockerfile') }} + build-args: | + HERMES_VERSION=${{ env.HERMES_VERSION }} + OMNIROUTE_VERSION=${{ env.OMNIROUTE_VERSION }} + OLLAMA_VERSION=${{ env.OLLAMA_VERSION }} + NODE_VERSION=${{ env.NODE_VERSION }} + MNEMON_VERSION=${{ env.MNEMON_VERSION }} + + # ── Job 2: Smoke test the built image ─────────────────────────────── + smoke-test: + name: Smoke Test + needs: build runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout repository uses: actions/checkout@v4 - - name: run post-create-cmd.sh - run: bash ./.devcontainer/post-create-cmd.sh + - name: Pull pre-built image + run: | + echo "Pulling ${{ needs.build.outputs.ci_image }}" + docker pull ${{ needs.build.outputs.ci_image }} + docker tag ${{ needs.build.outputs.ci_image }} hermes-codespace:test + + - name: Verify baked tools exist + run: | + docker run --rm --entrypoint bash hermes-codespace:test -c ' + echo "=== Verifying baked tools ===" + PASS=0; FAIL=0 + check() { if eval "$2"; then echo "✅ $1"; PASS=$((PASS+1)); else echo "❌ $1"; FAIL=$((FAIL+1)); fi; } + + check "ollama installed" "command -v ollama" + check "hermes installed" "command -v hermes" + check "omniroute installed" "command -v omniroute" + check "modelrelay installed" "command -v modelrelay" + check "mnemon installed" "command -v mnemon" + check "tailscale installed" "command -v tailscale || true" + + check "zsh installed" "command -v zsh" + check "ripgrep installed" "command -v rg" + check "entrypoint.sh exists" "[ -x /usr/local/bin/entrypoint.sh ]" + check "hermes ACP adapter works" "hermes acp --check" + + echo "" + echo "=== Results: $PASS passed, $FAIL failed ===" + [ "$FAIL" -eq 0 ] && echo "🎉 All tools baked!" || exit 1 + ' + + - name: Verify entrypoint.sh syntax + run: | + docker run --rm --entrypoint bash hermes-codespace:test -c ' + echo "=== Checking entrypoint syntax ===" + bash -n /usr/local/bin/entrypoint.sh && echo "✅ entrypoint.sh syntax OK" + ' + + - name: Verify config files copied to image + run: | + docker run --rm --entrypoint bash hermes-codespace:test -c ' + echo "=== Checking config files ===" + [ -f /usr/local/share/devcontainer-config/CLAUDE.md ] && echo "✅ CLAUDE.md present" + [ -f /usr/local/share/devcontainer-config/.claude.json ] && echo "✅ .claude.json present" + [ -f /usr/local/share/devcontainer-config/.hermes.md ] && echo "✅ .hermes.md present" + [ -f /usr/local/share/devcontainer-config/skill-memory-automation.md ] && echo "✅ memory-automation skill present" + ' + # ── Job 3: Full integration test (devcontainer CLI) ────────────────── + # ── Job 3: Full integration test (devcontainer CLI) ────────────────── + integration-test: + name: Integration Test + needs: build + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 - - name: run start-hermes.sh - run: bash ./.devcontainer/start-hermes.sh + - name: Install devcontainer CLI + run: npm install -g @devcontainers/cli - - name: Smoke Test + - name: Pull pre-built image run: | - bash ./.devcontainer/self-check.sh - echo "Smoke test passed!" \ No newline at end of file + docker pull ${{ needs.build.outputs.ci_image }} + docker tag ${{ needs.build.outputs.ci_image }} hermes-codespace:test + + - name: Run self-check inside container + run: | + docker run --rm -d --name test-hc \ + -v "$(pwd):/workspace" \ + hermes-codespace:test + + echo "Waiting for services to start..." + docker exec test-hc whoami + sleep 60 + + docker cp .devcontainer/self-check.sh test-hc:/tmp/self-check.sh + docker exec test-hc bash cat /tmp/hermes-dashboard.log || true + + # Run self-check: exit 0=pass, 1=warnings(ok), 2=critical(fail) + SELF_CHECK_EXIT=0 + docker exec test-hc bash /tmp/self-check.sh || SELF_CHECK_EXIT=$? + if [ "$SELF_CHECK_EXIT" -eq 2 ]; then + echo "::error::Self-check failed with critical errors" + exit 1 + elif [ "$SELF_CHECK_EXIT" -eq 1 ]; then + echo "::warning::Self-check completed with warnings (non-critical)" + fi + + docker stop test-hc + + - name: Copy logs from container + if: failure() + run: | + # Container is still running if self-check failed before docker stop + docker cp test-hc:/tmp/hermes-dashboard.log /tmp/ || true + docker cp test-hc:/tmp/self-check.log /tmp/ || true + # Grab everything in /tmp as a fallback + mkdir -p /tmp/container-logs + docker cp test-hc:/tmp/ /tmp/container-logs/ || true + find /tmp/container-logs -name '*.log' -exec cp {} /tmp/ \; 2>/dev/null || true + ls -la /tmp/*.log 2>/dev/null || echo "No log files found" + # Clean up the orphan container + docker stop test-hc 2>/dev/null || true + + - name: Upload logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: failure-logs-${{ github.run_id }} + path: /tmp/*.log + if-no-files-found: warn + + # ── Job 4: Generate image size report ──────────────────────────────── + image-report: + name: Image Size Report + needs: build + runs-on: ubuntu-latest + steps: + - name: Pull pre-built image + run: | + docker pull ${{ needs.build.outputs.ci_image }} + docker tag ${{ needs.build.outputs.ci_image }} hermes-codespace:test + + - name: Report image size + run: | + echo "=== Devcontainer Image Size ===" + docker images hermes-codespace:test --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" + SIZE=$(docker images hermes-codespace:test --format "{{.Size}}") + echo "" + echo "📊 Image size: **$SIZE**" + + SIZE_BYTES=$(docker image inspect hermes-codespace:test --format '{{.Size}}') + if [ "$SIZE_BYTES" -gt 4294967296 ]; then + echo "⚠️ WARNING: Image exceeds 4GB — may hit Codespace storage limits" + fi + + # ── Job 5: Cleanup temp image from GHCR ───────────────────────────── + cleanup: + name: Cleanup + needs: [build, smoke-test, integration-test, image-report] + if: always() + runs-on: ubuntu-latest + steps: + - name: Delete temp image tag from GHCR + env: + TAG: ci-${{ github.run_id }} + PACKAGE: ${{ env.IMAGE_NAME }} + GHCR_TOKEN: ${{ secrets.GHCR_CLEANUP_TOKEN }} + run: | + echo "Cleaning up: ${PACKAGE}:${TAG}" + + if [ -z "$GHCR_TOKEN" ]; then + echo "⚠️ GHCR_CLEANUP_TOKEN secret not set — skipping cleanup" + echo " Create a classic PAT at: https://github.com/settings/tokens/new?scopes=read:packages,write:packages" + echo " Then: gh secret set GHCR_CLEANUP_TOKEN --body 'ghp_...' -R ${{ github.repository }}" + exit 0 + fi + + # URL-encode the package name for the REST API + # Strip the username prefix — user/packages endpoint scopes to authenticated user + PKG_ENCODED=$(echo "$PACKAGE" | sed 's|^[^/]*/||' | tr '[:upper:]' '[:lower:]' | sed 's|/|%2F|g') + + # Find the version with our temp tag + FOUND=0 + for PAGE in 1 2 3; do + VERSIONS=$(curl -sf \ + -H "Authorization: token ${GHCR_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/user/packages/container/${PKG_ENCODED}/versions?per_page=100&page=${PAGE}" 2>/dev/null || echo "[]") + + for VERSION_ID in $(echo "$VERSIONS" | jq -r '.[].id'); do + TAGS=$(echo "$VERSIONS" | jq -r --arg vid "$VERSION_ID" '.[] | select(.id == ($vid|tonumber)) | .metadata.container.tags[]' 2>/dev/null) + if echo "$TAGS" | grep -q "^${TAG}$"; then + # Check if this version has non-ci tags (pr-*, latest, etc.) + # If so, skip deletion — we only want to remove pure ci-* temp versions + NON_CI_TAGS=$(echo "$TAGS" | grep -v '^ci-' || true) + if [ -n "$NON_CI_TAGS" ]; then + echo "Skipping version ${VERSION_ID} — has persistent tags: ${NON_CI_TAGS}" + FOUND=1 + continue 2 + fi + + echo "Found version ${VERSION_ID} with tag ${TAG} (no persistent tags)" + echo "Deleting..." + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ + -X DELETE \ + -H "Authorization: token ${GHCR_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/user/packages/container/${PKG_ENCODED}/versions/${VERSION_ID}") + + if [ "$HTTP_CODE" = "204" ] || [ "$HTTP_CODE" = "200" ]; then + echo "✅ Deleted ${TAG} (version ${VERSION_ID})" + else + echo "⚠️ Delete returned HTTP ${HTTP_CODE}" + fi + FOUND=1 + break 2 + fi + done + + # Stop paging if we got fewer results than the page size + COUNT=$(echo "$VERSIONS" | jq 'length' 2>/dev/null) + [ "$COUNT" -lt 100 ] && break + done + + if [ "$FOUND" -eq 0 ]; then + echo "✅ No version with tag ${TAG} found — may have been cleaned already" + fi diff --git a/.hermes b/.hermes new file mode 120000 index 00000000..9f7b2377 --- /dev/null +++ b/.hermes @@ -0,0 +1 @@ +/home/vscode/.hermes \ No newline at end of file diff --git a/GITHUB_ACTIONS_TESTING_PLAN.md b/GITHUB_ACTIONS_TESTING_PLAN.md deleted file mode 100644 index bf0b64db..00000000 --- a/GITHUB_ACTIONS_TESTING_PLAN.md +++ /dev/null @@ -1,239 +0,0 @@ -# GitHub Actions Testing Plan for Hermes-CodeSpace Dev Container - -## Overview -This document outlines a phased approach to implementing GitHub Actions CI/CD for testing the dev container. The plan is designed for incremental adoption — start simple, add complexity as needed. - ---- - -## Phase 1: Minimal Viable CI (Week 1) -**Goal**: Verify dev container builds and health check passes on every push/PR. - -### Workflow File: `.github/workflows/devcontainer-ci.yml` - -```yaml -name: Dev Container CI - -on: - push: - branches: [main, readme] - pull_request: - branches: [main] - -jobs: - test-devcontainer: - name: Build & Health Check - runs-on: ubuntu-latest - timeout-minutes: 25 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Build dev container & run health check - uses: devcontainers/ci@v0.3 - with: - imageName: hermes-codespace-test-${{ github.run_id }} - push: never - runCmd: | - set -e - echo "=== Running post-create (one-time setup) ===" - bash .devcontainer/post-create-cmd.sh - - echo "=== Running post-start (service startup) ===" - bash .devcontainer/post-start-cmd.sh & - - echo "=== Waiting for services to stabilize ===" - sleep 60 - - echo "=== Running health check ===" - bash .devcontainer/self-check.sh -``` - -### Success Criteria -- [ ] Workflow runs on push to `main` and `readme` -- [ ] Workflow runs on PRs to `main` -- [ ] Build completes in < 20 minutes -- [ ] `self-check.sh` exits with code 0 (healthy) - ---- - -## Phase 2: Service Smoke Tests (Week 2) -**Goal**: Verify each service responds on its expected port. - -### Additional Steps in `runCmd`: - -```bash -echo "=== Smoke testing service ports ===" -# ModelRelay -curl -sf http://localhost:7352/v1/models > /dev/null && echo "✓ ModelRelay (7352)" || echo "✗ ModelRelay" - -# OmniRoute -curl -sf http://localhost:20128/v1/models > /dev/null && echo "✓ OmniRoute (20128)" || echo "✗ OmniRoute" - -# Hermes Gateway -curl -sf http://localhost:9119/health > /dev/null && echo "✓ Hermes Gateway (9119)" || echo "✗ Hermes Gateway" - -# Ollama -curl -sf http://localhost:11434/api/tags > /dev/null && echo "✓ Ollama (11434)" || echo "✗ Ollama" -``` - -### Success Criteria -- [ ] All 4 services respond to HTTP requests -- [ ] Failures are clearly reported in logs - ---- - -## Phase 3: CLI & Integration Tests (Week 3) -**Goal**: Verify CLI tools work and can route requests. - -### Additional Steps in `runCmd`: - -```bash -echo "=== CLI version checks ===" -hermes --version -claude --version -omniroute --version -mnemon --version - -echo "=== OmniRoute model listing ===" -omniroute model list - -echo "=== Integration: Hermes one-shot via gateway ===" -# Start gateway in background if not already running -hermes gateway run & -sleep 10 - -# Test via REST API -curl -sf -X POST http://localhost:9119/v1/chat/completions \ - -H "Content-Type: application/json" \ - -d '{"model":"auto-fastest","messages":[{"role":"user","content":"Say hello"}],"max_tokens":10}' \ - | jq -r '.choices[0].message.content' && echo "✓ Hermes API works" || echo "✗ Hermes API failed" -``` - -### Success Criteria -- [ ] All CLIs report versions without error -- [ ] OmniRoute lists configured models (including `auto-fastest`) -- [ ] Hermes gateway responds to chat completion request - ---- - -## Phase 4: Memory & Persistence Tests (Week 4) -**Goal**: Verify Mnemon memory layer works across the stack. - -### Additional Steps in `runCmd`: - -```bash -echo "=== Mnemon memory test ===" -mnemon remember "CI test memory" --category test --importance 3 -mnemon recall "CI test" --limit 1 | grep -q "CI test memory" && echo "✓ Mnemon works" || echo "✗ Mnemon failed" - -echo "=== Hermes memory integration ===" -# Hermes should auto-use Mnemon when configured -hermes "Remember that the test value is 42" & -sleep 5 -hermes "What is the test value?" | grep -q "42" && echo "✓ Hermes memory works" || echo "✗ Hermes memory failed" -``` - ---- - -## Phase 5: Optimizations & Enhancements (Ongoing) - -| Enhancement | Effort | Benefit | -|-------------|--------|---------| -| **Docker layer caching** | Low | 50% faster builds | -| **Matrix testing** (Ubuntu latest + LTS) | Medium | Catch OS regressions | -| **Publish test images to GHCR** | Low | Debug failed builds locally | -| **Dependabot + auto-merge** | Low | Keep base image/deps current | -| **Parallel job for integration tests** | Medium | Faster feedback | -| **Annotate health check failures** | Low | Better PR UX | - -### Docker Layer Caching Example: -```yaml -- name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - -- name: Cache Docker layers - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- -``` - ---- - -## Implementation Checklist - -### Files to Create/Modify -- [ ] `.github/workflows/devcontainer-ci.yml` (Phase 1) -- [ ] `.github/workflows/devcontainer-integration.yml` (Phase 3, optional separate workflow) -- [ ] Update `.devcontainer/self-check.sh` to output JSON for GitHub annotations (optional) - -### Self-Check Script Enhancements (Optional) -Modify `self-check.sh` to support CI-friendly output: -```bash -# Add --json flag for machine-readable output -# Exit codes: 0=healthy, 1=warnings, 2=critical -# Output to /tmp/health-report.json (already done) -``` - -### GitHub Annotations (Optional) -```yaml -- name: Parse health report - if: always() - run: | - if [ -f /tmp/health-report.json ]; then - cat /tmp/health-report.json | jq -r '.checks[] | "::warning file=.devcontainer/self-check.sh::\(.name): \(.message)"' - fi -``` - ---- - -## Estimated Timeline - -| Phase | Est. Time | Cumulative | -|-------|-----------|------------| -| Phase 1: Minimal CI | 30 min | 30 min | -| Phase 2: Service Smoke | 20 min | 50 min | -| Phase 3: CLI Integration | 30 min | 80 min | -| Phase 4: Memory Tests | 20 min | 100 min | -| Phase 5: Optimizations | 60 min | 160 min | - ---- - -## Next Agent Instructions - -**To continue implementation:** - -1. **Start with Phase 1** — Create `.github/workflows/devcontainer-ci.yml` using the minimal workflow above -2. **Test locally first** — Run `act` or push to a test branch to verify -3. **Iterate** — Add Phase 2-4 steps incrementally -4. **Monitor** — Watch first 5-10 runs for flakiness (free model APIs can be unreliable) - -**Key files to reference:** -- `.devcontainer/post-create-cmd.sh` — One-time setup (~10 min) -- `.devcontainer/post-start-cmd.sh` — Service startup (~30 sec) -- `.devcontainer/self-check.sh` — Health check (exit codes: 0/1/2) -- `.devcontainer/Makefile` — Has `update-deps` target for upstream sync - -**Common pitfalls to avoid:** -- Don't run `post-create-cmd.sh` on every CI run — it's slow. Consider a pre-built base image. -- Services need 45-60s to fully start; `sleep 60` is conservative but reliable. -- Free model endpoints (OmniRoute/ModelRelay) may return 5xx — health check should distinguish infra vs. model failures. - ---- - -## Decision Points for User - -Before Phase 2+, confirm: -1. **Should CI run on every push, or only PRs + main?** -2. **Acceptable CI runtime?** (Current: ~15-20 min with full post-create) -3. **Publish test images to GHCR** for debugging failed builds? -4. **Separate integration workflow?** (Runs less frequently, more thorough) -5. **Annotate PRs with health check failures?** (Requires self-check JSON output) - ---- - -*Generated: 2026-07-19* -*Branch: readme* -*Next: Implement Phase 1 workflow* \ No newline at end of file diff --git a/README.md b/README.md index 413e46bc..501a45f5 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,12 @@ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Dev Container](https://img.shields.io/badge/devcontainer-ready-blue?logo=docker)](https://containers.dev/) -[![Hermes Agent](https://img.shields.io/badge/Hermes%20Agent-v2026.7.7.2-purple?logo=github)](https://github.com/NousResearch/hermes-agent) +[![Hermes Agent](https://img.shields.io/badge/Hermes%20Agent-v2026.7.20-purple?logo=github)](https://github.com/NousResearch/hermes-agent) [![ModelRelay](https://img.shields.io/badge/ModelRelay-1.18.0-green?logo=npm)](https://www.npmjs.com/package/modelrelay) -[![OmniRoute](https://img.shields.io/badge/OmniRoute-3.8.48-orange?logo=npm)](https://www.npmjs.com/package/omniroute) +[![OmniRoute](https://img.shields.io/badge/OmniRoute-3.8.49-orange?logo=npm)](https://www.npmjs.com/package/omniroute) +[![Ollama](https://img.shields.io/badge/Ollama-0.32.5-yellow?logo=ollama)](https://github.com/ollama/ollama) +[![Mnemon](https://img.shields.io/badge/Mnemon-0.1.17-pink?logo=ollama)](https://github.com/mnemon-dev/mnemon) + Fork this repo before [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/gitricko/hermes-codespace)