Composing What Runs¶
coding is nine features, not one system. Some people want the whole stack; some want the LLM proxy and nothing else; some cannot run Docker at all. Rather than forking the install, you switch parts off.
The nine features¶
| id | what it is | needs Docker |
|---|---|---|
lsl | Verbatim session transcripts, kept per repo in <repo>/.coding/history/ | no |
observations | The observation β digest β insight pipeline | no |
knowledge | Semantic analysis, UKB workflows, the knowledge graph, VKB | yes |
codegraph | The graphify code knowledge graph and its MCP endpoint | yes |
constraints | Guardrail rules checked before every tool call | yes |
llm-proxy | Provider routing, fallback and token accounting | no |
performance | Per-task measurement, experiments, the kgbench benchmark | no |
health | Health coordinator, auto-healing, the monitoring dashboard | no |
statusline | The tmux / agent status line | no |
Everything is on by default. An install with no configuration is byte-for-byte the historical stack, so there is nothing to do unless you want less.
Change it from the terminal¶
coding-features # what is on, and why
coding-features set knowledge off # one feature
coding-features profile proxy-only # a preset
coding-features explain knowledge # why is this off?
Or from the dashboard¶
Health β Features, or localhost:3032/features directly.

Each row carries its id, when the change takes effect, and whether it needs Docker. Nothing happens until Save & apply.
Four presets¶
| profile | leaves on | Docker |
|---|---|---|
full | everything | yes |
logging-only | lsl, health, statusline | no |
proxy-only | llm-proxy, statusline | no |
minimal | statusline | no |
Only knowledge, codegraph and constraints need Docker. Switch all three off and the launcher never starts the container or asks for a daemon β which is what makes the bottom three profiles work on a machine without Docker Desktop.
Dependencies resolve downwards, never upwards¶
Three features are built on others:
A dependent whose dependency is off is switched off too. The dependency is never switched on for you. Turning something off is an explicit instruction and is honoured exactly; turning something on by implication would start services you did not ask for.
The editor previews the whole cascade before you commit to it. One click on Live Session Logging takes Observations and Knowledge Base with it β each greyed out, each saying which dependency it is waiting on, and the footer counting what would survive the save:

A blocked feature's toggle is not merely greyed β it will not move. Turning Knowledge Base back on from that state would be undone by the resolver a moment later, and a switch that silently flips itself back is worse than one that refuses.
When a change takes effect¶
Not everything can apply instantly, and the UI says which is which rather than implying they are all the same.
| tier | what it covers | when |
|---|---|---|
live | status line, dashboard gating, coordinator checks, CLI gates | next read β no restart |
on save | host daemons and container programs | immediately; only the delta is started or stopped |
new sessions | agent hooks | next agent launch β --settings is fixed at launch |
Where the setting lives¶
Four layers, last one wins:
- built-in defaults β everything on
<repo>/config/features.yamlβ committed, shared by the team~/.coding/features.yamlβ this machine; what the CLI and the dashboard writeCODING_FEATURE_<ID>=on|offβ this shell only
coding-features explain <id> names the layer that decided, so "why is this off" has one answer everywhere β the CLI, the status line tooltip and the dashboard chip all quote the same reason string.
Turning off the thing you are looking at¶
The dashboard is served by the health feature. Saving proxy-only or minimal from the editor therefore stops the editor. It asks first, and whatever terminal or log the apply lands in prints the way back:
β οΈ Health Monitoring is now off: the coordinator and the dashboard have stopped.
The dashboard cannot turn it back on, because the dashboard is part of it.
To restore everything: coding-features profile full
What a pared-down install looks like¶
The status line is the fastest way to see the effect β a disabled feature contributes no badge at all, rather than a greyed-out one:
full β everything on:
[π₯β] [AXβCβ] [π72%β7] [πβ] [N:OPEN P:AUTO] [π§ β] ββββββββ 46% [π8-9] 08:19
logging-only β health, sessions and the log tranche survive; the knowledge, constraint and proxy badges go with their features:
[π₯β] [AXβCβ] [N:OPEN P:AUTO] ββββββββ 47% [π8-9] 08:19
proxy-only β one badge, the gauge, the clock:
[π§ β] ββββββββ 47% 08:19
minimal:
ββββββββ 47% 08:19
The context gauge and the clock are core, not a feature, so they survive every profile. The examples above are generated from the real renders by scripts/render-statusline-png.mjs --spans, not typed β hand-written bars on this site had already drifted to ten-cell gauges and a split [N:] [P:] pair months after both changed.
Nine features, one resolver, four surfaces that must agree about them. This tier is the mechanism: how a decision is reached, who is allowed to act on it, and which failures are deliberately asymmetric.
The resolver¶
lib/features/resolve.cjs is the only thing that decides whether a feature is on. It is CommonJS on purpose β the status line renders it on every tmux tick and cannot afford ESM resolution β and it is mtime-cached, so a read costs a stat().
Four layers, evaluated in order, last one wins:
| # | layer | scope |
|---|---|---|
| 1 | built-in defaults (all on) | the product |
| 2 | <repo>/config/features.yaml | the team β committed, shipped fully commented out |
| 3 | ~/.coding/features.yaml | this machine β what coding-features and the dashboard write |
| 4 | CODING_FEATURE_<ID>=on\|off | this shell |
Layer 2 exists so a project can pin what it needs without every developer configuring it by hand; it ships entirely commented out, so it doubles as schema documentation in the place you would look for it.
Every resolution carries a reason string, and every surface quotes it verbatim rather than re-deriving one β coding-features explain, the status line tooltip, the dashboard chip. That is what makes "why is this off" have exactly one answer.
Dependencies¶
The rule is one-directional: a dependent whose dependency is off is auto-disabled; a dependency is never auto-enabled. Off is an explicit instruction and is honoured exactly. On-by-implication would start services nobody asked for, which is how a "minimal" install quietly becomes a full one.
The resolver applies this transitively to a fixed point, and so does the dashboard's preview β switching lsl off greys out observations and knowledge. An editor that previewed only one level was worse than one that previewed none, because the second surprise arrived after the click.
Fail-open and fail-closed are deliberately asymmetric¶
| kind of code | on an unparseable config | why |
|---|---|---|
| anything that STARTS a process β launcher, service starter, CLI guards | fail closed: abort | half-starting a system is worse than not starting it |
| anything that DISPLAYS β status line, dashboard, coordinator checks | fail open: show everything, plus the error | a UI that silently drops half its content is indistinguishable from a broken build |
Apply tiers¶
| tier | covers | mechanism |
|---|---|---|
live | status line, dashboard gating, coordinator checks, CLI gates | all read the mtime-cached resolver on next use |
apply | host daemons and container programs | scripts/apply-features.mjs diffs and starts/stops only the delta |
session | agent hooks | --settings is fixed at launch, so new sessions only |
apply deliberately does not restart the world: coding --claude already does a full start, and a config edit should cost the smallest disruption that makes the config true.
The three kinds of artifact¶
Adding a service, daemon, container program, tab or badge means declaring its feature, and four mappings have to agree β lib/features/daemons.mjs, scripts/apply-features.mjs, docker/entrypoint.sh and the architecture reference. tests/features/*.test.mjs fails the build when they drift.
There is one artifact that fits neither table: the ETM. enhanced-transcript-monitor.js is the writer behind lsl, and the health coordinator spawns it per project detached and unref()ed β no launchd label, no supervisord program, and it outlives whoever started it. It is therefore gated in three places rather than one:
| path | when lsl is off |
|---|---|
launch (SERVICE_CONFIGS.transcriptMonitor) | not started |
| coordinator safety-net sweep | does not spawn |
| coordinator reap | SIGTERMs every running ETM |
apply (reconcileEtm) | SIGTERMs every running ETM |
The last two overlap on purpose: the coordinator can only reap while it is running, and minimal and proxy-only stop it in the same pass that switches lsl off.
Why the API lives on the coordinator, not the dashboard¶
/features is served by the health coordinator on :3034. The dashboard runs inside the coding-services container; the file it edits (~/.coding/features.yaml) is on the host, and applying a change runs launchctl / systemctl / schtasks. server.js reverse-proxies /api/features so the browser sees one origin. There is exactly one writer implementation, lib/features/write.mjs.
Docker¶
Only knowledge, codegraph and constraints need the container. With all three off, _start_services() skips it entirely and the launcher never demands a daemon.
health deliberately does not need Docker: the coordinator and both dashboard servers have host implementations, which is what lets logging-only run a dashboard on a machine with no Docker Desktop at all.
Reference¶
The engineering reference β the full featureβartifact matrix, port table, badge ownership, and the coordinator health-check mapping β is docs/architecture/features.md in the repo.