Files
chemenu/raw/notes/Conversation Transcript - Versioning, CI-CD and Content Migration Session 2026-08-30.md
T
torben 18ae28f918
CI / verify (push) Failing after 32s
Release / release (push) Successful in 38s
Chemenu 2.1.0 - deterministischer Wissenskompiler
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.
2026-09-01 16:26:14 +02:00

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 in kb/. 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 records claude-opus-5 for 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 VERSION file versioning the stack, not the content. Key argument: no auto-bump from commit messages, because publish --message "ingest: ..." writes content commits into the same repository, so a conventional-commit reading would turn every ingest into a release.
  • A wikitool version command group; .gitea/workflows/ci.yml with the four steps from the TODO plus a version gate and an export smoke test; .gitea/workflows/release.yml triggered by a VERSION change, tagging and publishing a dist export tarball.
  • 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-docker is 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:

  1. Authentication is ${{ gitea.token }}, documented on the Act Runner page under "Gitea-Token" - not secrets.GITEA_TOKEN, and not a 1Password secret, since nothing in these workflows leaves the instance.
  2. The label-to-image mapping for linux-docker is the one thing the wiki does not record, and it decides whether a python3 exists at all. The job image was therefore named explicitly (container: image: debian:trixie-slim, the base Scenario B already proves works with actions/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.
  • lint read 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 for lint (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.py already 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:

  1. migrate verify --from 1c3ca39 over 248 pages: zero invariant violations in 2.2 s - but 13 reported "removed pages" that are not pages. The historical side listed every .md under kb/ while the working-tree side used iter_kb_pages, which skips COLLECTION.md, INDEX.md and the kb-root meta files. Two different definitions of "page." Fixed with a shared kb_scan.is_page_path, and pinned by a regression test.

  2. Negative control: one of two [[Docker]] occurrences removed from kb/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 silent
    

    This is the claim the whole strategy rests on: lint reads a single revision, so a reference that went missing leaves a corpus that is still perfectly consistent.

  3. version bump --major correctly 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.

  4. The dev tree itself became the first migrate baseline case: it predates .wikitool-kb.json, but its content never lagged its machinery, so 1.0.0.

  5. 630 tests, docs verify, instructions verify, lint --fail-on-error, and a full setup-instance.md replay against a fresh dist 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.x and 1.x; from 1.0.0 it 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: true instructions under instructions/migrations/, so they ship with dist export without a second export path. Baseline is 1.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 must apt-get install nodejs as the first step, before checkout. act_runner executes JavaScript actions with node inside the job container.
  • actions/checkout@v7, actions/upload-artifact@v3 (v4 is limited on this instance).
  • debian:trixie-slim works and carries python3 3.13. Labels linux-docker and container-builder both 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 404 for an invisible repo and a non-existent one - so curl proves 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.