Skip to content

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:

git fetch origin && git switch -c fix/<slug> 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 --hard a tree you did not create. Move a ref with git update-ref or git 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 88008899: 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.