The working regime¶
This page owns the rules every change obeys on its way into main: how a branch is cut, what a
commit says, when a merge request may merge, what runs locally and what runs on GitLab, what a
change must be tested with, and who may cut a release tag. Other pages state their own subject and
link here for the rule; Add a feature owns the recipe and
Run the tests owns the suites.
Who this binds¶
Every repository an agent commits to: Nerthus.*, and the corpora an agent writes
(repozytorium-*).
Narrators writing lore from their own devices are exempt: they read no CLAUDE.md.
A tool that writes a repository writes on a namespace of its own and nobody writes one by hand:
apply/<utc>, import/<utc> and drift/<utc> in Nerthus.Infra, and the machine namespaces below.
An agent merges its own work¶
No project carries an approval rule and none is added. The pipeline is the reviewer of record, and an agent may merge on green and cut a release tag on green.
That standing ends the day a production site exists. A site declared with env: prod in
Nerthus.Infra's inventory is the evidence. On that day self-merge and agent tag-cutting stop, and an
operator merges anything a production site reads.
Git¶
One logical change per commit. The subject line is <type>[(<scope>)]: <summary>, within 72
characters, and the types are feat fix docs test refactor chore ci merge. A programme task adds
its Task: trailer; Read the programmes' history owns that grammar.
Cut every branch from freshly fetched origin/main:
Never git switch main followed by git switch -c. A worktree holding main makes the first
command fail, and the second then cuts from whatever HEAD was; a branch cut from a stale main
reverts what landed meanwhile.
Branch names are proj/<n>-<slug>, fix/<slug>, feat/<slug>, chores/<slug>, or ci/<slug>.
The machine namespaces — vm/<utc-stamp>, lang/<id>, deploy/<shortsha> — belong to machines.
Two further rules:
- Never
git reset --harda tree you did not create. Move a ref withgit update-reforgit reset --soft. - Delete no unmerged branch without a tag. Tag the tip
archive/<date>/<slug>, read it back, then delete.archive/is a protected tag namespace.
main is protected and refuses a force-push. A runbook that must unprotect it says so and
re-protects it in the same session.
A branch, and when it dies¶
Done means merged to main with a green pipeline. A pushed branch is not done, and neither is
a passing local run.
A long-lived project branch is legitimate. It may be red: intermediate pushes are allowed to fail, and the gate sits at the merge.
A branch with no activity for seven days is considered dead. Activity is the latest of a commit, a push, a pipeline run, or a merge-request comment. Override it by saying so in the branch's most recent commit message or in the merge-request description — an override is a sentence a person wrote. A dead branch is flagged and kept; deleting it is a person's decision.
A project ledger records the merge commit, not the branch name. A row naming a branch names something that can still vanish.
Merging¶
Every change to main arrives as a merge request, including a one-line documentation fix.
Merge commits, never a squash and never a rebase-merge. Every project is set to merge with squash
off, and the source branch is deleted on merge.
Green has a definition. A merge request may merge when:
| failed jobs | none |
allow_failure warnings |
tolerated — those checks are advisory by declaration |
| unplayed manual jobs | fine: a manual job is an opt-in action |
| a skipped pipeline | never merges |
only_allow_merge_if_pipeline_succeeds and allow_merge_on_skipped_pipeline carry that on the
GitLab side. Neither is turned off, and no API call merges around them.
A project spanning repositories lands all-or-nothing, in dependency order: the contract or data store, then the code, then the documentation. No half merges until every half is green. Paired branches share their name exactly.
Read the diff stat before merging. Red never merges, and never merges to be fixed on main: a red
baseline hides every failure after it.
Base freshness is a question about the diff, not the tip. A branch that is behind
origin/main at merge time needs origin/main merged in and a fresh pipeline only when a commit it
lacks touches a path the branch touches, or a test ledger, a CI file (.gitlab-ci.yml, ci/) or
a shared fixture (tests/fixtures/); the check is
git fetch origin
comm -12 <(git diff --name-only origin/main...HEAD | sort) <(git diff --name-only HEAD...origin/main | sort)
and an empty intersection with none of those paths on the right-hand side merges as it stands.
One merge request per repository per project. A project is several logical changes and several
commits; it is not several pipelines. Land them as one merge request per repository, in the
all-or-nothing order below, and open it with auto-merge on green
(merge_when_pipeline_succeeds=true on the API, sha pinned) unless it must wait for a sibling in
another repository — then merge it by hand the moment the sibling lands. A merge request green and
unmerged for an hour is an agent's idle time, not the runner's.
Local, and GitLab¶
Write code, tests and documentation locally. Before every push, the formatters, the linters, the repository's gate scripts and the focused tests for the change run locally and pass: CI confirms, it does not discover. A task branch runs no branch pipeline; its merge request's pipeline is its one lane.
Decrypting a secret locally for a task that needs it is expected; printing one is not. Prove a token works by using it. Writing to a lore repository from a device is expected too, because that is how narrators work.
Three things happen only on GitLab:
| Why | |
|---|---|
| the full test suite | slow, and CI runs it on every merge request |
| the image build and the registry push | not even to a scratch tag |
| everything scheduled or published | the credentials and the runners live there |
A local green is a hypothesis. Some checks need GitLab's credentials or runners and cannot run locally at all, so a change verified only locally is not verified.
GitLab work goes through glab. glab mr create --fill still prompts and times out without a
terminal, so open a merge request with glab api --method POST; a sandboxed shell resets the TLS
connection to gitlab.com, which is not the API being down.
Parallel agents take a worktree each and a port each. The checkouts share one HEAD, and two
servers on one machine collide on a port. The range is 8800–8899: agent slot k binds the API
on 8800 + 10k and its Postgres on 8802 + 10k. A worktree exports the sibling checkout paths it
needs explicitly, because it sits a level away from them.
What a change is tested with¶
A route carries a mandatory floor: a service test, a contract-layer test over real HTTP, a CLI test over the same service function, and its row in the generated reference. A fix carries red-then-green proof in the merge request: the test fails at the parent commit and passes at the tip. Everything else is the author's judgement.
Every new test states what would make it pass while being wrong. The defects that matter are checkers with the wrong frame of reference rather than wrong answers. The shapes to look for:
- a guard with no caller is not a check;
- "nothing moved" is unfalsifiable without a positive control;
- assert each population, never only the sum;
- a validator that never sees the request cannot tell two requests apart;
- an optional argument that disables a check disables it;
- a scan's grammar must match its subject's grammar;
- enumerate the universe, not a named list;
- a stamp read from another tree looks verified and is wrong.
Fixtures are synthetic and disclosed, and no real key or token appears in one. Unit tests read the public-domain Trylogia fixture corpus.
Releases¶
An agent may cut a release on a green main. A release is a vX.Y.Z tag, and semver follows
contract impact: major when a consumer must change, minor for a new capability, patch for a fix.
Nerthus.Platform versions its package, image and contracts together. An image is pinned only when
a release tag built it, and the release merge request is the one write path to versions.lock;
Cut a Platform release is the procedure. Nerthus.Config
releases nerthus-config the same way, on its own repository. Before a release that pushes, prove
the credential it pushes with by using it, never by reading its expiry.
How this page reaches an agent¶
Each repository's CLAUDE.md carries a short digest of these rules and a link here. A CI job fails
when a digest and this page disagree, so the two cannot drift.
The digest comparison lives in this repository as ci/assert-regime-digest.py, and every other
repository runs it by cloning this one inside the job, so the check cannot ship stale; this
repository runs it against its own CLAUDE.md to keep the canonical copy from being emptied. Commit
grammar, the Task: trailer and branch naming are ci/gates.py in Nerthus.Platform and
Nerthus.Config, and nerthus-config gates regime in Nerthus.Infra; each judges a merge request's own
commits and never history.
No check enforces base freshness or the pairing of branches across repositories; review does.