Operate the personal Hermes fork that uses fork/main as an upstream mirror and fork/prod as the canonical deploy branch. Covers install, update, maintainer sync flow, and Telegram topic scoping.
Operate the personal Hermes fork that uses fork/main as an upstream mirror and fork/prod as the canonical deploy branch. Covers install, update, maintainer sync flow, and Telegram topic scoping.
version
0.2.0
Hermes fork prod
Use this skill when working with the personal Hermes fork hosted at Git-on-my-level/hermes-agent.
Key invariant:fork/prod is always a superset of fork/main. Commits merged directly to prod (via PR or GitHub merge button) are never lost — they're rescued onto main first, then flow into prod naturally.
Prerequisites
Clean working tree (script will refuse to run otherwise)
GitHub branch rule for prod must allow push + force-push (for recovery scenarios)
Local branches main and prod should exist and track their respective fork/* remotes
Regular sync
Run the update script:
cd ~/.hermes/hermes-agent
./scripts/update-prod-branch.sh # live run
DRY_RUN=1 ./scripts/update-prod-branch.sh # preview what would happen
That script does:
Fetchorigin and fork remotes (with prune)
Check for upstream changes — if none but prod has custom commits, skip to step 4
Update main — rebase local main onto origin/main, push fork/main
Rescue prod-only commits — find any commits on fork/prod not on main, cherry-pick them onto main, push updated fork/main
Integrate into prod — merge main into prod (fast-forward if possible, merge commit if histories diverged)
Pushfork/prod
This ensures:
Custom PRs merged to prod survive syncs (they get rescued to main, then merged forward)
main stays a clean upstream mirror (all prod-only patches are also on main)
No data loss — nothing is ever force-pushed away without being integrated first
After resolving cherry-pick / merge conflicts
If step 4 (cherry-pick) or step 5 (merge) hits conflicts:
Re-apply patches adapted to current code — do NOT git am literally. Read each patch for INTENT and implement it in the current code structure. Use subagents (Codex preferred), batched by dependency group (infrastructure first, then gateway, then CLI, then trivial).
Validate syntax after every subagent batch (see *** corruption pitfall below).
The previous workflow (rebase prod onto main then force-push prod) had a critical flaw: any commit merged directly to prod was silently clobbered on every sync. The force-push overwrote fork/prod with the rebased content, losing whatever was only on prod.
main still stays clean (it's the upstream mirror + rescued patches)
Normal case is fast-forward (no force-push needed day-to-day)
Only recovery scenarios need --force-with-lease
Telegram topic scoping
This fork supports inbound Telegram topic allowlists on the prod line.
It also supports session-scoped model settings for Telegram topics: each topic/session can retain its own model, provider, and base_url in the persisted session state. That means different topics can effectively run different models/providers, and /new clears conversation state without resetting the active model settings.
This allows multiple Hermes bots to share one Telegram supergroup while each bot only responds in its assigned topic.
Worktree setup
Use worktrees for feature and PR branches, not for the live deploy checkout.
Example:
git worktree list
# /Users/dazheng/.hermes/hermes-agent [prod]# /Users/dazheng/.hermes/hermes-agent-pr-foo [feat/foo]
Recommended rule:
~/.hermes/hermes-agent should be the canonical launchd-managed deploy checkout
that checkout should stay on prod
PRs and experiments should use separate worktrees
avoid a dedicated prod worktree for the live service unless you also deliberately bind the venv, editable install, and launchd plist to that same path
Why:
a split setup like venv in ~/.hermes/hermes-agent but editable install / runtime code in ~/.hermes/hermes-agent-prod-worktree is brittle
hermes update, editable installs, and launchd path generation derive from the current project root
if those roots drift apart, you can get path breakage like Python or module resolution failures tied to the worktree path
Important operational finding:
a split install can happen where the venv lives under ~/.hermes/hermes-agent/venv but the editable package points at ~/.hermes/hermes-agent-prod-worktree
launchd may then run Python from the main repo venv while importing code from the prod worktree
this usually works until paths drift or the worktree disappears, then you can get brittle failures like Python/path not found during updates or restarts
If the desired state is "launchd-managed prod branch, not a worktree deployment", collapse back to a single checkout:
current branch is prod and refs/remotes/fork/prod exists
otherwise fall back to main / origin/main
So even if a user temporarily lands on a feature branch or detached HEAD, hermes update should converge back to the prod line when the install-channel metadata or prod tracking metadata is present.
If you are validating the live prod branch, inspect hermes_cli/update_channel.py and hermes_cli/main.py and confirm:
prod resolves to (channel="prod", remote="fork", branch="prod")
non-prod checkouts are switched to prod before update
macOS operational finding:
for launchd-managed prod agents, the dangerous part is often not the git update itself but the post-update gateway restart
a broken restart path can make /update succeed while Telegram stays down
the reliable macOS behavior is: detached launchctl kickstart -k gui/$UID/<label> plus bounded restart healthchecks
healthchecks should verify more than launchctl command exit status; use launchd state, fresh gateway PID, runtime status, and recent gateway log signals
if restart verification fails, update output should explicitly say code update succeeded but gateway restart healthcheck failed, and print the failing checks plus recent log excerpt
The update command will auto-reset to match fork/prod. Manual fallback:
cd ~/.hermes/hermes-agent-prod-worktree
git fetch fork
git reset --hard fork/prod
Remote branch deletion safety
Be careful deleting remote branches in this fork because one branch is literally named prod while the remote is named fork, so the remote-tracking ref appears as fork/prod.
That makes commands like:
git push fork --delete fork/prod
ambiguous-looking and easy to misuse. It deletes the remote branch named prod on remote fork (because git push <remote> --delete <branch> interprets the argument as a remote branch name, not a remote-tracking ref).
For destructive cleanup, prefer explicit ref syntax:
git push fork :refs/heads/stale-branch-name
Examples:
# delete the stale remote branch literally named "fork/prod"
git push fork :refs/heads/fork/prod
# delete the canonical prod branch only if you truly mean it
git push fork :refs/heads/prod
After any remote branch deletion, immediately verify with:
After changing a bot to prod and/or topic scoping:
cd ~/.hermes/hermes-agent-prod-worktree # or main repo if no worktree
git branch --show-current
git rev-parse HEAD
git rev-parse fork/prod
Expected:
current branch is prod
HEAD matches fork/prod unless there are new unpushed local commits
Then restart the gateway and verify:
/status works in the assigned topic
plain messages work in the assigned topic
the bot ignores other topics in the same supergroup
Critical deployment check for topic allowlists:
Do not assume allowed_inbound_targets is active just because it is present in config.
Verify the live launchd service and editable install are actually pointing at the prod checkout/worktree that contains the allowlist code.
Check launchd plist, pip show hermes-agent location, and imported gateway platform module paths all agree.
Failure mode to watch for:
config contains allowed_inbound_targets
a prod worktree exists
but launchd still runs ~/.hermes/hermes-agent on main
result: topic allowlist config is silently ignored and /new can still respond in other topics
Operational notes
/new or /reset resets conversation state only; it does not claim topic ownership.
Topic ownership is enforced by allowed_inbound_targets.
If Telegram privacy mode was changed recently, remove and re-add the bot before testing.
For deployed prod agents, do not manually switch them back to main for updates; use hermes update from prod.
Pitfall: subagent patch tool *** corruption
When delegating file edits to subagents (Codex, GLM subagents, etc.), their patch tools can silently replace actual code with *** in files that contain redacted or placeholder content (API keys, auth types, token values). This produces syntactically invalid Python that passes visual review if you only look at surrounding context.
Symptoms:
SyntaxError: closing parenthesis ')' does not match opening parenthesis '{'
Lines like api_key_env_vars=*** "ZAI_API_KEY" or models = _fetch_github_models(api_key=*** timeout=timeout)
Backslash-escaped quotes where none should exist: (\\\"Z.AI / GLM\\\", ...
Prevention:
After every subagent batch, run syntax validation on ALL modified .py files:
for f in <modified-files>; do
python3 -c "import ast; ast.parse(open('$f').read()); print('$f ok')"done
Search for literal *** in modified files: grep -n '=\\*\\*\\*' file.py
If a file is badly corrupted, reset it to upstream and re-apply only the intended change:
git checkout origin/main -- path/to/corrupted_file.py
# then manually apply just the prod-specific change
Root cause: The subagent's patch tool appears to match and replace content that looks like redacted placeholders, sometimes overwriting actual code that happens to be adjacent. Files with many redacted values (auth.py, models.py, doctor.py, setup.py) are highest risk.