Skip to content

Domain ​

app/src/domain/ is types, invariants, and pure functions. No filesystem, no sockets, no Google SDK, no OpenTUI. If a function needs I/O, it does not belong here.

Brand IDs (domain/ids.ts): serviceId(), profileId() wrap strings so start/profile APIs cannot mix raw names accidentally. RPC still uses strings at the wire; convert at the command boundary (StartService, ResolveStart).

domain/config/ ​

FileRole
types.tsDevctlConfig and every nested config type (ServiceConfig, ProxyConfig, HttpRecipeConfig, LlmConfig, …). Helpers: emptyService, graceSeconds, stopOnExit, dependencyName, HTTP key names. This is the post-load snapshot.
paths.tsConfigDirName (.devctl), ConfigFileName (config.yaml).
env-ref.ts${…} reference parsing and secretTemplateLabel.
snapshot.tsDiff two configs after reload (configSnapshotDiff) — which services need restart.
provenance.tsTypes for “which file/layer set this key” (filled by the adapter).

DevctlConfig also carries runtime-only fields set at load: repoRoot, configPath, provenance. They are not authored in YAML.

Current schema version: CurrentVersion = 1.

domain/service/ ​

FileRole
services.tsLifecycle string constants (STARTING, HEALTHY, …), Runtime (live row), startupPlan / shutdownPlan / shutdownPlanExact, resolveStartRequest, dependentsClosure, profileEnvironment. Cycle detection lives in the config validator; plans assume a valid graph.
lifecycle.tsLEGAL_TRANSITIONS / canTransition / transition. Illegal edges throw KindConfiguration.
policies.tsStartupPolicy, RestartPolicy, HealthPolicy — interpret YAML (never / on_failure / always, wait-for-healthy, retry budget).
container-limits.tsParse memory / cpus / pids_limit into a Docker/Podman shape.
container-volumes.tsScope named volumes per stack (devctl-<id>-<name>), their first-use seeds, and seed_from / shared_volumes checks.
watch.tsPure helpers for file-watch restart (paths, ignore globs).

Waves: startupPlan returns Plan { profile, steps, waves, blockers? }. A configured profile clips the dependency closure to profile.services ∪ selected. Independent services share a wave (Promise.allSettled in the orchestrator); the next wave waits only for members a later step depends on with condition: service_healthy. Shutdown reverses dependencies unless shutdownPlanExact (stop only the named set).

domain/status.ts ​

Wire/UI snapshots: StartRequest, StatusSnapshot, IdentitySnapshot, ProxySnapshot, McpSnapshot, ReloadResult, TraceResponse, CredentialEntrySnapshot, log/LLM page aliases used across RPC. Keep this module free of adapter classes.

StartRequest.auto is internal. Only health-triggered restarts set it so the restart counter is not reset. overlay is the sticky session config layer; extra_env is ephemeral process-env for targeted services.

domain/identity/ ​

FileRole
identity.tsIdentity value type, configuredServiceAccounts, identityBlockers (start must not launch SA services without ADC).
google-status.tsGoogleStatus DTO (gcloud installed, ADC, email, project source).

User vs service identity stay separate types. The proxy route decides which one is used; nothing “falls back” silently.

domain/logs/ ​

FileRole
logs.tsLogRecord / ingest / filter / page / facets / parser plugin type. Barrel for the rest.
types.tsLevel names, sources.
parse.tsInfer level from free-form lines.
filter.ts, pagination.tsQuery algebra and cursors.
redact.tsRecord-level redaction.
record.ts, ids.tsNormalize records; trace/span/request ids.
display.tsFormatting helpers shared with TUI.
severity.tsLevel ordering (INFO+).
regex.tsSafe search compilation.
python-literal.ts, otlp-value.ts, any-value.tsDecode nested log payloads (Python literals, OTLP AnyValue).

domain/proxy/ ​

FileRole
strip-prefix.tsForward-path rewrite for strip_prefix.
body-transform.tsRequest-body string/regex replacement for transform.request_body.
grpc-ok.tsListed gRPC statuses that are not proxy errors, including inspect capture.
listen.tsCompare listener binds (host+port; empty host = 127.0.0.1).

domain/http/ ​

Named outbound recipes (not the reverse proxy):

FileRole
recipes.tsWhich recipes a service env references; implicit start dependencies; effectiveStartupDependencies.
json-path.tsOutput extraction from JSON bodies.
jwt.tsJWT expiry for recipe cache (cache.jwt).

domain/llm/ ​

FileRole
llm.tsLlmCall, paging, redactLlmCall, stripLlmBodies.
types.tsSource/driver strings, LlmCall (including optional caller).
match.tsFilter matching.
redact.tsBody/header redaction.
caller.tsNormalize caller names; header / spend-log / completion-body extraction.

domain/traffic/ ​

FileRole
traffic.tsBarrel: TrafficCall, paging, redact, payload builders.
types.tsCall, payload, filter, page types.
match.tsFilter matching including body search.
redact.tsredactTrafficCall / stripTrafficBodies.
payload.tsHTTP/gRPC body views (pretty JSON, gRPC base64 + optional JSON text).
sse-frames.tsBlank-line SSE event split; text/event-stream content-type check.

domain/telemetry/ ​

FileRole
types.tsSpan tree DTOs.
otlp.tsOTLP JSON shapes we accept.
otel-env.tsEnv keys injected for in-process OTLP (OTEL_*).
span-kind.tsSpan kind constants.

domain/net/ ​

Loopback policy used by MCP, web, proxy, and doctor:

FileRole
hosts.tsisLoopbackBindHost, isLoopbackHostname, isLoopbackPeer, formatHostPort.
ports.tsPort holder types for doctor / freePort.
mcp-port.tsDefault MCP port derivation.

Never bind 0.0.0.0 from domain defaults; adapters must refuse non-loopback hosts.

domain/doctor/types.ts ​

Report, DoctorProgress, DoctorRuntimeContext, issue records. Application and TUI import these instead of adapter doctor classes.

domain/session/session.ts ​

PersistedState, PersistedProcess — what state.json contains (PIDs, profile, ports, session id). Recovery logic that inspects processes is an adapter (recover.ts).

domain/ui/preferences.ts ​

TuiConfig, keybinds, MCP tool lists, TuiPreferencePatch. Persistence is the config adapter. dismissed_notifications stores notice ids (for example update:0.10.0).

domain/notifications.ts ​

Operator-facing notices. First kind is update, built from UpdateCheck. Visibility is a receipts policy (dismiss forever vs snooze). Presentation owns the banner; GitHub HTTP stays in adapters/update.

domain/update.ts ​

UpdateCheck and formatUpdateStatus (pure formatting). GitHub HTTP is adapters/update.

When you add a domain type ​

  1. Put the invariant here (validation of values, not of YAML syntax).
  2. YAML syntax and unknown keys stay in adapters/config.
  3. If presentation needs a DTO, prefer domain/status.ts or a focused domain module over inventing a parallel type in the TUI.

Released under the MIT License. Use at your own risk.