|
| 1 | +# ADR-0006: Make `rhiza-cli` the home of the Claude Code plugin |
| 2 | + |
| 3 | +**Status:** Proposed |
| 4 | + |
| 5 | +## Context |
| 6 | + |
| 7 | +Rhiza currently ships across two repositories: |
| 8 | + |
| 9 | +- **`rhiza-cli`** (this repo) — the engine. A Typer application (`rhiza`) with real |
| 10 | + domain models (`models/lock.py`, `models/template.py`, the `_git/` merge engine) |
| 11 | + and eight commands: `init`, `sync`, `status`, `tree`, `validate`, `list`, |
| 12 | + `uninstall`, `summarise`. It is pip/uv-installable (`project.scripts.rhiza`) and |
| 13 | + is what `make sync` invokes inside managed repos. |
| 14 | +- **`rhiza-config`** — the Claude Code plugin. A `.claude-plugin/plugin.json` plus |
| 15 | + `commands/*.md` exposing four slash commands: `/boost`, `/quality`, `/revisit`, |
| 16 | + `/stats`. |
| 17 | + |
| 18 | +These two serve different execution contexts (a CLI for humans and CI; slash |
| 19 | +commands driven interactively by an agent), but they overlap in substance. The |
| 20 | +`/stats` command was recently reworked into a bundled Python script |
| 21 | +(`rhiza-config/scripts/stats.py`) so it could run without an agent. That script |
| 22 | +re-derives, with ad-hoc regular expressions, information that `rhiza-cli` already |
| 23 | +computes properly — its "Rhiza template status" section duplicates what |
| 24 | +`rhiza status` and `rhiza tree` produce from `models/lock.py` and |
| 25 | +`models/template.py`. |
| 26 | + |
| 27 | +That duplication is the crux. With the plugin in a separate repo, every command |
| 28 | +that needs engine logic faces a bad choice: **reimplement it** (drift, as |
| 29 | +`stats.py` began to) or **coordinate across two repos** (a version-matching |
| 30 | +burden). The plugin has no way to call the engine it conceptually depends on, |
| 31 | +because a plugin install and a CLI install are unrelated artifacts. |
| 32 | + |
| 33 | +## Decision |
| 34 | + |
| 35 | +Relocate the Claude Code plugin into `rhiza-cli`, so a single repo has two faces: |
| 36 | +the CLI (unchanged) and a Claude Code plugin whose slash commands are thin |
| 37 | +wrappers over the co-located engine. |
| 38 | + |
| 39 | +**1. Plugin lives beside the engine.** Add `.claude-plugin/plugin.json` (+ a |
| 40 | +marketplace entry, plugin `name: "rhiza"`) and a `commands/` directory to |
| 41 | +`rhiza-cli`. The four slash commands move here from `rhiza-config`. |
| 42 | + |
| 43 | +**2. Slash commands invoke the bundled CLI.** A plugin install copies the repo |
| 44 | +into `~/.claude/plugins/cache/…` but does **not** put `rhiza` on `PATH`. Because |
| 45 | +the plugin root then contains `pyproject.toml` + `src/`, commands run the engine |
| 46 | +from its own bundled source: |
| 47 | + |
| 48 | +```bash |
| 49 | +uvx --from "${CLAUDE_PLUGIN_ROOT}" rhiza status |
| 50 | +``` |
| 51 | + |
| 52 | +This needs no separate `pip install` and **guarantees the slash commands and the |
| 53 | +CLI are the same version**. (Requiring `uv tool install rhiza` on `PATH` was |
| 54 | +considered and rejected as the default — see Alternatives.) |
| 55 | + |
| 56 | +**3. Deterministic commands become CLI subcommands; judgement commands stay |
| 57 | +Markdown.** The governing rule: |
| 58 | + |
| 59 | +| Slash command | Disposition | |
| 60 | +|---|---| |
| 61 | +| `/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. | |
| 62 | +| `/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. | |
| 63 | +| `/quality` | Stays `.md` (code-quality judgement). | |
| 64 | +| `/revisit` | Stays `.md`; may lean on `rhiza summarise`. | |
| 65 | + |
| 66 | +That is: deterministic/report work belongs in the engine wrapped by a thin `.md`; |
| 67 | +judgement work stays in `.md` but delegates its mechanical sub-steps to the engine. |
| 68 | + |
| 69 | +**4. One version, one release.** The plugin `version` tracks the `rhiza` package |
| 70 | +version, enforced by a parity check (mirroring `rhiza-config`'s |
| 71 | +`check_version_parity.py`). `rhiza-cli`'s existing release machinery (`cliff`, |
| 72 | +`rhiza_release.yml`, bump scripts) becomes the single release path. |
| 73 | + |
| 74 | +**5. `rhiza-config` is retired.** It becomes either a marketplace pointer at the |
| 75 | +`rhiza-cli` plugin or a deprecated repo with a redirect. Because the plugin |
| 76 | +`name` is `"rhiza"` in both, existing installs need a documented transition. |
| 77 | + |
| 78 | +### Phased rollout (each phase non-breaking on its own) |
| 79 | + |
| 80 | +1. **Skeleton (additive):** add `.claude-plugin/` + `commands/` to `rhiza-cli`, |
| 81 | + copying the four `.md` files; add manifest-validation + version-parity to CI. |
| 82 | +2. **Wire to the engine:** rewrite command bodies to `uvx --from |
| 83 | + "${CLAUDE_PLUGIN_ROOT}" rhiza …`; add `rhiza stats`; retire the duplicated |
| 84 | + `stats.py` logic. |
| 85 | +3. **Cut over:** point `rhiza-config`'s marketplace at this plugin (or deprecate |
| 86 | + it); announce the transition. |
| 87 | +4. **Cleanup:** single version/release story; delete the `rhiza-config` scripts. |
| 88 | + |
| 89 | +## Consequences |
| 90 | + |
| 91 | +- ✅ **Single source of truth.** Engine logic lives once; slash commands wrap it |
| 92 | + instead of reimplementing it. The `stats.py` drift risk is removed, not grown. |
| 93 | +- ✅ **`rhiza stats` gets *better*, not just relocated** — it uses the real lock |
| 94 | + and template models rather than the regex parsing the standalone script used. |
| 95 | +- ✅ **No version skew.** `uvx --from "${CLAUDE_PLUGIN_ROOT}"` runs the plugin's |
| 96 | + own copy of the CLI; slash commands and CLI cannot disagree. |
| 97 | +- ✅ **One release process** for both faces. |
| 98 | +- ⚠️ **Marketplace continuity.** The plugin `name` is shared across both repos; |
| 99 | + users who installed from `rhiza-config` need a migration path. |
| 100 | +- ⚠️ **`uvx --from` cold start.** The first invocation builds an ephemeral |
| 101 | + environment (seconds). Acceptable for interactive use; `uv tool install rhiza` |
| 102 | + can be documented as a faster opt-in. |
| 103 | +- ⚠️ **Dual audience.** `rhiza-cli`'s README/issues now serve both CLI users and |
| 104 | + plugin users; docs must address both. |
| 105 | +- ⚠️ **Two unrelated "plugin" systems coexist.** `rhiza-cli`'s `rhiza.plugins` |
| 106 | + entry-point mechanism (CLI subcommand plugins, e.g. `rhiza-tools`) is unrelated |
| 107 | + to the Claude Code plugin (just files under `.claude-plugin/` + `commands/`); |
| 108 | + documentation must keep the two from being conflated. |
| 109 | + |
| 110 | +## Alternatives considered |
| 111 | + |
| 112 | +- **Keep the repos separate and reimplement engine logic in `rhiza-config` |
| 113 | + scripts.** Rejected — this is the drift `stats.py` already started; it scales |
| 114 | + badly as more commands need engine data. |
| 115 | +- **Port the CLI commands *into* `rhiza-config`.** Rejected — it inverts |
| 116 | + ownership: the substantive Python (models, merge engine) already lives in |
| 117 | + `rhiza-cli`, and moving it toward the presentation layer is backwards. |
| 118 | +- **Require `uv tool install rhiza` on `PATH` instead of `uvx --from`.** Rejected |
| 119 | + as the default — it adds a setup step and reintroduces version skew between an |
| 120 | + installed CLI and the plugin. Fine as a documented fast-path opt-in. |
0 commit comments