How Experimental Target Providers Handle Format Evolution in Compound Engineering

The Compound Engineering Plugin handles format evolution through a decoupled architecture where target registries, converters with hook-event maps, and writer modules isolate format changes to specific components without breaking the CLI pipeline.

The EveryInc/compound-engineering-plugin repository treats every output format—including OpenCode, Codex, Droid, Cursor, Pi, and Gemini—as an experimental target. This design philosophy ensures the system remains adaptable as underlying provider specifications evolve, allowing developers to update individual conversion layers without disrupting the entire conversion pipeline.

Decoupled Architecture Components

The plugin isolates format volatility across three distinct layers. When an experimental target provider changes its schema, developers modify only the relevant layer while the CLI and core logic remain untouched.

Target Registry (src/targets/index.ts)

The central registry exposes a targets object where each entry declares three properties: implemented, convert, and write. This structure allows the system to toggle targets on or off during format transitions.

Adding a new experimental provider requires a single registry entry. Setting implemented to false temporarily disables a target while its format undergoes updates, preventing broken conversions without removing code. The CLI references this registry when processing the --to <target> flag, making new targets automatically available to users.

Converters with Hook-Event Maps (src/converters/claude-to-*.ts)

Converter modules translate Claude-Code concepts into target-specific schemas. Each converter contains a hook-event map (HOOK_EVENT_MAP) that bridges Claude hook names to provider-specific events.

In src/converters/claude-to-opencode.ts, the map includes experimental session events such as:

PreCompact: { events: ["experimental.session.compacting"], type: "session" },

Source: line 55 in the file.

When Claude emits PreCompact, the converter generates a hook listening for the experimental OpenCode event experimental.session.compacting. If future OpenCode releases rename or remove this event, the conversion logic absorbs the change without affecting the writer module.

Writer Modules (src/targets/*.ts)

Writer modules take intermediate bundles and emit final files in the provider's required directory structure. Each writer knows the exact file layout for its format—whether JSON, TOML, or custom configuration files.

When an experimental target's format changes, only the corresponding writer requires updates. The rest of the pipeline—from CLI parsing through converter logic—remains intact, minimizing the surface area of breaking changes.

Handling Unmapped Events

The conversion pipeline includes defensive mechanisms for experimental features that lack stable mappings. When a Claude hook cannot be mapped to a target event, the system preserves this information rather than silently dropping it.

Lines 78-82 in src/converters/claude-to-opencode.ts implement this safeguard:

const unmappedComment = unmappedEvents.length > 0
  ? `// Unmapped Claude hook events: ${unmappedEvents.join(", ")}\n`
  : ""

Source: lines 78-81.

This logic emits a comment in the generated hooks file listing any unmapped Claude events. Developers reviewing the output can immediately identify which constructs require manual migration or updated mappings, providing a clear upgrade path as experimental formats stabilize.

Practical Workflow for Format Updates

Converting to Experimental Targets

The CLI provides direct access to all experimental targets through the --to flag:


# OpenCode (experimental)

bunx @every-env/compound-plugin install compound-engineering --to opencode

# Codex (experimental)

bunx @every-env/compound-plugin install compound-engineering --to codex

# Pi (experimental)

bunx @every-env/compound-plugin install compound-engineering --to pi

These commands invoke src/commands/install.ts, which forwards the target name to the registry and executes the appropriate converter and writer chain.

Adding New Experimental Mappings

To support a new Claude hook like SessionPause mapping to an upcoming OpenCode event, update the converter's hook-event map:

SessionPause: { events: ["experimental.session.paused"], type: "session" },

After this change, subsequent conversions automatically include the new mapping. The writer module requires no modification because it consumes the intermediate bundle format, not the specific event names.

Summary

  • Target registry (src/targets/index.ts) centralizes provider availability through the implemented flag, allowing temporary disabling during format updates.
  • Converters isolate schema translation through HOOK_EVENT_MAP, mapping Claude hooks to experimental provider events without touching file-writing logic.
  • Unmapped event tracking preserves unhandled hooks as comments in generated files, preventing silent data loss during format evolution.
  • Writer modules encapsulate file-layout knowledge, so format changes affect only the specific target's writer implementation.
  • CLI integration automatically surfaces new targets via the registry, requiring no command-layer changes when adding or updating providers.

Frequently Asked Questions

What happens when an experimental target changes its event format?

When an experimental target changes its event format, you update the corresponding entry in the converter's HOOK_EVENT_MAP (e.g., in src/converters/claude-to-opencode.ts). The system preserves unmapped events as comments in the generated output, allowing conversions to complete successfully while flagging items that need manual review. The writer module remains unchanged because it consumes an intermediate bundle rather than raw event names.

How do I add support for a new Claude hook in an experimental target?

Add the mapping to the HOOK_EVENT_MAP in the appropriate converter file. For example, to map a new SessionPause hook to an OpenCode event, insert SessionPause: { events: ["experimental.session.paused"], type: "session" } into src/converters/claude-to-opencode.ts. The next conversion will automatically include this mapping in the generated hooks file without requiring changes to the writer or CLI.

Where is the experimental target registry defined?

The experimental target registry is defined in src/targets/index.ts. This file exports a targets object where each key represents a provider (such as opencode, codex, or pi) and each value contains implemented, convert, and write properties. The CLI references this registry to validate --to arguments and route conversions to the correct handler.

Can I disable an experimental target while updating its format?

Yes. Set the implemented property to false in the target's registry entry within src/targets/index.ts. This prevents the CLI from executing conversions for that target while you modify its converter logic or writer implementation. Once the format updates are complete and tested, toggle implemented back to true to re-enable the target.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →