plugin-harness¶
Provenance¶
- Source:
.spec/spexcode/spec-cli/sessions/injected-context/harness-delivery/plugin-harness/spec.md - Source SHA-256:
4ebecb6e5017505433ccf9b488f0f392da27580e2dd8d31b503029968b65b80f
plugin-harness¶
[[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 materializes
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 .adopter-a-plugin > .claude-plugin > .codex-plugin and adopter-a/Claude both
read a .claude-plugin directly — so the SAME emit serves AdopterA, Claude, and a future Codex.
The pieces map from the same [[surface]] nodes the native path materializes, but through a plugin host's seams:
- the contract (the [[harness-delivery]] assembly — the
surface: systemplugin bodies) 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/adopter-a normalize; the superpowers pattern), the stand-in for the--append-system-prompta plugin host can't take. The additionalContext JSON is encoded at MATERIALIZE 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/adopter-a 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 manifest dispatch runs exactly as for a native;harness.shroutespluginthrough the claude family (adopter-a/Claude share Claude's tool names +file_path) via its default case, no separate arm. The per-event command bakesSPEXfor the cli-needing handlers (the bundle re-emits at the git-native materialize anchors, [[commit-surgery]] — never on a harness event). - skills / commands / agents ship as files in the Claude-plugin layout, reusing the same materialized skill and agent contents the native path uses plus a command build. Commands become real host slash-menu entries here — the bundle is reconciled as the exact current path set, so a deleted surface node removes its obsolete generated plugin file without replacing unchanged siblings. Reconciliation enumerates only each owned dynamic directory's immediate entries and removes names absent from the current surface set — 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 exclude block — like
every other materialized artifact, regenerated per clone/launch, never committed.