Markdown 165 lines
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/eeco_logo_dark.png">
<img src="assets/eeco_logo_light.png" alt="eeco" width="280">
</picture>
<h1>Extending</h1>
<p><i>Where each kind of extension plugs in, seam by seam.</i></p>
<p>
<a href="README.md"><b>README</b></a> ·
<a href="VISION.md"><b>Vision</b></a> ·
<a href="docs/COCKPIT.md"><b>Cockpit</b></a> ·
<a href="docs/USAGE.md"><b>Usage</b></a> ·
<a href="docs/ARCHITECTURE.md"><b>Architecture</b></a> ·
<a href="docs/PUBLIC_API.md"><b>Public API</b></a> ·
<b>Extending</b> ·
<a href="CONTRIBUTING.md"><b>Contributing</b></a> ·
<a href="docs/UPGRADING.md"><b>Upgrading</b></a> ·
<a href="VERSIONING.md"><b>Versioning</b></a> ·
<a href="CHANGELOG.md"><b>Changelog</b></a> ·
<a href="SECURITY.md"><b>Security</b></a>
</p>
</div>
---
## What this is
This is the how-to companion to the **Extension seams** table in
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md#extension-seams). That table is the
authoritative map of where each kind of extension is registered; this guide
expands the two most common seams into step-by-step examples and gives a short
reference for the rest.
eeco is built so that most extensions have a **single registration point** plus
one new file. The verb dispatcher, the workflow registry, the config parser, and
the TUI command index each read from one list, and adding an entry there is the
whole job for the low-friction seams. One seam — a new hook type — is
deliberately higher friction; see [Hook types](#hook-types) below.
Before you start, read [CONTRIBUTING.md](CONTRIBUTING.md) for the build, the
gates, and the Definition of Done every change meets.
## Add a builtin workflow
A builtin workflow is a Go type that ships in the binary and runs via
`eeco run <name>`. The whole job is one new file plus one line in the registry.
**1. Implement the `Workflow` interface** (`internal/workflow/workflow.go`) in a
new file `internal/workflow/<name>.go`. The interface is three methods:
```go
func (myWorkflow) Name() string { return "my-workflow" }
func (myWorkflow) Summary() string { return "one line shown by eeco run." }
func (myWorkflow) Run(env Env) (Result, error) { /* ... */ }
```
Use an existing workflow as the model — `internal/workflow/leakguard.go` is a
good template for a read-only gate.
**2. Register it** by adding a zero value to the `DefaultRegistry` slice literal
in `internal/workflow/registry.go`:
```go
for _, w := range []Workflow{commentHygiene{}, leakGuard{}, /* ... */, myWorkflow{}} {
```
Lookup, listing, and tab-completion are all derived from this slice — there is
nothing else to wire.
**3. Test it** in `internal/workflow/<name>_test.go`.
**4. If it is user-facing**, the workflow name is part of the frozen surface: add
it to [`docs/PUBLIC_API.md`](docs/PUBLIC_API.md) (builtin workflow names),
document it in [`docs/USAGE.md`](docs/USAGE.md) and run `make sync-guide` to
mirror that into the binary, then add a [CHANGELOG.md](CHANGELOG.md) entry.
**Worked reference:** commit `575557b` added the `commit-guard` workflow exactly
this way — `internal/workflow/commitguard.go` plus the one registry line plus a
test — in the same change that added its companion hook type.
## Add a `config.local` key
`config.local` is the per-workspace settings file. A new key is a struct field
plus a parse case.
**1. Add the field** to the `Config` struct in `internal/config/config.go`, with
its JSON tag.
**2. Give it a default** if it carries one — a `Default…` const applied in `Load`
so an absent key resolves to a safe value (the unknown-to-default floor).
**3. Parse it** in `applyLocal`, the switch that maps each `config.local` key
onto its field. Validate or normalize there when the value is constrained: an
enum key falls back to its default on garbage input, never crashes.
**4. Test it** in `internal/config/config_test.go`, including the garbage-in
case.
**5. If it is user-facing**, config keys are frozen: add it to
[`docs/PUBLIC_API.md`](docs/PUBLIC_API.md) and document it in
[`docs/USAGE.md`](docs/USAGE.md) (then `make sync-guide`), with a CHANGELOG
entry.
**Mind the ripple.** The registration is small, but every read site is a touch.
Commit `5d5b2d9` added `workspace_history` — a single struct field and parse
case, yet roughly fifteen call sites across `cmd/eeco/` read it to decide whether
to auto-commit. Plan for where the value is consumed, not only where it is
declared.
## Other seams (reference)
Each entry below is the short form; the
[Extension seams table](docs/ARCHITECTURE.md#extension-seams) carries the
canonical list with the full touch-set.
- **CLI verb** *(low)* — register in the `run` dispatch switch in
`cmd/eeco/main.go` and add it to the `usage` const; put the runner in a new
`cmd/eeco/<verb>.go`, reusing the `loadInitedConfig` / `loadRepoConfig` /
`newFlagSet` guards in `cmd/eeco/helpers.go`.
- **Memory frontmatter field** *(medium)* — add the field to the `Fact` struct in
`internal/memory/fact.go`, then teach `frontmatter.go` to read it (`setField`)
and write it (`Serialize`); add a `Validate` rule if it is constrained.
- **AI provider** *(medium)* — add the provider type (`Name` / `Run`) and wire it
into the `Select` chooser in `internal/ai`, plus the `config.local` key that
selects it (see above).
- **TUI slash-command** *(low)* — add it to the `commandIndex` in
`internal/tui/commands.go` and handle it in the `dispatch` switch; completion
is derived automatically.
- **TUI chat tool** *(low)* — add it to the `chatTools` set in
`internal/tui/tools.go` and handle it in `chatExecutor`. The chat-tool registry
is read-only by construction, so a tool never gains a new write capability.
- **Hook type** *(HIGH)* — see below.
### Hook types
A new hook type is the one irreducibly multi-file seam, and the friction is the
price of the trust boundary. A hook is an opt-in, reversible escape from the
workspace, so every hook type carries a ledger field that records its install for
exact, byte-identical removal. Adding one touches the name const and the `Names`
list, the `ledger` struct, the enable/disable/refresh trio, and the `Status`
read-out in `internal/hooks/hooks.go`, plus the CLI dispatch in
`cmd/eeco/hooks.go` — and they stay in lock-step. Commit `575557b` is the worked
reference (it added the `commit-guard` hook). Prefer the single-registration
seams above; add a hook type only when the capability genuinely needs to leave
the workspace. See **Trust boundaries** in
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for why this coupling is
deliberate.
## Before you ship
Every change meets the Definition of Done in
[CONTRIBUTING.md](CONTRIBUTING.md): `make verify`, `make gates`, `make lint`, and
`make cover-check` green; additive over
[`docs/PUBLIC_API.md`](docs/PUBLIC_API.md) unless you declare a semver bump (see
[`VERSIONING.md`](VERSIONING.md)); no AI attribution; a CHANGELOG entry when the
change is user-facing.
---
[← Prev: Public API](docs/PUBLIC_API.md) · [Next: Contributing →](CONTRIBUTING.md)