Extensions

The extension seam

A fork customizes which phases run, which skills those phases use, and the prompts — entirely from a top-level daydream_ext package, without editing any file under daydream/. Every flow (deep, shallow, review, pr-feedback) is an ordered list of named steps executed by a small engine (run_flow in daydream/flows/engine.py) over a shared context. A per-run Registry holds three namespaces — phases and flows, skill slots, and named prompts — seeded with everything Daydream does by default, then handed to the extension for mutation through the same API the built-ins used.

The authoritative, versioned contract (full name inventories, prompt kwargs, stable context keys, bump policy) is docs/extensions.md in the repository, drift-guarded by a test that pins the document to the registered inventories in code.

The extension module

A fork creates one package next to daydream/:

daydream_ext/
└── __init__.py

__init__.py exports exactly two things:

DAYDREAM_EXT_API = 1 # must equal daydream's EXTENSION_API_VERSION
def register(registry): # receives a daydream.extensions.Registry
... # mutate flows / skills / prompts / stacks here

Discovery order:

  1. $DAYDREAM_EXT_DIR — explicit path to the package directory, loaded fresh on every run.
  2. import daydream_ext — the fork extension package.
  3. No extension — builtins-only registry. Absence is silent and normal.

A present-but-broken extension is a loud, named error before any work happens: a missing or mismatched DAYDREAM_EXT_API raises ExtensionVersionError naming both versions; a missing register, an import failure, or an exception inside register() raises ExtensionError with the original message. All of them exit the run with code 1. There is no compatibility range — the API version is a single integer that bumps on any breaking change to the registry API, flow or step names, prompt names or kwargs, skill slots, or the documented stable context keys.

Upstream's pyproject.toml pre-declares daydream_ext in the wheel packages list, so a fork ships the package with zero upstream-file edits and wheels keep working.

What a fork can do

Everything goes inside register(registry):

from daydream.extensions import FlowStep, StackRule
def register(r):
# Insert, remove, replace, or reorder flow steps
r.register_phase(FlowStep(name="my_gate", run=_my_gate))
r.insert_after("deep", anchor="intent", step="my_gate")
r.remove("deep", "alternatives")
# Remap a built-in stack's review skill
r.override_skill("stack:python", "my-fork:review-python")
# Add a new stack via glob rules (evaluated before the built-in table)
r.add_stack(StackRule("proto", ("*.proto",), "my-fork:review-proto"))
# Bind a skill to a phase (feeds shallow skill resolution)
r.override_skill("phase:review", "my-fork:review-python")
# Override a named prompt wholesale (receives the built-in kwargs)
r.override_prompt("review", my_builder)

Per-phase model and backend configuration needs no extension code — [tool.daydream.phases.<name>] in pyproject.toml or .daydream.toml accepts arbitrary phase names, including fork-defined ones (see Configuration).

Validating an extension

daydream ext validate

Loads the extension, reports its source and API version, resolve-checks every flow entry, skill slot, and stack rule, and prints a per-namespace summary. Broken references exit 1 naming the broken piece. Runs anywhere — no target repo needed. The same resolve pass runs as a mandatory pre-flight in run_flow, so an unresolvable flow fails before any step executes.

Limits

  • Backends are the built-in three (claude, codex, pi); forks cannot register new ones.
  • Prompt override is wholesale only — there is no append/compose hook.
  • The parse, test, commit, and internal control-loop prompts are not registered; they are schema-coupled.
  • The workspace/diff/recorder preamble runs before any flow step; phases begin at exploration.

Back to Daydream

Daydream overview