Inheritance via extends
A child type: app service inherits all fields from the named parent. The child then overrides only the fields it declares. Multi-level chains are supported and resolved in topological order — a grandchild gets the parent’s defaults indirectly via its direct parent.
Contents
Section titled “Contents”App-only guard
Section titled “App-only guard”extends: is rejected at load on any type: tool or type: infra entry. The discriminator is enforced before merge so cross-type chains never resolve.
flowchart LR A[main<br/>defaults: dir, dirs, cli] --> B[main-debug<br/>extends: main] A --> C[main-stage<br/>extends: main] C --> D[main-stage-debug<br/>extends: main-stage]
Resolution rules
Section titled “Resolution rules”- Scalar fields (
dir,dir_internal,work_dir_internal,cli.mode,cli.shell,cli.user,cli.workdir) — child wins when set, parent fills in only when child’s value is empty.typeis required in everyservice.ymland is never inherited. dirs— parent’s list comes first; child entries are appended; duplicates are removed (parent order preserved).configs— child wholly replaces parent when set (child has its own list); parent’s list is used only when child omits the key.cli.env— recursive map merge: parent provides defaults, child overrides per key.render.ide.enabled,render.ide.template,render.ai.enabled,render.ai.template,render.git.enabled, andrender.git.template— inherited like scalar fields. Child’s explicitenabled: true|falseor non-emptytemplateoverride the parent’s; omitted values inherit from parent. This allows grandchildren to inherit settings indirectly.render.config— inherited wholesale when the child declares norender.config:block of its own (parent’s block is cloned); a child that declares its ownrender.config:keeps it and does not merge with the parent’s.generated— inherited when the child declares nogenerated:map of its own (parent’s map is cloned); a child with its owngenerated:map wholly replaces the parent’s, not merged.compose— inherited when the child declares nocompose:list of its own (parent’s list is cloned); the child’s own list wholly replaces the parent’s, not merged.container,required,depends_on— never inherited. A child that omitscontainerdefaults to its service folder name at load time. Each child specifies its owndepends_on.
Topological sort
Section titled “Topological sort”The extends: graph is resolved in topological order, so multi-level chains (C → B → A) merge correctly regardless of map iteration order. Cycles and unknown parents are reported as load errors. For each child, only zero-value fields are inherited from the parent; child fields take precedence on conflicts. Inherited slices and maps are copied defensively, so mutating a child never corrupts the parent.
Worked example
Section titled “Worked example”type: appcontainer: app-mainrequired: truedir: ./services/maindirs: [logs, home, runtime]cli: shell: bash user: www-datarender: ide: enabled: truetype: appextends: main # inherits dir, dirs, cli, render, etc.container: app-main-debugrequired: falsecompose: - compose/services/main/debug.ymlcli: env: - XDEBUG_CONFIG="cli_color=1"render: ide: template: main-debug # override template, keep enabled: true from parentmain-debug gets dir, dirs, base cli, and render.ide.enabled: true from main. It overrides render.ide.template to use a custom template subdirectory (workspace/templates/ide/main-debug/), and adds its own compose overlay and extra env. When dwe render ide runs, both services share dir: ./services/main, so the most-derived (main-debug) wins and renders its custom template; main is skipped with a collision warning.