analysis: add 16 evidence-backed drift findings

Twelve drift findings and four works-well findings, each citing file
paths, line numbers, commit hashes or rows in analysis/data/. Every
'neckbeard mechanism' field names a concrete rule in neckbeard v0.1.1 or
states explicitly that none exists.
This commit is contained in:
Thore Cimbal
2026-08-10 12:00:00 +00:00
parent 959bf882e9
commit eaa00f0ed2
16 changed files with 739 additions and 0 deletions
@@ -0,0 +1,47 @@
# F-001: Milestone M5 was decided and executed, but the steering docs still call it an open question
category: claim-vs-reality
confidence: high
evidence:
- `roadmap.md:6-14` (last commit 2026-08-09, `git log -1 main -- roadmap.md`) states
the milestone set as "M1M4", gives the distribution "M1 33 · M2 21 · M3 4 · M4 12
von 70" under "Stand 2026-08-06", and frames the Härtung milestone as an explicitly
open question: "Ob das ein eigener Meilenstein „Härtung" werden soll oder M1 bewusst
breit bleibt, ist **offen** — zu entscheiden im Refinement, nicht nebenbei."
- `CLAUDE.md:98` (last commit 2026-08-09) likewise binds every issue to "Gruppen-Milestones
M1M4".
- Measured against the live tracker (`analysis/data/gitlab_issues.json`, 71 open issues):
M1 19 · M2 22 · M3 4 · M4 12 · **M5 — Härtung 14**. The milestone exists and is populated.
- All 14 M5 issues live in `axion1337.chat-gitops` and carry `updated_at` 2026-08-09.
- `verfahren/retro/2026-08-09.md:115-117` records the decision: "**M5 — Härtung** angelegt,
14 Issues aus M1 verschoben", with the dividing line and the resulting distribution
"M1 18 · M2 21 · M3 4 · M4 13 · M5 14".
- No ADR covers it: `decisions/` holds 0001-0009, none mentions M5 or Härtung
(`grep -rn 'M5\|Härtung' decisions/` -> no match).
- The retro's own numbers are already stale on the day they were written: retro says
M1 18 / M2 21 / M4 13 / 70 total, the tracker says M1 19 / M2 22 / M4 12 / 71 total.
impact: The two documents a new session is told to read first (`CLAUDE.md`, then
`roadmap.md`) describe a milestone set that no longer exists, and pose as undecided a
question that was decided. An agent following `CLAUDE.md:98` would file a hardening issue
into M1, silently undoing the split. The decision itself survives only as a bullet in a
dated retro file — the one place the repo's own rules do not point sessions to.
`CLAUDE.md:79-82` makes an ADR mandatory for process decisions and calls documenting an
exception instead of deciding it "ein Fehler"; by the repo's own standard this decision is
under-recorded.
root-cause pattern: **Decision executed in the tool, documentation not carried along.**
The change was made where the work is (GitLab milestones), noted where the session ended
(the retro), and never propagated to the documents that claim to be canonical.
neckbeard mechanism: Partly prevented, partly a gap.
Prevented: ADR-0002 (`docs/adr/0002-in-repo-issues.md`, Decision) puts issues in the repo
as `docs/issues/NNNN-slug.md`, so re-bucketing 14 issues *is* a commit in the same tree as
`roadmap.md` — a reviewer sees the docs that were not updated alongside it. `AGENTS.md:79`
plus `scripts/gen_status.py` make `STATUS.md` a generated index ("do not edit by hand"), so
counts are never hand-copied prose that can rot. `WORKFLOW.md:128` puts "STATUS review:
anything stale or surprising" on the refinement agenda.
**Gap in neckbeard:** it has no milestone or roadmap concept at all — no artifact type in
`schema.yaml` groups issues into a delivery bucket, and nothing would have flagged the
M1-vs-M5 split as needing an ADR. Session 2 must decide where "worauf zahlt es ein" lives.
@@ -0,0 +1,56 @@
# F-002: The timestamp anonymisation rule reaches one of six repos; 237 commits still publish real working hours
category: claim-vs-reality
confidence: high
evidence:
- `CLAUDE.md:116-134` declares the rule for "**alle** Repos der Gruppe `axion1337.chat`
und die ThreadNet-Dienste": author *and* committer date set to 12:00:00 UTC, with the
stated purpose "damit sich aus der Historie keine persönlichen Arbeitszeiten ablesen
lassen".
- Measured over every ref of all six repos (`analysis/data/timestamp_anonymisation.tsv`,
produced by `analysis/scripts/inv_timestamps.py`, author time rendered in UTC):
| repo | own/agent commits with real clock time |
|---|---|
| management | 0 |
| axion1337.chat-gitops | 187 |
| threadnet-operating | 21 |
| thread-net-git | 11 |
| threadnet-call | 14 (all on `origin/livekit`) |
| ThreadNet-Web | 4 |
Total 237. Upstream Element/Matrix commits are excluded from that count — they were
never this project's to rewrite.
- `thread-net-git` and `threadnet-operating` were not rewritten at all: they hold **no**
12:00:00 commits and appear in no section of `shared/commit-zuordnung-2026-08-07.md`
(sections cover gitops, management, ThreadNet-Web, threadnet-call only).
- The exposed times are exactly the ones the rule exists to hide. On
`axion1337.chat-gitops`'s `turn-secret-rotation-20260728-192656` branch alone, the
hour-of-day distribution of the 41 commits is 00h ×1, 01h ×6, 21h ×8, 22h ×1, 23h ×6.
- All of this is publicly readable: every one of the six repos mirrors to
`rohana.axion1337.de`, verified in sync by `git ls-remote` (see F-013).
- `CLAUDE.md:143-147` does carry a caveat, but it names push times, issue and comment
timestamps, pipeline runs and package publications — **not** the git history itself.
`shared/commit-zuordnung-2026-08-07.md:41-44` discloses the untouched pre-boundary
commits, but frames it as an SHA matter ("haben ihre SHA behalten"), not as a
147-commit hole in the privacy goal.
impact: The rule's stated goal is not achieved for five of six repos, while both the rule
and its caveat read as though the remaining exposure were limited to non-git surfaces.
Anyone reading `CLAUDE.md` would reasonably conclude the git history is clean. Effort was
spent (a 251-commit rewrite, three restarted release pipelines, a mapping document) for a
result that a `git log` on any side branch undoes.
root-cause pattern: **Rule declared repo-wide, applied to a subset — and the gap between
the two is documented in a third place, in different terms.**
neckbeard mechanism: **Gap in neckbeard.** Nothing in `AGENTS.md`, `WORKFLOW.md`,
`schema.yaml` or `scripts/validate.py` concerns commit hygiene, author identity or history
rewriting; `docs/aar/2026-08-09-neckbeard-v1-creation.md` ("Git is the changelog") treats
git as a given, not as an artifact under governance. The nearest applicable rule is
`AGENTS.md:50-55` ("Never claim something works without evidence... Uncertainty is
reported, never swallowed"), which is a behavioural rule for an agent, not a check on the
repo. A field test finding worth carrying back: a framework that governs documents while
the project's most sensitive claims are about its *git history* leaves that surface
unguarded.
@@ -0,0 +1,34 @@
# F-003: Five author identities of one person remain, one with a malformed e-mail
category: claim-vs-reality
confidence: high
evidence:
- `shared/commit-zuordnung-2026-08-07.md:19-22` states that the rewrite unified author
identity: "bei einigen Commits zusätzlich die Autoren-Identität (drei Varianten
derselben Person auf eine vereinheitlicht)".
- Author fields still present across the six repos
(`analysis/data/timestamp_anonymisation.tsv`, column `author`):
`Thore Cimbal <cfx@riot.8shield.net>`, `Scrublord MacBad <Scrublord@Mac.Bad>`,
`ScrublordMcBad <gamemaster@axion1337.de>`, `sorb <gamemaster@axion1337.de>`,
`sorB <cfxqriot.8shield.net>`.
- That is five variants, not the three the document says were collapsed into one.
- `sorB <cfxqriot.8shield.net>` (2 commits on `ThreadNet-Web` main) is not a valid e-mail
address — the `@` is missing, `cfx@riot` became `cfxqriot`.
- The unification reached only the rewritten commits: `management` main shows a single
identity, while `axion1337.chat-gitops` main still carries 135 commits as
`Scrublord MacBad <Scrublord@Mac.Bad>` and 10 as `ScrublordMcBad`.
impact: Same exposure class as F-002 and the same false sense of completion. In addition,
the malformed address makes those two commits unattributable by tooling that matches on
e-mail, and `Scrublord@Mac.Bad` leaks a machine name. Contributor statistics on the public
Gitea mirror show one person as four or five contributors.
root-cause pattern: **Rule declared repo-wide, applied to a subset** — the same pattern as
F-002, in the same operation. Both were verified for the property that was easy to check
(SHA pairs matched) and not for the property the operation existed to achieve.
neckbeard mechanism: **Gap in neckbeard**, as in F-002 — no commit or identity hygiene
exists in the framework. The closest structural analogue is `scripts/validate.py`, which
enforces that declared metadata matches reality for *documents* (`check_fields`,
`apply_rules`); nothing does the equivalent for commit metadata.
@@ -0,0 +1,46 @@
# F-004: Open work items live only in host markdown, against the repo's own "no backlog markdown" rule
category: process-drift
confidence: high
evidence:
- The rule, twice stated: `CLAUDE.md:72-75` — "**Alles Offene ist ein Issue** … Kein neues
Backlog-Markdown anlegen; `hosts/`/`shared/` sind nur Bestand + Historie" — and
`README.md:47` — "Alle offenen Punkte sind **Issues in diesem Projekt**".
- Open next steps that exist only as prose in `hosts/` and `shared/`:
- `hosts/overmind.md:90` — "**Nächster Schritt:** `element-desktop-build` von rohana in
die Lab-Registry umziehen … bewusst zurückgestellt" (section OVERMIND-01, marked
"Status: erledigt").
- `hosts/overmind.md:111-115` — "**Offen:** … Falls der Hang trotz EEE-off + neuer
Firmware wiederkehrt: gezielter ASPM-Fix statt globalem Kernel-Parameter".
- `hosts/cfgmon.md:148` — "**Nächster Schritt:** die drei manuellen Schritte oben, dann
→ erledigt" (section CFGMON-11).
- `shared/branding.md:210` — a whole `## Offen` section.
- Section IDs in `hosts/`/`shared/` with **no** corresponding issue anywhere in the group
(checked against all 111 issues in `analysis/data/gitlab_issues.json`):
`OVERMIND-01`, `CFGMON-11`, `CFGMON-12`, `CFGMON-13`, `MATRIX-05`.
For contrast, `OVERMIND-02` -> management#4 and `LABNET-01` -> management#11 do exist.
- `hosts/overmind.md:146` even routes readers to a markdown-only item as if it were
tracked: "CFGMON-11 (Gitea-CI-Rückbau)".
impact: An item like the `element-desktop-build` migration is invisible to the group board,
carries no milestone, no priority and no owner, and appears in no refinement. It is
deferred with a reason — a good reason — inside a file the process declares to be history.
`README.md:74` names the distinction between "gemacht" and "bewusst gelassen" as the most
frequent question; for these five IDs the answer exists in exactly one place and it is not
the one the process points at.
root-cause pattern: **Two backlogs, one rule.** The rule says one system; the host
documents are the natural place to write while working on a host, so items land there and
the migration to an issue is a separate act of discipline that sometimes does not happen.
neckbeard mechanism: Directly prevented by ADR-0002
(`docs/adr/0002-in-repo-issues.md`, Decision: "In-repo Markdown issues
(`docs/issues/NNNN-slug.md`, status in frontmatter, generated `STATUS.md` as index)").
Under it there is no gap between "writing in the doc" and "filing an issue" — both are
markdown files in the same tree, and `scripts/validate.py` enforces that an issue has a
`status` from `schema.yaml`'s enum `[open, in-progress, done, rejected]` (`check_fields`,
`schema.yaml:76-84`), so an open item cannot exist as a bold prose label. `scripts/gen_status.py`
then lists it in `STATUS.md` whether or not anyone remembered it.
Residual gap: nothing in neckbeard would stop a wiki page under `docs/wiki/` from carrying
an "Offen" section; the guard is `WORKFLOW.md:126` wiki lint, a human ritual.
@@ -0,0 +1,44 @@
# F-005: A live document routes the reader to the Gitea tracker that three documents declare dead
category: claim-vs-reality
confidence: high
evidence:
- The claim, stated three times and each time absolutely:
- `README.md:22-27` — "**Keine Ausnahmen mehr.** … der Gitea-Tracker ist leer …
**Alle Issues leben auf git.lab.**"
- `hosts/overmind.md:38-42` — "**Issues nicht mehr** … der Tracker ist leer.
**Ohne Ausnahme: Issues leben auf git.lab.**"
- `CLAUDE.md:49` — "**Issues leben auf git.lab.** Die alten Gitea-Issues sind
geschlossen und verweisen dorthin."
- `hosts/cfgmon.md:176` (section CFGMON-12) resolves its status against that tracker:
"**Status:** abgelöst durch [gitops#48](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/48)
(2026-08-01, sorb: HOHE Priorität …) — Plan-Skizze und die offene
Erreichbarkeits-Entscheidung git.lab-only vs. extern stehen dort".
- So the substance of an open decision is said to live in an issue on the tracker that is
declared empty. The link is the only pointer to it in the repo.
- The number is additionally unsafe: `CLAUDE.md:50-52` warns that gitops issue numbers
shifted in the migration ("z. B. Gitea#48 → GitLab#46") and that "alte „gitops#N"-Verweise
meinen die Gitea-Nummer". A reader who resolves `gitops#48` on git.lab lands on a
different issue than the one meant — and `analysis/data/links.tsv` records
`axion1337.chat-gitops#48` as an existing GitLab issue, so nothing looks broken.
- CFGMON-12 has no issue of its own anywhere in the group (F-004).
impact: A high-priority decision ("HOHE Priorität", plus an open reachability question) is
reachable only through a link into a system the documentation says is retired, under a
number the documentation says is ambiguous. Both failure modes are silent: the link
resolves, and so does the wrong git.lab issue.
root-cause pattern: **Decision executed in the tool, documentation not carried along**
the migration was completed and announced, but the documents that pointed into the old
system were not swept. Compounded by **two backlogs, one rule** (F-004).
neckbeard mechanism: Removed by construction under ADR-0002
(`docs/adr/0002-in-repo-issues.md`): with issues as files in the repo, a reference to an
issue is a relative link, and `scripts/validate.py`'s `check_body_links` reports
"inline link target missing" when it does not resolve — a renumbering or a retired tracker
becomes a failing check instead of a working link to the wrong thing. ADR-0002 explicitly
lists this as the con of forge issues: "issue ↔ artifact links leave the repo".
**Gap in neckbeard:** `check_body_links` only validates repo-relative targets; external
URLs are format-checked at most (`schema.yaml:22-26` applies that only to frontmatter link
fields). A doc pointing at a dead external tracker stays green.
@@ -0,0 +1,43 @@
# F-006: Three abandoned branches keep publishing exactly what the rewrite removed from main
category: dead-artifact
confidence: high
evidence:
- `axion1337.chat-gitops` `origin/turn-secret-rotation-20260728-192656`: 41 commits not on
main, head `f30d2f5` dated 2026-07-28, **not merged** (`git merge-base --is-ancestor` ->
no). All 41 carry real clock times (00h01h and 21h23h heavy). They are the *pre-rewrite*
versions of commits the mapping table lists as replaced — 42 mapping rows resolve as
`ok-both-present` in `analysis/data/sha_refs.tsv`, meaning the old SHA is still reachable.
- The same branch exists on the public mirror:
`git ls-remote https://rohana.axion1337.de/sorb/axion1337.chat-gitops.git` ->
`f30d2f582ba14d85a7dec97c4db9bb2677ca824d refs/heads/turn-secret-rotation-20260728-192656`,
identical to git.lab.
- `threadnet-call` `origin/livekit`: 14 own commits with real clock times, head `c12a4116`
(2026-07-31 22:22), mirrored publicly with the same head.
- `axion1337.chat-gitops` `origin/wiki`: head `0ff598e`, 2026-05-14. `CLAUDE.md:63` already
describes it as "ein überholter Mai-Abzug von `docs/`", and management#19 ("DOC-02:
Veralteten `wiki`-Branch im gitops-Repo entfernen?") tracks it — open, `priority:low`,
no `status:` label, i.e. plain backlog.
- The rotation branch's own successor mechanism is live: the scheduled CI job
`canonize_rotation` exists at `.gitlab-ci.yml:51` in the gitops repo, as `CLAUDE.md:56-60`
describes. The branch predates it and was never cleaned up.
impact: The single most expensive operation in this repo's history — a 251-commit rewrite
that restarted three release pipelines and needed its own mapping document — is undone for
42 of those commits by a branch nobody looks at, on a host that is public. The `wiki` branch
additionally offers a three-month-old copy of the documentation next to the current one,
which is the "zwei Wahrheiten über dieselbe Sache" failure `CLAUDE.md:94-97` names as worse
than an incomplete one.
root-cause pattern: **Artifacts outlive their purpose without an owner.** All three branches
served a finished purpose; none had a defined end. The one that is noticed (`wiki`) sits in
the backlog without a status label, which is where low-priority hygiene goes to rest.
neckbeard mechanism: **Gap in neckbeard.** The framework governs files inside a checkout,
not refs: nothing in `AGENTS.md`, `WORKFLOW.md` or `scripts/validate.py` sees a branch.
The nearest existing mechanisms are `WORKFLOW.md:122` (refinement item 2, "Backlog triage
over `docs/issues/`") and `WORKFLOW.md:126` (wiki lint, which hunts "orphan pages") — both
would need a branch equivalent to catch this. `schema.yaml`'s `warn_if_orphan` rule shows
the framework already accepts "reachable from nowhere" as a reportable condition for
documents; the field test shows refs need the same treatment.
@@ -0,0 +1,47 @@
# F-007: How many repos are mirrored has three different answers, and one of them is wrong
category: claim-vs-reality
confidence: high
evidence:
- `CLAUDE.md:36-37` — "**Gespiegelt wird nur die Gruppe `axion1337.chat`** (die **fünf**
Produkt-Repos und `management`)" — i.e. six repos.
- `hosts/overmind.md:27-31` — "Stand 2026-08-09 **sieben**: die **sechs** Produkt-Repos
(ThreadNet-Web, threadnet-call, thread-net-git, threadnet-operating,
axion1337.chat-gitops, seit heute auch `game-operating`) **und `management`**".
- Both files were last committed on the same day, 2026-08-09
(`git log -1 main -- <file>`), so this is not a lag between an old and a new statement;
the repo asserts five and six product repos simultaneously.
- Measured (`git ls-remote` against both hosts, heads of `main`):
- management, axion1337.chat-gitops, ThreadNet-Web, threadnet-call, thread-net-git,
threadnet-operating -> **all six in sync**, identical SHAs.
- `game-operating` -> git.lab `ead1272823`, `rohana.axion1337.de/sorb/game-operating`
-> **no repository**. The "seit heute auch `game-operating`" claim does not hold.
- `gameserver` -> git.lab `d5c6ccb2e9`, Gitea `48441a500f`, **diverged** — which is
exactly what `hosts/overmind.md:33-35` says and management#32 tracks.
- Caveat on the negative result: the mirror naming convention is `sorb/<name>` for all
six verified repos; a `game-operating` mirror under a different name would not have
been found. Confidence high on the contradiction, medium on the specific cause.
impact: The mirror set is the answer to "what can the Hetzner cluster still reach when the
lab is down" — the reason the two-host topology exists at all (`CLAUDE.md:26-33`). A repo
believed mirrored but not mirrored is a gap in exactly that guarantee, and the belief is
recorded in the file that is meant to be authoritative for host questions.
management#28 ("MIRROR-01: Ein Ausfall der Push-Mirrors bleibt unbemerkt") shows the risk
class is known; it sits in the backlog at `priority:low` with no status label.
root-cause pattern: **Decision executed in the tool, documentation not carried along**
here in the inverse direction: the document was updated ahead of the tool, and the number
in the *other* document was not touched at all.
neckbeard mechanism: `WORKFLOW.md:136-138` (Knowledge Handling) is the governing rule:
"Contradictions are resolved or explicitly flagged — never left silently coexisting", with
the detection ritual at `WORKFLOW.md:126` (refinement item 4: "Wiki lint (content-level,
beyond `validate.py`): contradictions between pages, claims superseded by newer sources").
`AGENTS.md:50-52` adds "Never claim something works without evidence".
**Partial gap:** the rule exists, the detection is a human ritual on a weekly cadence, and
`scripts/validate.py` checks structure and link targets only — two numbers contradicting
each other across two files is invisible to it. `AGENTS.md:101-103` ("If a deterministic job
lacks a script, propose one") points the right way, but "is this claim still true" is not
deterministic in general. The tractable subset — a claim of the form "N repos are mirrored"
— is checkable, and this field test wrote such a check ad hoc.
@@ -0,0 +1,42 @@
# F-008: The five components have four naming schemes, and one display name maps to a repo that shares no name with it
category: structural-drift
confidence: high
evidence:
- Slugs verified against git.lab with `git ls-remote` (see `analysis/SCOPE.md` §3):
`threadnet-call`, `thread-net-git`, `threadnet-operating`, `ThreadNet-Web`,
`axion1337.chat-gitops`.
- `thread-net-git` breaks the `threadnet-*` pattern of its siblings;
`https://git.lab/axion1337.chat/threadnet-git.git` returns "project could not be found".
- `ThreadNet-Web` is the only CamelCase slug. `ThreadNet-Web` and `threadnet-web` return
byte-identical ref lists (same project via GitLab's case-insensitive path handling), and
the repo's own documents use both plus a lowercase registry path
`registry.git.lab/axion1337.chat/threadnet-web/desktop-build` (`hosts/overmind.md:54`).
`hosts/matrix.md:20` uses the Gitea spelling `sorb/ThreadNet-Web`.
- "ThreadNet Server Suite" = `axion1337.chat-gitops` rests on a single sentence,
`CLAUDE.md:6`. The slug contains no ThreadNet element; nothing else in the repo repeats
the equation. This session had to ask the human to confirm it (Phase-0 STOP, question 3).
- `vision/threadnet.md:9-11` lists the product line as "ThreadNet-Web …, threadnet-call
…, thread-net-git, threadnet-operating" — four names in three casings, and the fifth
component is absent from the vision that defines the line.
impact: Nothing in the repo lets a reader or an agent get from a display name to a
repository deterministically. This session could not, and the Phase-0 scope freeze existed
partly to resolve it. Every automation that iterates over "the ThreadNet repos" needs a
hand-maintained table, and the case-insensitive redirect hides the inconsistency until
something case-sensitive (a container path, a CI cache key, a `git clone` into a
case-sensitive filesystem) meets it.
root-cause pattern: **Rule declared repo-wide, applied to a subset** — the branding
decision in `shared/branding.md` fixes what the *product* is called on every surface, and
was carried through thoroughly there, but repository names were never part of that sweep.
neckbeard mechanism: **Gap in neckbeard**, with a near-miss worth noting. `schema.yaml`
enforces naming as a first-class rule for artifacts — `filename: "^\\d{4}-[a-z0-9-]+\\.md$"`
for ADRs and issues, `"^[a-z0-9-]+\\.md$"` for wiki pages — and `scripts/validate.py` reports
"filename does not match" plus "id does not match filename prefix". The framework therefore
already holds the position that names must be mechanically derivable. It applies that only
inside one repo; a multi-repo project has no equivalent, and neckbeard has no artifact type
that declares "these are the components and this is what they are called". Session 2 needs
one; today `analysis/SCOPE.md` §3 is the only such table that exists.
@@ -0,0 +1,47 @@
# F-009: Documentation attention and actual work mass point at different components
category: structural-drift
confidence: medium
evidence:
- Open issues per component (`analysis/data/gitlab_issues.json`):
`axion1337.chat-gitops` 35 · `management` 26 · `ThreadNet-Web` 9 · `threadnet-call` 1 ·
`thread-net-git` 0 · `threadnet-operating` 0.
- Commit volume over all refs (`analysis/data/git_activity.tsv`):
`axion1337.chat-gitops` 269 · `ThreadNet-Web` 52 · `threadnet-operating` 21 ·
`threadnet-call` 12 own · `thread-net-git` 11.
- Repository substance (`analysis/data/tree_*.txt`): `thread-net-git` holds **4 tracked
files** (`.gitignore`, `README.md`, `docker-compose.yml`, one file under `backup/`) and
one markdown document; last commit 2026-08-02, 7 days before the reference date.
`threadnet-operating` holds 27 files, 2 documents.
- Against that, `vision/threadnet.md:9-11` presents all four ThreadNet repos as equal
members of the product line, and `roadmap.md:50-62` gives the ThreadNet line six numbered
items, five of which concern `ThreadNet-Web` and none of which concern `thread-net-git`
or `threadnet-operating`.
- The component carrying half the open work, `axion1337.chat-gitops`, is named in the
vision document **not at all** and in `CLAUDE.md` once, parenthetically, as an equation
to a display name (`CLAUDE.md:6`).
- All 14 issues of the newly created M5 — Härtung milestone are in `axion1337.chat-gitops`.
impact: A reader forming a mental model from `vision/` and `roadmap.md` will weight five
components roughly equally and will not learn that one of them holds half the backlog while
two are effectively dormant. Session-start reading therefore mis-primes exactly the
prioritisation that the roadmap exists to steer. This is a mismatch, not a defect: dormancy
may be entirely intended — but the documents do not say so, and `README.md:68-70` requires
each point to describe "den tatsächlichen Zustand".
root-cause pattern: **Documents describe the intended shape, the tracker holds the actual
one, and nothing reconciles them.** Related to F-001 in mechanism: the tracker is where
reality accumulates, the documents are written once.
neckbeard mechanism: `AGENTS.md:79` plus `scripts/gen_status.py`: `STATUS.md` is a generated
overview of open issues, active designs and recent ADRs, explicitly "do not edit by hand",
and `WORKFLOW.md:128` puts reviewing it for "anything stale or surprising" on the refinement
agenda. With ADR-0002's in-repo issues, work mass is countable from the tree, so the
imbalance would be visible in a generated artifact rather than inferable only from an API.
**Gap in neckbeard:** `gen_status.py` covers one repo. This project is six, and neckbeard
has no cross-repo status concept — the same missing "components" artifact as in F-008.
confidence note: medium rather than high — the underlying counts are exact, but "attention
should follow work mass" is a judgement about intent, and dormant-by-design is a legitimate
answer the documents simply do not give.
@@ -0,0 +1,53 @@
# F-010: Hand-written "Stand" labels are older than the commits that touched the same file
category: doc-drift
confidence: medium
evidence:
- Label vs. last commit on `main` (`analysis/data/docs_inventory.tsv`, `git log -1`):
| file | "Stand" label | last commit |
|---|---|---|
| `CLAUDE.md:18` | 2026-08-01 | 2026-08-09 |
| `roadmap.md:3` | 2026-08-06 | 2026-08-09 |
| `hosts/overmind.md:14` | 2026-07-31 | 2026-08-09 |
- The labels are not merely old, they are contradicted inside their own file:
`hosts/overmind.md` carries the table label "**Stand** 2026-07-31" at line 14 and the
sentence "Stand 2026-08-09 **sieben**" at line 28.
- `CLAUDE.md` is labelled "Projektrealitäten (Stand 2026-08-01)" while containing a rule
section headed "Commit-Konventionen (seit 2026-08-07)" — six days newer than the label
above it.
- Scale of the surface: 40 documents, 813 extracted claim lines, of which 68 carry an
explicit date assertion (`analysis/data/claims.tsv`, trigger `date-claim`).
- The whole document set is young — every management doc is ≤ 8 days old
(`docs_inventory.tsv`, max `days_since_change` = 8) — so this is drift accumulating at
high speed, not neglect over time.
impact: The label is what a reader uses to decide whether to trust a section, and here it
systematically under-reports freshness, which trains readers to distrust current content
and, worse, gives no signal at all when a section really is stale. In `roadmap.md` the
combination is actively misleading: "Stand 2026-08-06" over numbers that were already wrong
on 2026-08-09 (F-001).
root-cause pattern: **Hand-maintained metadata about a file, kept inside the file.**
Git already knows the answer; the label is a second source of truth that must be updated by
hand and therefore is not.
neckbeard mechanism: Prevented by `AGENTS.md:79``STATUS.md` is "Generated overview …
do not edit by hand", produced by `scripts/gen_status.py` — combined with
`AGENTS.md:101-103`: "Deterministic jobs (status generation, validation, link checks) are
done by scripts, not by you. If a deterministic job lacks a script, propose one instead of
doing it by inference." A file's freshness is exactly such a deterministic job.
`docs/aar/2026-08-09-neckbeard-v1-creation.md` reinforces it from experience:
"Deterministic jobs belong in scripts, not inference — this rule paid for itself repeatedly",
and "Git is the changelog. No separate log file." (`WORKFLOW.md:140`) is the same principle
applied to history.
Note for Session 2: `schema.yaml` requires a `date` field on adr, design and aar artifacts
(`kind: date`), which is a *creation/decision* date and legitimately hand-set — that is not
the same thing as a freshness label and should not be conflated when migrating.
confidence note: medium — the dates are exact, but a "Stand" label may be intended as
"state of the described system as last verified", not "state of this text". Under that
reading the labels are defensible for `hosts/overmind.md`; they are not for
`roadmap.md`, whose own header says "Zahlen hier veralten, das Board nicht".
@@ -0,0 +1,44 @@
# F-011: "Project repos have their own CLAUDE.mds" holds for one of five components
category: claim-vs-reality
confidence: medium
evidence:
- `CLAUDE.md:5-7` — "Projekt-Repos haben eigene CLAUDE.mds für ihre Spezifika (z. B. die
ESS-/Flux-Details in „ThreadNet Server Suite" = `axion1337.chat-gitops`) — bei Widerspruch
gilt für Arbeitsweise und Prozess **diese** Datei."
- Present in the five component checkouts (`analysis/data/tree_*.txt`):
`axion1337.chat-gitops/CLAUDE.md` — yes. `threadnet-call`, `thread-net-git`,
`threadnet-operating`, `ThreadNet-Web` — none.
- The conflict-resolution rule the sentence sets up ("bei Widerspruch gilt … **diese**
Datei") therefore has nothing to resolve in four of five repos, and a session opened
directly in one of those four gets no pointer to the canonical conventions at all.
- The one that exists is consistent with its claim: `CLAUDE.md:230-233` says the Karpathy
block was taken verbatim from the gitops CLAUDE.md and must not be edited. Verified —
the block is byte-identical in both files; the management copy only appends its own
closing note after it. That claim holds.
impact: Four of five component repos have no session-start instructions. The conventions
that `CLAUDE.md:118` declares binding for "**alle** Repos der Gruppe" (commit style,
timestamp anonymisation, secrets handling) are unreachable from inside those repos — which
is a plausible contributing cause of F-002 and F-003, where exactly those four repos are
the ones the rules did not reach.
root-cause pattern: **Rule declared repo-wide, applied to a subset** — and here the
distribution mechanism for the rules is itself the thing that was only partly applied.
neckbeard mechanism: This is the exact problem ADR-0001
(`docs/adr/0001-agents-md-canonical.md`) decides. Its Context names it outright:
"Maintaining per-harness copies of the rules would guarantee drift", and its Decision makes
`AGENTS.md` the single rule file with `CLAUDE.md` reduced to a one-line pointer — the
neckbeard repo's own `CLAUDE.md` is exactly that one line ("Read AGENTS.md — the canonical
instruction file for this repository. All rules live there."). Adopting it converts the
present claim into a checkable fact: each repo carries a pointer, the rules exist once.
**Gap in neckbeard:** ADR-0001 solves one-repo-many-harnesses; it does not address
many-repos-one-ruleset. Nothing in the framework says how five component repos share the
governing `AGENTS.md` — vendoring a copy re-creates the drift ADR-0001 rejects, and a link
to another repo breaks the portability ADR-0003 protects. Session 2 has to answer this, and
it is the sharpest framework question this field test produced.
confidence note: medium — the sentence can be read as descriptive-in-general rather than as
a claim about all five repos, and it names only gitops as an example.
@@ -0,0 +1,46 @@
# F-012: Six commit references in the docs resolve nowhere, and nothing would have caught it
category: doc-drift
confidence: high
evidence:
- All SHA-shaped tokens in the 40 management documents were resolved against every ref of
all six repos and against the rewrite mapping (`analysis/scripts/inv_shas.py` ->
`analysis/data/sha_refs.tsv`): 22 resolve, **6 do not**.
- The six, with their nature established individually:
- `hosts/cfgmon.md:54` `dfe04c4a` and `verfahren/deploy-uebergabe.md:84` `dfe04c4`
a commit pushed directly to Gitea and overwritten by the mirror. Unresolvable **by
design**; the surrounding text documents the loss ("vom Mirror überschrieben").
- `verfahren/deploy-uebergabe.md:85` and
`verfahren/aar/2026-08-01-cve-pipeline-gitops47.md:13` `2b715ca` — same class, the
second documented overwrite ("2b715ca→0bd77e2").
- `hosts/overmind.md:23` `7645a2b` — a commit in the `vendor/windows` repo, which the
human placed out of scope; not resolvable here, not a defect.
- `hosts/overmind.md:87` `5bc25447` — not a commit at all but a **container image tag**
("Tags `5bc25447` + `stable`"); a false positive of the extractor, recorded as such.
- So of six, four are intentional records of lost commits and two are out-of-scope or
mis-typed as commits. **No accidental orphan was found** — and that is the finding worth
keeping: the mapping list did its job (F-015).
- What no mechanism provides: none of this was checkable before this session wrote a
script for it. `analysis/data/links.tsv` covers markdown link targets and reports 2
broken links in 11,390 (both malformed upstream Element changelog entries), but a SHA in
prose is not a link and was never checked.
impact: Low today, structurally high. The repo cites commits as evidence throughout — it is
the house style, and a good one (`README.md:68-70`, "woher stammt die Aussage?"). That style
depends on citations staying resolvable across exactly the kind of history operation this
project performed on 2026-08-07, and the only thing that kept it working was a document
someone remembered to write by hand.
root-cause pattern: **Evidence-by-citation without a checker.** The convention is sound and
followed; its integrity rests on human diligence at rewrite time.
neckbeard mechanism: **Gap in neckbeard.** `scripts/validate.py` checks link integrity in
two places — `check_links` for frontmatter link fields and `check_body_links` for inline
markdown links, erroring with "inline link target missing" — and `schema.yaml:19-20`
extends body-link checking to root-level prose via `link_only: ["*.md"]`. Commit SHAs cited
in prose are outside both. Given `AGENTS.md:101-103` ("Deterministic jobs … are done by
scripts … If a deterministic job lacks a script, propose one"), resolving a cited SHA is a
textbook deterministic job — `git cat-file -e` — and its absence is a real hole for any
project whose docs cite commits. `analysis/scripts/inv_shas.py` is a working reference
implementation.
@@ -0,0 +1,49 @@
# F-013: The mirror topology works exactly as documented — all six repos in sync
category: works-well
confidence: high
evidence:
- `main` heads compared between canonical git.lab and the Gitea mirror, both read live
with `git ls-remote`:
| repo | git.lab | rohana/Gitea | |
|---|---|---|---|
| management | `2f012a6cd2` | `2f012a6cd2` | in sync |
| axion1337.chat-gitops | `151ef3c8d0` | `151ef3c8d0` | in sync |
| ThreadNet-Web | `d3bd5b20c2` | `d3bd5b20c2` | in sync |
| threadnet-call | `d13cce7337` | `d13cce7337` | in sync |
| thread-net-git | `2f4a15ce85` | `2f4a15ce85` | in sync |
| threadnet-operating | `32f89afebf` | `32f89afebf` | in sync |
- The reasoning behind the topology is written down and is genuinely good:
`CLAUDE.md:26-33` explains *why* two locations exist rather than only asserting it — the
Hetzner cluster must rebuild when the lab is off — and pre-empts the tempting cleanup with
a named warning: "⚠️ **Die Flux-Quelle nicht „geradeziehen"** auf git.lab: Das sähe
aufgeräumter aus und würde die Verfügbarkeit der Produktion an das Lab koppeln".
- The failure mode is documented with its rescue procedure, not just prohibited:
`README.md:16-20` and `CLAUDE.md:45-48` — pull the `.patch` from Gitea, `git am` to
preserve authorship, push via git.lab.
- The one standing exception is explicit, justified, bounded, and has a live mechanism:
the TURN rotation CronJob (`CLAUDE.md:53-60`), canonised daily by the scheduled CI job
`canonize_rotation`, present at `.gitlab-ci.yml:51` in the gitops repo. The alarm design
is stated deliberately — "diese rote Pipeline **ist** der Alarm, einen zusätzlichen
Termin gibt es bewusst nicht."
impact: This is the part of the current approach that must survive migration untouched.
The value is not the sync itself but the form: a decision with its reason, its
counter-argument, its rescue path, and its one exception each written where the next
session will look.
root-cause pattern: **A decision recorded with its reasoning outlives the person who made
it.** Every claim in this cluster held up under measurement; the ones that failed elsewhere
in this report are the ones recorded as bare assertions.
neckbeard mechanism: This is what `docs/adr/` is for. `AGENTS.md:81` — "Architecture
Decision Records — binding; never edited, only superseded" — and `AGENTS.md:89-90`, "Before
proposing options (Gate 2), read the relevant ADRs and AARs first — past decisions and
learnings are input, not trivia." The existing `decisions/0001-gitlab-kanonisch-push-mirror.md`
already is an ADR in all but schema; migrating it needs frontmatter per `schema.yaml:39-52`
(`type`, `id`, `status`, `date`) and nothing else. The "don't straighten this out" warning
maps to the ADR Consequences section, and `WORKFLOW.md:56` ("Options & trade-offs … pro/contra
each, chosen option, and why") is where the two-host reasoning belongs.
@@ -0,0 +1,49 @@
# F-014: The issue discipline the docs describe is actually lived — measurably
category: works-well
confidence: high
evidence: all measured against `analysis/data/gitlab_issues.json` (111 issues, 71 open).
- **Milestone duty** (`CLAUDE.md:98-103`, "Jedes Issue gehört zu genau einem Meilenstein"):
**0 of 71** open issues lack a milestone. Across all four active projects, in every one.
- **Exactly one status label** (`CLAUDE.md:76-78`, `README.md:60`): **0** issues carry more
than one `status:` label.
- **WIP limit 2** (`CLAUDE.md:77`, "**WIP-Limit 2** — auch sessionübergreifend zu
verteidigen"): `status:doing` = **0**. The limit is not merely respected, there is no
work parked as in-progress at all.
- **No priority in titles** (`CLAUDE.md:90-97`, cleaned up 2026-08-06 after 34 migrated
issues carried prefixes and two contradicted their own label): **0** open issues carry a
`[HIGH]`/`[MEDIUM]`/`[LOW]` prefix. Priority lives only in `priority:*` labels
(48 medium, 37 low, 12 high, 4 critical).
- **Legacy IDs preserved and never reused** (`README.md:64-66`, "IDs … werden **nie
wiederverwendet**; sie leben in Issue-Titeln weiter"): **23** issues carry a leading
legacy ID (`^ID:`), across **23 distinct** IDs — no ID identifies two issues. Three
further titles mention an ID belonging to another issue (`LABNET-02` in management#15,
#26 and #27), which is the scheme used as intended: per `CLAUDE.md:92-93` the IDs
"benennen den Gegenstand", so referring to one from another issue is a cross-reference,
not a collision.
- **Closed, not deleted** (`README.md:72-74`): 40 closed issues are present and readable
alongside the 71 open ones.
- Link hygiene in the management repo is likewise clean: of 161 links in its documents,
**0 broken** (`analysis/data/links.tsv`); the two broken links in the whole corpus are
malformed upstream Element changelog entries in `ThreadNet-Web`.
impact: The rule set is not aspirational. Where a rule is mechanically checkable and the
tracker enforces it — a milestone field, a label — compliance is total. That is the
strongest single argument for the migration: this project already works the way neckbeard
assumes, and the failures found elsewhere in this report are concentrated exactly where no
mechanism enforces anything (prose, branches, commit metadata, cross-document consistency).
root-cause pattern: **Where the tool can hold the rule, the rule holds.** The inverse of the
pattern behind F-001, F-004, F-005 and F-010.
neckbeard mechanism: Preserved and extended by ADR-0002
(`docs/adr/0002-in-repo-issues.md`) plus `schema.yaml:76-84`, which makes `status` an enum
(`[open, in-progress, done, rejected]`) that `scripts/validate.py` enforces via
`check_fields` — the file-level equivalent of "exactly one status label", but checkable in
CI rather than by convention. Note the direction of loss to plan for: neckbeard's issue
schema has **no milestone and no priority field**, and
`docs/aar/2026-08-09-neckbeard-v1-creation.md` lists "priority field on issues (YAGNI,
revisit via refinement)" among the ideas deliberately not adopted. This project uses both,
on 100% and 101 of 111 issues respectively. Recorded here as field-test evidence, not as a
proposal — see REPORT.md's gap list.
@@ -0,0 +1,43 @@
# F-015: The rewrite mapping list does what it promises — 251 of 251 rows verified
category: works-well
confidence: high
evidence: `analysis/scripts/inv_shas.py` -> `analysis/data/sha_refs.tsv`, every row of
`shared/commit-zuordnung-2026-08-07.md` re-checked against the actual repositories.
- Row count matches the claim exactly: 117 (gitops) + 78 (management) + 47 (ThreadNet-Web)
+ 9 (threadnet-call) = **251**, and `CLAUDE.md:136` says "251 Commits neue SHAs".
- Of the 251 pairs: **209 `ok`** (new SHA resolves, old one gone as expected) and
**42 `ok-both-present`** (new resolves, old still reachable — all 42 explained by the
stale branches in F-006, not by a mapping error).
- **0 inverted** (new missing while old resolves) and **0 unresolvable** (neither present).
There is no row in the table that does not describe a real commit pair.
- Consequently no accidental orphan reference exists in the docs: all six unresolvable SHA
citations have individual, benign explanations (F-012).
- The document is honest about its own boundaries in a way that measurably helped:
`shared/commit-zuordnung-2026-08-07.md:39-44` states what is *not* in the table
(pre-boundary commits, 147 in gitops, 3 in ThreadNet-Web) — which is what let this
session classify unresolved SHAs correctly instead of reporting them as breakage.
- It records the operational trap it hit rather than quietly fixing it
(`:24-37`): the tag force-push restarted three release pipelines, and the reason nothing
was overwritten is named as luck, not design — "**Das war Glück, keine Planung**" — with
the follow-up tracked as ThreadNet-Web#14.
impact: A destructive, irreversible operation was made auditable after the fact by one
hand-written document, and the audit passes three days later. The unusual quality is the
self-criticism: the near-miss is written down as a near-miss, which is exactly the material
a future session needs and exactly what usually gets smoothed over.
root-cause pattern: **A risky operation paired with a written reconstruction of what it
did.** Note the limit, recorded as F-004's sibling: the `backup-vor-rewrite` branches the
document names as its verification basis (`:14-17`) exist on **no** remote today, so the
mapping can no longer be re-derived — only, as here, spot-checked against surviving refs.
neckbeard mechanism: `docs/aar/` is the home for this document type — `AGENTS.md:83`,
"Standalone After Action Reviews (incidents, major deviations only)", with the required
frontmatter at `schema.yaml:67-74` (`type`, `status` from `[open, harvested]`, `date`) and
the content shape at `WORKFLOW.md:84-89` (planned / actual / why the difference /
learnings, then harvest into the wiki). `WORKFLOW.md:105` makes it mandatory rather than
optional: "Incidents and major misdiagnoses get a standalone AAR in `docs/aar/`". The
`status: harvested` transition is the piece this project lacks — it writes AARs but has no
mechanism that marks a learning as absorbed.
@@ -0,0 +1,49 @@
# F-016: The repo names its own unresolved problems instead of presenting a clean face
category: works-well
confidence: high
evidence:
- Known problems written down *and* filed as issues, verified present in
`analysis/data/gitlab_issues.json`:
- `hosts/overmind.md:33-35` — "⚠️ `gameserver` … hat **keinen** Mirror … dort liegt auf
Gitea ein gleichnamiges Repo mit anderem Stand" -> management#32, open. **Verified
true by measurement**: git.lab `d5c6ccb2e9` vs Gitea `48441a500f`, diverged (F-007).
- `CLAUDE.md:63` — the gitops `wiki` branch is "ein überholter Mai-Abzug" -> management#19,
open. **Verified**: head `0ff598e`, 2026-05-14 (F-006).
- Mirror failure going unnoticed -> management#28 "MIRROR-01", open.
- Restore never rehearsed -> management#30, "Der Restore ist nie geprobt — Sicherungen
sind bisher eine Vermutung".
- `CLAUDE.md:143-147` — the timestamp rule "**schützt nur die Git-Historie**", with push
times, issue timestamps, pipelines and packages named as remaining exposure. Incomplete
(F-002) but volunteered.
- `CLAUDE.md:94-97` explains a rule by the damage its absence did, with numbers: 34 migrated
issues carried title prefixes, "davon **zwei mit einer anderen Aussage als ihr Label**",
and states the principle — "Zwei Wahrheiten über dieselbe Sache sind schlimmer als eine
unvollständige."
- `CLAUDE.md:149-158` ("Redlichkeit & gelebte Lehren") requires verified claims to be
separated from suspicion, states "Korrelation ≠ Kausalität — ein plausibler Verdacht ist
kein Befund", and closes with "Fehlschläge und übersprungene Schritte werden benannt,
nicht geglättet; „fertig" heißt verifiziert."
- `verfahren/retro/2026-08-09.md:70-71` records CI that was green while doing nothing
(`build_embedded` "lud **nie** ein Artefakt hoch") and a published npm package of 12.5 KB
instead of 12.8 MB — failures found and written down rather than quietly fixed.
impact: This is why the present analysis was possible at all. Most of the drift in this
report was found by taking the repo's own statements seriously and checking them; that only
works because the statements are specific, dated and falsifiable. A repo that wrote
"Mirrors are configured" instead of "seven repos, these six product ones plus management"
would have yielded no findings and no confidence either way.
root-cause pattern: **Specific, falsifiable claims are a feature.** Every finding in this
report is downstream of a claim precise enough to be wrong.
neckbeard mechanism: This is `AGENTS.md:50-55` almost verbatim — "Never claim something
works without evidence: a test run, command output, a rendered result. 'Should work' is not
a status" and "Uncertainty is reported, never swallowed. Flag your shakiest calls." It
recurs at `WORKFLOW.md:68` (Gate 3, "Shakiest calls: name the decisions you are least
confident about") and `WORKFLOW.md:113-114` (every session ends by answering which choices
it is least confident about). `WORKFLOW.md:137-139` completes it: "If the wiki has no
confident answer, say so. Never file a low-confidence synthesis back as knowledge."
The habit already exists here; neckbeard gives it fixed places to live rather than relying
on the author's temperament.