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:
$DAYDREAM_EXT_DIR— explicit path to the package directory, loaded fresh on every run.import daydream_ext— the fork extension package.- 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.