plugin-harness¶
The plugin BUNDLE emitter — render the whole SpexCode system into one self-contained Claude-plugin bundle dropped into the spexcode.json-named folder, so a plugin host (z-code/Claude/future Codex) reads it natively with zero --append-system-prompt.
harness-select resolves a {"plugin":"<folder>"} target but emits nothing — it only validates the choice
and (being exclusive) leaves every native harness to be pruned. This node is that missing EMITTER: it renders
the entire SpexCode system into ONE self-contained bundle and drops it under the host-scanned <folder> as
<folder>/plugins/spexcode/. harness-delivery's materialize calls it AFTER pruning the natives (a plugin
is a SUPERSET delivery, so it replaces them, never coexists).
The bundle is the de-facto Claude-plugin schema: a .claude-plugin/plugin.json (name: spexcode, version,
description) pointing at hooks/, skills/, commands/, agents/. One .claude-plugin bundle reaches every
host because their discovery order is .zcode-plugin > .claude-plugin > .codex-plugin and z-code/Claude both
read a .claude-plugin directly — so the SAME emit serves ZCode, Claude, and a future Codex.
The pieces map from the same surface nodes the native path renders, but through a plugin host's seams:
- the contract (the harness-delivery assembly —
docs/AGENT_GUIDE.mdfollowed by thesurface: systembodies) is NOT an always-onCLAUDE.mdblock here — the bundle never edits the repo's own files. It maps to a SessionStart hook that emitshookSpecificOutput.additionalContext(the harness-neutral injection Claude/z-code normalize; the superpowers pattern), the stand-in for the--append-system-prompta plugin host can't take. The additionalContext JSON is encoded at RENDER time intohooks/contract-context.json, so the runtime injector (inject-contract.sh) is a trivialcat— never a fragile shell escaping of arbitrary prose. The contract is delivered by a hook, NOT a resident skill. - the hooks reuse the SAME
dispatch.shwiring as the natives —dispatch.sh+ its shell mirrorharness.share copied verbatim intohooks/, andhooks/hooks.json(the Claude/z-code shape{ "hooks": { "<Event>": [...] } }) binds every lifecycle event todispatch.sh, located via the host's${CLAUDE_PLUGIN_ROOT}variable. The dispatcher's first arg is the harness idplugin, so its gate + manifest dispatch run exactly as for a native;harness.shroutespluginthrough the claude family (z-code/Claude share Claude's tool names +file_path) via its default case, no separate arm. The per-event command bakesSPEXso the content-hash gate can re-materialize (re-emitting this bundle) on a.configedit. - skills / commands / agents ship as files in the Claude-plugin layout, reusing the same skill and agent renders the native path uses plus a command render. Commands become real host slash-menu entries here — the plugin counterpart of the native path serving its command presets through the dashboard.
clean() is the bundle's surgical inverse, identity-gated on the bundle's own plugin.json name so it removes
ONLY a spexcode bundle, never a folder the user filled with another plugin. Because a plugin folder is an
open-ended string (unlike the finite native adapter set), pruning a DESELECTED folder needs the PREVIOUS set:
materialize keeps a tiny ledger in the global store of the folders it last emitted, and on each run cleans any
folder the current set dropped (plugin→native, or folder A→B). The emitted bundle is a generated, machine-local
artifact (its hooks.json bakes this install's SPEX path), so it joins the managed .gitignore block — like
every other materialized artifact, regenerated per clone/launch, never committed.