| name | hermes-cortex |
| version | 2.0.0 |
| description | Install, configure, and maintain Hermes Cortex — the observability and knowledge layer for Hermes Agent (Ollama, mycortex, Langfuse, Cortex Dashboard, nginx, systemd/launchd services, Hermes plugins, offline code assistant, offline content auto-update). |
| tags | ["setup","installation","mycortex","langfuse","dashboard","nginx","systemd","launchd","linux","macos","plugins","docker-compose","offline","code-assistant","rag","bible","hymns"] |
| related_skills | ["hermes-agent","docker-compose-common-issues"] |
Hermes Cortex Setup
Hermes Cortex is the local AI infrastructure layer that provides:
- Ollama — Local LLM inference server
- mycortex — Postgres-native knowledge brain with hybrid RAG search (legacy brain replacement, decommissioned 2026-08-02)
- systemd (Linux) / launchd (macOS) services — Persistent daemons for Ollama, mycortex, Dashboard
- Hermes plugins —
/brain slash command for knowledge queries (mycortex-command)
mycortex is DEAD — mycortex replaces it. All mycortex CLI commands below are
legacy. The mycortex CLI lives at ops/scripts/manage/mycortex (deployed to
~/.hermes-cortex/scripts/mycortex): sources add|list|remove, sync,
search, list, stats, doctor. Runs over mycortex-postgres (:15432) —
no daemon, no bun; sync is a cron. mycortex troubleshooting sections further
down are historical and kept only for migration reference.
Installation
Prerequisites
# Verify Python 3.12+ (canonical interpreter for cortex scripts)
python3 --version
# Verify Docker (required for mycortex-postgres + Langfuse)
docker --version
# Supported OS: Linux (systemd, apt/dnf/pacman), macOS (launchd, brew), WSL
uname -s # Linux or Darwin
Run the Installer
The installer lives at ops/install/install.sh (moved from repo root in v2.0.0):
git clone --depth 1 https://github.com/lukemcqueen/hermes-cortex.git ~/hermes-cortex
cd ~/hermes-cortex
bash ops/install/install.sh
# Check prerequisites only (no install):
bash ops/install/install.sh --check
# macOS — Laptop profile: lean, no Docker
CORTEX_PROFILE=laptop bash ops/install/install.sh
# Linux — auto-detects systemd, apt/dnf/pacman
CORTEX_OS=linux bash ops/install/install.sh
Idempotent — safe to re-run. Skips already-installed steps.
The installer runs ~30 steps (exact count varies by OS/profile):
| Step | Component | What it does |
|---|
| 0 | System Check | Verifies OS, RAM, disk, Docker, network before touching anything |
| 1 | Ollama | Native installer per OS, bound to localhost; pulls nomic-embed-text:v1.5 |
| 2 | Bun | JavaScript runtime (still used by some tooling) |
| 3 | mycortex-postgres | Docker Postgres + pgvector, schema migrations (replaces the legacy brain) |
| 4 | Brain directories | ~/brain/{default,…} with MECE schema, .gitignore, git init |
| 5 | mycortex sources | Registers brain dirs as mycortex sources (via manage/mycortex CLI) |
| 6 | Hermes mycortex plugin | /brain slash command (plugins/mycortex-command) |
| 7 | Hermes utility scripts | heartbeat, memory-to-brain-sync, check-memory-budget, etc. |
| 8 | Cron jobs | Essential crons (auto-remediation, health, memory sync) + orchestrator-only |
| 9 | Hermes skills | Deploys repo skills/ → ~/.hermes/skills/ via sync_skills() |
| 10 | Hooks | Scoring pre-commit hooks to all projects + post-merge hook |
| 11 | MCP servers | loop-governance (loop-gov-mcp.py), tasks (task-mcp.py) |
| 12 | Web Cache | Semantic web result cache |
| 13 | Langfuse | Docker Compose stack with auto-generated secrets |
| 14 | Cortex Dashboard | Flask app + systemd/launchd service |
| 15 | Offline tools | cache cascade + ZIM viewer |
| 16 | nginx | Reverse proxy (SSL or local-only mode) |
| 17 | Hardening | File permission lockdown on sensitive files |
| 18 | Bootstrap | Bootstraps brain sources (verify + index) |
What gets installed:
| Component | Port | Purpose |
|---|
| Ollama | 11434 | Local LLM serving |
| mycortex-postgres | 15432 | Knowledge brain (Postgres + pgvector, Docker) |
| Langfuse | 3000 | LLM observability (Docker) |
| Cortex Dashboard | 8901 | System health + Langfuse companion |
| ClickHouse (Langfuse) | 8123/9000 | Analytics DB for Langfuse v3 |
| nginx | 11002, 11003 | Reverse proxy for external access |
Script Deployment Architecture
Hermes Cortex uses a two-directory deployment model:
| Directory | Purpose | Managed by |
|---|
~/.hermes-cortex/scripts/ | Canonical runtime location — all cortex scripts deployed here | cortex-update.sh register map |
~/.hermes/scripts/ | Cron resolution — Hermes Agent cron scheduler looks here | Symlinks → ~/.hermes-cortex/scripts/ |
How scripts flow: Repo source → cortex-update.sh → ~/.hermes-cortex/scripts/ → symlink → ~/.hermes/scripts/. Do NOT manually copy scripts to ~/.hermes/scripts/ — always use cortex-update.sh to deploy, then create symlinks.
To add a new cortex script:
- Add
register "ops/path/to/script" "${HERMES_HOME}/scripts/name" to cortex-update.sh
- Run
bash ~/.hermes-cortex/scripts/cortex-update.sh
- Create symlink:
ln -sf ~/.hermes-cortex/scripts/name ~/.hermes/scripts/name
- Register in
install-crons.sh if it's a cron script
Cleanup of stale duplicates: If both ~/.hermes/scripts/ and ~/.hermes-cortex/scripts/ have a copy of the same file, replace the ~/.hermes/scripts/ copy with a symlink:
rm ~/.hermes/scripts/<file> && ln -sf ~/.hermes-cortex/scripts/<file> ~/.hermes/scripts/<file>
This was done for 42 files in June 2026. Any future cortex-update maintenance should follow the same pattern — never leave stale regular-file copies in ~/.hermes/scripts/.
Cron job script resolution: The cron scheduler resolves scripts from HERMES_HOME/scripts/ (~/.hermes/scripts/), resolving symlinks via .resolve(). The install-crons.sh function checks SCRIPTS_DIR="${HOME}/.hermes-cortex/scripts" when verifying script existence during cron creation.
External access (via nginx):
- Langfuse:
https://your-domain.com:11002 (TLS + basic auth, upstream 127.0.0.1:3000)
- Cortex Dashboard:
https://your-domain.com:11003 (TLS + basic auth)
Verify Installation
# Ollama health
curl -s http://127.0.0.1:11434/api/tags | jq .
# mycortex CLI (deployed path)
~/.hermes-cortex/scripts/mycortex --help
# mycortex doctor
~/.hermes-cortex/scripts/mycortex doctor
# mycortex sources
~/.hermes-cortex/scripts/mycortex sources list
# Langfuse (Docker) — v3 health endpoint
docker ps | grep langfuse
curl http://localhost:3000/api/public/health
# Cortex Dashboard
curl http://localhost:8901/api/health
# Linux: systemctl status cortex-dashboard
# macOS: launchctl list | grep cortex-dashboard
# nginx
nginx -t
# Linux: systemctl status nginx
# macOS: brew services list | grep nginx
# Services (systemd on Linux, launchd on macOS)
# Linux: systemctl --user list-units | grep -E "(ollama|mycortex|cortex)"
# macOS: launchctl list | grep -E "(ollama|mycortex|cortex)"
# Hermes plugin
hermes plugins list | grep mycortex
ls -la ~/.hermes/plugins/mycortex-command/
Post-Install Verification
After the basic health checks pass, run these deeper verifications to confirm mycortex is actually indexing content — not just registered:
# Run the brain bootstrap health check (deployed path — repo: ops/scripts/install/bootstrap-brain.sh)
bash ~/.hermes-cortex/scripts/bootstrap-brain.sh --check-only
# Look for: "X source(s) have indexed pages"
# If ALL show "0 pages indexed", brain directories are empty — add .md files
# Run the full heartbeat report (deployed path — repo: ops/scripts/health/heartbeat.py)
python3 ~/.hermes-cortex/scripts/heartbeat.py --report
# Expected: Overall OK or DEGRADED with actionable items
# Check memory budget (deployed path — repo: ops/scripts/health/check-memory-budget.sh)
bash ~/.hermes-cortex/scripts/check-memory-budget.sh --report
# If >85%, run the pointer pattern to compress entries
# Verify mycortex has actual indexed pages per source
~/.hermes-cortex/scripts/mycortex sources list | grep -v "0 pages"
# Should show at least one source with >0 pages
Why this matters: mycortex sources can be registered and "healthy" but have zero indexed pages. The bootstrap-brain.sh --check-only script detects this condition. If all sources show 0 pages, the /brain command returns nothing useful regardless of how much content exists in brain directories.
Built-in Health Check
After install, the simplest readiness check is:
bash ~/.hermes/scripts/cortex-health.sh
This produces a single-pane report covering Ollama, Langfuse, mycortex sources,
mycortex sync freshness, memory sync freshness, Cortex Dashboard, and disk usage.
Exits with:
- 0 (HEALTHY) — all green
- 1 (DEGRADED) — non-critical issues like stale brain sources
- 2 (CRITICAL) — core services down
Also supports --json for programmatic consumption and --watch
(auto-recheck every 30s).
For deeper inspection, run the systematic audit below.
1. Cross-Reference Installer Scripts
The installer references utility scripts from the repo via the $(_scripts)/<name>
helper (e.g. $(_scripts)/health/heartbeat.py), so the repo copy is canonical —
installing fresh deploys the real file, not a stale inline copy. Legacy heredoc
fallbacks exist for a few scripts (bootstrap-brain, check-memory-budget, heartbeat,
send-agent-learning, memory-to-brain-sync) but are fallback-only; the _scripts()
path is preferred. (Fixed 2026-08-08: 16 stale $(_scripts)/<name> refs in
ops/install/install.sh pointed at pre-restructure paths and silently deployed
7KB heartbeat stubs instead of the 20KB repo version.)
Verify the installed copy matches the repo source (SOURCE header stripped):
# Compare deployed heartbeat vs repo
diff <(sed '1,2d' ~/.hermes-cortex/scripts/heartbeat.py | sed '/^# SOURCE:/d') \
<(sed '/^# SOURCE:/d' ops/scripts/health/heartbeat.py)
If they differ, the installed script is stale — run cortex-update.sh to redeploy:
bash ~/hermes-cortex/ops/scripts/cortex-update.sh
# Verifies: deployed checksum (minus SOURCE header) == repo source
2. Verify the mycortex Sync Path
mycortex has no daemon — knowledge sync is a cron job
(agent-mycortex-sync.sh → manage/mycortex sync). There is no autopilot and
no sync-watch; the old mycortex launchd daemon pair (legacy autopilot /
com.legacy-brain.sync-watch) was decommissioned with mycortex (2026-08-02).
# Verify sync cron exists
cronjob action=list | grep mycortex-sync
# Run a manual sync to verify the pipeline
~/.hermes-cortex/scripts/mycortex sync --force
# Check the sync cron's last run status
cronjob action=list | grep -A2 mycortex-sync
If a legacy mycortex daemon is still present, remove it:
launchctl bootout gui/$(id -u)/legacy autopilot 2>/dev/null || true
launchctl bootout gui/$(id -u)/com.legacy-brain.sync-watch 2>/dev/null || true
rm -f ~/Library/LaunchAgents/com.mycortex.*.plist ~/.legacy-brain/sync-watch.sh
3. Verify cortex-update.sh covers all deployed files
The update script uses a register() function to map repo paths → installed paths.
Only registered files get auto-deployed when changed. See references/cortex-update-deployment-map.md
for the full current map (core scripts, self-remediation, offline tools, dashboard, agent inbox, templates, langfuse).
New file additions: When adding a new script to the repo, add a register() line to cortex-update.sh
(ops/scripts/cortex-update.sh) so it gets auto-deployed on next update. Source paths
use the canonical ops/ prefix, e.g. register "ops/scripts/health/heartbeat.py".
See references/cortex-update-deployment-map.md for the complete map, update modes (delta/force/status),
and the macOS sha256sum→shasum fallback.
4. Verify Langfuse Containers
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}" | grep langfuse
Expected: 6 containers (langfuse-web, langfuse-worker, postgres, redis, clickhouse, minio),
all healthy. If none exist, Langfuse was never deployed or Docker Desktop isn't running.
5. Check Every mycortex Source Has Indexed Pages
~/.hermes-cortex/scripts/mycortex sources list | grep -v "0 pages" | grep -v "^$"
~/.hermes-cortex/scripts/mycortex sources list | grep "0 pages"
Sources showing "0 pages, never synced" are registered but empty. They need content
before the sync cron has anything to index. See "Existing Repo Setup" below.
5. Audit the Auto-Update Path
head -5 ~/.hermes-cortex/offline/auto-update.sh 2>/dev/null || head -5 ~/hermes-cortex/offline/auto-update.sh
grep "^HERMES_DIR\|^OFFLINE_DIR" ~/.hermes-cortex/offline/auto-update.sh 2>/dev/null
If the script references $HOME/hermes-cortex/offline/ (hardcoded path), it breaks
when the repo is cloned somewhere other than ~/hermes-cortex. The script should
derive its path from its own location or use ~/.hermes-cortex/offline/.
6. Verify Offline Code Index
ls -la ~/offline/code-index.json 2>/dev/null || ls -la ~/hermes-cortex/offline/code-index.json 2>/dev/null || echo "MISSING — run: offline_code index"
If no index exists, the offline code assistant is non-functional. The installer
creates the corpus files but doesn't build the index automatically.
Critical Pitfalls
⚠️ The legacy-brain pitfalls below are historical — the legacy brain was
decommissioned 2026-08-02 and replaced by mycortex. They are kept for
migration reference only. Active pitfalls for the current stack (nginx,
Langfuse, cortex-update, loop-governance, hooks) follow after the legacy-brain
block; the git-repo-requirement lesson carries over to mycortex git-mode
sources.
legacy brain npm Package Collision (HISTORICAL — legacy brain removed)
PROBLEM: bun install -g on the legacy brain's package name (npm squatter) installs the WRONG package.
The npm registry has mycortex@1.3.1 by stormcolor — a dead 2018 GPU JavaScript library with NO CLI binary. This is NOT the knowledge base tool.
SOLUTION (no longer applicable — the legacy brain was replaced by mycortex): mycortex is a repo-managed Python CLI, installed via cortex-update.sh, never npm/bun.
The real mycortex (garrytan/mycortex, 20.9k stars) was a Postgres-native personal knowledge base with hybrid RAG search, self-wiring knowledge graphs, and synthesis. It must be installed from GitHub, not npm.
VERIFICATION (historical): After install, confirm:
mycortex --version # Should return version like "mycortex 0.42.25.0"
which mycortex # Should return ~/.bun/bin/mycortex
mycortex Source Directory Requirements (carries over to mycortex git-mode)
PROBLEM: mycortex sync fails silently on source directories that aren't git-initialized.
SOLUTION: Every source directory must be a git repository before adding to mycortex:
cd ~/brain/default
git init
git add -A
git commit -m "initial brain state"
Then add the source:
mycortex sources add mybrain --path ~/brain/default --name "mybrain"
Default Source is Special
PROBLEM: The default mycortex source is built-in (backs pre-v0.17 brain) and cannot have --path configured.
SOLUTION: Skip the default source when configuring. Use a separate source name like mybrain:
# WRONG - will fail silently
mycortex sources add default --path ~/brain/default
# RIGHT - use a different name
mycortex sources add mybrain --path ~/brain/default --name "mybrain"
Sync Daemon (Patched — Smart Polling)
As of Moses' round-2 patches, the sync daemon no longer relies on
--all --skip default. Instead it:
- Runs
mycortex sources list each cycle to count registered non-default sources
- If zero sources exist (fresh install), logs
"skipping — run seed-project-brain.sh" and sleeps
- Only runs
mycortex sync --all --skip default --no-pull when sources exist
No more useless polling on an empty install, and no more silent failures
from the built-in default source.
Redundancy with autopilot: The autopilot (legacy autopilot) is a
self-maintaining daemon that internally handles sync every ~150s alongside
extraction and embedding. When both daemons run, sync-watch fails every cycle
because autopilot holds the exclusive lock. If autopilot is running and healthy,
sync-watch should be disabled (see Section 2 of Built-in Health Check).
New installs auto-detect: As of commit 7f2205d, install-legacy-sync.sh
now checks for legacy autopilot before setting up sync-watch and skips
it if autopilot is present. Existing installs with both daemons should disable
sync-watch manually (see Troubleshooting).
cortex-update.sh restart_legacy_sync — bootout before rm
PROBLEM: When cortex-update.sh detects a change to install-legacy-sync.sh,
it calls the restart_legacy_sync function. The original code did:
rm -f ~/.legacy-brain/sync-watch.sh
bash ~/.hermes/scripts/install-legacy-sync.sh
This is wrong. launchd KeepAlive keeps the old process alive even after the
script file is deleted. install-legacy-sync.sh checks service_running() and
sees PID 30588 (still alive) — so it prints "already running" and skips
regenerating sync-watch.sh. The result: the plist points to a deleted file.
If the process ever dies, launchd cannot restart it.
FIX (applied in commit 893ddc4): Always bootout the service before removing
the script:
launchctl bootout gui/$(id -u)/com.legacy-brain.sync-watch 2>/dev/null || true
sleep 1
rm -f ~/.legacy-brain/sync-watch.sh
bash ~/.hermes/scripts/install-legacy-sync.sh
Verification: After a cortex update, check that sync-watch.sh exists and has a
new PID:
ls -la ~/.legacy-brain/sync-watch.sh # must exist
launchctl list com.legacy-brain.sync-watch | grep PID # should be different from before
grep "skip default" ~/.legacy-brain/sync-watch.sh # confirm --skip default is present
nginx zone-defs duplicate after
PROBLEM: Running cortex-update.sh deploys hermes-zone-defs.conf
(which defines limit_req_zone, limit_conn_zone, and map blocks). If the main
nginx.conf already defines the same zones from a previous setup, nginx fails with:
nginx: [emerg] limit_req_zone "general" is already bound to key "$binary_remote_addr"
DIAGNOSE:
nginx -t 2>&1 | grep "already bound"
grep limit_req_zone /opt/homebrew/etc/nginx/nginx.conf /opt/homebrew/etc/nginx/hermes-zone-defs.conf
FIX: Comment out the zone definitions in nginx.conf — hermes-zone-defs.conf is the
canonical source (included by hermes-services.conf):
sed -i '' 's/^ limit_req_zone / #limit_req_zone /' /opt/homebrew/etc/nginx/nginx.conf
sed -i '' 's/^ limit_conn_zone / #limit_conn_zone /' /opt/homebrew/etc/nginx/nginx.conf
nginx -t && nginx -s reload
PREVENT: After any `` update, always run nginx -t. If it fails with
"already bound", apply the fix above. The zone-defs file is the single source of truth.
Loop-Governance Module — Setup Pitfalls
The loop-governance module (core/governance/) was added for TDD cycle scoring. First-time setup must account for these issues:
1. macOS default bash (3.2) lacks declare -A
The setup.sh script uses declare -A (associative arrays, bash 4+). macOS ships bash 3.2. This produces:
declare: -A: invalid option
The setup still completes because the error is non-fatal. Scripts are copied correctly despite this warning. If needed, install bash 5 via Homebrew: brew install bash.
2. Symlink targets use underscores, not hyphens
The repo files are named with underscores (score_cycle.py, loop_feedback.py, loop_config.py, auto_apply.py) but the ~/.local/bin/ wrappers are named with hyphens (score-cycle, loop-feedback, etc.). When creating symlinks, point to the underscore file:
# CORRECT
ln -sf ~/hermes-cortex/core/governance/score_cycle.py ~/.local/bin/score-cycle
ln -sf ~/hermes-cortex/core/governance/loop_feedback.py ~/.local/bin/loop-feedback
ln -sf ~/hermes-cortex/core/governance/auto_apply.py ~/.local/bin/auto-apply
ln -sf ~/hermes-cortex/core/governance/loop_config.py ~/.local/bin/loop-config
# WRONG — file doesn't exist
ln -sf ~/hermes-cortex/core/governance/score-cycle.py ~/.local/bin/score-cycle # NO
If you symlink to a nonexistent target, bash will follow the dead symlink when using cat > and create a stub file at the target path. This corrupts the source file. Verify all targets exist:
for f in score-cycle loop-feedback auto-apply loop-config; do
target=$(readlink ~/.local/bin/$f)
[ -f "$target" ] && echo "✅ $f" || echo "❌ $f -> $target MISSING"
done
3. Register the MCP server for agent access
After running setup.sh, register the loop-governance MCP server so agents can use cache_search, cycle_query, feedback_accept, etc. as MCP tools instead of CLI:
hermes mcp add \
--command $HOME/.hermes/mcp-venv/bin/python3 \
--args ~/hermes-cortex/mcp-servers/loop-gov-mcp.py \
loop-governance
The MCP server exposes 7 tools: cache_search, config_show, config_set, cycle_query, cycle_stats, feedback_accept, feedback_override. These are the primary interface for agents -- the CLI (score-cycle, loop-feedback) is the fallback for pre-commit hooks and scripts.
Pitfall — config path: The MCP server's args path in ~/.hermes/config.yaml must point to the actual file. If hermes-cortex was cloned to a different path than ~/hermes-cortex/, update the args. A stale path (pointing to a non-existent file) causes MCP tool failures with no visible error -- the process starts but runs old code.
Verification: After registering, run hermes mcp list. The loop-governance server should show ✓ enabled with 7 tools. If MCP tools return "Error: no such column", kill stale loop-gov-mcp.py processes and let Hermes restart them.
4. AGENTS.md rule #10 — Score every change (non-negotiable)
After every change (code, config, script, or deployment), log it to the loop-governance DB. Two paths:
Path A — MCP tools (for agents):
- Before coding:
mcp_loop_governance_cache_search(query="task description")
- After change:
mcp_loop_governance_cycle_query(task_id="<task>")
- Provide feedback:
mcp_loop_governance_feedback_accept(cycle_id=N) or feedback_override(...)
Path B — CLI tools (for hooks/scripts):
score-cycle --task <task-id> --cycle <N> --code-file <file> --prev-code-file <file> --pass-pct <pass-rate>
This applies to ALL changes — not just TDD cycles. For config/IT changes with no tests, pass --pass-pct 100 if verification succeeded.
Scoring guidelines by change type:
| Change Type | --test-file | --pass-pct |
|---|
| Code change (TDD cycle) | Test file | Actual test pass rate |
| Config/IT change | N/A (omit) | 100 if verification passed, 0 if failed |
| Script edit | Any invocation that proves it works | 100 if ran without error |
| Deployment | Health check endpoint or proof of life | 100 if healthy |
If the system's decision was wrong, use loop-feedback override <id> --note "...".
Three-layer enforcement (see README.md for full docs):
| Layer | What | How to install | Bypass |
|---|
| Pre-commit hook | Runs score-cycle on every git commit | bash ~/.hermes-cortex/scripts/install-score-hook.sh --all |
| SOUL.md directive | Rule in every Hermes session's system prompt | Edit ~/.hermes/SOUL.md (add Mandatory Directives section) | Remove the directive |
| Cron auditor | Scans every 6h for unscored changes | Auto-created by install-crons.sh | N/A |
Dogfood your own rules: When you introduce a new rule or process that mandates scoring, immediately run score-cycle on your own changes to validate the tooling works end-to-end. This catches missing shebangs, Python version mismatches, scoring calibration gaps, and feedback CLI tooling issues before they hit production. The user will call you out if you mandate something and don't do it yourself — it erodes trust in the rule.
Deployment convention: New enforcement scripts must be registered in cortex-update.sh via register(). They deploy to ~/.hermes-cortex/scripts/ — the cron scheduler resolves scripts from there via SCRIPTS_DIR="${HOME}/.hermes-cortex/scripts". Do NOT manually copy to ~/.hermes/scripts/; use cortex-update.sh to deploy properly.
Verification: Run bash ~/hermes-cortex/core/governance/verify.sh — expects 14/14 passed, 0 warnings, 0 failures.
Template for new projects: Copy the standalone template from the hermes-cortex repo's docs/templates/AGENTS-loop-governance.md to add loop-governance rules to any project's AGENTS.md. See skill reference agents-loop-governance-template.md.
4. score-cycle/loop-feedback shebangs — Python 3.12+ required (macOS default is 3.9)
The score-cycle and loop-feedback entry points use #!/usr/bin/env python3. macOS ships Python 3.9 which doesn't support PEP 604 (str | None syntax). All Hermes projects now require 3.12+.
Ensure the shebang points to a 3.12+ interpreter:
# Use uv-managed 3.12 (default for Hermes projects)
ls ~/.local/bin/python3.12
# Or pyenv-managed 3.12+
ls ~/.pyenv/versions/ | grep '^3\\.1[2-9]'
# Update shebang:
sed -i '' '1s|#!/usr/bin/env python3|#!/Users/\$(whoami)/.local/bin/python3.12|' \\
~/hermes-cortex/core/governance/score_cycle.py \
~/hermes-cortex/core/governance/loop_feedback.py
When deploying to a new machine, verify python3 --version resolves to 3.12+. The install.sh probes python3 python3.12 (plus any available 3.13/3.14) in order and selects the first 3.12+ with sqlite3 extension support.
5. Cron prompt URLs must use external inbox address
After a cortex update that adds or modifies cron jobs, verify the cron prompt doesn't contain the old internal URL (127.0.0.1:8903). The external URL is https://your-domain.com:13004. Check:
hermes cron list | grep "inbox-processor\\|inbox-watchdog"
# Verify prompt/preview doesn't say 127.0.0.1:8903
cortex-update.sh offline_knowledge symlink blocks deploy
PROBLEM: cortex-update.sh maps ~/.hermes-cortex/bin/offline_knowledge from the repo's ops/offline/offline_knowledge.py. If the ~/.hermes-cortex/bin/ directory does not exist, the symlink creation fails with:
ln: $HOME/.hermes-cortex/bin/offline_knowledge: No such file or directory
This is a non-fatal error by itself, but if cortex-update.sh runs with set -e (exit on error), the script stops immediately. Any register() lines AFTER the broken symlink — including newly added scripts — never get deployed.
FIX: Ensure the target directory exists before running cortex-update:
mkdir -p ~/.hermes-cortex/bin
bash ~/.hermes-cortex/scripts/cortex-update.sh
PREVENT: When registering a new file that creates a symlink in cortex-update.sh, add a mkdir -p for the target directory before the ln -sf command, or use ln -sf ... 2>/dev/null || true to prevent a failed symlink from blocking the rest of the update.
Delta engine skips after manual git pull
before invoking cortex-update.sh, the delta engine sees old_commit == new_commit
and exits with "Already up to date". Files that actually changed in the repo don't
get deployed because the commit comparison passes through.
FIX: Use `` to bypass commit comparison and checksum every file:
bash ~/.hermes/scripts/cortex-update.sh
PREVENT: Either (a) let cortex-update.sh handle its own git pull instead of
pulling manually, or (b) always use `` after any manual pull.
Launchd plist paths — shell/CORTEX_HOME variables NOT expanded
PROBLEM: launchd (via launchctl load) does NOT expand shell variables like
$HOME or CORTEX_HOME in plist files. Using them in ProgramArguments,
WorkingDirectory, StandardOutPath, or StandardErrorPath causes the job to
fail with EX_CONFIG (exit code 78) immediately on load.
This affects ALL cortex plists: com.hermes.health-server.plist,
com.hermes.cortex-dashboard.plist, com.hermes.agent-inbox.plist, and any
agent-created plists.
DIAGNOSE:
launchctl list com.hermes.health-server
# Look for: "LastExitStatus" = 78; (EX_CONFIG)
# Or: paths showing as literal "$HOME/..." instead of "/home/<username>/..."
FIX: Use hardcoded absolute paths in ALL plist key values:
<!-- WRONG — launchd sees the literal string "$HOME" -->
<string>$HOME/.hermes/scripts/health-server.py</string>
<!-- RIGHT — hardcoded absolute path -->
<string>$HOME/.hermes/scripts/health-server.py</string>
Affected files in the cortex repo (fixes applied in commit e1ff1c7):
ops/scripts/com.hermes.health-server.plist — paths used $HOME
ops/services/dashboard/com.hermes.cortex-dashboard.plist — paths used CORTEX_HOME
PREVENT: Before creating or modifying any launchd plist, verify every path is
a hardcoded absolute path for the target user. The ~/ tilde prefix does work
(launchd expands it), but $HOME and any custom env var do NOT. When in doubt
use /Users/<username>/ explicitly.
VERIFICATION: After fixing, confirm the job loads and shows expanded paths:
launchctl load ~/Library/LaunchAgents/com.hermes.health-server.plist
launchctl list com.hermes.health-server
# Expected: "Program" = "/home/<username>/.hermes/..." (expanded, not literal "$HOME")
# Expected: "LastExitStatus" = 0
Launchd minimal PATH — always set EnvironmentVariables PATH in generated plists
PROBLEM (2026-08-10, ledger 88090aae): launchd jobs run with a minimal
PATH (/usr/bin:/bin:/usr/sbin:/sbin), so python3 resolves to the macOS
system Python 3.9.6 at /usr/bin/python3. Cortex tooling requires 3.10+
(dict | None union syntax) — the daily com.hermes.cortex-update plist
crashed with TypeError: unsupported operand type(s) for | every 3am until
given an explicit PATH. A plist generated by
install-cortex-update-cron.sh shipped with NO EnvironmentVariables block.
FIX: Every generated/installed plist must carry an explicit PATH that
puts Homebrew/usr-local ahead of /usr/bin:
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>
(Applied to install-cortex-update-cron.sh in commit c806edf4 — if you
generate or hand-write a plist, include the block; launchctl unload +
load to apply.) The com.hermes.health-push and com.hermes.cortex-dashboard
plist templates already carry PATH — check any NEW plist against them.
PREVENT: after writing any plist, verify EnvironmentVariables.PATH exists
and includes the toolchain dir (/opt/homebrew/bin on Apple Silicon,
/usr/local/bin on Intel).
Python 3.12 Default — Mandatory Pre-Merge Version Check
Python 3.12 is now the explicit default for all Hermes Cortex projects. All scripts,
cron jobs, and tooling assume 3.12+ with PEP 604 union syntax (str | None), PEP 695
type-parameter syntax, and match/case statements — all natively supported.
macOS ships Python 3.9 as the system default, so a version check is mandatory before
deploying any .py file to production (cron jobs, install scripts, MCP servers):
# Quick version check — must be 3.12+
python3 --version # Expected: Python 3.12+
python3 -c "import sys; assert sys.version_info >= (3, 12), 'Need 3.12+'"
The uv/pyenv-managed Python is the canonical interpreter. Install or locate it:
# uv-managed 3.12 (preferred)
ls ~/.local/bin/python3.12
# pyenv-managed
pyenv install 3.12 # or 3.13
pyenv global 3.12
# Homebrew (fallback)
brew install python@3.12
Verification checklist before deploying any new .py script:
- ✅
python3 --version resolves to 3.12+ (not macOS 3.9)
- ✅
python3 -c "compile(open('script.py').read(), 'script.py', 'exec')" passes
- ✅
python3 -c "import script" passes (no NameError, no missing imports)
- ✅
python3 script.py exits 0 or produces expected output
- ✅ All module-level function references resolve before their usage point
- ✅ Script is registered in
cortex-update.sh MAP if deployed via that path
What about older scripts? The repo no longer maintains 3.9 compatibility — scripts that
still use Optional[str] or Union[int, str] are legacy and should be updated to PEP 604
syntax when touched.
Installer Script Divergence (FIXED 2026-08-08)
PROBLEM (historical): Utility scripts were embedded inside install.sh as
heredocs that could drift apart from the standalone versions in ops/scripts/.
Confirmed divergences (v1.0.0 era):
heartbeat.py — embedded copy was a 7KB stub missing check_memory_sync_freshness()
and check_service(); repo version is 20KB with Linux systemd support
memory-to-brain-sync.py — embedded copy wrote separate files; repo version
writes current.md + monthly archive with YAML frontmatter
bootstrap-brain.sh / check-memory-budget.sh — embedded stubs
FIX (2026-08-08): ops/install/install.sh now resolves utility scripts via the
$(_scripts)/<name> helper (e.g. $(_scripts)/health/heartbeat.py), so the
repo copy is canonical and fresh installs deploy the real file. Heredocs
remain only as fallback for a few scripts (bootstrap-brain, check-memory-budget,
heartbeat, send-agent-learning, memory-to-brain-sync). Also added
ensure_agent_identity so first cortex-update works on fresh non-orch installs.
Guardrail: the doctor's check_install_sh_refs check (in
ops/scripts/manage/cortex_doctor/checks.py) verifies every $(_scripts)/<name>
reference resolves to a real file — it WARNS if a helper path breaks again.
For repo maintainers: if you add a new helper script, update install.sh's
_scripts()-referenced paths AND keep the doctor check green. Never let a
deployed inline copy drift from the repo source.
Project Bootstrapping — seed-project.sh
Hermes Cortex provides seed-project.sh to deploy the development harness (AGENTS.md, .hermes-cortex/ infrastructure, loop-governance scoring, pre-commit hooks, and project skills) to any project.
Quick Start
# Default: merge mode, all components, backup created automatically
bash ~/.hermes-cortex/scripts/seed-project.sh --project=/path/to/project --name="Project Name"
# Preview what would change
bash ~/.hermes-cortex/scripts/seed-project.sh --project=/path/to/project --mode=diff
# Deploy specific components only
bash ~/.hermes-cortex/scripts/seed-project.sh --project=/path/to/project \
--components=AGENTS.md,.hermes-cortex,pre-commit
# Custom skill set
bash ~/.hermes-cortex/scripts/seed-project.sh --project=/path/to/project \
--skill-refs=change-test-loop,engineering-approach,save-lesson
Modes
| Mode | Behavior | When to use |
|---|
| merge (default) | Backup existing, write only changed files (checksum delta). | First seed, routine updates |
| overwrite | Backup existing, then overwrite everything. | Known-clean target |
| diff | Preview. No writes. No backup. | Safety check before running |
Backup Architecture
Every seed creates a timestamped backup under .hermes-cortex/.seed-backups/<ts>/:
project/.hermes-cortex/.seed-backups/
├── 20260626_150000-12345/
│ ├── AGENTS.md ← backed up BEFORE modify
│ ├── .hermes-cortex/ ← excludes .seed-backups/ (circular)
│ ├── .git/hooks/pre-commit
│ └── manifest.json
Restore from latest: seed-project.sh --restore=/path/to/project
Restore from specific: seed-project.sh --restore=/path/to/project@<timestamp>
List backups: seed-project.sh --list-backups=/path/to/project
Components
| Component | What it deploys | Source |
|---|
| AGENTS.md | Templated agent guidelines with project name, date, commit. Includes contract rules 1-11 and loop-gov scoring section. | docs/templates/AGENTS.seed.md or custom --template |
| .hermes-cortex | Dir structure: sessions/archive/, memory/, skills/ + .gitignore (excludes memory, db, secrets). | Built-in |
| pre-commit | Loop-governance pre-commit hook via install-score-hook.sh | ops/scripts/pre-commit-score |
| loop-gov | score-cycle + loop-feedback wrappers in .hermes-cortex/loop-governance/ | Wrapper scripts |
| skills | Selected skills from ~/.hermes/skills/ into project's .hermes-cortex/skills/ | ~/.hermes/skills/ (via symlink) |
Pitfalls (Discovered During Development)
- Functions must
return, not exit. exit 0 inside a function kills the entire shell. Only exit from top-level main().
local var=$(cmd) swallows exit codes. With set -e, split into local var; var=$(cmd) || true to preserve error handling.
- Circular backup. Backups live inside
.hermes-cortex/.seed-backups/. Exclude .seed-backups/ when copying .hermes-cortex/ or it tries to copy itself.
- Restore must guard against missing data. Never
rm -rf the target before confirming the backup source exists. Move aside (never delete), then restore on failure.
- Timestamp collisions. Two seeds in the same second collide. Append
-$RANDOM to the timestamp.
- Template braces must match. A missing
} in {{PLACEHOLDER}} leaves the placeholder unexpanded.
Design Principles
Backup-first: Every deployment operation (seed, update, restore) creates a backup of any file it modifies. --no-backup requires explicit opt-in. The rationale: "I can't tell you how many times I wish I had a backup but didn't."
Idempotent by default: SHA256 delta engine only writes changed files. Re-seeding is safe.
Restore is a first-class operation: Every deployment tool must have a --restore flag. Backups are worthless without a restore mechanism.
References
references/seed-project-bootstrapping.md — Full reference with usage details, component docs, restore guide
docs/templates/AGENTS.seed.md — AGENTS.md template deployed by seed-project.sh
Skills Architecture: Three-Layer Model
Hermes Cortex uses a three-layer skill model with different purposes per layer. Understanding this hierarchy is essential for any agent working on the repo.
The Three Layers
| Layer | Location | Purpose | Managed by |
|---|
| Canonical source | ~/hermes-cortex/skills/ | Public reusable skills distributed by the installer. These are the curated set — ~40 skills across devops, software-development, MCP, github, etc. | git push to repo |
| Global installed | ~/.hermes/skills/ | Hermes Agent's primary skill directory (~150 skills). Contains cortex skills + ecosystem skills (apple/, creative/, gaming/, mlops/, testing/, etc.) | cortex-update.sh sync_skills() + manual additions |
| Project overrides | ~/hermes-cortex/.hermes-cortex/skills/ | Project-specific skill overrides tracked in the repo. Hermes checks this FIRST when working in the hermes-cortex repo, falling back to ~/.hermes/skills/ for anything not found here. These are condensed versions (e.g. 72-line agent-contract vs 990-line global). | Tracked in repo (project-specific) |
How Skills Flow
skills/ (canonical, ~40 skills)
↓ cortex-update.sh : sync_skills() checksums each SKILL.md
~/.hermes-cortex/skills/ → (symlink) → ~/.hermes/skills/ (global, ~150 skills)
↑
~110 ecosystem skills untouched by sync
Key relationship: ~/.hermes-cortex/skills/ is a symlink → ~/.hermes/skills/. When cortex-update.sh calls sync_skills(), it copies from skills/ to ~/.hermes-cortex/skills/, the write resolves through the symlink to ~/.hermes/skills/. Non-cortex skills (apple/, creative/, gaming/, etc.) are completely untouched — sync_skills() only overwrites files whose checksums differ from the source.
Contrast with Script Deployment
Scripts and skills flow in opposite directions:
Scripts: ops/scripts/ → ~/.hermes-cortex/scripts/ → (symlink) → ~/.hermes/scripts/
Skills: skills/ → ~/.hermes-cortex/skills/ → (symlink resolves to) ~/.hermes/skills/
Scripts: ~/.hermes-cortex/scripts/ is primary, ~/.hermes/scripts/ is the cron-resolution symlink target.
Skills: ~/.hermes/skills/ is primary (Hermes Agent loads from here), ~/.hermes-cortex/skills/ is the symlink pointing back.
cortex-update.sh sync_skills()
The sync_skills() function in cortex-update.sh uses a checksum-based delta engine:
# Called during every or delta update
# Compares SHA256 of skills/<file> vs installed destination
# Only copies when checksums differ — preserves non-cortex skills
sync_skills() {
local skill_repo="${REPO_DIR}/skills"
local skill_dest="${HERMES_HOME}/skills" # resolves via symlink → ~/.hermes/skills/
while IFS= read -r -d '' skill_file; do
if needs_update "$skill_file" "$dest"; then
copy_file "$skill_file" "$dest"
fi
done < <(find "$skill_repo" -name "SKILL.md" -type f -print0)
# Also syncs reference files under each skill's references/ directory
while IFS= read -r -d '' ref_file; do
if needs_update "$ref_file" "$dest"; then
copy_file "$ref_file" "$dest"
fi
done < <(find "$skill_repo" -path "*/references/*" -type f -print0)
}
Truncation guard (since 2026-08-02): sync_skills() refuses to overwrite a
FULL deployed SKILL.md with a truncated repo stub. A stub is detected by the
same markers the doctor's check_skill_stubs uses — the literal
Full content (truncated) string (Jul-17 1KB import stubs) or an
--- End skill --- dump under 1500 bytes. When the repo source is a stub and
the deployed copy is not, the copy is skipped with a SKILL STUB GUARD warning
(override with FORCE=true). Full repo sources always deploy (repo is the
source of truth). This prevents the 131 known repo stubs from clobbering full
copies on agents that still have them. Recovery path:
agent-skill-stub-audit.py --send on source agents restores full content into
skills/, then a normal cortex-update.sh syncs it out.
The delta engine means:
- Only skills in
skills/ are ever touched — ~110 unique ecosystem skills (apple/, creative/, gaming/, mlops/, testing/) are completely safe
- New skills added to
skills/ get installed on next cortex-update.sh
- Updated skill files (checksum changed) get overwritten automatically
- No manual copying needed — reverse direction (repo ← ~/.hermes/skills/) is for when you create a skill in the agent and want to commit it to the repo
Setup (Applied June 2026)
The symlink was created once:
# 1. Migrate any unique skills from old .hermes-cortex/skills/ that didn't exist in ~/.hermes/skills/
# Checked: only mcp-server-building and repo-organization were unique → copied manually
# 2. Remove old directory, create symlink
rm -rf ~/.hermes-cortex/skills
ln -s ~/.hermes/skills ~/.hermes-cortex/skills
# 3. Verify with cortex-update
bash ~/hermes-cortex/ops/scripts/cortex-update.sh
# Expected output: "Skills: N updated, M unchanged"
What This Means for Agent Sessions
When an agent works inside the hermes-cortex repo:
- Hermes checks
.hermes-cortex/skills/ (in the repo) first — these are project-specific overrides
- Falls back to
~/.hermes/skills/ (global, via symlink) for anything not in the overrides
- The overrides are intentionally condensed versions — e.g. the 72-line
agent-contract used when working on hermes-cortex vs the 990-line public version
For agents on other projects (not working in the hermes-cortex repo):
- Only
~/.hermes/skills/ is used (no .hermes-cortex/skills/ project override exists)
cortex-update.sh keeps these current automatically
Services (systemd on Linux, launchd on macOS)
Ollama Service
# Linux (systemd):
systemctl status ollama
# macOS (launchd):
launchctl list | grep com.ollama.serve
tail -f ~/Library/Logs/com.ollama.serve.out
mycortex-postgres (Docker)
# Status / logs (Docker container, both OSes)
docker ps | grep mycortex-postgres
docker logs --tail 50 mycortex-postgres
# Sync is a cron, not a daemon — see install-crons.sh / cronjob list
cronjob action=list | grep mycortex-sync
Hermes Plugin
The /brain slash command is installed at ~/.hermes/plugins/mycortex-command/
(legacy brain command was decommissioned 2026-08-02).
Enable the plugin after install:
hermes plugins enable mycortex-command
This takes effect on the next Hermes session. Verify with:
hermes plugins list | grep mycortex
# Should show "enabled" status
Usage:
/brain <query>
Files:
__init__.py — Plugin implementation with mycortex query integration
plugin.yaml — Hermes plugin configuration
Troubleshooting:
If /brain command doesn't work:
- Check plugin status:
hermes plugins list
- If "not enabled":
hermes plugins enable mycortex-command
- Start a new Hermes session (
/reset or restart CLI)
Hermes Langfuse Tracing Plugin
Hermes Agent ships a built-in Langfuse observability plugin (observability/langfuse) that auto-traces every LLM call, tool invocation, and conversation turn. After setting up Langfuse as a Docker server (step 6 of install.sh), you must explicitly enable and configure this plugin — it is NOT auto-activated by the installer.
Setup
# Install the Langfuse Python SDK
pip install langfuse
Create an API Key
Option A — Via Langfuse UI: Settings → API Keys (requires catching the ~2s serving window).
Option B — Direct DB insert (recommended when web container cycles):
- Generate key pair with Python (run from terminal where
bcrypt is available):
import bcrypt, hashlib, secrets
rand = secrets.token_hex(16)
sk = f"sk-lf-titus-{rand}a1b2"
pk = f"pk-lf-titus-{secrets.token_hex(16)}"
bc = bcrypt.hashpw(sk.encode(), bcrypt.gensalt(rounds=11)).decode().replace("$2b$", "$2a$")
- Write the SQL and execute via
docker cp (avoids shell $ expansion):
docker cp /tmp/insert-key.sql langfuse-postgres-1:/tmp/
docker exec langfuse-postgres-1 psql -U postgres -f /tmp/insert-key.sql
Set Environment Variables
Write to ~/.hermes/.env via Python (avoids shell $ expansion in the secret key):
lines = [
f"HERMES_LANGFUSE_PUBLIC_KEY={pk}",
f"HERMES_LANGFUSE_SECRET_KEY={sk}",
"HERMES_LANGFUSE_BASE_URL=http://localhost:3000",
"HERMES_LANGFUSE_ENV=local",
]
with open(os.path.expanduser("~/.hermes/.env"), "a") as f:
for line in lines:
f.write(line + "\n")
Enable the Plugin
hermes plugins enable observability/langfuse
# Takes effect on next Hermes session
Verification
hermes plugins list | grep langfuse should show "enabled"
- After a few conversation turns, traces appear in Langfuse UI (http://localhost:3000)
The langfuse SDK queues traces and retries on failure. The web container's restart cycle doesn't lose data — the SDK flushes during brief UP windows.
Post-Update Verification
Hermes Agent ships a built-in Langfuse observability plugin (observability/langfuse) that auto-traces every LLM call, tool invocation, and conversation turn. After setting up Langfuse as a Docker server (step 6 of install.sh), you must explicitly enable and configure this plugin — it is NOT auto-activated by the installer.
Setup
# 1. Install the Langfuse Python SDK
pip install langfuse
# 2. Create a Langfuse API key
# Option A: Via Langfuse UI at Settings → API Keys (requires catching the ~2s window)
# Option B: Via direct DB insert (when web container cycles restart)
Option B — Direct DB insert (recommended):
# Generate key pair
python3 -c "
import bcrypt, hashlib, secrets
rand = secrets.token_hex(16)
sk = 'sk-lf-titus-' + rand
pk = 'pk-lf-titus-' + secrets.token_hex(16)
bc = bcrypt.hashpw(sk.encode(), bcrypt.gensalt(rounds=11)).decode().replace('\$2b\$', '\$2a\$')
key_id = 'cmqkey-' + secrets.token_hex(8)
print(f'PK: {pk}')
print(f'SK: {sk}')
print(f'KEY_ID: {key_id}')
print(f'BCRYPT: {bc}')
" > /tmp/key-data.txt
# Insert into DB
eval "$(cat /tmp/key-data.txt | sed 's/ //g')"
docker exec langfuse-postgres-1 psql -U postgres -c "
INSERT INTO api_keys (id, created_at, note, public_key, hashed_secret_key,
display_secret_key, project_id, organization_id, scope, is_in_app_agent_key)
VALUES (
'KEY_ID_PLACEHOLDER', now(), 'Hermes Agent Tracing',
'PK_PLACEHOLDER', 'BCRYPT_PLACEHOLDER',
'sk-lf-titus-...LAST4', 'default-project',
(SELECT id FROM organizations LIMIT 1), 'PROJECT', false
);
"
NOTE: The $ in bcrypt hashes get mangled by shell expansion. Always use docker cp with a .sql file to insert the API key, not inline docker exec -c. See Troubleshooting → bcrypt Password Hash via docker cp for the safe pattern.
3. Set Environment Variables
# Add to ~/.hermes/.env (via Python to avoid shell $ expansion)
python3 -c "
import os, base64
lines = [
'HERMES_LANGFUSE_PUBLIC_KEY=pk-lf-titus-YOUR_PUBLIC_KEY',
'HERMES_LANGFUSE_SECRET_KEY=sk-lf-titus-YOUR_SECRET_KEY',
'HERMES_LANGFUSE_BASE_URL=http://localhost:3000',
'HERMES_LANGFUSE_ENV=local',
'HERMES_LANGFUSE_RELEASE=1.0.0',
]
with open(os.path.expanduser('~/.hermes/.env'), 'a') as f:
for line in lines:
f.write(line + chr(10))
"
4. Enable the Plugin
hermes plugins enable observability/langfuse
# Takes effect on next Hermes session
Verification
The plugin takes effect on the next Hermes session. To verify it's working:
# Check plugin status
hermes plugins list | grep langfuse
# Should show "enabled"
# After a few conversation turns, check Langfuse for new traces
# Data appears at http://localhost:3000 (during the ~2s serving window)
Important Notes
- The plugin sends traces for every LLM call via the langfuse Python SDK. The SDK queues traces and retries on failure, so brief server downtime doesn't lose data.
- The
BASE_URL must point to http://localhost:3000 for local setups. For remote access via nginx, use the external URL (e.g., https://your-domain.com:11002).
- The plugin is bundled with Hermes in
plugins/observability/langfuse/. It requires the langfuse pip package at runtime (installed in step 1).
Post-Update Verification
After pulling latest cortex code and running cortex-update.sh, verify the update actually took effect:
1. Check Deployment Map Completeness
New files in scripts/ (repo root) are NOT auto-registered unless they have a register() line in cortex-update.sh. Files in ops/scripts/ are only registered if explicitly listed. Run:
cd ~/hermes-cortex
git diff --name-only HEAD~1..HEAD | grep "^scripts/\|^ops/"
Then cross-reference against the register map in cortex-update.sh:
grep "^register " ~/.hermes/scripts/cortex-update.sh | grep -o '"[^"]*"' | head -80
git diff --name-only HEAD~1..HEAD | grep "^scripts/|^ops/"
Common misses: scripts at scripts/ root level (e.g. scripts/moses-inbox-remediate.sh), new skill directories under skills/ (these ARE auto-synced by sync_skills() — no register line needed).
2. Test Companion Scripts
New pipeline components often include companion scripts. Verify they run on the
canonical 3.12+ interpreter before deploying:
# Quick syntax and import check
python3 -c "compile(open('<new-script>.py').read(), '<new-script>.py', 'exec')"
python3 -c "import sys; sys.path.insert(0, '.'); import $(basename <new-script> .py)"
# Run the new script — expected: silent exit or [] (no pending items)
bash ~/.hermes/scripts/<new-script>.sh
# For .py scripts: verify import works and exit code matches watchdog pattern
python3 -c "import sys; sys.path.insert(0, '$HOME/.hermes/scripts'); import <script_basename>"
python3 ~/.hermes/scripts/<script>.py
echo "Exit: $?" # Should be 0
3. Verify New Cron Jobs Were Created
New pipelines often need new cron jobs that aren't auto-created by cortex-update.sh:
hermes cron list | grep <pipeline-name>
If missing, create them per the new skill's setup instructions. Common patterns:
- no_agent watchdog —
--schedule "every 10m" --script "<name>.sh" --no-agent --deliver origin
- LLM-driven processor —
--schedule "every 10m" --skills "<skill-name>" --enabled-toolsets terminal,file,web --deliver origin
4. Check for Stale System-Alert / Service-Recovery Scripts
The most common drift: installed watchdog scripts lag behind the repo. The `` flag catches these, but verify:
diff ~/hermes-cortex/ops/scripts/health/system-alert-watchdog.py ~/.hermes/scripts/system-alert-watchdog.py
diff ~/hermes-cortex/ops/scripts/health/service-recovery.py ~/.hermes/scripts/service-recovery.py
Zero output = in sync. Any diff means the installed version is stale.
5. Report Improvements to the Orchestrator
If the update revealed missing registrations, compatibility issues, or bugs, report them to the orchestrator via the git-based inbox immediately (see "Reporting Issues" below).
Reporting Issues to the Orchestrator
Standing directive: After every cortex update or install, report any bugs, missing registrations, compatibility issues, or improvement suggestions to the orchestrator (send to inbox_orchestrator — the shared orchestrator inbox). This is not optional — it's how the repo stays healthy.
Where to Send
Use the Agent Bus via inbox_send MCP tool. Do NOT write files directly to any filesystem path:
# Use inbox_send MCP tool — not file writes
# Example via Hermes MCP:
# inbox_send(to="moses", subject="<descriptive subject>", body="<details>", topic="moses")
| Tool | Purpose | Key params |
|---|
inbox_send | Send message to another agent | to, subject, body, topic, priority |
inbox_read | Read recent messages | limit, topic, unread_only |
inbox_watch | Check for new messages | limit |
⚠️ Agents must NOT write to ~/private-data/messages/inbox/ directly. The file-based inbox is deprecated. All agent messaging goes through the PGMQ Agent Bus. Agents running in the Hermes Cortex fleet get inbox_send / inbox_read / inbox_watch as MCP tools automatically.
Maintenance
Update mycortex
mycortex is a repo-managed Python CLI + Postgres schema — update it via the repo,
not a package manager:
# Pull latest hermes-cortex
cd ~/hermes-cortex && git pull --rebase
# Deploy updated CLI + schema
bash ~/hermes-cortex/ops/scripts/cortex-update.sh
# Verify version
~/.hermes-cortex/scripts/mycortex doctor
Apply mycortex migrations
Schema migrations run automatically from ops/services/mycortex/schema/
(apply via manage/mycortex sync / install.sh). To apply manually:
~/.hermes-cortex/scripts/mycortex sync --force
If migrations fail: run ~/.hermes-cortex/scripts/mycortex doctor and check
ops/services/mycortex/migrate.py output, then see Troubleshooting below.
Keep cortex-associated skills up to date
hermes skills check # Check all installed skills for updates
hermes skills update <name> # Update a specific skill
The hermes-cortex repo ships some local skills — these are updated via git pull in the repo, not via hermes skills update. Only hub/official skills are updated by hermes skills update.
Rebuild Knowledge Index
~/.hermes-cortex/scripts/mycortex sync --source mybrain --force
mycortex Source Migration: Merge Sources via Clean Reimport
Legacy mycortex export→wipe→reimport guidance is historical. For mycortex,
sources are registered via the mycortex CLI; consolidation uses the same
export→wipe→reimport pattern against ops/scripts/manage/mycortex.
See docs/design/mycortex-DESIGN.md and docs/legacy Postgres-migration.md.
Pitfalls (carried over):
- The
default source is protected — it appears after init but will have 0 pages. It cannot be removed or cleaned up.
- After wipe+reinit, run
mycortex sync --force to rebuild — no daemons to reload.
Verify:
~/.hermes-cortex/scripts/mycortex stats # Should match original page count
~/.hermes-cortex/scripts/mycortex sources list # mybrain has pages, default is empty
~/.hermes-cortex/scripts/mycortex search "test" --limit 1 # Returns results
mycortex PGLite Recovery
Removed — this system uses Postgres (pgvector).
PGLite recovery no longer applicable. See [docs/legacy Postgres-migration.md](docs/legacy Postgres-migration.md).
Embedding model verification:
curl -s http://localhost:11434/api/embeddings \
-d '{"model":"nomic-embed-text:v1.5","prompt":"test"}' | \
python3 -c "import sys,json; d=json.load(sys.stdin); print(f'{len(d[\"embedding\"])}d')"
# Expected: "768d"
Health Check
# Run the heartbeat script (deployed path)
python3 ~/.hermes-cortex/scripts/heartbeat.py
# Check all components
~/.hermes-cortex/scripts/mycortex doctor
curl -s http://127.0.0.1:11434/api/tags
# Verify mycortex sources have indexed pages
bash ~/.hermes-cortex/scripts/bootstrap-brain.sh --check-only
# Check memory budget
bash ~/.hermes-cortex/scripts/check-memory-budget.sh --report
mycortex Source Management Quick Reference
Useful commands for managing sources day-to-day:
# List all sources with page counts
~/.hermes-cortex/scripts/mycortex sources list
# Sync a single source (or --force all)
~/.hermes-cortex/scripts/mycortex sync --source mybrain
# Search across sources
~/.hermes-cortex/scripts/mycortex search "query" --limit 5
# Stats / doctor
~/.hermes-cortex/scripts/mycortex stats
~/.hermes-cortex/scripts/mycortex doctor
Cron integration: The agent-mycortex-sync cron job runs sync automatically;
hermes-cortex-sync runs health checks and reports to Telegram.
Security Posture
After installation, run a quick security audit to lock down file permissions and verify network exposure.
File Permission Lockdown
Hermes stores API keys, conversation history, and project data in ~/.hermes/. Several of these files default to world-readable (0644) and should be restricted to owner-only:
chmod 600 ~/.hermes/config.yaml
chmod 600 ~/.hermes/.hermes_history
chmod 600 ~/.hermes/kanban.db
chmod 600 ~/.hermes/SOUL.md
chmod 600 ~/.hermes/interrupt_debug.log
chmod 600 ~/.hermes/gateway.lock ~/.hermes/gateway.pid
for f in ~/.hermes/config.yaml.bak.*; do chmod 600 "$f" 2>/dev/null; done
Verify everything is 0600 except logs and cache dirs:
cd ~/.hermes && stat -f "%Lp %N" config.yaml .hermes_history kanban.db SOUL.md auth.json .env
Bind Address Audit
Verify no services are exposed beyond localhost:
lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null | grep -v "127.0.0.1\|::1"
Expected output: only macOS system processes (ControlCenter, Docker Desktop internal ports, rapportd). No user services should appear.
If a service binds to 0.0.0.0, fix it in its config (e.g. app.run(host="127.0.0.1") for Flask, --host 127.0.0.1 for Ollama).
nginx — Local-Only Unload
nginx is only needed for external/public access (TLS reverse proxy). For local-only setups, it's dead weight that can sit in error state with log-permission issues. Unload it:
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.nginx.plist
brew services stop nginx
If you later want external access, fix the log-permission issue first:
sudo chown "$(whoami):staff" /opt/homebrew/var/log/nginx/*.log
# then start nginx
brew services start nginx
Restart Resilience Check
All services should survive reboot via launchd RunAtLoad or Docker's restart:always policy:
# Verify launchd services
launchctl list | grep -E "(ollama|mycortex|cortex|hermes|docker)"
# Verify Docker container restart policies
for c in $(docker ps -a -q); do
docker inspect "$c" --format '{{.Name}} {{.HostConfig.RestartPolicy.Name}}'
done
Expected: all Hermes Cortex services (ollama.serve, mycortex.sync-watch, cortex-dashboard, ai.hermes.gateway) present with RunAtLoad. Langfuse containers show always, other project containers show always or unless-stopped.
Security Checklist (Post-Install)
| Check | Command | Pass |
|---|
| Files locked 0600 | ls -la ~/.hermes/.env ~/.hermes/config.yaml | Owner-only |
| No public bindings | lsof -iTCP -sTCP:LISTEN | 127.0.0.1 only |
| Firewall enabled | /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate | State = 1 |
| Docker restart:always | docker inspect $(docker ps -qa) --format '{{.Name}} {{.HostConfig.RestartPolicy.Name}}' | always/unless-stopped |
| SSH remote login off | sudo systemsetup -getremotelogin (ask) | Off |
| SSH private key perms | ls -la ~/.ssh/ | Private keys 0600 |
Troubleshooting
mycortex Command Not Found
# Check if installed
~/.bun/bin/mycortex --version
# Reinstall if needed (NOT from npm!)
bun install -g github:garrytan/mycortex
# Ensure PATH includes ~/.bun/bin
export PATH="$HOME/.bun/bin:$PATH"
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.zshrc
offline_knowledge Command Not Found
# Check if installed
ls -la ~/.hermes/bin/offline_knowledge
# ~/.hermes/bin is NOT in PATH by default — add it:
export PATH="$HOME/.hermes/bin:$PATH"
echo 'export PATH="$HOME/.hermes/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
# Or use full path:
~/.hermes/bin/offline_knowledge stats
Langfuse Containers Not Starting (v3)
The one-command setup script cortex-setup-langfuse.sh generates
all required secrets and deploys the stack:
bash ~/.hermes/scripts/cortex-setup-langfuse.sh --start
This creates ~/langfuse/.env with 11 auto-generated secrets, copies
the docker-compose file, and runs docker compose up -d. No more
staring at :? expansion errors because someone forgot to create .env.
Common causes (if manual setup was used):
- Missing
LANGFUSE_ENCRYPTION_KEY (32-byte hex) — required at startup
- Missing
CLICKHOUSE_MIGRATION_URL — must use clickhouse://clickhouse:9000 (Go driver TCP), NOT http://localhost:8123
CLICKHOUSE_CLUSTER_ENABLED must be false without Zookeeper
- Missing
LANGFUSE_S3_EVENT_UPLOAD_BUCKET — Zod schema rejects missing required S3 vars
cd ~/langfuse
docker compose logs langfuse-web # Check for ZodError
docker compose logs langfuse-worker
docker compose up -d
# Force recreate containers if env vars changed
docker compose down
docker compose up -d
# Check health after 30s
curl http://localhost:3000/api/public/health
Full env reference: See references/langfuse-v3-migration.md for all required vars and typical values.
Langfuse Web Fails with P1000 Auth Error
Symptom: Error: P1000: Authentication failed against database server in docker logs langfuse-langfuse-web-1. The web container starts, runs Prisma migrations, then crashes repeatedly.
Root cause: The DATABASE_URL in docker-compose.yml was written as a literal placeholder (***) instead of a variable reference (**). When .env is regenerated (e.g., after mycortex migration or Langfuse reinstall), Postgres gets the new password from .env via **, but the web container's DATABASE_URL retains the old/hardcoded value.
Diagnose:
# 1. Check for the error
docker logs langfuse-langfuse-web-1 --tail 10 2>&1 | grep P1000
# 2. Verify the compose file has variable reference, not literal placeholder
sed -n '/DATABASE_URL/p' ~/langfuse/docker-compose.yml | xxd
# $ = 0x24 → variable reference (correct)
# * = 0x2a → literal asterisk (wrong — hardcoded placeholder)
Fix:
# Update docker-compose.yml to use variable reference
sed -i '' 's/postgresql:\\/\\/postgres:\\*\\*\\*@postgres/postgresql:\\/\\/postgres:***@postgres/' ~/langfuse/docker-compose.yml
# Force-recreate BOTH web and worker
docker compose -f ~/langfuse/docker-compose.yml up -d --force-recreate langfuse-web langfuse-worker
Also fix the repo source:
sed -i '' 's/postgresql:\\/\\/postgres:\\*\\*\\*@postgres/postgresql:\\/\\/postgres:***@postgres/' ~/hermes-cortex/ops/install/deploy/docker-compose.langfuse.yml
Langfuse Web Container Cycles After Init
After fixing the auth error, the web container may still cycle. Next.js exits with code 0 after init scripts complete (creating default org/user/project). Docker restart: always triggers a new cycle. The container serves HTTP for only ~1-4s between MCP registration and exit.
Detection:
docker inspect langfuse-langfuse-web-1 --format '{{.RestartCount}}'
# Shows 6+ restarts
Catching the brief window (browsing works but browser starts too slow):
for i in $(seq 1 60); do
r=$(curl -sS --max-time 2 http://localhost:3000/ 2>/dev/null)
[ -n "$r" ] && echo "$r" | head -5 && break
sleep 2
done
Langfuse Stale Users Cause Init Hang
After multiple restart cycles, Next.js init scripts (which create default org/project/user) may hang permanently if user records already exist. Container stays up 5+ minutes without reaching MCP registration. Fix:
DELETE FROM users WHERE email IN ('user@example.com', 'hermes2@example.com');
CASCADE handles related records. Next container restart recreates them cleanly.
bcrypt Password Hash via docker cp
When .env password doesn't match the DB hash, update via SQL file to avoid shell $ expansion mangling the hash:
python3 -c "
import bcrypt
pw = 'plaintext_password'
salt = bcrypt.gensalt(rounds=12).decode().replace('\$2b\$', '\$2a\$')
h = bcrypt.hashpw(pw.encode(), salt.encode()).decode().replace('\$2b\$', '\$2a\$')
print(h)
" > /tmp/hash.txt
echo "UPDATE users SET password = '$(cat /tmp/hash.txt)' WHERE email = 'user@example.com';" > /tmp/fix.sql
docker cp /tmp/fix.sql langfuse-postgres-1:/tmp/
docker exec langfuse-postgres-1 psql -U postgres -f /tmp/fix.sql
Langfuse uses $2a$ bcrypt prefix (not Python's default $2b$) — replace after generation.
Cortex Dashboard Not Loading
# Check launchd service
launchctl list | grep cortex-dashboard
# Check logs
tail -50 ~/.hermes/logs/cortex-dashboard.log
# Restart
launchctl unload ~/Library/LaunchAgents/com.hermes.cortex-dashboard.plist
launchctl load ~/Library/LaunchAgents/com.hermes.cortex-dashboard.plist
nginx 502 Bad Gateway
# Check backend services are running
curl http://localhost:3000 # Langfuse v3 (health endpoint)
curl http://localhost:8901 # Dashboard
# Check nginx config
nginx -t
# Apple Silicon: /opt/homebrew/var/log/nginx/*.log
# Intel Mac: /usr/local/var/log/nginx/*.log
tail -50 /opt/homebrew/var/log/nginx/*.log 2>/dev/null || tail -50 /usr/local/var/log/nginx/*.log
# Restart nginx
brew services restart nginx
Plugin Not Working
# Verify enabled
hermes plugins list
# If "not enabled":
hermes plugins enable mycortex-command
# Start a new Hermes session (/reset or restart CLI)
Autopilot Shows synced=0 — No Sources Registered (HISTORICAL — legacy brain removed)
Legacy mycortex daemon diagnosis. With mycortex, "0 pages" means the sync cron
hasn't run or brain dirs are empty — see "Existing Repo Setup — Seeding Brain
Content" and run ~/.hermes-cortex/scripts/mycortex sources list.
Symptom: Autopilot cycles every ~150s but every cycle shows synced=0 extracted=0 embedded=0 and orphans=N. Brain directories under ~/brain/ have content but mycortex doesn't index anything.
Root cause: The installer creates brain directories with git repos and content, but does NOT register them as mycortex sources. Only the built-in default source exists — and it's a federated source with no filesystem path, so it can never sync from disk.
Diagnostic sequence:
# 1. Check autopilot is running
launchctl list | grep mycortex
# 2. Check autopilot cycles — look for synced=0
tail -20 ~/.legacy-brain/autopilot.log
# 3. Verify brain directories have content
ls ~/brain/
# 4. (Postgres engine — autopilot no longer blocks CLI access)
# Skip straight to CLI commands
# 5. Check what sources are registered
mycortex sources list
# If only "default" appears, no named sources exist
# 6. Check DB stats
mycortex stats
# 7. Compare registered sources vs brain directories
# For each dir in ~/brain/ that has a git repo, register it
for d in ~/brain/*/; do
name=$(basename "$d")
if [ -d "$d/.git" ]; then
echo "Need to register: $name"
fi
done
# 8. Register each project as a mycortex source
mycortex sources add <name> --path ~/brain/<name> --name "<Name>"
# 9. Do initial sync
mycortex sync --all --no-pull
mycortex extract --stale
# 10. Autopilot runs independently — no manual restart needed
# CLI commands work concurrently with autopilot on Postgres
Pitfalls:
mycortex sources list and mycortex stats work concurrently with autopilot on Postgres (pgvector). No need to stop autopilot.
- Every brain directory must be a git repo —
git rev-parse --is-inside-work-tree confirms. If not, git init && git add -A && git commit -m "initial".
- The
default source is built-in and cannot be removed or configured with --path. Skip it.
- After registration, wait for ~2 autopilot cycles before the
/brain slash command returns results.
mycortex sync fails
PGLite engine removed. This system uses Postgres (pgvector).
No longer applicable — mycortex migrated to Postgres.
See [docs/legacy Postgres-migration.md](docs/legacy Postgres-migration.md) for troubleshooting with Postgres.
mycortex Embedding Times Out on Large Documents
Symptoms:
Error embedding <slug>: [embed(ollama:nomic-embed-text:v1.5)] The operation timed out.
Some pages embed successfully (showing progress) but others fail partway through. Large daily memory files and long reference docs fail consistently.
Root cause: The default mycortex embed timeout (AI_EMBED_TIMEOUT_MS) is 60 seconds. Ollama's nomic-embed-text:v1.5 takes longer than 60s to generate 768-dim embeddings for large documents.
Fix: Set the environment variable in ALL mycortex scripts:
export MYCORTEX_AI_EMBED_TIMEOUT_MS=300000
mycortex embed --stale
Files that need this env var:
~/.legacy-brain/autopilot-run.sh — before the exec line
~/.hermes/scripts/mycortex-nightly-dream.sh — after PATH export
~/.hermes/scripts/mycortex-update-sync.sh — same
Verification:
# Before: check current embedded count
mycortex stats | grep "Embedded"
# After retry
export MYCORTEX_AI_EMBED_TIMEOUT_MS=300000
mycortex embed --all 2>&1 | tail -5
# Expected: "Embedded X chunks across Y pages" with 0 errors
mycortex stats | grep "Embedded"
# Expected: Embedded = Chunks
mycortex Migration Failures
Migrations can stall or fail for several reasons. Follow this diagnostic chain:
1. Check what failed
cat ~/.legacy-brain/migrations/completed.jsonl | grep -E '(fail|partial|retry)' | tail -5
Look for the last "status":"partial" or "status":"failed" entry — it specifies which phase
and why (e.g. "source \"mybrain\" has uncommitted changes").
2. Clean the brain repo if dirty
The v0.32.2 migration (facts fence) refuses to write if the brain directory has uncommitted
changes. Fix:
cd ~/brain/default
git status --short # check for dirty files
git add -A && git commit -m "clean state before migration"
Then re-run mycortex apply-migrations --yes.
3. Unstick a wedged migration
If mycortex apply-migrations --yes reports a migration is "WEDGED (3+ consecutive partials)":
mycortex apply-migrations --force-retry <version> --yes # e.g. 0.32.2
mycortex apply-migrations --yes
4. Autopilot lock times out all mycortex CLI commands
Symptoms: Every mycortex CLI command hangs or times out — mycortex stats, mycortex sources list, mycortex search, all of them.
Postgres engine: With Postgres (pgvector), CLI commands can run concurrently with the autopilot — no lock contention. If commands still hang, the issue is something else (see other troubleshooting steps or [docs/legacy Postgres-migration.md](docs/legacy Postgres-migration.md)).
For legacy PGLite, the autopilot held the exclusive database lock. This is no longer the case with Postgres.
Diagnose: Check if mycortex processes are running:
ps aux | grep mycortex | grep -v grep
Fix:
# Release the lock by stopping both mycortex services
launchctl bootout gui/$(id -u)/legacy autopilot 2>/dev/null || true
launchctl bootout gui/$(id -u)/com.legacy-brain.sync-watch 2>/dev/null || true
sleep 2
# Now CLI commands will respond
mycortex <command>
# Reload services
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/legacy autopilot.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.legacy-brain.sync-watch.plist
bootout stops the process cleanly — no lock files to remove. After the CLI work, bootstrap (not load) reloads both services. Verify they're running:
launchctl list | grep mycortex
# Should show PID (running), exit code 0
Ollama Not Responding
# Check service status
launchctl list | grep com.ollama.serve
# Restart service
launchctl stop com.ollama.serve
launchctl start com.ollama.serve
# Wait for startup (can take 5-10 seconds)
sleep 5
curl http://127.0.0.1:11434/api/tags
Sync Daemon Not Running
# Check if loaded
launchctl list | grep com.legacy-brain.sync-watch
# Reload if needed
launchctl unload ~/Library/LaunchAgents/com.legacy-brain.sync-watch.plist
launchctl load ~/Library/LaunchAgents/com.legacy-brain.sync-watch.plist
Public vs Private Repo Split
Hermes Cortex uses a two-repo architecture:
| Repo | Visibility | Contents |
|---|
hermes-cortex (public) | Public | ops/install/install.sh, ops/install/deploy/ (compose files), ops/services/dashboard, nginx configs, skeleton config, architecture docs, skills, bump-version script |
private-data (private) | Private | Full config.yaml with personal settings, brain content on brain-* branches, custom scripts, SSL certs |
Domain Privacy Rule: Use example.com as placeholder in public repo files. Put your real domain in private repo only. See references/public-repo-privacy.md for the full pattern including git history rewriting.
What the public installer NOW includes:
- ✅ Ollama (systemd/launchd service)
- ✅ mycortex (Postgres + pgvector via Docker, schema migrations, CLI)
- ✅ Brain directory structure + .gitignore + git init
- ✅ mycortex sync cron (no daemon)
- ✅
/brain Hermes plugin (mycortex-command)
- ✅ Langfuse (Docker Compose, auto-generated secrets)
- ✅ Cortex Dashboard (Flask app + systemd/launchd)
- ✅ nginx reverse proxy (SSL or local-only mode)
- ✅ Utility scripts (heartbeat.py, memory-to-brain-sync.py, check-memory-budget.sh, etc.)
- ✅ Cron jobs, hooks, MCP servers (loop-governance, tasks)
What still lives in private repo:
- Full personal
config.yaml with custom settings
- Brain content (on
brain-* branches, not main)
- Custom utility scripts beyond the core set
- SSL certificates for nginx
To apply personal config after public install:
git clone <private-repo-url> ~/private-data
cp ~/private-data/config/config.yaml ~/.hermes/config.yaml
Architecture:
- Public repo: full installer + observability stack (safe to share)
- Private repo: secrets + personal config + brain content
Offline Code Assistant
A local RAG-powered coding assistant using Ollama embeddings + a curated snippet corpus spanning 27 languages with 367 examples. Two-tier system: small model + RAG ≈ productivity of a 7B model. All data stays on your machine.
Setup
The code corpus lives in the repo at offline/code-corpus/. Build the index:
cd ~/hermes-cortex
bash offline/prep-code.sh
This generates code snippet files across 20+ languages, builds embeddings with nomic-embed-text:v1.5, and writes the index to ~/offline/code-index.json.
Ollama models needed:
nomic-embed-text:v1.5 — embeddings (pulled by install.sh)
qwen2.5:3b — code generation (~1.7 GB, must pull manually: ollama pull qwen2.5:3b)
Usage
# From the repo root
python3 offline/offline_code.py search "flask rest api" # Find relevant snippets
python3 offline/offline_code.py gen "binary search tree" # Generate code via Ollama
python3 offline/offline_code.py index # Rebuild the search index
python3 offline/offline_code.py stats # Corpus statistics
search — Returns snippets ranked by cosine similarity (nomic-embed-text:v1.5), with language, tags, and score. Top matches include full code blocks.
gen — Finds the most relevant snippets via RAG, injects them as context, then generates code with qwen2.5:3b. Falls back gracefully if the model isn't pulled.
Corpus languages: Python, JavaScript, TypeScript, Go, Rust, C, C++, C#, Java, Kotlin, Swift, Dart, Elixir, PHP, Ruby, Lua, R, Shell, SQL, Terraform, Docker, Nix, Zig, PowerShell, Kubernetes (27 languages, 367 snippet files).
Top tags: pattern, api, web, algorithm, cli, io, net, util, async, testing, security, config, data.
Pitfalls
- Model not found on
gen — Run ollama pull qwen2.5:3b first. The 1.7 GB download takes 2-3 minutes on broadband.
- Outdated index — Rebuild with
offline_code.py index --force after adding new snippet files.
|- Python version — The corpus generation needs Python 3.12+ (3.9's sqlite3 can't enable_load_extension). Use python3.12 which is the Hermes default.
Offline Content
The repo bundles several offline content tools for low-connectivity environments:
| Tool | Description | Script |
|---|
| Bible downloader | Downloads public domain Bible translations (KJV, WEB, ASV, etc.) | offline/prep-bible.sh |
| Hymn collection | Downloads Open Hymnal Project content (PDFs, ABC, MIDI) | offline/prep-hymns.sh |
| Offline reader | Local web UI for Bible and hymns | offline/offline-reader.py |
| Auto-update | Weekly cron job that checks for content updates silently | offline/auto-update.sh |
| Knowledge cascade | mycortex + kiwix ZIM + web_cache fallback chain | offline/offline_knowledge.py |
Known gap — no lesson subcommand: The offline_knowledge tool has bible and hymns subcommands but NOT lesson — offline_knowledge lesson index and offline_knowledge lesson search do not exist. Lessons live as markdown files in ~/brain/*/lessons/ indexed by mycortex, not as a separate offline knowledge corpus. See references/offline-knowledge-subcommands.md for the subcommand architecture pattern and how lessons would be added.
Known Issues — Bible Prep
prep-bible.sh has an unbound variable bug ($tmp_txt never assigned) that crashes the download. bible-parse.py has 4 pattern-matching issues with KJV headers (colons, "Saint" vs "St.", "General" prefix) and cannot parse WEB's "Book NN Name" format at all. See references/bible-prep-issues.md for the full reproduction details and fixes — send this to the repo owner.
Auto-Update
The auto-update.sh script is designed for weekly cron scheduling:
- Checks internet before doing anything (silent exit if offline)
- Only produces output when something actually changed
- Logs to
~/offline/auto-update.log
# Check & update everything
./offline/auto-update.sh
# Check only, no downloads
./offline/auto-update.sh --check
# Verbose mode
./offline/auto-update.sh --verbose
Architecture: Uber-Agent + mycortex Knowledge Separation
Current approach (as of June 2026): One single Hermes agent (default profile) with knowledge isolation via mycortex sources, not Hermes profiles. This replaced the earlier profile-per-project model.
Rationale