ADR-031 — Repo-owned companions for skeleton files
Status: accepted (2026-10-23, spec 086) · Extends: spec 060 (devbox.json merge)
Context
Section titled “Context”/shared:update-service overwrites skeleton files (CI workflow, CLAUDE.md, .claude/settings.json, Dockerfile,
lint config) with the template’s version and merges only devbox.json, where the template wins on shared recipes. Repos
had no supported place to add to these files. An update therefore silently removed repo gates: ai-ops lost its chart-lint gate
when ci was reset (#132). It also removed extra CI jobs, permissions for the repo’s own tools and project guidance in
CLAUDE.md. The report only said “review MODIFIED skeleton files” and did not tell a template change from a lost repo change.
Updates are about to be rolled out to every repo unattended (spec 096), and that needs a reliable “nothing was lost” signal.
Decision
Section titled “Decision”- Skeleton files stay platform-owned and are overwritten. Edits are never merged into them. Every skeleton file
that repos extend has a repo-owned companion that updates never touch:
- gates: the
ci-extrarecipe indevbox.json. The template’scirecipe ends by running it when it exists, and every template’s CI workflow has aci-extracheck (a required check, green when the recipe is absent); - other tools: recipes, packages and env vars of the repo’s own in
devbox.json(spec 060); - permissions: the repo’s own
allow/ask/denyentries in.claude/settings.json, which the update keeps. Entries the platform removed since the repo’s version are removed; - extra CI jobs: workflow files of the repo’s own in
.github/workflows/; - project guidance:
.claude/project-notes.md. It is created empty, imported by the template’sCLAUDE.mdand project-owned (_skip_if_exists). No template defines a companion’s content.shapes.py checkenforces the hooks (thecistep, theci-extrajob, the notes file and its import).
- gates: the
- An update compares against a baseline. It renders the template as it was at the repo’s previous platform version with the repo’s answers. Only a difference between the repo and that baseline is a repo customisation. When no baseline can be rendered, every difference counts. That can over-report losses, but it never misses one.
- The update reports every repo customisation it overwrote in one
lostlist (text and--json). Each entry names the companion the addition belongs in. An emptylostlist means nothing the repo added was lost, and it is the merge signal for unattended fleet updates. - Gates that a repo had chained into the old
cirecipe are moved into a newci-extraonce, automatically, when the repo has none yet.
Consequences
Section titled “Consequences”- A skeleton update can no longer weaken a repo’s gates or break its unattended runs without saying so.
CLAUDE.mdand the CI workflow stay identical across the fleet, so platform changes reach every repo in full.- Customisations with no companion (Dockerfile, the template’s CI jobs, lint config) are still overwritten. They
show up in
lostand belong in the platform, proposed through/shared:report-issue. - Any new skeleton file that repos are expected to extend needs a companion and a
shapes.py checkrule before it ships. - An update renders the template twice when something changed. The baseline needs the platform’s
v<version>tags in the checkout that runscplat.
Not included
Section titled “Not included”- Merging repo edits into skeleton files (a three-way merge like
copier update). - Changing
cplat adopt’s merge of an existingcirecipe (spec 071).