| name | hermes-deploy-pipeline |
| description | Enforce the Hermes deploy pipeline — always write to ~/.hermes first, test, then deploy to ~/.hermes_prod. Use whenever creating or modifying launchd jobs, scripts, configs, or any file that needs to run in the Hermes production environment. |
| when_to_use | Use when: creating launchd jobs, modifying scripts that run via launchd, adding cron jobs, updating config files, or anything that needs to survive across sessions and be in the prod environment. Also use when the user says 'make this permanent', 'add this to the schedule', 'create a cron job', or 'set this up.' |
| arguments | ["file_path","description"] |
| argument-hint | <file_path> [description] |
| context | inline |
Hermes Deploy Pipeline
The Rule
Always write to ~/.hermes (staging) first. Test. Then deploy to ~/.hermes_prod/ via scripts/deploy.sh.
Going straight to prod or using wrong paths causes:
- Staging/prod drift (untracked changes in prod)
- Git history bypassed (no commit record)
- Broken rollback capability
The Contract
When adding any file that needs to run permanently in the Hermes environment:
- Write to
~/.hermes/ — not ~/.hermes_prod/, not ~/.hermes/ (prod symlink), not any ad-hoc path
- Test in staging — run the script/launchd manually, verify it works
- Commit to git —
cd ~/.hermes && git add -A && git commit && git push origin main
- Deploy via pipeline —
~/.hermes/scripts/deploy.sh
⚠️ The "fix already on origin/main, deployed is stale" case has a separate recipe. When the fix you need is already merged to origin/main but the deployed file is stale AND staging is on a dirty non-main branch (so deploy.sh will die on the uncommitted-changes check), do NOT open a duplicate PR. The user's "commit to main" intent is already satisfied by the existing commit. The right move is the surgical-sync recipe in references/staging-dirty-surgical-sync.md (worktree from origin/main → cp to staging + prod → launchctl kickstart -kp to force re-exec). Verified 2026-06-20 against PR #619 / ai.hermes-watchdog false-DOWN alert loop.
File Routing
| File type | Write to | Notes |
|---|
| launchd plists | ~/.hermes/launchd/ | Then copy to ~/Library/LaunchAgents/ after deploy |
| Scripts (hermes-managed) | ~/.hermes/scripts/ | Deployed to prod via sync step |
| Cron jobs | Use cronjob tool | Registers in hermes_prod's kanban.db directly |
| Config files (prod) | ~/.hermes/config.yaml | See "Config File Editing" section below — deploy.sh does NOT sync config.yaml to prod. Manual cp + restart required. |
| Config files (prod) | ~/.hermes/config.yaml (staging) ↔ ~/.hermes_prod/config.yaml (prod, byte-identical mirrors) | See "Config File Editing" section below — deploy.sh does NOT sync config.yaml to prod. Manual cp + launchctl kickstart -k required. As of Hermes v0.13.0 (2026-06-25), there is NO config.staging.yaml — older docs that reference it are stale. The user-facing "iteration budget" knob is agent.max_turns (line 20), NOT max_iterations — see references/two-config-files-prod-vs-staging.md. |
| Hermes Agent core files | ~/projects/hermes-agent/ (staging) | Primary write target for run_agent.py, agent/ modules. projects_other/ is an AO worker mirror, NOT a write target. Deploy syncs staging → prod. |
| Skills | ~/.hermes/skills/ | Deployed via deploy pipeline |
| Skills (project-specific thin pointers) | ~/.hermes/skills/<name>/SKILL.md | Contains only path to canonical skill in repo + hard-gate reminders. Canonical lives in .claude/skills/ inside the project repo. See skill-creator → "Project-specific skills" section. |
| Skills (wrapper over a bundled skill — binary substitution) | ~/.hermes/skills/<name>/SKILL.md | Re-exports a bundled skill (e.g. , ) but swaps the because the user has a bashrc wrapper that pins provider/env (e.g. = ). Two-mode structure mirrors the bundled skill; only the binary name changes. Edit the bundled skill in place instead ONLY if you can guarantee no upstream re-deploy will overwrite you; otherwise the wrapper survives a future . Worked example 2026-07-24: (v1.0.0) re-exports (v2.2.0) with as the default binary; live-verified round-trips to MiniMax-M3 in ~6-8s. |
Config File Editing
~/.hermes/config.yaml is the canonical staging config (gitignored — has tokens baked in). ~/.hermes_prod/config.yaml is the prod live copy. The deploy.sh script does NOT sync these — it only does git pull + restart + canary. For config.yaml changes:
- Edit
~/.hermes/config.yaml (staging, source of truth)
cp ~/.hermes/config.yaml ~/.hermes_prod/config.yaml to sync
diff ~/.hermes/config.yaml ~/.hermes_prod/config.yaml to verify
- Restart prod gateway: see "Gateway Restart" below
- Verify with
python3 -c "from hermes_cli.config import load_config; print(load_config().get('auxiliary', {}).get('vision', {}))" from a venv that has hermes-agent installed (e.g. ~/.hermes/.venv)
Protected vs editable config: The "no changes without approval" rule in ~/.hermes/AGENTS.md and ~/.hermes_prod/AGENTS.md only covers hermes.json and hermes.staging.json (gateway runtime: timeoutSeconds, maxConcurrent, subagents.maxConcurrent, main model selection). Other sections of config.yaml (auxiliary.*, openrouter.*, bedrock.*, prompt_caching.*, compression.*) are NOT in the protected set and can be edited normally. When in doubt, check scripts/doctor.sh — it enforces the protected values and will FAIL on drift.
⚠️ Iteration-budget confusion (2026-06-25, Hermes v0.13.0). The user's "iteration budget" maps to agent.max_turns at line 20 of ~/.hermes/config.yaml — NOT max_iterations (which is delegation.max_iterations: 500 at line 348, a subagent fan-out cap), NOT goals.max_turns: 20 at line 361, and there is NO config.staging.yaml in this install (older docs referenced it — stale). The two configs that EXIST are byte-identical mirrors: ~/.hermes/config.yaml (staging) and ~/.hermes_prod/config.yaml (prod). Both must be edited together. Diagnostic one-liner before any "bump the budget" task:
grep -nE "^\s*max_turns:|^\s*max_iterations:" ~/.hermes/config.yaml ~/.hermes_prod/config.yaml
Code default is 90; current value is 60 (deliberately lowered, not a bug). doctor.sh does NOT enforce max_turns (only maxConcurrent/timeoutSeconds/subagents.maxConcurrent), so AGENTS.md "no changes without approval" is the only gate. Full recipe + three-knob table + root-cause checklist for "edit didn't stick": references/two-config-files-prod-vs-staging.md.
Gateway Restart (without full deploy)
deploy.sh does the full pipeline (pull + restart + canary). For a config-only change you can do the same restart pattern manually:
DOMAIN="gui/$(id -u)"
LABEL="ai.hermes.prod"
PID="$(launchctl print "${DOMAIN}/${LABEL}" 2>/dev/null | grep '^ *pid' | awk '{print $3}' || true)"
[ -n "$PID" ] && kill -TERM "$PID" 2>/dev/null
for i in $(seq 1 15); do curl -sf --max-time 3 http://127.0.0.1:8643/health >/dev/null 2>&1 && break; sleep 2; done
curl -sf http://127.0.0.1:8643/health
bash ~/.hermes/scripts/hermes-canary.sh
Note: launchctl print may return empty pid= if the job is registered but not currently running. In that case launchd respawns on its own; just wait on the health endpoint.
For non-gateway launchd jobs (e.g. ai.hermes.watchdog) the restart is different. launchctl bootout on a running job fails with Boot-out failed: 3: No such process, and bootstrap after that fails with Bootstrap failed: 5: Input/output error. The safe nudge for an already-registered, currently-not-running job is launchctl kickstart -kp gui/$(id -u)/<label> — it re-execs the script on the next interval (or immediately, depending on the job's StartInterval). See references/staging-dirty-surgical-sync.md for the full recipe.
Provider Attribution Headers (OpenRouter / Vercel AI Gateway)
If a user reports "why is my OpenRouter dashboard showing my app name and URL on every call?" — this is by design. The auxiliary client (projects_other/hermes-agent/agent/auxiliary_client.py) hardcodes attribution headers for OpenRouter and Vercel AI Gateway:
_OR_HEADERS_BASE = {
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Title": "Hermes Agent",
"X-OpenRouter-Categories": "productivity,cli-agent",
}
_AI_GATEWAY_HEADERS = {
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Title": "Hermes Agent",
"User-Agent": f"HermesAgent/{_HERMES_VERSION}",
}
OpenRouter reads X-Title for the App column and HTTP-Referer for the Referer column. The "Subject" column in OpenRouter's dashboard is the first user-message text of the call — so prompts like "Favicon for google" or "Favicon for https://hermes-agent.nousresearch.com/" appear verbatim. See references/openrouter-attribution-headers.md for the full diagnostic recipe (favicon auto-captioning, vision_analyze default model, why Gemini 3 Flash appears in the dashboard).
Launchd Job Lifecycle
Write plist → ~/.hermes/launchd/<name>.plist
↓
Test: launchctl load ~/Library/LaunchAgents/<name>.plist
↓ (if works)
Commit + push to origin main
↓
deploy.sh → syncs to ~/.hermes_prod/
↓
Load in prod: launchctl load ~/Library/LaunchAgents/<name>.plist
launchd plist location: Always ~/Library/LaunchAgents/ for user-level agents. ~/.hermes/launchd/ holds the canonical source; deploy syncs to prod.
Launchd EnvironmentVariables and the login-env wrapper pattern (added 2026-07-09)
launchd launches plists with a stripped PATH (/usr/bin:/bin:/usr/sbin:/sbin) and no .bashrc sourced. Any command the daemon needs that lives outside that default — br at ~/.cargo/bin/br, gh at /opt/homebrew/bin/gh, sqlite3 if not in the system path, etc. — will silently fail with FileNotFoundError: [Errno 2] No such file or directory: '<tool>' and exit 1. The launchd state = not running, last exit code = 0 then hides the crash behind KeepAlive=true's restart loop: the daemon churns every StartInterval, every tick dies at the same line, and the user sees a stale-looking log.
Two patterns fix this — pick the one the plist already uses:
(A) EnvironmentVariables block in the plist (recommended for new plists). Add a <key>EnvironmentVariables</key><dict> block with the keys the daemon needs:
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>$HOME/.cargo/bin:$HOME/.local/bin:$HOME/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
<key>HERMES_SLACK_BOT_TOKEN</key>
<string>[REDACTED_SLACK_TOKEN]</string>
<key>FACTORY_SLACK_CHANNEL_ID</key>
<string>C0REPLACEMEWITHCHANNELID</string>
</dict>
(B) launchd-wrapper.sh indirection (existing pattern in dark-factory's ai.dark-factory.af-tick.plist). The plist invokes a wrapper script that sources ~/.bashrc (or ~/.zshrc) before exec'ing the actual tick script. This is less secure for secrets (bashrc typically contains tokens) but gives the daemon the full user PATH including all installed CLIs. The wrapper exists at daemon/launchd/launchd-wrapper.sh in dark-factory; copy the pattern, do not bypass it with EnvironmentVariables if the repo already standardizes on the wrapper.
For secrets / channel IDs, ALWAYS use placeholder substitution — never commit real tokens. Pattern:
- Plist template (committed to repo) has
@VAR@ placeholders: <string>@HERMES_SLACK_BOT_TOKEN@</string>.
install-launchagents.sh (or equivalent installer) reads the real value from ~/.bashrc / 1Password CLI / op read op://Vault/Item/field and substitutes before launchctl bootstrap.
- CI / pre-commit guard rejects commits that contain
xoxb- or C0[A-Z0-9]{10} literals in .plist or .plist.template files.
Diagnostic when the daemon exits 1 every tick (verified 2026-07-09, dark-factory /af):
launchctl print gui/$(id -u)/<label> | grep -E "state|last exit"
tail -50 ~/Library/Logs/<service>.err.log
If you see this pattern: open the plist template, add the EnvironmentVariables block OR ensure the plist invokes a launchd-wrapper.sh that sources ~/.bashrc. Do NOT just symlink br into /usr/local/bin — that hides the underlying deployment-environment bug and bites the next daemon that needs a different binary.
Fail-soft notification pattern (also added 2026-07-09, dark-factory libnotify-slack.sh): daemon scripts that post to Slack should gate every notification on slack_capable returning 1 (both HERMES_SLACK_BOT_TOKEN and FACTORY_SLACK_CHANNEL_ID set). When unset, the helper no-ops silently so the daemon runs in environments without Slack without breaking the tick loop. Pattern:
if [ "$(slack_capable)" != "1" ]; then return 0; fi
This is the opposite of "crash loud on missing creds" — for notifications, silent no-op is correct; for dispatch, fail-loud is correct. Pick the right default per call site. Full recipe + the dark-factory daemon/scripts/libnotify-slack.sh template at references/launchd-env-injection-and-wrapper.md.
Deploy Script Flags (verified 2026-06-25 via deploy.sh --help)
~/.hermes/scripts/deploy.sh
~/.hermes/scripts/deploy.sh --skip-pull
~/.hermes/scripts/deploy.sh --skip-restart
~/.hermes/scripts/deploy.sh --no-sync
Stage summary (verified 2026-06-25, post-Stage-4.6):
| Stage | Purpose | Skippable? |
|---|
| 2 | git pull on ~/.hermes/ | --skip-pull |
| 4.5 | Policy-file sync (CLAUDE.md/SOUL.md/TOOLS.md/HEARTBEAT.md) staging → prod | --no-sync |
| 4.6 | Skills sync (skills/ tree, add-only rsync -c) staging → prod (NEW 2026-06-25) | --no-sync |
| 4 | Gateway restart | --skip-restart |
| 5.5 | Policy-file drift warning (non-blocking) | --skip-restart (only) |
| 5.5b | Skills drift warning (non-blocking, NEW 2026-06-25) | --skip-restart (only) |
| 5.6 | Cron jobs + launchd plist drift warning | --skip-restart (only) |
| 5 | Canary (LLM round-trip) | --skip-restart (only) |
The --system hermes, --skip-push, and --prod-only flags referenced in older memory entries DO NOT EXIST — verified 2026-06-25 when I tried --system hermes and got Unknown argument. If you see those flags in memory or prior session transcripts, they are stale — use --help to verify what's actually supported. The script's actual contract is "git pull + sync policy files + sync skills + restart + canary," NOT a parameter-driven system selector.
Anti-Patterns
-
❌ Writing directly to ~/.hermes_prod/ as primary action
-
❌ Writing skills directly to ~/.hermes_prod/skills/<name>/SKILL.md with skill_manage(action='patch'|'edit') without first writing the same change to ~/.hermes/skills/<name>/SKILL.md (added 2026-06-23). The skill_manage tool writes to whichever path you pass — it does NOT auto-stage. The user catches this every session ("why do you keep forgetting this studd"). The right sequence: (a) write to STAGING first (~/.hermes/skills/<name>/SKILL.md), (b) cp -p ~/.hermes/skills/<name>/SKILL.md ~/.hermes_prod/skills/<name>/SKILL.md to deploy, (c) cd ~/.hermes && git add skills/<name>/SKILL.md && git commit && git push origin main to record in git history. The "I wrote to prod because the running agent reads from prod" justification is wrong — the running agent reads from prod, so the manual cp step in (b) makes it visible in one extra shell call. Do NOT skip staging "to save time." The skillify cost is the same; the audit trail is the difference.
-
❌ Creating launchd plist in ~/Library/LaunchAgents/ first, then trying to "sync back"
-
❌ Bypassing git commit before deploy
-
❌ Running deploy.sh without first verifying staging works
-
❌ Writing agent code to projects_other/ (the prod fork at $HOME/projects_other/hermes-agent/) as the primary development path — this puts work in the deploy target instead of staging. The correct path is ~/projects/hermes-agent/ (the git-tracked staging checkout). projects_other is a mirror/fork for AO workers, not a write target.
-
⚠️ Known orphan: ai.hermes-staging plist. This launchd job (~/Library/LaunchAgents/ai.hermes-staging.plist) is NOT managed by install-launchagents.sh and has a different label than the repo template (ai.smartclaw.hermes-staging). Jeffrey wants it disabled. If you find it running, stop it with launchctl bootout gui/$(id -u)/ai.hermes-staging + launchctl disable gui/$(id -u)/ai.hermes-staging. Do NOT just rename to .plist.disabled — it gets re-created by setup/reconfigure.
-
⚠️ When prints (and the same for skills), it means the two paths resolve to the same directory on this machine — writing a file to IS already writing to prod. will print "files are identical (not copied)". Verified against + + rollout on 2026-07-20: all three files byte-identical between staging and prod after a single — no second step needed. The SOP "write to staging then to prod" still applies when staging and prod are SEPARATE paths, but the verification step () is the way to tell which mode you're in. Don't waste a tool call doing in single-dir mode.
⚠️ Skills Discovery — skill_manage writes to whichever path you pass (added 2026-06-23)
When updating an existing skill via skill_manage(action='patch'|'edit'), the tool writes to the absolute path you pass in the tool call — it does NOT auto-stage to ~/.hermes/skills/ first. If you pass ~/.hermes_prod/skills/<name>/SKILL.md, the change lands in prod directly, bypassing staging. The user catches this every session because the running gateway loads from prod (so the change IS visible) but the git history in ~/.hermes is missing it (so a future deploy drift-audit will flag the gap).
Right sequence for any skill update:
- Read the skill via
skill_view(name='<name>') — returns the prod-loaded version (what the running gateway sees).
- Edit
~/.hermes/skills/<name>/SKILL.md (STAGING) — use skill_manage with the staging path, OR terminal to write directly.
cp -p ~/.hermes/skills/<name>/SKILL.md ~/.hermes_prod/skills/<name>/SKILL.md — make the change live in prod (1 shell call).
cd ~/.hermes && git add skills/<name>/SKILL.md && git commit -m "[Auto] <brief description>" && git push origin main — record in git history.
If you accidentally wrote to ~/.hermes_prod/skills/<name>/SKILL.md first: cp -p ~/.hermes_prod/skills/<name>/SKILL.md ~/.hermes/skills/<name>/SKILL.md to back-fill staging, then continue from step 4. The cost is the same; the audit trail is preserved.
Why this isn't already enforced: the skill_manage tool is a generic file-write primitive — it doesn't know which tree is staging vs prod, and it doesn't know the user's deploy-pipeline preferences. The skill author must remember to pass the staging path. This anti-pattern is the most common cause of "why do you keep forgetting this studd" frustration.
⚠️ Slash command AND RESOLVER.md AND SOUL.md ## COMMIT: are three
separate wiring points** for a new trigger phrase or slash command. Missing
any one breaks the auto-fire path:
.claude/commands/<name>.md — for explicit /name <arg> invocations
(resolved by SOUL.md "Slash Command Discovery" rule order:
.claude/commands/<name>.md → ~/.claude/commands/<name>.md →
~/.claude/skills/<name>/SKILL.md).
~/.hermes_prod/skills/RESOLVER.md — for natural-language phrase
matching. The resolver is loaded by the gateway at startup; the prod
copy is the runtime source of truth (staging has its own RESOLVER.md
via git ls-files, but they drift if not synced).
~/.hermes_prod/SOUL.md ## COMMIT: <name> Trigger line — for
trigger phrases that should fire the skill automatically based on
session-init scan. RESOLVER.md alone is not enough; the SOUL.md
COMMIT is what guarantees the skill loads on every session, not just
when a user types the exact phrase.
Verified 2026-06-20: adding "finish the job" to the finish-the-job skill
initially only wired RESOLVER.md (and the slash command file), so the
literal phrase did NOT auto-fire on session-init. Adding it to the SOUL.md
## COMMIT: finish-the-job Trigger line closed the gap.
-
⚠️ Hermes self-mods in a heavily-dirty live checkout: use a fresh origin/main worktree (added 2026-07-22). When ~/.hermes/ is the live production checkout (no ~/.hermes_prod separation) AND it carries dozens of unrelated uncommitted/staged files from prior sessions, do NOT work directly in it. Every git checkout -B <branch> from the dirty HEAD carries that noise onto the PR head, which is precisely the pollution class the SOUL.md pr-clean-branch-from-main-no-history-bloat and never-push-onto-someone-elses-pr-head commits guard against. The right move is identical to the staging-dirty recipe: git worktree add ~/.worktrees/<repo>/<purpose> -b <new-branch> origin/main, commit + push + open PR from there, leave the live checkout untouched. Verified 2026-07-22, PR #790 on scripts/cron-backup-sync.sh: live checkout had 80+ unrelated dirty files; used git worktree add ~/.worktrees/jleechanclaw/cron-backup-no-routine-slack -b fix/cron-backup-no-routine-slack origin/main to keep the PR at 2 files / 19+ / 21-. The references/staging-dirty-surgical-sync.md recipe is for single-file fixes already on origin/main; this pattern is for new commits on a fresh branch when the live checkout is dirty. Cross-reference the AO spawn retry failure mode (Internal server error INTERNAL_ERROR from a healthy daemon, 2026-07-22 same session) — instead of retrying the spawn, pivot to the inline worktree + push + open PR pathway.
-
⚠️ Wrapper skill over a bundled binary: edit the bundled skill, NOT the wrapper, when the source-of-truth is upstream (added 2026-07-24). When you have a wrapper skill (e.g. claude-code-claudem re-exports bundled claude-code) and the bundled skill gets an upstream update (a new flag, a new mode, a new gotcha), the durable move is to (a) re-read the bundled skill via skill_view (or the symlinked ~/.hermes/skills/autonomous-ai-agents/<name>/SKILL.md if skill_view is wedged), (b) port the new content into the wrapper's "What the bundled skill now says" section, (c) leave a pointer in the wrapper noting the upstream version it was last reconciled against. Do NOT silently diverge from upstream — the wrapper's value is "I track the canonical skill + I swap the binary"; if it stops tracking, it becomes a stale fork. Verified 2026-07-24: claude-code-claudem v1.0.0 notes it re-exports claude-code v2.2.0; when ships v2.3.0, the wrapper needs a sync commit in the same PR cycle.
Proof Before Claim
When asked "is it working?", the answer must include raw terminal output showing:
launchctl list | grep <name> — job is loaded (exit 0, not error)
- Manual test run of the script — clean output
git log --oneline -1 — committed to git
Not:
- "It should work"
- "The commands were run"
- Summary descriptions of expected behavior
Skills Discovery
Before writing any new file, check if a relevant skill exists in ~/.hermes_prod/skills/:
launchd-job-authoring — for creating launchd plists
skillify — for making new workflows permanent
hermes-agent — for configuring Hermes itself
If none exist, build the file, then offer to skillify the pattern.
Commit Message Convention
For deploy pipeline commits:
[Auto] <brief description>
- <file 1>: what changed
- <file 2>: what changed
Example:
[Auto] Add auto-push launchd jobs for llm-wiki and user-scope
- scripts/auto-push-to-main.sh: reusable push-to-main script with codex --yolo fallback
- launchd: plists for llm-wiki and user-scope, 30-min interval
Support Files
references/circuit-breaker.md — live diagnostic commands, state transitions, and config for the circuit breaker failover system.
swap-hermes-provider (skill) — when the user asks to add / remove / replace a model provider in Hermes' config. Covers all six touch-points in ~/.hermes/config.yaml + ~/.hermes/scripts/launchd-env-wrapper.sh + the prod mirror, plus the hermes config set vs Python-edit workaround for the patch-tool security guard on config.yaml. Worked example at references/rm-opencode-go-glm51.md. The old references/opencode-go-glm51.md file here was the same provider's reference, removed 2026-07-16 when the opencode-go/glm-5.1 provider was deleted from the live config.
references/exportcommands-runbook.md — /exportcommands workflow: export ~/.claude/ dirs to jleechanorg/claude-commands. Covers prerequisites, timeout (≥300s), content filters, union merge logic, and pitfalls (must run from git repo root).
references/openrouter-attribution-headers.md — why every auxiliary call shows up in the OpenRouter dashboard as "App: Hermes Agent", why "Favicon for X" prompts appear, and the vision_analyze auto-captioning flow. Diagnostic recipe for any future "why is Hermes using X via OpenRouter" question.
references/actual-deploy-state-2026-06-09.md — read this before assuming scripts/deploy.sh exists. Documents the actual manual-cp sync procedure (the umbrella SKILL.md references a deploy.sh that doesn't exist), the staging/prod drift observed, and the cmux-send-submit rollout as a worked example. Critical if you hit "I changed the skill but the running agent doesn't see it" — 90% chance it's staging/prod drift, not the change being wrong.
references/actual-deploy-state-2026-06-23.md — read this FIRST before the 2026-06-09 baseline. Updates the deploy state as of the #682 bring-to-green: deploy.sh now EXISTS at ~/.hermes/scripts/deploy.sh AND ~/.hermes_prod/scripts/deploy.sh (verified). SOUL.md runtime path is ~/.hermes_prod/workspace/SOUL.md (NOT ~/.openclaw/SOUL.md as the 2026-06-09 doc said). The deploy-pipeline POLICY_FILES gap is unchanged: deploy.sh Stage 4.5 only syncs 4 files (CLAUDE.md, SOUL.md, TOOLS.md, HEARTBEAT.md); skills/scripts/launchd/ROADMAP all require manual or a custom Stage 4.5 step. Includes a staging/prod SOUL.md drift incident from 2026-06-23 (the COMMIT block was in staging but not prod) and a 3-question staging/prod sanity check to run before claiming "deployed."
Quick Reference
write_file content "..." to "~/.hermes/launchd/ai.hermes.schedule.example.plist"
launchctl load ~/Library/LaunchAgents/<name>.plist
cd ~/.hermes && git add -A && git commit && git push origin main
~/.hermes/scripts/deploy.sh
git worktree add ~/.worktrees/<repo>-fix-<purpose> -b fix/<purpose> origin/main
cp -p <file> ~/.hermes/<file> && cp -p <file> ~/.hermes_prod/<file>
launchctl kickstart -kp gui/$(id -u)/<label>