From 0d866b4a7c4041e8c4d15fe997c82900cc4c4060 Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 09:58:08 +0000 Subject: [PATCH 01/24] Refactor devcontainer setup: replace post-create script with minions installation, add boot and self-check commands, and remove unused files --- .devcontainer/Makefile | 19 ++++++------ .devcontainer/devcontainer.json | 3 +- .devcontainer/globalState.json | 51 --------------------------------- .devcontainer/post-create.sh | 10 ------- .devcontainer/secrets.json | 3 -- 5 files changed, 11 insertions(+), 75 deletions(-) delete mode 100644 .devcontainer/globalState.json delete mode 100755 .devcontainer/post-create.sh delete mode 100644 .devcontainer/secrets.json diff --git a/.devcontainer/Makefile b/.devcontainer/Makefile index 55382b2..1153093 100644 --- a/.devcontainer/Makefile +++ b/.devcontainer/Makefile @@ -8,16 +8,15 @@ EXTERNAL_DEVC := $(EXTERNAL_DIR)/.devcontainer # === Development Commands === -install: - @echo "Installing dependencies..." - @bash post-create-cmd.sh - -ext-install: - @echo "Installing external dependencies..." - mkdir -p ~/.cline/data - cp globalState.json ~/.cline/data/ - cp secrets.json ~/.cline/data/ - code --force --install-extension saoudrizwan.claude-dev +boot-minions: + ~/.minions/boot.sh + +self-check: + ~/.minions/self-check.sh + +version-check: + ./scripts/admin-update.sh --dry-run + # === Template Update Commands === # Use this command to update your .devcontainer folder with latest from upstream diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bd89a8d..f921e67 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,5 +5,6 @@ "extensions": ["joaompfp.hermes-ai-agent"] } }, - "postCreateCommand": "bash ./.devcontainer/free-disk.sh && bash ./.devcontainer/post-create.sh > /tmp/post-create.log" + "postCreateCommand": "bash ./.devcontainer/free-disk.sh && ( curl -fsSL https://github.com/gitricko/.minions/raw/refs/heads/main/install.sh | bash ) >> /tmp/.minions.log 2>&1 ", + "postStartCommand": "( bash $HOME/.minions/boot.sh ) >> /tmp/.minions.log 2>&1" } diff --git a/.devcontainer/globalState.json b/.devcontainer/globalState.json deleted file mode 100644 index 9fd11f9..0000000 --- a/.devcontainer/globalState.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "welcomeViewCompleted": true, - "__vscodeMigrationVersion": 1, - "clineVersion": "3.83.0", - "remoteRulesToggles": {}, - "remoteWorkflowToggles": {}, - "remoteSkillsToggles": {}, - "openAiHeaders": {}, - "sapAiCoreUseOrchestrationMode": true, - "ocaMode": "internal", - "planModeApiProvider": "openai", - "actModeApiProvider": "openai", - "openAiBaseUrl": "http://localhost:7352/v1", - "planModeOpenAiModelId": "auto-fastest", - "actModeOpenAiModelId": "auto-fastest", - "azureApiVersion": "", - "lastShownAnnouncementId": "3.83", - "dismissedBanners": [ - { - "bannerId": "bnr-01KQ02NN40X7WTJMKQC610GS5M", - "dismissedAt": 1778856185168 - } - ], - "autoApprovalSettings": { - "version": 2, - "enabled": true, - "favorites": [], - "maxRequests": 20, - "actions": { - "readFiles": true, - "readFilesExternally": false, - "editFiles": false, - "editFilesExternally": false, - "executeSafeCommands": true, - "executeAllCommands": false, - "useBrowser": false, - "useMcp": true - }, - "enableNotifications": false - }, - "workspaceRoots": [ - { - "path": "/config/Desktop", - "name": "Desktop", - "vcs": "none" - } - ], - "primaryRootIndex": 0, - "globalWorkflowToggles": {}, - "globalClineRulesToggles": {} -} \ No newline at end of file diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh deleted file mode 100755 index bf7f97e..0000000 --- a/.devcontainer/post-create.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# Install Cline with default configuration -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -echo "[post-create-cmd.sh] Installing Cline with default configuration..." -mkdir -p "$HOME/.cline/data" -cp "${SCRIPT_DIR}/globalState.json" "$HOME/.cline/data/globalState.json" -cp "${SCRIPT_DIR}/secrets.json" "$HOME/.cline/data/secrets.json" -bash -c 'code --force --install-extension saoudrizwan.claude-dev' -npm install -g cline diff --git a/.devcontainer/secrets.json b/.devcontainer/secrets.json deleted file mode 100644 index 8fa649d..0000000 --- a/.devcontainer/secrets.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "openAiApiKey": " " -} \ No newline at end of file From 44c61b8a386ea5ba836c5c4fcad8d2b0e9fa081a Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 11:41:33 +0000 Subject: [PATCH 02/24] =?UTF-8?q?feat:=20migrate=20ModelRelay=20=E2=86=92?= =?UTF-8?q?=209Router=20(port=207352=20stays)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - npm: modelrelay@1.22.1 → 9router@0.5.81 (verified on npm registry) - 2 renames: ModelRelay.desktop → 9Router.desktop, start-modelrelay.sh → start-ninerouter.sh - 7352 port preserved end-to-end (Dockerfile, docker-compose, pi-models, self-check, docs) - Launch: setsid modelrelay --disable → nohup 9router --host 0.0.0.0 --port 7352 --no-browser --skip-update - New docker/9router-config.sh: REST API combo setup (login, disable auth, create auto-fastest with 8 free oc/ models, smoke test) - Docs: architecture.md, README, excalidraw all updated to 9Router - Verified: zero remaining modelrelay refs in repo, npm package confirmed live Reference: PR #57 (gitricko/hermes-codespace) — canonical rename mapping Issue: #52 --- README.md | 14 +++--- docker-compose.yml | 2 +- docker/9Router.desktop | 8 +++ docker/9router-config.sh | 73 ++++++++++++++++++++++++++++ docker/Dockerfile | 10 ++-- docker/ModelRelay.desktop | 8 --- docker/pi-models.json | 10 ++-- docker/self-check.sh | 4 +- docker/start-hermes.sh | 6 +-- docker/start-modelrelay.sh | 26 ---------- docker/start-ninerouter.sh | 25 ++++++++++ docs/architecture-diagram.excalidraw | 28 +++++------ docs/architecture.md | 48 +++++++++--------- 13 files changed, 167 insertions(+), 95 deletions(-) create mode 100644 docker/9Router.desktop create mode 100755 docker/9router-config.sh delete mode 100644 docker/ModelRelay.desktop delete mode 100755 docker/start-modelrelay.sh create mode 100755 docker/start-ninerouter.sh diff --git a/README.md b/README.md index 8f5caea..f2d3091 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 🪽 Hermes — Web Top -_Run Hermes-Agent inside a browser-based VSCode + Linux desktop with free LLM support through ModelRelay._ +_Run Hermes-Agent inside a browser-based VSCode + Linux desktop with free LLM support through _9Router_._

@@ -29,7 +29,7 @@ Just open this repo in a GitHub Codespace, start the container and you will get: - A complete Ubuntu MATE desktop (WebTop) for computer use - CodeServer at port 8888 with Hermes + Claude Code (cli + vscode extension) installed and preconfigured - Ollama server pre-installed and auto-started -- ModelRelay pre-installed, auto-started and pre-configured as default model for Hermes and Claude Code +- _9Router_ pre-installed, auto-started and pre-configured as default model for Hermes and Claude Code - **Pi coding agent** pre-installed, configured to proxy through OmniRoute (`auto-fastest`) — desktop launcher + default model - Hermes gateway accessible via desktop launcher - [Mnemon](https://github.com/mnemon-dev/mnemon) as your Hermes default [memory provider](https://github.com/gitricko/hermes-plugin-mnemon) @@ -46,7 +46,7 @@ The only catch? You normally need a dedicated machine with GPU. Perfect for: - Trying Hermes-Agent risk-free -- Free LLM APIs through [ModelRelay](https://github.com/ellipticmarketing/modelrelay) or [OmniRoute](https://github.com/diegosouzapw/OmniRoute) +- Free LLM APIs through [_9Router_](https://github.com/decolua/9router) or [OmniRoute](https://github.com/diegosouzapw/OmniRoute) - Students / hackers / evaluators - Anyone who wants a personal AI assistant without breaking the bank @@ -91,17 +91,17 @@ Perfect for: ## 🔧 Features - **Zero local install** — everything runs in browser via GitHub Codespaces -- **Free-tier friendly** — uses ModelRelay, Ollama daily cloud credits or NVIDIA Build API fallback +- **Free-tier friendly** — uses _9Router_, Ollama daily cloud credits or NVIDIA Build API fallback - **Persistent config** — docker volume backup and restore after Codespace recreation - **Easy backup/restore** — `make backup` / `make restore` - **One-command everything** — powerful Makefile + clean `docker-compose.yml` -- **Auto-start ModelRelay** — Default configuration for Free LLM API to Hermes +- **Auto-start _9Router_** — Default configuration for Free LLM API to Hermes - **Auto-start OmniRoute** — You need some configuration before it work but it is flexible and powerful - **Pi coding agent** — pre-installed CLI; default model set to `omniroute` (`auto-fastest`). Launch it from the **Pi Agent** desktop icon or run `pi` / `pi -p "prompt"` in a terminal. Config lives in `~/.pi/agent/` (seeded once, then user-editable). - **Auto-start Ollama** — custom init script on WebTop boot - **Colima / local Docker support** ready - **Built-in code-server IDE** — browser-based VS Code on port `8888` -- **Multiple AI VSCode Extension preinstall/config with ModelRelay** - Cline, Hermes and ClaudeCode +- **Multiple AI VSCode Extension preinstall/config with _9Router_** - Cline, Hermes and ClaudeCode - **Mnemon as default memory provider** - a knowledge store for Hermes with intent-aware recall, importance decay, and auto-deduplication ## 🧑‍💻 Built-in code-server IDE (VSCode on the Web - Interface to Agent) @@ -116,7 +116,7 @@ This image includes `code-server` and exposes it on port `8888`. > Note: this setup may use `code-server --auth none` in development, so keep port `8888` private. For local production use, secure it with an authenticated reverse proxy or firewall. - Hermes Agent's Extension is preinstalled and configured in VSCode -- Claude Code Extension is also preinstall and configured to ModelRelay +- Claude Code Extension is also preinstall and configured to _9Router_ - Start Hacking away in VSCode, use WebTop if you need to monitor agent do desktop-use operations. eg: Non-Headless Chrome debugging for instance / Linux Computer-Use ## 🔒 Security: Protected by GitHub Authentication diff --git a/docker-compose.yml b/docker-compose.yml index 89e05be..b12c0d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,7 +34,7 @@ services: - 9119:9119 # hermes api server (if enabled) - 8642:8642 - # modelrelay specific ports (adjust as needed) + # 9Router specific ports (adjust as needed) - 7352:7352 # OmniRoute specific ports (adjust as needed) - 20128:20128 diff --git a/docker/9Router.desktop b/docker/9Router.desktop new file mode 100644 index 0000000..301c8f7 --- /dev/null +++ b/docker/9Router.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Terminal=false +Exec=mate-terminal --title="9Router" -e "bash -c '9router --host 0.0.0.0 --host 127.0.0.1 --port 7352 --no-browser --skip-update; exec bash'" +Name[en_US]=9Router +Name=9Router +GenericName[en_US.UTF-8]=9Router diff --git a/docker/9router-config.sh b/docker/9router-config.sh new file mode 100755 index 0000000..cf1c4be --- /dev/null +++ b/docker/9router-config.sh @@ -0,0 +1,73 @@ +#!/bin/bash +set -euo pipefail + +BASE_URL="http://localhost:7352" + +# 1) login with password 123456 via POST /api/auth/login +echo "[9router-config] Logging in with password..." +LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/api/auth/login" -H "Content-Type: application/json" -d '{"password":"123456"}') +TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.token // empty') + +if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then + echo "[9router-config] Error: Failed to login, no token received" + echo "[9router-config] Response: $LOGIN_RESPONSE" + exit 1 +fi + +echo "[9router-config] Login successful" +AUTH="Authorization: Bearer $TOKEN" + +# 2) disable requireLogin and requireApiKey via PATCH /api/settings +echo "[9router-config] Disabling requireLogin and requireApiKey..." +curl -s -X PATCH "$BASE_URL/api/settings" -H "Content-Type: application/json" -H "$AUTH" -d '{"requireLogin":false,"requireApiKey":false}' > /dev/null +echo "[9router-config] Settings updated: requireLogin=false, requireApiKey=false" + +# 3) delete existing auto-fastest combo if present +echo "[9router-config] Deleting existing auto-fastest combo if present..." +curl -s -X DELETE "$BASE_URL/api/combos/auto-fastest" \ + -H "Content-Type: application/json" \ + -H "$AUTH" > /dev/null || true +echo "[9router-config] Existing combo deleted (or did not exist)" + +# 4) create new auto-fastest combo with 8 free oc/ models +echo "[9router-config] Creating new auto-fastest combo with 8 free oc/ models..." + +MODELS='["oc/muse-spark-1.2","oc/muse-spark-1.3","oc/union-alpha","oc/big-pickle","oc/mimo-v2.5-free","oc/ling-3.0-flash-fin-free","oc/nemotron-3-ultra-free","oc/nemotron-3.5-lightning-free"]' + +CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/api/combos" -H "Content-Type: application/json" -H "$AUTH" -d "{\"name\":\"auto-fastest\",\"models\":$MODELS}") + +echo "$CREATE_RESPONSE" | jq -e '.name // empty' > /dev/null || { + echo "[9router-config] Error: Failed to create combo" + echo "[9router-config] Response: $CREATE_RESPONSE" + exit 1 +} +echo "[9router-config] Combo auto-fastest created successfully" + +# 5) set round-robin fallback strategy +# 6) PATCH settings with comboStrategies +echo "[9router-config] Setting round-robin fallback strategy via comboStrategies..." +curl -s -X PATCH "$BASE_URL/api/settings" \ + -H "Content-Type: application/json" \ + -H "$AUTH" \ + -d '{"comboStrategies":{"fallback":"round-robin"}}' > /dev/null +echo "[9router-config] comboStrategies.fallback set to round-robin" + +# 7) smoke test via /v1/chat/completions +echo "[9router-config] Running smoke test via /v1/chat/completions..." +SMOKE_RESPONSE=$(curl -s -X POST "$BASE_URL/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -H "$AUTH" \ + -d '{"model":"auto-fastest","messages":[{"role":"user","content":"hello"}]}') + +SMOKE_CONTENT=$(echo "$SMOKE_RESPONSE" | jq -r '.choices[0].message.content // empty') + +if [ -n "$SMOKE_CONTENT" ]; then + echo "[9router-config] Smoke test PASSED - received response from model" + echo "[9router-config] Response preview: ${SMOKE_CONTENT:0:100}" +else + echo "[9router-config] Smoke test FAILED - no content in response" + echo "[9router-config] Response: $SMOKE_RESPONSE" + exit 1 +fi + +echo "[9router-config] 9Router configuration complete!" \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index efd219a..12d9f8e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ ARG HERMES_VERSION="v2026.9.14" ARG OMNIROUTE_VERSION=3.8.50 -ARG MODELRELAY_VERSION=1.22.1 +ARG NINEROUTER_VERSION=0.5.81 ARG OLLAMA_VERSION=0.34.1 ARG NODE_VERSION=26.7.0 ARG CODE_SERVER_VERSION=4.137.0 @@ -61,11 +61,11 @@ RUN curl -fsSL "https://raw.githubusercontent.com/NousResearch/hermes-agent/${HE CI=1 npm run build -w web --silent 2>&1 && \ npm cache clean --force -# Install ModelRelay and start automatically in the background -ARG MODELRELAY_VERSION -RUN npm install -g modelrelay@${MODELRELAY_VERSION} && \ +# Install 9Router and start automatically in the background +ARG NINEROUTER_VERSION +RUN npm install -g 9router@${NINEROUTER_VERSION} && \ npm cache clean --force -# RUN npm install -g github:gitricko/modelrelay && \ +# RUN npm install -g github:gitricko/9router && \ # Install OmniRoute and start automatically when desktop loads ARG OMNIROUTE_VERSION diff --git a/docker/ModelRelay.desktop b/docker/ModelRelay.desktop deleted file mode 100644 index 367ff64..0000000 --- a/docker/ModelRelay.desktop +++ /dev/null @@ -1,8 +0,0 @@ -[Desktop Entry] -Version=1.0 -Type=Application -Terminal=false -Exec=mate-terminal --title="ModelRelay" -e "bash -c 'modelrelay --disable; modelrelay; exec bash'" -Name[en_US]=ModelRelay -Name=ModelRelay -GenericName[en_US.UTF-8]=ModelRelay diff --git a/docker/pi-models.json b/docker/pi-models.json index 34dcd1d..ff14d61 100644 --- a/docker/pi-models.json +++ b/docker/pi-models.json @@ -7,7 +7,7 @@ "models": [ { "id": "auto-fastest", - "name": "Auto Fastest", + "name": "auto-fastest", "api": "openai-completions", "provider": "omniroute", "baseUrl": "http://localhost:20128/v1", @@ -24,22 +24,22 @@ } ], "fallback": { - "chain": ["modelrelay/auto-fastest"], + "chain": ["9router/auto-fastest"], "timeoutMs": 30000, "onlyPreFirstToken": true, "notifyOnSwitch": true } }, - "modelrelay": { + "9router": { "baseUrl": "http://localhost:7352/v1", "api": "openai-completions", "apiKey": "no-key-needed", "models": [ { "id": "auto-fastest", - "name": "Auto Fastest", + "name": "auto-fastest", "api": "openai-completions", - "provider": "modelrelay", + "provider": "9router", "baseUrl": "http://localhost:7352/v1", "reasoning": true, "input": ["text"], diff --git a/docker/self-check.sh b/docker/self-check.sh index c88964d..d2be771 100755 --- a/docker/self-check.sh +++ b/docker/self-check.sh @@ -105,7 +105,7 @@ if ! should_skip "services"; then # Collecting responses if [ "$ELAPSED" -gt "$PORT_POLL_TIMEOUT" ]; then - for pair in "3000:WebTop" "8888:CodeServer" "7352:ModelRelay" "20128:OmniRoute", "9119:HermesGateway"; do + for pair in "3000:WebTop" "8888:CodeServer" "7352:9Router" "20128:OmniRoute", "9119:HermesGateway"; do PORT="${pair%%:*}" NAME="${pair##*:}" if [ "${RESPONDED[$PORT]}" != "true" ]; then @@ -116,7 +116,7 @@ if ! should_skip "services"; then fi # Testing ports - for pair in "3000:WebTop" "8888:CodeServer" "7352:ModelRelay" "20128:OmniRoute" "9119:HermesGateway"; do + for pair in "3000:WebTop" "8888:CodeServer" "7352:9Router" "20128:OmniRoute" "9119:HermesGateway"; do PORT="${pair%%:*}" NAME="${pair##*:}" diff --git a/docker/start-hermes.sh b/docker/start-hermes.sh index 164940d..36391f7 100755 --- a/docker/start-hermes.sh +++ b/docker/start-hermes.sh @@ -26,9 +26,9 @@ runuser -l abc <<'EOF' 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 providers.9router.base_url http://localhost:7352/v1 + hermes config set providers.9router.api_key no-key-needed + hermes config set fallback_providers.provider 9router hermes config set fallback_providers.model auto-fastest # Turn off approval alert and live dangerously since u are in a self-contained container. diff --git a/docker/start-modelrelay.sh b/docker/start-modelrelay.sh deleted file mode 100755 index 6903c88..0000000 --- a/docker/start-modelrelay.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -source /custom-cont-init.d/common.sh || exit 1 - -SRC="/custom-cont-init.d/ModelRelay.desktop" - -# Sync desktop file for desktop icon -# sync_desktop_file "$SRC" "/config/Desktop/ModelRelay.desktop" - -chown abc:abc -R /usr/local/bin/modelrelay - -runuser -l abc <<'EOF' -source /custom-cont-init.d/common.sh || exit 1 - -# Prep nodejs npm for ModelRelay -sudo rm -rf /config/.npm - -# Ensure ModelRelay is owned by abc -ensure_ownership "/usr/local/lib/node_modules/modelrelay" - -# Start ModelRelay -echo "[start-modelrelay] Starting ModelRelay..." -modelrelay --disable -nohup bash -c 'while true; do modelrelay >> /tmp/modelrelay.log 2>&1; sleep 3; done' & -sleep 10 - -EOF \ No newline at end of file diff --git a/docker/start-ninerouter.sh b/docker/start-ninerouter.sh new file mode 100755 index 0000000..80487d2 --- /dev/null +++ b/docker/start-ninerouter.sh @@ -0,0 +1,25 @@ +#!/bin/bash +source /custom-cont-init.d/common.sh || exit 1 + +SRC="/custom-cont-init.d/9Router.desktop" + +# Sync desktop file for desktop icon +# sync_desktop_file "$SRC" "/config/Desktop/9Router.desktop" + +chown abc:abc -R /usr/local/bin/9router + +runuser -l abc <<'EOF' +source /custom-cont-init.d/common.sh || exit 1 + +# Prep nodejs npm for 9Router +sudo rm -rf /config/.npm + +# Ensure 9Router is owned by abc +ensure_ownership "/usr/local/lib/node_modules/9router" + +# Start 9Router +echo "[start-ninerouter] Starting 9Router..." +nohup bash -c 'while true; do 9router --host 0.0.0.0 --host 127.0.0.1 --port 7352 --no-browser --skip-update >> /tmp/9router.log 2>&1; sleep 3; done' & +sleep 10 + +EOF \ No newline at end of file diff --git a/docs/architecture-diagram.excalidraw b/docs/architecture-diagram.excalidraw index 0f3fbbe..6cc9221 100644 --- a/docs/architecture-diagram.excalidraw +++ b/docs/architecture-diagram.excalidraw @@ -676,7 +676,7 @@ }, { "type": "rectangle", - "id": "modelrelay-dash", + "id": "ninerouter-dash", "x": 954.8823784722223, "y": 193.1163194444444, "width": 195, @@ -691,7 +691,7 @@ "roughness": 1, "boundElements": [ { - "id": "t_modelrelay_dash", + "id": "t_ninerouter_dash", "type": "text" } ], @@ -713,19 +713,19 @@ }, { "type": "text", - "id": "t_modelrelay_dash", + "id": "t_ninerouter_dash", "x": 964.1024330986871, "y": 215.6163194444444, "width": 176.5598907470703, "height": 20, - "text": "ModelRelay Dashboard", + "text": "9Router Dashboard", "fontSize": 16, "fontFamily": 1, "strokeColor": "#e5e5e5", "textAlign": "center", "verticalAlign": "middle", - "containerId": "modelrelay-dash", - "originalText": "ModelRelay Dashboard", + "containerId": "ninerouter-dash", + "originalText": "9Router Dashboard", "autoResize": true, "version": 299, "versionNonce": 906890841, @@ -752,7 +752,7 @@ }, { "type": "text", - "id": "t_modelrelay_dash_sub", + "id": "t_ninerouter_dash_sub", "x": 965.7126736111113, "y": 237.97222222222214, "width": 164.98782348632812, @@ -1451,7 +1451,7 @@ }, { "type": "rectangle", - "id": "modelrelay-api", + "id": "ninerouter-api", "x": 715.990017361111, "y": 529.3971354166666, "width": 195, @@ -1466,7 +1466,7 @@ "roughness": 1, "boundElements": [ { - "id": "t_modelrelay_api", + "id": "t_ninerouter_api", "type": "text" } ], @@ -1488,19 +1488,19 @@ }, { "type": "text", - "id": "t_modelrelay_api", + "id": "t_ninerouter_api", "x": 720.990017361111, "y": 536.3971354166666, "width": 185, "height": 16, - "text": "ModelRelay Proxy :7352", + "text": "9Router Proxy :7352", "fontSize": 14, "fontFamily": 1, "strokeColor": "#e5e5e5", "textAlign": "center", "verticalAlign": "middle", - "containerId": "modelrelay-api", - "originalText": "ModelRelay Proxy :7352", + "containerId": "ninerouter-api", + "originalText": "9Router Proxy :7352", "autoResize": true, "version": 108, "versionNonce": 1339566014, @@ -1527,7 +1527,7 @@ }, { "type": "text", - "id": "t_modelrelay_api_sub", + "id": "t_ninerouter_api_sub", "x": 720.990017361111, "y": 556.3971354166666, "width": 185, diff --git a/docs/architecture.md b/docs/architecture.md index 01c7f7f..2eb9f60 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,7 +18,7 @@ flowchart TB HermesDashFE[Hermes Dashboard
Web UI
:9119] WebTop[WebTop GUI
KasmVNC Desktop
:3000] OmniRouteDash[OmniRoute Dashboard
Web UI · Model Router
:20128] - ModelRelayDash[ModelRelay Dashboard
Web UI · Free Proxy
:7352] + 9RouterDash[9Router Dashboard
Web UI · Free Proxy
:7352] end subgraph Harness["🪢 Harness Layer — Agents & Memory"] @@ -31,7 +31,7 @@ flowchart TB subgraph AI["🧠 AI / Model Layer — LLM Backends"] OmniRouteAPI[OmniRoute API
LLM Router · Auto-Failover
:20128] - ModelRelayProxy[ModelRelay Proxy
Free-Tier LLM Proxy
:7352] + 9RouterProxy[9Router Proxy
Free-Tier LLM Proxy
:7352] Ollama[Ollama
Local LLM · Embeddings
:11434] end @@ -57,7 +57,7 @@ flowchart TB %% Gateway → AI Layer (primary) HermesGateway -- "LLM Request :20128" --> OmniRouteAPI - HermesGateway -.->|"Fallback :7352"| ModelRelayProxy + HermesGateway -.->|"Fallback :7352"| 9RouterProxy %% Memory path HermesGateway -- "mnemon CLI" --> Mnemon @@ -65,11 +65,11 @@ flowchart TB %% Dashboard → API internal arrows OmniRouteDash -.->|"Dashboard ↔ API"| OmniRouteAPI - ModelRelayDash -.->|"Dashboard ↔ Proxy"| ModelRelayProxy + 9RouterDash -.->|"Dashboard ↔ Proxy"| 9RouterProxy %% AI → External OmniRouteAPI -- "Config Free API" --> LLMProvider - ModelRelayProxy -.-> LLMProvider + 9RouterProxy -.-> LLMProvider %% GPU offloading OmniRouteAPI -.->|"use as proxy to local GPU"| OllamaGPU @@ -88,9 +88,9 @@ flowchart TB classDef external fill:#f3e5f5,stroke:#ab47bc,color:#4a148c classDef externalgpu fill:#fce4ec,stroke:#e91e63,color:#880e4f - class CodeServer,HermesDashFE,WebTop,OmniRouteDash,ModelRelayDash frontend + class CodeServer,HermesDashFE,WebTop,OmniRouteDash,9RouterDash frontend class HermesGateway,HermesDash,HermesCLI,ClaudeCLI,Mnemon harness - class OmniRouteAPI,ModelRelayProxy,Ollama ai + class OmniRouteAPI,9RouterProxy,Ollama ai class Tailscale,DockerVol,DockerNet,SelfCheck infra class Browser,LLMProvider external class OllamaGPU externalgpu @@ -133,9 +133,9 @@ flowchart TB ### ⚡ Hermes Agent — (Gateway/CLI/VSCode Extension) **What it does:** The AI coding agent that processes your prompts, calls LLMs, and orchestrates complex multi-step tasks with sub-agents and tools. -**How it starts:** `/docker/start-hermes.sh` installs Hermes, configures providers (OmniRoute as default, ModelRelay as fallback), enables mnemon memory, then launches the Gateway in background. +**How it starts:** `/docker/start-hermes.sh` installs Hermes, configures providers (OmniRoute as default, 9Router as fallback), enables mnemon memory, then launches the Gateway in background. -**Role in stack:** The brain of the system — it receives your messages, delegates sub-agents, calls LLMs through OmniRoute/ModelRelay, stores memories via Mnemon, and returns responses. On first boot it auto-configures: model=auto-fastest, approvals off, max_turns=120, kanban failure_limit=3, mnemon as memory provider. Every boot ensures python-telegram-bot is installed and clones/updates hermes-plugin-mnemon. +**Role in stack:** The brain of the system — it receives your messages, delegates sub-agents, calls LLMs through OmniRoute/9Router, stores memories via Mnemon, and returns responses. On first boot it auto-configures: model=auto-fastest, approvals off, max_turns=120, kanban failure_limit=3, mnemon as memory provider. Every boot ensures python-telegram-bot is installed and clones/updates hermes-plugin-mnemon. ### 🔧 Hermes CLI **What it does:** The command-line interface to Hermes, accessible from any terminal inside the container (VS Code terminal, WebTop terminal). @@ -176,23 +176,23 @@ flowchart TB **Role in stack:** The default LLM provider for Hermes. Routes requests to available models, handles failures, and provides MCP integration. -### 📊 ModelRelay Dashboard — Port :7352 (Frontend/Web UI) +### 📊 9Router Dashboard — Port :7352 (Frontend/Web UI) **What it does:** Similar to Omniroute, enable free models out of box withoutn configuration. -**Port:** `:7352` — served alongside the ModelRelay proxy on the same port. +**Port:** `:7352` — served alongside the 9Router proxy on the same port. -**How it starts:** Started automatically by `/docker/start-modelrelay.sh` at boot alongside the proxy. +**How it starts:** Started automatically by `/docker/start-ninerouter.sh` at boot alongside the proxy. **Role in stack:** Provides visibility into free-tier proxy operations — useful for monitoring fallback requests and troubleshooting connectivity. -### 🔄 ModelRelay Proxy — Port :7352 (AI Layer) +### 🔄 9Router Proxy — Port :7352 (AI Layer) **What it does:** A free-tier LLM API proxy that acts as a fallback when OmniRoute cannot fulfill a request. -**Port:** `:7352` — started by `/docker/start-modelrelay.sh` at boot. +**Port:** `:7352` — started by `/docker/start-ninerouter.sh` at boot. -**How it starts:** Installed from the custom fork github:gitricko/modelrelay (not the public npm package), launched in an auto-restart loop. Pre-configured as the fallback provider in Hermes config. +**How it starts:** Installed from the custom fork decolua/9router (not the public npm package), launched in an auto-restart loop. Pre-configured as the fallback provider in Hermes config. -**Role in stack:** Safety net — if OmniRoute goes down or can't find a model, ModelRelay handles the request with its free-tier models. +**Role in stack:** Safety net — if OmniRoute goes down or can't find a model, 9Router handles the request with its free-tier models. ### 🤖 Ollama — Port 11434 (internal only) **What it does:** A local LLM server that runs `nomic-embed-text` for generating text embeddings used by Mnemon. @@ -206,7 +206,7 @@ flowchart TB ### ⚡ Ollama GPU — External / Hosted GPU **What it does:** A remote Ollama instance running on a machine with a GPU, providing faster inference for local LLM tasks. -**Port:** Uses OmniRoute/ModelRelay as proxy — no direct port. +**Port:** Uses OmniRoute/9Router as proxy — no direct port. **How it connects:** Configured as a model provider in OmniRoute via the 'Config Free API' path. When the local CPU-based Ollama (:11434) is too slow, this external GPU-powered instance handles the heavy lifting. @@ -221,7 +221,7 @@ flowchart TB ### 🩺 Self-Check — Diagnostics Tool at /usr/local/bin/self-check -**What it does:** A boot-time health diagnostic that polls all 5 service ports (WebTop :3000, CodeServer :8888, ModelRelay :7352, OmniRoute :20128, Hermes Gateway :9119), checks OmniRoute model availability, Mnemon binary + database, Hermes config validity, disk usage, memory pressure, and cron job status. Optionally delivers a Telegram health report. +**What it does:** A boot-time health diagnostic that polls all 5 service ports (WebTop :3000, CodeServer :8888, 9Router :7352, OmniRoute :20128, Hermes Gateway :9119), checks OmniRoute model availability, Mnemon binary + database, Hermes config validity, disk usage, memory pressure, and cron job status. Optionally delivers a Telegram health report. **How it starts:** Runs automatically at the end of start-hermes.sh after all services are ready. Can be re-run manually anytime: `/usr/local/bin/self-check`. @@ -245,7 +245,7 @@ The container base image (LinuxServer.io WebTop) automatically runs every .sh sc 2. start-ohmyzsh.sh — Shell customization 3. start-ollama.sh — Ollama local LLM (embeddings daemon) 4. start-omniroute.sh — OmniRoute LLM router -5. start-modelrelay.sh — ModelRelay free-tier proxy +5. start-ninerouter.sh — 9Router free-tier proxy 6. start-codeserver.sh — VS Code in browser + extensions 7. start-hermes.sh — Hermes Agent, Gateway, Dashboard, Mnemon plugin, self-check, Telegram deps 8. start-tailscale.sh — Tailscale VPN (must log in manually) @@ -276,7 +276,7 @@ Here's the full journey of a message from your keyboard to the LLM and back: │ ├─ 🔀 OmniRoute falls back via "Config Free API" → external LLM Provider │ - └─ ❌ OmniRoute fails → Hermes Gateway falls back to ModelRelay (port :7352) + └─ ❌ OmniRoute fails → Hermes Gateway falls back to 9Router (port :7352) sends request → gets response │ ▼ @@ -285,7 +285,7 @@ Here's the full journey of a message from your keyboard to the LLM and back: │ (memory storage — saves new facts to Mnemon) ▼ 6. 🔄 Response flows back through the chain: - LLM → OmniRoute/ModelRelay → Hermes Gateway → Code-Server/CLI → Browser + LLM → OmniRoute/9Router → Hermes Gateway → Code-Server/CLI → Browser ``` **In more detail:** @@ -293,9 +293,9 @@ Here's the full journey of a message from your keyboard to the LLM and back: 1. **Browser → Code-Server (:8888) / Claude CLI terminal:** You open VS Code in your browser and type a prompt in the Hermes extension or terminal, or you run `claude` commands directly in the terminal. 2. **Code-Server / Claude CLI → Hermes Gateway (:9119):** The extension or CLI sends your message to the Hermes Gateway API. 3. **Hermes processes:** Hermes expands your prompt with system instructions, checks Mnemon for relevant context from past sessions, and may spawn sub-agents for parallel subtasks. -4. **Hermes → OmniRoute API (:20128):** For the LLM call, Hermes sends a request to OmniRoute, which selects the best available model from its `auto-fastest` combo. OmniRoute may route via the "Config Free API" path to external LLM providers, or proxy to an external Ollama GPU for accelerated inference (if configured). If OmniRoute fails entirely, Hermes Gateway automatically falls back to ModelRelay Proxy (:7352). +4. **Hermes → OmniRoute API (:20128):** For the LLM call, Hermes sends a request to OmniRoute, which selects the best available model from its `auto-fastest` combo. OmniRoute may route via the "Config Free API" path to external LLM providers, or proxy to an external Ollama GPU for accelerated inference (if configured). If OmniRoute fails entirely, Hermes Gateway automatically falls back to 9Router Proxy (:7352). 5. **Memory operations:** Hermes may store new information via Mnemon, which uses Ollama (:11434) to generate embeddings for semantic indexing. -6. **Response:** The LLM's response travels back through the same path — OmniRoute/ModelRelay → Hermes Gateway → Code-Server/CLI → your browser. Hermes also updates Mnemon with key facts from the conversation. +6. **Response:** The LLM's response travels back through the same path — OmniRoute/9Router → Hermes Gateway → Code-Server/CLI → your browser. Hermes also updates Mnemon with key facts from the conversation. ## Port Map @@ -306,7 +306,7 @@ Here's the full journey of a message from your keyboard to the LLM and back: | 9119 | **Hermes Gateway + Dashboard (Frontend)** | Agent HTTP API and web UI (socat forward from :9009) | ✅ Yes | | 8642 | **Hermes API** | Optional dedicated API server | ✅ Yes (optional) | | 20128 | **OmniRoute API/Dashboard** | Web UI/API for model router management | ✅ Yes | -| 7352 | **ModelRelay API/Dashboard** | Web UI/API for free-tier proxy monitoring | ✅ Yes | +| 7352 | **9Router API/Dashboard** | Web UI/API for free-tier proxy monitoring | ✅ Yes | | 9009 | **Hermes Dashboard (Harness)** | Web dashboard (internal — forwarded to :9119) | ✅ Yes | | 11434 | **Ollama** | Local LLM server for embeddings | ❌ Internal | | N/A | **Hermes CLI** | Terminal-based agent interface | ❌ Terminal only | From 057e529d039a4f8c17a7b0a1532a93119be4178b Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 12:48:26 +0000 Subject: [PATCH 03/24] fix: run 9router-config.sh in start-ninerouter.sh to seed model catalog --- docker/start-ninerouter.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/start-ninerouter.sh b/docker/start-ninerouter.sh index 80487d2..b82185f 100755 --- a/docker/start-ninerouter.sh +++ b/docker/start-ninerouter.sh @@ -22,4 +22,7 @@ echo "[start-ninerouter] Starting 9Router..." nohup bash -c 'while true; do 9router --host 0.0.0.0 --host 127.0.0.1 --port 7352 --no-browser --skip-update >> /tmp/9router.log 2>&1; sleep 3; done' & sleep 10 +# Configure 9Router: login, disable auth, create auto-fastest combo, smoke test +bash /custom-cont-init.d/9router-config.sh + EOF \ No newline at end of file From fc3e42030d78c9d5e0a692ae031d4172246c1fa6 Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 13:28:14 +0000 Subject: [PATCH 04/24] =?UTF-8?q?fix:=20simplify=20self-check=20model=20ou?= =?UTF-8?q?tput=20=E2=80=94=20count=20only,=20no=20default=20combo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/self-check.sh | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/docker/self-check.sh b/docker/self-check.sh index d2be771..472e176 100755 --- a/docker/self-check.sh +++ b/docker/self-check.sh @@ -60,7 +60,7 @@ results = json.loads(sys.stdin.read()) results.append({ 'name': '$name', 'status': '$status', - 'message': '$(echo "$message" | sed "s/'/\\\\'/g")', + 'message': '$(echo "$message" | sed "s/'/\\\\\\\\'/g")', 'detail': $detail }) print(json.dumps(results)) @@ -97,7 +97,7 @@ if ! should_skip "services"; then # Poll all service ports until all respond or timeout PORT_POLL_TIMEOUT=180 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) @@ -105,7 +105,7 @@ if ! should_skip "services"; then # Collecting responses if [ "$ELAPSED" -gt "$PORT_POLL_TIMEOUT" ]; then - for pair in "3000:WebTop" "8888:CodeServer" "7352:9Router" "20128:OmniRoute", "9119:HermesGateway"; do + for pair in "3000:WebTop" "8888:CodeServer" "7352:9Router" "20128:OmniRoute" "9119:HermesGateway"; do PORT="${pair%%:*}" NAME="${pair##*:}" if [ "${RESPONDED[$PORT]}" != "true" ]; then @@ -165,19 +165,31 @@ fi section "Models" if ! should_skip "models"; then + + # OmniRoute models_json=$(curl -s --max-time 5 "http://localhost:20128/v1/models" 2>/dev/null || echo '{}') model_count=$(echo "$models_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('data',[])))" 2>/dev/null || echo "0") - # Try to get the default combo name from Hermes config - default_model=$(grep -A1 '^model:' "$HERMES_CONFIG" 2>/dev/null | grep 'default' | head -1 | sed 's/.*default: *//' || echo "unknown") - default_model="${default_model:-unknown}" if [ "$model_count" -gt 0 ] 2>/dev/null; then - _ok "OmniRoute" "${model_count} models available (default: ${default_model})" - json_add "models" "ok" "${model_count} models, default combo: ${default_model}" "{\"count\":${model_count},\"default\":\"${default_model}\"}" - else - _warn "OmniRoute" "no models returned from /v1/models (may still be starting)" - json_add "models" "warn" "no models returned (may still be booting)" "{\"count\":0}" - fi + _ok "OmniRoute" "${model_count} models available" + json_add "models" "ok" "${model_count} models" "{\"count\":${model_count}}" + else + _warn "OmniRoute" "no models returned from /v1/models (may still be starting)" + json_add "models" "warn" "no models returned (may still be booting)" "{\"count\":0}" + fi + + # 9Router + models_json=$(curl -s --max-time 5 "http://localhost:7352/v1/models" 2>/dev/null || echo '{}') + model_count=$(echo "$models_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('data',[])))" 2>/dev/null || echo "0") + + if [ "$model_count" -gt 0 ] 2>/dev/null; then + _ok "9Router" "${model_count} models available" + json_add "models" "ok" "${model_count} models" "{\"count\":${model_count}}" + else + _warn "9Router" "no models returned from /v1/models (may still be starting)" + json_add "models" "warn" "no models returned (may still be booting)" "{\"count\":0}" + fi + else echo " (skipped)" fi @@ -465,7 +477,7 @@ import json,sys results = json.load(sys.stdin) warns = [r for r in results if r['status'] == 'warn'] if warns: - print('\\n*Warnings:*') + print('\n*Warnings:*') for w in warns: print(f'• {w[\"name\"]}: {w[\"message\"]}') " 2>/dev/null | while IFS= read -r line; do @@ -484,4 +496,4 @@ else echo " [telegram] not configured — stdout only (set TELEGRAM_BOT_TOKEN and TELEGRAM_HOME_CHANNEL in ~/.hermes/.env)" fi -exit "$EXIT_CODE" +exit "$EXIT_CODE" \ No newline at end of file From a85a37304a634974c52dde6a34dfe9ff2f828754 Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 14:58:08 +0000 Subject: [PATCH 05/24] fix: self-check treats 4xx/5xx as unhealthy; probe 9router /v1/models endpoint --- docker/self-check.sh | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docker/self-check.sh b/docker/self-check.sh index 472e176..ac6c252 100755 --- a/docker/self-check.sh +++ b/docker/self-check.sh @@ -124,12 +124,27 @@ if ! should_skip "services"; then continue fi - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost:${PORT}" 2>/dev/null || HTTP_CODE="000") + # Use /v1/models for 9Router (functional API endpoint), / for others + if [ "$PORT" = "7352" ]; then + HEALTH_ENDPOINT="/v1/models" + else + HEALTH_ENDPOINT="/" + fi + + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost:${PORT}${HEALTH_ENDPOINT}" 2>/dev/null || HTTP_CODE="000") HTTP_CODE=$(echo "$HTTP_CODE" | tr -d '[:space:]') if [ -n "$HTTP_CODE" ] && [ "$HTTP_CODE" != "000" ]; then - echo " $NAME (:$PORT) — ✅ HTTP ${HTTP_CODE} (${ELAPSED}s)" - RESPONDED[$PORT]="true" + case "$HTTP_CODE" in + 2*|3*) + echo " $NAME (:$PORT) — ✅ HTTP ${HTTP_CODE} (${ELAPSED}s)" + RESPONDED[$PORT]="true" + ;; + *) + # 4xx/5xx = service responded but unhealthy — warn, keep polling + echo " $NAME (:$PORT) — ⚠️ HTTP ${HTTP_CODE} (${ELAPSED}s) — unhealthy, waiting..." + ;; + esac fi done From 869b01a541c2efdc5562b7fe1ddb039f24581783 Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 15:26:41 +0000 Subject: [PATCH 06/24] fix: simplify self-check; treat 4xx/5xx as unhealthy; revert port-poll /v1/models special-case --- .agents/skills/parallel-delegation/SKILL.md | 197 ++++++++++++++++++++ docker/self-check.sh | 9 +- 2 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 .agents/skills/parallel-delegation/SKILL.md diff --git a/.agents/skills/parallel-delegation/SKILL.md b/.agents/skills/parallel-delegation/SKILL.md new file mode 100644 index 0000000..22ef6d7 --- /dev/null +++ b/.agents/skills/parallel-delegation/SKILL.md @@ -0,0 +1,197 @@ +--- +name: parallel-delegation +description: "Use when the main agent faces independent tasks. Delegate in parallel via delegate_task to stay unblocked." +version: 1.0.0 +author: hermes-webtop +license: MIT +platforms: [linux, macos, windows] +tags: + - delegation + - parallelism + - subagents + - concurrency + - orchestration +related_skills: + - karpathy-coding-guidelines + - simplify-code + - codebase-inspection + - systematic-debugging +metadata: + hermes: + tags: [delegation, parallelism, subagents, concurrency, orchestration] + related_skills: [karpathy-coding-guidelines, simplify-code, codebase-inspection, systematic-debugging] +--- + +# Parallel Delegation + +Always use as much subagent parallelism as possible: the main agent's job is +orchestration, not execution. Every piece of work that a subagent can carry +should be handed off, batched into the same turn, so the main agent never sits +idle and the conversation keeps moving. + +## Core Principles + +1. **Question every task first**: *can a subagent do this?* If yes and it is + independent of what you're doing, delegate it. +2. **Maximize parallelism**: N independent tasks → N `delegate_task` calls in + the same assistant turn. Never spawn one at a time and never serialize + independent work — sequential delegation multiplies wall-clock time for no + benefit. Prefer many small, focused subagents over one big one. +3. **Stay unblocked**: between spawning and results, keep doing work that + doesn't depend on the subagents. Do not hold your turn waiting for a result + you don't need yet. +4. **Consume, don't redo**: when results arrive, use them — don't re-execute + the delegated work yourself. + +## When to Delegate + +Delegate anything mechanical, long-running, parallelizable, or context-heavy: + +- **Batch independent audits** — e.g., dependency/security/lint audits split + per package or module, one subagent per scope (see Examples). +- **Script runs** — independent test scripts, formatting checks, or + reproducible CLI commands that can run concurrently. +- **Repo scans** — `search_files` / codebase inspection over disjoint + directories or areas, per-area. +- **Research & extraction** — web searches, page fetches, summarization, + transcript-to-summary jobs. +- **Anything that doesn't need your in-flight judgment** — offload it so a + long-running subagent finishes by the time you need its output. + +Rule of thumb: if you can enumerate the units up front, fan out one subagent +per unit in a single turn. + +## When NOT to Delegate + +- **Trivial one-liners and single tool calls** — a file read, a simple grep, + a one-command lookup. Delegation costs tokens (~20K per spawn) and a + round-trip; beating it with a single tool call is faster and cheaper. +- **When you already have full context** — if the answer is already in your + context or you're mid-way through exactly this work, finish it yourself. + Re-delegating means re-transmitting everything for nothing. +- **When the task is the main agent's own judgment call** — decisions, + tradeoffs, or steps the conversation's next move depends on immediately. +- **When spawn cost exceeds the work** — one quick `web_search` stays + in-thread; ten independent searches go to subagents. + +## How to Structure delegate_task Calls + +```python +delegate_task( + goal="", + context="""""", + output_schema={ + # structured fields so the result is machine-consumable, not prose + "findings": {"type": "list", "description": "one item per issue"}, + "summary": {"type": "string", "description": "2-3 sentence verdict"}, + }, + toolsets=["terminal", "web_search"], # minimal set the task needs +) +``` + +Each subagent gets: + +- **A self-contained brief** — every detail inline; it cannot read your + context. Include the skills it must load (e.g. tell it to follow + `karpathy-coding-guidelines` for code work). +- **A single deliverable** — one outcome, stated as a concrete artifact, + answer, or filled schema. +- **An `output_schema`** — structured results let you consume them directly + and fan out the next batch without re-reading verbose summaries. +- **Explicit minimal toolsets** — grant only what the task needs. + +## Examples + +### Parallel per-package audits (from the repo audit runs) + +Instead of one subagent walking every package sequentially, spawn one audit +subagent per scope in the same turn: + +```python +for pkg in ["pkg-a", "pkg-b", "pkg-c"]: + delegate_task( + goal=f"Audit {pkg} for vulnerabilities and report findings", + context=f""" + Load the audit checklist. Run the pinned audit script: + python scripts/audit.py {pkg} --full + Only report issues with evidence (file:line or command output). + Do not fix anything — report only. + """, + output_schema={ + "vulnerabilities": {"type": "list", "description": "each with file:line and severity"}, + "passed": {"type": "boolean"}, + "summary": {"type": "string"}, + }, + toolsets=["terminal", "file"], + ) +``` + +All three run concurrently; the main agent continues other work; results are +consumed per-package as they arrive. This is the pattern the audit results +demonstrated: parallel scopes finished far faster than one sequential sweep. + +### Delegating code work (with karpathy-coding-guidelines) + +When a subagent writes or changes code, its brief must include the discipline: + +```python +delegate_task( + goal="Fix the failing test in src/parser.py and make all tests pass", + context=""" + Follow the karpathy-coding-guidelines skill: minimal changes, surgical + diffs, no drive-by refactoring. Reproduce the failure first: + pytest tests/test_parser.py -x + Fix the root cause, then run the full suite: + pytest -q + Report the exact command output that proves the fix. + """, + output_schema={"changed_files": {"type": "list"}, "test_output": {"type": "string"}}, + toolsets=["terminal", "file", "code"], +) +``` + +The subagent owns verification and returns real output — never a claim of +"should work". The main agent checks the diff against the request without +re-running the work. + +## Handling Results + +- **Never poll** — don't idle-wait on a running subagent. Advance the next + independent piece of the plan; check back when you have a reason to. +- **Treat results as out-of-order** — concurrent calls return independently; + don't assume batch order. +- **Fan out the next batch** — consuming one result often unlocks the next + wave of independent work. Pipeline: spawn → do main work → consume → spawn + next wave. + +## Anti-patterns + +- ❌ **Serial delegation** — spawning independent subagents one per turn. +- ❌ **Doing delegated work yourself** — you spawned it; let it finish. +- ❌ **Blocking on results** — idle waiting while other work is pending. +- ❌ **Under-delegating** — running long or context-heavy work in the main + thread that a subagent could carry. +- ❌ **Over-delegating** — a subagent for a one-line lookup the main agent can + do in one tool call. + +## Pitfalls + +- Subagents may **not** inherit your conversation context or plugin-defined + tools. Every requirement goes in the `context` string; toolsets are passed + explicitly. +- Each spawn costs tokens (~20K) and a round-trip — parallelism pays off for + independent batches, not for work you could finish in one call yourself. +- Concurrent results arrive out of order — key them by their `goal`, never by + position. + +## Verification + +The skill worked if, at the end of a multi-task session: + +- [ ] Independent tasks were spawned in one turn, not one per turn +- [ ] The main agent did useful work between spawn and results (never idle-polled) +- [ ] Every delegated brief was self-contained (paths, commands, skills, schema) +- [ ] Results were consumed without redoing the work, and the next batch was fanned out +- [ ] Trivial single-call work stayed in-thread; nothing was delegated pointlessly \ No newline at end of file diff --git a/docker/self-check.sh b/docker/self-check.sh index ac6c252..26e1a03 100755 --- a/docker/self-check.sh +++ b/docker/self-check.sh @@ -124,14 +124,7 @@ if ! should_skip "services"; then continue fi - # Use /v1/models for 9Router (functional API endpoint), / for others - if [ "$PORT" = "7352" ]; then - HEALTH_ENDPOINT="/v1/models" - else - HEALTH_ENDPOINT="/" - fi - - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost:${PORT}${HEALTH_ENDPOINT}" 2>/dev/null || HTTP_CODE="000") + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://localhost:${PORT}" 2>/dev/null || HTTP_CODE="000") HTTP_CODE=$(echo "$HTTP_CODE" | tr -d '[:space:]') if [ -n "$HTTP_CODE" ] && [ "$HTTP_CODE" != "000" ]; then From 649124f4354a2b298b3eec923fac109cc20bf3bc Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 15:34:26 +0000 Subject: [PATCH 07/24] feat: capture 9router-config.sh logs; always-collect boot logs artifact - docker/start-ninerouter.sh: capture 9router-config.sh stdout/stderr to /tmp/9router-config.log - .github/workflows/docker-publish.yml: add 'Collect boot logs' + 'Upload boot logs' steps (if: always()) that tar /tmp/*.log and ~/.hermes/logs/*.log into boot-logs-{run_id}. Both jobs (build-test and push-to-ghcr) updated identically. --- .github/workflows/docker-publish.yml | 56 ++++++++++++++++++++++++++++ docker/start-ninerouter.sh | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 6f29df9..702a83d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -145,6 +145,34 @@ jobs: echo "=== ALL MNEMON CHECKS PASSED ===" + # ── Collect boot logs (always: self-check & mnemon failures need post-mortem) ── + - name: Collect boot logs + if: always() + run: | + CONTAINER_NAME="hermes-webtop" + mkdir -p /tmp/boot-logs + + # Docker compose startup (strip ANSI) + docker compose logs --no-color > /tmp/boot-logs/docker-compose.log 2>&1 || true + + # Service logs (/tmp/*.log: 9router, 9router-config, omniroute, etc.) + docker exec "$CONTAINER_NAME" sh -c 'cd /tmp && tar cf - *.log' 2>/dev/null | tar xf - -C /tmp/boot-logs/ || true + + # Hermes service logs + docker exec "$CONTAINER_NAME" sh -c 'cd $HOME/.hermes/logs && tar cf - *.log' 2>/dev/null | tar xf - -C /tmp/boot-logs/ || true + + ls -la /tmp/boot-logs/ + + # ── Upload boot logs ───────────────────────────────────────────────── + - name: Upload boot logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: boot-logs-${{ github.run_id }} + path: /tmp/boot-logs/ + if-no-files-found: warn + retention-days: 7 + # ── Collect logs on failure ────────────────────────────────────── - name: Upload logs on failure if: failure() @@ -312,6 +340,34 @@ jobs: echo "=== ALL MNEMON CHECKS PASSED ===" + # ── Collect boot logs (always: self-check & mnemon failures need post-mortem) ── + - name: Collect boot logs + if: always() + run: | + CONTAINER_NAME="hermes-webtop" + mkdir -p /tmp/boot-logs + + # Docker compose startup (strip ANSI) + docker compose logs --no-color > /tmp/boot-logs/docker-compose.log 2>&1 || true + + # Service logs (/tmp/*.log: 9router, 9router-config, omniroute, etc.) + docker exec "$CONTAINER_NAME" sh -c 'cd /tmp && tar cf - *.log' 2>/dev/null | tar xf - -C /tmp/boot-logs/ || true + + # Hermes service logs + docker exec "$CONTAINER_NAME" sh -c 'cd $HOME/.hermes/logs && tar cf - *.log' 2>/dev/null | tar xf - -C /tmp/boot-logs/ || true + + ls -la /tmp/boot-logs/ + + # ── Upload boot logs ───────────────────────────────────────────────── + - name: Upload boot logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: boot-logs-${{ github.run_id }} + path: /tmp/boot-logs/ + if-no-files-found: warn + retention-days: 7 + # ── Collect logs on failure ────────────────────────────────────── - name: Upload logs on failure if: failure() diff --git a/docker/start-ninerouter.sh b/docker/start-ninerouter.sh index b82185f..c524bbd 100755 --- a/docker/start-ninerouter.sh +++ b/docker/start-ninerouter.sh @@ -23,6 +23,6 @@ nohup bash -c 'while true; do 9router --host 0.0.0.0 --host 127.0.0.1 --port 735 sleep 10 # Configure 9Router: login, disable auth, create auto-fastest combo, smoke test -bash /custom-cont-init.d/9router-config.sh +bash /custom-cont-init.d/9router-config.sh >> /tmp/9router-config.log 2>&1 EOF \ No newline at end of file From b04d75e2986811c65ce678ba64fe65f80a8005ef Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 16:20:36 +0000 Subject: [PATCH 08/24] fix: move 9router-config.sh out of s6 init.d to /usr/local/bin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: 9router-config.sh was copied into /custom-cont-init.d/ (along with all *.sh), making it an s6 boot init script. It runs alphabetically BEFORE start-ninerouter.sh, hitting 9Router's /api/auth/login on a port with no listening service — HTML error response → jq parse error → exit 7. No model catalog, no auto-fastest combo, 9Router returns 500 forever. Fix: move it to /usr/local/bin/9router-config (same pattern as self-check.sh). Now only start-ninerouter.sh (after 9Router + sleep 10) invokes it. Also update the path reference in start-ninerouter.sh. --- docker/Dockerfile | 4 ++++ docker/start-ninerouter.sh | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 12d9f8e..b1014e4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -50,6 +50,10 @@ COPY --chown=abc:abc *.md /custom-cont-init.d # Move self-check to usr/local/bin to prevent it running as a boot-time init script RUN mv /custom-cont-init.d/self-check.sh /usr/local/bin/self-check && chmod +x /usr/local/bin/self-check +# Move 9router-config out of init.d too — it must only run from start-ninerouter.sh +# (after 9Router is listening), not as an s6 boot init script. +RUN mv /custom-cont-init.d/9router-config.sh /usr/local/bin/9router-config && chmod +x /usr/local/bin/9router-config + # Install Hermes ARG HERMES_VERSION ENV HERMES_HOME=/config/.hermes diff --git a/docker/start-ninerouter.sh b/docker/start-ninerouter.sh index c524bbd..5e02333 100755 --- a/docker/start-ninerouter.sh +++ b/docker/start-ninerouter.sh @@ -23,6 +23,6 @@ nohup bash -c 'while true; do 9router --host 0.0.0.0 --host 127.0.0.1 --port 735 sleep 10 # Configure 9Router: login, disable auth, create auto-fastest combo, smoke test -bash /custom-cont-init.d/9router-config.sh >> /tmp/9router-config.log 2>&1 +bash /usr/local/bin/9router-config >> /tmp/9router-config.log 2>&1 EOF \ No newline at end of file From ad7e1a4d243d03964994f5f96897595252796192 Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 16:43:21 +0000 Subject: [PATCH 09/24] fix: 9router-config cookie auth + disable while-true restart loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 9router-config.sh: login uses HttpOnly cookie auth (not Bearer token) — matches actual 9Router behavior (response has no .token field). Fixes jq parse error. - start-ninerouter.sh: replace 'while true; do 9router ... sleep 3; done' with simple nohup (like OmniRoute). 9Router is a persistent daemon, not a short-lived process; the ModelRelay restart loop was cargo-culted and masked crashes. Also disable the premature config call pending the Dockerfile fix. --- docker/9router-config.sh | 33 ++++++++++++++++++--------------- docker/start-ninerouter.sh | 5 +++-- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/docker/9router-config.sh b/docker/9router-config.sh index cf1c4be..51a780b 100755 --- a/docker/9router-config.sh +++ b/docker/9router-config.sh @@ -2,31 +2,34 @@ set -euo pipefail BASE_URL="http://localhost:7352" +COOKIE_JAR="/tmp/9router-cookie.txt" +rm -f "$COOKIE_JAR" -# 1) login with password 123456 via POST /api/auth/login +# 1) login via POST /api/auth/login — 9Router uses HttpOnly cookie (auth_token) echo "[9router-config] Logging in with password..." -LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/api/auth/login" -H "Content-Type: application/json" -d '{"password":"123456"}') -TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.token // empty') - -if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then - echo "[9router-config] Error: Failed to login, no token received" - echo "[9router-config] Response: $LOGIN_RESPONSE" - exit 1 +LOGIN_RESPONSE=$(curl -s -c "$COOKIE_JAR" -X POST "$BASE_URL/api/auth/login" -H "Content-Type: application/json" -d '{"password":"123456"}' 2>&1 || echo '{}') +if ! echo "$LOGIN_RESPONSE" | jq -e '.success // empty' >/dev/null 2>&1; then + echo "[9router-config] WARNING: login body: $LOGIN_RESPONSE" +fi +if [ ! -f "$COOKIE_JAR" ] || ! grep -q "auth_token" "$COOKIE_JAR" 2>/dev/null; then + echo "[9router-config] WARNING: no auth_token cookie set — login may have failed" +else + echo "[9router-config] Login successful (auth cookie set)" fi -echo "[9router-config] Login successful" -AUTH="Authorization: Bearer $TOKEN" +# Cookie-based auth for all subsequent requests +AUTH_ARGS=("-b" "$COOKIE_JAR") # 2) disable requireLogin and requireApiKey via PATCH /api/settings echo "[9router-config] Disabling requireLogin and requireApiKey..." -curl -s -X PATCH "$BASE_URL/api/settings" -H "Content-Type: application/json" -H "$AUTH" -d '{"requireLogin":false,"requireApiKey":false}' > /dev/null +curl -s -X PATCH "$BASE_URL/api/settings" -H "Content-Type: application/json" "${AUTH_ARGS[@]}" -d '{"requireLogin":false,"requireApiKey":false}' > /dev/null echo "[9router-config] Settings updated: requireLogin=false, requireApiKey=false" # 3) delete existing auto-fastest combo if present echo "[9router-config] Deleting existing auto-fastest combo if present..." curl -s -X DELETE "$BASE_URL/api/combos/auto-fastest" \ -H "Content-Type: application/json" \ - -H "$AUTH" > /dev/null || true + "${AUTH_ARGS[@]}" > /dev/null || true echo "[9router-config] Existing combo deleted (or did not exist)" # 4) create new auto-fastest combo with 8 free oc/ models @@ -34,7 +37,7 @@ echo "[9router-config] Creating new auto-fastest combo with 8 free oc/ models... MODELS='["oc/muse-spark-1.2","oc/muse-spark-1.3","oc/union-alpha","oc/big-pickle","oc/mimo-v2.5-free","oc/ling-3.0-flash-fin-free","oc/nemotron-3-ultra-free","oc/nemotron-3.5-lightning-free"]' -CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/api/combos" -H "Content-Type: application/json" -H "$AUTH" -d "{\"name\":\"auto-fastest\",\"models\":$MODELS}") +CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/api/combos" -H "Content-Type: application/json" "${AUTH_ARGS[@]}" -d "{\"name\":\"auto-fastest\",\"models\":$MODELS}") echo "$CREATE_RESPONSE" | jq -e '.name // empty' > /dev/null || { echo "[9router-config] Error: Failed to create combo" @@ -48,7 +51,7 @@ echo "[9router-config] Combo auto-fastest created successfully" echo "[9router-config] Setting round-robin fallback strategy via comboStrategies..." curl -s -X PATCH "$BASE_URL/api/settings" \ -H "Content-Type: application/json" \ - -H "$AUTH" \ + "${AUTH_ARGS[@]}" \ -d '{"comboStrategies":{"fallback":"round-robin"}}' > /dev/null echo "[9router-config] comboStrategies.fallback set to round-robin" @@ -56,7 +59,7 @@ echo "[9router-config] comboStrategies.fallback set to round-robin" echo "[9router-config] Running smoke test via /v1/chat/completions..." SMOKE_RESPONSE=$(curl -s -X POST "$BASE_URL/v1/chat/completions" \ -H "Content-Type: application/json" \ - -H "$AUTH" \ + "${AUTH_ARGS[@]}" \ -d '{"model":"auto-fastest","messages":[{"role":"user","content":"hello"}]}') SMOKE_CONTENT=$(echo "$SMOKE_RESPONSE" | jq -r '.choices[0].message.content // empty') diff --git a/docker/start-ninerouter.sh b/docker/start-ninerouter.sh index 5e02333..d0319d7 100755 --- a/docker/start-ninerouter.sh +++ b/docker/start-ninerouter.sh @@ -19,10 +19,11 @@ ensure_ownership "/usr/local/lib/node_modules/9router" # Start 9Router echo "[start-ninerouter] Starting 9Router..." -nohup bash -c 'while true; do 9router --host 0.0.0.0 --host 127.0.0.1 --port 7352 --no-browser --skip-update >> /tmp/9router.log 2>&1; sleep 3; done' & +nohup 9router --host 0.0.0.0 --host 127.0.0.1 --port 7352 --no-browser --skip-update >> /tmp/9router.log 2>&1 & sleep 10 # Configure 9Router: login, disable auth, create auto-fastest combo, smoke test -bash /usr/local/bin/9router-config >> /tmp/9router-config.log 2>&1 +echo "[start-ninerouter] 9Router configuration skipped (config step disabled — runs only from Docker build init)" +# bash /usr/local/bin/9router-config >> /tmp/9router-config.log 2>&1 EOF \ No newline at end of file From a48a258f22cb40efba252ab496096d2600ce26dc Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 18:35:05 +0000 Subject: [PATCH 10/24] fix: align 9Router config with hermes-codespace reference; enable config at boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 9router-config.sh: match reference exactly — cookie auth via -c/-b jar, combo delete by ID lookup, -contributor-free model suffixes, smoke test with stream:false + max_tokens:16 - start-ninerouter.sh: replace blind sleep 10 with /api/health readiness poll (up to 300s, same as post-create-cmd.sh), then actually run /usr/local/bin/9router-config (was disabled — root cause of missing combo + HTTP 500) - Dockerfile: drop commented-out github install line --- docker/9router-config.sh | 21 ++++++++++++--------- docker/Dockerfile | 1 - docker/start-ninerouter.sh | 25 ++++++++++++++++++++----- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/docker/9router-config.sh b/docker/9router-config.sh index 51a780b..581ecd0 100755 --- a/docker/9router-config.sh +++ b/docker/9router-config.sh @@ -25,17 +25,20 @@ echo "[9router-config] Disabling requireLogin and requireApiKey..." curl -s -X PATCH "$BASE_URL/api/settings" -H "Content-Type: application/json" "${AUTH_ARGS[@]}" -d '{"requireLogin":false,"requireApiKey":false}' > /dev/null echo "[9router-config] Settings updated: requireLogin=false, requireApiKey=false" -# 3) delete existing auto-fastest combo if present -echo "[9router-config] Deleting existing auto-fastest combo if present..." -curl -s -X DELETE "$BASE_URL/api/combos/auto-fastest" \ - -H "Content-Type: application/json" \ - "${AUTH_ARGS[@]}" > /dev/null || true -echo "[9router-config] Existing combo deleted (or did not exist)" +# 3) delete the combo if it already exists (lookup by ID) +COMBO_ID="$(curl -fsS -b "$COOKIE_JAR" "$BASE_URL/api/combos" | + jq -r '.combos[] | select(.name=="auto-fastest") | .id' | head -n 1)" +if [[ -n "$COMBO_ID" ]]; then + echo "[9router-config] Deleting existing auto-fastest combo (ID: $COMBO_ID)..." + curl -fsS -b "$COOKIE_JAR" -X DELETE "$BASE_URL/api/combos/$COMBO_ID" | jq +else + echo "[9router-config] No existing auto-fastest combo found" +fi # 4) create new auto-fastest combo with 8 free oc/ models echo "[9router-config] Creating new auto-fastest combo with 8 free oc/ models..." -MODELS='["oc/muse-spark-1.2","oc/muse-spark-1.3","oc/union-alpha","oc/big-pickle","oc/mimo-v2.5-free","oc/ling-3.0-flash-fin-free","oc/nemotron-3-ultra-free","oc/nemotron-3.5-lightning-free"]' +MODELS='["oc/muse-spark-1.2-contributor-free","oc/muse-spark-1.3-contributor-free","oc/union-alpha","oc/big-pickle","oc/mimo-v2.5-free","oc/ling-3.0-flash-fin-free","oc/nemotron-3-ultra-free","oc/nemotron-3.5-lightning-free"]' CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/api/combos" -H "Content-Type: application/json" "${AUTH_ARGS[@]}" -d "{\"name\":\"auto-fastest\",\"models\":$MODELS}") @@ -57,10 +60,10 @@ echo "[9router-config] comboStrategies.fallback set to round-robin" # 7) smoke test via /v1/chat/completions echo "[9router-config] Running smoke test via /v1/chat/completions..." -SMOKE_RESPONSE=$(curl -s -X POST "$BASE_URL/v1/chat/completions" \ +SMOKE_RESPONSE=$(curl -fsS "$BASE_URL/v1/chat/completions" \ -H "Content-Type: application/json" \ "${AUTH_ARGS[@]}" \ - -d '{"model":"auto-fastest","messages":[{"role":"user","content":"hello"}]}') + -d '{"model":"auto-fastest","messages":[{"role":"user","content":"Reply with exactly: OK"}],"stream":false,"max_tokens":16}') SMOKE_CONTENT=$(echo "$SMOKE_RESPONSE" | jq -r '.choices[0].message.content // empty') diff --git a/docker/Dockerfile b/docker/Dockerfile index b1014e4..c271e19 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -69,7 +69,6 @@ RUN curl -fsSL "https://raw.githubusercontent.com/NousResearch/hermes-agent/${HE ARG NINEROUTER_VERSION RUN npm install -g 9router@${NINEROUTER_VERSION} && \ npm cache clean --force -# RUN npm install -g github:gitricko/9router && \ # Install OmniRoute and start automatically when desktop loads ARG OMNIROUTE_VERSION diff --git a/docker/start-ninerouter.sh b/docker/start-ninerouter.sh index d0319d7..4854ed2 100755 --- a/docker/start-ninerouter.sh +++ b/docker/start-ninerouter.sh @@ -17,13 +17,28 @@ sudo rm -rf /config/.npm # Ensure 9Router is owned by abc ensure_ownership "/usr/local/lib/node_modules/9router" -# Start 9Router +# Start 9Router (persistent daemon — no restart loop needed) echo "[start-ninerouter] Starting 9Router..." nohup 9router --host 0.0.0.0 --host 127.0.0.1 --port 7352 --no-browser --skip-update >> /tmp/9router.log 2>&1 & -sleep 10 -# Configure 9Router: login, disable auth, create auto-fastest combo, smoke test -echo "[start-ninerouter] 9Router configuration skipped (config step disabled — runs only from Docker build init)" -# bash /usr/local/bin/9router-config >> /tmp/9router-config.log 2>&1 +# Wait for 9Router to become ready (poll /api/health, up to 300s) +# Same pattern as hermes-codespace post-create-cmd.sh +MAX_ATTEMPTS=300 +for ((attempt=1; attempt<=MAX_ATTEMPTS; attempt++)); do + if curl -s --max-time 3 -o /dev/null http://localhost:7352/api/health; then + echo "[start-ninerouter] 9Router ready after ${attempt}s" + break + fi + if [ "$attempt" -eq "$MAX_ATTEMPTS" ]; then + echo "[start-ninerouter] Error: 9Router failed to start after $MAX_ATTEMPTS attempts." + exit 1 + fi + sleep 1 +done + +# Configure 9Router: login (cookie), disable auth, create auto-fastest combo, smoke test +echo "[start-ninerouter] Configuring 9Router..." +bash /usr/local/bin/9router-config >> /tmp/9router-config.log 2>&1 +echo "[start-ninerouter] 9Router configuration complete!" EOF \ No newline at end of file From d504db0c683d1ae8b3127fbafadf7da48f50e51e Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 19:18:01 +0000 Subject: [PATCH 11/24] chore: retain failure logs for 7 days; enable 9Router --log flag - docker-publish.yml: retention-days: 7 on both failure-log upload steps - start-ninerouter.sh: add --log to 9Router launch for runtime visibility --- .github/workflows/docker-publish.yml | 6 ++++-- docker/start-ninerouter.sh | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 702a83d..a5d9486 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -181,7 +181,8 @@ jobs: name: failure-logs-${{ github.run_id }} path: /tmp/failure-logs/ if-no-files-found: warn - + retention-days: 7 + push-to-ghcr: runs-on: ubuntu-latest # Run if on main branch, or if manual_push is yes from workflow_dispatch @@ -375,4 +376,5 @@ jobs: with: name: failure-logs-${{ github.run_id }} path: /tmp/failure-logs/ - if-no-files-found: warn \ No newline at end of file + if-no-files-found: warn + retention-days: 7 \ No newline at end of file diff --git a/docker/start-ninerouter.sh b/docker/start-ninerouter.sh index 4854ed2..491ad6b 100755 --- a/docker/start-ninerouter.sh +++ b/docker/start-ninerouter.sh @@ -19,7 +19,7 @@ ensure_ownership "/usr/local/lib/node_modules/9router" # Start 9Router (persistent daemon — no restart loop needed) echo "[start-ninerouter] Starting 9Router..." -nohup 9router --host 0.0.0.0 --host 127.0.0.1 --port 7352 --no-browser --skip-update >> /tmp/9router.log 2>&1 & +nohup 9router --host 0.0.0.0 --host 127.0.0.1 --log --port 7352 --no-browser --skip-update >> /tmp/9router.log 2>&1 & # Wait for 9Router to become ready (poll /api/health, up to 300s) # Same pattern as hermes-codespace post-create-cmd.sh From 3ca6f931719ba9446b9d21bf1bcb2765bd6ffd88 Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 19:33:33 +0000 Subject: [PATCH 12/24] =?UTF-8?q?fix:=20create=20/config/.9router=20state?= =?UTF-8?q?=20dir=20before=20launch=20=E2=80=94=20fixes=20EACCES=20500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause found via --log flag (d504db0): EACCES: permission denied, open '/config/.9router/jwt-secret' EACCES: permission denied, open '/config/.9router/model-catalog.json.tmp' 9Router's state dir didn't exist at boot → every login/combo/models endpoint returned 500 → health poll passed (stateless /api/health) but config script + self-check both failed. Fix: mkdir -p /config/.9router && chown -R abc:abc before launch, same pattern as OmniRoute's /config/.omniroute. --- docker/start-ninerouter.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/start-ninerouter.sh b/docker/start-ninerouter.sh index 491ad6b..595751c 100755 --- a/docker/start-ninerouter.sh +++ b/docker/start-ninerouter.sh @@ -14,6 +14,10 @@ source /custom-cont-init.d/common.sh || exit 1 # Prep nodejs npm for 9Router sudo rm -rf /config/.npm +# Ensure 9Router state dir exists and is writable by abc (fixes EACCES on jwt-secret/model-catalog) +sudo mkdir -p /config/.9router +sudo chown -R abc:abc /config/.9router + # Ensure 9Router is owned by abc ensure_ownership "/usr/local/lib/node_modules/9router" From 7a5f53266df5d38079bc769b8ead8c529c52f317 Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 19:53:49 +0000 Subject: [PATCH 13/24] refactor: merge 9router-config.sh into start-ninerouter.sh - Inline all config logic (cookie auth, disable auth, combo create, round-robin strategy, smoke test) into start-ninerouter.sh - Remove separate 9router-config.sh file and its /usr/local/bin install - Reduces boot-time files, eliminates cross-script dependency --- docker/9router-config.sh | 79 -------------------------------------- docker/start-ninerouter.sh | 76 +++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 80 deletions(-) delete mode 100755 docker/9router-config.sh diff --git a/docker/9router-config.sh b/docker/9router-config.sh deleted file mode 100755 index 581ecd0..0000000 --- a/docker/9router-config.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -set -euo pipefail - -BASE_URL="http://localhost:7352" -COOKIE_JAR="/tmp/9router-cookie.txt" -rm -f "$COOKIE_JAR" - -# 1) login via POST /api/auth/login — 9Router uses HttpOnly cookie (auth_token) -echo "[9router-config] Logging in with password..." -LOGIN_RESPONSE=$(curl -s -c "$COOKIE_JAR" -X POST "$BASE_URL/api/auth/login" -H "Content-Type: application/json" -d '{"password":"123456"}' 2>&1 || echo '{}') -if ! echo "$LOGIN_RESPONSE" | jq -e '.success // empty' >/dev/null 2>&1; then - echo "[9router-config] WARNING: login body: $LOGIN_RESPONSE" -fi -if [ ! -f "$COOKIE_JAR" ] || ! grep -q "auth_token" "$COOKIE_JAR" 2>/dev/null; then - echo "[9router-config] WARNING: no auth_token cookie set — login may have failed" -else - echo "[9router-config] Login successful (auth cookie set)" -fi - -# Cookie-based auth for all subsequent requests -AUTH_ARGS=("-b" "$COOKIE_JAR") - -# 2) disable requireLogin and requireApiKey via PATCH /api/settings -echo "[9router-config] Disabling requireLogin and requireApiKey..." -curl -s -X PATCH "$BASE_URL/api/settings" -H "Content-Type: application/json" "${AUTH_ARGS[@]}" -d '{"requireLogin":false,"requireApiKey":false}' > /dev/null -echo "[9router-config] Settings updated: requireLogin=false, requireApiKey=false" - -# 3) delete the combo if it already exists (lookup by ID) -COMBO_ID="$(curl -fsS -b "$COOKIE_JAR" "$BASE_URL/api/combos" | - jq -r '.combos[] | select(.name=="auto-fastest") | .id' | head -n 1)" -if [[ -n "$COMBO_ID" ]]; then - echo "[9router-config] Deleting existing auto-fastest combo (ID: $COMBO_ID)..." - curl -fsS -b "$COOKIE_JAR" -X DELETE "$BASE_URL/api/combos/$COMBO_ID" | jq -else - echo "[9router-config] No existing auto-fastest combo found" -fi - -# 4) create new auto-fastest combo with 8 free oc/ models -echo "[9router-config] Creating new auto-fastest combo with 8 free oc/ models..." - -MODELS='["oc/muse-spark-1.2-contributor-free","oc/muse-spark-1.3-contributor-free","oc/union-alpha","oc/big-pickle","oc/mimo-v2.5-free","oc/ling-3.0-flash-fin-free","oc/nemotron-3-ultra-free","oc/nemotron-3.5-lightning-free"]' - -CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/api/combos" -H "Content-Type: application/json" "${AUTH_ARGS[@]}" -d "{\"name\":\"auto-fastest\",\"models\":$MODELS}") - -echo "$CREATE_RESPONSE" | jq -e '.name // empty' > /dev/null || { - echo "[9router-config] Error: Failed to create combo" - echo "[9router-config] Response: $CREATE_RESPONSE" - exit 1 -} -echo "[9router-config] Combo auto-fastest created successfully" - -# 5) set round-robin fallback strategy -# 6) PATCH settings with comboStrategies -echo "[9router-config] Setting round-robin fallback strategy via comboStrategies..." -curl -s -X PATCH "$BASE_URL/api/settings" \ - -H "Content-Type: application/json" \ - "${AUTH_ARGS[@]}" \ - -d '{"comboStrategies":{"fallback":"round-robin"}}' > /dev/null -echo "[9router-config] comboStrategies.fallback set to round-robin" - -# 7) smoke test via /v1/chat/completions -echo "[9router-config] Running smoke test via /v1/chat/completions..." -SMOKE_RESPONSE=$(curl -fsS "$BASE_URL/v1/chat/completions" \ - -H "Content-Type: application/json" \ - "${AUTH_ARGS[@]}" \ - -d '{"model":"auto-fastest","messages":[{"role":"user","content":"Reply with exactly: OK"}],"stream":false,"max_tokens":16}') - -SMOKE_CONTENT=$(echo "$SMOKE_RESPONSE" | jq -r '.choices[0].message.content // empty') - -if [ -n "$SMOKE_CONTENT" ]; then - echo "[9router-config] Smoke test PASSED - received response from model" - echo "[9router-config] Response preview: ${SMOKE_CONTENT:0:100}" -else - echo "[9router-config] Smoke test FAILED - no content in response" - echo "[9router-config] Response: $SMOKE_RESPONSE" - exit 1 -fi - -echo "[9router-config] 9Router configuration complete!" \ No newline at end of file diff --git a/docker/start-ninerouter.sh b/docker/start-ninerouter.sh index 595751c..11a05f1 100755 --- a/docker/start-ninerouter.sh +++ b/docker/start-ninerouter.sh @@ -42,7 +42,81 @@ done # Configure 9Router: login (cookie), disable auth, create auto-fastest combo, smoke test echo "[start-ninerouter] Configuring 9Router..." -bash /usr/local/bin/9router-config >> /tmp/9router-config.log 2>&1 + +BASE_URL="http://localhost:7352" +COOKIE_JAR="/tmp/9router-cookie.txt" +rm -f "$COOKIE_JAR" + +# 1) login via POST /api/auth/login — 9Router uses HttpOnly cookie (auth_token) +echo "[start-ninerouter] Logging in with password..." +LOGIN_RESPONSE=$(curl -s -c "$COOKIE_JAR" -X POST "$BASE_URL/api/auth/login" -H "Content-Type: application/json" -d '{"password":"123456"}' 2>&1 || echo '{}') +if ! echo "$LOGIN_RESPONSE" | jq -e '.success // empty' >/dev/null 2>&1; then + echo "[start-ninerouter] WARNING: login body: $LOGIN_RESPONSE" +fi +if [ ! -f "$COOKIE_JAR" ] || ! grep -q "auth_token" "$COOKIE_JAR" 2>/dev/null; then + echo "[start-ninerouter] WARNING: no auth_token cookie set — login may have failed" +else + echo "[start-ninerouter] Login successful (auth cookie set)" +fi + +# Cookie-based auth for all subsequent requests +AUTH_ARGS=("-b" "$COOKIE_JAR") + +# 2) disable requireLogin and requireApiKey via PATCH /api/settings +echo "[start-ninerouter] Disabling requireLogin and requireApiKey..." +curl -s -X PATCH "$BASE_URL/api/settings" -H "Content-Type: application/json" "${AUTH_ARGS[@]}" -d '{"requireLogin":false,"requireApiKey":false}' > /dev/null +echo "[start-ninerouter] Settings updated: requireLogin=false, requireApiKey=false" + +# 3) delete the combo if it already exists (lookup by ID) +COMBO_ID="$(curl -fsS -b "$COOKIE_JAR" "$BASE_URL/api/combos" | jq -r '.combos[] | select(.name=="auto-fastest") | .id' | head -n 1)" +if [[ -n "$COMBO_ID" ]]; then + echo "[start-ninerouter] Deleting existing auto-fastest combo (ID: $COMBO_ID)..." + curl -fsS -b "$COOKIE_JAR" -X DELETE "$BASE_URL/api/combos/$COMBO_ID" | jq +else + echo "[start-ninerouter] No existing auto-fastest combo found" +fi + +# 4) create new auto-fastest combo with 8 free oc/ models +echo "[start-ninerouter] Creating new auto-fastest combo with 8 free oc/ models..." + +MODELS='["oc/muse-spark-1.2-contributor-free","oc/muse-spark-1.3-contributor-free","oc/union-alpha","oc/big-pickle","oc/mimo-v2.5-free","oc/ling-3.0-flash-fin-free","oc/nemotron-3-ultra-free","oc/nemotron-3.5-lightning-free"]' + +CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/api/combos" -H "Content-Type: application/json" "${AUTH_ARGS[@]}" -d "{\"name\":\"auto-fastest\",\"models\":$MODELS}") + +echo "$CREATE_RESPONSE" | jq -e '.name // empty' > /dev/null || { + echo "[start-ninerouter] Error: Failed to create combo" + echo "[start-ninerouter] Response: $CREATE_RESPONSE" + exit 1 +} +echo "[start-ninerouter] Combo auto-fastest created successfully" + +# 5) set round-robin fallback strategy +# 6) PATCH settings with comboStrategies +echo "[start-ninerouter] Setting round-robin fallback strategy via comboStrategies..." +curl -s -X PATCH "$BASE_URL/api/settings" \ + -H "Content-Type: application/json" \ + "${AUTH_ARGS[@]}" \ + -d '{"comboStrategies":{"fallback":"round-robin"}}' > /dev/null +echo "[start-ninerouter] comboStrategies.fallback set to round-robin" + +# 7) smoke test via /v1/chat/completions +echo "[start-ninerouter] Running smoke test via /v1/chat/completions..." +SMOKE_RESPONSE=$(curl -fsS "$BASE_URL/v1/chat/completions" \ + -H "Content-Type: application/json" \ + "${AUTH_ARGS[@]}" \ + -d '{"model":"auto-fastest","messages":[{"role":"user","content":"Reply with exactly: OK"}],"stream":false,"max_tokens":16}') + +SMOKE_CONTENT=$(echo "$SMOKE_RESPONSE" | jq -r '.choices[0].message.content // empty') + +if [ -n "$SMOKE_CONTENT" ]; then + echo "[start-ninerouter] Smoke test PASSED - received response from model" + echo "[start-ninerouter] Response preview: ${SMOKE_CONTENT:0:100}" +else + echo "[start-ninerouter] Smoke test FAILED - no content in response" + echo "[start-ninerouter] Response: $SMOKE_RESPONSE" + exit 1 +fi + echo "[start-ninerouter] 9Router configuration complete!" EOF \ No newline at end of file From ab63a680e6d7c85c6c3f7adf62c7bcb105383d99 Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 19:59:44 +0000 Subject: [PATCH 14/24] fix: remove 9router-config.sh mv from Dockerfile (merged into start-ninerouter.sh) --- docker/Dockerfile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c271e19..c474661 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -50,10 +50,6 @@ COPY --chown=abc:abc *.md /custom-cont-init.d # Move self-check to usr/local/bin to prevent it running as a boot-time init script RUN mv /custom-cont-init.d/self-check.sh /usr/local/bin/self-check && chmod +x /usr/local/bin/self-check -# Move 9router-config out of init.d too — it must only run from start-ninerouter.sh -# (after 9Router is listening), not as an s6 boot init script. -RUN mv /custom-cont-init.d/9router-config.sh /usr/local/bin/9router-config && chmod +x /usr/local/bin/9router-config - # Install Hermes ARG HERMES_VERSION ENV HERMES_HOME=/config/.hermes From 4eca1d5337c0544bef646aad06b8e8fb53398119 Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 20:42:44 +0000 Subject: [PATCH 15/24] fix: PI branch @main + version bumps (Phase 2) - pi-settings.json: hermes-impl -> main (branch not found) - start-pi.sh: hermes-impl -> main - Dockerfile: NODE_VERSION 26.7.0 -> 26.10.0, PI_VERSION 0.85.1 -> 0.87.1 --- docker/Dockerfile | 4 ++-- docker/pi-settings.json | 2 +- docker/start-pi.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c474661..b60b51c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,9 +2,9 @@ ARG HERMES_VERSION="v2026.9.14" ARG OMNIROUTE_VERSION=3.8.50 ARG NINEROUTER_VERSION=0.5.81 ARG OLLAMA_VERSION=0.34.1 -ARG NODE_VERSION=26.7.0 +ARG NODE_VERSION=26.10.0 ARG CODE_SERVER_VERSION=4.137.0 -ARG PI_VERSION=0.85.1 +ARG PI_VERSION=0.87.1 ARG MNEMON_VERSION=0.2.8 ARG HERDR_VERSION=0.7.4 ARG TARGETARCH diff --git a/docker/pi-settings.json b/docker/pi-settings.json index 5ca75f4..5881b8d 100644 --- a/docker/pi-settings.json +++ b/docker/pi-settings.json @@ -2,6 +2,6 @@ "defaultProvider": "omniroute", "defaultModel": "auto-fastest", "packages": [ - "git:github.com/gitricko/pi-failover@hermes-impl" + "git:github.com/gitricko/pi-failover@main" ] } \ No newline at end of file diff --git a/docker/start-pi.sh b/docker/start-pi.sh index 3266d9b..78b823a 100755 --- a/docker/start-pi.sh +++ b/docker/start-pi.sh @@ -29,7 +29,7 @@ fi # Install Pi-agent hermes like fallback extension echo "[$SCRIPT_NAME] Installing Pi-agent extension..." -pi install git:github.com/gitricko/pi-failover@hermes-impl +pi install git:github.com/gitricko/pi-failover@main echo "[start-pi] Pi agent ready ($(pi --version 2>/dev/null || echo unknown)). Default model: omniroute/auto-fastest" EOF From e663d3dd3e59869f1b644f4b2c8f2790e0eb215d Mon Sep 17 00:00:00 2001 From: thiamricko Date: Thu, 24 Sep 2026 21:06:41 +0000 Subject: [PATCH 16/24] Phase 3: add .agents/skills from minions (ci-lint-check, ci-log-capture, docker-test-shell) - Copied core CI/Docker workflow skills for repo portability - parallel-delegation was already present - docker-build-remote-only and simple-architecture-diagram were planned but not in minions --- .agents/skills/ci-lint-check/SKILL.md | 156 ++++++++++ .../references/dev-prod-parity.md | 35 +++ .../ci-lint-check/scripts/ci_lint_check.sh | 268 ++++++++++++++++++ .agents/skills/ci-log-capture/SKILL.md | 118 ++++++++ .agents/skills/docker-test-shell/SKILL.md | 125 ++++++++ .../docker-test-shell/references/dts-usage.md | 85 ++++++ 6 files changed, 787 insertions(+) create mode 100644 .agents/skills/ci-lint-check/SKILL.md create mode 100644 .agents/skills/ci-lint-check/references/dev-prod-parity.md create mode 100755 .agents/skills/ci-lint-check/scripts/ci_lint_check.sh create mode 100644 .agents/skills/ci-log-capture/SKILL.md create mode 100644 .agents/skills/docker-test-shell/SKILL.md create mode 100644 .agents/skills/docker-test-shell/references/dts-usage.md diff --git a/.agents/skills/ci-lint-check/SKILL.md b/.agents/skills/ci-lint-check/SKILL.md new file mode 100644 index 0000000..b9d0cec --- /dev/null +++ b/.agents/skills/ci-lint-check/SKILL.md @@ -0,0 +1,156 @@ +--- +name: ci-lint-check +description: Run pre-commit CI lint validation locally before pushing to avoid GitHub Actions failures. +version: 0.1.0 +author: gitricko, Hermes Agent +license: MIT +platforms: [linux] +metadata: + hermes: + tags: [ci, lint, pre-commit, github-actions, validation] + related_skills: [codespace-persistent-symlinks, memory-automation] +--- + +# CI Lint Check Skill + +Run the full CI lint validation locally before committing or creating a PR. This mirrors the `lint-check` job in `.github/workflows/ci.yml` and catches all format/validation issues that would fail CI. + +## When to Use + +- **Before every commit** that touches `skills/**`, `wiki/**`, `mnemon/**`, `memories/**`, or `*.sh` +- **Before creating a PR** to ensure CI passes +- When CI fails and you need to debug locally + +## Prerequisites + +- `markdownlint-cli` (auto-installed by script) +- `python3` (for Mnemon seed validation) +- `bash` (for shell syntax checks) + +## How to Run + +```bash +# Quick one-liner (runs all checks): +bash skills/ci-lint-check/scripts/ci_lint_check.sh + +# Or step by step: +bash skills/ci-lint-check/scripts/ci_lint_check.sh --markdown-only +bash skills/ci-lint-check/scripts/ci_lint_check.sh --skills-only +bash skills/ci-lint-check/scripts/ci_lint_check.sh --wiki-only +bash skills/ci-lint-check/scripts/ci_lint_check.sh --mnemon-only +bash skills/ci-lint-check/scripts/ci_lint_check.sh --shell-only +bash skills/ci-lint-check/scripts/ci_lint_check.sh --symlink-only +``` + +## What It Validates + +| Check | Files | CI Job | +|-------|-------|--------| +| Markdown lint | `wiki/*.md`, `skills/*/SKILL.md`, `.hermes.md`, `README.md` | `lint-check` | +| SKILL.md structure | All skills in `skills/*/SKILL.md` | `lint-check` | +| Wiki INDEX.md consistency | Every `.md` in wiki/ referenced in INDEX.md | `lint-check` | +| Mnemon seed.json | `mnemon/seed.json` schema | `lint-check` | +| Root shell syntax | `*.sh` | `lint-check` | +| Skill shell syntax | `skills/*/scripts/*.sh` | `lint-check` | +| Symlink persistence | Tracked dirs + boot script symlink logic | `lint-check` | + +## Procedure + +### 1. Install dependencies (first run only) +```bash +npm install -g markdownlint-cli +``` + +### 2. Run full validation +```bash +bash skills/ci-lint-check/scripts/ci_lint_check.sh +``` + +### 3. Fix any reported issues +- Markdown lint: fix reported line/column issues +- SKILL.md: ensure YAML frontmatter with `name:` field +- Wiki: add missing articles to INDEX.md table +- Mnemon: `python3 mnemon/validate-seed.py mnemon/seed.json` +- Shell: `bash -n