Skip to content

feat: multi-agent runtime + 8 new agents (Hermes via PyPI plugin)#39

Open
ChasLui wants to merge 14 commits into
cytostack:mainfrom
ChasLui:dev
Open

feat: multi-agent runtime + 8 new agents (Hermes via PyPI plugin)#39
ChasLui wants to merge 14 commits into
cytostack:mainfrom
ChasLui:dev

Conversation

@ChasLui
Copy link
Copy Markdown

@ChasLui ChasLui commented May 9, 2026

Multi-Agent Runtime PR

Summary

This PR adds a --agent <name> flag to openwolf init, supporting 8 new
AI coding agents
in addition to Claude Code:

Agent Notes Closes
codex ~/.codex/AGENTS.md marker block #2
gemini ~/.gemini/GEMINI.md marker block #22
opencode ~/.config/opencode/openwolf-instructions.md + patches opencode.json instructions[] #5, #6
openclaw ~/.openclaw/workspace/AGENTS.md marker block
hermes Python plugin (entry-point hermes_agent.plugins.openwolf), installed into Hermes' venv via uv pip install + ~/.hermes/config.yaml plugins.enabled patch. Published to PyPI as openwolf-hermes 0.1.0
cline OS-aware Cline rules path (~/Library/Application Support/cline/rules.md on macOS, XDG/AppData on Linux/Windows)
cursor ~/.cursor/USER_RULES.md (experimental — Cursor has no documented global rules path as of 2026-05; adapter writes the file and prints guidance to paste into Cursor → Settings → Rules)
pi-mono ~/.pi/agent/AGENTS.md (or $PI_CODING_AGENT_DIR/AGENTS.md). Verified against badlogic/pi-mono docs

Default behavior (no --agent flag) is bit-for-bit identical to v1.0.4:
the original initCommand() flow runs unchanged. Existing users see no
difference.

Architecture

A new src/agents/ directory introduces an AgentAdapter interface:

interface AgentAdapter {
  name: AgentName;
  detect(): boolean;
  installGlobal(opts: InstallOpts): Promise<void>;
  uninstallGlobal(): Promise<void>;
  parseHookInput(stdin: string): NormalizedHookInput;
  emitHookOutput(decision: HookDecision): string;
  projectDirEnvVar: string;
}

Each adapter encapsulates one agent's quirks: where to write the integration
file, what hook protocol the agent speaks (if any), how to parse/emit hook
JSON. Core OpenWolf logic (anatomy / cerebrum / token-ledger) stays
agent-agnostic. The shared OPENWOLF_SNIPPET constant in
src/agents/openwolf-snippet.ts is the single source of truth for the
soft-instruction protocol; both snippets/openwolf-cross-agent.md (human
docs) and the embedded const must stay in sync when the protocol changes.

Full design rationale + rejected alternatives in
docs/adr/ADR-001-multi-agent-runtime.md.

Honest Limitations

OpenWolf's full power (auto-injecting anatomy descriptions before reads,
catching repeated reads, post-write anatomy refresh) requires hooks at the
agent's Read / Write / Edit tool boundaries. Of the 8 new agents:

  • codex / gemini: hook protocols only support shell-command
    interception (no file-op matchers). Soft instructions only.
  • opencode: has instructions[] array — clean injection, but no
    runtime hook (we do not ship a TS plugin; agent reads .wolf/
    voluntarily per the instructions).
  • openclaw / cline / cursor / pi-mono: same soft-instruction pattern
    via their respective rules / instructions file.
  • hermes: Python plugin with pre_tool_call hook can write to
    .wolf/memory.md + token-ledger.json on file ops, plus a
    /openwolf slash command (status / scan). Hermes API has no
    system-prompt injection point so it cannot auto-inject anatomy
    descriptions like Claude does.

Only claude still gets the full hook-driven experience. The other 8
agents get the "agent reads .wolf/ voluntarily" experience, which is
strictly better than no integration but weaker than Claude's. Documented
per-adapter in src/agents/*.ts headers and in ADR-001 "Findings during
Phase 1a".

Hermes plugin distribution

The openwolf-hermes Python plugin ships as a separate PyPI package, not
bundled in the npm openwolf package:

  • Source lives in src/agents/hermes/python/ (this PR adds it)
  • .github/workflows/publish-hermes-plugin.yml builds + publishes via
    PyPI Trusted Publishing (OIDC), tag-triggered on openwolf-hermes-v*
  • v0.1.0 already published 2026-05-09 → https://pypi.org/project/openwolf-hermes/0.1.0/
  • After this PR merges, the HermesAdapter can switch from
    uv pip install -e <path> (dev install) to plain
    uv pip install openwolf-hermes for users without a fork checkout

Testing

End-to-end tested locally (macOS arm64, all reachable agents installed and
live). The 4 agents the test machine had installed all passed
install/uninstall/idempotent/reinstall flows:

Agent Install Idempotent (reinstall ×2) Uninstall Reinstall
codex ✅ count=1
gemini ✅ count=1
opencode ✅ instructions.md + opencode.json ✅ no dup path ✅ both removed
openclaw ✅ count=1
hermes ✅ openwolf-hermes 0.1.0 in venv + config.yaml plugins.enabled ✅ entry count=1 ✅ pkg uninstalled, config rolled back
cline ✅ ~/Library/Application Support/cline/rules.md ✅ count=1
pi-mono / cursor not installed locally — adapters fail-fast on detect() as expected

tsc --noEmit reports 0 type errors on all new code. Pre-existing errors
in src/cli/, src/hooks/, bin/ are upstream and untouched (need
@types/node install).

Phased landing

If a single 13-commit PR is too much, this can land in 5 separate PRs:

  1. ADR-only (bddf690, be6c54e): documentation + AgentAdapter
    skeleton. Zero behavior change.
  2. Codex + Gemini (13e3d66, 963fbc8): two soft-instruction
    adapters + --agent flag.
  3. OpenClaw + OpenCode (981084f): two more adapters.
  4. Hermes (c08bb69, a40b0dd): Python plugin + adapter.
  5. CI + extra agents + docs (adef463, db60152, bdd4b96,
    e115b3f, 48c7a3e, d5821fe): GitHub Actions for PyPI publishing,
    pi-mono / cline / cursor adapters, OIDC Trusted Publishing config,
    PR description draft.

Happy to split if preferred.

Out of scope (future PRs)

  • ClaudeAdapter refactor: src/cli/init.ts HOOK_SETTINGS + Claude-
    specific .claude/settings.json write logic still lives in init.ts.
    Phase 1d in ADR-001 plans to refactor it into
    ClaudeAdapter.installGlobal() so Claude becomes "just another
    adapter". Behavior identical, code organization cleaner. Holding back
    to keep this PR review-able.
  • Copilot adapter: per-project (.github/copilot-instructions.md),
    doesn't fit the installGlobal interface. Needs an
    installPerProject(projectDir) extension first.
  • Windsurf / kilocode / antigravity: global instruction file paths
    uncertain; defer until verified or feature requested.
  • AGPL-3.0 derivative concerns: the OpenCode soft-instruction
    approach avoids any in-process AGPL plugin. The Hermes Python plugin
    is in-process — its pyproject.toml declares
    license = "AGPL-3.0-only", but Hermes itself is not AGPL. Would
    value upstream's perspective on whether this license posture is OK or
    needs adjustment.
  • Test infrastructure: no integration tests for adapters yet.
    Manual testing only. Would add vitest + per-adapter tests if
    direction is acceptable.

Author note

I'm a heavy OpenWolf user across 9 agents (claude + 8 above) and was
about to write 8 separate plugins as workarounds. The adapter pattern in
this PR pushes the common code (anatomy / cerebrum / ledger maintenance)
back into OpenWolf core where it belongs, with thin per-agent wrappers.
The PyPI Trusted Publishing pipeline is also configured so Hermes plugin
releases are cryptographically tied to this repo's CI — no long-lived
secrets.

Happy to discuss direction, scope, or split the PR however maintainers
prefer.

— Chao Liu (@ChasLui)

ChasLui added 9 commits May 9, 2026 13:53
Adds proposal for refactoring OpenWolf to support 6 agents
(claude/codex/gemini/opencode/openclaw/hermes) via per-agent
AgentAdapter abstraction. Phase 0 lands the ADR text and the
src/agents/types.ts interface skeleton; subsequent phases
implement adapters and refactor src/cli/init.ts + src/hooks/.

See: docs/adr/ADR-001-multi-agent-runtime.md
Lands AgentAdapter implementations as placeholders (methods throw
"not yet implemented"). Phase 1b refactors src/cli/init.ts +
src/hooks/* to consume these adapters.

Findings during this phase (recorded in ADR-001):
- Codex hook protocol only supports matcher "shell" — no file-op
  hooks. OpenWolf on Codex is degraded to shell + soft-instruction.
- Gemini hook protocol only supports BeforeTool matcher
  "run_shell_command". Same degradation.

Verified against pandafilter v1.3.5 implementations of
~/.codex/panda-rewrite.sh and ~/.gemini/settings.json.

Adapters added:
- src/agents/types.ts — AgentAdapter interface (Phase 0, refresher)
- src/agents/claude.ts — full-fidelity adapter, mirrors current behavior
- src/agents/codex.ts — shell-only adapter (degraded)
- src/agents/gemini.ts — shell-only adapter (degraded)
- src/agents/index.ts — registry + detectInstalled()

See: docs/adr/ADR-001-multi-agent-runtime.md
…--agent flag

Codex and Gemini hook protocols cannot host OpenWolf's file-op hooks
(see ADR-001). Instead, deliver OpenWolf as a soft-instruction:
write a marker-delimited section to ~/.codex/AGENTS.md and
~/.gemini/GEMINI.md telling the agent to follow the OpenWolf
protocol when cwd has a .wolf/ directory.

Idempotent — re-running `openwolf init --agent codex` only refreshes
content between <!-- openwolf:start --> and <!-- openwolf:end -->.

Changes:
- src/agents/codex.ts: real detect/installGlobal/uninstallGlobal
- src/agents/gemini.ts: real detect/installGlobal/uninstallGlobal
- src/agents/snippets/openwolf-cross-agent.md: shared instruction body
- src/cli/index.ts: `openwolf init --agent <name>` flag with values
  claude (default, current per-project behavior unchanged) | codex |
  gemini | all. Plus `--uninstall` to remove integration.

ClaudeAdapter installGlobal still throws — Phase 1c refactors
src/cli/init.ts HOOK_SETTINGS into ClaudeAdapter without changing
behavior. Default (no --agent) flow preserved bit-for-bit.

Tested: tsc --noEmit emits zero errors on agents/ and cli/index.ts.
Phase 1b's CodexAdapter/GeminiAdapter readSnippet() tried to fs-read
src/agents/snippets/openwolf-cross-agent.md at runtime with a wrong
relative path; tsc doesn't copy .md to dist anyway. Result: install
fell through to the embedded short fallback (one paragraph) instead
of the full 7-step protocol.

Fix: extract OPENWOLF_SNIPPET as a TypeScript const in
src/agents/openwolf-snippet.ts (single source of truth), with shared
stripMarkerBlock() and withSnippet() helpers. Both adapters import
it — zero runtime fs lookup, snippet ships inside the JS bundle.

src/agents/snippets/openwolf-cross-agent.md is kept as
human-readable documentation (ADR/PR diff readability); both files
must stay in sync when the protocol changes.

End-to-end tested 2026-05-09:
- openwolf init --agent codex   → ~/.codex/AGENTS.md gets full 7-step snippet
- openwolf init --agent gemini  → ~/.gemini/GEMINI.md ditto
- Idempotent: reinstall 2× → exactly 1 marker block
- --uninstall → marker block stripped, surrounding content preserved
OpenClaw adapter:
  - target: ~/.openclaw/workspace/AGENTS.md
  - same marker-block strategy as Codex/Gemini (OpenClaw hook system
    is shell-only, no file-op matchers)
  - mkdirs ~/.openclaw/workspace/ if it doesn't exist yet

OpenCode adapter:
  - OpenCode supports instructions[] array in opencode.json
  - write snippet body to ~/.config/opencode/openwolf-instructions.md
    (OpenWolf-owned file, no marker needed)
  - patch opencode.json instructions[] to include the path
  - uninstall removes path AND deletes the file
  - idempotent: re-running adds path only once

End-to-end tested 2026-05-09:
- install/uninstall both adapters
- idempotent (reinstall 2× → marker count 1, instructions[] no dup)
- ~/.openclaw/workspace/AGENTS.md gets marker block coexisting with
  user's existing manual content
- ~/.config/opencode/opencode.json instructions[] grows from 6→7

5/6 agents now covered by openwolf init --agent. Hermes (Phase 3)
requires Python plugin — see ADR-001.
Hermes has no instructions[] / soft-prompt injection point — its
only extension surface is the Python plugin system (entry-point
group hermes_agent.plugins). So Phase 3 ships:

1. src/agents/hermes/python/ — openwolf-hermes Python package
   - pyproject.toml (entry-point: openwolf = "openwolf_hermes")
   - src/openwolf_hermes/__init__.py (~180 lines)
     · pre_tool_call hook: detect cwd .wolf/, append memory.md
       on file read/write, bump token-ledger.json counters
     · /openwolf slash command: status (read .wolf/ state) and
       scan (subprocess `openwolf scan`)
     · register(ctx) entry point per Hermes plugin protocol
   - README.md explains what Hermes API allows vs not
2. src/agents/hermes.ts — HermesAdapter
   - detect: ~/.hermes exists + hermes binary in PATH
   - installGlobal: locate hermes venv via realpath of `hermes`
     binary → its `python` neighbor → `uv pip install --python
     <venv-py> -e <fork>/src/agents/hermes/python` + patch
     ~/.hermes/config.yaml plugins.enabled to add "openwolf"
   - uninstallGlobal: uv pip uninstall + strip from config.yaml
   - YAML patcher uses regex (no yaml lib dep) supporting both
     inline `[a, b]` and block `\n    - a\n    - b` forms
3. src/agents/index.ts — register HermesAdapter

Bug fixed during Phase 3: pyproject.toml had both `license =
"AGPL-3.0-only"` and `License :: OSI Approved` classifier —
setuptools >= 80 enforces PEP 639 mutual exclusion. Removed the
classifier; license expression remains.

End-to-end tested 2026-05-09 in real ~/.hermes/update-candidates/
hermes-agent-v2026.4.13/venv:
- install: plugin appears in entry_points("hermes_agent.plugins")
  alongside rtk-rewrite; config.yaml plugins.enabled grows from
  [rtk-rewrite] to [rtk-rewrite, openwolf]
- import: `python -c "import openwolf_hermes"` works
- register(FakeCtx): registers /openwolf command and pre_tool_call
- pre_tool_call(tool_name="read_file", args={path}): noop when
  cwd has no .wolf/ (correct)
- idempotent: reinstall keeps openwolf entry count at 1
- uninstall: package gone, config rolled back, plugin no longer
  importable — clean
- reinstall: full state restored

Phase 3 closes 6/6 agent coverage. Phase 4 (PyPI publish, ADR
"out of scope" → "PR to upstream") remaining.
Phase 3 commit accidentally tracked .egg-info/ and __pycache__/
generated by `uv pip install -e`. Add a local .gitignore and
untrack the artifacts.
Adds:
- .github/workflows/publish-hermes-plugin.yml — tag-triggered PyPI
  publish via OIDC Trusted Publishing (no long-lived API token).
  Build + twine check on every tag; upload only when tag matches
  openwolf-hermes-v* and dry_run is not set.
- src/agents/hermes/python/RELEASE.md — one-time PyPI Trusted
  Publisher setup steps + per-release flow (version bump, tag,
  push) + yank guidance.

Phase 4b: removes PyPI-account-dependence from the publish loop
once a maintainer (or @ChasLui) configures the trusted publisher
in PyPI. Publishing is then `git tag openwolf-hermes-v0.2.0 &&
git push --tags`.

Adapter still uses dev-install (`uv pip install -e <fork>/...`)
during alpha; will switch to plain `uv pip install openwolf-hermes`
after first PyPI release.
ChasLui added 5 commits May 9, 2026 14:35
Adds 3 more agents to the multi-agent runtime, all via the same
soft-instruction marker pattern (no hooks). Total: 9/9 agents
covered by `openwolf init --agent`.

- src/agents/pi-mono.ts — writes ~/.pi/agent/AGENTS.md (or
  $PI_CODING_AGENT_DIR/AGENTS.md if set). Clean global injection
  point per pi-mono CLI's documented config layout.
  See https://github.com/badlogic/pi-mono.
- src/agents/cline.ts — writes the OS-specific Cline global rules
  file: ~/Library/Application Support/cline/rules.md (macOS),
  $XDG_CONFIG_HOME/cline/rules.md (Linux), %APPDATA%/cline/rules.md
  (Windows). Verified against panda init --agent cline 1.3.5
  install layout on macOS.
- src/agents/cursor.ts — EXPERIMENTAL. Cursor IDE has no
  documented global rules path as of 2026-05; User Rules go through
  Settings UI. Adapter writes ~/.cursor/USER_RULES.md and prints
  guidance instructing the user to paste content into Cursor →
  Settings → Rules → User Rules manually. If Cursor adopts a
  global path in the future, content is already there.

types.ts: AgentName union extends to 9 agents.
index.ts: REGISTRY adds cline / cursor / pi-mono entries.
cli/index.ts: --help string lists all 9 agents.

End-to-end tested 2026-05-09 on macOS:
- detect: pi-mono / cursor → false (not installed locally),
  cline → true (~/Library/Application Support/cline already exists
  from prior panda install).
- cline install/uninstall/reinstall — all ✓ idempotent (marker
  count 1 after 2 installs, 0 after uninstall).

Skipped (per ADR-001 deferred):
- copilot — per-project (.github/copilot-instructions.md), not a
  global install target. Needs interface extension for
  installPerProject(projectDir) before it can land.
- windsurf / kilocode / antigravity — global instruction file
  paths uncertain; defer until verified or feature requested.
… TODO)

User has set PYPI_API_TOKEN as a repo secret (project-scoped legacy
token). Switch the publish job from OIDC Trusted Publishing to API
token auth so the first releases can ship without additional PyPI
dashboard configuration.

Trusted Publishing is still the long-term goal (no long-lived
token, finer-grained per-workflow scope, no rotation burden). The
migration recipe is now documented in RELEASE.md "Recommended
long-term" section — when ready, swap permissions/environment in
the workflow and delete the secret in one PR.

Workflow changes:
- Removed environment: pypi (only needed for OIDC).
- Removed permissions: id-token: write.
- Added with.password: ${{ secrets.PYPI_API_TOKEN }} on the
  pypa/gh-action-pypi-publish step.

Tag a release as before:
  git tag openwolf-hermes-v0.1.0 && git push --tags

The PYPI_API_TOKEN secret value is never logged — gh-action-pypi-publish
masks it. If the token is ever exposed in logs, revoke at
https://pypi.org/manage/account/token/ and rotate.
Replaces API token auth with Trusted Publishing per
https://pypi.org/manage/account/publishing/. Project doesn't exist
on PyPI yet, so first publish uses a Pending Publisher — see
RELEASE.md for setup steps.

Workflow changes:
- Re-add environment: { name: pypi, url: ... }
- Re-add permissions: id-token: write
- Remove with.password (OIDC fills it in)

After PyPI Pending Publisher is registered + first tag is pushed,
the PYPI_API_TOKEN secret can be deleted:
  gh secret delete PYPI_API_TOKEN --repo ChasLui/openwolf

RELEASE.md now leads with Trusted Publishing as the canonical path
and keeps API token as a documented fallback for outages.
@ChasLui ChasLui changed the title feat: multi-agent runtime — codex/gemini/opencode/openclaw/hermes feat: multi-agent runtime + 8 new agents (Hermes via PyPI plugin) May 9, 2026
@ChasLui ChasLui marked this pull request as ready for review May 9, 2026 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant