Rembric plugin development
Skill susomejias/rembric/.agents/skills/rembric-plugin-development
Apply when creating, modifying, or reviewing any Rembric agent plugin. Triggers on changes under `apps/plugin/`, on new clients added alongside Claude Code / Codex CLI / Hermes Agent / opencode / Pi, on edits to `apps/plugin/mcp-bridge/`, `apps/plugin/bin/rembric-plugin-core.mjs`, per-client manifest changes, or plugin install/uninstall scripts. End-to-end validation against `pnpm run dev:docker:up` is mandatory whenever local testing is feasible.From its SKILL.md
npx -y skills add susomejias/rembric --skill rembric-plugin-developmentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 12 stars12 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.4 KB, as published. Nobody here has run it
Rembric plugin development
Authoritative specs: openspec/specs/{claude-code-plugin,codex-distribution,hermes-agent-plugin,opencode-plugin,pi-plugin,plugin-session-protocol}/. Archived decisions: openspec/changes/archive/.
Mandatory workflow
- OpenSpec change first. Run
/opsx:propose(or amend an existing change). Plugin work always touches ≥2 specs and ≥3 files. Skipping the change is the failure mode that produces drift. - Two release tracks:
server+ unifiedplugin(no cascade). release-please runs exactly two components, nonode-workspace/linked-versions/grouping.server(apps/server, package@rembric/server, tagserver-v*) builds the Docker image.plugin(apps/plugin— the WHOLE tree, noexclude-paths, package@rembric/plugin, tagplugin-v*) carries one unified version for all five clients; itsextra-filesupdate every client carrier in lock-step (.claude-plugin/{package,plugin}.json,.codex-plugin/{package,plugin}.json,.hermes-plugin/plugin.yaml,.opencode-plugin/plugin.tscomment,.pi-plugin/package.json). A change to ANY plugin file bumps the singlepluginversion — claude/codex/opencode/hermes/pi never diverge; the CHANGELOG (scoped by conventional commit) records what actually changed. Apluginrelease NEVER rebuilds the server image (publish-dockergates onserver_release_created), but it IS what publishes@rembric/pito npm (trusted-publishing OIDC, provenance, no long-lived token)..pi-plugin/has to live insideapps/plugin/for that: release-please attributes a release by the paths of the commits under the component'spath, so a client outside it would never cause a release and its carrier would only move when something unrelated did.release-please.ymlcarries aconcurrencyguard (cancel-in-progress: false) so a rapid second merge can't cancel tag-minting. The former six-component +node-workspacecascade was retired (changeunify-plugin-release-track) after its anchor-tag fragility produced phantom release PRs. Legacy per-client tags (claude-code-plugin-v*, …) stay in history, inert. - End-to-end against
pnpm run dev:docker:upbefore reporting done — see E2E discipline below. - Docs sweep:
README.md,docs/agents.md,apps/plugin/README.md, the in-pluginREADME.md,apps/plugin/CHANGELOG.md. New-client checklist in references/files-checklist.md.
No tool watches the per-client manifest dirs for you.
eslint.config.jsignoresapps/plugin/*/**, which matches the dot-directories, and none of them matchpnpm-workspace.yaml::packages(apps/*,packages/*) — sopnpm -rdoes not reach them, ESLint does not lint their TypeScript, and anydependenciesthey declare are not installed by the repo's own install (.claude-plugin/package.json'sworkspace:*dep is dead letter today). Their tests run only becauseapps/server/vitest.config.ts::includelists a literal glob per client; a new client without its glob leaves a test file written and never executed, and the suite is green on nothing.
The three single-source-of-truth rules
.rembricis the only per-repo slug source. Dotenv file withPROJECT_SLUG=<lowercase-hyphen>. Regex^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$.apps/plugin/mcp-bridge/rembric-dotenv.mjsis the only JS/TS implementation ofparseDotenv+readRembricSlug+SLUG_RE. The bridge, opencode plugin, and Pi extension import it (the installer rewrites the opencode import to the installed absolute path). Inlining any of those in another.mjs/.tsfile fails the build (invariant test).apps/plugin/bin/rembric-plugin-core.mjsis the only JS/TS implementation of the nudge strings,stripPrivateTags,truncate,diag, the session HTTP client, the transcript accumulator and the flush helpers. Both JS/TS clients (opencode, Pi) import it, which is what makes the nudge strings byte-identical and the<private>redaction identical by construction rather than by review. Itsagentparameter is required with no default:sessions.agentis written once per session into append-only rows with no repair verb, so a default misfiles sessions permanently.rembric-plugin-core.d.mtsis hand-written (apps/pluginhas no build step and no typecheck) and is the only thing that makes the omission a compile error.
Bash (apps/plugin/scripts/_api.sh) and Python (apps/plugin/.hermes-plugin/__init__.py) keep their own implementations — cross-language wrappers cost more than the duplication. They MUST agree on the regex value.
Per-client gotchas (read on touch)
Each client has 3–5 non-obvious behaviors that bit us. Before modifying that client's files, skim its section in references/per-client-gotchas.md. Sample of what's there:
- Claude Code:
${user_config.*}works in bothmcp.json::envAND hook commands; keychain is the SoT for credentials. - Codex CLI:
${user_config.*}is NOT substituted; subprocess env is cleared before MCP spawn → MUST list every needed var inenv_vars: [...];${CLAUDE_PLUGIN_ROOT}doesn't work in MCP args. - Hermes Agent:
plugin.yaml::hooks: [...]array gates lifecycle invocation — overriding a method without listing the hook is a silent no-op. - opencode: every named export of a plugin file is invoked as a Plugin function — export ONLY
RembricPlugin. The bridge MUST live outside~/.config/opencode/plugins/. Sub-agent filtering (parentID || title.endsWith(" subagent)")) is mandatory. - Pi: no built-in MCP (its own docs say so, deliberately) → this is the one client whose plugin holds the MCP client, discovering tools with
tools/list; tools register under provider-safe names (.→_) because a real provider rejects the whole payload on a dot; shutdown is awaited and two Ctrl-C presses within 500 ms fire it in the interactive TUI (a single press does not, and print mode registers no SIGINT); nothing is injected from its settings file, so credentials come from the shell.
Shared-vs-divergent discipline
Per memory 01KRNZM2VFCME5HNT8N78HZW18: shared logic lives in shared paths. Divergence is allowed ONLY when the platform forces it.
- MUST be shared:
apps/plugin/mcp-bridge/rembric-dotenv.mjs,apps/plugin/bin/rembric-plugin-core.mjs,apps/plugin/scripts/*.sh(Claude+Codex hooks),apps/plugin/commands/*.md(Pi consumes them verbatim as prompt templates — reference them, never copy). The published@rembric/mcp-bridgepackage is the shared stdio transport; there is no repository launcher to copy. - Legitimately divergent today:
hooks/hooks.jsonvshooks/hooks.codex.json(env-substitution rules differ);.claude-plugin/mcp.jsonvs.codex-plugin/mcp.json(Claude keychain substitution differs from Codex's curatedenv_vars); Python in-process provider for Hermes; JS/TS in-process for opencode; the MCP transport in.pi-plugin/index.ts, because that host has no MCP client for the published bridge to plug into.
Sanity check: git ls-files apps/plugin/ should show ONE copy of each shared resource. Two paths with near-identical content is a sync bug.
Install / uninstall script invariants
- Never edit the user's agent config file (
settings.json,config.toml,opencode.json,~/.hermes/config.yaml). Print the snippet with<PLACEHOLDERS>; the user pastes. - Idempotent: a second run produces an empty diff.
chmod 644for files invoked asnode <path>(the +x bit isn't needed fornode x.mjs).- Fail loudly on missing source files.
Don't overcommit the spec to behavior the server doesn't support
Before adding a handler that POSTs to an endpoint, verify the endpoint exists:
grep -nE "app\\.(post|get)" apps/server/src/server/api-router.ts
If it doesn't exist: either add the endpoint first (separate OpenSpec change) OR drop the handler from this scope (tasks.md says DEFERRED, change CHANGELOG documents it). Don't ship a handler that 404s.
This bit us in add-opencode-plugin — first iteration speced chat.message POSTing to /prompts/passive, which didn't exist. Cost: spec rework + scope reduction post-merge.
Adding a brand-new client
Always start with a Phase 0 spike in tasks.md that validates platform assumptions BEFORE any plugin code. Examples of what to spike, plus the cwd/PWD propagation experiment template, live in references/new-client-spike.md.
The spike's outcome MUST be recorded as a comment in BOTH tasks.md (<!-- spike result: plan-a -->) and the plugin's source file (// cwd-spike-result: plan-a).
End-to-end validation discipline
You MUST exercise the change against a real Rembric dev stack before reporting work as done, unless local e2e is genuinely impossible. Spec validation and unit tests are necessary but not sufficient for plugin work — the multi-process flow (agent → bridge → MCP → HTTP → SQLite → dashboard) breaks in non-obvious places.
Minimum required steps:
pnpm run dev:docker:up. Wait for[bootstrap] listening on. Capture the seededdemo-writertoken from the seed banner.- Install the plugin (
bash apps/plugin/.<X>-plugin/install.sh), configure the client end-to-end with real URL + real token, drop.rembricwithPROJECT_SLUG=demoin the working directory. - Exercise the lifecycle path your change affects. The exact commands per client (opencode
mcp list, tsx-driven handler invocation, dashboard SQLite verification, etc.) are in references/e2e-walkthrough.md. - Tear down:
docker compose ... down, uninstall, restore user's config file to its prior state (placeholders if it didn't exist before).
Prefer the isolated variant of steps 2-4 when the client's CLI supports it — scratch HOME, per-run extension load instead of install, deliberately invalid API key. It touches none of the operator's config, so there is nothing to restore and no half-restored state to leave behind, and it costs no LLM calls. Six rails plus the paired-control method: references/e2e-walkthrough.md § 5b.
If you still cannot drive the agent TUI (a client with no per-run load, or keychain integration you can't script):
- Direct handler invocation via
pnpm exec tsxcovers ~95% of the handler logic. Pattern fromadd-opencode-plugin: import the installed plugin module, set env vars, call handlers with mock inputs that mirror the platform's event shape. - Tell the user explicitly what you DID verify vs what you DID NOT. Don't say "verified e2e" when you only ran unit tests. List the manual smoke steps the user should run.
If local e2e is genuinely impossible (Codex /hooks interactive trust gate, Claude Code keychain integration, tool you don't have installed): say so out loud. Tell the user:
"I can't drive
<specific path>from this environment. Unit tests cover<list>. Manual verification needed for<list>. Want the steps?"
Honest > glossing-over.
Self-check before commit
If any answer is "no" or "I don't know", stop and resolve it.
- OpenSpec change open or amended for this work
- Two-track release respected (
server+ unifiedplugin; no node-workspace/cascade; all five clients share the onepluginversion viaextra-files; plugin release never rebuilds Docker) - No duplication of
parseDotenv/SLUG_RE/ therembric-plugin-corehelpers / endpoint strings without justification -
pnpm vitest run+pnpm run typecheck+pnpm run lint+openspec validate <change> --strictall clean - Exercised against
pnpm run dev:docker:up(or explicitly told the user what isn't verified) - Docs sweep done (README, docs/agents.md, apps/plugin/README.md, in-plugin README, CHANGELOG)
- Install/uninstall idempotent (verified by running twice)
- User's local state restored (dev stack down, plugin uninstalled, config placeholders)