# Plugin contract (/docs/plugin-contract)



`HarnessPlugin` is the consumer extension contract for
`@pleach/core`. Every sibling SKU — `@pleach/compliance`,
`@pleach/eval`, `@pleach/gateway`, `@pleach/replay`, `@pleach/mcp`,
`@pleach/coding-agent`, `@pleach/sandbox`, `@pleach/langchain`,
`@pleach/base-tools`, `@pleach/observe`, `@pleach/recipes` — is a
plugin that implements this contract. All ship at
`0.1.0 · FSL-1.1-Apache-2.0` in the first-wave cut; `@pleach/trust-pack`
alone remains a reserved npm name. See
[Packages](/docs/packages) for the canonical per-SKU status
table. Your own consumer code extends the runtime through the
same surface; sibling SKUs slot in alongside without changing
graph shape.

The contract is small on purpose. Plugins fill named slots; they
don't get a handle on the lattice, the synthesize seam, or the
modelfamily matrix. That bounded surface is what makes substrate
guarantees (one synthesize per turn, family-locked routing,
replay determinism) hold no matter what plugins do.

> **Plugins are a thematic island.** Not one of the [six cluster
> triplets](/docs/concept-clusters#the-six-cluster-triplets) —
> `HarnessPlugin` is a bounded contract (74 optional hooks — 67
> `contribute*` hooks plus 7 top-level lifecycle hooks — plus
> four structural invariants), not a three-concept cluster. See
> [What lives outside the cluster pattern](/docs/concept-clusters#what-lives-outside-the-cluster-pattern).

<SourceMeta source="{ label: &#x22;src/plugins/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/plugins&#x22; }" />

## `definePleachPlugin()` — the typed factory [#definepleachplugin--the-typed-factory]

`definePleachPlugin()` is the recommended construction surface.
It takes the same fields as a raw `HarnessPlugin` literal plus a
flat `capabilities` menu, and returns a `HarnessPlugin` with
autocomplete that surfaces "what can my plugin contribute?" in one
place instead of paging the 1100-line interface.

The factory exports from the top-level barrel:

```typescript
import { definePleachPlugin } from "@pleach/core";
import type { PluginCapabilities } from "@pleach/core";
```

```typescript
export const myPlugin = definePleachPlugin("my-plugin", {
  prompts: [personaBlock],
  safetyPolicies: [refusalPolicy],
  fabricationDetectors: [phantomToolDetector],
  streamObservers: [redactPiiObserver],
  tools: [searchCorpus, fetchUrl],
  intentToolMap: [intentMapEntry],
  toolCouplingHints: [couplingHint],
  runtimeAwarePrompts: (ctx, state) => buildPerTurnPrompts(ctx, state),
  _raw: {
    version: "0.1.0",
    // hooks not yet covered by the structured menu — see below
    contributeFabricationGuard: () => myFabricationGuard,
    contributeSynthesisDirectiveBlocks: (ctx) => [domainDirective(ctx)],
  },
});
```

Capability fields take **values directly**, not thunks — `prompts: [personaBlock]`, not `prompts: () => [personaBlock]`. The factory
forwards each field into the corresponding `contribute*` method on
`HarnessPlugin`. One field is the exception: `runtimeAwarePrompts`
accepts either a `(ctx, state) => PromptContribution[]` function or
a fixed array, mirroring the underlying hook's per-turn shape.

### `PluginCapabilities` — the initial 8 slots [#plugincapabilities--the-initial-8-slots]

| Key                    | Type                                                                               | Maps to                          |
| ---------------------- | ---------------------------------------------------------------------------------- | -------------------------------- |
| `prompts`              | `readonly PromptContribution[]`                                                    | `contributePrompts`              |
| `runtimeAwarePrompts`  | `((ctx, state) => readonly PromptContribution[]) \| readonly PromptContribution[]` | `contributeRuntimeAwarePrompts`  |
| `safetyPolicies`       | `readonly SafetyContribution[]`                                                    | `contributeSafetyPolicies`       |
| `fabricationDetectors` | `readonly FabricationDetector[]`                                                   | `contributeFabricationDetectors` |
| `tools`                | `readonly ToolDefinitionLite[]`                                                    | `contributeTools`                |
| `streamObservers`      | `readonly StreamObserverRegistration[]`                                            | `contributeStreamObservers`      |
| `intentToolMap`        | `readonly IntentToolMapEntry[]`                                                    | `contributeIntentToolMap`        |
| `toolCouplingHints`    | `readonly CouplingHint[]`                                                          | `contributeToolCouplingHints`    |

The cluster covers the most-used contribution surface. Future
hooks land additively — widening `PluginCapabilities` doesn't
break existing call sites.

### `_raw` — the forward-compat escape hatch [#_raw--the-forward-compat-escape-hatch]

`_raw` is typed as `Partial<HarnessPlugin>` and accepts any
contribution hook not surfaced through the structured cluster
above. Reach for it when contributing:

* `contributeFabricationGuard` (the typed hook for the prior
  untyped-bag fabrication-guard implementation — see below)
* `contributeSynthesisDirectiveBlocks`
* `contributeFinalizationPasses`
* `contributeDetectionRules`
* `contributeIntentClassifiers`
* `contributeSandboxBridge`
* `contributeInterruptUIHandlers`
* `contributeCitationRuleSet`
* `contributeChatManifestProvider`
* `contributeBatchingHints`, `contributeEventTypes`
* `extraGraphNodes`, `prePlanPrimer`, `postSynthesisGuard`
* The deprecated bare-property forms — flagged in
  [`inspectRuntime()`](/docs/runtime-inspector) as
  `deprecated-wired`

Keys present in `_raw` win on collision with the structured
cluster (last-write-wins) so authors migrating bespoke plugins
keep full control.

The raw object literal still works — `HarnessPlugin` is a public
type and existing plugins keep loading. The factory is what new
plugins should reach for; it picks up future capability additions
without the plugin author having to chase type changes by hand.

> **For larger plugins**, see [Plugin bundles](/docs/plugin-bundles)
> — thematic facet sub-paths
> (`@pleach/core/plugins/safety`, `@pleach/core/plugins/prompts`, …)
> plus `composePlugin()` for assembling a plugin across multiple
> files. Both APIs produce a `HarnessPlugin`; choose by plugin
> size, not contract surface.

The substrate's `audit:plugin-contract-completeness` audit
asserts every `contributeX` hook on `HarnessPlugin` has both a
paired `PluginManager.collectX` accessor and at least one
substrate consumer site. **41 hooks** are catalogued today: 38
wired end-to-end, 3 baselined awaiting consumer
(`contributeBatchingHints` is on the 1.2.0 retirement track per
`audit:harness-plugin-deprecated-usage`; `contributeEventTypes`
runs an intentional collector-bypass; `contributeMiddleware` ships
the Stage 1 contract while Stage 2 consumer rewires land). The 5
hard-deprecated lifecycle hooks (`onSessionCreated`,
`onToolCompleted`, `onMessageAdded`, `queryExtensions`,
`sandboxAvailability`) have been retired; replace them
with `contributeStreamObservers`, `contributeDetectionRules`, or
the typed `runtime.events.iterate` projection surface.

## What a plugin can do [#what-a-plugin-can-do]

| Capability                                                 | API surface                                                                          | When you reach for it                                                                                                                                                                                               |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Register **extra graph nodes** in lattice enrichment slots | `extraGraphNodes`                                                                    | Intent detection, plan generation, safety review, quality scoring                                                                                                                                                   |
| Register **stream observers** on provider seams            | `contributeStreamObservers`                                                          | Per-chunk inspection, content rewriting, halt on policy violation                                                                                                                                                   |
| Register **prompt contributors**                           | `contributePrompts`, `contributeRuntimeAwarePrompts`                                 | Static system blocks, runtime-aware sections, retrieval-driven additions                                                                                                                                            |
| Register **safety policies**                               | `contributeSafetyPolicies`                                                           | Refusal-pattern detectors, policy-bound rewriters                                                                                                                                                                   |
| **Subscribe** to lifecycle events                          | `runtime.events.iterate({chatId, fromSequenceNumber})` + `contributeStreamObservers` | Domain events, cross-cutting telemetry, third-party sinks. The legacy lifecycle callbacks (`onSessionCreated`, `onToolCompleted`, `onMessageAdded`) have been retired — read from the event log projection instead. |
| **Emit** named-channel envelopes                           | seam observer `emit` verdict                                                         | Pipe structured chunks through to downstream consumers                                                                                                                                                              |

The slot model means a plugin's surface area is auditable: what it
contributes is visible at registration time, not threaded through
opaque callbacks. A reviewer reading `plugins: [compliancePlugin,
gatewayPlugin, myPlugin]` against a runtime construction sees the
exact list of contributions each name brings by walking each
plugin's `contributeStreamObservers`, `extraGraphNodes`,
`contributePrompts`, and `contributeSafetyPolicies` exports — there
is no hidden channel through which a plugin could mutate the graph,
the seam set, or the modelfamily matrix.

## What a plugin cannot do [#what-a-plugin-cannot-do]

These boundaries are enforced by lint, by type system, or by
runtime invariant — not by convention.

| Forbidden                                                      | Enforced by                                                 |
| -------------------------------------------------------------- | ----------------------------------------------------------- |
| Add an out-of-lattice edge to the graph                        | `audit:graph-stages` (CI gate)                              |
| Bypass the singleton synthesize seam                           | `SynthesizeSeamHolder` + `TurnSynthesizeCounter` (runtime)  |
| Reach across the `seams/` boundary into the modelfamily matrix | `lint:harness-boundary` (CI gate)                           |
| Register an async stream-observer dispatch path                | Type signature is `onChunk(chunk, ctx): Verdict`, sync only |
| Use a raw `callClass: "..."` literal outside a seam factory    | `lint:callclass-literals` (CI gate)                         |

The async-observer restriction is the one that surprises people.
The reason it's sync is the replay determinism story: an
observer that returns `Promise<Verdict>` introduces non-determinism
into the stream, and replay determinism is the load-bearing
property that the shipping `@pleach/eval@0.1.0` and
`@pleach/replay@0.1.0` SKUs are built around — and that any
host's own diff harness can lean on today.

## The lattice slots [#the-lattice-slots]

Each of the four lattice stages exposes named enrichment slots
that plugins fill. Slots are typed; a plugin contributing to a slot
declares which stage it belongs to and what channels it reads and
writes.

| Stage         | Enrichment slots                                        |
| ------------- | ------------------------------------------------------- |
| `anchor-plan` | `intentDetection`, `planGeneration`, `anchorBuilding`   |
| `tool-loop`   | `toolSelection`, `toolExecution`, `dataSilo`, `jobSilo` |
| `synthesize`  | `synthesizerPreamble`, `citationInjector`               |
| `post-turn`   | `qualityScoring`, `consolidation`, `recoveryShaping`    |

A plugin can fill zero or more slots. Slots themselves don't
multiply — two plugins contributing to `intentDetection` chain
deterministically by registration order, not by reduction.

`synthesize` is a true singleton stage — only the `synthesizer`
node runs there. Recovery shaping (refusal hints, retry narration,
garble recovery) is a `post-turn` concern: it moved off the
lattice into the recovery stream filters, which fire at stage
completion via the `StreamObserverRegistry`. See
[Stream observers](/docs/plugins/stream-observers).

## Stream observer verdicts [#stream-observer-verdicts]

Observers ride on top of every provider seam. For each inbound
chunk the seam dispatches the observer ladder; each observer
returns one of four verdicts:

| Verdict    | Effect                                                    |
| ---------- | --------------------------------------------------------- |
| `continue` | Pass through unchanged                                    |
| `amend`    | Replace chunk content 1:1 — strict, no multiplex          |
| `emit`     | Pass through and emit a named-channel envelope downstream |
| `stop`     | Stop the stream; downstream reads the stop sentinel       |

`amend` being 1:1 is deliberate: a one-chunk-in, many-chunks-out
observer would break the byte-replay property. Plugins that need
fan-out emit named envelopes on a channel, not extra stream chunks.
Concretely: a redaction observer that replaces a span with
`[REDACTED]` returns an `amend` verdict whose `chunk.text` is the
cleaned string, and the chunk count stays the same. A metrics
observer that wants to ship a structured side-effect returns an
`emit` verdict with an envelope on a named channel (e.g. a
`metrics` channel), and the main stream sees the original chunk
pass through unchanged.

## `contributeFabricationDetectors` slot [#contributefabricationdetectors-slot]

A plugin can register one or more fabrication detectors through
this slot. Each detector receives a typed context — the completed
tools for the turn, the assistant content, the user text, the set
of known tool names, and the current call class — and returns a
`FabricationFinding` or `null`. The graph's `FabricationNode`
iterates the union of every plugin's detectors per turn.

```typescript
contributeFabricationDetectors?(): readonly FabricationDetector[]
```

```typescript
// lib/plugins/phantomToolDetector.ts
import type { HarnessPlugin } from "@pleach/core";
import type { FabricationDetector } from "@pleach/core/plugins";

const phantomToolDetector: FabricationDetector = {
  id: "phantom-tool",
  detect(ctx) {
    const match = ctx.assistantContent.match(/\b([a-z_]+)\(/);
    if (!match) return null;
    const name = match[1];
    if (ctx.knownToolNames.has(name)) return null;
    return {
      detectorId: "phantom-tool",
      severity: "medium",
      reason: `Mentions ${name}() but no such tool is registered`,
      evidence: { name },
    };
  },
};

export const myPlugin: HarnessPlugin = {
  name: "my-plugin",
  contributeFabricationDetectors: () => [phantomToolDetector],
};
```

See [Fabrication detection](/docs/fabrication-detection) for the
full detector contract, the context shape, and how findings flow
through the post-graph pipeline.

## `contributeSynthesisDirectiveBlocks` [#contributesynthesisdirectiveblocks]

A plugin can return synthesis-mode directive blocks that the
runtime composes into the synthesis-time prompt seam. Each block
declares an `id`, a `priority`, and the directive text the seam
should fold in.

```typescript
contributeSynthesisDirectiveBlocks?(
  ctx: SynthesisDirectiveContext,
): readonly SynthesisDirectiveBlock[]
```

The hook receives a runtime-context argument carrying the channel,
the resolved model, and the tenant. Contributions can be
context-conditional — return a block for `gateway` channels and
skip it for others, or vary the text by tenant tier.

The substrate ships its default synthesis-mode block at priority
10\. Higher-priority contributions stack over the default; same-
priority contributions append in registration order. See
[Prompts](/docs/prompts) for the composition order overview.

## `contributePrompts` / `contributeRuntimeAwarePrompts` [#contributeprompts--contributeruntimeawareprompts]

Two prompt-contribution hooks split by when their inputs resolve.
`contributePrompts(ctx)` returns prompts that are independent of
runtime state — persona blocks, static system instructions,
provider-version notes. `contributeRuntimeAwarePrompts(ctx)`
returns prompts that depend on values resolved per-turn, such as
the active tool set, the channel mode, or the tenant.

```typescript
contributePrompts?(ctx: PromptContext): readonly PromptContribution[]
contributeRuntimeAwarePrompts?(
  ctx: RuntimePromptContext,
): readonly PromptContribution[]
```

The runtime calls each hook once per turn and merges results into
the composed prompt via the order documented in
[Prompts](/docs/prompts).

For the friendly API helpers (`appendPrompt`, `prependPersona`,
`replaceCore`, `scopedPrompt`, `gatedPrompt`, `createPlugin`) that
wrap these hooks, see [Prompt builder](/docs/prompt-builder).

## `contributeScrubbers` [#contributescrubbers]

Host plugins register additional `Scrubber` instances by returning
them from `contributeScrubbers()`. The runtime composes scrubbers
across plugins; each scrubber declares the event-type allowlist it
covers.

```typescript
contributeScrubbers?(): readonly Scrubber[]
```

`audit:c8-event-type-allowlist-coverage` gates the build on every
persisted event type having at least one scrubber registered —
even a pass-through counts. See [Scrubbers](/docs/scrubbers) for
the gate's rationale and [Compliance](/docs/compliance) for the
four bundled scrubbers.

## `DomainContextStrategy` (`isTableValueWord` host-supply) [#domaincontextstrategy-istablevalueword-host-supply]

`DomainContextStrategy` is the host-supply seam for domain-context
heuristics the substrate previously hardcoded. Hosts pass a
strategy object at runtime construction; the substrate calls into
it from the word-classification and garble-recovery paths.

The substrate's word-classification step now calls out to
`DomainContextStrategy.isTableValueWord(word)` instead of
consulting a built-in word list. Hosts override this to teach the
substrate domain-specific vocabulary without forking the
substrate — a search host registers query types, a
medical host registers ICD codes, a finance host registers ticker
shapes.

Other strategy entries (body-garble dispatcher, short-content
garble, early-coherence) are similarly host-overridable. See the
source for the full strategy interface.

<SourceMeta source="{ label: &#x22;src/strategies/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/strategies&#x22; }" />

## Minimal plugin shape [#minimal-plugin-shape]

```typescript
// pleach.plugin.ts
import type { HarnessPlugin } from "@pleach/core";

export const myPlugin: HarnessPlugin = {
  name: "my-plugin",

  contributeStreamObservers: () => [
    {
      when: { callClass: "synthesize" },
      factory: () => ({
        observerId: "redact-pii",
        onChunk(chunk) {
          // Return { kind: "continue" } to pass the chunk through, or
          // { kind: "amend", chunk: rewritten } to rewrite it in-flight.
          return { kind: "continue" };
        },
      }),
    },
  ],

  contributePrompts: () => [
    /* PromptContribution entries */
  ],
};
```

Lifecycle events (session created, tool completed, message added)
are **not** plugin callbacks — the `onSessionCreated` /
`onToolCompleted` / `onMessageAdded` hooks have been retired.
Subscribe to the same signals through the typed event-log
projection instead:

```typescript
for await (const event of runtime.events.iterate({ chatId })) {
  /* react to tool.completed, message.added, … */
}
```

Register the plugin once at runtime construction:

```typescript
const runtime = new SessionRuntime({
  storage: new SupabaseAdapter({ client: supabase }),
  checkpointer: new SupabaseSaver({ client: supabase }),
  plugins: [myPlugin, compliancePlugin, gatewayPlugin],
  userId: "user_123",
});
```

Registration order is the dispatch order — the substrate doesn't
re-order plugins. Two plugins that genuinely commute will produce
the same observable output regardless of order; two that don't
should be sequenced explicitly at the registration site. The
canonical non-commuting pair is a PII-redaction observer and a
metrics observer that records chunk lengths: register redaction
first and the metrics observer sees the redacted text length;
register metrics first and it sees the original length. Both
orderings are legal; the substrate's job is to make the choice
visible at the construction site, not to pick for you.

## `contributeFabricationGuard` [#contributefabricationguard]

The fabrication guard hook returns one `FabricationGuardImpl`
instance — a single object whose methods the substrate's
fabrication pipeline calls during a synthesized turn. Until
recently the implementation reached the runtime through the
`orchestratorHotpath` untyped bag; the typed hook replaces it.

```typescript
contributeFabricationGuard?(): FabricationGuardImpl | null | undefined
```

`FabricationGuardImpl` carries one method per detection signal:

| Method                                                         | Purpose                                                                               |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `applyFabricationGuard(params)`                                | The top-level pipeline — wipes paragraphs, returns the `FabricationGuardResultShape`. |
| `isGuardBlockedFailure(errorMessage)`                          | Classify a failed-tool error as guard-blocked vs real upstream failure.               |
| `detectBulkFailureFabrication(params)`                         | Fires when failure ratio is high and the response carries quantitative content.       |
| `detectUnqueriedCitations(params)`                             | Citations to sources the model never queried this turn.                               |
| `detectSelfFabricationConfession(params)`                      | Confession phrase + an 8-hex prefix matching a prior tool-use's jobId.                |
| `detectMethodResultFabricationConfession(params)`              | Phrase-only confessions retracting fabricated method-result tables.                   |
| `contentReferencesMissingTools(content, unavailableToolNames)` | Bytewise scan over prose for any tool in an unavailable set.                          |
| `detectIdentifierMismatch(params)`                             | Identifiers in prose absent from tool results.                                        |

The hook returns one impl per plugin. If multiple plugins
contribute, registration order determines dispatch order.
Returning `null` or `undefined` skips the contribution — the
substrate falls through to its baseline behavior, and any signal
whose slot is unfilled cleanly skips per [Fabrication
detection](/docs/fabrication-detection).

`contributeFabricationGuard` is not in the structured
`PluginCapabilities` cluster (the initial 8 keys).
Reach for it through `_raw`:

```typescript
// lib/plugins/corpusGuard.ts
import { definePleachPlugin } from "@pleach/core";
import type { FabricationGuardImpl } from "@pleach/core/plugins";

const corpusGuard: FabricationGuardImpl = {
  applyFabricationGuard(params) { /* ... */ },
  isGuardBlockedFailure(err) { /* ... */ },
  // ...remaining methods
};

export const corpusGuardPlugin = definePleachPlugin("corpus-guard", {
  _raw: {
    version: "0.2.0",
    contributeFabricationGuard: () => corpusGuard,
  },
});
```

### `bag-entry-retirement-readiness` audit status [#bag-entry-retirement-readiness-audit-status]

The audit tracks per-entry migration status from the
`orchestratorHotpath` untyped bag to the typed hook surface.
Current state on the `OrchestratorHotpathModules` shape:

| Entry              | Status           | Detail                                                                                                                |
| ------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------- |
| `streamHelpers`    | **RETIRED**      | Bag field deleted; the typed `SessionRuntimeConfig.metaToolNames` is canonical. Field count on the bag dropped 3 → 2. |
| `toolCoupling`     | **RETIRE-READY** | Typed replacement landed; no novel bag reads remain.                                                                  |
| `fabricationGuard` | **NEAR-READY**   | Typed `contributeFabricationGuard` hook landed; residual bag reads remain on the deprecation path.                    |

Hosts still reading from the bag should migrate via the bare → method
codemod documented in
[Host adapter](/docs/host-adapter#pleach-plugin-modernize-codemod).

## `[Pleach:capability-not-contributed]` breadcrumbs [#pleachcapability-not-contributed-breadcrumbs]

Eleven `contribute*` collectors emit a one-shot
`[Pleach:capability-not-contributed]` console line when they
aggregate to an empty array. The breadcrumb is dedup-keyed on
`${sessionId}:${capability}`, so it fires once per session per
capability rather than per turn. The runtime keeps working — the
collector returns its empty result and the substrate proceeds —
but the line surfaces a missing plugin without spamming logs.

The eleven gated capabilities:

| Capability hook                      |
| ------------------------------------ |
| `contributeStreamObservers`          |
| `contributeStreamFilters`            |
| `contributeSafetyPolicies`           |
| `contributeFabricationDetectors`     |
| `contributePostToolTier`             |
| `contributeRefClassValidators`       |
| `contributeMiddleware`               |
| `contributeRuntimeAwareMiddleware`   |
| `contributeToolCouplingHints`        |
| `contributeIntentToolMap`            |
| `contributeIntentParameterResolvers` |

Payload shape:

```js
console.log("[Pleach:capability-not-contributed]", {
  capability: "contributeStreamObservers",
  sessionId: "sess_018f...",
  hint: "register a HarnessPlugin that implements contributeStreamObservers() to provide this capability",
});
```

To surface the same gaps at construction time — before any seam
fires — call `inspectRuntime(runtime)` and walk
`report.capabilities` for rows whose `wiredCount === 0`. See
[Runtime inspector](/docs/runtime-inspector#capability-breadcrumbs-separate-from-the-inspector).

## Sibling SKUs as plugins [#sibling-skus-as-plugins]

The `@pleach/*` siblings surface as plugins (or as DI-friendly
clients that consume the plugin contract). The table below reflects
where each SKU sits today — most ship a real Phase A surface; a
few are contract-only or still landing slice-by-slice. Pin exact
versions per the [Packages](/docs/packages) bucket guidance.

| Package                        | What it contributes                                                                                                                                                                                                                                                                                                                                                 | Status                      |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| `@pleach/compliance` `0.1.0`   | Scrubbers (SSN-US, Luhn, US-DL, KeyedRegex) + `ComplianceRuntime` contract substrate; event-chain attestation; verification utilities for HIPAA / GDPR / PCI-DSS / SOC 2.                                                                                                                                                                                           | Shipping                    |
| `@pleach/eval` `0.1.0`         | `EvalSuite` + `EvalCase` discriminated union + scorers + report formatters. Reads the `AuditableCall` ledger via the event-log projection (`runtime.events.iterate`); consumes `@pleach/replay`'s `ReplayClient` via DI.                                                                                                                                            | Shipping                    |
| `@pleach/gateway` `0.1.0`      | `GatewayClient` — thin wrapper over `@pleach/core`'s model-family substrate; per-tenant BYOK key routing; family-strict cascade pivot; per-call cost emission; OTel `llm.invocation` span emission.                                                                                                                                                                 | Shipping                    |
| `@pleach/replay` `0.1.0`       | `ReplayClient` walks the canonical event log via `runtime.events.iterate` / `fold`. `createReplayRuntime` factory ships real bodies for `replayTurn`, `fromSnapshot`, `fork`, and `aggregateMultiTenant` — zero throw sites remain.                                                                                                                                 | Shipping                    |
| `@pleach/mcp` `0.1.0`          | Concrete `MCPServer` class wrapping `SessionRuntime` over stdio. SSE + WebSocket transport arms ship in the union for shape-locking; `server.start({ transport: "sse" \| "websocket" })` throws `NotImplementedError("D-PA-181")` until the next slice. Multi-tenant `registerSession()` throws `NotImplementedError("D-PA-184")` until `@pleach/gateway` C3 lands. | Shipping (stdio end-to-end) |
| `@pleach/coding-agent` `0.1.0` | Typed `CodingAgentRuntime` contract at `@pleach/coding-agent/runtime`. `start()`, `stop()`, and `executeStep()` bodies all land at the `0.1.0` cut. `executeStep()` throws `PACK_270_D3_EXECUTE_STEP_NOT_STARTED_MESSAGE` only when called before `start()`.                                                                                                        | Shipping                    |
| `@pleach/sandbox` `0.1.0`      | Vendor-neutral `SandboxProvider` contract + in-memory fixture. Canonical name `SandboxProvider`; legacy `SandboxAdapter` retained as a `@deprecated` alias.                                                                                                                                                                                                         | Shipping (contract)         |
| `@pleach/observe` `0.1.0`      | Destination-flexible audit-row SDK. `init` + per-turn recorder + destination plug, five destinations (memory, postgres, supabase, otel, pleachHosted), PII redaction, sampling, fingerprint, subagent.                                                                                                                                                              | Shipping                    |
| `@pleach/recipes` `0.1.0`      | Composable one-line factories: `simpleChatbot`, `ragChatbot`, `observableChatbot`, `compliantChatbot`, `instrumentedCodingAgent`. Each subpath pulls only the `@pleach/*` peers its recipe needs.                                                                                                                                                                   | Shipping                    |

The plug-and-play guarantee already holds — none of the above can
break the lattice, the singleton seam, or the family lock (those
are CI gates on the core repo, not opt-in discipline). Each
sibling's own README documents the slots it fills and the config
it expects.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Contribution namespaces" href="/docs/plugins/namespaces" description="The nine namespaces every contribution hook resolves to — the canonical map of where a plugin plugs in." />

  <Card title="Architecture" href="/docs/architecture" description="The six pieces of the substrate — stage lattice, call classes, seams, family-lock, audit ledger, event log." />

  <Card title="Packages" href="/docs/packages" description="The @pleach/* matrix — which sibling does what." />

  <Card title="Auditable call row" href="/docs/auditable-call-row" description="The audit row every plugin's contributions are reflected in." />

  <Card title="Host adapter" href="/docs/host-adapter" description="setHarnessModuleLoader and the R-track that retires loader keys into plugin slots." />
</Cards>
