Skip to content

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.

The Features editor

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:

lsl ──▢ observations ──▢ knowledge
llm-proxy ──▢ performance

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:

Dependent features switching themselves off

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:

  1. built-in defaults β€” everything on
  2. <repo>/config/features.yaml β€” committed, shared by the team
  3. ~/.coding/features.yaml β€” this machine; what the CLI and the dashboard write
  4. CODING_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

lsl ──▢ observations ──▢ knowledge
llm-proxy ──▢ performance

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.