Chemenu kompiliert Rohnotizen zu einem verlinkten, quellengebundenen Wiki: raw/ -> types/ + tools/ -> kb/ -> reports/. Was mechanisch ist, macht tools/wikitool; was Urteil braucht, macht ein Agent unter Contracts, deren Grenzen in Code durchgesetzt sind statt im Prompt. Dieser Commit ist der Startpunkt der oeffentlichen Historie. Die vorherige Entwicklung fand in einer privaten Instanz statt und ist nicht Teil dieses Repositorys; ihre Erzaehlung steht vollstaendig in CHANGES.md, das mit 44 Eintraegen von 0.1.0 bis 2.1.0 erhalten geblieben ist. Der mitgelieferte Korpus ist ein Testbett und eine Demo: 170 Seiten ueber den Stack selbst - Gates, Lint, Versionierung, Suche, das Wiki-Muster. Er dokumentiert das Werkzeug mit den eigenen Mitteln des Werkzeugs. Lizenz: AGPL-3.0 fuer den Stack (tools/, types/), CC-BY-4.0 fuer die Inhalte. Die Grenze zwischen beiden ist der Dateiplan, den dist export berechnet - siehe NOTICE.
20 KiB
Conversation Transcript - Versioning, CI/CD and Content Migration Session
Source: Claude Code session (
claude-opus-5), llm-wiki-test1 workspace Collected: 2026-08-30 (session spanned 2026-08-29 to 2026-08-30) Participant: Torben Fidelity: faithful summary transcript, not a verbatim log. Reconstructed by the assistant at the end of the session. Torben's questions and decisions are rendered closely; the assistant's reasoning is condensed. Command outputs quoted below are real. No credentials appeared in the session; internal hostnames mentioned are already documented inkb/. Revised 2026-08-30 with turns 10-13, which happened after the first version was written; the model attribution was corrected at the same time (the first version said "Claude Sonnet 5", the session log recordsclaude-opus-5for all 501 messages).
Covers three pieces of work that turned out to be the same mechanism seen from different ends:
stack versioning with CI and release artifacts (shipped as 0.1.0, then 1.0.0), a content
migration strategy built on top of it, and finally getting the pipeline to actually run.
Resulting commits: 2508f7a, 7d63d61, c3034ab, db03b08, aace3e7, 401d700, b94166b.
Turn 1 - "Bearbeite den CI/CD-Punkt aus todo.md ... Was denkst du?"
Torben invoked the stack-dev skill and asked for the CI/CD item from TODO.md to be built
such that semantic versioning comes with it, that the version then drives update detection, and
that release files come out with which a wiki can be set up.
Assistant proposal, after reading TODO.md, tools/CONTRACT.md, dist_cmd.py, cli.py,
docs_verify.py and .gitignore:
- A root
VERSIONfile versioning the stack, not the content. Key argument: no auto-bump from commit messages, becausepublish --message "ingest: ..."writes content commits into the same repository, so a conventional-commit reading would turn every ingest into a release. - A
wikitool versioncommand group;.gitea/workflows/ci.ymlwith the four steps from the TODO plus a version gate and an export smoke test;.gitea/workflows/release.ymltriggered by aVERSIONchange, tagging and publishing adist exporttarball. - Update detection via a stamp file written into every export, compared against the origin's release feed.
- Explicitly not shipping CI into a distributed instance, because
runs-on: linux-dockeris a site-specific runner label.
Turn 2 - Three design decisions
The assistant asked three questions with recommendations. Torben's answers:
| Question | Decision |
|---|---|
Network call in wikitool vs. strictly offline |
version check may make the network call - own command, no key, timeout, injectable fetch so tests never touch a network |
| Build the upgrade path now, or detection only | Detection first, dist upgrade later |
Start at 1.0.0 or 0.1.0 |
0.1.0 (against the assistant's recommendation) |
Because 0.x was chosen, the assistant defined compatibility as "same leftmost non-zero
component" (the rule Cargo's caret ranges use), so that 0.1.x -> 0.2.0 carries the
migration signal that MAJOR carries from 1.0.0 on.
Turn 3 - "Prüfe die Actions gegen den Gitea Runner, die Doku liegt im Wiki"
Sent mid-turn. The assistant ran wiki-query against the corpus before writing the workflow
YAML. The wiki corrected two assumptions:
- Authentication is
${{ gitea.token }}, documented on theAct Runnerpage under "Gitea-Token" - notsecrets.GITEA_TOKEN, and not a 1Password secret, since nothing in these workflows leaves the instance. - The label-to-image mapping for
linux-dockeris the one thing the wiki does not record, and it decides whether apython3exists at all. The job image was therefore named explicitly (container: image: debian:trixie-slim, the base Scenario B already proves works withactions/checkout@v4).
Also confirmed from the corpus: routing labels are linux-docker, container-builder,
k3s-deploy; actions/upload-artifact@v3 (v4 support is limited).
A local finding of the same kind: setting WIKI_TRACE=0 job-wide breaks two telemetry tests
that assert a trace is written, so CI sets WIKI_TRACE_DIR outside the workspace instead.
Turn 4 - Publish, and the Mass-Update Gate
wikitool publish exited 42 with a 21-file breakdown, which was reproduced in full for Torben.
He replied "Freigegeben", and the clearance line published 2508f7a.
Turn 5 - A correction: the origin repository is private
Attempting to verify whether CI had run, the assistant discovered that its own earlier inference
was wrong. Before the push, version check had returned HTTP 404 and this was read as "no
release published yet, so the feed is anonymously readable". It is not:
$ curl -s -w "\nHTTP:%{http_code}\n" ".../api/v1/repos/torben/llm-wiki-test1"
{"message":"not found",...} HTTP:404
$ curl -s -w "\nHTTP:%{http_code}\n" ".../api/v1/repos/torben/definitely-not-a-repo-xyz"
{"message":"not found",...} HTTP:404
Gitea answers an anonymous caller identically for an invisible repository and a non-existent
one. Consequence: WIKITOOL_UPDATE_TOKEN is a requirement rather than an exception while the
repo stays private, and no conclusion about CI state can be drawn from outside. Corrected in
CHANGES.md and INSTALL.md, published as 7d63d61.
Turn 6 - "Die Runner laufen nicht" + the next task
Torben: the runners do not run; he will debug it later at his PC and will provide a Gitea MCP server for it. (Dictated "kaufen" for "laufen".) Then the new task:
"Wie gehen wir denn mit semantischen Änderungen am Content um, die machen wir ja gerade mehrfach. Lass uns eine Migrationsstrategie erarbeiten und umsetzen. Wir brauchen erst mal einen Plan."
Turn 7 - Research: the methodology already existed and had been deleted
The assistant recovered the closed translate-kb-de workshop from git history
(git show de0862f:work/translate-kb-de/README.md and plan.md) and found a complete working
methodology that had been thrown away when the workshop closed:
- Units sized by the iteration budget (30 calls → ceiling near 24 pages, target ≤ 21), and batches sized separately by the Mass-Update Gate - conflating the two "cost eleven unnecessary clearances in the first cut of this plan".
- Per unit, before anything else: frontmatter, H1, wikilink targets and cite-ids compared
against
HEAD. lintread in full every unit, not just the plausibly-affected sections - unit 1's frontmatter bug surfaced as a schema error on a field nobody had edited.- Summaries written by the orchestrating session, never pasted from a subagent ("they embellish: one turned 'measuring application performance and responsiveness' into 'Latenz und Durchsatz unter Lastbedingungen'").
Two further findings from reading the code:
kb_scan.extract_wikilinks()returns a set. That is correct forlint(does the reference resolve?) and wrong for a migration check (did one go missing?). Three of the four defects the translation found had unchanged link sets and only changed counts.sections.pyalready documents a named migration pattern in its docstring: canonical name plus aliases is "what lets a corpus migrate page by page instead of all at once", and removing an alias is a breaking change rather than a cleanup.
Turn 8 - Plan rejected, with three substantive points
Torben rejected the first plan:
"Lass uns doch mit Version 1.0.0 anfangen, damit wir den 0.* Sonderfall nicht implementieren müssen. Wo speichern wir denn die aktuelle Version einer KB? Wie decken wir ab, dass eine KB ggf. über mehrere Versionen aktualisiert werden muss? Wir müssen ja eigentlich sowas wie 'migrate from 1.3.1 to 1.4.0' und dann 'migrate from 1.4.7 to 2.0.0' in Sequenz ausführen? In diesem Kontext müssen wir keine Beta oder ähnliche Version Keys unterstützen. x.y.z ist die maximale Granularität, die wir brauchen."
The second question exposed a real error in the plan: it had conflated the stack version with the content version. An instance can carry machinery 1.4.0 while its content is still in 1.2.0 shape - and that is the state every upgrade passes through. The first plan had written this off as a "known limitation" instead of solving it.
Revised design:
| Fact | File | Written by | Answers |
|---|---|---|---|
| Stack version | VERSION |
version bump |
which machinery is installed |
| Release stamp | .wikitool-release.json |
dist export |
where that machinery came from |
| KB version | .wikitool-kb.json |
migrate done |
what shape the content is in |
Separate files because the two have opposite rules - the stamp is generated and must never be hand-edited, the KB state is mutable instance state.
On sequencing: migrate status builds the interval (kb_version, VERSION] from the migration
documents and orders it ascending; 1.3.1 → 2.0.0 runs 1.4.0, then 1.7.0, then 2.0.0. That no
migration targets 1.3.x is not a special case - it is simply not in the interval. migrate done
refuses any version that is not the next link, making a skip impossible and an interrupted
multi-step upgrade resumable.
Starting at 1.0.0 also removed a self-contradiction the assistant had shipped in 0.1.0: the
guidance in stack-dev/SKILL.md assigned --minor to both "new capability" and "requires
migration", which cannot both be true under 0.x. The compat_key code needed no change - it
is stated uniformly - only the guidance did.
Plan approved on the second attempt.
Turn 9 - Implementation and verification
Built: corpus_diff.py, kb_state.py, migrate_cmd.py (list/status/verify/done/
baseline), instructions/migrate-corpus.md, the migrates_to:/migration_kind: schema
fields, boundary enforcement in both version bump and docs verify, and a kb-version check
in doctor. Deliberately not built: mechanical runner primitives (a DSL for zero migrations)
and dist upgrade.
Verification against the real corpus, which unit tests cannot replace:
-
migrate verify --from 1c3ca39over 248 pages: zero invariant violations in 2.2 s - but 13 reported "removed pages" that are not pages. The historical side listed every.mdunderkb/while the working-tree side usediter_kb_pages, which skipsCOLLECTION.md,INDEX.mdand the kb-root meta files. Two different definitions of "page." Fixed with a sharedkb_scan.is_page_path, and pinned by a regression test. -
Negative control: one of two
[[Docker]]occurrences removed fromkb/entities/tools/Act Runner.md, leaving the link set unchanged.$ tools/wikitool migrate verify --from HEAD --fail-on-error 248 page(s) compared, 0 added, 0 removed, 1 finding(s). - kb/entities/tools/Act Runner.md: wikilinks - 'Docker' 2->1 $ tools/wikitool lint --fail-on-error # exit 0, all 21 checks silentThis is the claim the whole strategy rests on:
lintreads a single revision, so a reference that went missing leaves a corpus that is still perfectly consistent. -
version bump --majorcorrectly refused without a migration document, then succeeded with--no-migration "no distributed instance exists yet; 1.0.0 is the migration baseline"- the new mechanism's first real use. -
The dev tree itself became the first
migrate baselinecase: it predates.wikitool-kb.json, but its content never lagged its machinery, so1.0.0. -
630 tests,
docs verify,instructions verify,lint --fail-on-error, and a fullsetup-instance.mdreplay against a freshdist export.
Published as c3034ab after the Mass-Update Gate breakdown (29 counted files) was reproduced
and cleared.
Turn 10 - "Store this conversation in the wiki as raw source in /raw/notes"
The first version of this file. raw/CONTRACT.md lists "conversation transcripts" under
notes/ explicitly, and a sibling already existed (Conversation Transcript - AGENTS.md Skill Restructuring Session 2026-08-04.md), so the format was inherited rather than invented. Labelled
a summary rather than a log, because a verbatim reconstruction would have been fabrication.
Published as db03b08.
Immediately afterwards the Gitea MCP server became available, and a read-only diagnosis of the long-standing "the runners don't run" item produced the opposite of what had been assumed:
list_runs -> 6 runs; 46-51 all conclusion: failure
OCI runtime exec failed: exec: "node": executable file not found in $PATH
❌ Failure - Main actions/checkout@v4
exitcode '127': command not found
The runners had been picking the workflows up all along. actions/checkout is a JavaScript
action that act_runner executes with node inside the job container, and the pinned
debian:trixie-slim has none. The image was pinned precisely as a precaution against the
undocumented linux-docker label mapping - the precaution caused the failure.
Turn 11 - "Check the gitea-mcp repo, branch ci-build"
Torben pointed at two working workflows on another repo and asked for their shape to be picked up, plus: the wiki workflows must not run on content changes.
torben/gitea-mcp@ci-build, ci-build.yaml, runs 42-45 green. The whole answer was one
line in an apt list:
- name: Install CI Dependencies
run: apt-get install -y --no-install-recommends git nodejs curl unzip ca-certificates build-essential
- name: Checkout Code
uses: actions/checkout@v7
nodejs installed before the checkout, and checkout at @v7. Both were adopted in ci.yml
and release.yml. runs-on: linux-docker was kept: runs 46-51 proved it routes and starts the
container, so the label was never the problem.
For the second half, paths-ignore on everything publish touches. Three choices are worth
recording because they are all refusals to infer: kb/CONTRACT.md is deliberately not
excluded (it lives under a content directory but belongs to the stack); no !**/CONTRACT.md
negation, because Gitea's support for negated filter patterns is undocumented; and the list is
written twice instead of shared through a YAML anchor, because GitHub's parser rejects anchors
outright and Gitea's is not documented to accept them. The patterns fail open - anything
unanticipated still triggers CI. Published as aace3e7.
Turn 12 - The first green run finds a real bug
Run 52 got past checkout and reached pytest for the first time ever. Two of 630 tests failed:
FAILED test_new_page.py::test_new_source_author_falls_back_to_git_config
FAILED test_provenance.py::test_new_source_with_multiple_raw_files
AssertionError: ERROR No author configured for this instance.
2 failed, 628 passed in 13.85s
config.default_author() runs git config user.name with cwd=config.ROOT; the fixture root
is not a repository, so the answer came from the global git config of whoever ran the suite.
In the container, as root, there is none. Both tests had been green on every developer machine
for months without ever testing what their names claimed.
Fixed in the tests rather than by giving CI an identity: the first now makes its fixture root a
real repository with a local user.name and asserts the concrete name, which tests the
fallback more sharply than before; the second sets WIKI_AUTHOR, since it is a raw_files:
test and authorship was only a precondition. Verified locally with
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null pytest - 630 passed.
That made it 1.0.1, and because VERSION moved, release.yml fired on its own. It answered
the last open question of the session without being asked:
Created release v1.0.1 (id 54).
Uploaded llm-wiki-stack-1.0.1.tar.gz.
Uploaded llm-wiki-stack-1.0.1.tar.gz.sha256.
${{ gitea.token }} may create releases and tags and upload assets. No Actions secret with
write:repository is needed. Published as 401d700.
Turn 13 - "Can we take the CI/CD part out of todo.md?" then: move it to issues
Two cleanups. First TODO.md: the entire retrospective went out (64 lines), because the same
history was already in the 0.1.0, 1.0.0 and 1.0.1 changelog entries and a second copy is
exactly the drifting duplicate the stack avoids everywhere else. Published as b94166b.
Then the remaining work moved to Gitea issues #7-#11 - dist upgrade, hardening the suite
against silent environment dependencies, the nightly drift check, coverage, and confirming
paths-ignore actually matches. TODO.md now links to them instead of describing them.
The split was made along a line worth naming: issue #11 is explicitly an observation, not a build task - the proof arrives on its own with the next content-only publish. Filing it as work would have invited someone to build machinery for a question that answers itself.
Decisions worth carrying forward
- The version describes the stack; content has its own version. Conflating them makes the mid-upgrade state unrepresentable.
- Compatibility = leftmost non-zero component. Uniform across
0.xand1.x; from1.0.0it reads as plain semver. No pre-release suffixes - a second ordering rule would have to be honoured by the release feed, the migration chain and the compatibility check alike. - Migrations are
manual: trueinstructions underinstructions/migrations/, so they ship withdist exportwithout a second export path. Baseline is1.0.0; anything older is re-exported, not migrated. - Count, never set, when asking whether a rewrite dropped something.
- CI tags, never an agent - which is what keeps AGENTS.md invariant 5 intact.
- The wiki corrected the assistant twice in this session (the Gitea token form, the runner image), and the assistant's own inference was wrong twice more: anonymous API access, and the claim that the runners never accepted the workflows. Checking the corpus before writing infrastructure code paid for itself; inferring past what the corpus actually said did not.
- A CI run is evidence; a local run is a habit. Two tests asserted a fallback they never exercised, and stayed green for months, because every machine that ran them happened to satisfy the precondition. Nothing short of a foreign environment would have found it.
- The failure and the fix lived in different places. The symptom was in CI, the defect was in the tests. Giving CI a git identity would have made the run green and left the bug.
What this changed about the runner, in one place
For anyone writing the next workflow against the CI runner - do not re-derive this:
- A pinned
container:image mustapt-get install nodejsas the first step, before checkout. act_runner executes JavaScript actions withnodeinside the job container. actions/checkout@v7,actions/upload-artifact@v3(v4 is limited on this instance).debian:trixie-slimworks and carries python3 3.13. Labelslinux-dockerandcontainer-builderboth accept a job that names its own image.${{ gitea.token }}suffices for releases, tags and asset uploads.- The repo is private, and Gitea answers anonymous callers with an identical
404for an invisible repo and a non-existent one - socurlproves nothing. Read runs through the MCP server.
Open at the end of the session
The pipeline runs. v1.0.1 is published with tarball and .sha256. What remains is tracked as
Gitea issues rather than prose: #7 dist upgrade, #8 hardening the test suite, #9
the nightly drift check, #10 coverage, #11 confirming paths-ignore matches.
One item is genuinely unresolved rather than merely unbuilt: whether Gitea evaluates the
paths-ignore patterns as expected. It needs no work, only a look at list_runs after the next
content-only publish - and after this session, that is exactly the kind of claim that should not
be assumed.