The .reef File Format
The .reef file declares the habitat. reef-mcp reads it once, resolving every config value it needs — where arms live, where TRACE writes, where the dotnet-script runtime is — before anything runs.
The same philosophy as .reach and .octo: write the declaration. The system resolves the implementation.
Basic Structure
Flat key value lines, # comments, one sources list block. No nesting, no wrapper syntax:
# reef <name>
trace_file <path to reach-trace.ndjson>
arms_dir <path to arm .cs files>
octo_dir <path to .octo workflow files>
workspace <path to workspace root>
reach_repo <path to reach repo>
dotnet_root <path to the .NET installation>
dotnet_script <path to the dotnet-script binary>
sources
- <dot.path>
- <dot.path>Every key is optional — reef-mcp falls back to an environment variable of the same name (ARMS_DIR, TRACE_FILE, and so on) if the .reef key is missing, and the environment variable always wins if both are set. Adopting .reef is zero-risk for that reason: nothing already working via env vars changes just because a .reef file exists alongside it.
The Keys
| Key | Env var fallback | What it declares |
|---|---|---|
trace_file | TRACE_FILE | The append-only TRACE log |
arms_dir | ARMS_DIR | Directory containing .cs arm files |
octo_dir | OCTO_DIR | Directory containing .octo workflow files |
workspace | WORKSPACE | Root of the local workspace |
reach_repo | REACH_REPO | Path to a REACH spec/example repo, if used |
dotnet_root | DOTNET_ROOT | Root of the .NET installation dotnet-script needs |
dotnet_script | DOTNET_SCRIPT | Absolute path to the dotnet-script binary |
trace_file, arms_dir, and octo_dir may be relative — they resolve relative to wherever the .reef file itself lives, which is the actual portability win: clone the whole thing to a new machine, and those three just work without editing a single path. workspace, reach_repo, dotnet_root, and dotnet_script are genuinely machine-specific and should stay absolute.
dotnet_root/dotnet_script have no built-in fallback default — if neither the env var nor .reef supplies them, run_arm refuses to execute anything and says so clearly, rather than silently guessing at a path that might be wrong on a different machine.
The sources Block
Declares what this habitat is allowed to touch:
sources
- git.commits
- reef.trace
- fetch-market-dataCurrent state: parsed and reported — check_habitat surfaces the declared list — but not enforced. Nothing yet cross-checks an arm's own SOURCES: line against what's declared here. That's real, deliberately deferred work: enforcing it means parsing every arm's header and gating run_arm on the result, which hasn't been built because nothing has needed it yet. Declaring sources now means the habitat is honestly described even before anything reads that description as a boundary.
A Real Example
# reef dev-machine
trace_file reach-trace.ndjson
arms_dir arms
octo_dir octo
workspace /Users/<you>/workspace
reach_repo /Users/<you>/workspace/reach
dotnet_root /opt/homebrew/opt/dotnet/libexec
dotnet_script /Users/<you>/.dotnet/tools/dotnet-script
sources
- git.commits
- reef.tracetrace_file/arms_dir/octo_dir are relative — this whole file can move with the repo it lives in. The rest are this machine's actual paths.
.reef and .reef.example
.reef holds real, machine-specific values — gitignored, never committed. .reef.example is the committed template, placeholder values (/Users/<you>/...) instead of real ones — the same split as .env/.env.example. Copy the example, fill in real paths, done.
.reef vs .octo — Side by Side
# my-machine's .reef — the habitat (declared once)
trace_file reach-trace.ndjson
arms_dir arms
sources
- outlook.inbox
- outlook.calendar
- git.commits# morning.octo — the workflow (runs on demand)
name morning-brief
arms
- MORNING-BRIEF
- REPO-STATUS
surface
title "Good morning."
close
tone warm.reef doesn't change when a workflow changes. .octo doesn't need to know where arms_dir resolves to. Independent, complementary declarations — habitat and intent, each doing exactly one thing.
Design Principles
Declare once, resolve everywhere. Every path, every source — declared once, resolved through one function (resolveConfig) everywhere it's needed.
Env var wins, .reef is the fallback. Not a competing config system — a safety net under the one that already works. This is what makes adopting .reef additive rather than a migration.
Declaration is not configuration. git.commits in the sources block isn't a connection string — it's a statement that this source is part of this habitat, whether or not anything checks it yet.
The file travels with the habitat, not the machine. Relative paths for what lives inside the repo; absolute paths only for what's genuinely tied to this machine.