Files changed: - CHANGES.md - VERSION - docs/ownership-and-templates.md - tools/CONTRACT.md
7.8 KiB
Ownership and Templates
Chemenu ships two kinds of files side by side, and at a glance they look the same: both are
plain markdown, both sit in the repo root or under kb/, both get read at session start. But a
stack upgrade treats them completely differently. Some - AGENTS.md,
kb/CONTRACT.md, the per-stage contracts - are identical in every instance
that runs this stack and are the next release's to replace (with one caveat about local edits,
below). Others - USER.md,
SOUL.md, kb/CONVENTIONS.md, ENVIRONMENT.md - describe one particular instance, and
overwriting them would silently erase a choice someone made on purpose.
Two different kinds of truth
The stack-owned files describe how the tool works. kb/CONTRACT.md opens by saying it holds
what tools/wikitool enforces or what follows mechanically from how it operates - see
kb/CONTRACT.md, lines 10-13. That kind of statement doesn't vary by
instance: the compiler behaves the same way regardless of who is running it, so the sentence
describing that behavior can be copied byte-for-byte into every checkout without becoming
wrong anywhere.
The instance-owned files describe a choice: which language pages are written in, what tone the
agent takes, who the operator is, which git remote is authoritative, which MCP servers are
reachable. None of that follows from the tool's mechanics - two instances of the identical
stack can answer all of these differently and both be correct. AGENTS.md § Personalization
frames the split the same way for kb/CONTRACT.md versus kb/CONVENTIONS.md: "the split is by
who may change the sentence, not by what it is about." A rule about page structure could in
principle have been written per-instance too, but then every instance answering "not German" to
setup would be hand-editing a file the stack also ships, and the next dist export merge would
hand the instance's own file back to it, discarding the customization.
Why silent overwrite is the failure being designed against
A stack update is meant to be a routine, low-risk operation: pull the latest release, get
whatever fixes and features shipped since the last one. That only stays low-risk if the update
knows which files it's allowed to touch. If USER.md or kb/CONVENTIONS.md were treated the
same as AGENTS.md - shipped and periodically re-copied - an upgrade would quietly replace a
description of this operator, in this language, with whatever placeholder or default the
stack maintainers wrote. The damage wouldn't be loud: nothing crashes, the files still parse,
the agent just starts acting on the wrong premises until someone notices the voice or the
language changed.
Keeping the boundary at the file level, rather than trying to merge changes within a shared file, means an upgrade never has to guess which lines are "stack" and which are "instance" - the file itself already answers that.
Why the boundary is a predicate rather than a list
For a while the boundary was written down as a list of paths - once in dist_cmd.py, once in
the merge procedure a private instance was told to run by hand, and once in the check that
procedure ended with. Three copies of one fact, which is the shape AGENTS.md
invariant 8 exists to forbid, and they drifted exactly as predicted: the hand-run procedure was
still naming three paths after the collection contracts had moved to the instance's side of the
line, so it discarded upstream changes to files it had never heard of, while its own final check
excluded the same three paths and therefore reported success.
chemenu/ownership.py replaced the lists with one question - is this path, under a content
stage, the stack's or the instance's? - answered by shape rather than by enumeration:
<stage>/CONTRACT.md, and anything ending .template. Both consumers ask it, so dist export
and wikitool upstream merge cannot disagree, and a machinery file added under a content stage
tomorrow is recognised by both without either being edited. The deeper point is not the
deduplication: a list has to be maintained by whoever remembers it exists, and the failure mode
when nobody does is silence, because a path the list has never heard of simply looks like
content.
Why a .template, not just an absent file
The mechanism for instance-owned content is a .template file the distribution ships instead
of the real one - USER.md.template, SOUL.md.template, kb/CONVENTIONS.md.template,
ENVIRONMENT.md.template. An alternative would have been to ship nothing at all and let a
brand-new instance start from a blank page. The template exists because a blank page doesn't
tell instructions/setup-instance.md what shape the answer
should take, and it gives nothing for a validator to check afterward.
A template carries a placeholder value - a sentinel - in the fields that need a real answer.
Setup interviews the operator and replaces the sentinel with what they actually said. That
gives doctor a mechanical way to tell "personalized" from "not yet": a file that still
contains the sentinel hasn't been through setup, regardless of whether the file exists. That's
also why ENVIRONMENT.md only warrants a WARN rather than a FAIL when absent - see
AGENTS.md § Environment - while a missing or unfilled
USER.md/SOUL.md/kb/CONVENTIONS.md is a harder failure: ENVIRONMENT.md describes one
checkout among possibly several and is gitignored for that reason, so its absence is a normal
state rather than a sign setup was skipped.
The consequence in practice
An upgrade sorts every shipped path into three categories, not two - and the third one only becomes visible once an upgrade is a command rather than a hand-run copy:
- Verbatim files -
AGENTS.md,kb/CONTRACT.md, the per-stage contracts, everything undertools/,types/andinstructions/- are the release's to replace. .template-sourced files -USER.md,SOUL.md,kb/CONVENTIONS.md, eachkb/<name>/COLLECTION.md,ENVIRONMENT.md, theroot: kbtype-specs - are never written by an upgrade at all. The distribution ships only the.templatebeside them, so the filled file is out of reach by construction rather than by a rule someone has to remember.- Seeded-once files -
.wikitool-kb.json,CHANGES.md,kb/log.md,raw/*/.gitkeep- are written into a new instance bydist exportand belong to the instance from then on. They are the awkward category: they sit in the release stamp's file list like any other shipped file, so an upgrade has to exclude them deliberately (chemenu.ownership.is_export_stubandis_upgrade_preserved). An upgrade that re-seeded them would reset the record of which migrations ran, or erase the changelog the instance wrote for itself.
The first category carries a caveat that the word "verbatim" hides. It says who decides the content, not that overwriting is always safe: an instance can still have edited a verbatim file
- a patched
tools/, a locally adjusted instruction - and an upgrade assuming otherwise would destroy that silently. Avoiding that assumption is the whole reasondist exportrecords a sha256 per shipped file in.wikitool-release.json.wikitool dist upgradecompares every candidate path against the digest recorded when it was installed, overwrites only what still matches, and refuses rather than overwrite what does not.
So the practical rule is narrower than "overwrite the verbatim files, leave the rest alone": overwrite the verbatim files this instance has not touched, never write the other two categories, and make a locally changed file a decision someone takes deliberately instead of one an upgrade takes for them. The template-sourced files were filled in once, by a person, for a reason, and nothing about a newer release of the stack's mechanics gives it standing to override that.