ADR-0006: Make rhiza-cli the home of the Claude Code plugin¶
Status: Rejected — reversed in practice (2026-07-12)
Update (2026-07-12). This proposal was not adopted; the opposite direction was implemented. Rather than folding the plugin into
rhiza-cliand retiringrhiza-config, the two-repo split was kept and the presentation / utility commands were moved out ofrhiza-cliintorhiza-config:
rhiza-cliPR #599 (merged 2026-07-12) removed thestatus,tree,uninstall, andvalidateCLI commands. (validate.pystays only as an internal helper forsync/init; it is no longer a command.) The dependencyrich, used only by the oldtreeoutput, was dropped.rhiza-confignow carries those commands as standalonescripts/{status,tree,uninstall,validate}.py+commands/*.mdslash commands, alongsidestats.pyand the agent commands (/boost,/quality,/revisit,/stats). It remains a separate Claude Code plugin marketplace, not retired.- This is precisely the "port the CLI commands into
rhiza-config" alternative that the Decision below rejected. The trade-off it warned about (engine logic drifting into the presentation layer, version-matching burden across two repos) is now accepted deliberately.What did not move:
syncandsummarisestay inrhiza-clias the real engine (models/lock.py,models/template.py, the_git/merge). The upstreamjebel-quant/rhizarhiza_sync.ymlstill invokes them viauvx "rhiza>=…" sync/… summarise, andrhiza-config's/boostdelegates its mechanical sync step torhiza syncrather than reimplementing it.The Context, Decision, and Alternatives below are retained as the historical record of what was proposed. The command inventory in Context ("eight commands") describes
rhiza-clias it stood when this ADR was written.
Context¶
Rhiza currently ships across two repositories:
rhiza-cli(this repo) — the engine. A Typer application (rhiza) with real domain models (models/lock.py,models/template.py, the_git/merge engine) and eight commands:init,sync,status,tree,validate,list,uninstall,summarise. It is pip/uv-installable (project.scripts.rhiza) and is whatmake syncinvokes inside managed repos.rhiza-config— the Claude Code plugin. A.claude-plugin/plugin.jsonpluscommands/*.mdexposing four slash commands:/boost,/quality,/revisit,/stats.
These two serve different execution contexts (a CLI for humans and CI; slash
commands driven interactively by an agent), but they overlap in substance. The
/stats command was recently reworked into a bundled Python script
(rhiza-config/scripts/stats.py) so it could run without an agent. That script
re-derives, with ad-hoc regular expressions, information that rhiza-cli already
computes properly — its "Rhiza template status" section duplicates what
rhiza status and rhiza tree produce from models/lock.py and
models/template.py.
That duplication is the crux. With the plugin in a separate repo, every command
that needs engine logic faces a bad choice: reimplement it (drift, as
stats.py began to) or coordinate across two repos (a version-matching
burden). The plugin has no way to call the engine it conceptually depends on,
because a plugin install and a CLI install are unrelated artifacts.
Decision¶
Relocate the Claude Code plugin into rhiza-cli, so a single repo has two faces:
the CLI (unchanged) and a Claude Code plugin whose slash commands are thin
wrappers over the co-located engine.
1. Plugin lives beside the engine. Add .claude-plugin/plugin.json (+ a
marketplace entry, plugin name: "rhiza") and a commands/ directory to
rhiza-cli. The four slash commands move here from rhiza-config.
2. Slash commands invoke the bundled CLI. A plugin install copies the repo
into ~/.claude/plugins/cache/… but does not put rhiza on PATH. Because
the plugin root then contains pyproject.toml + src/, commands run the engine
from its own bundled source:
This needs no separate pip install and guarantees the slash commands and the
CLI are the same version. (Requiring uv tool install rhiza on PATH was
considered and rejected as the default — see Alternatives.)
3. Deterministic commands become CLI subcommands; judgement commands stay Markdown. The governing rule:
| Slash command | Disposition |
|---|---|
/stats |
Fold into a new rhiza stats subcommand that reuses models/lock.py/template.py (not regex) and deduplicates against status/tree/summarise; the .md becomes a thin uvx --from … rhiza stats wrapper. |
/boost |
Stays an agent-orchestrated .md (conflict resolution, quality scorecard, issue-dedup are genuine LLM work) but calls rhiza sync/rhiza status for its mechanical steps. |
/quality |
Stays .md (code-quality judgement). |
/revisit |
Stays .md; may lean on rhiza summarise. |
That is: deterministic/report work belongs in the engine wrapped by a thin .md;
judgement work stays in .md but delegates its mechanical sub-steps to the engine.
4. One version, one release. The plugin version tracks the rhiza package
version, enforced by a parity check (mirroring rhiza-config's
check_version_parity.py). rhiza-cli's existing release machinery (cliff,
rhiza_release.yml, bump scripts) becomes the single release path.
5. rhiza-config is retired. It becomes either a marketplace pointer at the
rhiza-cli plugin or a deprecated repo with a redirect. Because the plugin
name is "rhiza" in both, existing installs need a documented transition.
Phased rollout (each phase non-breaking on its own)¶
- Skeleton (additive): add
.claude-plugin/+commands/torhiza-cli, copying the four.mdfiles; add manifest-validation + version-parity to CI. - Wire to the engine: rewrite command bodies to
uvx --from "${CLAUDE_PLUGIN_ROOT}" rhiza …; addrhiza stats; retire the duplicatedstats.pylogic. - Cut over: point
rhiza-config's marketplace at this plugin (or deprecate it); announce the transition. - Cleanup: single version/release story; delete the
rhiza-configscripts.
Consequences¶
- ✅ Single source of truth. Engine logic lives once; slash commands wrap it
instead of reimplementing it. The
stats.pydrift risk is removed, not grown. - ✅
rhiza statsgets better, not just relocated — it uses the real lock and template models rather than the regex parsing the standalone script used. - ✅ No version skew.
uvx --from "${CLAUDE_PLUGIN_ROOT}"runs the plugin's own copy of the CLI; slash commands and CLI cannot disagree. - ✅ One release process for both faces.
- ⚠️ Marketplace continuity. The plugin
nameis shared across both repos; users who installed fromrhiza-configneed a migration path. - ⚠️
uvx --fromcold start. The first invocation builds an ephemeral environment (seconds). Acceptable for interactive use;uv tool install rhizacan be documented as a faster opt-in. - ⚠️ Dual audience.
rhiza-cli's README/issues now serve both CLI users and plugin users; docs must address both. - ⚠️ Two unrelated "plugin" systems coexist.
rhiza-cli'srhiza.pluginsentry-point mechanism (CLI subcommand plugins, e.g.rhiza-tools) is unrelated to the Claude Code plugin (just files under.claude-plugin/+commands/); documentation must keep the two from being conflated.
Alternatives considered¶
- Keep the repos separate and reimplement engine logic in
rhiza-configscripts. Rejected — this is the driftstats.pyalready started; it scales badly as more commands need engine data. - Port the CLI commands into
rhiza-config. Rejected — it inverts ownership: the substantive Python (models, merge engine) already lives inrhiza-cli, and moving it toward the presentation layer is backwards. - Require
uv tool install rhizaonPATHinstead ofuvx --from. Rejected as the default — it adds a setup step and reintroduces version skew between an installed CLI and the plugin. Fine as a documented fast-path opt-in.