-
Notifications
You must be signed in to change notification settings - Fork 0
The Bridge
Caduceus runs your worker, but the worker is your code. The bridge is the thin script that connects the daemon to whatever AI harness you want — OpenCode, pi, codex, claude-code, your own custom thing. If you want to swap harnesses, the bridge is the only file you edit.
Setup seeds a reference implementation at
~/.hermes/caduceus/worker-bridge.py. It's yours — plugin
updates never overwrite it. That's the whole point of the
user-owned bridge pattern.
The reference bridge calls OpenCode's gentle-orchestrator
agent. The daemon has no opinion about what your bridge
invokes; it treats the bridge as a black box that reads
some environment variables, does the work, and exits with a
code.
The daemon rebuilds the worker's environment from scratch. Every variable the bridge reads comes from this list:
| Variable | What it is |
|---|---|
CADUCEUS_ISSUE_NUMBER |
Numeric issue ID. |
CADUCEUS_ISSUE_TITLE |
Issue title. |
CADUCEUS_ISSUE_BODY |
Issue body, raw Markdown. |
CADUCEUS_ISSUE_REPO |
owner/repo slug. |
CADUCEUS_ISSUE_LABELS_JSON |
Current label names as a JSON array. |
CADUCEUS_WORKTREE_PATH |
The isolated directory your worker operates in. |
CADUCEUS_RUN_ID |
ID for this run; names the transcript log. |
CADUCEUS_BRANCH_NAME |
The daemon-owned branch. Read it, never create/rename branches. |
CADUCEUS_CONTEXT_JSON |
Structured context for advanced multi-turn harnesses (timeline, trusted edits, allowed threads). |
Plus whatever your worker_env_allowlist admits, on top of
the sensible defaults (PATH, HOME, USER, SHELL,
LANG, LC_ALL, TERM, TMPDIR, OPENAI_*,
ANTHROPIC_*, OPENROUTER_*, OPENCODE_*).
The daemon never passes a GitHub credential to the
worker. Not GITHUB_TOKEN, not GH_TOKEN, not anything
token-shaped. Even if you add the name to the allowlist, it
is denied. This is enforced in the daemon, not in the
bridge, and it is not configurable. Your worker can read
your files (same user), but it cannot push to GitHub or
comment under the daemon's identity. That's the security
model, and it's load-bearing.
On success (exit 0), your worker leaves a
worker-result.json at the worktree root:
{
"status": "success",
"summary": "What the worker did, as Markdown. Becomes the PR description.",
"commit_message": "fix(component): description",
"pull_request_title": "fix(component): description"
}Rules that will bite you if ignored:
-
statusmust be exactly"success". Anything else is a failure. -
summaryis required, non-empty, and becomes the PR body (code tickets) or the findings comment (investigation tickets). -
commit_messageandpull_request_titleare required even for investigation tickets, where they're ignored. One harness, one result shape, no forking. - Unknown top-level fields are rejected. The daemon doesn't silently ignore slop.
- The whole file is capped at 1 MiB.
- An optional
artifactsobject (keys + JSON values) renders into the PR body, escaped and size-limited.
-
0— success. The daemon readsworker-result.jsonand finalises. - anything else — failure or abstain. The transcript is captured, the claim released, and the retry budget applies. The daemon doesn't care which non-zero code you picked; pick meaningful ones for your own debugging.
The daemon trusts you here — the bridge is your code, so the contract is enforced by discipline, not runtime checks:
-
Don't write into
<state_dir>. That directory is the daemon's. Your worker writes only inside its worktree. - Don't create or rename git branches. The daemon owns the branch name. Create a different one and the finalize step won't see your work; rename the daemon's and the next tick can't resume.
- Don't call the GitHub API. You have no token — deliberately. The local clone (fetched by the daemon) is your read source; the context JSON is your structured source.
- Don't install signal handlers. The supervisor delivers SIGINT/SIGTERM to the whole worker session; your handlers confuse the kill propagation and a timed-out worker may not die.
- Don't background yourself. The supervisor is not a process reaper. A daemonised child of the bridge is the supervisor's problem child. This is the most important rule of bridge-writing.
- Don't retry the harness internally. Retries are the daemon's job. One invocation, one exit.
Edit your user-owned worker-bridge.py. The
invoke_harness(worktree, prompt_file, run_id, labels, branch_name) function is the single user-editable hook —
replace its body and the rest of the bridge is
harness-agnostic plumbing you never need to touch.
Caduceus docs
- Home — what it is, and the one rule
- Installation — get it running
- Configuration — the settings that matter
- The-Bridge — make the worker yours
- State-Recovery — when things go wrong
- Troubleshooting — fix it
- FAQ — quick answers