Files changed: - CHANGES.md - VERSION - instructions/wiki-lint/SKILL.md - kb/CONTRACT.md - tools/CONTRACT.md - tools/chemenu/commands/lint.py - tools/chemenu/commands/page_ops.py - tools/chemenu/kb_scan.py - tools/chemenu/lint_core.py - tools/chemenu/tests/test_kb_scan.py - tools/chemenu/tests/test_lint.py - tools/chemenu/tests/test_page_ops.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
541 KiB
Changelog
This file tracks changes to the wiki stack itself - AGENTS.md, the
instructions/ layer, tools/wikitool, and the contracts. It is distinct from
kb/log.md, which is the audit trail of wiki content operations (ingests,
queries, lints, page creates/updates) performed by the LLM against kb/.
Previously each of AGENTS.md and README.md carried its own "Version
History" table. Those have been consolidated here so there is one place to
look for "what changed in the tooling/schema, and when." From now on,
document any change to the stack (schema, instructions, wikitool commands,
contracts) as a new entry at the top of this file instead of editing inline
version history tables.
Since 0.1.0 an entry's heading also carries the stack version it describes
(## <version> - <date> - <title>). wikitool version bump writes that
heading, and wikitool docs verify refuses a tree whose VERSION and newest
versioned entry disagree. Entries below 0.1.0 predate versioning and keep
their date-only headings.
Since 4.4.0 the stack carries one running candidate between two
releases rather than a fresh version per bump - see
instructions/dev/version-parts.md. While a candidate is open its heading
names it with a -beta.N suffix (## 4.4.0-beta.2 - <date> - <title>), and
every bump of that same candidate updates this one entry in place rather than
opening another: the heading's version/date/title move, and the bump's
--title joins a machine-managed <!-- wikitool:bumps --> list right under
the entry's **Author:**/**Breaking Change:**/**Migration:** lines -
written and read by wikitool version bump, never by hand.
**Breaking Change:** accumulates, because one candidate can cross the
compatibility boundary more than once and each crossing is a separate thing an
operator has to act on: one reason stays on the marker line, a second and
further ones move to bullets beneath a bare marker. **Migration:** does not -
it answers one yes/no question about the candidate as a whole, so a later
answer replaces the earlier one.
That list is graded, not a flat chronological dump: each bump carries an
impact (--impact high|medium|low, default medium), and the list renders
grouped under **High/Medium/Low impact** headings - except when every bump
so far is medium, where it stays flat with no headings at all, exactly as
it always did before grading existed. wikitool version regrade corrects a
grade after the fact, against a single read of the whole list. Below the
list comes a short summary paragraph, written once at release time, and below
that one ### <bump title> changeset per bump, in chronological order -
wikitool version release refuses to close a candidate that collected two or
more bumps and has no summary there (a one-bump candidate is exempt, since its
single changeset already reads as one). This layering exists because a
long-running candidate's bump list, left flat and ungraded, grows unreadable
as a release announcement - the concrete case that forced it was 5.0.0, one
entry across roughly 1440 lines.
wikitool version release is what closes a candidate: it strips the suffix
and turns the entry into an ordinary, suffix-free one, leaving the bump list,
summary and changesets as the record of what happened. A distributed instance
never sees a -beta. version at all (release.yml only ever releases a fixed
one), so the suffix and everything below the heading are a dev-checkout
concern - readable here, never shipped as something to parse.
8.0.0-beta.30 - 2026-10-03 - lint: a wikilink wrapped across a line break is its own finding; rename and rm see it
Author: Torben Nehmer
Breaking Change:
- Page titles must form valid, unique file names on Windows and macOS: new and rename refuse forbidden characters, reserved names (including INDEX and COLLECTION), a trailing dot or space, and titles that collide with another page by case or Unicode normalization; lint reports existing violations as hard errors - rename each affected page with tools/wikitool rename
- publish without --no-push now exits 1 before committing when the remote is unreachable or not configured, where it used to commit locally and fail at the push - an offline session or a local-only instance must pass --no-push
- new, rename, move and raw accept refuse a target whose path below the instance root is over 160 characters (UTF-16 code units); lint reports existing files over it as Long Paths (advisory) - rename each affected page with tools/wikitool rename, and shorten an incoming/ file name before raw accept
- tools/wikitool now refuses to start (exit 42) until tools/preflight.sh (PowerShell 7: tools/preflight.ps1) has passed in the checkout - after updating, run it once: it checks Python, git and ripgrep, records their paths in .wikitool-tools.json and sets up tools/.venv
- An instance is installed only from a release, into an empty folder (instructions/setup-instance.md); dist export, a clone of the origin repo and a private clone with the origin as upstream are no install paths any more, and wikitool upstream merge, upstream verify and instructions/private-instance.md are gone - an instance built one of those ways is reinstalled from a release into an empty folder and its kb/, raw/ and personal files are copied over. The preflight release asset installs into its own folder, which must be empty apart from the script and a .git, instead of creating a chemenu/ subfolder
- A file in a subdirectory of incoming/ is no longer accepted - raw accept and raw fetch --html refuse it; a subdirectory is now one source, accepted whole with raw accept incoming/. Drop files directly into incoming/ and adjust any script that writes to incoming//; files still waiting in such a subdirectory are moved up into incoming/, or, if they belong together, accepted as one folder
Migration: none required - No page format changes: the title and path rules only refuse names, each affected page is renamed with tools/wikitool rename, and the removed install paths touch no page
High impact
- wikitool: one data record per command -
-h, index and CONTRACT.md render from cli_contract (Gitea #121 Phase 1) - dist upgrade --latest: one-command update from the release feed
- Page titles must form valid, unique file names on Windows and macOS
- Preflight: prerequisites checked and tool paths recorded before wikitool runs (#151, POSIX half)
- Installation only from a release, into an empty folder; upstream merge/verify and the other install paths removed (#153)
- incoming/ as a queue: raw pending picks the next entry, raw accept takes a whole folder
Medium impact
- CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them
- dist export no longer cuts the dist export record out of the shipped tools/CONTRACT.md
- fail() prints the command's ON FAILURE lines on stderr
- Budget gate and loop-breaker refusals exit without a traceback
- Super Productivity API path: unwrap the {ok, data} envelope, exclude the inbox project, ready-aware health (#162)
- Live tracker suite: WIKITOOL_TASKS_CONFIG override, real-tracker tests for Super Productivity and CalDAV, nightly workflow and test image
- Bug-report collector: tools/bugreport.py and instructions/bug-report.md
- Bug-report collector can pseudonymise identities, in two stages
- publish: the gate lists the staged state; a missing or unreachable remote stops before the commit
- Path budget: a file's path stays at 160 characters or fewer so a Windows checkout works without long paths (#163)
- PowerShell 7 preflight and launcher: tools/preflight.ps1, tools/wikitool.ps1, doctor checks for execution policy and Mark of the Web
- Preflight as a release asset: download, verify and unpack the stack, then run the tree preflight
- trace-hook.ps1: Copilot hooks no longer open Windows' choose-an-app dialog
- Windows-Portabilität: Pfadtrenner, Zeilenenden, Encoding und Locks
- INSTALL.md an die Installationsinstruktionen gekoppelt: Voraussetzungen generiert, Setup-Fragen geprüft
- tools/bugreport: Starter für den Bugreport-Sammler, überspringt die Store-Aliase (#166)
- publish keeps a closing trailer block of --message last, so git reads Co-Authored-By again (#149)
- Stack-Entwicklung in drei Phasen: stack-dev (Design), stack-build, stack-close - Übergabe über den Tracker, kein Modellwechsel in der Sitzung
- raw fetch: a sanctioned intake for a URL into incoming/
Low impact
- version bump no longer points at version release in its output
- stack-close: wait for CI through the authenticated Gitea connection, with timings and a give-up point
- wikitool: usage lines name wikitool, and the -h acceptance checks become tests
- Command records, Git group: NOTES as bullets, one exit line per cause, examples and prohibitions
- Command records, Catalog and log group: bullets, examples, prohibitions
- Command records, Distribution and versioning group: one line per cause, examples, prohibitions
- Command records, Pages group: one line per cause, examples, prohibitions
- Command records, Links and citations group: one line per cause, examples, prohibitions
- Command records, Finding and checking group: one line per cause, examples, prohibitions
- Command records, Provenance group: examples, exit lines per cause
- Command records, Raw material and uploads group: one line per cause, examples, prohibitions
- Command records, Workshop runs and session budget group: examples, prohibitions
- Command records, Types, instructions and docs group: one line per cause, examples, prohibitions
- Command records, Telemetry group: examples, the missing --fail-on-error exit line
- Command records, Content migrations group: one line per cause, examples, prohibitions
- Command records, Private instances group: one line per cause, examples, prohibitions
- Command records, Instance health group: one bullet per check, examples
- Command records: NOTES is always a tuple of bullets; every record's examples are tested
- network: property defined; sync, publish and upstream merge marked networked
- log append: unreadable --body-file is an ERROR line, not a traceback
- Command records: three more mismatches from #142 aligned to code
- docs contract: the merged-stream test pins its own ON FAILURE line
- gates.md and a run_budget comment name kb pages by title, not by a path that moved
- cli.py: removed a stale duplicate help-patch line outside the typer._click fallback's try (Gitea #148)
- new: concept/source record notes name their layout-computed subdirectory; source usage names its required fields (Gitea #150)
- new_page/type_resolver comments no longer claim only entities declare a layout:
- Demo corpus follows the decided project pages: three states, seed with their items (#156)
- bug-report instruction: offer WIKI_TRACE=1 for a reproduction
- reports/CONTRACT.md: only the collector's two counting calls take the bugreport session id
- bug-report instruction: step 1 no longer calls every bundle unpseudonymised
- setup-instance step 15 names --no-push for a local-only first publish; gates.md and a docstring follow #159
- raw/CONTRACT.md points at the path budget for a name accepted from incoming/
- preflight.ps1: the asset-mode error helper is Exit-Asset, so PSScriptAnalyzer passes
- kb/CONTRACT.md: an external article's raw_files point under raw/, a raw fetch capture names both files
- raw fetch --html record and kb/CONTRACT.md path budget follow the folder accept
- stack-close no longer records which model and effort ran each phase
- lint: a wikilink wrapped across a line break is its own finding; rename and rm see it
lint: a wikilink wrapped across a line break is its own finding; rename and rm see it
A [[...]] written across a line break - prose wrapped at a fixed column, with the break
landing inside the link - was read with the break as part of its title. lint reported the
target as a missing page, with the line break in the printed name; rm's inbound check did not
count it, so a page referenced only that way was deleted without the --yes confirmation; and
rename left it pointing at the old title. A link target is now read through one function,
kb_scan.normalize_link_target(), which folds a line break and its indentation to one space -
used by the link graph, the index check, rename and rm alike. That such a link is wrapped
is still a defect: lint reports it in a new hard section, Wrapped Wikilinks, and reports it
as broken as well only if the folded title is missing. No instance's lint gets redder: every
link the new section names was a hard broken-link finding before. kb/CONTRACT.md § Titles are
identifiers states the rule (Gitea #115).
stack-close's closing comment no longer carries a per-phase table of model, effort, session
shape and compaction, nor the size/ label beside it; the comment is the changelog line, then
the issue closes. The operator decided the models used need not be documented (Gitea #170), so
the record is dropped without a replacement, together with the sentences that leaned on it: stack-mode.md § Sessions
and models, and two in docs/model-and-effort-selection.md. Which model suits which phase is
unchanged. Dev-only apart from one docs/ sentence; no behaviour change.
raw fetch --html record and kb/CONTRACT.md path budget follow the folder accept
Two sentences the previous change left behind, found in its closing check. raw fetch --html's
command record still named a file "not under incoming/" as its refusal, though since the
previous change a saved page inside a subdirectory of incoming/ is refused too; it now says
"not directly in incoming/" and names moving the file up as the fix. kb/CONTRACT.md's path
budget paragraph named only renaming a file in incoming/ as the remedy for raw accept; for a
folder the remedy is a shorter folder name or shorter names inside it, and it now says so. No
behaviour change.
incoming/ as a queue: raw pending picks the next entry, raw accept takes a whole folder
Breaking. A subdirectory of incoming/ was tolerated and ignored since the date shard
replaced the type directories, so an old incoming/documents/ habit kept working. It protected
nothing - the kind of source comes from its content now, as source_type: - and it left a folder
of files that belong together with no way in: raw accept took files at most one level down,
bundled them flat under the first file's stem, lost the folder's name and left the emptied
directory behind, and a folder with subfolders could not be accepted at all. Now a file argument
of raw accept (also with --replaces) and of raw fetch --html must sit directly in
incoming/; a file inside a subdirectory is refused, and the message names both ways out.
A folder is one source. raw accept incoming/<folder> --fidelity ... --authority ... moves
every file below it to raw/<YYYY>/<MM>/<folder>/ at the same relative path and removes the
directories left empty. The folder name is the bundle name and falls under the existing name
rule; the files inside do not, so several README.md in one tree are no conflict. It is accepted
alone (no other argument, no --page, no --replaces), and every check runs before anything
moves: an empty folder, or a hidden entry, a symlink or a special file anywhere below it, is
refused with each entry named - which is what guarantees that the clean-up, an rmdir per
directory and never a recursive delete, cannot take a file with it. A folder that passes the
large-tree thresholds continues with work new --input raw/<YYYY>/<MM>/<folder>, which the
success message prints.
tools/wikitool raw pending reads incoming/ as a queue without changing it: the
top-level entries, with top-level files sharing a stem as one bundle (a raw fetch pair) and a
folder as one candidate, oldest first by modification time - a bundle or folder as new as its
newest file - each marked with whether raw accept would take it, by the same checks, and why
not. The default is the first acceptable one. wiki-ingest uses it when the user names nothing:
it announces the entry it took, ingests exactly that one, and says at the end how many still
wait. raw/CONTRACT.md § Getting a file in describes candidates, order and the limit of an
mtime, which is a document's last change only when it was copied with its timestamps kept.
No migration: no page changes and raw/ is untouched. An instance whose scripts write to
incoming/<type>/ changes them to write directly into incoming/.
kb/CONTRACT.md: an external article's raw_files point under raw/, a raw fetch capture names both files
kb/CONTRACT.md still told a source page for an external article to point raw_files: at "the
local copy under raw/articles/" - wrong since promotions moved to the date shard, and wrong in
exactly the path raw fetch now makes common. It now says the local copy under raw/, and that
a page captured with raw fetch lists both the received .html and the derived .md.
wiki-ingest gains a URL as a second example trigger. Found in the closing check of #120.
raw fetch: a sanctioned intake for a URL into incoming/ (#120)
A URL the user wanted ingested had no tool and no procedure: every session built its own chain
of curl, a guessed character set, boilerplate cut by line number and a header written from
memory, so two sessions turned the same article into two different, permanent raw/ files. The
new tools/wikitool raw fetch <url> fetches the page and writes two files into incoming/ - the
HTML exactly as received, and a .md with a fixed header (url, final_url, retrieved,
http_status, content_type, charset and where it came from, title, derived_from) above a
Markdown-like text derived from it. raw accept then promotes both in one call as one bundle,
unchanged, so the capture fields are still asked once, at the same point of wiki-ingest. The
HTML is kept because it is what was received: a claim stays checkable against it even where the
derivation lost something.
The derivation is deterministic and needs no new dependency (urllib, html.parser): charset
from the byte-order mark, then the HTTP header, then a <meta> in the first 4 KiB, then UTF-8,
with undecodable bytes replaced and reported; content root <main>, else a single <article>,
else <body>, with navigation, header, footer, aside, forms and scripts dropped and links made
absolute. Only http/https on every redirect hop, 30 s, 25 MiB, no cookies; a non-HTML answer
(plain text, PDF, image) is stored as received with no derivation. For a paywall, a login or a
script-rendered page, raw fetch --html incoming/<file>.html --url <url> derives the same .md
from a page the human saved from their own browser, without network access - the tool holds no
credentials. The header's values are YAML-quoted where needed (charset: "utf-8 (from: header)"),
so the block parses as the frontmatter its --- fences suggest.
raw/CONTRACT.md has a new section "Getting a URL in: raw fetch" (the bundle rule, the header,
--html, and that only a URL the user named is fetched), and says why the MCP server gets no
fetch tool. wiki-ingest starts a URL with raw fetch and stops at a teaser instead of
ingesting it.
Stack-Entwicklung in drei Phasen: stack-dev (Design), stack-build, stack-close (#168)
Bisher hatte die Stack-Entwicklung zwei Skills für drei Phasen und koppelte jeden Phasenwechsel an
einen Modellwechsel mitten in der Sitzung: stack-dev umfasste Design und Bau und bot am Übergang
/model sonnet an, stack-close bot /model opus an und wartete auch noch auf CI. Der Wechsel
fand in der Praxis nie statt (#50), er hätte den Prompt-Cache verworfen, und die Bauphase passt
nicht in Sonnets Kontext (#151 B lief in die Kompaktierung). Ein roter CI-Lauf machte außerdem die
Abschlussphase wieder zur Bauphase.
Jetzt hat jede Phase ihren Skill, und übergeben wird über einen Zustand im Tracker statt über den Kontext einer Sitzung:
stack-devist der Design-Skill und bleibt der automatische Einstieg. Er endet an einem Body, der ready ist (instructions/dev/issue-tracking.md§ Ready to build), und nennt dem Betreiber/stack-build #N.stack-build(neu) prüft zuerst, ob der Body ready ist, baut, bumpt, zieht die Dokumente nach, publiziert und wartet auf grünes CI. Den Body pflegt er an drei festen Stellen: bei einer Abweichung, nach dem Publish, bei grünem CI.stack-closeprüft den Endzustand des Bodys und veraltetedocs/- und Contract-Prosa, dann schließt er das Issue. Der Schließkommentar nennt pro Phase Modell, Effort, Sitzungsgrenze und Kontextüberlauf, dazusize/.
stack-build und stack-close tragen disable-model-invocation: true. In Claude Code kann sie
deshalb nur der Betreiber starten, und jeder Phasenwechsel ist ein echter Halt, an dem er über
Weitermachen, /clear oder ein anderes Modell entscheidet. Keiner der drei Skills bietet noch
einen /model- oder /effort-Wechsel in der Sitzung an. Laut Anthropics Doku zum Prompt-Caching
invalidiert auch eine Effort-Änderung den gecachten Gesprächsverlauf, daher wird auch dort an
einer Übergabe geschnitten statt umgeschaltet.
Gemeinsames steht je einmal in eigenen Instruktionen: die Mode-Regeln, die Phasentabelle und der
Katalog der Dev-Verfahren in instructions/dev/stack-mode.md, die lokalen Checks, publish und
das CI-Warten in instructions/dev/publish-and-ci.md. Die Notiz, die publish nach einem
Stack-Publish druckt, sagt jetzt, dass CI noch kommt und erst danach die ungeprüfte Strecke
beginnt. docs/model-and-effort-selection.md beschreibt die Modellwahl pro Sitzung und nennt
Sonnet nicht mehr als Standard für die Bauphase.
publish keeps a closing trailer block of --message last (#149)
publish appended its Files changed: list to the end of --message. git reads trailers only
from a message's last paragraph, so a message ending in Co-Authored-By:/Claude-Session:
lines ended up with the file list as its last paragraph, and git log --format=%(trailers),
git interpret-trailers --parse and the co-author display on Gitea and GitHub read nothing.
That held for every attributed commit publish made.
The list now goes in front of a closing trailer block. Whether --message ends in one is
decided by git interpret-trailers --parse --no-divider, not by a pattern of our own, so
continuation lines, (cherry picked from ...), the title rule and the share of trailer lines a
block needs are judged the way git judges them. --no-divider matches how git log reads a
commit: past a --- line. The list moves only when git reads exactly the same trailers from the
result as from --message. A message without a trailer block is committed byte-identically to
before. Neither gate token covers --message, so a --confirm or --confirm-rebase token
clears exactly what it cleared before.
Commits already on main keep their unreadable trailers: rewriting them would need a force-push.
tools/bugreport: the collector finds its own Python (#166)
The bug-report collector is the one part of the stack that has to run when nothing else does,
and it was started as python3 tools/bugreport.py. On the Windows target that name is the
Microsoft Store's alias in Git Bash, and in PowerShell python can be one too - so the report
failed exactly where it was needed.
It now has a launcher pair after the pattern of tools/wikitool: tools/bugreport (sh, for
Linux, macOS and Git Bash) and tools/bugreport.ps1, which PowerShell resolves the same string
to first. They look for a Python the way the preflight does - python3, python on PATH; on
Windows python, py -3, python3, with the registry's PATH added under PowerShell - and then
try the venv's. Each candidate is probed for 3.8 or newer before it runs anything, and nothing
under WindowsApps is ever started. One that is too old, or that the machine refuses (a venv
python.exe blocked by Defender), is passed over. Finding none, the launcher exits 1 and says
why, naming any Python it found too old and the direct call with a full path as the way out.
Neither launcher assumes the preflight, .wikitool-tools.json or the venv, and the PowerShell
one keeps to what Windows PowerShell 5.1 understands, so a report never fails on the shell's
version first.
instructions/bug-report.md, INSTALL.md § Troubleshooting, the stage-2 line the collector
prints and the CI step that runs it in the distribution all use tools/bugreport. The shell test
for shipped instructions now refuses a bare python3/python/py call in a command block.
Starting bugreport.py directly still works.
INSTALL.md held to the installation instructions (#154)
The installation procedure has one source, the instructions under instructions/; INSTALL.md
is the human guide beside it, in the instance's language, and since #153 it no longer retells
the steps. One shared file was rejected (D12): an agent reads every sentence as an instruction,
and the two readers need different things. What the two still share is two enumerable lists,
and both are now checked by docs verify:
- Prerequisites.
INSTALL.mdcarries one generated region per platform value oftools/prerequisites.txt-<!-- wikitool:prerequisites -->for every platform,<!-- wikitool:prerequisites-windows -->for Windows only - rendered as label and minimum version, without the manifest's English reason field. The newwikitool docs prerequisites [--apply]rewrites them; it never places a missing region, since where a list belongs is the human guide's decision, and reports it instead. A tool added to the manifest failsdocs verifyuntil the region is regenerated. - Setup questions. Every place
instructions/setup-instance.mdasks the user something carries<!-- setup-question: <key> -->(eight today:identity,remote,kb-language,domain,personalization,environment,telemetry,task-tracker), and the matching bullet inINSTALL.md§ "Was der Agent dich fragt" carries the same marker.docs verifycompares the two sets in both directions. Markers rather than a frontmatter list: they sit where the question is asked, visible to whoever adds the next one, and the instruction schema stays closed.
The prose that no check reads is session work. instructions/dev/doc-pull-through.md gains rows
mapping the installation instructions to INSTALL.md and dev-setup.md to DEVELOPMENT.md,
applied before the publish; stack-close step 3 reads the pair again after it and files a
deviation as a follow-up issue.
Installation only from a release, into an empty folder; upstream merge/verify and the other install paths removed (#153)
The install run analysed in Gitea #140 failed on an instruction that contradicted itself, and the
path it took (Weg D, a private clone with this repo as upstream) was one of four. All four were
cut down to one (D2, D3): an instance is installed from the latest release into an empty folder,
dist export is a build tool, and a clone of this repository is development. None of the removed
paths was in use, so the break has no transition.
- Removed.
wikitool upstream mergeandupstream verify(commands/upstream_cmd.pyand its tests, the "Private instances" group of command records) andinstructions/private-instance.md. Every reference in the shipped tree goes with them;ownership.is_stack_ownedstays, becausedist exportuses it. The 3.0.0 migration document dates its reference in words. The Publish-Remote Gate stays: any checkout with two remotes needs it, and only its rationale lost the reference to the removed path.gates.mdloses its section onupstream mergebypassing the Mass-Update Gate. - The install path.
instructions/setup-instance.mdstarts at the release: it is attached to every release as an asset, together withinstructions/preflight.md(E1), and the installation sentence inINSTALL.mdpoints the agent at both through the API'sreleases/latest- Gitea 1.26 has no stable "latest" download link. Step 0 downloads the preflight asset with the shell's own command (E2, so no Mark of the Web) and runs it with the bypass prefix. The export step is gone,git initruns only where there is no repository yet, and an empty clone keeps itsorigin. The steps are renumbered; references to the personalization step name it rather than its number. - The preflight asset installs in place (E6, changes D40). It installs into its own folder,
which has to be empty apart from the script and a
.git, unpacks into a temporary folder inside it, moves the stack up and removes itself, so the first commit carries the stack and nothing else.--intoinstalls elsewhere under the same rule. A non-empty target is refused with exit 1. Tests cover both shells, an empty clone, and the self-removal. Under bash the folder-too-long guidance printedC:\\Chemenuwith a doubled backslash; it now prints one. wikitool dist adopt(E3). Copieskb/*/COLLECTION.md.templateandtypes/*.templateto their unsuffixed names, never over an existing file. It replaces thefor … cploop insetup-instance.md, the twocplines inupgrade-instance.mdand the loop in the CI replay.- Shell-neutral instructions (E4). A command block in a shipped instruction is a
tools/wikitoolorgitcall or the preflight's own call;instructions/CONTRACT.mdstates it, andtests/test_instructions_shell.pyholds it, with two exceptions of one line per shell: the session id (D26) and the preflight download (E2). Migration documents are out of scope. The same test checks that every PowerShell preflight call in a shipped file carriespwsh -NoProfile -ExecutionPolicy Bypass -File.session-setup.mdloses the bash-only inline form and the$(date +%s)id;upgrade-instance.md,ingest-large-tree.mdandmcp-read-server.mdfollow, as does the hintwork newprints. - Session id under Copilot (E5). Neither Copilot CLI nor Copilot's agent mode in VS Code sets
a session variable (checked against their documentation), so
session.HARNESS_ENV_VARSis unchanged.setup-instance.mdhas the agent setWIKITOOL_SESSION_IDwith the line for its shell beforedoctor, andsession-setup.mdsays why. - CI. The replay builds a release tarball the way
release.ymldoes and starts the preflight asset with--archivein an empty folder (D41). It no longer asserts the launcher's exit 42 before the preflight - the asset runs the tree preflight itself, and the launcher's refusal is tested intest_preflight.py.release.ymlattachessetup-instance.mdandpreflight.mdfrom the exported tree. - Docs.
INSTALL.mddescribes one path: what has to be there first (Windows: PowerShell 7,RemoteSigned, Git for Windows, a folder of at most 95 characters), the sentence for the agent, the questions it asks, and what to do at every stop of the preflight in both modes.DEVELOPMENT.mdgains the development checkout,dist exportas a build and test tool, and the private release feed that used to sit inINSTALL.md;instructions/dev/dev-setup.mdis its agent-side counterpart.bootstrap.mdis for a further checkout of an existing instance (D11).docs/ownership-and-templates.mdrecords why an instance comes only from a release.
Windows-Portabilität: Pfadtrenner, Zeilenenden, Encoding und Locks
The Python package assumed POSIX in several places that nothing on Linux would ever reveal
(Gitea #152, part of #140). In the run analysed in #140, instructions verify failed on all 23
instructions on Windows. This changeset makes the package behave the same on Windows, and holds
it there with guards that run in the ordinary Linux CI.
- Path separators. Every
str(<path>.relative_to(...))is now.as_posix(), as are the error messages that printed a relative path. On Windows these strings came out askb\x.md. They were then compared with POSIX keys or stored. The type-spec self-reference check intype_resolver.pyis one of them, and it failed every validation. - ripgrep paths.
rg --jsonwrites\on Windows, and--path-separator /does not reach its JSON output (measured on the target system, T3).search/ripgrep.pyconverts the separator where it parses a match, and does so only whenos.sepis\. Nothing is lost: no Windows path component can contain\, and since #155 no page title can either. - Line endings. A new
.gitattributes(* text=auto eol=lf) keeps every text file LF in a checkout withcore.autocrlf=true. Without it the sh launcher gets CRLF and Git Bash fails withenv: 'bash\r'.raw/andincoming/are-text, so a source is stored byte for byte as it arrived.dist exportships the file. The index was LF throughout already, so renormalizing changes nothing. Every text write now passesnewline="\n", so the byte comparison of published skill copies and the per-file sha256 indist upgradeagree on Windows too. - Decoding. Every
subprocesscall withtext=Truenamesencoding="utf-8". Without it, Windows decodesgitandrgoutput in the locale's code page (cp1252). - wikitool's own output.
tools/run_wikitool.py, the file both launchers run, sets stdout and stderr to UTF-8. Python on Windows writes into a pipe in cp1252. Measured on the target system, PowerShell decodes the output of a child process with[Console]::OutputEncoding. Under Copilot that is UTF-8, and Git Bash passes bytes through unchanged. Before this changedoctorshowedFu�notenunder both harnesses. A console is unaffected either way. - Hook payloads.
trace_ingest.pyreads its stdin as UTF-8 bytes. A locale-decoded read failed outright on Windows when a payload contained a character such asŁ, whose UTF-8 form holds a byte that cp1252 leaves undefined. - Locks. The budget counter and the telemetry writer locked with
fcntland silently skipped the lock where it does not exist. Parallel calls on Windows could then lose a budget increment. The new modulechemenu/filelock.pyis the only one allowed to importfcntlormsvcrt. On Windows it locks one byte far past the file's data withmsvcrt.locking, because a Windows lock is mandatory and a lock on the data would blocktrace_ingest.pyfrom reading a trace. It waits for a contended lock the wayflockdoes.
New tests: tests/test_portability.py reads the source of tools/chemenu and the scripts
beside it. It fails on a stringified relative_to, on a text open/read/write without
encoding=, on a text write without newline=, on text=True without encoding=, and on an
fcntl/msvcrt import outside filelock.py. Each detector also gets the defect it exists
for, so a guard that matches nothing cannot pass. The fcntl fallback is tested with the
import hidden and a fake msvcrt. A real Windows lock is not exercised, because CI runs only
Linux. The UTF-8 output and stdin are tested under PYTHONIOENCODING=cp1252, which simulates
the Windows pipe. Further tests feed the T3 JSON line through search/ripgrep.py with a
simulated Windows separator and check .gitattributes with git check-attr. Whether doctor
shows Fußnoten under Copilot and Claude Code is checked by hand on the target machine.
trace-hook.ps1: Copilot hooks no longer open Windows' choose-an-app dialog
On the Windows target machine, Copilot opened Windows' "choose an app" dialog for trace-hook
on hook events (Gitea #164). Some PowerShell had run ./tools/trace-hook ..., and an
extensionless sh script has no program associated with it. The bash field of
.github/hooks/wiki-trace.json was not the path in: both Copilot clients take the powershell
field on Windows. Copilot CLI, however, also reads .claude/settings.json, whose
UserPromptSubmit hook has only a command, and it runs that command under PowerShell on
Windows.
tools/trace-hook.ps1 is the PowerShell twin of tools/trace-hook, after the
wikitool/wikitool.ps1 pattern: PowerShell on Windows resolves ./tools/trace-hook to the
.ps1 first. It does what the sh script does: it runs trace_ingest.py with the venv's Python
in either layout, records nothing without a venv, and exits 0 whatever happens. It has no
#Requires -Version 7, because VS Code starts hooks under Windows PowerShell 5.1. No hook
command string changed, so Linux, macOS and Claude Code under Git Bash behave exactly as
before. The preflight's and doctor's Mark of the Web check and CI's PSScriptAnalyzer step
already cover every .ps1 under tools/.
New tests: one guards that every extensionless hook target has a .ps1 twin. The pwsh tests
cover arguments and stdin reaching the venv Python, silence without a venv, and exit 0 when
the Python fails. Whether the dialog is really gone in both clients is checked by hand on the
target machine.
preflight.ps1: the asset-mode error helper is Exit-Asset, so PSScriptAnalyzer passes
The helper that ends asset mode with exit 1 was called Stop-Asset. Stop is one of the verbs
PSScriptAnalyzer reads as changing system state, so PSUseShouldProcessForStateChangingFunctions
failed the CI pwsh job on the previous bump. It is now Exit-Asset, beside Exit-WithGuide;
behaviour is unchanged.
Preflight as a release asset: download, verify and unpack the stack, then run the tree preflight
Section C of #151, the first install. Until now a new user had to get the stack onto the machine
before any preflight could run - which is exactly the step that fails on a machine without git
or a short enough path. Each release now also attaches preflight.sh and preflight.ps1 as
assets, and a script without tools/prerequisites.txt beside it runs in asset mode: it
downloads the release tarball and its .sha256, stops with exit 1 unless the checksum matches,
reads the folder limit from tools/prerequisites.txt inside the archive, and unpacks into
chemenu/ next to itself (--into <path> for another place). It unpacks into a temporary
sibling and renames, requires exactly one top-level folder, and refuses an existing target
without touching it. Then it runs the unpacked tree's own preflight, passing --set and the exit
code through, so everything after the unpack is the tree mode that already existed.
The asset copies are made by release.yml: it writes the same release's tarball and checksum
URLs into two placeholder lines of each script, checks that the substitution took, and uploads
the two files under exactly those names. The tree copies keep the placeholders empty; an asset
script with empty placeholders and no --archive exits 1 saying it does not come from a release.
--archive <tarball> uses a tarball already on disk (its <tarball>.sha256 must sit beside it)
for a machine that cannot download, and is the entry point the tests use.
On POSIX asset mode needs curl, tar and sha256sum (or shasum) and stops with exit 42 and
the usual guidance block when one is missing; the PowerShell script uses what Windows 10 and 11
ship (Invoke-WebRequest, Get-FileHash, tar.exe) and stops the same way only when tar is
gone. On Windows with long paths off, the 95-character folder limit is judged at the final
target before anything is unpacked, so a too-long --into stops with exit 42 and the fix is a
shorter folder. Git Bash gets the target and archive converted with cygpath -u, because GNU
tar would read C: as a host.
The tests build a release tarball with a checksum and run both scripts against it, including a
local HTTP server for the PowerShell download, the 95/96 boundary, a failed checksum, two
top-level folders, an existing target, and a test that ties the placeholder lines to the sed
expressions in release.yml. tools/CONTRACT.md, tools/README.md, README.md,
instructions/preflight.md (a new decision point for the asset script),
instructions/dev/testing-conventions.md and INSTALL.md describe the first-install route.
PowerShell 7 preflight and launcher: tools/preflight.ps1, tools/wikitool.ps1, doctor checks for execution policy and Mark of the Web
The PowerShell half of #151. Harnesses that run in PowerShell 7 on Windows (GitHub Copilot CLI,
for one) resolve tools/wikitool to tools/wikitool.ps1 before the sh launcher, so without it
the call ended silently. tools/wikitool.ps1 does what the sh launcher does: it stops with
exit 42 until the preflight has written a complete .wikitool-tools.json, accepts either venv
layout, and passes the CLI's exit code through. There is deliberately no .cmd.
tools/preflight.ps1 (#Requires -Version 7) answers the same questions as preflight.sh
from the same tools/prerequisites.txt and writes the same file, byte for byte - CI compares
the two. It is always started as
pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1: the bypass holds for that
one process, changes no setting, and lets the script report a Mark of the Web on itself. On
Windows it also reads the machine's PATH from the registry, so a session that inherited an
old one still finds a tool installed since, and it never runs or records the Microsoft Store
alias. What only it checks: the effective execution policy (Restricted or AllSigned is a
stop; when a group policy sets it, the output says that only the administrator can change it
and points at Git Bash) and any *.ps1 under tools/ carrying a Mark of the Web from the
internet zone, with the Unblock-File line that fixes it.
doctor gains execution-policy and script-marks (Windows only, OK elsewhere). The
launcher's STOP text, toolpaths.PREFLIGHT, doctor's fix line, the missing-dependency message
and dist export's summary name the PowerShell call beside the sh one. bugreport.py starts
wikitool.ps1 with the same bypass, so a blocking policy shows up as a doctor finding instead
of stopping the report. instructions/preflight.md carries both calls, the --set form and the
two new decision points; INSTALL.md has the Windows prerequisite and troubleshooting for the
policy and the Mark of the Web.
The tests run against the same stub machine as the sh ones and skip without pwsh. CI gets a
pwsh job in the new image chemenu-ci-pwsh (.gitea/pwsh-ci/, built by
pwsh-ci-image.yml, monthly and on change): PSScriptAnalyzer over tools/*.ps1, both
preflights compared, tools/wikitool started from pwsh, and the PowerShell tests.
Preflight: prerequisites checked and tool paths recorded before wikitool runs (#151, POSIX half)
The Windows install that prompted this found Python missing, then rg, and the agent worked
around each gap instead of stopping. tools/preflight.sh is now the one way a checkout gets
set up. It checks the tools tools/prerequisites.txt lists (Python 3.11+, git, ripgrep, and
PowerShell 7 on Windows) and records each one's absolute path in .wikitool-tools.json, which
is gitignored and per checkout. It then creates tools/.venv from the recorded Python with
-m venv and -m pip. Anything missing, too old or unusable ends in exit 42 with a numbered
block for the user: what, why, the command that fixes it, what next. --set <tool>=<path>
takes a path the user names. It is POSIX sh because it has to run before Python is known to
exist, under dash, bash and Git Bash. On Windows it tries python, py -3, python3 in that
order, never runs a Microsoft Store alias, and records sys.executable. With long paths off it
refuses an install folder over 95 characters, the other half of the 160-character path budget.
tools/wikitool is POSIX sh now as well. It stops with exit 42 until the preflight has written
a complete file, reads both venv layouts, and starts Python through tools/run_wikitool.py
instead of PYTHONPATH. Every git and rg the package starts goes through
chemenu.toolpaths, so it comes from the recorded path rather than the session's PATH. A
path that has gone is an ERROR line naming the preflight, not a traceback. doctor gains
tool-paths and install-dir. The harness hooks start trace_ingest.py through
tools/trace-hook, under the venv's Python, because the script's python3 shebang is the
Store alias in Git Bash. The CI workflows, bootstrap.md, setup-instance.md and
upgrade-instance.md run the preflight instead of their own venv steps. The rules an agent
follows around it are in instructions/preflight.md. The PowerShell half and the download
mode for a first install follow in the same candidate.
raw/CONTRACT.md points at the path budget for a name accepted from incoming/
The path budget's close-out review found the raw stage contract silent on it, although raw accept now refuses a name that would put the file over it. raw/CONTRACT.md § Getting a file in
says so in one sentence and links the rule in kb/CONTRACT.md rather than restating it.
Path budget: a file's path stays at 160 characters or fewer so a Windows checkout works without long paths (#163)
Windows counts 259 characters for a whole path, the install folder included, and the target system
has long paths off. A page title long enough to be a sentence made a checkout fail there, in a
place nobody would look. The path of any file below the instance root now has a budget of 160
characters, counted in UTF-16 code units the way Windows counts (an emoji takes two); the folder
limit that doctor and the install preflight enforce is the other half of the same sum.
new (every root), rename (--to only, also under --dry-run), move (a single page, and
--reconcile, which skips and names the target as it does for an occupied one) and raw accept
(the remedy is renaming the file in incoming/; incoming/ and raw/ stay unchanged) refuse a
longer path with exit 1 before writing anything. lint reports existing files over the budget
under a new advisory finding, Long Paths, for kb/ and raw/; it is not a hard error, so
--fail-on-error does not start failing a corpus that predates the rule. The fix for an existing
page is wikitool rename, and --from is never checked, so renaming away from a long title works.
The measurement is titles.path_budget_problem, a pure function beside the title rules, and
kb/CONTRACT.md § Titles are identifiers carries the normative statement.
setup-instance step 15 names --no-push for a local-only first publish; gates.md and a docstring follow #159
The close-out review of #159 found three places the change had not reached. setup-instance.md
step 15 shows the first publish without --no-push and announces exit 42; a local-only instance
now gets exit 1 there, before the gate, so the step says so and names the flag. gates.md said
the Mass-Update Gate counts "working-tree changes before publish stages them", which no longer
describes a gate that stages into a scratch index; it now says the uncommitted changes publish is
about to stage. The _reconcile_summary docstring still named "no remote" as an outcome with no
message.
publish: the gate lists the staged state; a missing or unreachable remote stops before the commit (Gitea #159)
The Mass-Update Gate counted a path twice. collect_changes read git status --porcelain, which
reports the index and the working tree separately. A path staged as deleted that sits in the working
tree again (git rm -r raw, then git restore --source=HEAD -- raw/CONTRACT.md) appears as D and
??; the gate counted both, while git add -A cancels them out and the commit held neither. The list
a human approved therefore named a deletion and a new file that were never committed. collect_changes
now stages into a scratch copy of the index (GIT_INDEX_FILE) and reads git diff --cached --no-renames
from it, so the list is the state the commit will hold. The real index and the working tree stay
byte-identical, also when the computation fails; git add writes the new blobs into the object store,
where gc collects the unreferenced ones.
- The digest in the
--confirmtoken is now the blob id of the staged content instead of a sha256 over the working-tree file, so the token binds to exactly what is committed. A deletion still has none. - A rename is listed as its old path deleted plus its new path added, which is what the commit holds and
what the "deletions by name" note has to see. The
renamedstatus is gone from the scale line. _numstat,_untracked_stat,_changed_files,parse_porcelain_entriesandparse_porcelain_zare removed; nothing else called them. The "Files changed:" list in the commit message comes from the same list and is correct for the same reason.- With
--path, the list is restricted to that subtree even when more is staged, as the commit is.
One message for two states became three. A failed fetch was reported as "No remote configured, or
origin could not be reached". It is now no-remote, remote-lacks-branch (the remote answers and has no
such branch yet - the first publish of an instance) or unreachable, told apart by git remote get-url
and the exit code of git ls-remote --exit-code, each with its own message. sync exits 0 in all three.
publish stops before the commit when it cannot publish. Without --no-push, unreachable and
no-remote end the call with exit 1 at the reconcile - before the gate, git add and the commit, and
also on a clean tree, where it used to say "Nothing to commit". It used to commit and fail at the push,
which left a commit that only a hand-made git push could send. The messages name --no-push as the
way to a local commit; the next publish that reaches the remote sends that commit along. remote-lacks-branch
is unaffected, so the first publish of an instance still commits and pushes. The Publish-Remote Gate,
which runs first when .wikitool-remotes.json exists, is unchanged, and so is the retry after a rejected
push: a remote that has become unreachable by then reports the original push error.
This is a breaking change in the sense of the version model: an offline session, or an instance that
stays local, has to pass --no-push on every publish. No content changes, so **Migration:** stays
"none required". instructions/publish-cycle.md has the new decision point, setup-instance.md step 4
and INSTALL.md say what a local-only instance now sees without the flag.
bug-report instruction: step 1 no longer calls every bundle unpseudonymised
Step 1 told the agent to announce that the bundle "is not pseudonymised" and then, one paragraph
later, to offer --pseudonymise. It now says the bundle holds machine, user and path names and the
git remotes unless it is pseudonymised.
Bug-report collector can pseudonymise identities, in two stages (Gitea #158)
A bundle carries machine, user and path names, and an installation failure usually turns on the
shape of those names, not on the names. tools/bugreport.py --pseudonymise therefore replaces
each identity by a placeholder of the same shape and leaves the structure alone. It is opt-in,
costs one more step, and the instruction recommends it for every channel except a direct handover
to the maintainer over a secure channel.
- Stage 1 is mechanical. The script reads user,
USERDOMAIN/COMPUTERNAME, hostname, home and repository path,git config user.name/user.emailand the remote URLs, and replaces each word of them in every text file, in raw and JSON-escaped form. A placeholder is an HMAC-SHA256 of the lowercased word under a per-bundle random salt, so it keeps length, digit/ASCII/non-ASCII class and per-occurrence case, is injective through a counter, and differs between two bundles. Words of the stack's own vocabulary, top-level domains and the public origin stay readable. Matching is whole-identity, longest first, on word boundaries. - Stage 2 is a model's judgement, applied by the script. The agent reads the review list plus
CHRONOLOGY.mdandMANIFEST.mdin full, and trace and transcripts in full only under 100 KB per file, and names further people, companies, customers, internal hosts and projects in a candidate file.--bundle DIR --candidates FILEapplies them with the same machinery and packs the zip again; the model replaces nothing itself. It refuses with exit 1 and an unchanged bundle when the mapping is gone, and can be repeated. - Three local files - the mapping with its salt, the review list and the candidate file - sit beside the bundle directory, never inside it and never in the zip. All three hold originals.
MANIFEST.mdnames three privacy states (none, stage 1, stage 1 and 2) and lists the placeholders, never an original. The closing output and the instruction name the residual uncertainty: stage 2 can miss a name in free text it did not read in full.instructions/bug-report.md,reports/CONTRACT.md,tools/README.mdandINSTALL.mddescribe both stages and the three files; CI runs the collector with--pseudonymisefrom the exported distribution and checks that the mapping is beside, not in, the bundle and that the host name is gone.
reports/CONTRACT.md: only the collector's two counting calls take the bugreport session id
The contract said all of the collector's wikitool calls run under bugreport-<stamp>. Only
instructions verify and docs verify do; version show, doctor and budget status inherit
the caller's id, as gates.md and the script already said. INSTALL.md's troubleshooting entry
also no longer claims page titles stay out of the whole bundle: they stay out of what the script
generates, while the trace - included by default - may carry them.
bug-report instruction: offer WIKI_TRACE=1 for a reproduction
A distributed instance records no trace by default, so the default "trace in" often had nothing to
include. instructions/bug-report.md now offers, when the failure can be reproduced, to repeat the
failing step in one new shell with WIKI_TRACE=1 set for that session only - never by changing
the checkout's telemetry configuration.
Bug-report collector: tools/bugreport.py and instructions/bug-report.md (Gitea #157)
A failure on one machine used to reach the maintainer as a description. tools/bugreport.py now
collects the first round of answers into one bundle, and it does so when wikitool itself does
not start: it uses the standard library only, imports nothing from chemenu, and keeps to
Python 3.8 syntax.
- Four layers. The environment (OS, every Python and shell found, harness,
PATH, git configuration, venv, line endings, on Windows also long paths, execution policy and mark-of-the-web), the stack (VERSION, the.wikitool-*.jsonfiles, git status and log, the shape ofkb/andraw/with path lengths and names that break on Windows), verbatim output ofversion show,doctor,budget status,instructions verifyanddocs verifyplus the caller's session trace, and the agent's chronology with any transcripts. The result isreports/bugreport-<UTC stamp>/and a zip beside it. - Secrets are always removed; page titles are kept out of everything the script generates
unless
--titlesis given. Environment variable names are all recorded, values only for a fixed list. The trace, the chronology and the transcripts are marked in the manifest as possibly containing page content and titles. Nothing is uploaded. - Its own session id. The two counting calls run under
WIKITOOL_SESSION_ID=bugreport-<stamp>, so collecting a report neither spends nor is refused by the caller's budget.gates.md§ "Taking a new session id" names this as the second permitted case, for that script alone. instructions/bug-report.md(manual: true) is the agent's side: what to tell the user, a fact-only chronology template, and the rule to stop before sending anything.setup-instance.mdandupgrade-instance.mdoffer it at a failure that has no obvious cause.reports/CONTRACT.mdnames the bundle as a third kind of output,tools/README.mdexplains why the script sits beside the package,INSTALL.mdhas a troubleshooting entry, and CI runs the script from the exported distribution.
Demo corpus follows the decided project pages: three states, seed with their items (Gitea #156)
The first build of the live tracker suite shipped two demo project pages whose names came from a
draft seed rather than from the decision the operator had made. This replaces them with the
decided three, one per state: the weekly review treats differently:
Chemenu 8.0.0 - Installation und Windows (active), Aufgabenverwaltung mit Tracker-Anbindung
(completed) and Reproduktionslauf des Korpus (dormant).
- Seed backup. Rebuilt by starting Super Productivity on the three projects, filing the
items through wikitool's own writer and keeping the backup the app then wrote itself: eight
open items in the active project, one of them
waitingwith an overdue follow-up, and one someday item.MANIFEST.jsonnames the one edit made after the app wrote it - the REST API cannot file a backlog item, so that item was moved there in the backup. - Recorded answers re-recorded against 19.1.0 from the new seed.
test_sp_recorded.pynow checks the demo projects too - counts, the waiting item and its date, the someday item - on both read paths. reviewagainst the seed reportswaiting_overduefor the waiting item and stays quiet on the completed and the dormant project, which is the join the demo exists to show.
Live tracker suite: WIKITOOL_TASKS_CONFIG override, real-tracker tests for Super Productivity and CalDAV, nightly workflow and test image (Gitea #156)
The task-tracker adapters were only ever tested against fakes, which is how #162 stayed hidden.
This adds a suite that runs the documented task new / task list / task close / review
workflow against a real tracker, and the machinery to run it on a clock.
WIKITOOL_TASKS_CONFIGnames the tracker configurationtask,reviewanddoctorread instead of.wikitool-tasks.json, so one checkout can be run against several trackers in turn. A set variable that names no file is an error naming the path - never "no tracker configured". Listed in INSTALL.md's variable table and in the affected command records.- The
live_trackersuite (tests/test_tracker_live.py,tests/tracker_live.py) writes only into a project namedChemenu Live-Test, prefixes every item with the run id, deletes nothing, and aborts before the first write when the marker project is missing. It skips without a tracker;CHEMENU_LIVE_REQUIREturns a missing one into a failure. A tracker of your own is named by.wikitool-tasks.d/<name>.json(gitignored, absolute paths only) andCHEMENU_LIVE_PROFILE. - Headless Super Productivity (
tests/sp_headless.py) starts the packaged app on a seeded profile, accepts its startup restore dialog over the DevTools protocol and refuses to start when something already answers on the fixed API port. - Recorded fixtures (
tests/fixtures/sp/) hold real answers of v19.1.0 and a backup the app wrote itself;test_sp_recorded.pyreplays them in the default run, andtests/record_sp_fixtures.pyrecords them again. - CI.
ci.ymlruns the CalDAV half against a Radicale process on every push.tracker-live.ymlruns both providers nightly inside thechemenu-sp-liveimage, whichsp-live-image.ymlbuilds daily when the update channel has a new Super Productivity version and monthly regardless. The image follows the channel rather than a pin, because installed desktop clients update themselves. - Docs.
instructions/dev/tracker-testing.mdhas the profile procedure per tracker, when an agent dispatches the nightly run, what a red night means and how to refresh the fixtures;testing-conventions.mdnames the live suite as the one deliberate exception to the hermetic default.docs verifygained an ignore canary for.wikitool-tasks.d/. - Demo corpus.
kb/gtd/technik/gains three project pages taken from this repository's own work, one perstate:the weekly review treats differently (active, completed, dormant); the seed backup carries matching projects with their items, soreviewhas something to join. The first build shipped two differently named pages; the entry below corrects them.
The Docker package chemenu-sp-live has to be linked to this repository once, by hand, after the
first image build.
Super Productivity API path: unwrap the {ok, data} envelope, exclude the inbox project, ready-aware health (Gitea #162)
Preparing the live tracker tests (#156) ran the real Super Productivity v19.1.0 headless for the
first time, and four assumptions about its local REST API did not hold. The test fakes had been
written from the same assumptions, so the suite stayed green while access: "api" could not read
anything.
- The envelope. The app answers
{"ok": true, "data": ...}or{"ok": false, "error": {"code", "message"}}; the adapter expected a bare list. Every read -review,task list,task new,task close- failed with "did not return a list of objects"._ApiClientnow unwrapsdata, raises with the API's own code and message onok: false, and refuses a body without the envelope instead of guessing. - The inbox.
GET /projectsand the backup snapshot both listINBOX_PROJECT, soreviewwould have reported "Inbox" as a tracker project without akb/page. Both readers and the writer's name-to-id lookup now drop it. health()requiresdata.rendererReady; an app whose renderer is still loading no longer counts as healthy, anddoctorsays so.- Closing a task sets
doneOnandmodified; the module docstring claimed it does not. The behaviour is harmless - the store fills them exactly as for a tick in the UI - the reasoning is corrected.
The fakes in test_superproductivity.py, test_task_cmd.py and test_new_page.py now answer in the real envelope,
and a test pins that a bare list is rejected. Drop-in in both directions; no .wikitool-tasks.json
change.
Page titles must form valid, unique file names on Windows and macOS (Gitea #155)
A title is the wiki's only identifier for a page and becomes the file name one to one, but nothing
checked that the name was usable outside Linux. A corpus written on Linux could not be checked out
on Windows (CON.md, A: B.md, a trailing dot) or collapsed two pages into one on macOS and
Windows (Foo.md and FOO.md, or the same accented title in NFC and NFD). The rule is now stated
once, in kb/CONTRACT.md § "Titles are identifiers", implemented as pure functions in
chemenu/titles.py, and enforced on every platform - a corpus written on Linux is read on the
others.
A title is refused when it is empty, contains one of < > : " / \ | ? * or a control character,
ends with a dot or a space, or starts - before its first dot, ignoring case and trailing spaces -
with a Windows device name (CON, PRN, AUX, NUL, COM0-COM9, LPT0-LPT9, including the
superscript digits) or with INDEX or COLLECTION, the two names the stack owns next to a page.
Two titles collide when their NFC-normalized, case-folded forms are equal. The full title,
including a type's title_prefix, is what is checked.
newchecks the title for every type and every root, then the collision against the corpus forkb/pages, then that the target file does not exist - all before anything is created, the tracker project included. This last check also fixes a data-loss bug found on the way: for theroot: repotypes,new instruction --name gatessilently overwroteinstructions/gates.md.newnever overwrites an existing file now.rename --tois checked the same way, also under--dry-run, with the page itself excluded so a case-only rename (FootoFOO) still works.rename --fromis deliberately never checked: it is how a page that is already invalid gets fixed.moverefuses a target that an existing entry claims under another case or normalization, for a single move and in--reconcilealike.lintreports existing violations under Unportable Titles, with the colliding paths named and awikitool renameremedy. The finding is a hard error at everykb_versionand is deliberately not migration-gated: there is no migration for it, sokb_versionnever advances on its account, and each affected page is renamed individually.
The bump is --major because a corpus that carries such a title stops passing lint --fail-on-error
after the upgrade; a demo/testbed corpus and the shipped instructions are clean. Uncertain and
refused conservatively: whether COM0, LPT0 and the superscript forms are device names on every
Windows version differs, so all of them are refused.
tools/CONTRACT.md is regenerated, and kb/CONTRACT.md, kb/CONVENTIONS.md and its template,
instructions/page-lifecycle.md, instructions/wiki-lint/SKILL.md and README.md carry the
rule.
dist upgrade --latest: one-command update from the release feed (Gitea #161)
Updating a tarball instance took a manual detour: version notes, then fetching the .tar.gz and
its .sha256 from the release page by hand, then dist upgrade <tarball>. dist upgrade --latest
now does the middle part. It asks the release feed (update_url from the stamp,
$WIKITOOL_UPDATE_URL and $WIKITOOL_UPDATE_TOKEN as before) which release is latest, downloads
the release's archive and checksum into a scratch directory, verifies the archive against the
checksum, and hands over to the existing upgrade path unchanged - classification,
--keep-local/--take-release, migration report and every refusal are the same code. <source>
becomes optional; exactly one of it and --latest must be given.
The order is what matters. The local preconditions run first, then the feed is asked, and the
version it reports is judged before any download: already installed is a success no-op, an
older version is a downgrade refusal, a -beta.N version needs --pre. Only then are the two
assets looked up - by exact name, chemenu-stack-<version>.tar.gz and its .sha256, in the
assets of the release object the version query already returned, using the feed's own
browser_download_url values; no URL is composed. A release missing either asset is refused
before the first download with the assets it does have and its release page named. The checksum
is mandatory on this path (a <source> archive without a sibling .sha256 is still only a
WARN), and the archive's own VERSION must equal the feed's version, otherwise nothing is
applied. --dry-run downloads and verifies too - classification needs the tree - and removes
everything again; the scratch directory goes away on every exit.
--expect <version> (only with --latest) closes the gap between reading the notes and applying
the update: the feed only offers its latest release, so if a newer one appeared since
version notes was read, the run refuses before downloading anything and names both versions.
instructions/upgrade-instance.md passes the version from step 2 in the dry run and in the real
run. The comparison is on parsed versions, so a v prefix does not matter.
$WIKITOOL_UPDATE_TOKEN is sent to an asset download only when the asset URL has the feed's
scheme, host and port, and as an unredirected header, so a redirect cannot carry it elsewhere.
There is no https enforcement: the checksum comes from the same host as the archive and protects
against transfer errors, not against a compromised feed - INSTALL.md says so.
The asset names are a Python constant (version.ARCHIVE_NAME) that a test ties to
.gitea/workflows/release.yml, so renaming them in one place fails the suite instead of the next
upgrade. dist upgrade is now network: yes in its record and in the pinned set in test_cli.py,
the version check record's "never reached implicitly" note names the one explicit exception, the
fetch_latest docstring says "three commands", and tools/CONTRACT.md is regenerated. The tests
run against a local http.server that logs every request and its Authorization header, so the
token rule and the "no asset request before the checks pass" rule are asserted on the wire. The
upstream merge pointers in the dist upgrade record and failure message stay as they are; they
belong to the separate work on the clone path.
new_page/type_resolver comments no longer claim only entities declare a layout:
Two code comments - new_page.py's module docstring and TypeResolver.get_layout's - still said
subtype-driven placement applied to "currently just entities", the same stale assumption behind
the new record's flat concept/source paths. They now name the four shipped type-specs that
declare a layout:. A test docstring that claimed to run new source's usage line verbatim
now says what the test does: it passes the fields that line names. Comments only.
new: concept/source record notes name their layout-computed subdirectory; source usage names its required fields (Gitea #150)
new's cli_contract record promised kb/concepts/<Name>.md and
kb/sources/Source - <Name>.md for the concept and source variants - flat, with no
subdirectory. Both type-specs have carried a layout: for a while (concept_type/source_type
picks the area, same rule an entity or a project already follows), so the actual path is
kb/concepts/<subdir>/<Name>.md and kb/sources/<subdir>/Source - <Name>.md. Nothing wrote a
page to the wrong place - new computes the path itself - but the record is exactly what an
agent reads to find one afterwards, and it was wrong: instructions/gates.md named two concept
pages by their old flat path (fixed in the gates.md entry further down) with the record itself as the plausible source of that
assumption. Both notes now name the subdirectory and where it comes from; comparison's note was
checked against types/comparison.md and left alone; it genuinely has no layout:.
Found in the same pass: new source's example line omitted source_type (required, no default
since #66) and fidelity/authority (enforced by new itself since #67), so copying it verbatim
always failed. It now names all three.
The new test in tools/chemenu/tests/test_new_page.py reads each variant's note out of the
record itself and turns it into the path pattern it promises, then checks what new actually
wrote against that pattern - coupled to the note text, not to a path re-typed into the test, which
is what let the existing per-type path tests stay green through this exact drift. A companion test
asserts every Writes \kb/...`variant has a case, so a future variant without one is caught here instead of silently going unchecked.tools/CONTRACT.mdregenerated viawikitool docs contract --apply`.
cli.py: removed a stale duplicate help-patch line outside the typer._click fallback's try (Gitea #148)
The typer._click.core.Command.format_help patch was applied twice: once inside a
try/except (ImportError, AttributeError) meant to let a future typer without typer._click
degrade to Click's own plain help instead of crashing every invocation, and once more on the next
module-level line, unconditionally. Whenever the try actually failed, that second line referenced
two names the failed import never defined and raised NameError at import time - the exact crash
the fallback exists to prevent, on every single wikitool call. Typer 0.27.2 still has the module,
so nothing showed it in practice; the line was pure dead weight until the day it wasn't. Removed,
so the patch is applied exactly where the try already applies it.
The new regression test drives chemenu.cli in a subprocess with a builtins.__import__ hook
that raises only for typer._click.core imported from chemenu.cli/__main__, then checks that
wikitool search -h still exits 0 with Click's own plain help. Two more direct ways to simulate
the missing module were tried and rejected: sys.modules['typer._click.core'] = None also breaks
typer's own lazy import of typer._click.decorators inside get_help_option, so -h fails
regardless of what cli.py does; importlib.reload(chemenu.cli) reruns the module in the same
__dict__, so the names from the first, real import survive and the broken line runs
successfully - a test built that way would stay green against the exact bug it exists to catch.
gates.md and a run_budget comment name kb pages by title, not by a path that moved
instructions/gates.md pointed at kb/concepts/Mass-Update Gate.md and
kb/concepts/Iteration and Cost Limits.md, and a comment in run_budget.py at the second - both
pages live under kb/concepts/workflows/, where types/concept.md places a workflow concept. A
page's title is its only stable identifier, and its directory is whatever the type-spec computes,
so the three references now name the page by title and no longer carry a path. The comment also
says why the page still quotes the old 15-25 band: deliberately, as a sourced claim about the
field, next to the band measured here. Text only.
Budget gate and loop-breaker refusals exit without a traceback
cli.main() calls run_budget.record_and_check() before Typer ever dispatches to a subcommand,
so a refusal leaves through _util.fail()'s typer.Exit outside any Click context - nothing
caught it there, so the process exited 1 correctly but printed a Python traceback right after the
ERROR line (Gitea #147). That traceback sat exactly in the output AGENTS.md § Gates asks a
session to show a human and stop on; it reads as a crash rather than a gate and invites the retry
the message forbids (Gitea #52). main() now catches typer.Exit around that one call and exits
with its code directly, and two new subprocess tests - through the real python -m chemenu.cli
entry point, not the direct record_and_check() call the existing cross-process tests use -
pin that a tripped gate leaves neither stream carrying a traceback, still emits gate.refused,
and neither records a wikitool.call nor advances the session's counter for the refused call
itself. Both refusal messages, the budget command group's own help text, and one code comment
also pointed at "the tooling contract's 'Iteration and Cost Limits'" or "...Tool Error
Contracts" - neither section exists by that name - and now name AGENTS.md § Tool error contract
and instructions/gates.md § Iteration Budget Gate and loop-breaker, the sections that do.
CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them
A second task-tracker adapter, caldav (RFC 4791/5545), so an instance is not bound to Super
Productivity - built against Nextcloud Tasks with iOS Erinnerungen as the mobile client and
verified against a real account. TaskReader/TaskWriter are implemented in full without
tasks/protocol.py changing at all, proving the provider layer is genuinely exchangeable. The
mapping: a project is a calendar collection whose only supported component is VTODO; WAITING
is a waiting category; follow_up_at is DTSTART, never DUE; a project's created falls
back to the earliest item's own CREATED (no server in the test account returns
DAV:creationdate on a calendar); create_project uses a real MKCALENDAR call and so never
needs a human-clearance step the way Super Productivity's does; close_item changes only
STATUS/COMPLETED/PERCENT-COMPLETE/LAST-MODIFIED/DTSTAMP on the existing resource and
refuses on an ETag conflict; nothing is ever deleted. Every address used - a list's or an item's
- comes from the server's own
href, never built from the configured URL and a name, since the configured URL may be an alias for a different canonical path.
Alongside it, wikitool review's checks 2 and 3 no longer silently skip a value a provider
cannot supply - a WAITING item with no follow_up_at, or a tracker project with no
determinable creation date - and instead report it as its own finding
(waiting_no_follow_up/project_age_unknown), for every provider. icalendar is now a required
dependency (tools/requirements.txt), imported only when caldav is actually configured.
version bump no longer points at version release in its output
version bump used to end with "and version release once the candidate is ready to ship", and
an agent read that as its own next step and fixed a candidate into a release unasked. The hint
is gone: whether a candidate ships is the user's decision, as DEVELOPMENT.md already says for
humans, and instructions/dev/version-parts.md step 7 now says so for agents.
stack-close: wait for CI through the authenticated Gitea connection, with timings and a give-up point
Sessions kept improvising how to wait for CI before closing an issue, and one of those
improvisations - an anonymous curl loop against the Actions API, which answers 401 even for
this public repo - treated every error as "not finished yet" and never ended. stack-close step 2
now names the one way to do it: read the runs through the authenticated Gitea connection, first
check after about 4 minutes (5 with a release job), then once a minute, and hand back to the user
after 15. Any shell loop that polls instead must end on the first unexpected response. Dev-only;
nothing here ships to an instance.
wikitool: one data record per command - -h, index and CONTRACT.md render from cli_contract (Gitea #121 Phase 1)
--help/-h, the tools/CONTRACT.md command reference, and the run-time budget exemption list
used to be four hand-maintained copies of the same facts about a command - a docstring, two
tables in tools/CONTRACT.md (§ Commands, § Error contracts), and run_budget.py's own
SKIP_COMMANDS/SKIP_COMMAND_PATHS sets - and they had already drifted (xref add/
xref link-source were missing --dry-run from their documented synopsis; the tool error
contract's non-idempotent list disagreed with the per-command retry-policy cells it stood next
to). All 61 commands (60 existing, plus the new docs contract) now carry one
cli_contract.CommandRecord - name, synopsis, properties (effect, idempotency, atomicity,
budget, network, gates), exit status and retry policy - attached to the command function by a
@cli_contract.record(...) decorator in its own module. Three views render from that one
source: wikitool <cmd> -h (the full record, plain text), wikitool -h (an index line per
command, fixed-width and grep-stable), and tools/CONTRACT.md's generated
<!-- wikitool:commands --> region (wikitool docs contract [--apply]), which replaces the two
old tables. run_budget.is_exempt now reads a command's budget: property directly instead of
carrying its own list, which is what wikitool -h | grep non-idempotent/budget:exempt now
answers for AGENTS.md's tool error contract instead of a hand-written enumeration.
Help itself changed shape: Rich's boxed panels are off (typer.core.HAS_RICH = False) for both
--help and a usage error, so the output is the same plain, GNU-style text with or without a
TTY - byte-identical, which a new test pins by comparing a real run against one with isatty
patched. -h is now a recognised alias for --help on every command (no command used the flag
for anything else). wikitool docs verify grew three checks to hold the new machinery to the
same "checked or absent" rule as everything else it enforces: every registered command has
exactly one cli_contract record and appears exactly once in cli_contract.GROUPS, every
command's non-hidden flags match its record's SYNOPSIS in both directions, and no command's
rendered help - a docstring above its \f marker, or an option's own help= text - cites an
issue number (a distributed instance has no tracker to resolve one against, the same reasoning
check_no_issue_references already applied to shipped .md files).
Redactional work - examples, an explicit "never do this" section per command, and pulling the
"why" out of a record's NOTES into its own section - is Phase 2 (Gitea #142), deliberately not
part of this change: this pass moved the existing table cells' text into records mechanically,
without editing it beyond the one documented fix (the --dry-run synopsis gap above) and the
docstring/option-text rewording needed to stop citing issue numbers in rendered help.
wikitool: usage lines name wikitool, and the -h acceptance checks become tests
Under tools/wikitool - which runs python -m chemenu.cli - every usage line and "Try ... -h"
hint named python -m chemenu.cli, a command nobody should copy; the CLI now passes
prog_name="wikitool" explicitly. The properties the previous change promised for help output -
byte-identical with and without a TTY, -h and --help identical for every command, no Rich
frame characters in help or in a usage error, the top-level index listing at least the seven
non-idempotent commands - had only been checked by hand; they are now pinned in
test_cli.py.
dist export no longer cuts the dist export record out of the shipped tools/CONTRACT.md
dist export removes every dist:strip-start/dist:strip-end marker span from the files it
ships, and the dist export record's own notes quoted that marker pair literally - so the export
cut a piece out of the middle of the generated command region, and the fresh instance's own
docs verify then reported that region stale. CI's fresh-instance replay caught it on the first
push. The old table row carried the same quote and was mangled the same way; nothing compared the
shipped copy against its source until the region became generated. The record now names the
markers without spelling out the pair, and a new test pins that the export plan ships
tools/CONTRACT.md byte-identical to the working tree.
Command records, Git group: NOTES as bullets, one exit line per cause, examples and prohibitions
First pass of the editorial rewrite of the command records, starting with sync and publish.
Text only - no command behaves differently. The record model grew what the rewrite needs:
notes takes a tuple of present-tense bullets (a plain string, the old form, still renders as
one paragraph until every group is done), and Failure is now one cause with its own reaction
and exit code (cause, reaction, code 0/1/42) instead of one lumped "exit 1 means"/"retry
policy" pair per command. EXIT STATUS lists one line per cause; ON FAILURE repeats the cause
next to its reaction so each line reads on its own; an explicit exit-42 cause replaces the
generic gate line, and a record declaring one without a gate is refused at import. The field
rename is mechanical across all records, so every not-yet-rewritten command's ON FAILURE line now
reads <old exit-1 text> -> <old retry text>.
sync and publish now carry copyable EXAMPLES (including the re-run after exit 42), a NEVER
section (do not retry or force a failed git step, do not pass an unapproved token, do not edit
.wikitool-remotes.json past a refusal), SEE ALSO, and self-contained NOTES in place of "exactly
like sync". Sentences that only explained why left the records; each was already a comment
at the code that implements it. publish's atomic property says "every gate" instead of
"both gates" - it has three, and all run before staging. How a record's prose is written is now
stated once, in the CommandRecord docstring, and tools/README.md points there.
Command records, Catalog and log group: bullets, examples, prohibitions
index rebuild, log append and log status rewritten the same way; text only. log append
now lists its two exit-1 causes separately and states in NEVER what its retry policy said in
prose: check the tail of kb/log.md before re-running after an uncertain outcome.
index rebuild's NOTES name the nested-page warning and what --dry-run prints, both of which
the command already did. log status no longer claims it "reports 0" for a missing log - it
reports that nothing is logged yet, which is what it always printed.
Command records, Distribution and versioning group: one line per cause, examples, prohibitions
dist export, dist upgrade and the six version commands rewritten the same way; text only.
The long single-paragraph records - dist upgrade and version bump above all - are now one
bullet per behaviour, and each exit-1 cause carries its own reaction, taken from what the
command's own error message already tells the caller (migrate baseline, migrate status,
commit or stash, upstream merge). Three corrections to what the records claimed: dist upgrade
listed an equal source version both as a no-op success and as an exit-1 refusal - it is a
success, and EXIT STATUS now says so; dist export and dist upgrade gained the exit-1 causes
their code already had but their records omitted (a missing licence, an export plan leaking
instance data, a bad archive checksum); and version regrade's NOTES, garbled when they were
moved over mechanically, read as sentences again. Two code comments changed with them: the one
above dist export's raw//incoming/ anchors had claimed .gitignore drops incoming/,
which stopped being true when !/incoming/.gitkeep was added, and dist upgrade's stamp write
now carries the reasoning for writing the stamp whole after --keep-local, which used to live
in the record.
Command records, Pages group: one line per cause, examples, prohibitions
new, task new/list/close, touch, rename, rm and move rewritten the same way;
text only. new's long per-variant notes moved into NOTES bullets, leaving each variant one line
that says where the page lands, and new project's tracker cases - name taken, read-only access
path, a provider that cannot create projects (exit 42, cleared with --resume) - each got their
own exit line and reaction. Every "same posture as new project" and "same reasoning as
task new" in the three task records is replaced by the fact it pointed at. The records also
name failures the code already had and the old text left out: new's missing capture field and
its page-write failure after the tracker project was confirmed, and the partial-write failures of
rename, rm and move --reconcile. The reasoning behind task new/task close never exiting
42, and behind task close taking an id rather than a title, moved into task_cmd.py's module
docstring.
Command records, Links and citations group: one line per cause, examples, prohibitions
xref add/remove/link-source, links show and cite id/add/sync rewritten the same
way; text only. xref add's label refusal, previously described only in NOTES, is now an exit
line of its own, and its exit text says which page's type is checked for related: (A's - the
only one the code looks at). xref link-source states that a missing target is skipped while the
rest are still linked, before the run exits 1, and loses the history of the See Also bullet it
no longer writes. The two cite write commands and cite id carry AGENTS.md invariant 1's rule
on citation ids as NEVER, where an agent looking up one command finds it.
Command records, Finding and checking group: one line per cause, examples, prohibitions
lint, search and review rewritten the same way; text only. lint's single sentence
listing every check is now one bullet per category, with which findings are hard, advisory or
migration-gated stated per bullet; review's five checks are one bullet each. search splits
its four exit-1 causes, and carries AGENTS.md's "do not grep kb/ yourself" as NEVER next to
the scope rule that justifies it. One claim is deliberately left as it stood: lint's record
still describes the quote limit in blockquoted lines while the code counts quotes - which of
the two is meant is not this change's call to make.
Command records, Provenance group: examples, exit lines per cause
sources coverage, sources trace and sources rebuild-index rewritten the same way; text
only. sources trace separates an argument error from the uncovered-file finding it also exits
1 on, since the second is a result to act on rather than an argument to fix;
sources rebuild-index names kb/provenance.md as generated in NEVER.
Command records, Raw material and uploads group: one line per cause, examples, prohibitions
raw accept and the four upload commands rewritten the same way; text only. raw accept's
two paragraph-long variant notes became one line each, with the behaviour in NOTES bullets and
its two exit-1 lists split into seven causes. The name-occupied refusal now carries, in its own
reaction and in NEVER, what raw/CONTRACT.md already asks of an agent: show the message and
wait, since only the user can tell a new edition from a second source. upload accept states its
gate's shape itself instead of pointing at the Mass-Update Gate's.
Command records, Workshop runs and session budget group: examples, prohibitions
work new, work close, budget status and budget reset rewritten the same way; text only.
budget reset now carries AGENTS.md invariant 6's rule as NEVER - it is never run on an agent's
own initiative to get past a budget refusal - and work close states the caller's side of its
--yes: the run's conclusions are in kb/ first.
Command records, Types, instructions and docs group: one line per cause, examples, prohibitions
types list/describe, instructions sync/verify/list and docs verify/toc/contract
rewritten the same way; text only. docs verify's single sentence naming some twenty checks is
now grouped by what it checks (commands, collections, types, ignore canaries, shipped issue
references, tables of contents, links), with six exit-1 causes and a reaction each. docs toc's
"never fails on content" had been carried over as an exit-1 cause, so the index listed it as
exit:0,1; it is now a success line and the index says exit:0, which is what the command has
always done. The reasoning docs toc's record carried about its scope already lived in
toc.py's module docstring and now lives only there.
Command records, Telemetry group: examples, the missing --fail-on-error exit line
eval sessions and eval score rewritten the same way; text only. eval score --fail-on-error
has always exited 1 on a failed scorecard, but its record only listed the missing-trace case; it
now names both.
Command records, Content migrations group: one line per cause, examples, prohibitions
The five migrate commands rewritten the same way; text only. migrate done and
migrate baseline state as NEVER what their prose implied - never force the chain's order,
never advance the version with baseline --force or by editing .wikitool-kb.json - and
migrate verify separates a bad --from revision from the findings it exits 1 on. The reasoning
behind counting marker pairs rather than comparing their names, and behind an offered migration
ignoring the chain, moved into the verify and done docstrings.
Command records, Private instances group: one line per cause, examples, prohibitions
upstream merge and upstream verify rewritten the same way; text only. upstream merge's
single paragraph is now one bullet per step of the merge, and its exit-1 causes - including the
fetch failure, a failing git step inside the open merge, and the post-commit leak, which its old
record mentioned only in passing - each carry their own reaction.
Command records, Instance health group: one bullet per check, examples
doctor rewritten the same way; text only. Its one-sentence inventory of every check is now one
bullet per area, each stating which outcome is OK, WARN or FAIL. One stale reason was
dropped rather than moved: the record justified the conventions FAIL by xref/cite writing
out of the section headings, while the check's own docstring calls those headings cosmetic and
grounds the FAIL in the file binding every page.
Command records: NOTES is always a tuple of bullets; every record's examples are tested
With every group rewritten, CommandRecord.notes no longer accepts the single-paragraph string
it carried over from the first pass; a record that passes one is refused at import. Two tests
over the real registry hold what the rewrite established: every command has at least one
example, and every command with a gate shows how its clearance is passed back in (--confirm,
--confirm-rebase or --resume).
fail() prints the command's ON FAILURE lines on stderr
_util.fail() used to print only its ERROR line; the reaction a caller needs the moment a
command declines lived one lookup away, in wikitool <cmd> -h's ON FAILURE section - exactly
the lookup AGENTS.md's own tool error contract already warned is the one most likely to be
skipped in the heat of a failure. fail() now prints that section's exit-1 causes (each with a
reaction) right after the ERROR line, on stderr and as plain text rather than through Rich - a
reaction can carry a literal [--flag], which Rich would otherwise read as markup. A record
with no exit-1 cause of its own falls back to a bare see: wikitool <cmd> -h pointer;
docs contract is the one real command that hits it today. Nothing about stdout changes: a
command's output on success, or up to and including its ERROR line on failure, is
byte-identical to before. cli.py's and _util.py's lookup of the running command's
cli_contract path is now one shared function, cli_contract.path_of, in place of a private
copy that used to live only in cli.py.
network: property defined; sync, publish and upstream merge marked networked
The network: property had no written meaning, and the records disagreed with the code and with
each other. sync, publish and upstream merge talk to a git remote (fetch, ls-remote,
push) but said network: no, while version check's record called itself one of only two
networked commands - next to review, task new/list/close and doctor, all of which
already said yes. cli_contract.Network now carries the meaning: yes when at least one path
through the command can reach an endpoint outside the checkout, by an HTTP call of wikitool's
own or by a git operation against a remote; what is possible counts, not what is usual, so
--offline, --no-fetch or a local-path remote does not turn it back to no, and a git call
that only reads refs already on disk is not network access. sync, publish and upstream merge now say yes; upstream verify only reads fetched refs and stays no. The count claim
is gone from version check's record, from two docstrings in version_cmd.py, and from
version.fetch_latest, which claimed to be the only place talking to a remote host although the
CalDAV and Super Productivity providers do too. A test over the real registry pins the set of
network: yes commands, so a change to it is an edit to that test rather than silent drift.
log append: unreadable --body-file is an ERROR line, not a traceback
log append read --body-file with an unguarded Path.read_text(). A missing file, a
directory or a file that is not valid UTF-8 ended the call in a Python traceback, while the
command's record promised an ERROR line with "fix the path and retry once" - so an agent was
told to treat the same failure as case 2 of the tool error contract (validation error, retry
once) by the record and as case 4 (unexpected error, do not retry a non-idempotent command) by
the output. The read is now guarded against OSError and UnicodeDecodeError and leaves
through fail(), which also gives the budget slot back, before kb/log.md is opened: the log
stays byte-identical in every failure case, and a parametrised test pins all three. The
record's cause now names the cases ("missing, not a readable file, or not valid UTF-8").
Command records: three more mismatches from #142 aligned to code
Three more text-vs-code disagreements from #142's collection issue (Gitea #146), plus a
correction of one of #142's own text fixes - all decided in the code's favour and fixed in the
text, no behaviour change. lint's quote-limit cap counts blockquotes, not lines, since #22
changed the unit; kb/CONTRACT.md's Quotation cap section had kept the old wording and now
names the same unit, with code masked out first. docs contract's record carried no exit-1
cause at all although contract_command fails when tools/CONTRACT.md is missing, so it now
names that cause; the merged-stream test that used to exercise the see: fallback through this
very gap now pins the command's own ON FAILURE line literally, and the fallback itself stays
covered by its own fixture-based test. xref add's atomic property still described a two-write
shape, though the code and the record's own NOTES say an edge is written into A only and B is
never touched; it now says so. publish's atomic property, tightened earlier in this
candidate to "every gate runs before staging", still missed the one case that breaks it: on the
single retry of a rejected push, the rebase-review gate can exit 42 after the local commit
exists - nothing is pushed, and the --confirm-rebase re-run pushes that commit.
The five other text fixes #142 made in the code's direction were checked against the code and stand as they are.
docs contract: the merged-stream test pins its own ON FAILURE line
The previous change's test compared docs contract's output only against
render_failure_hint() of the same record - which would still pass if the record lost its
exit-1 cause again, because both sides would then fall back to the same see: line. It now also
pins the ON FAILURE (wikitool docs contract -h): header and the missing-file cause literally;
removing the cause from the record fails it.
7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join
Author: Torben Nehmer
Breaking Change:
- docs verify now requires an adopted
projecttype-spec (schema requiringstate:) and itskb/gtd/collection - an instance must adopt types/project.md(.schema.yaml) and kb/gtd/COLLECTION.md from their .template before docs verify passes again - superproductivity's provider section in .wikitool-tasks.json now requires 'access' ('api' or 'snapshot'), no default and no fallback between the two; 'db_path' no longer exists at all. An existing config must add 'access' and, if it used 'db_path', switch to 'backups_dir' (see INSTALL.md's example).
Migration: none required - No kb/ page content changes - the break is confined to .wikitool-tasks.json, an instance-owned, gitignored file every operator already edits by hand per INSTALL.md's example.
High impact
- SP-Zugriffsweg explizit (access: api/snapshot, #133) und follow_up_at-Korrektur (dueWithTime/dueDay, #135)
Medium impact
- Typ
projectund Collectionkb/gtd/: das Vorhaben als eigene Seitenart - Task-tracker provider layer, with a Super Productivity adapter
- wikitool review: the weekly GTD review as a read-time join
- wikitool new project: Seite und Tracker-Projekt unter einem Namen
- Skill weekly-review: turning wikitool review's findings into decisions
- Doku-Nachzug zu #119: die Verpflichtungsschicht erreicht Installation, Setup und docs/
- task new: einen zweiten Schreibweg in den Tracker (ein Posten, keine Seite)
- wiki-ingest: raw accept rückt hinter die Verpflichtungsentscheidung
- Weekly review proposes task new/task close; tracker gains a closing write path
Low impact
- new project: Testabdeckung fuer die required-responsibility-Ablehnung
- docs/-Nachzug: Exit 42 als Haltung, und die Adoption eines neu ausgelieferten Templates
- Skill-Namensfamilien: weekly-review -> gtd-weekly-review, dritte Person in allen Descriptions
- Veraltete Skill-Aufzaehlungen in der Instruction-Schicht nachgezogen
- gtd-weekly-review: task new nachgezogen, veralteter Begründungszeiger korrigiert
- instructions/CONTRACT.md drops other files' step counts from the copy-in-checklist rationale
Dieser Kandidat bringt die Verpflichtungsschicht in den Stack: ein neuer Seitentyp project und
die Collection kb/gtd/ geben dem Vorhaben (Ziel, Beteiligte, dauerhafter Status) eine eigene
Seitenart neben dem Artefakt; eine Task-Tracker-Provider-Schicht (chemenu.tasks) verbindet das
mit einem echten Tracker, mit Super Productivity als erstem Adapter; wikitool review liest beide
Seiten zur Laufzeit zusammen statt sie zu synchronisieren, und das neue Skill
gtd-weekly-review macht dessen Funde zu Entscheidungen. Der Tracker bekommt zwei neue
Schreibwege dazu (task new, task close) neben den bestehenden Lesekommandos, und wiki-ingest
stellt seither bei jeder Quelle die Verpflichtungsfrage in beide Richtungen. Zwei
Grenzübertritte kommen mit: docs verify verlangt jetzt den adoptierten Typ project und die
Collection kb/gtd/, und .wikitool-tasks.jsons Provider-Abschnitt verlangt ein explizites
access (api/snapshot) ohne Fallback auf db_path. Der Rest der Bumps sind Nacharbeiten und
kleinere Korrekturen an genau dieser Naht - Doku-Nachzug, Namensfamilien, eine korrigierte
Zurückweisungs-Meldung, und die hier laufende Bereinigung veralteter Schrittzahlen in der
Instruction-Schicht.
Typ project und Collection kb/gtd/: das Vorhaben als eigene Seitenart
Gitea #119 (Paket #123): ein neuer Seitentyp project fuer das Vorhaben - Ziel, Beteiligte,
dauerhafter Status, offene Schleifen - abgegrenzt gegen das Artefakt (entity/codebase, seit
6.2.0). types/project.md/.schema.yaml und die neue Collection kb/gtd/ (Bereiche haus/,
finanzen/, technik/ ueber responsibility:) folgen exakt dem Muster, das entity/concept/
source/comparison schon vorgeben - kein Code noetig fuer wikitool new project, types list
oder den Template-Versand, alles daran ist bereits generisch.
Neu ist nur eine Zeile Code: project tritt neben source in
kb_collections.STACK_REQUIRED_TYPES, nach demselben "fordern statt besitzen"-Idiom (D16) - ein
Type-Spec name: project, dessen Schema state: fuehrt, muss existieren, weil der
Wochenrueckblick (#125) sonst nichts hat, wogegen er ein Tracker-Projekt abgleichen kann. Das
macht docs verify zum Grenzuebertritt (siehe Breaking Change oben): eine Instanz, die die
neue tools/-Fassung uebernimmt, ohne types/project.md.template und
kb/gtd/COLLECTION.md.template zu adoptieren, faellt fortan durch, wo sie vorher bestand. Dabei
aufgefallen und mitkorrigiert: kb_collections.declaration_issues()s Meldung fuer eine fehlende
Pflicht-Collection nannte immer source, unabhaengig davon, welcher Typ tatsaechlich fehlte -
jetzt benennt sie den Typ, den stack_required_collection_owners() tatsaechlich dafuer
verantwortlich macht. kb/entities/COLLECTION.md traegt jetzt einen gtd:-Block (vorerst nur
see-also), ohne den keine Kante von einer Entity auf ein Vorhaben autorisierbar waere - das ist
der Block, auf den #118 wartet.
Task-tracker provider layer, with a Super Productivity adapter
Gitea #119 (Paket #124): die Schicht, ueber die wikitool an einen Aufgaben-Tracker kommt -
ohne dass eine Instruction je erfaehrt, welcher es ist (D25). chemenu.tasks.protocol deklariert
TaskReader/TaskWriter als getrennte Protocols, chemenu.tasks.superproductivity implementiert
beide gegen Super Productivity, .wikitool-tasks.json (chemenu.tasks.config) traegt Provider,
Verbindungsangaben und die drei Schwellwerte des Wochenrueckblicks (#125). wikitool doctor
berichtet den konfigurierten Provider, seinen Lesepfad-Status und ob seine lokale REST-API
antwortet - read-only, FAILt nur auf eine kaputte Konfiguration, nie auf einen nicht laufenden
Tracker. Kein Kommando entsteht hier (#124s eigene Abgrenzung) - das ist #125/#126.
Zwei Zwischenbefunde aus der Umsetzung, gegen den tatsaechlichen Quellcode von
super-productivity/super-productivity (master, 2026-09-19) verifiziert:
- Der Lesepfad liest keine
db.json- die gibt es auf dem Desktop nicht, der Live-Zustand liegt in IndexedDB. Gelesen wird die neueste Datei unter dessen periodischen Dateisystem-Backups (electron/backup.ts,<userData>/backups/<timestamp>.json), deren Inhalt exakt die verifizierte Form hat. - Die lokale REST-API kann keine Projekte anlegen -
GET /projectsexistiert,POST /projectsnicht (electron/local-rest-api-handler.service.ts). Damit entfaellt fuer diesen Provider der in #119 D31 vorgesehene automatische Schreibpfad;create_projectprueft weiterhin die Namenskollision (D8), verlangt dann aber menschliches Eingreifen statt es zu simulieren:SuperProductivityWriter.create_projectwirft ein neueschemenu.errors.HumanInterventionRequiredmit Anweisungen fuer den Menschen und einemverify(), das den Lesepfad danach erneut befragt statt der Bestaetigung einfach zu glauben. Dieselbe Klasse haengt sich an den bestehendenEXIT_NEEDS_CLEARANCE-Code (42) - keine neue benannte Gate, aber dieselbe Haltung: dem Menschen die Ausgabe zeigen und anhalten, statt eine Umgehung zu erfinden. Die CLI-seitige Uebersetzung (needs_clearance) folgt mit dem Kommando in #126; #124 liefert nur die Bibliotheksseite. #119s Umsetzungstabelle und #124s eigener Akzeptanzkriterien-Absatz sind entsprechend nachgezogen.
Ausserdem verifiziert, ohne Designfolgen: Super Productivitys Someday/Maybe-Aequivalent ist der
bestehende backlogTaskIds-Puffer je Projekt, keine eigene Tag-Konvention.
wikitool review: the weekly GTD review as a read-time join
Gitea #119 (Paket #125): das tragende Bauteil - wikitool review joint die Tracker-Seite
(chemenu.tasks, #124) und die kb/gtd/-Projektseiten ueber den case-normalisierten Namen und
gibt einen Bericht aus. Es speichert nichts, nicht einmal eine reports/-Datei (D3) - search
ist das naechste Vorbild dafuer, und review ist deshalb genauso vom Iterationsbudget
ausgenommen.
Fuenf Pruefungen (#119 D10/D26), alle in chemenu.review.run_review: stalled (Tracker-
Projekt ohne offene Posten, kb/-Seite state: active - dormant/completed/abandoned
melden nie, D27), waiting_overdue (follow_up_at aelter als stalled_waiting_days),
unpaged_project (Tracker-Projekt ohne kb/-Seite, aelter als unpaged_project_weeks),
no_open_loop (kb/-Seite active, aber kein Tracker-Projekt dieses Namens oder keine
offenen Posten - die Gegenrichtung des vorigen Abgleichs, D8s beidseitiger unmatched-Bericht),
someday_stale (Someday-Posten seit someday_stale_months unveraendert, ueber Kalendermonate
gerechnet statt ueber Tage / 30). Ein Tracker-Projekt ohne offene Posten mit aktiver kb/-Seite
erfuellt zugleich stalled und no_open_loop - beide melden, das ist keine Dopplung, sondern zwei
verschiedene Aussagen ueber denselben Zustand.
Jeder Providerzugriff ist einzeln abgesichert: scheitert projects(), entfallen die vier darauf
aufbauenden Pruefungen; scheitert someday_items(), entfaellt nur die fuenfte; scheitert
open_items() fuer ein einzelnes Tracker-Projekt, faellt nur dieses eine aus den betroffenen
Pruefungen heraus, der Rest laeuft weiter. Ein so unvollstaendiger Bericht setzt complete auf
false, druckt trotzdem alles, was noch entschieden werden konnte, und die CLI beendet sich mit
Exit 1 - nie mit einem leisen Teilbericht, der wie eine ruhige Woche aussieht. Fehlt
.wikitool-tasks.json ganz, oder ist es kaputt, scheitert der Aufruf sofort und sagt das - das
ist ein Konfigurationsfehler, kein Erreichbarkeitsproblem, und braucht deshalb keinen Teilbericht.
--json traegt dieselben Befunde maschinenlesbar (findings/checks_run/checks_skipped/
kb_project_count/complete); ein Test haelt beide Formen gegeneinander, wie es
test_mcp_server.py fuer den MCP-Lesepfad gegen die CLI tut.
wikitool new project: Seite und Tracker-Projekt unter einem Namen
Gitea #119 (Paket #126): wikitool new project --name X --set responsibility=Y legt jetzt, wenn
.wikitool-tasks.json einen Tracker konfiguriert, zusaetzlich ein gleichnamiges Tracker-Projekt
an - ein Geburtsort, ein Name (D8/D31). Tracker vor Seite: erst steht die Tracker-Seite fest,
erst danach wird die kb/-Seite geschrieben, damit ein Fehlschlag zwischen beiden immer im
selben, bereits bekannten Zustand landet - "Tracker-Projekt ohne Seite", das reviews Pruefung 3
ohnehin meldet - nie im unbekannten "Seite ohne Tracker-Projekt". Ist kein Tracker konfiguriert,
bleibt es bei der reinen Seitenanlage, jetzt aber ausdruecklich als solche vermerkt statt
stillschweigend.
chemenu.tasks.build_reader/build_writer (neu in chemenu/tasks/__init__.py) sind die eine
Dispatch-Tabelle von TasksConfig.provider auf einen konkreten Adapter, jetzt von review.py
und new_page.py geteilt statt zweimal derselben if cfg.provider == "superproductivity".
Fuer einen Provider ohne Schreibpfad (Super Productivity, #124: keine POST /projects) wirft
create_project chemenu.errors.HumanInterventionRequired - das Kommando zeigt die Anweisung
und beendet sich mit Exit 42, ohne irgendetwas anzulegen. Die offene Frage aus #126s eigenem
Issue-Text war, wie ein zustandsloser CLI-Prozess bei einem erneuten Aufruf eine echte
Namenskollision von "der Mensch hat gerade getan, worum genau dieses Kommando gebeten hat"
unterscheidet - beides sieht am Lesepfad identisch aus (Tracker hat den Namen, kb/ noch keine
Seite). Entschieden (mit dem Betreiber, nicht allein): ein explizites --resume, das ein Treffer
im Tracker als bestaetigte Fortsetzung liest statt als Kollision - ohne --resume bleibt jeder
Treffer eine Ablehnung samt Fundort, auch bei einem Wiederholungsaufruf. --resume ohne einen
tatsaechlich fehlenden Tracker-Eintrag wirft dieselbe HumanInterventionRequired-Meldung erneut,
keine stille Weiterarbeit auf Zuruf. --resume bei jedem anderen Typ wird abgelehnt.
Ein erzwungener Fehlschlag der eigentlichen Seiten-Schreibaktion (Schritt 3) nach bereits
bestaetigtem Tracker-Projekt ist eigens getestet: die Meldung nennt, dass die Tracker-Seite schon
steht und nur die kb/-Seite fehlt, nie umgekehrt.
new project: Testabdeckung fuer die required-responsibility-Ablehnung
Gitea #119 (Paket #126s eigenes AC): --responsibility ist Pflicht und ein Wert ausserhalb des
Enums wird abgelehnt - beides galt schon vorher generisch ueber types/project.schema.yamls
required:/enum: (#123), ohne dass #126 dafuer neuen Code brauchte. Nachgetragen: ein Test,
der das fuer den fehlenden Fall (--set responsibility=... ganz weggelassen) tatsaechlich belegt,
statt es nur zu behaupten.
Skill weekly-review: turning wikitool review's findings into decisions
Gitea #119 (Paket #127): instructions/weekly-review/SKILL.md, publiziert nach .agents/skills/
und .claude/skills/. wikitool review liefert fuenf Befunde (#125); dieser Skill fuehrt das
Gespraech, das aus jedem eine Entscheidung macht - je Befund mindestens zwei Handlungsoptionen und
ein Unterscheidungsmerkmal, wie in #127s Akzeptanzkriterien gefordert.
Der Skill nennt bewusst keinen Provider, keine Datei- und keine API-Form (D25) - eine neue
Regressionstest (test_weekly_review_skill_names_no_provider) haelt das am echten Repo-Inhalt
fest, nicht nur als Review-Behauptung. Erinnert im Text an D28 (Personen in ## Beteiligte
bleiben Erwaehnung, bekommen keine Seite) und D7 (die Seite fasst die Aufgabenliste nie
zusammen). Die Kommandoflaeche bleibt bei review/new project; alles Aufgabenbezogene - eine
naechste Aktion anlegen, follow_up_at verschieben, einen Someday-Eintrag streichen - bleibt eine
Handlung im Tracker selbst, weil dafuer kein wikitool-Kommando existiert (D31).
Doku-Nachzug zu #119: die Verpflichtungsschicht erreicht Installation, Setup und docs/
Gitea #119, nach Abschluss von #122-#127: die sechs Umsetzungspakete haben ihre Vertragszeilen
jeweils mitgebracht (tools/CONTRACT.md, kb/CONTRACT.md), aber drei Flaechen blieben zurueck,
die kein Paket fuer sich allein besass - und keine davon faellt bei docs verify auf, weil dort
keine Zeile fehlt, sondern Prosa.
INSTALL.md§ Konfiguration kannte.wikitool-tasks.jsonnicht. Die Datei stand intools/CONTRACT.mdsdoctor-Zeile und inreviews Fehlerkontrakt, also dort, wo ein Agent nachschlaegt - nur nicht dort, wo ein Mensch die Form nachschlaegt. Sie steht jetzt neben.wikitool-telemetry.jsonund.wikitool-remotes.json, mit vollstaendigem Beispiel, den drei Schwellwerten als Konfiguration statt Schema, und dem fuer Super Productivity getrennten Lese-/Schreibpfad. Diedoctor-Beschreibung unter § Verifikation nennt den Tracker jetzt mit.setup-instance.mdbot den Tracker nie an. Eine neue Instanz bekam Typ und Collection ueber die generische Template-Adoption (Schritt 5), aber nichts fragte nach der anderen Haelfte des Rueckblicks. Neuer Entscheidungspunkt (Schritt 11, parallel zur Telemetrie): einmal fragen,.wikitool-tasks.jsonanlegen oder nichts tun - kein Tracker ist ein gueltiger Endzustand. Die Form steht nicht hier, sondern inINSTALL.md(Invariante 8). Folgenummern 12-16 nachgezogen, Skill-Liste umweekly-reviewergaenzt.upgrade-instance.mdhatte keinen Pfad fuer ein neu ausgeliefertes.template. Genau der Fall, den 7.0.0 erzwingt: Schritt 5 sagte „newbraucht keine Entscheidung", und die Schritt-6-Tabelle sagt, instanzeigene Dateien koennten dort gar nicht auftauchen - beides richtig und zusammen irrefuehrend, weil ein neuestypes/<name>.md.templatefuer einen geforderten Typ sehr wohl eine Handlung braucht. Schritt 5 benennt diese eine Ausnahme jetzt, Schritt 9 traegt die Reparatur neben der TOC-Reparatur: die gewoehnliche Adoption, mit den zweicp-Zeilen, ausdruecklich keine Datenmigration.
Dazu die Begruendung selbst: docs/knowledge-and-commitment.md ist neu und haelt fest, warum
Wissen und Verpflichtung zwei Schichten sind (verschiedene Halbwertszeiten), warum nicht
synchronisiert wird (Muster 4, mit den drei verworfenen Anordnungen), warum der Join zur Lesezeit
passiert und nichts speichert, warum ein Name die Pflichten eines Identifiers erbt, warum eine
Seite ihre Aufgabenliste nie zusammenfasst, warum Archivierung ein state:-Wert ist, und warum
keine Instruction je den Provider nennt. Das stand bisher ausschliesslich in Gitea #119 - und ein
Issue ist genau das, was dist export nicht mitliefert: eine ausgelieferte Instanz bekam den
Mechanismus ohne das Warum. AGENTS.md § File naming zaehlt jetzt sechs statt fuenf von dort
verlinkte docs/-Seiten.
tools/README.md § Layout fuehrt review.py und das Paket tasks/; und
instructions/dev/doc-pull-through.md bekommt die zwei Zeilen, deren Fehlen dieser Nachzug ist:
eine fuer eine instanzeigene Konfigurationsdatei (INSTALL.md § Konfiguration + der
setup-instance-Entscheidungspunkt + doctors Vertragszeile), eine fuer einen neuen geforderten
Seitentyp oder eine neue Collection (Type-Spec/COLLECTION.md + kb/CONTRACT.md + beide
Adoptionspfade). Die Zeile zur docs/-Begruendung sagt jetzt zusaetzlich, dass eine Entscheidung
ohne Seite die eigentliche Luecke ist, weil Begruendungen aus einem Issue nie ausgeliefert werden.
docs/-Nachzug: Exit 42 als Haltung, und die Adoption eines neu ausgelieferten Templates
Die Abschlusspruefung dieses Kandidaten (stack-close Schritt 3) hat zwei docs/-Seiten gefunden,
deren Begruendung die Pakete #124/#126 bzw. #123 verschoben hatten, ohne dass jemand sie nachzog -
beide unpruefbar, weil eine docs/-Seite per Konstruktion keinen normativen Satz traegt.
docs/why-gates-are-code.mdkannte Exit 42 nur als Gate. Die Seite oeffnet mit „vier harte Grenzen", und seit #124/#126 verlaesstHumanInterventionRequireddenselben Code, ohne eine fuenfte Gate zu sein. Neuer Abschnitt „Exit 42 is a posture, and it outgrew the gates": die vier Gates teilen eine Weigerung (die Operation waere moeglich, das Werkzeug fuehrt sie unbesehen nicht aus), der neue Fall ist das Gegenteil (die Operation ist gar nicht moeglich, kein Token koennte das aendern) - gemeinsam ist beiden nur, was der Exit-Code tatsaechlich sagt: anhalten, einem Menschen zeigen, nichts umgehen. Dazu die verworfene Alternative, die Luecke in der Instruction-Schicht zu beschreiben - genau die Prosa-Regel, gegen die diese Seite argumentiert.docs/ownership-and-templates.mdbeschrieb die.template-Kategorie nur von innen. „Wird von einem Upgrade nie geschrieben" stimmt weiterhin fuer eine bereits adoptierte Datei; der Fall, dass ein Release ein Template fuer einen Typ liefert, den die Instanz noch gar nicht hat, stand nirgends - und genau der ist seit #123 der Grenzuebertritt. Die Kategorie nennt ihn jetzt als die eine Gestalt, in der „das Upgrade schreibt diese Datei nie" zu Arbeit wird, mit Verweis auf den Schritt inupgrade-instance.md, den Stack 7.0.0-beta.7 dort angelegt hat.
Skill-Namensfamilien: weekly-review -> gtd-weekly-review, dritte Person in allen Descriptions
Gitea #129: the published skill collection had drifted into three naming shapes where it should
have three families. weekly-review (added earlier in this same candidate, never released)
named neither its domain nor its distribution boundary, unlike wiki-* and stack-* either
side of it - renamed to gtd-weekly-review, joining a new gtd- prefix for the commitment layer
(kb/gtd/, types/project.md, docs/knowledge-and-commitment.md) that sits beside wiki- (the
knowledge pipeline) and stack- (the stack's own development, under instructions/dev/).
instructions/CONTRACT.md § "Writing an instruction" now states both the family-prefix
convention and, separately, that a skill's description speaks in third person per Anthropic's
skill-authoring guidance - all eight published skills' descriptions were rewritten to match
("Processes...", not "Process..."); the flat instructions/<name>.md form keeps its existing
imperative-title convention, since its description is read on demand rather than injected into
the system prompt. No --breaking line: the renamed skill was introduced by this same
unreleased candidate, so no existing instance carries the old name to migrate away from.
Veraltete Skill-Aufzaehlungen in der Instruction-Schicht nachgezogen
Gitea #129s Abschlusspruefung: die Skill-Umbenennung hat sichtbar gemacht, dass mehrere Dokumente die Skill-Liste hart aufzaehlen und beim Wachsen der Liste still veralten. Vier Stellen waren falsch, eine davon schon vor diesem Paket:
instructions/CONTRACT.md§ "When a skill carries a copy-in checklist" zaehlte „die zwei Skills mit Block und die drei ohne" - also fuenf von inzwischen acht. Die Zahlen im Einstiegssatz sind jetzt ganz raus (sie unterscheiden sich ohnehin zwischen diesem Repo und einer ausgelieferten Instanz, dieinstructions/dev/nicht hat), die Aufzaehlung nennt alle ausgelieferten Skills, und die beiden Dev-Skills stehen in einemdist:strip-Block.- Dieselbe Passage nannte
wiki-querymit sechs Schritten - es sind sieben, und zwar schon laenger. Die Regel selbst (ein Flow ab acht Schritten und still scheiternde Schritte) bleibt unveraendert; kein Skill wechselt dadurch die Seite. instructions/CONTRACT.md§ "outbound reference" sprach im Praesens von „this repo's seven skills", wo die Zahl zu einer datierten Messung (52 von 58 Links) gehoert - jetzt als „at the time" markiert, statt die Messung nachzurechnen.instructions/bootstrap.mdund die Scope-Abschnitte vonstack-dev/stack-closelisteten die Content-Skills ohnegtd-weekly-reviewauf.
Dazu test_the_real_repo_publishes_the_six_wiki_skills -> ..._every_skill: der Test pruefte
sechs der acht Skills und trug die veraltete Zahl im Namen; er nennt jetzt alle acht, nach
Familien erklaert. Dass die Passage weiterhin Schrittzahlen fremder Dateien zitiert, die genauso
still veralten koennen, ist als eigene Frage festgehalten und hier bewusst nicht geloest.
SP-Zugriffsweg explizit (access: api/snapshot, #133) und follow_up_at-Korrektur (dueWithTime/dueDay, #135)
Gitea #133: der Super-Productivity-Adapter unterscheidet jetzt zwei sich ausschliessende
Zugriffswege, per Pflichtfeld access ohne Default und ohne Laufzeit-Ruckfall - "api" liest
und (fuer #132 vorbereitet) schreibt ausschliesslich ueber die lokale REST-API und den aktuellen
Zustand, "snapshot" liest ausschliesslich den juengsten Backup-Schnappschuss und ist von dort
aus nie schreibbar. db_path entfaellt vollstaendig, backups_dirs Glob ist auf das echte
Zeitstempelmuster (YYYY-MM-DD_HHmmss.json) gehaertet, und beide Wege blenden archivierte
Tracker-Projekte aus - verifiziert gegen den tatsaechlichen Quellcode von
super-productivity/super-productivity (master, 2026-09-20). SuperProductivityApiReader ist
der neue Leser fuer access: "api", gegen eine Attrappe getestet, nie gegen eine laufende App.
tools/wikitool new project verweigert auf einer access: "snapshot"-Instanz jetzt vollstaendig
(Exit 1, weder Tracker-Projekt noch Seite) statt den nicht mehr moeglichen 42er-Menschenschritt zu
versuchen; wikitool doctor und wikitool review berichten nur noch den tatsaechlich
konfigurierten Weg, und jede Antwort von review nennt jetzt explizit, aus welchem Weg sie
stammt (bei snapshot samt Alter des gelesenen Schnappschusses). Aufgeloest damit: #134, dessen
Verdacht (--resume koennte gegen einen veralteten Schnappschuss verifizieren) durch den Wegfall
der Projektanlage auf snapshot-Instanzen gegenstandslos wurde.
Gitea #135, im selben Zug korrigiert: follow_up_at las bislang remindAt, das sich nur bei
einer mit Uhrzeit terminierten und benachrichtigten Aufgabe fuellt - ein ganztaegiger, stiller
Tickler (dueDay ohne dueWithTime, das haeufigste WAITING-Muster) hatte dadurch nie einen
follow_up_at und fiel bei Pruefung 2 des Wochenrueckblicks still durch. Gelesen wird jetzt
dueWithTime, sonst dueDay - Super Productivitys eigene Leseregel - nie deadline* (D9 bleibt
in der Sache unveraendert, nur die falsche Berufung auf sie ist korrigiert). Bestehende Instanzen
sehen dadurch rueckblickend mehr Befunde, nicht weniger.
task new: einen zweiten Schreibweg in den Tracker (ein Posten, keine Seite)
Gitea #132: eine Quelle kann Wissen und eine Verpflichtung zugleich tragen (eine Kundenreklamation
etwa), und bislang hatte nur die Wissenshaelfte einen Schreibweg. chemenu.tasks.protocol.TaskWriter
traegt jetzt eine zweite Methode, create_item - Projekt, Titel, optional WAITING mit
follow_up_at, optional ein Freitext-Rueckverweis in notes -, darueber das neue Kommando
wikitool task new. Anders als create_project schreibt sie tatsaechlich: bei Super Productivity
existiert POST /tasks, wo POST /projects fehlt, also gibt es hier keinen
HumanInterventionRequired-Fall. Das Kommando ordnet kein Projekt selbst zu - ein --project, das
zu keinem Tracker-Projekt passt, oder --waiting ohne vorhandenen waiting-Tag scheitert laut,
exit 1, statt zu raten oder einen Posten ohne seinen Status anzulegen. Die Eingangs-Ablage
(--inbox) ist eine eigene, ausdrueckliche Form am Kommando, nie ein Ersatz fuer ein vergessenes
--project.
Verifiziert gegen super-productivity/super-productivity@master (2026-09-20): Super Productivitys
INBOX_PROJECT ist zwar immer ein echtes Projekt-Entity im Store, aber
selectUnarchivedProjects - der Selektor hinter GET /projects - filtert es ueber seine feste id
unbedingt heraus. Ein per --inbox abgelegter Posten erscheint deshalb in keiner
wikitool review-Pruefung, nicht weil eine Ausnahme dafuer noetig waere, sondern weil der Eingang
in der Projektliste schlicht nie auftaucht - der Ingest-Skill nennt diese Kosten jetzt ausdruecklich,
wenn er die Route anbietet.
Der Ingest-Skill (instructions/wiki-ingest/SKILL.md) fragt in Schritt 5 jetzt auch nach einer
Verpflichtung, nicht nur nach dem Wissen, und legt Titel und vorgeschlagenes Projekt in einem
Bestaetigungsschritt vor (nie eine automatische Zuordnung, auch nicht bei einem eindeutigen
search-Treffer). Der Posten wird vor der Quellenseite angelegt - dieselbe Tracker-vor-Seite-
Reihenfolge, die new project schon haelt, hier mit eigenem Beleg: eine Rohdatei ohne Quellenseite
meldet lint als uncovered_raw_files, eine stillschweigend verlorene Verpflichtung meldet
nichts. Der bestehende Regressionstest, der sicherstellt, dass kein Skill den Tracker-Provider
nennt, ist entsprechend auf wiki-ingest erweitert. docs/knowledge-and-commitment.md und
tools/CONTRACT.md sind nachgezogen; Gitea #128 (der zweite Adapter) traegt jetzt create_item
in seiner eigenen Flaeche.
wiki-ingest: raw accept rückt hinter die Verpflichtungsentscheidung
Gitea #136, eine offen gebliebene Teilfrage aus #132: raw accept lief dort weiterhin ganz am
Anfang des Laufs, vor Lesen, Diskussion und Verpflichtungserkennung - scheiterte task new, fand
sich eine bereits nach raw/ befoerderte Rohdatei vor, obwohl #132s eigenes Kriterium "keine
Seite geschrieben und keine Rohdatei befoerdert" verlangte. docs/knowledge-and-commitment.md
behauptete diese Eigenschaft seit demselben Commit bereits als Tatsache; der Baum beschrieb ein
Design, das es nicht gab.
wiki-ingests Schritte 1-5 sind neu geordnet: lesen, Metadaten, search, Diskussion inklusive
Verpflichtung und task new, dann erst raw accept. Die Schritte 6-12 behalten ihre Nummern
unveraendert, ebenso jeder Fremdverweis, der eine dieser Nummern nennt. Zwei Stellen sind dabei
verschaerft, nicht nur verschoben: die fidelity/authority-Frage in Schritt 5 benennt jetzt
ausdruecklich, dass die Datei zu diesem Zeitpunkt schon vollstaendig gelesen ist - der Moment, in
dem die Versuchung, den Wert aus dem Inhalt zu erschliessen statt ihn zu erfragen, am groessten
ist -, und derselbe Schritt benennt, dass seine Kollisionsverweigerung jetzt spaeter faellt, nach
Lesen, Diskussion und moeglicherweise bereits angelegtem Tracker-Posten.
Eine Ausnahme bleibt bewusst bestehen: ein Lauf, der wegen Volumen oder Breite an
instructions/ingest-large-tree.md uebergibt, befoerdert weiterhin vor der Verpflichtungsfrage -
dessen work new --input <pfad> verweigert jeden Pfad ausserhalb raw/, und der Lauf ist ohnehin
nicht atomar, da er unit-weise ueber Tage publiziert und seine eigene Verpflichtungsfrage erst in
Schritt 5d je Unit stellt. docs/knowledge-and-commitment.md nennt diese Ausnahme jetzt explizit,
statt die Eigenschaft unbedingt zu behaupten. README.md und instructions/CONTRACT.md sind
nachgezogen.
Zwei Fragen, die sich beim Durchsehen der Naht zwischen Wissen und Verpflichtungen zusaetzlich
zeigten - gtd-weekly-reviews veraltete Zaehlung der GTD-Kommandos, und ob der Weekly Review
task new kuenftig anbieten soll -, sind bewusst nicht Teil dieser Aenderung: Gitea #137 und #138.
gtd-weekly-review: task new nachgezogen, veralteter Begründungszeiger korrigiert
Gitea #137, der liegengebliebene Pull-Through von #132 am GTD-Rand: instructions/gtd-weekly- review/SKILL.md behauptete zweimal, review und new project seien die einzigen zwei
GTD-Kommandos, und begruendete die Haltung "der Nutzer handelt selbst in seinem Tracker" mit
"because wikitool has no command for it" - seit #132s task new schlicht falsch. Der
Begruendungszeiger fuer die schmale Kommandoflaeche zeigte zudem auf types/project.md und
kb/gtd/COLLECTION.md; die Begruendung steht tatsaechlich in
docs/knowledge-and-commitment.md § "Status has exactly one home", die #132 korrekt nachzog,
waehrend dieses Skill unberuehrt blieb.
Beide Stellen benennen task new jetzt als drittes, tatsaechlich vorhandenes Kommando, das dieses
Skill bewusst nicht aufruft - die Haltung selbst ist unveraendert, nur ihre Begruendung ist jetzt
eine Wahl statt eine Behauptung ueber eine fehlende Faehigkeit. Ob der Weekly Review task new
kuenftig anbieten soll, bleibt unentschieden in Gitea #138; dieses Issue korrigiert nur, was
nachweislich falsch dastand.
Weekly review proposes task new/task close; tracker gains a closing write path
Gitea #138 entschied die dort offene Frage: der Weekly Review bietet task new jetzt bei
stalled/no_open_loop Option (a) an, nach ausdruecklicher Bestaetigung von Titel und Projekt in
einer Frage - dieselbe Haltung wie im Ingest. Die zweite Haelfte derselben Naht war unentschieden
liegen geblieben: eine Quelle kann eine Verpflichtung anlegen, aber nie schliessen. Der Tracker
bekommt dafuer einen dritten, letzten Schreibweg, task close --id, der einen Posten erledigt
markiert - niemals loescht, verifiziert gegen Super Productivitys master-Branch, dass
PATCH /tasks/:id mit isDone: true bit-identisch zur eigenen "erledigt"-Checkbox der App ist.
task list --project liefert dazu die Item-Ids, die task close und die Review-Funde fuer
waiting_overdue/someday_stale jetzt mitfuehren. wiki-ingest stellt die Verpflichtungsfrage
seither in beide Richtungen (oeffnen und schliessen), und ingest-large-tree Schritt 5d begruendet
jetzt in einem Satz, warum diese Frage pro Unit gestellt wird statt einmal pro Baum.
Die zwei Instruktionsstellen, an denen die Haltung zum Tracker-Schreibzugriff bislang doppelt
stand, sind auf eine zusammengezogen; die zweite verweist nur noch.
docs/knowledge-and-commitment.md § "Status has exactly one home" zaehlt die Kommandoflaeche
korrekt (zwei Lese-, drei Schreibkommandos) und haelt fest, warum sie bei "anlegen" und "erledigt
markieren" endet, nie bei "loeschen" oder "aendern".
instructions/CONTRACT.md drops other files' step counts from the copy-in-checklist rationale
instructions/CONTRACT.md § "When a skill carries a copy-in checklist" used to justify the
threshold by citing each other skill's step count by number - evidence that the two-halves test
(length and a silently-omittable step) is what actually decides wiki-ingest/wiki-lint, not a
count fitted after the fact. Nothing kept those numbers in sync with the SKILL.md files they
described: one of them had already drifted silently (wiki-query cited at six steps where it had
been seven for a while), and the passage itself named only five of the eight skills that exist -
neither wrong number failed any check, because docs verify reads presence, not another file's
prose (instructions/dev/doc-pull-through.md).
Of the three fixes considered - a docs verify check against a codified step-counting convention,
a qualitative rewrite that drops the numbers, or tracking the sync as a manual doc-pull-through.md
duty - the qualitative rewrite won: it is the only one of the three that removes the possibility of
drift rather than catching or documenting it, and the per-skill counts were decoration for the
two-halves test, never load-bearing for it. The passage now names which skills qualify and why,
without citing a number that belongs to a file it does not own. Today's counts were checked against
the actual files before this bump (wiki-ingest twelve, wiki-lint nine, wiki-manage two flows
of seven, wiki-query seven, wiki-status five, gtd-weekly-review five, stack-dev six,
stack-close four) - all correct, confirming the passage was not itself wrong, only unguarded.
6.2.0 - 2026-09-19 - Entity-Subtyp project nach codebase umbenannt
Author: Torben Nehmer
- Entity-Subtyp project nach codebase umbenannt
Entity-Subtyp project nach codebase umbenannt
entity_type: project bezeichnete in diesem Korpus ausnahmslos Codebasen; Gitea #119 zieht
daraus einen eigenen Typ fuer das Vorhaben (gtd/project, Paket #123), wodurch der bisherige
Wert homonym geworden waere. types/entity.schema.yaml und types/entity.md fuehren jetzt
codebase statt project, kb/entities/projects/ heisst kb/entities/codebases/, und die elf
betroffenen Seiten wurden ausschliesslich ueber wikitool touch/move umgezogen. Kein
Grenzuebertritt: types/entity.md ist .template-basiert, dist upgrade schreibt nie die
adoptierte Kopie einer Instanz, nur den mitgelieferten Standard daneben - eine bestehende
Instanz behaelt ihren eigenen project-Wert unangetastet und uebernimmt die Umbenennung erst,
wenn sie es sich vornimmt.
6.1.0 - 2026-09-17 - Upgrade-Pfad und Iteration-Budget-Gate gehaertet, wiki/-Pfadliterale bereinigt
Author: Torben Nehmer
High impact
- Session-Id-Fallback: Harness-Variable schliesst die Luecke zwischen Telemetrie-Join und Iteration-Budget-Gate
Medium impact
- Upgrade-Prozedur als eigene Instruktion statt als Prosa in INSTALL.md
- Migrationsdokument prueft gegen eine festgehaltene Vorher-Ausgabe, Beispielverweis auf die .template-Form
- dist upgrade: --take-release nimmt fuer einen lokal geaenderten Pfad die Release-Fassung
- version notes antwortet auf einer ausgelieferten Instanz aus dem Release-Feed
- new: scaffold materializes a schema default only for a required field
Low impact
- Stale
wiki/path literals swept out of tools/ and types/, with a test guarding against the next rename - CHANGES.md/Guard-Docstring: die Zahl der nachgezogenen Pfadliterale korrigiert (33, nicht 27)
Dieser Kandidat sammelt, was ein getraceter 5.0.0-auf-6.0.0-Upgrade-Lauf auf einer echten Instanz
offengelegt hat: ein fehlender Upgrade-Leitfaden, zwei falsche Verweise im Migrationsdokument,
eine fehlende dritte Antwort in dist upgrade fuer eine lokal geaenderte Datei, und
version notes, das auf einer ausgelieferten Instanz nie antworten konnte. Im selben Lauf zerfiel
die Sitzung durch einen PID-basierten Session-Id-Fallback in 21 Telemetrie-Buckets, wodurch das
Iteration-Budget-Gate strukturell unerreichbar blieb - behoben durch eine Registry bekannter
Harness-Session-Variablen, mit einem SIGPIPE-Nebenbefund im CLI-Emitter gleich mit. Dazu,
unabhaengig vom getraceten Lauf: ein Scaffold-Fix, der obligation: required nicht mehr in jede
neue Instruktion schreibt, und eine Bereinigung von 33 stehengebliebenen wiki/-Pfadliteralen aus
der wiki/-nach-kb/-Umbenennung, mit einem Test-Guard gegen die naechste Umbenennung.
Kein Grenzuebertritt: jede Aenderung ist in beide Richtungen ein Drop-in, additiv gegenueber
6.0.1.
Upgrade-Prozedur als eigene Instruktion statt als Prosa in INSTALL.md
Der Upgrade-Pfad einer ausgelieferten Instanz stand nur in INSTALL.md § "Eine Instanz
aktualisieren" - einem Dokument fuer Menschen, das AGENTS.md § File naming ausdruecklich als
"never by an agent as instruction" fuehrt. Ausgefuehrt wird er aber von einer Agent-Sitzung,
jedes Mal. Der getracete 5.0.0-auf-6.0.0-Lauf auf einer echten Instanz zeigt, was daraus folgt:
der erste Tool-Call listete instructions/ mit, fand keine passende Instruktion, oeffnete die
naechstliegende (private-instance.md, der falsche der beiden Wege) und fiel dann auf INSTALL.md
zurueck. migrate verify --from <commit vor dem Tausch> - INSTALL.md Schritt 6, erster
Pruefschritt - lief in 33 Werkzeugaufrufen kein einziges Mal, und die Agent-Sitzung wurde nie neu
gestartet, obwohl AGENTS.md im selben Commit +44/-3 bekommen hatte. Die anschliessende Migration
lief damit unter dem alten Kontrollplan.
Dahinter lagen drei Reihenfolgen nebeneinander: die in INSTALL.md, die im Abschlussbericht von
dist upgrade, und die tatsaechlich gelaufene. Genau der Zustand, den Invariante 8 verbietet.
instructions/upgrade-instance.md ist jetzt die eine Fassung: dreizehn Schritte von der
Sitzungs-Id bis zum zweiten Publish, mit dem Sitzungsneustart an der Stelle, an der der neue
Kontrollplan zu gelten anfaengt - nach dem Publish der Maschinerie, vor der Migrationskette, und
mit migrate status als Wiedereinstiegspunkt fuer die neue Sitzung. manual: true, weil die
Prozedur einmal pro Release laeuft und nie implizit aufgegriffen werden darf; ein Skill wuerde
seine description dafuer in jede Sitzung legen. Auffindbar ist sie ueber den Abschlussbericht
von dist upgrade, der statt einer eigenen Schrittliste jetzt die Datei nennt und das Kommando,
bei dem der Lauf weitergeht (instructions sync). INSTALL.md behaelt, was ein Mensch vorher
entscheidet, und den einen Sonderfall, den die Instruktion nicht abdecken kann, weil es sie dort
noch nicht gibt: den ersten Sprung auf 4.5.0.
Zwei Schritte der Instruktion sagen ausdruecklich, dass sie eine Luecke umgehen, und was sie
ueberfluessig macht. Schritt 2 liest die Release-Notes von der Release-Seite statt mit
version notes, weil eine Instanz ihre CHANGES.md als Stub bekommt und dist upgrade sie nie
ueberschreibt - der Befehl kann dort nicht heute und nicht spaeter antworten. Schritt 6 nimmt fuer
eine lokal veraenderte stackeigene Datei die Release-Fassung von Hand, weil es zu --keep-local
kein Gegenstueck gibt; dabei geht der noetige Commit ueber publish --no-push, nicht ueber
git commit - Invariante 5 kennt keine Ausnahme fuer "ist ja nur eine Vorbedingung", und genau
diese Ausnahme hat sich der beobachtete Lauf genommen.
instructions/session-setup.md sagt jetzt, dass ein export nur traegt, solange die Shell
traegt. Mehrere Harnesses starten pro Tool-Call eine frische Shell - das Arbeitsverzeichnis
ueberlebt, Shell-State nicht - und dann faellt jeder Aufruf auf seine eigene Parent-PID zurueck.
Im gemessenen Lauf wurde eine Sitzung so zu 21 Telemetrie-Buckets mit hoechstens drei Aufrufen
pro Bucket: das Iteration-Budget-Gate (60) und der Loop-Breaker (3 identische in Folge) konnten
strukturell nicht ausloesen. Die Anleitung nennt deshalb die Inline-Form pro Aufruf und den
Einzeiler, mit dem sich beantworten laesst, welcher Fall vorliegt.
Verifiziert: docs verify (73 ausgelieferte Dokumente, 58 Referenzdateien),
instructions verify (23 Instruktionen, 7 Skills) und 1276 Tests gruen - einer davon neu und auf
genau die Stelle gerichtet, an der die Doppelung wieder entstehen wuerde: der Abschlussbericht
von dist upgrade muss die Instruktion und ihr Wiedereinstiegskommando nennen, nicht eine zweite
Kopie der Liste.
Kein Grenzuebertritt: eine neue Instruktionsdatei und ein geaenderter Meldungstext sind in beide
Richtungen ein Drop-in. Eine Instanz, die zurueckgeht, behaelt die Datei als ueberzaehlige Datei,
und nichts liest sie automatisch - manual: true heisst genau das.
Migrationsdokument prueft gegen eine festgehaltene Vorher-Ausgabe, Beispielverweis auf die .template-Form
instructions/migrations/6.0.0-type-guidance-split.md verlangte in seinem Verifikationsschritt,
die Ausgabe von types describe <name> muesse "read the same as it did before this migration" -
ohne dass ein Schritt davor dieses Vorher festhielt. Eine Pruefung gegen einen Zustand, den
niemand aufgeschrieben hat, faellt auf das Gedaechtnis des Ausfuehrenden zurueck, und bei ueber
150 Zeilen Ausgabe je Typ ist das keins. Der getracete 6.0.0-Lauf hat entsprechend durch
| head -250 und | tail -80 geprueft und "structurally identical to before" geurteilt; was
das uebersah, lag in der Mitte der source-Ausgabe. Das Dokument schreibt die Ausgabe jetzt in
einem eigenen Schritt vor der Aenderung in eine Datei und diffed hinterher, mit
grep -c '^## Authoring guidance' als Ein-Zahl-Probe: zwei Koepfe sind richtig - einen setzt
types describe selbst, einen bringt die Guidance-Datei mit.
Als generisches Muster steht dasselbe jetzt in instructions/migrate-corpus.md § "Writing the
migration document", weil es nicht an diesem einen Dokument haengt: migrate verify traegt seine
Baseline im letzten Commit, ob jemand daran denkt oder nicht - eine Migration an der Maschinerie
statt an kb/ hat gar keine, und genau dort entsteht die Behauptung, die sich nicht widerlegen
laesst.
Zweiter Fehler im selben Dokument: der Beispielverweis auf types/entity.md zeigt in einer
ausgelieferten Instanz auf die beim Setup adoptierte Kopie - also auf genau den Vorher-Zustand,
den der Schritt entfernen laesst. Der Nachher-Zustand liegt dort unter
types/entity.md.template, und im Ursprungs-Repo existiert diese Datei ueberhaupt nicht:
dist export re-keyt types/<name>.md erst beim Export. Der Satz konnte in einer Instanz also
nicht bloss unguenstig sein, er konnte dort nie stimmen. Dazu sagt der Schritt jetzt die Sprache
des Pointer-Absatzes - englisch, weil Anleitungsprosa an einen Agenten Control Plane ist,
unabhaengig davon, wem die Datei gehoert - und dass das auch fuer behaltene lokale Prosa gilt:
die wird uebersetzt, nicht umbenannt. Die Tabelle dazu wird verlinkt statt kopiert
(types/type-spec.md § "Who owns a type-spec"), und ein behaltener Abschnitt bekommt einen
eigenen Namen statt der Ueberschrift, die types describe schon selbst setzt.
Derselbe Defekt eine Ebene hoeher, gefunden beim Nachmessen: types/source.md trug hier im
Ursprungs-Repo noch einen Rest-Abschnitt ## Authoring guidance mit einem einzigen Bullet, der
die title_prefix-Frontmatter wiederholte - types describe source gab drei Koepfe aus, die
anderen drei Typen zwei. Die Sprachzentralisierung hat den Abschnitt uebersetzt, der
Guidance-Split den Rest der Prosa ausgelagert und diesen Bullet stehenlassen. Die Datei wird beim
Export zu types/source.md.template, also haette ihn jede neu aufgesetzte Instanz mit adoptiert.
Entfernt, geprueft mit genau dem Muster, das der Schritt oben jetzt vorschreibt: Vorher-Datei,
Diff, vier entfernte Zeilen und sonst nichts, alle vier Typen komponieren jetzt mit zwei Koepfen.
Verifiziert: docs verify (73 ausgelieferte Dokumente, 58 Referenzdateien),
instructions verify (23 Instruktionen, 7 Skills) und 1276 Tests gruen. Kein neuer Test: die
Aenderung ist Prosa in zwei Instruktionen und ein entfernter Abschnitt aus einem Type-Spec -
was hier mechanisch pruefbar waere, prueft docs verify bereits als Type-Spec gegen sein Schema.
Kein Grenzuebertritt: in beide Richtungen ein Drop-in. Die Korrektur gilt denen, die noch
upgraden - eine Instanz, die das Angebot bereits genommen hat, liest das Dokument nicht noch
einmal. Fuer sie lohnt der eine Befehl, mit dem der Schaden hier gefunden wurde:
grep -c '^## Authoring guidance' ueber types describe <name> fuer alle vier Typen, drei
bedeutet einen Rest-Abschnitt im eigenen Type-Spec.
dist upgrade: --take-release nimmt fuer einen lokal geaenderten Pfad die Release-Fassung
dist upgrade kannte zwei Antworten auf eine lokal geaenderte Datei und die dritte, die man
eigentlich will, war keine davon. --keep-local behaelt die Aenderung - und weil der neue Stamp
die Release-Digest trotzdem schreibt, wird dieselbe Datei bei jedem kuenftigen Upgrade erneut
gemeldet. Fuer eine Datei, die der Instanz gar nicht gehoert, ist das der dauerhaft falsche
Zustand. Der andere angebotene Weg, "reconcile them by hand first", hatte kein Werkzeug: im
getraceten 5.0.0-auf-6.0.0-Lauf war eine kb/CONTRACT.md durch ein Format-on-Save um
Tabellen-Whitespace verschoben, und das kostete eine Handkopie aus dem entpackten Tarball, einen
Commit nur zur Herstellung der Clean-Tree-Vorbedingung des naechsten Kommandos - und damit einen
rohen git commit, an AGENTS.md Invariante 5 vorbei, die fuer "ist ja nur eine Vorbedingung"
keine Ausnahme kennt.
--take-release <pfad> ist die fehlende Antwort: schreibe fuer diesen Pfad die Release-Fassung,
statt abzubrechen. Wiederholbar, weil der Pfad die Entscheidung benennt - --keep-local verliert
nichts, --take-release verwirft eine lokale Aenderung, und die zwei sind darum nicht symmetrisch
genug fuer ein pauschales Flag. Beide gelten pro Pfad und komponieren auf einem Aufruf, was der
gemischte Fall braucht: eine Datei zuruecksetzen, eine andere behalten. Ohne --keep-local bricht
ein blockierter Pfad, zu dem nichts gesagt wurde, weiter ab; ein --take-release-Pfad, der gar
nicht blockiert ist, wird abgelehnt - auch im --dry-run, denn das ist ein Fehler im Argument
und nicht ein Zustand des Baums, und ein still ignorierter Tippfehler haette ein erfolgreiches
Upgrade gemeldet und die Aenderung behalten, die verworfen werden sollte.
Anders als bei --keep-local ist die Drift danach weg und nicht bloss uebergangen: die Datei
stimmt wieder mit der Digest ueberein, die der Stamp fuehrt, und verschwindet aus der Meldung.
Dazu die Abbruchmeldung selbst, die den Fehlgriff mitverursacht hat. Sie nannte --keep-local und
"reconcile by hand", sagte aber nicht, dass es zu --keep-local kein Gegenstueck gibt - der Lauf
kuendigte woertlich an, "I'll let the upgrade take the release's version", und rief das Kommando
ohne Flag auf. Jetzt nennt sie alle drei Antworten mit fertig eingesetzter Kommandozeile, im Muster
des Mass-Update-Gates, und sagt ausdruecklich, dass keine davon der Default ist.
instructions/upgrade-instance.md Schritt 6 traegt entsprechend nicht mehr die Drei-Schritt-Handreparatur, sondern die Entscheidung und den Dry-Run, mit dem man sie vorher sieht.
version notes antwortet auf einer ausgelieferten Instanz aus dem Release-Feed
version notes liest die lokale CHANGES.md. Eine ausgelieferte Instanz bekommt die aber als
neunzeiligen Stub ohne einen einzigen Versionseintrag, und CHANGES.md steht in
chemenu.ownership.is_upgrade_preserved - dist upgrade ueberschreibt sie also nie. Der Stub
bleibt der Stub, dauerhaft. Der Befehl konnte dort nicht nur heute nicht antworten, sondern nie,
und das an genau der Stelle, an der die Antwort am meisten zaehlt: dem Grenzuebertritt, vor dem
Breaking Change: und Migration: gelesen werden muessen. Der getracete
5.0.0-auf-6.0.0-Lauf kam nur weiter, weil er die Release-Notes ueber einen MCP-Server holte - ein
Weg, den die Anleitung nicht nannte und den eine Instanz ohne erreichbaren Server gar nicht hat.
Fehlt der Eintrag lokal, fragt der Befehl jetzt den Feed aus update_url - denselben, den
version check benutzt - und druckt den body des Release, den release.yml im Ursprungs-Repo
ohnehin aus version notes baut. Drei Praezisierungen halten das von einem stillen Netzaufruf
auseinander:
- Nur mit Release-Stamp. Ein Baum ohne
.wikitool-release.jsonist ein Dev-Checkout und behaelt die alte Fehlermeldung. Damit kann der neue Pfad im Ursprungs-Repo und in CI nicht betreten werden - auch nicht vonrelease.ymls eigenemversion notes. - stdout traegt nur die Notes. Die Zeile, welcher Feed gefragt wird, und die, welche Version
geantwortet hat, gehen nach stderr.
release.ymlleitet stdout in die Datei um, die es als Release-Body postet; alles andere dort waere Inhalt im Release. --offlineverweigert den Aufruf und scheitert mit derrelease_urlaus dem Stamp. Dieselbe Seite nennt auch jeder Fehlerfall des Feeds, damit ein Lauf, der die Notes nicht lesen kann, wenigstens weiss, wo sie stehen. Ein leererbodyist ebenfalls ein Fehler: eine leere Antwort darf nicht als "dieses Release hat nichts zu melden" durchgehen.
Gefragt werden kann nur das neueste Release: update_url ist die einzige URL, die der Stamp
fuehrt, und eine /releases/tags/<tag>-URL daraus zusammenzusetzen waere eine geratene
API-Form statt einer gelesenen (Invariante 7). Antwortet der Feed eine andere Version als die
gefragte, wird das auf stderr benannt und die Notes werden trotzdem gedruckt - das ist nicht der
Randfall, sondern der Hauptfall, weil die Notes vor dem Tausch gelesen werden, wenn VERSION
noch das Release nennt, das verlassen wird.
instructions/upgrade-instance.md Schritt 2 und INSTALL.md § "Version und Updates" tragen
entsprechend nicht mehr den Hinweis, dass der Befehl auf einer Instanz nicht antwortet; damit ist
auch die letzte der beiden Werkzeugluecken aus dieser Instruktion heraus, und ihr Vorwort nennt
keine mehr.
Bei der Gelegenheit zwei Eintraege aus tools/CONTRACT.md § "Future considerations (not
implemented)" entfernt, die dort seit ihrer Umsetzung falsch standen: der MCP-Server-Wrapper und
dist upgrade selbst. Beide sind im selben Dokument weiter oben als existierend beschrieben.
Session-Id-Fallback: Harness-Variable schliesst die Luecke zwischen Telemetrie-Join und Iteration-Budget-Gate
Gemessen an einem getracten Lauf (33 wikitool-Aufrufe, eine Sitzung): unter Claude Code, dessen
Bash-Tool jeden Aufruf in einer frisch initialisierten Shell ausfuehrt, fiel chemenu.session
ohne gesetztes WIKITOOL_SESSION_ID auf os.getppid() zurueck - eine neue "Sitzung" pro Aufruf.
Der Lauf zerfiel so in 21 Telemetrie-Buckets (hoechster Bucket: 3 von 33 Aufrufen), und das
Iteration-Budget-Gate (60 Aufrufe, Loop-Breaker bei 3 identischen in Folge) sah nie mehr als 3 von
60 - strukturell unerreichbar, obwohl AGENTS.md es als eine der vier code-durchgesetzten
Sicherungen fuehrt. Derselbe Bruch traf den Telemetrie-Join: Hook-Events (prompt.submitted)
trugen die Harness-UUID, wikitool.call-Events die wechselnde PID - kein gemeinsamer Schluessel,
und eval score bewertete 1-3 Aufrufe statt 33.
chemenu.session bekommt eine dritte Stufe zwischen der expliziten Variable und dem
PID-Fallback: eine kleine Registry bekannter Harness-Session-Variablen (HARNESS_ENV_VARS),
heute mit einem verifizierten Eintrag, CLAUDE_CODE_SESSION_ID. Verifiziert heisst: gegen eine
echte Sitzung gemessen, dass die Variable ueber Tool-Aufrufe hinweg stabil bleibt (anders als die
Shell-PID) und exakt der Wert ist, den der UserPromptSubmit-Hook in die Trace schreibt - der
Wert wird unveraendert als Schluessel uebernommen, kein Praefix, keine Umschreibung, sonst waere
der Join wieder zerstoert. Ein Eintrag wird nur nach genau dieser Verifikation aufgenommen: ein
Variablenname, der zufaellig existiert und etwas anderes bedeutet, waere ein stillerer Fehler als
der PID-Fallback, den er ersetzt.
run_budgets Zustandsdatei (budget.json) traegt je Eintrag jetzt die Herkunft seiner Id; faellt
dieselbe Id-Zeichenkette unter eine andere Herkunft als die gespeicherte, beginnt ein neuer
Zaehler statt einen fremden zu erben - ein Eintrag ohne das Feld (vor dieser Aenderung
geschrieben) behaelt seinen Count unveraendert. doctor ist jetzt dreiwertig (OK fuer eine
explizite Variable oder eine erkannte Harness-Variable, WARN nur noch fuer den reinen
PID-Fallback), und sowohl budget status als auch der session.start-Event der wikitool-
Telemetriequelle nennen die Herkunft der Id.
Im selben Lauf gemessener Nebenbefund auf der Emitter-Seite: ein durch eine geschlossene Pipe
abgebrochener, ansonsten erfolgreicher Aufruf (... | head) stand mit exit_code: 1 in der
Trace - Click faengt BrokenPipeError selbst ab und erzwingt sys.exit(1), ununterscheidbar von
einem echten Fehler. cli.py installiert jetzt vor jedem Dispatch einen Wrapper um
stdout/stderr, der einen EPIPE-Schreibfehler schluckt, bevor Click ihn sieht, und markiert den
Trace-Eintrag stattdessen mit stdout_truncated: true bei unveraendertem, dem tatsaechlichen
Kommandoerfolg entsprechendem exit_code.
Reproduziert mit Tests, die echte Subprozesse statt In-Process-Aufrufe verwenden - os.getppid()
ist sonst ueber die Testlaufzeit hinweg konstant: 61 Aufrufe aus je eigenem Prozess mit nur der
Harness-Variablen loesen das Gate jetzt aus, drei identische ebenso den Loop-Breaker; vor dieser
Aenderung waeren beide Tests gruen und blind gewesen.
--minor: additiv (ein neues optionales source-Feld in budget.json, die Id faellt weiterhin
auf getppid() zurueck, wo keine Variable greift), keine der beiden Drop-in-Richtungen verletzt.
new: scaffold materializes a schema default only for a required field
tools/wikitool new instruction --name "x" schrieb bislang obligation: required in jede neue
Instruktion. obligation: ist ein Migrationsfeld (instructions/CONTRACT.md
§ instructions/migrations/) - eine gewoehnliche Instruktion ist keine Migration und hat nichts,
was laufen muesste. Ursache: new_page._build_frontmatter() materialisierte jedes
Schema-default: unbesehen; ueber alle acht types/*.schema.yaml gibt es genau zwei
(entity/concepts provenance, in required:; instructions obligation:, nicht).
Die Regel jetzt: ein Schema-default: wird nur fuer ein Feld materialisiert, das das Schema auch
in required: fuehrt. Auf einem optionalen Feld ist ein default: eine Lese-Annahme (was ein
fehlendes Feld bedeutet), keine Schreib-Vorgabe - sie hinzuschreiben macht aus der stillen
Annahme eine ausgesprochene Behauptung. instruction.obligations eigene Lese-Annahme steht
unveraendert und unabhaengig in kb_state.py (frontmatter.get("obligation") or REQUIRED).
Der array-Zweig direkt daneben (leere Liste fuer ein unbesetztes Array-Feld wie tags:) ist
davon ausdruecklich nicht betroffen - er bleibt fuer optionale wie Pflichtfelder gleich, weil ein
fehlender Schluessel sonst den Template-Filter-Suffix woertlich in den Body schreiben wuerde
({related|bullets} -> das Wort "bullets").
--patch: kein Bestandsdokument aendert sich (obligation: stand bislang nur explizit oder auf
den beiden Migrationsdokumenten), keine Migration noetig, und ein zurueckgerolltes Werkzeug
schriebe das Feld nur wieder mit.
Stale wiki/ path literals swept out of tools/ and types/, with a test guarding against the next rename
Die Wissensschicht wurde am 2026-08-21 von wiki/ nach kb/ umbenannt. Das Verzeichnis zog um,
die Zeichenkette nicht: 33 Stellen nannten weiter einen Pfad, den es nicht mehr gibt. Gemeldet
war davon eine - die Kopfzeile des Lint-Reports (Scanned N pages under `wiki/` ) - als
kosmetischer Einzelfall. Der Scan selbst war immer korrekt: run_lint(kb_dir) laeuft ueber
kb/, gezaehlt wird, was dort liegt. Falsch waren ausschliesslich die Beschriftungen.
Dreizehn davon sind nutzersichtbar. Die Fehlermeldungen von xref, cite, touch,
move, rm, rename, raw accept und log status nannten wiki/, ebenso die --help-Texte
von cite sync --all, provenance rebuild-index --dry-run und move --reconcile. Dazu die
description:-Felder in types/type-spec.schema.yaml, die ueber types describe und ueber jede
Schema-Validierungsmeldung bei einem Agenten landen. Zwei Stellen waren doppelt falsch:
git_publish.py und run_budget.py verwiesen auf wiki/concepts/Mass-Update Gate.md, waehrend
die Seite unter kb/concepts/workflows/Mass-Update Gate.md liegt - dort war auch die
Collection-Ebene veraltet.
Nicht angefasst: raw/ (unveraenderlich, was immer dort steht) und die Alteintraege dieser
Datei. Beide sind Aufzeichnungen dessen, was zu ihrer Zeit galt, keine Wegweiser - dieselbe
Unterscheidung, die instructions/dev/issue-tracking.md fuer den Tracker trifft.
Dass es vier Wochen unbemerkt blieb, ist der eigentliche Befund: kein Check liest ein Pfadliteral
in Quelltext. docs verify kam dafuer nicht in Frage, weil es shipped_prose() liest, also
Markdown - der Grossteil des Defekts sass in .py-Zeichenketten. Der Guard ist deshalb ein Test:
tools/chemenu/tests/test_source_hygiene.py scannt jede .py-Datei unter tools/chemenu/ sowie
tools/wikitool gegen eine Tabelle stillgelegter Stufenpfade. Die naechste Umbenennung traegt
dort eine Zeile nach und bekommt jede vergessene Stelle als Testfehler, statt als Zeichenkette,
die ein Jahr lang niemand liest. Die zwei Ausnahmen stehen bewusst als Liste mit Begruendung und
nicht als geschickteres Muster: eine Fixture-URL, in der wiki ein Repository-Name ist, und die
Guard-Datei selbst, die die stillgelegten Pfade ja gerade deklariert.
kb/entities/projects/Chemenu.md trug denselben Fehler in einer Kerndaten-Zeile und wurde ueber
touch nachgezogen. "Dreilagig" blieb dort stehen: das deckt sich mit der Concept-Seite
Three-Layer Architecture, die reports/ ausdruecklich als vierte Phase neben den drei
Schichten fuehrt.
--patch: keine Schnittstelle aendert sich, kein Verhalten, keine Migration. Ein
zurueckgerolltes Werkzeug gibt nur wieder die alten Beschriftungen aus.
CHANGES.md/Guard-Docstring: die Zahl der nachgezogenen Pfadliterale korrigiert (33, nicht 27)
Der Eintrag darueber nannte 27 nachgezogene Stellen und "rund die Haelfte davon nutzersichtbar".
Beides war falsch. Die 27 stammten aus einem wc -l, das nur tools/**/*.py gezaehlt hatte -
tools/wikitool, types/type-spec.md und die vier description:-Felder in
types/type-spec.schema.yaml fehlten darin. Nachgezaehlt am Commit selbst
(git show <sha> | grep -c '^-.*wiki/'): 33, davon 32 im Stack und eine auf der Seite
Chemenu. Nutzersichtbar sind davon dreizehn, also gut ein Drittel und nicht die Haelfte.
Derselbe Zahlendreher stand im Docstring von tools/chemenu/tests/test_source_hygiene.py, wo er
kuenftigen Lesern erklaert, wogegen der Guard schuetzt - dort ebenfalls korrigiert. Dass diese
Korrektur einen eigenen Bump braucht, ist kein Formalismus: der Docstring liegt unter tools/,
und das Version-Gate in .gitea/workflows/ci.yml ist nach Pfad geschnitten, nicht nach Absicht.
--patch: reine Prosakorrektur, kein Verhalten, keine Schnittstelle.
6.0.1 - 2026-09-16 - docs toc/verify erreichen die .template-Form einer Referenzdatei
Author: Torben Nehmer
High impact
- docs toc/verify erreichen die .template-Form einer Referenzdatei
docs toc/verify erreichen die .template-Form einer Referenzdatei
kb/CONVENTIONS.md.template war 105 Zeilen lang und trug keine TOC-Region. toc.target_files()
berechnete den Dateisatz ueber die adoptierten Namen, und eine Datei auf .md.template faellt
aus jedem dieser Walks heraus - also hat docs toc --apply das Template nie angefasst und
docs verify es nie gelesen. Eine Instanz, die es nach instructions/setup-instance.md
adoptiert, bekam damit eine kb/CONVENTIONS.md ohne Region und fiel am docs verify in
Schritt 13 derselben Anleitung um - dem Befehl, mit dem das Setup endet. Ausgeliefert war das in
6.0.0.
Eine in Scope stehende Datei nimmt ihr <name>.template jetzt mit hinein: das Template ist
dasselbe Dokument einen Schritt frueher in seinem Leben, und wer es auslaesst, laesst die
adoptierte Kopie den Fehler erben. docs verify prueft im Ursprungs-Repo damit 57 statt 56
Referenzdateien, in einer frisch exportierten Instanz 59.
Ausgeloest hat es ein Wachstum um sechs Zeilen: f350999 hat das Template von 99 auf 105 Zeilen
gebracht und damit ueber die Schwelle von 100. Seither war ci.yml auf jedem Push rot (Laeufe
279 bis 289) - was als Flackern gelesen wurde, weil jeder Push zusaetzlich einen gruenen
release.yml-Lauf erzeugt und die Paare wie Lauf und Wiederholung aussehen. Sie sind zwei
verschiedene Workflows.
Grenzuebertritt-Frage geprueft und verneint, gegen den dokumentierten Update-Weg: das Template ist
stack-eigen (ownership.is_stack_owned - jede .template unter einer Content-Stage), steht nicht
in UPGRADE_PRESERVED_PATHS, und dist upgrade schreibt es damit mit. Eine Instanz bekommt das
reparierte Template also durch den Upgrade selbst, ohne Handarbeit; der Rueckweg funktioniert
ebenso, weil die alte Maschinerie das Template gar nicht erst prueft. Handarbeit faellt nur an, wo
eine Instanz ihr stack-eigenes Template lokal veraendert hat - dist upgrade meldet genau das als
blocked und verlangt --keep-local.
Verifiziert: docs verify/instructions verify gruen, 1275 Tests gruen (3 neu: das Template einer
in Scope stehenden Datei steht im Dateisatz, ein .template ohne solche Datei daneben nicht
(USER.md.template), und ein Template ueber der Schwelle ohne Region ist ein Befund - der letzte
waere am heutigen Stand rot gewesen). Dazu der vollstaendige setup-instance.md-Replay gegen einen
frischen dist export: doctor, docs verify, instructions verify und lint laufen in der
frischen Instanz durch.
6.0.0 - 2026-09-15 - search: Pfad und Titel vollstaendig, Trunkierung sichtbar
Author: Torben Nehmer
Breaking Change:
- docs verify loest ab dieser Version jeden relativen Markdown-Link in den Referenzdateien auf und meldet ein totes Ziel als Fehler - auch in kb/CONVENTIONS.md und kb//COLLECTION.md, die eine Instanz selbst besitzt und die ein Drop-in-Copy der Maschinerie nicht ersetzt. Eine Instanz, deren eigene Konventions- oder Collection-Datei einen relativen Link mit falscher ../-Tiefe oder auf eine inzwischen geloeschte Datei traegt, sieht docs verify nach dem Update fehlschlagen, wo es vorher durchlief. Reparatur: den in der Meldung genannten Datei:Zeile-Link korrigieren - kein Werkzeuglauf, keine Inhaltsmigration.
- docs verify prueft die TOC-Region ab dieser Version auch auf types/.md und docs/.md. Eine Instanz, die die Page-Type-Spec-Templates adoptiert hat, traegt types/source.md und types/concept.md ohne Region und sieht docs verify nach dem Update fehlschlagen, wo es vorher durchlief; dasselbe gilt fuer eine selbst angelegte oder lokal geaenderte docs/-Seite ueber 100 Zeilen. Reparatur: tools/wikitool docs toc --apply - ein Werkzeuglauf, keine Inhaltsmigration.
Migration: none required - Keine kb/-Seite aendert ihre Form. Der Grenzuebertritt ist ein strengerer Check auf instanz-eigener Prosa, keine Schema- oder Frontmatteraenderung.
High impact
- SKILL.md: relative Links durch repo-root-relative Pfade ersetzt, docs verify/instructions verify pruefen Linkziele
- docs verify: der Linkziel-Check erreicht auch die instanz-eigenen kb/CONVENTIONS.md und COLLECTION.md - daher Grenzuebertritt
- TOC-Scope auf types/ und docs/ erweitert, Sprachregeln zentralisiert, --breaking akkumuliert
- types/: Seiten-Type-Spec-Anleitungsprosa in stackeigene guidance-Datei ausgelagert
Medium impact
- docs verify: ein nur als .template ausgeliefertes Linkziel gilt als aufgeloest
- Control-Plane-Sprache universell: Achse ist das Publikum, kein Instanz-Schalter
- types/type-spec.schema.yaml enforced against real type-spec frontmatter
- search: Pfad und Titel vollstaendig, Trunkierung sichtbar
Low impact
- gates.md/session-setup.md: die Budget-Ausnahme von version regrade haengt an der Aufrufform
- types/type-spec.md: Ownership und Sprache getrennt benannt (Nachzug zu #99)
Ausgangspunkt war ein realer Bruch: instructions sync kopiert jede SKILL.md in eine andere
Verzeichnistiefe, und 52 von 58 relativen Links darin zeigten in der publizierten Kopie ins Leere,
unbemerkt, weil kein Check je ein Linkziel gelesen hat. Die Reparatur - repo-root-relative Pfade
statt ../-Links - zieht zwei neue mechanische Checks nach sich (instructions verify verbietet
relative Links in SKILL.md, docs verify loest Linkziele in allen Referenzdateien auf), und der
zweite Check erreicht auch instanz-eigene Prosa (kb/CONVENTIONS.md, COLLECTION.md), die ein
Drop-in-Copy nicht ersetzt - der Grenzuebertritt, der diesen Kandidaten auf 6.0.0 eskaliert hat.
Denselben Linkziel-Check bekommt die TOC-Pflicht gleich mit auf types/ und docs/ erweitert, und
die Sprachregeln fuer die Control-Plane sind zu einer einzigen, publikumsbasierten Regel in
AGENTS.md zentralisiert statt eines Instanz-Schalters. Daneben, unabhaengig vom Linkproblem: die
Seiten-Type-Spec-Anleitungsprosa ist in eine stackeigene Guidance-Datei ausgelagert,
type-spec.schema.yaml wird jetzt gegen echte Type-Spec-Frontmatter durchgesetzt, und search
zeigt Pfad und Titel eines Treffers vollstaendig statt trunkiert.
gates.md/session-setup.md: die Budget-Ausnahme von version regrade haengt an der Aufrufform
Doku-Nachzug zu 5.1.0. Beide Dateien beschrieben die Budget-Ausnahme als feste Liste pro
Kommandoname ("fixed allowlist"); version regrade ist die erste Ausnahme, die nur in einer
Aufrufform liest - bar listet sie, mit Positionen schreibt sie CHANGES.md. Die Liste selbst
bleibt an ihrem einen Ort (tools/CONTRACT.md), beide Stellen benennen jetzt aber, dass dort ein
Eintrag pro Aufruf statt pro Namen gilt. Aufgefallen in der Schlussphase derselben Arbeit, deshalb
ein eigener Patch-Bump: der Pfad instructions/ liegt im Version-Gate der CI.
SKILL.md: relative Links durch repo-root-relative Pfade ersetzt, docs verify/instructions verify pruefen Linkziele
instructions sync kopiert jede SKILL.md byteidentisch in .agents/skills/ und
.claude/skills/ - eine andere Verzeichnistiefe als die Quelle, ohne deren Nachbardateien. 52 von
58 relativen Markdown-Links in den sieben Skills zeigten deshalb in der publizierten Kopie ins
Leere, unbemerkt, weil kein Check je ein Linkziel gelesen hat (Gitea-Meldung: ein session-setup.md-Read
schlug in einer ausgelieferten Instanz fehl). Alle 58 Links sind jetzt repo-root-relative
Klartextpfade (instructions/session-setup.md statt [session-setup.md](../session-setup.md)) -
sie ueberleben die Kopie unveraendert, weil sie nicht von der Position der lesenden Datei abhaengen.
instructions/CONTRACT.md § "A skill's outbound reference is a plain path, not a link" traegt die
Regel.
Zwei neue mechanische Checks verhindern das Wiederauftreten: instructions verify verbietet jeden
relativen Markdown-Link in einer SKILL.md (check_skill_reference_paths), docs verify loest
jeden relativen Link in den flachen Instructions und Contracts gegen den Arbeitsbaum auf
(check_reference_targets, ueber denselben Dateisatz wie docs toc). Nebenbei behoben:
instructions/dev/doc-pull-through.md hatte zwei Links mit falscher ../-Tiefe, unabhaengig vom
Skill-Copy-Problem.
docs verify: der Linkziel-Check erreicht auch die instanz-eigenen kb/CONVENTIONS.md und COLLECTION.md - daher Grenzuebertritt
Nachtraegliche Neueinstufung des Bumps darueber, kein zusaetzlicher Code. check_reference_targets
laeuft ueber den Dateisatz von docs toc, und fuenf Dateien darin gehoeren der Instanz statt dem
Stack: kb/CONVENTIONS.md und die vier kb/<collection>/COLLECTION.md. Ein Drop-in-Copy von
tools/, types/, instructions/ und AGENTS.md ersetzt sie nicht - ein toter relativer Link
darin laesst docs verify nach dem Update fehlschlagen, wo es vorher durchlief. Genau die Form,
die tools/README.md § Adding a command Schritt 5 als MAJOR-Zeile benennt ("a stricter check that
newly fails on shipped content an instance already had"), und instructions/dev/version-parts.md
entscheidet den Zweifelsfall zugunsten des Grenzuebertritts.
Gemessen bricht heute nichts: die zweite bekannte Instanz traegt 25 relative Links in diesen fuenf
Dateien, davon null tote; dieses Repo ebenso. Die Einstufung folgt der Reichweite des Checks, nicht
einem beobachteten Schaden - der Preis einer unnoetigen MAJOR ist eine Release-Notiz, der Preis
einer MINOR, die doch bricht, ist eine Instanz mit fehlschlagendem Update-Pfad unter einer
Versionsnummer, die Drop-in versprochen hat. Aufgefallen ist es in der Schlussphase beim Lesen der
eigenen Regel in tools/README.md, nicht durch einen Check - was docs/version-model.md ueber
genau diese Stelle sagt ("a person looking at the diff ... not a validator"), hat sich hier
wiederholt.
docs verify: ein nur als .template ausgeliefertes Linkziel gilt als aufgeloest
Defektbehebung am Check aus den beiden Bumps darueber, gefunden unmittelbar nach deren Publish.
check_reference_targets meldete auf einem frisch exportierten Baum 13 tote Links - kb/CONTRACT.md
neunmal, dazu german-terminology.md, kb-profiles.md und link-taxonomy.md - und zwar dafuer,
dass der Export tut, was er soll. kb/CONVENTIONS.md und die vier kb/<name>/COLLECTION.md sind
instanz-eigen: die Distribution traegt <name>.template, und die Instanz uebernimmt sie erst im
Personalisierungsschritt von instructions/setup-instance.md durch Umbenennen. Zwischen
dist export und diesem Schritt existiert die fertige Datei berechtigterweise nicht, waehrend die
stack-eigenen Dateien sie unter ihrem kuenftigen Namen verlinken - korrekt, denn so wird sie heissen.
Ein Linkziel gilt jetzt auch dann als aufgeloest, wenn daneben <ziel>.template liegt. Die
Ausnahme ist eng: fehlt beides, bleibt es ein Befund. Damit beschreibt der Check nicht laenger
"noch nicht personalisiert" als "kaputter Link" - diesen Zustand meldet doctor unter
conventions praezise und zustaendig.
CI war davon nie rot: der Replay in .gitea/workflows/ci.yml uebernimmt die Templates, bevor er
docs verify aufruft, und der dokumentierte Weg in setup-instance.md stellt die Personalisierung
(Schritt 5/6) ebenfalls vor die Verifikation (Schritt 13). Getroffen haette es jeden, der nach dem
Export einmal zur Kontrolle docs verify aufruft. Aufgefallen ist es, weil die Verifikation des
vorherigen Publishes den Arbeitsbaum geprueft hatte und nicht den exportierten - ausgerechnet bei
einer Aenderung, deren ganzer Gegenstand Kopien in anderer Verzeichnistiefe sind.
TOC-Scope auf types/ und docs/ erweitert, Sprachregeln zentralisiert, --breaking akkumuliert
Drei Straenge, ausgeloest von einer Beobachtung: manche agentengeladene Referenzdatei trug keine TOC, und manche Instruction war teilweise deutsch.
TOC-Scope. Die Pflicht aus 5.0.0 galt fuer AGENTS.md, die Stage-Contracts,
kb/CONVENTIONS.md, jede COLLECTION.md und die flache instructions/**.md-Form. docs/ und die
Seiten-Type-Specs fielen ohne genannten Grund heraus - waehrend SKILL.md und die Menschendoku
ihren Ausschlussgrund im Docstring stehen hatten, was die beiden anderen Luecken wie Absicht
aussehen liess. docs/ ist dabei genau der Fall, fuer den die Schwelle existiert:
AGENTS.md § File naming fuehrt es als agentengeladen per Link, also am zweiten Hop. Beide sind
jetzt drin; vier Dateien haben eine Region bekommen. SKILL.md bleibt die eine Ausnahme, und
zwar belegt statt behauptet: die vendorte Guidance setzt den SKILL.md-Body auf die Ladeebene, die
beim Triggern ganz gelesen wird, und richtet ihren eigenen TOC-Rat an die gebuendelten
Referenzdateien daneben. Ein Type-Spec wird zwar auch ganz geladen, aber eben auch als Datei
gelesen - deshalb traegt es eine Region, und types describe strippt sie aus seiner Ausgabe, weil
dort der ganze Body ohnehin mitkommt.
Sprache. Die Regel gab es schon ("the control plane stays English"), sie stand nur in
kb/CONVENTIONS.md - einer Datei, die der Instanz gehoert und die sie umschreiben darf, waehrend
die Regel stackeigene Dateien bindet. Sie ist nach AGENTS.md § File naming gezogen, zusammen mit
einer zweiten, die vorher gar nicht geschrieben stand: ein Agent spricht die KB-Sprache der
Instanz. Der Wert dafuer lebt weiter in kb/CONVENTIONS.mds language:; SOUL.mds eigene
Sprache:-Zeile war damit eine Dublette und ist weg. instructions/setup-instance.md - 297 Zeilen,
die einzige vollstaendig deutsche Instruction, verbatim an jede Instanz ausgeliefert - ist
uebersetzt, samt description:. Die zwei deutschen Blockquotes in den Dev-Skills sind es auch; sie
lesen sich jetzt als englisches Modell der Nachricht, die der Agent in der KB-Sprache ausspricht.
Dieselbe Regel gilt fuer alles, was als Template ausgeliefert wird - eine Instanz adoptiert es,
bevor sie ihre Sprache ueberhaupt gewaehlt hat. USER.md.template, SOUL.md.template und
ENVIRONMENT.md.template waren vollstaendig deutsch und sind uebersetzt; kb/sources/ und
kb/concepts/COLLECTION.md waren es in Teilen und ziehen jetzt mit kb/entities/ und
kb/comparisons/ gleich, die es laengst waren. Bei den vier Seiten-Type-Specs laeuft der Schnitt
mitten durch die Datei, und zwar entlang derselben Prosa/Identifier-Grenze, die kb/CONTRACT.md
schon innerhalb einer Seite zieht: die Anleitungsprosa ist Anweisung an einen Agenten und damit
Control Plane, der ## Template-Block und die layout:-Titel sind Seitentext und bleiben in der
KB-Sprache - wikitool new entity scaffoldet also weiter deutsche Ueberschriften.
kb/CONVENTIONS.md behauptete bis hierher, die Type-Specs folgten als Ganzes der KB-Sprache; der
Satz ist auf den tatsaechlichen Schnitt nachgezogen.
Mechanisch geprueft wird nichts davon: ein Stoppwort-Scan schluege auf dem zitierten Vokabular in
kb-profiles.md und link-taxonomy.md falsch an. Stattdessen nennen
instructions/CONTRACT.md § "Writing an instruction" und stack-dev die Regel an der Stelle, an
der sie befolgt oder verloren wird.
--breaking akkumuliert. Bis hierher ersetzte ein zweites --breaking die Zeile des
Kandidaten - der Eintrag versprach dann einen Bruch und lieferte zwei. Genau dieser Kandidat ist der
Fall: sein Linkziel-Uebertritt aus beta.1 und der TOC-Uebertritt von hier sind zwei Dinge, auf die
ein Betreiber getrennt reagieren muss. Eine Begruendung bleibt flach auf der Markerzeile, ab der
zweiten werden es Bullets; eine vor dieser Aenderung geschriebene Einzelzeile liest sich unveraendert
als Ein-Element-Liste zurueck, also musste kein bestehender Eintrag angefasst werden. --migration:
bleibt bewusst eine Einzelzeile - sie beantwortet eine Ja/Nein-Frage ueber den Kandidaten als Ganzes,
und --migration-required ist ihr Ruecknahmepfad. Fuer eine falsche Breaking-Begruendung gibt es
keinen; der Kandidat ist bis zum Release dev-lokal.
Nebenbefund, den die Scope-Erweiterung sofort aufgedeckt hat: docs/version-model.md verlinkte nach
instructions/dev/version-parts.md, das dist export wegschneidet - im Ursprungs-Repo gruen, in
jeder ausgelieferten Instanz ein toter Link. Jetzt ein Klartextpfad mit dem Satz, warum er keiner
ist.
types/type-spec.md: Ownership und Sprache getrennt benannt (Nachzug zu #99)
types/type-spec.md sagte weiterhin, Prosa, ## Template-Body und Sprache eines
Seiten-Type-Specs gehoerten der Instanz, die bei anderer KB-Sprache "einfach die Datei
uebersetzt" - genau das Gegenteil des Schnitts, den der Bump davor ausgeliefert hat. Aufgefallen
in der Schlussphase, beim Nachdenken darueber, welche Sprachregel fuer einen instanz-eigenen
neuen Seitentyp gilt.
Der Abschnitt trennt die zwei Fragen jetzt: Ownership sagt, wer eine Zeile aendern darf, die
Sprache folgt davon unabhaengig dem Publikum der Zeile - Anleitungsprosa an einen Agenten ist
Control Plane und englisch, ## Template-Body und layout:-Titel sind Seitentext in der
KB-Sprache, Feldnamen und Enum-Werte sind Identifier und werden nie uebersetzt. Als Tabelle, weil
der Schnitt mitten durch eine Datei laeuft und eine Aufzaehlung im Fliesstext ihn genau deshalb
nicht haelt. Der Satz bindet ausdruecklich auch einen Type-Spec, den eine Instanz sich selbst
schreibt: der ist zwar durchgaengig instanzeigen, aber seine Anleitungshaelfte hat trotzdem einen
Agenten als Leser.
Control-Plane-Sprache universell: Achse ist das Publikum, kein Instanz-Schalter
Die Sprachregel in AGENTS.md ruhte auf einer Begruendung, die schmaler war als sie selbst:
"Every file in the table above belongs to the stack and ships to instances that share none of
this instance's language choices, so:". Das traegt nur fuer ausgeliefertes Material und laesst
offen, was fuer ein Control-Plane-Dokument gilt, das eine Instanz nur fuer sich selbst schreibt -
eine eigene Instruction, ein selbst angelegter Seitentyp (types/ nimmt einen ohne Code-Aenderung
auf), ein weiterer Stage-Contract. Genau dort fallen Ownership und Publikum auseinander: die Datei
ist durchgaengig instanzeigen, ihre Anleitungshaelfte hat trotzdem einen Agenten als Leser.
Der Vorsatz nennt jetzt die tatsaechliche Achse - die For-Spalte der Tabelle darueber, also wer
die Zeile liest, und weder wem die Datei gehoert noch ob sie den Checkout je verlaesst. Regel 1
sagt ausdruecklich, dass sie auch fuer ein nie ausgeliefertes Control-Plane-Dokument gilt und dass
es neben kb/CONVENTIONS.mds language: bewusst keinen zweiten Sprachwert gibt.
kb/CONVENTIONS.md und ihr .template sagen dasselbe von ihrer Seite aus: die
Control-Plane-Sprache ist keine Einstellung, die diese Datei zurueckhaelt - es ist gar keine.
Die Begruendung dazu steht als neue docs/-Seite
(docs/language-boundaries.md), weil sie sonst in einem Jahr neu
verhandelt wird: warum Englisch (der Stack redet fast nur ueber Identifier, und die sind
englisch), warum kein Parameter (die Kosten traegt jede Datei, den Nutzen haette ein Dokument, das
ohnehin nur ein Agent liest), und was die Entscheidung wieder aufmachen wuerde. Die Seite haelt
zugleich fest, welches Argument falsch war: "Sprache folgt der Ownership" hat funktioniert,
solange nur ausgeliefertes Material betrachtet wurde, und faellt am instanz-eigenen Typ.
Nebenbei zwei Befunde derselben Ecke behoben. Der Docstring von
dist_cmd.instance_owned_type_stems() behauptete weiter, "its prose, its template and its
language are the instance's business" - Stand vor dem TOC-/Sprach-Bump oben. Und die
Aufzaehlung der docs/-Seiten in AGENTS.md sagte "Four pages exist today", waehrend das
Verzeichnis fuenf trug: docs/model-and-effort-selection.md fehlte, und zwar absichtlich, weil
ein Link dorthin die Claude-Code-eigene Entscheidung in die anderen drei Harnesses laden wuerde.
Der Satz zaehlt jetzt, was von hier aus verlinkt ist, und benennt die sechste Seite samt Grund.
types/: Seiten-Type-Spec-Anleitungsprosa in stackeigene guidance-Datei ausgelagert
Ein root: kb Type-Spec (entity, concept, source, comparison) hatte zwei Publika in
einer Datei: Anleitungsprosa fuer den Agenten (When to use/When NOT to use/Authoring guidance),
und Seitenmaterial (## Template-Block, layout:-Titel). Ownership gilt pro Datei, also wurde
die ganze Datei beim Setup als .template adoptiert und danach nie wieder angefasst - eine
Instanz, die ihre Type-Specs frueh adoptiert hat, las bis in alle Zukunft die Anleitung vom Tag
ihrer Erzeugung, weil dist upgrade das .template neben die adoptierte Datei schrieb, nie die
Datei selbst (docs/ownership-and-templates.md § "Where the file boundary strains").
Der urspruengliche Vorschlag drehte den Schnitt um (Type-Spec stackeigen, Seitenmaterial heraus)
und wurde beim Pruefen gegen setup-instance.md und evolve-subtypes.md verworfen: die
Frontmatter-Konfiguration (layout:, Enum-Werte, base_dir) ist instanzeigener Inhalt, keine
Stack-Maschinerie - beide Instructions weisen die Instanz an, Enum und layout:-Eintrag in
derselben Aenderung zu setzen. Stattdessen bleibt der Type-Spec instanzeigen, und nur die
maschinenabgeleitete Anleitungsprosa zieht in eine neue, stackeigene types/<name>.guidance.md,
verknuepft ueber ein optionales guidance:-Frontmatterfeld (neuer, nicht instanziierbarer Typ
type-guidance, wie lint-report ohne base_dir:). tools/wikitool types describe <name>
komponiert beide Haelften weiterhin zu einer Antwort - ein Agent muss nie wissen, dass ein Typ aus
zwei Dateien besteht. type_resolver.extract_template() liest das Template unveraendert allein
aus types/<name>.md; kein zweiter Ladepfad fuer wikitool new.
dist_cmd._plan_types()/find_leaks() teilten sich vorher name.split(".", 1)[0] als
Stamm-Berechnung - beides haette entity.guidance.md faelschlich als instanzeigenen Stamm
"entity" erkannt (die eine haette sie zum .template gemacht, die andere sie als Leak gemeldet).
Neuer gemeinsamer Prädikat _owned_type_stem() prueft die exakte Endung (<stem>.md oder
<stem>.schema.yaml), nicht den ersten Punkt.
Grenzuebertritt-Frage bewusst geprueft und verneint: Drop-in in beide Richtungen (ein Type-Spec
ohne guidance: verhaelt sich unveraendert, eine alte Maschinerie liest types/<name>.md wie
zuvor und die Guidance-Datei ist fuer sie inert), also --minor statt --major. Die einmalige
Adoption in einer bestehenden Instanz ist als instructions/migrations/6.0.0-type-guidance-split.md
dokumentiert - obligation: offered, der erste Gebrauch dieses seit 4.0.0 existierenden, bis jetzt
unbenutzten Mechanismus fuer ein instanzeigenes, upgradebares Machinery-File.
Verifiziert: tools/wikitool docs verify/instructions verify gruen, 1261 Tests gruen (8 neu:
get_guidance, das Template bleibt auf types/<name>.md allein geladen, die Guidance-Datei
schifft verbatim neben einem .template-adoptierten Type-Spec statt als weiteres .template,
ein dist upgrade schreibt verbesserte Guidance-Prosa in eine adoptierte Instanz obwohl deren
Type-Spec selbst nie im Stamp stand, types describe komponiert beide Haelften in JSON und
Textausgabe getrennt nachweisbar).
types/type-spec.schema.yaml enforced against real type-spec frontmatter
Bei der Vorbereitung der Aenderung oben fiel auf: types/type-spec.schema.yaml traegt
additionalProperties: false, kannte aber root: und capture_fields: nicht, obwohl
types/instruction.md bzw. types/source.md beide Felder tragen und type_resolver.get_root()/
get_capture_fields() sie lesen. Gegen das Schema validiert waeren beide Type-Specs ungueltig
gewesen. Dass es niemandem auffiel, war der eigentliche Befund: Type-Spec-Frontmatter wurde
nirgends gegen sein eigenes Schema validiert - resolver.validate_frontmatter() lief nur ueber
kb/-Seiten, neu erzeugte Seiten und Instruktionsdateien, nie ueber die Type-Specs selbst.
TypeResolver._validate_type_spec(), der einzige Weg, den der Selbstbezug type: types/type-spec.md
nimmt, prueft ausschliesslich, ob type/name/description vorhanden sind.
Beide fehlenden Felder ergaenzt (root: als Enum kb/repo, capture_fields: als Liste wie
page_ref_fields:), dazu guidance: (seit der Aenderung oben real benutzt, aber noch nie im
Schema). docs verify bekommt eine neue Pruefung: jede Datei unter types/ mit
type: types/type-spec.md validiert jetzt gegen types/type-spec.schema.yaml
(check_type_spec_frontmatter(), wiederverwendet resolver.list_type_specs() statt eines zweiten
Parse-Durchlaufs). types/type-spec.md § Validation Contract und die beiden docs verify-Zeilen
in tools/CONTRACT.md nennen das jetzt.
Daneben ein zweiter, unabhaengiger Befund derselben Aufraeumrunde behoben:
instructions/dev/doc-pull-through.md verwies fuer docs/-Seiten weiter auf "AGENTS.md § File
naming lists all four" - der Zaehler in AGENTS.md selbst war beim vorigen Bump schon auf fuenf
(plus eine sechste, nur von CLAUDE.md aus verlinkte) korrigiert worden, diese eine verbliebene
Stelle nicht.
Grenzuebertritt-Frage geprueft und verneint: additiv in beide Richtungen - eine bestehende Instanz
validiert bereits (0 Befunde gegen den realen Baum), und ein Type-Spec ohne die drei neuen Felder
bleibt unveraendert gueltig. --patch, kein --breaking, keine neue Migration noetig.
Verifiziert: tools/wikitool docs verify/instructions verify gruen, 1263 Tests gruen (2 neu:
alle Type-Specs dieses Repos validieren gegen ihr eigenes Schema; ein Type-Spec mit einem dem
Schema unbekannten Feld wird gemeldet, mit Dateiname und Feldname in der Meldung).
search: Pfad und Titel vollstaendig, Trunkierung sichtbar
Gemeldet wurde eine Sitzung, die nach wikitool search zusaetzlich grep -rl ueber kb/
laufen liess. Der Grep war redundant - search ist ein rg-Lauf ueber kb/ und kann keine
Seite verfehlen, die ein Grep findet -, aber die Ausgabe gab ihr drei Gruende dafuer, und die
sind der eigentliche Befund.
Die Tabelle nannte keinen Pfad, obwohl wiki-query verlangt, nur die Seiten zu lesen, auf
die die Suche zeigt. Sie kappte ausserdem den Titel auf 34 Zeichen - im gemeldeten
Transkript vier von fuenf Treffern -, und der Titel ist nach Invariante 2 der einzige
Identifier einer Seite und das Argument, das touch, xref add und cite add nehmen. Die
Sitzung hatte also weder etwas zum Oeffnen noch etwas zum Weiterreichen; grep -rl lieferte
genau beides.
Drittens war N result(s). die gekappte Zahl: run_search gab nur die beschnittene Liste
zurueck, also konnte kein Adapter die Gesamtzahl melden, und 20 result(s). auf einer Anfrage
mit 182 Treffern war von einem vollstaendigen Ergebnis nicht zu unterscheiden. Eine
Vollstaendigkeitsaussage, zu der die Ausgabe nicht berechtigt war - der staerkste denkbare
Anlass, ihr zu misstrauen.
Die Zeile hat jetzt die Form score | kind/subtype | titel | pfad | summary, ohne
Spaltenauffuellung. Titel und Pfad werden nie gekappt; die Summary ist das einzige verlustige
Feld und steht deshalb am Ende, wo ein | in Prosa beim Trennen mit maxsplit=4 folgenlos
bleibt (ein | im Titel schliesst die Wikilink-Syntax ohnehin aus). JSON als Default-Ausgabe
wurde erwogen und verworfen: ein Treffer ist flach, JSON kostet dafuer ein Vielfaches an Tokens,
und search existiert dafuer, Retrieval billig zu machen - der Fehler war ein fehlendes Feld,
kein Parse-Problem. Wer Struktur braucht, hat --json, api.search und MCP.
run_search gibt jetzt ein SearchResult mit Treffern, Gesamtzahl und Limit zurueck. Die
Tabelle schreibt 50 of 182 result(s) - raise --limit (0 for all) or narrow the query., das
JSON traegt total/truncated/limit neben count, dessen Bedeutung unveraendert bleibt
(len(results)), und api.search sowie der MCP-search-Tool tragen dieselben Felder. Das
Default-Limit steigt von 20 auf 50 und liegt als eine Konstante DEFAULT_LIMIT statt als drei
Literale in drei Adaptern: gekappt wurden bisher vor allem die strukturellen Sweeps
(--field '!sources'), die alphabetisch und nicht nach Relevanz sortiert sind, wo die Kappung
also eine beliebige Scheibe der Antwort wegwirft statt ihres schwaechsten Endes. Sichtbar zu
sein ist es, was ein endliches Default ueberhaupt erst unbedenklich macht.
AGENTS.md § Routing traegt die Regel an genau einer Stelle - search ist erschoepfend, ein
eigener Grep ueber kb/ fuegt nur die generierten Dateien hinzu, die Invariante 1 ohnehin
verbietet; tools/CONTRACT.md traegt daneben nur den Mechanismus.
Grenzuebertritt-Frage geprueft und verneint: kein Flag entfernt oder umbenannt, keine Umgebungsvariable, keine maschinengelesene Datei in ihrer Form veraendert, JSON rein additiv. Die Tabelle liest ein Agent, kein Skript, und ihre Aenderung verlangt keiner Instanz Handarbeit ab.
Verifiziert: docs verify/instructions verify gruen, 1272 Tests gruen (9 neu: Pfad vorhanden;
Titel und Pfad ungekappt bei langem Titel; eine Trefferzeile zerfaellt trotz | in der Prosa in
ihre fuenf Felder; ein gekapptes Ergebnis nennt die Gesamtzahl, ein ungekapptes behauptet
nichts; --limit 0 gilt nie als gekappt; die Gesamtzahl ueberlebt das Limit in run_search;
api.search meldet dasselbe; alle drei Adapter teilen ein Default-Limit - der MCP-Golden-Test
haelt die neuen Felder zwischen CLI und Server zusammen).
5.1.0 - 2026-09-12 - changelog: Kandidaten-Eintrag nach Impact gruppiert, version regrade zur Nachkorrektur, version release verlangt eine Zusammenfassung
Author: Torben Nehmer
High impact
- changelog: Kandidaten-Eintrag nach Impact gruppiert, version regrade zur Nachkorrektur, version release verlangt eine Zusammenfassung
Seit 4.4.0 sammelt ein laufender Kandidat alle Bumps in einem Eintrag; bei 5.0.0 wurde das
mit 20 Bumps und ~1440 Zeilen unlesbar, weil die Liste chronologisch und ungewichtet war und die
Release-Seite genau diesen Eintrag 1:1 uebernimmt (version notes, release.yml). Der Eintrag
ist jetzt geschichtet statt einer einzigen Wand Text: version bump --impact high|medium|low
(Default medium) graduiert jeden Bump, die Liste rendert nach High/Medium/Low gruppiert - ausser
wenn alles medium ist, dann bleibt sie flach wie bisher, damit jeder alte Eintrag und jeder
einfache Patch unveraendert bleibt. version regrade korrigiert eine Note im Nachhinein, gegen
einen einzelnen Lesevorgang der ganzen Liste, bevor der Kandidat geschlossen wird. Direkt unter
der Liste steht jetzt eine kurze Zusammenfassung, darunter je Bump ein eigener
### <Bump-Titel>-Changeset-Absatz; version release verweigert das Schliessen eines Kandidaten
mit zwei oder mehr Bumps, solange diese Zusammenfassung fehlt (ein Kandidat mit genau einem Bump
ist ausgenommen - sein Changeset ist bereits die Zusammenfassung, wie hier). Geschlossene
Eintraege wie der zu 5.0.0 bleiben in der alten Form stehen: die Release-Seiten sind laut
eigenem Footer unveraenderliche Snapshots, und instructions/dev/version-parts.md sowie
docs/version-model.md zitieren den 2.0.0-Eintrag mit Abschnittsnamen.
--breaking/**Migration:** sitzen jetzt oberhalb der Bump-Liste statt darunter, damit die fuer
einen Operator wichtigste Zeile nicht unter einer moeglicherweise langen Liste verschwindet.
5.0.1 - 2026-09-12 - publish: ungeborene main ist kein detached HEAD, erster Push zu leerem Remote (schliesst #96, #97)
Author: Torben Nehmer
- publish: ungeborene main ist kein detached HEAD, erster Push zu leerem Remote (schliesst #96, #97)
Zwei Defekte, die zusammen dazu führten, dass eine frisch aufgesetzte Instanz
sich über keinen dokumentierten Weg initial veröffentlichen ließ. Beide sitzen
in publish und wurden erst beim Einrichten einer 5.0.0-Instanz aus dem
Release-Tarball sichtbar - der Pfad, den instructions/setup-instance.md
Schritt 14 als den ersten schreibenden Aufruf einer neuen Instanz nennt.
Ein ungeborener Branch ist kein detached HEAD. current_branch() fragte
git rev-parse --abbrev-ref HEAD. Nach git init -b main, vor dem ersten
Commit, zeigt HEAD auf einen Ref, der sich nicht auflöst: rev-parse endet
mit Exit 128 und war damit von einem echten detached HEAD nicht zu
unterscheiden. Der erste publish einer neuen Instanz brach deshalb mit
"Refusing to push main from a detached HEAD" ab, und Invariante 5 schneidet
den naheliegenden Ausweg (git commit von Hand) ab. Die Funktion liest jetzt
git symbolic-ref --short -q HEAD - was HEAD benennt statt worauf es
zeigt. Damit beantwortet die Branch-Prüfung den ungeborenen Fall korrekt,
statt für ihn ausgesetzt werden zu müssen: sie vergleicht main mit main
und lässt durch. Ein echter detached HEAD wird unverändert abgelehnt, und ein
--branch, das nicht dem ausgecheckten entspricht, ebenfalls.
Ein nie gepushter Branch ist nicht "nicht ahead". _local_ahead_of_remote()
entscheidet, ob ein sauberer Working-Tree trotzdem etwas zu pushen hat, und
gab False zurück, sobald kein Tracking-Ref existierte. Genau das ist der Fall
bei einem frisch angelegten, leeren Remote-Repository: der lokale Commit stand,
publish meldete "Nothing to commit" und pushte nie - beliebig oft
wiederholbar. Unterschieden wird jetzt über git ls-remote --exit-code, dessen
Exit-Code die drei Lagen ohne Textvergleich trennt (0 = Ref vorhanden,
2 = Remote erreichbar und ohne diesen Ref, 128 = unerreichbar oder gar nicht
konfiguriert); auf die Meldung zu matchen schiede aus, weil git sie übersetzt.
Nur der mittlere Fall gilt als "ahead", und auch dort nur, wenn lokal
überhaupt ein Commit existiert. Ein unerreichbares Remote behält bewusst das
bisherige Verhalten, damit Offline- und Nur-lokal-Instanzen keine
Verhaltensänderung sehen. remote_ref_exists() bleibt unangetastet - sein
zweiter Aufrufer reconcile() meint damit weiterhin richtig "nichts zum
Abgleichen da".
tools/CONTRACT.md zieht beides nach: die publish-Zeile beschrieb den
Strandungsfall bisher als gelöst, was für einen nie gepushten Branch nicht
stimmte, und der Fehlerkontrakt benennt die Branch-Prüfung jetzt als
eigenständigen Exit-1-Grund. instructions/setup-instance.md und INSTALL.md
blieben inhaltlich richtig - sie hatten den Umweg nie beschrieben, sondern den
Weg, der jetzt tatsächlich funktioniert.
5.0.0 - 2026-09-11 - TOC-Pflicht in docs verify, Konfidenz-Mechanismus ersatzlos entfernt
Author: Torben Nehmer
- status/incoming: menschliche Stubs werden ausgearbeitet, nie so umgesetzt
- page move: eine kb-Seite folgt ihrem Subtype ins Verzeichnis, das ihr Type-Spec berechnet
- kb/CONTRACT.md: Tiefe 1 als Grenze - Katalog liest nur eine Area-Ebene
- raw accept: incoming/ als abgeleiteter Rohablage-Eingang (schliesst #58)
- raw accept: Stem-Eindeutigkeit im Typverzeichnis erzwingen, --replaces als einziger Weg daran vorbei (schliesst #64)
- update entity naming conventions to use singular form for consistency
- kb/concepts/ bekommt Areas: layout: fuer concept, Area-Titel aus jedem Type-Spec, Schwellen-Empfehlung im lint (schliesst #59)
- source_type: Default streichen, unclassified als sichtbares Fach, layout: fuer source (schliesst #66)
- raw accept: Datums-Shard statt Typverzeichnis, fidelity/authority am Drop-Punkt (schliesst #67)
- source_type ist Instanzsache: Profilkatalog, Setup-Frage, evolve-subtypes-Instruction (#68)
- instructions/CONTRACT.md: Skill-H1, Referenztiefe und Begruendungsprosa praezisiert (#71, #72, #79)
- SKILL.md-Sanierung: Checklisten, Kommandolisten, wiki-status-Hard-Rule, wiki-query-Filingpruefung (#70, #74, #75, #78)
- Ausgelieferte Doku zitiert keine Issue-Nummern mehr, docs verify prueft es (schliesst #77)
- TOC-Pflicht fuer Referenzdateien ueber 100 Zeilen; session-setup.md/gates.md nennen die tatsaechliche Budget-Ausnahmeliste (schliesst #73, #76)
- tools/CONTRACT.md: docs toc im Fehlerkontrakt, docs-verify-Zeile nennt die TOC-Pruefung; tools/README.md korrigiert das --major-Kriterium
- dist export erzeugt die TOC-Region nach dem Marker-Strip neu (CI-Fund im Export-Replay)
- Konfidenz-Mechanismus ersatzlos entfernt
- Konfidenz-Mechanismus ersatzlos entfernt
- version-parts.md dokumentiert den --migration-required-Ruecknahmepfad
- wiki-status verweist auf session-setup.md (schliesst #84)
- CLAUDE.md-Importkette entdrifted, Modellwahl nach docs/ verschoben (schliesst #81)
- Telemetrie-Default nach Installationsform, Byte-Deckel und Session-Retention
- tools/CONTRACT.md: raw accept Doku auf Datums-Shard und Capture-Felder nachgezogen (schliesst #89)
- MCP submit-Tool: Quarantäne-Schreibpfad mit Upload Review Gate (schliesst #32)
- AGENTS.md-Changelog-Absatz korrigiert: Contract-Prosa ist Sitzungsarbeit, doc-pull-through-Instruction ergaenzt
- docs verify prueft die Kommando- und Fehlerkontrakttabelle in tools/CONTRACT.md getrennt, 10 fehlende Fehlerkontrakt-Zeilen nachgetragen (schliesst #91)
- tools/CONTRACT.md als Nachschlage-Dokument strukturiert: ###-Gruppen in beiden Tabellen, spiegelnde Reihenfolge, Lead-in-Regel
- incoming/.gitkeep als Datei- statt Verzeichnismuster trackbar (schliesst #88)
- dist export-Doku: Typverzeichnis-Behauptung nach #67 korrigiert (schliesst #93)
- raw/*/.gitkeep-Glob in dist-upgrade-Doku auf den flachen raw/.gitkeep-Anker korrigiert
- Breiten-Auslöser für Ingests: eine einzelne, thematisch breite Quelle bekommt einen Extract-Pass statt einer Seite pro Namen
- Breiten-Auslöser für Ingests: Extract-Pass statt einer Seite pro Namen, types/source.md nachgezogen
- stack-close Schritt 3: ein Doku-Nachzug in einen versionierten Pfad braucht seinen eigenen Bump
Breaking Change: docs verify verlangt eine aktuelle Inhaltsverzeichnis-Region () auf AGENTS.md, jedem Stage-/Collection-Contract und jeder flachen instructions/**.md-Datei ueber 100 Zeilen - eine bestehende Instanz mit einer eigenen instructions/*.md-Datei ueber 100 Zeilen ohne TOC sieht docs verify nach dem Tool-Update neu fehlschlagen, bis einmalig 'wikitool docs toc --apply' laeuft und der Diff committet wird. Ausserdem verschwinden wikitool confidence decay und confidence init-base ersatzlos, touch --confidence-base ebenso, und die Schemas fuer entity/concept verlieren confidence/confidence_base vollstaendig (additionalProperties: false) - jede bestehende Instanz muss den Korpus migrieren, sonst werden alle Entity-/Concept-Seiten beim Schema-Update sofort schemainvalide. Ablauf: instructions/migrations/5.0.0-confidence-removal.md
Das Label status/incoming gibt es seit heute in Gitea: der Mensch legt einen
Stub an — zwei Sätze, ein Verdacht, ein „wäre interessant" — und der Stack
komplettiert ihn. instructions/dev/issue-tracking.md beschrieb es nicht, und
das ist die gefährlichere Hälfte: ein Stub sieht aus wie ein Body, und der
Body ist genau das, was eine Sitzung nach Schritt 2 als Spec glaubt. Zwei
solche Issues lagen bereits offen auf dem Board (#60, #61).
Neu in der Instruction ist deshalb ein eigener Abschnitt „Incoming stubs" mit
dem Verbot als erstem Satz — ein status/incoming-Issue wird nie so umgesetzt,
wie es dasteht — und der Ausarbeitung als sechsschrittigem Ablauf: den Wortlaut
als Absichtserklärung lesen, gegen den Baum prüfen, den Originaltext wörtlich in
den Kommentar retten, bevor der Rewrite ihn überschreibt, offene Fragen benennen
statt beantworten (kind/decision), erst dann die vier Pflichtlabel, dann das
Flag entfernen. Zwei Ausgänge wie bei status/unconfirmed: ausgearbeitet oder
mit Begründung geschlossen.
Der interessante Punkt ist Schritt 4: status/incoming ist das einzige Flag,
das Schritt 4 nicht qualifiziert, sondern aussetzt. Die vier Pflichtachsen
fehlen einem Stub nicht, sie sind noch nicht fällig — kind/, prio/ und
size/ sind Antworten auf Fragen, die niemand gegen den Baum geprüft hat. Ein
Stub auf Sicht durchzulabeln ist der Fehler, nicht das Weglassen: es lässt
Ungeprüftes triagiert aussehen.
Der Schaden, den das verhindert, ist derselbe wie bei #30, nur eine Stufe früher: dort schrieb ein Body einen Mechanismus vor und bekam dessen Bugs gebaut, hier schreibt ein Body gar nichts vor und bekommt die Lücke von demjenigen gefüllt, der ihn am schnellsten gelesen hat — inklusive Close, womit die Frage, für die der Stub stand, nie wieder gestellt wird.
Keine neue Schrittnummer, bewusst: stack-close/SKILL.md und ältere
CHANGES.md-Einträge verweisen namentlich auf „Schritte 2-3 und 7". Eine
Umnummerierung hätte diese Verweise still falsch gemacht — genau der Zerfall,
den dieselbe Datei in § Renames beschreibt.
Geändert: instructions/dev/issue-tracking.md
(Frontmatter, § When to run, Schritt 5, neuer § Incoming stubs, § What no tool
checks, § Decision points) und die Routing-Zeile in
instructions/dev/stack-dev/SKILL.md. Dev-only — dist export schließt
instructions/dev/ aus, eine ausgelieferte Instanz sieht davon nichts, deshalb
PATCH.
Nachgezogen in derselben Sitzung, aber als eigenes Paket (#63, Commit
a0aecfc): kb/concepts/Issue Label Scheme.md beschrieb weiter nur die beiden
alten status/-Flags und zählte sechzehn statt siebzehn Labels. Das ist
kb/-Inhalt und brauchte nach Invariante 3 erst eine Quelle — die Threadkopie
von #62/#63 in raw/notes/ — also eine wiki-manage-Sitzung statt dieser
hier. Die Seite trägt jetzt die dritte status/-Zeile samt Verbot und einen
Kernpunkt dazu, dass dieses Flag die Vier-Achsen-Pflicht aussetzt.
Offen bleibt die erste tatsächliche Anwendung der Regel: #60 und #61 sind weiterhin unausgearbeitete Stubs.
wikitool move (#56): Nichts im Stack bewegte bisher eine Seite über eine
Verzeichnisgrenze — rename schreibt laut eigenem Docstring nie über
target.path.parent hinaus, und new_page._target_dir rechnete die
Platzierung nur beim Anlegen. Ändert sich entity_type später, blieb die
Datei still am alten Ort liegen, und nichts prüfte das nach: weder
lint_core.py noch doctor.py verglichen den Ist-Ort einer Seite mit dem,
was ihr Type-Spec berechnen würde.
Die Platzierungslogik selbst gibt es jetzt genau einmal:
TypeResolver.compute_target_dir (plus TypeResolver.subtype_dir für den
layout:-Teil), new_page._target_dir delegiert nur noch dorthin. Darauf
aufbauend zwei neue Stücke:
wikitool move --page "<Titel>"bewegt eine Seite an den berechneten Ort;--reconcilewendet dieselbe Regel auf den ganzen Bestand an und ist idempotent (ein zweiter Lauf meldet nichts mehr zu tun). Beide Modi fassen weder Body noch Frontmatter an, und der Titel — die einzige Identität einer Seite — ändert sich nie, also folgt kein Referenz-Update. Ein bereits belegtes Ziel (ein alter Stem-Kollisionsrest) wird verweigert statt still überschrieben. Top-level registriert, wierename/rm, nicht unter einempage-Unterbefehl.lintbekommt einen neuen Befund, Misplaced Pages, mit Ist- und Soll-Pfad. Bewusst nicht inHARD_ERROR_KEYS: ein von Hand platzierter Bestand ist keine kaputte Seite, nur eine, diemoveaufräumen könnte — auf dieser Instanz sind das aktuell die dreikb/entities/projects/*/-Seiten, die #57 separat behandelt.
Ein Fund unterwegs, der ohne #56 unsichtbar geblieben wäre: migrate verify
schlüsselte Seiten über den repo-relativen Pfad. Ein reiner Move ergab
„N removed, N added, 0 compared" und lief grün durch — der eine mechanische
Check, für den instructions/migrate-corpus.md existiert, hätte bei genau der
Operation nichts geprüft, die dieses Issue einführt. Behoben: PageShape
trägt jetzt zusätzlich den Pfad, _shapes_at_revision/_shapes_now
schlüsseln über den Titel (die einzige Identität einer Seite), und
CorpusDiff.moved meldet einen reinen Ortswechsel separat — informativ,
niemals als Finding. Regressionstest deckt drei verschobene Seiten mit
compared == 3, added == 0, removed == 0 ab.
Bewusst nicht Teil dieses Pakets: die drei realen Seiten aus #57 bleiben
liegen (kein Korpus-Publish hier, nur Stack), und raw rename (#16) — das
git mv-mit-mv-Fallback aus #56s Entwurf war für Rohdateien gedacht;
rename/rm/move bewegen kb-Seiten über ein einfaches Path.rename, weil
publish ohnehin über git add -A staged.
Geändert: tools/chemenu/type_resolver.py (compute_target_dir,
subtype_dir), tools/chemenu/commands/new_page.py (delegiert),
tools/chemenu/commands/page_ops.py (move_command),
tools/chemenu/lint_core.py (find_misplaced, misplaced_pages),
tools/chemenu/corpus_diff.py und tools/chemenu/commands/migrate_cmd.py
(Titel-Schlüsselung, moved), tools/chemenu/commands/log_append.py (--op move), tools/CONTRACT.md, instructions/page-lifecycle.md,
instructions/publish-cycle.md. MINOR: rückwärts liest ein älterer Stack eine
verschobene Seite unverändert (Identität ist der Titel, nicht der Ort),
vorwärts reines Überkopieren.
Katalogtiefe (#57): index_build.group_pages las bislang genau zwei
Pfadsegmente unter kb/ (parts[0] als Collection, parts[1] als Area) und
faltete alles darunter still in die Level-1-Area. Real betroffen waren die
drei Seiten aus #56s Befund, kb/entities/projects/{kfchou,vanillaflava, yugasun}/*.md — im generierten Katalog nicht als eigener Ort sichtbar,
sondern als läge jede direkt in entities/projects/. kb/CONTRACT.md §
Collections beschrieb bis heute nur eine Ebene, ohne eine zweite
auszuschließen; der Baum hatte trotzdem eine, handplatziert, ohne
unterstützten Weg dorthin.
Entscheidung war (b) aus dem Issue: die drei Verzeichnisse auflösen statt den
Katalog rekursiv zu machen. Die Gruppierungsachse dahinter — Owner
(kfchou/vanillaflava/yugasun) — kommt aus keinem Frontmatter-Feld und
aus keinem Type-Spec, sondern aus einer Ad-hoc-Entscheidung beim Anlegen; sie
verdient keine zweite Verzeichnisebene. Tiefe 1 ist jetzt geschriebene Regel
in kb/CONTRACT.md § Collections, mit dieser Begründung.
Vier Stücke setzen das um:
kb_scan.find_nested_pagesliefert(title, page, depth)für jede Seite mehr als ein Verzeichnis unterhalb ihrer Collection — reine Pfadtiefe, unabhängig davon, ob der Typ auflöst, damit auch eine Seite mit kaputtemtype:nicht durchrutscht.lintbekommt den Befund Nested Pages, und anders alsmisplaced_pageshart: eine fehlplatzierte Seite katalogisiert noch korrekt von der falschen Stelle aus, eine verschachtelte macht den generierten Katalog selbst falsch, und es gibt keine Version, ab der das toleriert würde.index rebuildlehnt eine verschachtelte Seite nicht ab, sondern warnt (Entscheidung aus der Session: melden statt verweigern, damit ein Fremdinstanz-Upgrade mit handverschachtelten Seiten nicht hart bricht) —group_pagesfaltet weiterhin wie zuvor, die Warnung ist die neue Sichtbarkeit, nicht eine Verhaltensänderung der Faltung selbst.TypeResolver.get_layoutvalidiertlayout: {dir: ...}jetzt auf einen einzelnen Pfadabschnitt (kein/, kein\, kein./.., nicht leer) und schlägt fehl statt eine zweite Ebene über den einzigen unterstützten Weg — ein Type-Spec — entstehen zu lassen.
Ein fünftes Stück, das das Issue selbst nicht explizit forderte, aber die
move-Mechanik aus #56 sonst mit toten Verzeichnissen zurückgelassen hätte:
move (--page wie --reconcile) entfernt jetzt ein Verzeichnis, das es
durch den Wegzug seiner letzten Seite geleert hat — symmetrisch zum
mkdir(parents=True) auf der Zielseite. Ohne das hätten kfchou/,
vanillaflava/, yugasun/ den eigenen Fix überlebt, leer und für git
unsichtbar, aber für einen verzeichnisbasierten Test sichtbar.
Auf dieser Instanz angewendet: wikitool move --reconcile hat die drei
Seiten nach kb/entities/projects/ gezogen und die drei leeren
Owner-Verzeichnisse mitentfernt. migrate verify --from HEAD bestätigt
compared == 182, added == 0, removed == 0, alle drei als moved markiert.
Titelkollision trat wie im Issue erwartet keine auf.
Geändert: tools/chemenu/kb_scan.py (find_nested_pages),
tools/chemenu/lint_core.py (nested_pages, HARD_ERROR_KEYS),
tools/chemenu/commands/index_build.py (Rebuild-Warnung),
tools/chemenu/type_resolver.py (get_layout-Validierung),
tools/chemenu/commands/page_ops.py (_rmdir_if_emptied), tools/CONTRACT.md,
kb/CONTRACT.md § Collections, plus die drei realen Seiten unter
kb/entities/projects/. PATCH: reine Codeänderung ohne Schnittstellenwechsel,
gefaltet in den offenen 4.8.0-Kandidaten (max-wins gegen die MINOR-Bewegung
aus #56); die drei bewegten Seiten sind Korpus dieser Instanz, kein
ausgelieferter Inhalt.
Schließt #57.
raw accept (#58): raw/CONTRACT.mds Routing-Tabelle war bislang eine Regel für Menschen —
wer eine Datei ablegt, wählt articles//documents//notes//assets/ selbst, und mehrere
Dateien einer logischen Quelle waren im Dateisystem nicht als zusammengehörig erkennbar. Neu ist
ein gitignorierter Eingang incoming/, der dieselben vier Typverzeichnisse spiegelt: der Mensch
klassifiziert nur, indem er dort ablegt, tools/wikitool raw accept <datei> [<datei> ...] [--page "<Titel>"] berechnet die Beförderung nach raw/.
Zwei Entscheidungen, gegen die ursprüngliche Skizze im Issue: ein Bundle-Verzeichnis
(raw/<typ>/<stamm>/, benannt nach der ersten Datei) entsteht erst ab der zweiten Datei, nie
einheitlich — damit sind die 29 heute flach liegenden Bestandsdateien keine Ausnahme, sondern
bereits die Regelform, und die Frage „was passiert mit dem Bestand" beantwortet sich von selbst.
Und der Typ wird über das Eingangs-Unterverzeichnis deklariert, nicht über ein --type-Flag: die
Erklärung wird abgegeben, wenn der Mensch die Datei in der Hand hat, statt im Moment des
accept-Aufrufs neu geraten werden zu müssen.
--page deckt den Wachstumsfall ab: erweitert raw_files: einer bestehenden Source-Seite und
faltet deren schon abgelegte Einzeldatei ins neue Bundle, sobald das die Seite über eine Datei
hinaus wachsen lässt — ohne ein Fenster, in dem raw_files: ins Leere zeigt. Die dafür nötige
Rückwärtssuche und der Mehrfach-Owner-Schutz sind keine neue Mechanik, sondern
provenance.source_pages_by_raw_file, das lint schon für duplicate_raw_file_owners benutzt —
ein Owner-Konflikt lehnt die Beförderung ab, statt eine andere Seite unbemerkt zu brechen.
incoming/ ist für sources coverage und lint unsichtbar (beide laufen ausschließlich über
config.iter_raw_files(config.RAW_DIR)), und dass keine Datei dort je committet werden kann, ist
über docs_verify.REQUIRED_IGNORE_CANARIES bewiesen, nicht nur zugesichert. RAW_SUBDIRS
(dist_cmd.py) bleibt die einzige Quelle der Vier-Verzeichnis-Liste: docs verify
(check_raw_subdirs) hält raw/CONTRACT.mds Tabelle jetzt in beiden Richtungen dagegen, und
dist export sät incoming/<typ>/.gitkeep neben raw/<typ>/.gitkeep; instructions/bootstrap.md
legt den Eingang für einen bestehenden Klon nach, da er dort nie aus git kommt.
Geändert: tools/chemenu/commands/raw_cmd.py (neu, raw accept), tools/chemenu/cli.py,
tools/chemenu/commands/dist_cmd.py (RAW_SUBDIRS-Kommentar, incoming/*/.gitkeep),
tools/chemenu/commands/docs_verify.py (check_raw_subdirs, incoming/-Ignore-Kanarie),
.gitignore, raw/CONTRACT.md, tools/CONTRACT.md, instructions/bootstrap.md,
instructions/wiki-ingest/SKILL.md. MINOR: eine Umsortierung des Bestands wäre die Grenze
gewesen, findet aber unter der Bundle-erst-ab-zwei-Regel nicht statt — der Bestand bleibt
unangetastet, keine fremde Instanz muss migrieren, vorwärts wie rückwärts reines Überkopieren.
Schließt #58.
raw accept prüfte Kollisionen bisher nur auf einzelnen Dateipfaden
(dst.exists()), nie auf dem Bundle-Verzeichnis selbst. Weil der Bundle-Name
aus dem Stem der Primärdatei entsteht, konnte eine zweite, unabhängige Quelle
wortlos in das Bundle einer ersten wandern, sobald die Dateinamen zufällig
nicht kollidierten — verifiziert mit einem Wegwerf-Test: handbuch.txt +
anhang.md ohne --page landeten unbemerkt in einem bestehenden
raw/documents/handbuch/. lint meldete nichts, weil beide Quellen ihre
Dateien korrekt abdeckten.
Die Menge der Namen auf raw/<typ>/-Ebene — Dateistämme plus
Bundle-Verzeichnisnamen — ist jetzt eindeutig erzwungen (_occupied_stems in
raw_cmd.py), unter Ausnahme dessen, was der Aufruf selbst schon besitzt: ein
Bundle, das über --page wächst, oder ein bereits registriertes, sich
fortsetzendes Bundle. Ein belegter Stem wird mit Exit 1 abgelehnt und nennt
beide Auswege, ohne einen zu empfehlen — --replaces oder Umbenennen in
incoming/.
--replaces <raw-pfad> ist der einzige sanktionierte Weg, eine Rohdatei
wortwörtlich zu ersetzen: genau eine eingehende Datei, identischer Dateiname,
gleiches Typverzeichnis, Ablehnung bei mehr als einem Owner. raw_files:
bleibt unverändert, es wird keine kb/-Seite geschrieben, und die Altfassung
lebt ausschließlich in git log --follow weiter — kein Archivverzeichnis,
kein Hash im Dateinamen, kein neues Frontmatter-Feld. Nach einem Ersatz nennt
das Kommando die Source-Seite und ihre zitierenden Seiten, damit deren
Nachzug im selben Commit passiert wie die Ersetzung.
raw/CONTRACT.md § Rules trägt beide Regeln aus dieser Entscheidung
(unveränderlich, aber ersetzbar als Ganzes); instructions/wiki-ingest/SKILL.md
benennt den Kollisionsfall als Haltepunkt, an dem eine Sitzung die Meldung dem
Menschen vorlegt statt selbst zu entscheiden — dieselbe Klasse wie AGENTS.md
Invariante 6, auch ohne dass hier ein Exit-42-Gate greift.
Geändert: tools/chemenu/commands/raw_cmd.py (_occupied_stems,
_stem_collision_message, --replaces/_replace),
tools/chemenu/tests/test_raw_cmd.py (16 neue Tests), raw/CONTRACT.md,
tools/CONTRACT.md, instructions/wiki-ingest/SKILL.md. MINOR: raw accept
wurde nie released (letztes Release v4.7.4), die Verschärfung kostet also
keine Kompatibilitätsfrage, solange sie vor 4.8.0 landet.
Schließt #64.
kb/concepts/ bekommt Areas (#59): Sharden ist längst automatisch —
index_build.SHARD_THRESHOLD = 50, hergeleitet aus der wikieigenen Seite
Index Scaling — aber es passiert pro Area, und eine Area legt niemand an.
kb/concepts/ hatte keine, also war die Schwelle dort ein toter Wert: 80 Seiten
in einer einzigen Tabelle, weit über der eigenen Grenze, ohne dass je etwas
gefeuert hätte. Die Ursache war eine Asymmetrie in den Type-Specs — entity
deklarierte ein layout:, concept nicht, obwohl das Subtype-Feld fertig dalag.
types/concept.md deklariert es jetzt für alle sechs concept_type-Werte
(architectures/, patterns/, protocols/, workflows/, decisions/,
problems/). Für source bewusst nicht: 25 von 29 Seiten sind notes, die
Aufteilung ergäbe eine Area und vier Splitter, und kb/sources/ liegt mit 29
Seiten ohnehin unter der Schwelle. Ein Subtype-Feld zu haben ist kein Grund, es
als Achse zu benutzen.
Zwei Dinge im Code, beide Folgen desselben Befunds. _area_titles() in
index_build.py löste entity fest über find_type_by_name("entity") auf und
las nur dessen layout: — jeder zweite Typ mit einem layout: hätte
.title()-Namen auf dem Verzeichnisnamen bekommen statt der deklarierten Titel.
Es liest jetzt jedes Type-Spec, und zwar pro Collection geschlüsselt, damit
zwei Typen denselben Area-Namen für Verschiedenes benutzen dürfen. Und der neue
lint-Befund meldet eine Collection über der Schwelle ohne Areas, mit der
Verteilung ihres Subtype-Felds — als Empfehlung, nicht als Failure, und nur
dann, wenn die Aufteilung jede entstehende Area unter die Schwelle drückt. Das
begrenzt sich selbst in beide Richtungen: kb/comparisons/ mit einer Seite
feuert nie, und die schlechte Aufteilung nach source_type unterbleibt von
allein, ohne dass der Check etwas über Sources wüsste.
Zwei Dinge fielen unterwegs an, die das Issue nicht vorhergesehen hatte.
lint_core.py durfte SHARD_THRESHOLD/group_pages nicht aus
commands/index_build.py importieren — test_api.py prüft strukturell, dass
chemenu.api kein Modul unter chemenu.commands lädt, und der Import hätte den
ganzen CLI-Kopf mitgezogen. Die Gruppierung liegt deshalb neu in
tools/chemenu/catalog.py, entlang derselben Linie wie lint_core.py:
Korpusform hier, Darstellung dort. Und _anchor() strich mit [^a-z0-9\s-]
jeden Nicht-ASCII-Buchstaben ersatzlos — die Karte verlinkte auf #ablufe,
während die Überschrift im Shard #abläufe heißt. Vorher fiel das keinem auf,
weil alle Entity-Area-Titel zufällig ASCII sind; Abläufe ist der erste, der es
nicht ist.
Auf dieser Instanz angewendet: wikitool move --reconcile hat alle 80
Concept-Seiten in ihre Area gezogen, migrate verify --from HEAD bestätigt
182 compared, 0 added, 0 removed, 80 moved, 0 findings — kein Titel, kein
Body, kein Frontmatter-Feld angefasst. index rebuild erzeugt sechs Areas
(Abläufe 28, Architekturen 20, Muster 17, Entscheidungen 7, Problemstellungen 5,
Protokolle 3); keine über der Schwelle, also kein eigener Shard, und die
Schwelle wirkt wieder als Schwelle.
Geändert: types/concept.md (layout:), types/type-spec.md (wann ein
layout: sich lohnt), tools/chemenu/catalog.py (neu),
tools/chemenu/commands/index_build.py (area_titles, _anchor),
tools/chemenu/lint_core.py (unsharded_collections),
tools/chemenu/tests/ (Fixture-Concept liegt jetzt in seiner Area, plus neun
neue Tests), tools/CONTRACT.md, tools/README.md, README.md,
kb/concepts/COLLECTION.md, sowie die 80 bewegten Seiten unter kb/concepts/.
MINOR, nicht MAJOR: der Umzug ist ein Angebot, kein Zwang. Eine
bestehende Instanz, die move --reconcile nicht laufen lässt, bleibt
funktionsfähig — group_pages liest das Dateisystem, nicht das layout:, also
landen flache Bestandsseiten in der Area „All" und neu angelegte in ihrer
eigenen; beides rendert. Der gemischte Zustand meldet sich als lint-Befund
Misplaced Pages, der seit jeher advisory ist. Und ein Downgrade auf einen
Stack ohne dieses layout: funktioniert weiter: die Verzeichnisse bleiben
Verzeichnisse, nur die Anzeigetitel fallen auf .title() zurück. Kosmetik, kein
Bruch der Austauschbarkeit in beiden Richtungen.
Schließt #59.
Nachzug an 63b4bb8: die Umstellung der Namenskonvention auf HA Integration
hatte in README.md das Gegenbeispiel verloren — die Zeile las
Use singular for entities: `HA Integration.md` (not `HA Integration.md`), beide
Seiten des „not" identisch, also eine Regel ohne Fall, an dem sie greift.
kb/CONVENTIONS.md und kb/entities/COLLECTION.md hatten im selben Commit das
korrekte Paar bekommen; README.md zieht jetzt mit HA Integrations.md nach.
source_type hatte in types/source.schema.yaml ein default: notes — der Compiler wählte
das Sammelbecken, sobald niemand widersprach, nicht ein Mensch. #59 hatte den Bestand deshalb
für lopsided gehalten (25 von 29 Seiten notes) und source bewusst flach gelassen; nachgezählt
nach dem, was die Seiten tatsächlich sind (Dateiname, author:, Rohdatei), waren es 16
Session-Transkripte, 4 LLM-Analysen, 2 Tracker-Exporte und nur 3 echte Notizen. Der Default war
der Fehler, nicht das Enum.
Umgesetzt: default: gestrichen, wikitool new source verweigert jetzt ohne expliziten Wert.
Enum neu: transcript, analysis, article, document, notes, tracker, unclassified —
spec und image entfallen (null Seiten, nie am echten Material bewährt). unclassified ist
das neue, sichtbare Fach für eine Quelle, deren Kategorie noch nicht feststeht — eigene Area,
beratender lint-Befund (unclassified_source_pages), keine harte Fehlerklasse. types/source.md
deklariert jetzt ein layout: für alle sieben Werte.
Auf dieser Instanz angewendet, in einem eigenen work/reclassify-source-types/-Lauf: 22 der 29
Source-Seiten per wikitool touch --set source_type=<wert> auf ihren tatsächlichen Wert
korrigiert (16 transcript, 4 analysis, 2 tracker), 7 unverändert. wikitool move --reconcile hat
alle 29 danach in ihre Area gezogen (transcripts 16, analyses 4, articles 3, notes 3, trackers 2,
documents 1, unclassified 0). migrate verify --from HEAD bestätigt 182 compared, 0 added, 0 removed, 29 moved, 22 findings — die 22 sind exakt die beabsichtigten source_type-Änderungen,
kein Titel, kein Body, keine Wikilink- oder Zitatzahl angefasst. lint --fail-on-error grün,
insbesondere ohne Misplaced Pages, Nested Pages oder den neuen Unclassified Source Pages.
Geändert: types/source.schema.yaml (Enum, kein Default), types/source.md (layout:),
types/type-spec.md (source als Beispiel für ein bewusst fehlendes layout: ersetzt —
ein lopsided Feld wird repariert, nicht dauerhaft flach gelassen), tools/chemenu/lint_core.py
(unclassified_source_pages, neu), tools/CONTRACT.md, instructions/wiki-ingest/SKILL.md
(--set source_type=, unclassified als Ausweg), instructions/dev/corpus-policy.md
(Floor-Ausnahme für unclassified), kb/sources/COLLECTION.md (Areas, Autorschaft trennt
analysis von document), fünf Tests umgehängt (davon einer auf einen neuen
Fixture-Type-Spec, weil source als „hat Subtype-Feld, kein layout:"-Beispiel wegfällt),
sowie die 29 bewegten und 22 reklassifizierten Seiten unter kb/sources/.
MINOR, geprüft am Drop-in-Test: eine bestehende Instanz besitzt ihr eigenes
types/source.md (Auslieferung nur als .template), kopiert tools//types//instructions/
über sich und bleibt unverändert funktionsfähig — kein Downgrade-Bruch, kein umgeschriebenes
maschinengelesenes Format. Kein --breaking, kein Migrationsdokument.
Schließt #66.
raw accept: Datums-Shard statt Typverzeichnis, fidelity/authority am Drop-Punkt (#67,
Paket B von vier — A ist #66 oben, C ist #68, D ist #69): zwei unabhängige Befunde, ein
Codepfad.
Befund 1: raw/CONTRACT.mds vier Typverzeichnisse (articles/, documents/, notes/,
assets/) lösten keinen der drei Gründe ein, die einen Verzeichnis-Split rechtfertigen —
raw/ wird nie durchgeblättert, keine Klausel dieser Datei galt je pro Verzeichnis, alle vier
verrotten gleich (unveränderlich, nie gelöscht). Der Split kostete real: der Mensch trifft beim
Ablegen in incoming/<typ>/ eine Routing-Entscheidung, die später blind nach source_type:
abgeschrieben wird — genau darüber entstand der in #66 korrigierte Bias (raw/notes/ hielt laut
altem Contract-Text „Gesprächsprotokolle", 16 der 25 Dateien dort waren tatsächlich Transkripte).
Befund 2: was am Drop-Punkt bekannt ist und danach nirgends mehr — wie treu eine Erfassung ist
und was das Material über seinen Gegenstand behaupten darf. Zwei neue, unabhängige Achsen auf
types/source.md: fidelity (verbatim/published/secondhand/nontextual) und authority
(normative/reporting/opinion), beide mit unknown als backfill-only-Wert.
Umgesetzt:
raw acceptadressiert eine Datei jetzt überraw/<YYYY>/<MM>/, berechnet aus dem Annahmedatum — eine reine Funktion von etwas Unveränderlichem, kann also nie rebalancieren und keinen[^cite-id]-Anker brechen.incoming/wird flach; ein Unterverzeichnis wird toleriert und ignoriert statt inspiziert (alteincoming/<typ>/-Skripte laufen unverändert weiter). Bestandsdateien inraw/articles|documents|notes|assets/bleiben unbewegt und weiter gültige--replaces-Ziele — das Layout war nirgends versioniert, es gibt also keine „zwei Korpusformen". Wächst eine bereits promotete Einzeldatei zum Bündel, entsteht das Bündel an ihrem eigenen Speicherort, nie im heutigen Shard — ein Bündel aus altem und neuem Datum hätte keine eindeutig richtige Adresse.- Stem-Eindeutigkeit (#64) gilt jetzt global über
raw/, nicht mehr pro Typverzeichnis — ohne Typverzeichnisse als Grenze wäre die Prüfung sonst wirkungslos gegen ein Bündel in einem anderen Shard oder einem Alt-Verzeichnis. fidelity/authority: neu intypes/source.schema.yaml, ohnedefault:und bewusst nicht inrequired:(sonst bricht jede bestehende Instanz an der Validierung — die MINOR-Einstufung unten hängt daran). Erzwungen stattdessen im Werkzeug:raw acceptverlangt beide Flags immer; trägt der Aufruf--page, schreibt es sie direkt auf die Zielseite, sonst druckt es die fertigenew source --set fidelity=... --set authority=...-Folgezeile, undnew sourceverweigert seinerseits ohne beide Werte.unknownist backfill-only — wederraw acceptnochnew sourcedürfen es schreiben.- Capture-Felder sind fill-once, nicht auf
touch.pysUNSETTABLE-Denylist: eine Denylist hätte auch den ersten (Backfill-)Schreibzugriff verboten, den der Migrationslauf braucht.touch --set <feld>=<wert>schreibt nur, solange das Feld fehlt, und verweist danach aufraw accept --replacesals einzigen Korrekturweg — der einzige Aufruf, der einen bereits gesetzten Capture-Wert überschreiben darf, weil eine korrigierte Erfassung eine neue Edition der Quelle ist, keine Bearbeitung der Seite.types/source.mddeklariert die Feldliste selbst (capture_fields:), gelesen überTypeResolver.get_capture_fieldsstatt an drei Stellen hartkodiert. lintbekommt einen neuen beratenden Befund, Confidence Above Source Standing: die Autoritätsbewertung, diekb/CONVENTIONS.mds Confidence-Rubrik seit je verlangt („+0.1 für offizielle Doku"), aber nirgends festhielt. Eine stackseitige Obergrenzentabelle inkb/CONTRACT.md(reporting0.8,opinion0.6,secondhand/nontextual0.7,normative/verbatim/published/unknownohne Obergrenze) begrenztconfidence_base, ersetzt es aber nicht — eine Formel hätte zwei widersprechende Ableitungen derselben Zahl, und Autorität ist eine Obergrenze, kein Determinant. Nicht inHARD_ERROR_KEYS. Auf dieser Instanz meldet der Befund aktuell nichts: kein Bestand trägt die neuen Felder, das ist erwartet, nicht geprüft.- Aufgeräumt:
dist_cmd.RAW_SUBDIRSund der darauf laufendedocs verify-Check (check_raw_subdirs) entfallen ersatzlos,raw/CONTRACT.mds Routing-Tabelle beschreibt stattdessen den Shard, die Ignore-Kanarie wandert vonincoming/documents/probe.pdfaufincoming/probe.pdf.
MINOR, 4.8.0-beta.8 desselben Kandidaten — drei geprüfte Bedingungen: raw accept nimmt
weiterhin Dateien aus incoming/<irgendwas>/ an, statt sie zu verweigern; fidelity/authority
stehen nicht in required:; die zwei neuen Pflichtflags an raw accept sind eine
Verhaltensänderung, aber dieselbe Einstufung, die #66s new source-Verweigerung im selben
Kandidaten schon bekam. Kein --breaking, kein Migrationsdokument nötig — kein Bestand wird
durch diesen Bump ungültig.
Geändert: raw/CONTRACT.md, types/source.md, types/source.schema.yaml,
tools/chemenu/commands/raw_cmd.py (Neufassung), tools/chemenu/commands/new_page.py
(Capture-Feld-Pflicht), tools/chemenu/commands/touch.py (_capture_field_or_fail,
Fill-once), tools/chemenu/type_resolver.py (get_capture_fields),
tools/chemenu/commands/dist_cmd.py/docs_verify.py (Aufräumen), tools/chemenu/lint_core.py
(neuer Befund), kb/CONTRACT.md (Obergrenzentabelle), tools/CONTRACT.md, .gitignore,
instructions/bootstrap.md, instructions/wiki-ingest/SKILL.md (Schritt 1 und 6), zugehörige
Tests.
Der Backfill über den Bestand lief als eigener work/-Lauf (backfill-capture-fields,
Commit 00220f8) hinterher, nach instructions/migrate-corpus.md und durch das
Mass-Update-Gate: 29 Source-Seiten, nicht 31 wie zwischenzeitlich im Issue notiert — die
höhere Zahl zählte INDEX.md und COLLECTION.md mit.
Die Regel dieses Laufs war enger als die des laufenden Betriebs, weil ein nachgetragener
Capture-Wert erschlossen ist und nicht erhoben: ein echter Wert nur dort, wo die Art des
Artefakts ihn aus dem Material selbst festlegt, sonst unknown. Ergebnis: verbatim+reporting
18 (16 Gesprächstranskripte, 2 Tracker-Exporte — beide wörtliche Mitschnitte, beide Protokoll
statt Festlegung), secondhand+opinion 3 (LLM-Analysen), published+reporting 2,
published+normative 1 (Karpathys Idea-File, das definierende Dokument seines eigenen
Gegenstands), verbatim+normative 1 (das qmd-README, dessen Rohdatei ihre Treue selbst
deklariert), unknown+normative 1, unknown+reporting 3.
fidelity: unknown steht auf 4 der 29 Seiten, authority: unknown auf keiner. Die
Asymmetrie ist der interessante Teil: wer für einen Gegenstand zuständig war, ließ sich überall
aus dem Material beantworten — wie treu ein selbstverfasstes Cheat Sheet oder ein Anweisungsdokument
„erfasst", nicht, weil der Enum für ein originär geschriebenes Artefakt keinen Wert hat. Das ist
kein Backfill-Fehler, sondern genau die Grenze, die unknown markieren soll.
Und der lint-Befund hat einen Fall — 67 sogar: nach dem Backfill melden 67 von 152 Seiten
mit confidence_base mehr Konfidenz, als die Quellenlage trägt (47 gegen die 0.8-Grenze für
reporting, 20 gegen die 0.6-Grenze für opinion). Das ist kein Fehlalarm und auch keine
Nacharbeit dieses Eintrags: der Korpus ist zu gut der Hälfte aus Gesprächstranskripten kompiliert,
und die Rubrik in kb/CONVENTIONS.md lässt Quellenzahl und Aktualität allein bis 0.95 laufen,
während der Autoritätsterm der kleinste Summand ist. Ob daraus folgt, dass 67 Seiten überbewertet
sind oder dass die 0.8-Grenze für einen selbstdokumentierenden Korpus zu eng ist, ist eine
Entscheidung und keine Korrektur — sie hängt als Messung am Stub #60, der genau diesen Verdacht
ohne Zahlen aufgeschrieben hatte. Die Transkripte wurden ausdrücklich nicht auf normative
hochgestuft, nur damit der Report leiser wird.
Schließt #67.
source_type ist Instanzsache: Profilkatalog, Setup-Frage, evolve-subtypes-Instruction
(#68, Paket C von vier — A ist #66, B ist #67, D ist #69): reine Doku- und Instruction-Arbeit,
kein Korpus-Sweep.
Befund: source_types sieben Werte (transcript, analysis, article, document, notes,
tracker, unclassified) beschreiben diese Instanz, nicht den Stack — gegen drei
hypothetische Zielinstanzen (Handball-Verein, Produktentwicklung, Pen-&-Paper) hat die Liste
fast nichts gemeinsam, während fidelity/authority (#67) in allen vieren dieselben Werte
bleiben. Architektonisch war das längst wahr (types/source.md trägt root: kb, dist export
liefert es nur als .template), nur stellte nichts die Frage: instructions/kb-profiles.md
riet im entities-Abschnitt „Adapt the area list first", sagte im sources-Abschnitt aber kein
Wort zu source_type. Zweiter Befund, aus #66 mitgenommen: das unclassified-Fach bekam einen
beratenden lint-Befund, aber nie eine Prozedur, es wieder zu leeren.
Umgesetzt:
kb-profiles.mdssources-Abschnitt behandeltsource_typejetzt wieentitiesseine Area- Liste: als das, was zuerst anzupassen ist, mit zwei ausformulierten Domänenprofilen (Handball-Verein, Pen-&-Paper) als Anschauung, und dem ausdrücklichen Gegenbeispielfidelity/authority— die sind Stack-Vokabular und stehen nicht zur Wahl.instructions/setup-instance.mdSchritt 5 bekommt einen neuen Unterschritt: nach dem Anwendungsgebiet fragen,source_type-Vorschlag ableiten, Enum undlayout:in derselben Bearbeitung setzen. Mit der Ansage, dass der Betreiber zum Setup-Zeitpunkt null Quellen hat und seine Taxonomie vor jedem Material rät — das Ergebnis ist ein Startpunkt, keine Festlegung, undunclassifiedbleibt in jedem Vorschlag erhalten.- Neue Instruction
instructions/evolve-subtypes.md,manual: true: benennt die Weiterentwicklungsschleife, die werkzeugseitig schon vollständig existierte (Fach sehen → Wert samtlayout:ergänzen →touch/move --reconcile→index rebuild/migrate verify), über alle drei Subtype-Achsen (entity_type,concept_type,source_type— alle drei tragensubtype_field:undlayout:;comparisonkeins von beidem). Zwei Regeln im Body: Wert und Sweep sind untrennbar (ein deklarierter Wert ohne Seite lädt zum Raten ein — genau dernotes-Fall aus #66), und eine Aufnahmeschwelle von ≥3 Seiten, mitspec/imageaus #66 als Gegenbeispiel und einer benannten-Ausnahme-Klausel für Fälle wietrackerbei zwei Seiten.manual: trueverhindert, dass eine Taxonomie-Änderung in einen laufenden Ingest hineinstolpert — erwähnt auskb-profiles.md,setup-instance.mdundkb/sources/COLLECTION.md, aus keinem Skill, keiner AGENTS.md, keiner CLAUDE.md verlinkt. kb/sources/COLLECTION.mdbenenntevolve-subtypes.mdan derunclassified/-Zeile.
Die vom Vorbereitungs-Body übernommene, ursprünglich vierte Maßnahme entfiel: die
corpus-policy.md-Floor-Ausnahme für unclassified steht dort bereits seit #66.
MINOR, geprüft gegen den Drop-in-Test: eine Instanz kopiert instructions/ und types/
über sich, nichts wird umbenannt oder entfernt, kein Kommando, kein Flag, kein
maschinengelesenes Format. Kein --breaking, kein Migrationsdokument. 4.8.0-beta.9 desselben
Kandidaten.
Geändert: instructions/kb-profiles.md, instructions/setup-instance.md,
instructions/evolve-subtypes.md (neu), kb/sources/COLLECTION.md.
Schließt #68.
instructions/CONTRACT.md § Writing an instruction: drei offene Fragen entschieden
Drei Issues aus der #65-Analyse zeigten auf denselben Abschnitt. Alle drei enden dort, wo die Regel steht, nicht in einem Issue-Kommentar.
Der Imperativ-Titel bindet eine Instruction, nicht ein SKILL.md (#71).
Die Regel griff dem Wortlaut nach auf alle fünf Skills durch, deren H1
Nomenphrasen sind. Geprüft gegen die Primärquelle: Anthropic normiert name
und description und sagt zur Body-Überschrift nichts; die eigenen
Beispiel-Skills heißen # PDF Processing, # BigQuery Data Analysis. Dazu
das Sachargument — der H1 liegt auf keinem Retrieval-Pfad, weil über die
Aufnahme eines Skills die description entscheidet, die ab Sessionstart im
Kontext steht, während der Body erst beim Zugriff gelesen wird. In der Sitzung
kam ein Beleg dazu, den keines der Issues kannte: der vendorierte
commonplace-Korpus trägt dieselbe Imperativ-Titel-Regel, unabhängig
entstanden, und macht im selben Absatz dieselbe Ausnahme („for promoted skills,
the skill name is the title"). Die fünf Titel bleiben unverändert.
Referenztiefe: Anthropics „one level deep" gilt gebündeltem Material (#72).
Weg 2 der drei zur Wahl stehenden. Der Beleg für die Reichweite steht im
vendorierten codex-skill-creator/SKILL.md: die Beispiele der Regel sind
DOCX-JS.md, REDLINING.md, OOXML.md — alles Dateien im Skill-Bündel. Kein
Skill dieses Repos hat heute eine solche Datei, die Regel bindet hier also
wörtlich nichts. Für den Link von einem Skill auf einen repo-weiten Contract
fallen die beiden Hälften der Frage auseinander: die Mechanik (Zweit-Hop wird
womöglich nur angelesen) ist real und verzeichnisunabhängig, die Vorschrift ist
für diesen Fall von Anthropic nicht belegt. Die geteilten Contracts bleiben
geteilt — Invariante 8 hat sie dorthin gestellt, und § Frontload verlangt, dass
ein Schritt ohne Vorkontext entscheidbar ist, nicht dass jede Regel an ihm
wiederholt wird. Als Auflage bleibt das Billigere: ein Link sagt, was der
Schritt aus der Datei braucht.
Ob die Mechanik hier überhaupt beißt, wurde vor der Entscheidung im Eval-Aufbau
nachgesehen, und die Antwort ist: nicht messbar. Die L2-Trajectory-Regeln lesen
ausschließlich wikitool.call, gate.*, publish.commit und
prompt.submitted — keine Dateizugriffe eines Agenten; auf Claude Code ist
überhaupt kein Tool-Hook verdrahtet, ein head -100 hinterlässt also keine
Spur. Die zweite Hälfte der Behauptung, was am Ende im Kontextfenster stand,
erzeugt konstruktionsbedingt nirgends ein Event. Ein kausaler A/B-Vergleich
bräuchte den L3-Runner, der entworfen und nicht gebaut ist. Das steht jetzt im
Contract: eine Festlegung über Reichweite, keine Messung. Der Nebenfund — L2
sieht auf dem primären Harness gar keine Tool-Calls — ist ein eigenes Issue
wert und nicht Teil dieser Änderung.
Wieviel Begründung ein Schritt tragen darf, ist jetzt messbar (#79). Der
alte Satz („Keep reasoning out of the body […] keep only enough reasoning to
decide edge cases") zog in zwei Richtungen, und die größte Instruction des
Repos lebte in der Lücke. Neu sind eine Keep/Cut-Tabelle und zwei Tests:
Substitution — die Passage streichen und den Schritt noch einmal lesen; rät
ein Agent ohne Vorkontext jetzt, war es eine Entscheidungshilfe und sie bleibt,
egal wie lang. Once — eine Entscheidungshilfe steht an dem Schritt, an dem die
Entscheidung fällt, und an genau einem solchen (Invariante 8). Danach gemessen
stand die source_type/Capture-Asymmetrie in wiki-ingest zweimal; sie steht
jetzt einmal, in Schritt 1, und Schritt 6 trägt die Anweisung plus Verweis.
Die beiden anderen in #79 genannten Stellen — Namenskollision in Schritt 1,
## Not Extracted in Schritt 6 — bestehen den Substitutionstest und bleiben.
214 → 211 Zeilen; die Kürzung ist nicht der Zweck, die Eindeutigkeit ist es.
PATCH, geprüft gegen den Drop-in-Test: eine Instanz kopiert
instructions/ über sich, nichts wird umbenannt oder entfernt, kein Kommando,
kein Flag, kein maschinengelesenes Format, und der Rückweg funktioniert
genauso. Kein --breaking, kein Migrationsdokument.
Geändert: instructions/CONTRACT.md (§ Writing an instruction, drei neue
Unterabschnitte), instructions/wiki-ingest/SKILL.md (Schritte 1 und 6).
Schließt #71, #72 und #79.
Vier Befunde in der Skill-Prosa, ein Publish
Der Rest der #65-Analyse, soweit er die fünf SKILL.md selbst betrifft. Vier
Issues, fünf Dateien, kein Codeanteil.
Die Hard Rule von wiki-status war falsch (#70). Sie sagte „read-only.
Never writes, scaffolds, or modifies any file" — und Schritt 2 ruft lint,
schreibt also einen Report, was Schritt 2 sogar selbst beschreibt. Ein Agent,
der die Regel wörtlich nimmt, kann den Skill nicht ausführen; einer, der ihn
ausführt, hat die stärkste Aussage des Dokuments gebrochen, bevor er Schritt 5
erreicht. Das ist die teurere Sorte Widerspruch, weil die Hard Rule genau die
Stelle ist, an der ein Konflikt entschieden wird. Sie lautet jetzt wie die von
wiki-query — read-only gegenüber Wiki-Inhalt — und benennt den einen Write
mitsamt Grund: reports/ ist gitignored und trägt keine Wiki-Seite. Schritt 5
behauptet nicht mehr, es sei keine Datei geschrieben worden, sondern sagt, was
mit der geschriebenen nicht passiert (Semantic Review bleibt leer, nichts
wird ausgetragen — das ist wiki-lint Schritt 9). Der Decision Point „Never
publishes — nothing was written" trägt jetzt den wahren Grund: unter kb/ hat
sich nichts geändert, und der Report kann gar nicht in einen Commit geraten.
wiki-ingest und wiki-lint bekommen einen Abhak-Block (#74). Anthropics
Skill-Doku empfiehlt für „particularly complex workflows" eine Checkliste, die
der Agent in die Antwort kopiert und mitführt. Zwölf Schritte fallen
unzweifelhaft darunter. Der Ausschlag gibt aber nicht die Länge, sondern was
still ausfällt: ## Not Extracted in Schritt 6, die Coverage-Prüfung in
Schritt 10, die Lint-Kadenz in Schritt 12 — keiner davon erzeugt eine
Fehlermeldung, wenn er ausbleibt.
Die offene Frage des Issues — ob wiki-lint denselben Block bekommt — ist mit
ja beantwortet: neun Schritte, davon 3-6 reines Judgment, und ein Lauf, der
leise nur seine mechanische Hälfte gemacht hat, sieht aus wie ein
vollständiger. Damit haben zwei von fünf Skills einen Block und drei nicht, und
genau das wäre ohne festgeschriebenes Kriterium die nächste strukturelle
Ungleichheit im Sinne von #78. instructions/CONTRACT.md § Writing an
instruction trägt sie deshalb jetzt: ein Block, wenn ein Ablauf acht
Schritte oder mehr hat und darin still ausfallende Schritte stehen. Beide
Hälften nötig — ein langer Ablauf aus reinen Tool-Calls meldet seine Lücken
selbst, weil der nächste Call ohne den vorigen scheitert. Der Abschnitt nennt
die drei anderen Skills mit ihren Schrittzahlen, damit niemand aus Symmetrie
einen vierten Block nachrüstet.
wiki-query prüfte nicht, bevor es filete (#75). Die drei Kriterien
(Synthese über mehrere Seiten, etwas noch nicht Dokumentiertes, wird wieder
gefragt) standen im Filing-Schritt selbst, und der Skill darf mehrere Seiten
je Sitzung anlegen — es gab also keine Stelle, an der jede geplante Seite
einzeln gemessen wurde. Neuer Schritt 5 vor dem ersten new: Kandidaten
benennen, jeden für sich gegen alle drei halten, ein Stapel wird nie als Stapel
beurteilt. Wer durchfällt, wird nicht angelegt, sondern in der Antwort mit
einem Satz genannt — der Nutzer kann ihn trotzdem verlangen. Der bisherige
Filing-Schritt ist Schritt 6, log append Schritt 7, die Hard Rule zieht mit.
Der Mass-Update-Gate-Hinweis bleibt, sagt aber jetzt dazu, dass er keine
Ersatzprüfung ist: das Gate zählt Dateien und weiß nichts über Berechtigung,
und ein Stapel unter der Schwelle ist von ihm nicht freigegeben, nur nicht
angehalten worden. Dazu die session-setup.md-Zeile in derselben Form wie in
den drei anderen — Schritt 7 läuft immer und der Filing-Pfad zieht new,
xref add und die Rebuilds nach sich.
Die Kommandolisten gingen mit den Schritten auseinander (#78).
cite add fehlte in wiki-ingest und wiki-manage, obwohl beide es
ausdrücklich vorschreiben; types describe fehlte in wiki-ingest, wo
Schritt 6 die source_type-Werte daraus zieht; xref add fehlte in
wiki-lint, wo Schritt 1 das Umlabeln einer schwachen Kante darauf stützt;
publish fehlte in wiki-lint und wiki-query, wo je ein Decision Point es
beim Namen nennt. In die andere Richtung: rm stand in wiki-lints Liste,
ohne dass ein Schritt es begründet — die gefährlichere Richtung der Drift, weil
rm Seiten löscht. Dazu log status (entscheidet den Trigger, gelaufen wird
es von wiki-ingest) und die nie benutzten Flags lint --markdown und
lint --json. Beide Streichungen stehen jetzt als Deliberately absent unter
der Liste, mit Grund — sonst trägt sie jemand aus Vollständigkeit wieder ein.
Nicht als Drift gezählt und bewusst gelistet geblieben: publish,
log append, index rebuild und sources rebuild-index, wo ein Skill sie
über publish-cycle.md delegiert. Ebenso xref remove in wiki-manage, das
zum Unlinking-Fall gehört, den der Skill als Ganzes an page-lifecycle.md
abgibt; auch das steht jetzt als Satz dort, nicht als stille Annahme.
Der zweite Teil von #78 ist die Symmetrie: wiki-manage und wiki-status
tragen jetzt einen Beispielblock wie die drei anderen. Fünf Skills mit
demselben Aufbau sollten denselben Aufbau haben — ein fehlender Abschnitt liest
sich sonst als Aussage („hier gibt es keine typischen Fälle"), die niemand
gemeint hat.
Die offene Frage aus #78 — ob instructions verify diesen Abgleich künftig
selbst macht — ist mit ja beantwortet und als #83 ausgelagert. Trivial ist er
nicht: die drei Ausnahmen oben (Delegation über publish-cycle.md, benannte
Delegation an eine andere Instruction, „Deliberately absent") müsste ein Prüfer
alle kennen, sonst meldet er bei jedem Lauf dieselben Stellen. Nur die dritte
hat heute einen maschinenlesbaren Anker — den Absatz, den dieser Eintrag oben
eingeführt hat.
PATCH, geprüft gegen den Drop-in-Test: eine Instanz kopiert instructions/
über sich, nichts wird umbenannt oder entfernt, kein Kommando, kein Flag, kein
maschinengelesenes Format, und der Rückweg funktioniert genauso. Kein
--breaking, kein Migrationsdokument.
Geändert: instructions/wiki-status/SKILL.md, instructions/wiki-query/SKILL.md,
instructions/wiki-ingest/SKILL.md, instructions/wiki-lint/SKILL.md,
instructions/wiki-manage/SKILL.md, instructions/CONTRACT.md (§ Writing an
instruction, neuer Unterabschnitt „When a skill carries a copy-in checklist").
wiki-ingest bleibt mit 233 Zeilen unter Anthropics 500er-Schwelle.
Schließt #70, #74, #75 und #78.
Issue-Nummern in ausgelieferter Doku (#77): dist export lieferte
Dateien aus, die im Fließtext auf Issue-Nummern dieses Trackers verwiesen —
„flat since Gitea #67", „new source refuses without it (Gitea #66)". In einer
verteilten Instanz zeigt das auf nichts. Der Leser kann den Verweis weder
auflösen noch als unauflösbar erkennen, und eine Regel sieht damit so aus, als
stütze sie sich auf einen Beleg, den niemand beibringen kann. Das Board liegt
im Ursprungs-Repo, und instructions/dev/issue-tracking.md — die einzige Datei,
die das überhaupt sagt — wird von dist export mit dem Rest von
instructions/dev/ weggeschnitten. Gegenprobe zum eigenen Anspruch aus
instructions/CONTRACT.md § „Writing an instruction": „self-contained enough
for an agent with no prior context".
Gemessen statt geschätzt: ein Export in ein leeres Verzeichnis, grep -rn '#[0-9]', ergab 43 Treffer in 16 Dateien außerhalb von tools/**/*.py —
raw/CONTRACT.md allein acht. Das Issue hatte zehn gelistet.
Aufgelöst wurde nicht durch eine Markierung, sondern durch Umformulierung:
die Nummer fällt weg, die Datierung geht in Worte. Aus „flat since Gitea
#67" wird „flat since the addressing scheme dropped type directories", aus
„Pre-#67 files are not moved" wird „Files promoted under the old type
directories are not moved". Der Satz trägt sich damit selbst — es gibt keine
repoweite Notation zu definieren und an genau einer Stelle zu halten
(Invariante 8), und kein Leser von README.md muss AGENTS.md geladen haben,
um sie aufzulösen. Rückverfolgbar bleibt es hier über git blame → Commit-
Message; die tragen die Nummern ohnehin.
Zwei Stellen, an denen der Zeiger der ganze Wert des Satzes war und in Worten
nichts übrig geblieben wäre, stehen jetzt in einem
<!-- dist:strip-start/end -->-Block: in instructions/CONTRACT.md (was ein
Test der Kontextfenster-Behauptung kosten würde) und in EVALS.md (wo die
Coverage-Lücken geschlossen werden). Im Dev-Repo sichtbar, im Export weg — die
bestehende Konvention aus instructions/CONTRACT.md § instructions/dev/, hier
zum zweiten Mal angewandt statt neu erfunden.
docs verify prüft es jetzt — die offene Frage des Issues, mit Ja
beantwortet. check_no_issue_references liest nicht den Arbeitsbaum, sondern
den Text, den dist_cmd.build_plan() schreiben würde: dort leben ROOT_FILES,
der instructions/dev/-Ausschluss und das .template-Rekeying schon, und der
Text hat seine Marker-Blöcke bereits verloren. Deshalb ist ein Strip-Block
automatisch exemptiert, ohne dass der Check ihn kennen müsste.
Der Einwand aus instructions/dev/issue-tracking.md § „What no tool checks" —
wikitool soll den Tracker nicht kennen — trägt hier nicht, und das ist die
Grenze, die der Abschnitt jetzt selbst zieht: re.compile(r"#\d+") hat keinen
Client, keine URL und keinen Begriff vom Zustand eines Issues. Der Check sieht
eine Eigenschaft des Dokuments, nicht des Boards. Gemessen: null False
Positives über den gesamten Export, weil Markdown-Anker aus Wortzeichen
bestehen (](#gates) matcht nicht). Der erste Fund war prompt der Satz, den
diese Sitzung selbst in tools/CONTRACT.md geschrieben hatte, um die Regel zu
erklären.
tools/**/*.py bleibt bewusst außen vor, mit ~90 Treffern in Docstrings und
Kommentaren. Ein Code-Kommentar adressiert, wer die Zeile editiert, und das
passiert ausschließlich im Ursprungs-Repo: dist export schneidet den
stack-dev-Skill mit instructions/dev/ weg. Ein ausgeliefertes tools/ ist
Laufzeit-Maschinerie, keine Lektüre. .gitignore und tools/.coveragerc sind
aus demselben Grund nicht im Check — von Hand mitgezogen wurden sie trotzdem,
sodass der Export heute in keiner Datei außerhalb .py eine Nummer trägt.
MINOR, geprüft gegen den Drop-in-Test: kein Kommando, kein Flag, kein
Dateiformat, keine Umbenennung; der Rückweg funktioniert unverändert, die alte
Version führt den Check schlicht nicht aus. Kein --breaking, kein
Migrationsdokument. Eine Konsequenz ist zu kennen: der Check liest auch die
instanzeigenen kb/CONVENTIONS.md und kb/<collection>/COLLECTION.md, weil ein
Export sie als .template mitnimmt. Eine Instanz, die dort ihre eigene
Ticket-Nummer zitiert, bekommt beim nächsten docs verify ein Finding. Das ist
kein Fehlalarm — ein Export dieser Instanz würde den Verweis weitergeben —
aber es ist neu.
Geändert: tools/chemenu/commands/docs_verify.py (neuer Check plus
shipped_prose()), tools/chemenu/tests/test_docs_verify.py (sechs Tests:
sauberer Baum, präparierte Datei, Anker-Nicht-Treffer, .py außerhalb des
Scans, Strip-Block unsichtbar, verify bricht ab), tools/CONTRACT.md
(Kommandotabelle und Fehlerkontrakt-Zeile), tools/README.md,
instructions/dev/issue-tracking.md (neuer § Citing an issue in the repo, und
§ What no tool checks zieht die Grenze zwischen „was dieses Repo über den
Tracker schreibt" und „dem Tracker selbst"), sowie die 16 Doku-Dateien:
raw/CONTRACT.md, kb/CONTRACT.md, tools/CONTRACT.md, types/type-spec.md,
types/source.schema.yaml, kb/sources/COLLECTION.md,
kb/concepts/COLLECTION.md, instructions/wiki-ingest/SKILL.md,
instructions/evolve-subtypes.md, instructions/bootstrap.md,
instructions/kb-profiles.md, instructions/mcp-read-server.md,
instructions/CONTRACT.md, README.md, EVALS.md, INSTALL.md,
docs/pipeline-rationale.md, .gitignore, tools/.coveragerc.
Schließt #77.
Warum das eine MAJOR ist, obwohl kb/ unberührt bleibt (#73, #76): Anthropics
Skill-Authoring-Doku verlangt für Referenzdateien über 100 Zeilen ein
Inhaltsverzeichnis, damit ein Agent, der eine solche Datei nur mit head -100
anliest, trotzdem die volle Abschnittsübersicht sieht — dieselbe Vorschau-Mechanik,
die #72 schon für die Referenztiefe als real anerkannt hat. Ein von Hand
gepflegtes Inhaltsverzeichnis wäre die nächste Drift-Quelle; also ist es jetzt eine
dritte generierte Region neben xrefs und cites (<!-- wikitool:toc --> ...
<!-- /wikitool:toc -->, tools/chemenu/toc.py), erzeugt und geprüft wie jede
andere abgeleitete Kopie. wikitool docs toc [--apply] schreibt sie; docs verify
prüft jetzt, dass sie auf jeder Datei aktuell ist, die AGENTS.md, ein
Stage-/Collection-Contract oder die flache instructions/**.md-Form abdeckt (25
Dateien in diesem Repo, instructions/dev/ eingeschlossen — strukturell dieselbe
Dateiform, nur von dist export ausgenommen). Der Umfang ist berechnet, nie eine
Handliste: er folgt AGENTS.md § File naming, nicht einer Link-Traversierung ab den
fünf Content-Skills, und schließt types/<name>.md-Einzelspecs bewusst aus — die
laufen über wikitool types describe, das den Inhalt neu rendert statt die Datei
roh auszugeben, sodass die Vorschau-Mechanik dort gar nicht greift.
Das ist grenzüberschreitend, weil docs verify damit eine neue Pflichtprüfung
über bestehenden Inhalt bekommt: eine Instanz mit einer eigenen
instructions/*.md-Datei über 100 Zeilen, an der nichts geändert wurde, sieht
docs verify nach reinem Tool-Update neu fehlschlagen, bis einmalig
wikitool docs toc --apply läuft und der Diff committet wird — derselbe
Bruchtyp wie ein verschärftes Type-Spec-Pflichtfeld. Keine Migration nötig, weil
kein kb/-Inhalt betroffen ist; der einmalige docs toc --apply-Lauf ist der
volle Reparaturweg.
Zwei Nebenfunde beim Bauen der TOC-Regel, beide vor dem Bump behoben, weil sie
sonst denselben Bump falsch aussehen ließen: docs verifys
ISSUE_REFERENCE_RE (#\d+) hielt numerierte-Schritt-Anker wie
#2-fix-the-fidelity-before-writing-a-word für Issue-Zitate — die Regel nahm
bisher an, dass ein Anker immer mit einem Buchstaben beginnt, was für
nummerierte Überschriften (instructions/capture-session.md) nicht mehr gilt;
behoben durch einen Lookbehind, der genau die ](#...-Linkfragment-Form
ausschließt, ohne ein echtes (#66)-Zitat zu übersehen. Und
instructions_cmd.dev_only_forbidden_references prüfte mit blankem
name in text: ein TOC-Anker wie #where-stack-development-happens
(instructions/private-instance.md) enthält „stack-dev" als reine Teilzeichenkette,
ohne den Skill zu meinen — behoben durch eine wortgrenzengebundene
Regex-Suche.
Geändert: tools/chemenu/toc.py (neu), tools/chemenu/commands/docs_verify.py
(check_toc_regions, docs toc-Kommando, ISSUE_REFERENCE_RE-Lookbehind),
tools/chemenu/commands/instructions_cmd.py
(dev_only_forbidden_references wortgrenzengebunden), tools/CONTRACT.md
(docs toc-Zeile), tools/chemenu/tests/test_toc.py (neu, 15 Tests),
tools/chemenu/tests/test_docs_verify.py (drei neue Tests: nummerierter Anker,
geklammertes echtes Zitat, TOC-Region auf dem realen Baum),
tools/chemenu/tests/test_instructions_cmd.py (ein neuer Test für die
Teilzeichenketten-Kollision), sowie die 25 Referenzdateien, die jetzt eine
TOC-Region tragen: AGENTS.md, kb/CONTRACT.md, kb/CONVENTIONS.md,
kb/concepts/COLLECTION.md, raw/CONTRACT.md, tools/CONTRACT.md,
types/type-spec.md, instructions/CONTRACT.md, instructions/gates.md,
instructions/setup-instance.md, instructions/private-instance.md,
instructions/link-taxonomy.md, instructions/kb-profiles.md,
instructions/ingest-large-tree.md, instructions/capture-session.md,
instructions/claude-code-model-selection.md, instructions/german-terminology.md,
instructions/evolve-subtypes.md, instructions/mcp-read-server.md,
instructions/migrate-corpus.md, instructions/migrations/3.0.0-authoring-conventions.md,
instructions/migrations/4.0.0-link-taxonomy.md, instructions/dev/issue-tracking.md,
instructions/dev/testing-conventions.md, instructions/dev/version-parts.md.
Zusätzlich, unabhängig davon (#76): instructions/session-setup.md § Scope und
instructions/gates.md behaupteten, die Budget-Ausnahme richte sich danach, ob
ein Kommando das Wiki verändert. Tatsächlich zählt run_budget.py eine feste
Allowlist (SKIP_COMMANDS/SKIP_COMMAND_PATHS) — lint schreibt nur ins
gitignorte reports/, sieht also lesend aus, steht aber nicht auf der Liste und
zählt wie jedes mutierende Kommando. Beide Dateien verweisen jetzt auf die Liste
in tools/CONTRACT.md, statt sie mit einer falschen Faustregel zu umschreiben.
Kein Versionsbezug — reine Prosa-Korrektur, im selben Bump mitgeführt.
Nachgezogen in -beta.3: dist export erzeugt die TOC-Region jetzt nach
dem Marker-Strip neu. Ein <!-- dist:strip-start/end -->-Block kann eine ganze
Sektion umschließen — der in AGENTS.md umschließt ## Developing this stack —,
sodass die ausgelieferte Datei eine Überschrift weniger hat, als das im
Arbeitsbaum erzeugte Inhaltsverzeichnis auflistet. Die frische Instanz wäre
damit beim allerersten docs verify über eine Datei gefallen, die niemand
angefasst hat. Gefunden hat das die CI im Export-Replay („The distribution works
as a fresh instance"), nicht pytest und nicht docs verify im Arbeitsbaum —
beide sehen den gestrippten Text nie. Der Regressionstest sitzt jetzt in
test_dist_cmd.py.
Nachgezogen in -beta.2, weil docs verify die eigene Dokumentationstreue nur
für die Existenz einer Kommandozeile prüft, nicht für deren Inhalt: die
docs verify-Zeile in tools/CONTRACT.md nennt jetzt die TOC-Prüfung, die
Fehlerkontrakt-Tabelle bekommt die fehlende docs toc-Zeile (Schritt 3 in
tools/README.md § Adding a command verlangt beide Tabellen, geprüft wird nur
eine), und tools/README.md § Adding a command Schritt 5 nannte als
--major-Kriterium „wenn bestehender Inhalt migriert werden muss" — was
instructions/dev/version-parts.md ausdrücklich verneint und was dieser Bump
selbst widerlegt: grenzüberschreitend mit --no-migration.
Schließt #73, #76.
Konfidenz-Mechanismus ersatzlos entfernt (schließt #60). confidence,
confidence_base, der Zeit-Decay und die Konfidenz-Rubrik sind aus Schema,
Kommandos (confidence decay/init-base, touch --confidence-base), Lint
(confidence_exceeds_source_standing), Suche (--sort -confidence,
Konfidenzspalte) und jeder Doku-Stelle entfernt, die sie erwähnte.
Der Mechanismus wurde gemessen, nicht nur für unschön befunden: 43 der 152
betroffenen Seiten trugen nie mehr als den Schema-Vorgabewert 0.5, der Decay
hat seit seiner Einführung keinen einzigen Wert bewegt (confidence_decay.py
übersprang concept_type: decision, touch --confidence-base zog das
abgeleitete Feld nie mit), und kein Konsument im Stack hing außer über
corpus_diff.STRUCTURAL_FIELDS — eine Abhängigkeit von der Existenz des
Feldes, nicht von seinem Wert — überhaupt an ihm. Vier Entwürfe für eine
Reparatur (Rubrik nachjustieren, Quellenautorität anheben, aus
authority × fidelity berechnen, zwei Schubladen für Reifegrad und
Volatilität) scheiterten an denselben Messwerten. Die vollständige Studienlage
steht in #85, wo das Thema als zurückgestellt geführt wird, nicht als
verworfen.
An die Stelle tritt nichts Neues: kb/CONVENTIONS.md bekommt eine
Prosa-Hedging-Regel — nach Quellenlage hedgen statt nach Schwellenwert, siehe
kb/CONVENTIONS.md § Hedging — und die Arbeitsliste ersetzt
--field 'confidence<0.6' durch Prädikate auf tatsächlich aufgezeichneten
Feldern (--field '!sources', --field provenance=general). Mit dem Feld
stirbt auch confidence_exceeds_source_standing, der einzige automatische
Abgleich zwischen einer Seite und der Standing ihrer Quellen; ein Ersatz ohne
Zahl ist als Kandidat in #85 vorgemerkt, aber bewusst nicht Teil dieses Pakets.
version bump bekommt dabei ein neues Flag, --migration-required: der
laufende Kandidat hatte in einem früheren Bump --no-migration erklärt, und
dieses Paket macht die Erklärung falsch. Die **Migration:** none required-Zeile ist maschinengeschrieben (Invariante 1 verbietet den
Handgriff), und bislang gab es keinen Weg, sie zurückzunehmen, sobald ein
späterer Bump doch eine Migration braucht. Das Flag entfernt die Zeile
stattdessen und verlangt ein Migrationsdokument, das die neue Basisversion
referenziert, bevor es das tut.
Was das für eine bestehende Instanz bricht, steht in der **Breaking Change:**-Zeile oben. instructions/migrations/5.0.0-confidence-removal.md
ist der mechanische Strip der zwei Felder über den betroffenen Korpus,
mitsamt der Invariante, die ein automatisierter Lauf einhalten muss (kein
modified:-Bump, byte-identischer Body, unveränderte Referenzarrays).
Nachgezogen in -beta.6: instructions/dev/version-parts.md Schritt 6
beschreibt den Rücknahmepfad jetzt selbst. tools/CONTRACT.md führte das neue
Flag bereits, aber die Instruction, die eine Sitzung vor einem Bump liest,
kannte den Fall nicht — dieselbe Sitzung ist genau darüber gestolpert. Der
Abschnitt nennt auch, warum ihn nichts meldet: die Zeile ist
maschinengeschrieben, docs verify genügt ihr bloßes Vorhandensein, und die
Eskalationsprüfungen laufen nur auf dem Bump, der die Grenze zuerst
überschreitet.
Nachgezogen in -beta.7: instructions/wiki-status/SKILL.md trägt jetzt den
session-setup.md-Verweis, den die anderen vier Content-Skills längst haben.
Der Skill ruft in Schritt 2 lint auf, und lint steht nicht auf der
Ausnahme-Allowlist in run_budget.py — er zählt wie jedes mutierende
Kommando. Ohne exportierte WIKITOOL_SESSION_ID fällt die Zählung auf
getppid() zurück, die Sitzung erbt also den Stand irgendeiner fremden Shell.
Bis -beta.6 war das Fehlen des Verweises durch die falsche Regel gedeckt, die
session-setup.md § Scope selbst aufstellte („verändert das Wiki"); seit sie
die tatsächliche Allowlist nennt, ist es schlicht ein Loch.
Die Alternative — lint durch etwas Befreites ersetzen und wiki-status
wirklich budgetfrei machen — scheidet an der Sache aus: der Skill liest aus dem
Report die Graph-Auswertung (Broken Links, Orphans, Most-Linked Pages,
uncovered raw files), und kein befreites Kommando liefert die. doctor prüft
die Installation, nicht den Korpus; search ist Retrieval. Ein wiki-status
ohne lint wäre kein leichterer Skill, sondern ein leerer.
Damit gilt über alle fünf Content-Skills dieselbe Aussage: ein Skill verlinkt
session-setup.md genau dann, wenn er mindestens ein nicht-befreites
wikitool-Kommando aufruft. Geprüft wird sie nicht — instructions verify
kennt weder die Kommandolisten der Skills noch die Allowlist. Das wäre ein
eigener Schnitt.
Nachgezogen in -beta.8: CLAUDE.md importierte bislang USER.md, SOUL.md,
ENVIRONMENT.md und instructions/claude-code-model-selection.md zusätzlich
zu AGENTS.md — eine Harness-Drift, denn dieselben drei
Personalisierungsdateien werden auf den anderen drei Harnesses (Codex CLI,
Copilot, Vibe) allein durch AGENTS.mds eigene Anweisung gelesen, nie
injiziert. CLAUDE.md importiert jetzt nur noch AGENTS.md; die Bedingung
„if the runtime has not already injected them" in AGENTS.md §
Personalization entfällt, weil kein Runtime mehr injiziert.
instructions/claude-code-model-selection.md ist entfernt und als
docs/model-and-effort-selection.md neu geschrieben, in Empfehlungsstimme
statt als Instruktion: die Datei beschrieb überwiegend Handlungen, die eine
Sitzung nicht selbst ausführen kann (das eigene Modell, /code-review-Stufen),
und wurde im ganzen Baum nur von den beiden dev-only Skills stack-dev und
stack-close referenziert, deren Links jetzt dorthin zeigen. Eine
bestehende Instanz behält die entfernte Datei als Überbleibsel, bis sie
wikitool dist upgrade --prune laufen lässt oder die Datei von Hand löscht —
instructions verify meldet sie sonst neu als verwaist.
AGENTS.md §§ Personalization, Environment und File naming sind an den
Stellen gekürzt, die eine zweite Kopie einer Regel waren, die
docs/ownership-and-templates.md oder eine Invariante schon trägt; § File
naming verlinkt jetzt alle vier docs/-Seiten namentlich, was vorher
nirgends geschah. USER.md und SOUL.md verlieren an derselben Stelle
Rahmen- bzw. Herkunftsprosa, die USER.md.template bzw. ein Kommentar in
SOUL.md selbst schon trägt.
Telemetrie-Default nach Installationsform (#55): chemenu.telemetry.writer.enabled() war
eine Zeile - immer an, WIKI_TRACE=0 das einzige Opt-out. Richtig für dieses Repo, dessen
Traces das Messinstrument sind, mit dem der Stack sich selbst bewertet, aber die falsche
Voreinstellung für eine ausgelieferte Instanz: dort hat niemand Telemetrie bestellt, und
niemand liest EVALS.md, bevor die erste Datei geschrieben ist. Dazu kam eine zweite Lücke:
keine Mengenbegrenzung irgendeiner Art - reports/telemetry/<session>/trace.jsonl wächst,
solange die Instanz läuft, und nichts räumt je etwas weg.
tools/chemenu/telemetry/policy.py (neu) löst jetzt beides an einer Stelle, gekeyt auf den
aufgelösten Root, damit wikitool doctor, der Writer und der MCP-Server-Start-Guard dieselbe
Antwort für denselben Checkout geben. Ein Git-Clone dieses Repos bleibt beim alten Verhalten
(an, WIKI_TRACE=0 schaltet ab); eine per dist export ausgelieferte Instanz startet ab jetzt
mit Telemetrie aus - erkannt an der ohnehin vorhandenen, maschinengeschriebenen
.wikitool-release.json (Invariante 1). Wer sie dort anschalten will, legt eine
.wikitool-telemetry.json an (pro Checkout, gitignored, kein .template - wie
.wikitool-remotes.json); WIKI_TRACE überschreibt weiterhin beide Richtungen und schlägt die
Datei.
Zwei unabhängige, fail-silent durchgesetzte Mengendeckel greifen in beiden Installationsformen:
ein Byte-Deckel pro Session-Trace (Default 5 MiB, ein stat vor jedem Append) und eine
Retention über die Anzahl der Session-Verzeichnisse (Default 250). Die Retention reserviert den
Platz der gerade entstehenden Session, statt sie mitzuzählen - sonst pendelt der Bestand
dauerhaft bei keep+1 statt bei keep, weil jeder Lauf immer nur das räumt, was der vorige
Lauf über dem Limit gelassen hat. Am Byte-Limit schreibt ein weiterer Aufruf nichts mehr außer
einem einmaligen telemetry.limit-Event, per Exclusive-Create auf eine .limit-Sentinel-Datei
ausgelost - derselbe Ein-Schreiber-Trick wie beim session.start-Header, für den Fall, dass
mehrere Prozesse gleichzeitig auf denselben Trace schreiben. Eine Retention-Runde löscht
ausschließlich trace.jsonl/.limit der überzähligen Verzeichnisse und rmdirt nur, wenn
danach leer - nie rmtree, aus demselben Grund wie bei upstream merge (#30):
reports/telemetry/ hält lokale, nicht rekonstruierbare Daten, die eval score liest.
wikitool doctor bekommt einen neuen telemetry-Check (an/aus, warum, Menge gegen beide
Deckel, nie FAIL, wie publish-remotes und environment). Der MCP-Server-Start-Guard
(check_trace_destination) las WIKI_TRACE bisher selbst statt den Writer zu fragen - eine
zweite Kopie derselben Regel, die Invariante 8 verletzte, bevor diese Änderung sie schließt; er
fragt jetzt dieselbe Policy. instructions/setup-instance.md bekommt einen neuen
Entscheidungspunkt (Schritt 10).
MINOR, kein neuer Boundary-Crossing: additiv in beide Richtungen - eine bestehende Instanz
kopiert die neue Maschinerie über sich und hört still auf zu schreiben, ohne Hand-Arbeit oder
Migration; die alte Version zurücklegen stellt den alten Default wieder her, weil sie die neue
Datei und die neuen Variablen schlicht ignoriert. Der Kandidat trägt seine --breaking-Zeile
bereits aus einem früheren Bump (Confidence-Entfernung); diese Änderung fügt keine neue hinzu.
Geändert: tools/chemenu/telemetry/policy.py (neu), tools/chemenu/telemetry/writer.py,
tools/chemenu/telemetry/schema.py, tools/chemenu/commands/doctor.py,
tools/chemenu/mcp/server.py, tools/chemenu/config.py, tools/chemenu/version.py,
.gitignore, tools/chemenu/tests/conftest.py und die Telemetrie-/Doctor-/MCP-Server-Tests,
.gitea/workflows/ci.yml, EVALS.md, INSTALL.md, INSTALL-MCP.md, reports/CONTRACT.md,
tools/CONTRACT.md, instructions/setup-instance.md. Schließt #55.
tools/CONTRACT.md beschrieb raw accept noch vor #67: Typverzeichnisse statt Datums-Shard,
kein --fidelity/--authority, Stem-Eindeutigkeit "at raw/<type>/ level" statt global. Vier
Zellen (beide raw accept-Zeilen der Kommandotabelle, beide im Fehlerkontrakt) waren an #67
vorbeigeschrieben worden - raw/CONTRACT.md selbst war korrekt, docs verifys check_commands
prüft nur Kommandonamen gegen die Tabelle, nicht deren Prosa gegen den Code. Reine
Doku-Korrektur, kein Verhalten geändert. Schließt #89.
Der MCP-Server bekommt ein sechstes, optionales Tool: submit, ein Schreibpfad für Dokumente
von einem Aufrufer, der nicht dieses Terminal ist. #19s Eigenschaft "kein Tool schreibt" galt
strukturell - nichts unter chemenu.commands war importierbar - und diese Formulierung wird mit
einem echten Schreibpfad falsch. Die tragfähige Ersatzformulierung ist eine Positiv-Liste
statt einer Abwesenheit: der Serverprozess darf in genau ein Verzeichnis schreiben,
mcp-upload/, erzwungen durch eine einzige Funktion (chemenu.upload._write_atomic_within),
die jeden aufgelösten Zielpfad gegen dieses eine Verzeichnis prüft - Tests decken ..,
absolute Pfade und einen Symlink, der aus dem Verzeichnis hinausführt. Die alte
Abwesenheitseigenschaft bleibt daneben unverändert bestehen: die Reviewer-Kommandos
(upload accept/upload reject) liegen unter chemenu.commands und sind vom Server aus nicht
erreichbar.
Zwei Stufen vor raw/, zwei verschiedene Grenzen. mcp-upload/<id>/ hält Material, das
niemand geprüft hat; wikitool upload accept <id> --confirm <token> befördert es nach
incoming/, wo es sich nicht mehr von einer lokal abgelegten Datei unterscheidet und
wiki-ingest Schritt 1 unverändert greift. Ohne Token verweigert upload accept mit Exit
42 - der vierte Gate des Stacks, Upload Review Gate, gleiche Form wie der
Mass-Update-Gate: ein Token, der Id, Dateiname, Größe, Sha256 und Einreicher digestet, wird
also ungültig, sobald sich das Manifest ändert. wikitool upload reject <id> --reason "<warum>"
braucht keinen Gate - Ablehnen braucht keine Freigabe, nur Annehmen tut das - und löscht das
Material, behält aber Grund und Sha256 im append-only mcp-upload/ledger.jsonl.
Die Einreicher-Identität kommt ausschließlich aus einem HTTP-Header, nie aus einem
Tool-Argument. identity_header (Default X-Forwarded-User) in .wikitool-upload.json
nennt den Header; fehlt er auf der Anfrage, wird ohne jeden Schreibvorgang verweigert - eine
unzurechenbare Einreichung ist damit unmöglich, nicht nur unerwünscht. Das Manifest hält neben
submitter auch submitter_source (den Headernamen), damit der Datensatz sagt, worauf die
Behauptung ruht, statt sie als Tatsache zu führen. Die Middleware muss den Header selbst setzen
und eine vom Client mitgeschickte Kopie verwerfen - eine Deployment-Pflicht, dokumentiert in
INSTALL-MCP.md Schritt 5 und instructions/ingest-queue.md, die der Prozess selbst nicht
erzwingen kann.
.wikitool-upload.json ist ein struktureller Opt-in, nicht bloß eine Konfiguration. Fehlt
die Datei, wird das submit-Tool gar nicht erst registriert - anders als bei
.wikitool-remotes.json, wo Abwesenheit "unbeschränkt" heißt, heißt sie hier "der Schreibpfad
existiert nicht". Eine defekte Datei ist ein Startfehler des Servers (ValidationError beim
Aufbau) und ein FAIL in wikitool doctors neuem upload-intake-Check - nie "keine
Beschränkung". Weitere Schutzschichten in chemenu/upload.py: eine Größenprüfung auf der
base64-Länge vor dem Dekodieren (mit einem Toleranzband von 2 Bytes für Padding, damit sie
keine an der Grenze liegende, legitime Einreichung fälschlich ablehnt - der Nachdekodier-Check
bleibt die exakte Durchsetzung), eine Endungs-Positivliste, ein rollierendes
24-Stunden-Kontingent pro Einreicher (Anzahl und Bytes, aus dem Ledger berechnet, nie aus
Verweigerungen), und eine Ablehnung doppelter Inhalte, solange die erste Einreichung noch
wartet - unter Nennung der wartenden Id.
incoming/ bleibt komplett unberührt als zweite, unabhängige Grenze. Ein Unterverzeichnis
dort wird seit #67 toleriert und ignoriert, sodass eine Fremdeinreichung darunter still
promotierbar gewesen wäre - deshalb ein eigenes Top-Level-Verzeichnis mcp-upload/, gitignored,
ohne .gitkeep (die Schreibprimitive legt es selbst an). docs_verifys Ignore-Kanarien prüfen
jetzt auch mcp-upload/probe.pdf und .wikitool-upload.json.
MINOR, kein neuer Boundary-Crossing: additiv in beide Richtungen geprüft - eine bestehende
Instanz kopiert die neue Maschinerie über sich und bekommt ein neues, standardmäßig
unregistriertes Tool, ohne Hand-Arbeit oder Migration; kein .wikitool-kb.json-Feld, kein
Type-Spec, kein umbenanntes Kommando oder Flag, kein geändertes Dateiformat. Die alte Version
zurücklegen verliert nichts - der Schreibpfad existiert dort schlicht nicht. Der Kandidat trägt
seine --breaking-Zeile bereits aus einem früheren Bump (Confidence-Entfernung); diese Änderung
fügt keine neue hinzu.
Geändert: tools/chemenu/upload.py (neu), tools/chemenu/commands/upload_cmd.py (neu),
tools/chemenu/tests/test_upload.py (neu), tools/chemenu/tests/test_upload_cmd.py (neu),
tools/chemenu/mcp/server.py, tools/chemenu/tests/test_mcp_server.py,
tools/chemenu/commands/doctor.py, tools/chemenu/tests/test_doctor.py,
tools/chemenu/commands/docs_verify.py, tools/chemenu/cli.py, tools/chemenu/config.py,
.gitignore, tools/CONTRACT.md, AGENTS.md, instructions/gates.md,
instructions/mcp-read-server.md, instructions/wiki-ingest/SKILL.md,
instructions/ingest-queue.md (neu), raw/CONTRACT.md, docs/why-gates-are-code.md,
INSTALL-MCP.md, README.md. Schließt #32.
AGENTS.md behauptete, docs verify prüfe die Contract-Prosa selbst - das stimmt nicht (#90).
check_cli_readme() prüft nur, ob der gebacktickte Pfad einer Tabellenzeile registriert ist, nie
den Rest der Zelle: eine ausgetauschte Kommandobeschreibung oder ein neues --flag bleiben
unsichtbar, und TABLE_CELL_RE läuft über das ganze Dokument statt über eine abgegrenzte
"Kommandotabelle". AGENTS.md § Changelog zählte trotzdem "command tables, contracts, ignore
canaries" als maschinell geprüft auf und grenzte die Sitzungspflicht auf README.md/EVALS.md/
tools/README.md ein - tools/CONTRACT.md und jedes <stage>/CONTRACT.md standen damit auf
keiner Liste, die je jemand nachzieht.
Der Absatz nennt jetzt nur noch, was docs verifys eigene Zeile in tools/CONTRACT.md
tatsächlich auflistet (Verweis statt Kopie), und macht jede Zellenprosa - Kommandotabelle,
Fehlerkontrakt, Stage-Contract - ausdrücklich zur Sitzungsarbeit, neben den drei README-artigen
Dateien. Die Docstring von check_cli_readme() behauptet nicht mehr, sie prüfe
"tools/CONTRACT.md's command table" - sie beschreibt jetzt, dass TABLE_CELL_RE das ganze
Dokument scannt und nur den gebacktickten Pfad liest, nie die restliche Zelle.
Neue Instruction instructions/dev/doc-pull-through.md
(dev-only) listet je berührter Fläche, welches Dokument eine Behauptung darüber trägt: beide
Tabellen in tools/CONTRACT.md, der berührte <stage>/CONTRACT.md, AGENTS.md bei
verschobener Regel/Gate/Invariante, die README-artigen Dateien, docs/ bei verschobener
Begründung. stack-dev/SKILL.md bekommt dafür einen neuen Schritt 5 zwischen Versionsbump und
Verify/Publish (jetzt Schritt 6) - ein Satz plus Link, die Liste bleibt in der Instruction; die
Katalog-Liste in Schritt 2 und stack-close/SKILL.md Schritt 3 nennen die neue Instruction bzw.
die Contracts jetzt ebenfalls. version-parts.md und testing-conventions.md korrigieren dabei
zwei schon vorher falsche Schrittverweise auf stack-dev (Schritt 3 -> 4, Schritt 4 -> 6),
gefunden beim Nachziehen der Umnummerierung.
MINOR, kein neuer Boundary-Crossing: additiv und drop-in in beide Richtungen - eine
bestehende Instanz kopiert die neue Instruction und die korrigierte Prosa über sich, ohne
Migration oder Hand-Arbeit; kein Feld, kein Kommando, kein Flag ändert sich. Der Kandidat trägt
seine --breaking-Zeile bereits aus einem früheren Bump; diese Änderung fügt keine neue hinzu.
Geändert: AGENTS.md, tools/chemenu/commands/docs_verify.py,
instructions/dev/doc-pull-through.md (neu), instructions/dev/stack-dev/SKILL.md,
instructions/dev/stack-close/SKILL.md, instructions/dev/version-parts.md,
instructions/dev/testing-conventions.md. Verifiziert: tools/wikitool docs verify,
tools/wikitool instructions verify (nach instructions sync), volle pytest-Suite (1185
passed). Schließt #90.
docs verify prüfte § Commands und § Error contracts in tools/CONTRACT.md als einen Topf
(#91). TABLE_CELL_RE sammelte das erste gebacktickte Wort jeder Tabellenzeile über das ganze
Dokument, ohne Abschnittsgrenze - eine aus § Commands gelöschte Zeile fiel nicht auf, solange
derselbe Name noch in § Error contracts stand, und § Error contracts wurde gegen nichts
erzwungen. Gemessen am Baum vor diesem Fix (55 registrierte Kommandos): § Commands war
vollständig, § Error contracts fehlten zehn Zeilen - budget reset, instructions list,
instructions verify, log append, migrate status, sources rebuild-index, sources trace,
types describe, upload show, version notes - und docs verify blieb grün.
check_cli_readme() liest die beiden Tabellen jetzt über ihre ##-Überschrift ab (neue
section_text()), unabhängig voneinander und je Tabelle in beide Richtungen; fehlt eine der
beiden Überschriften, meldet die Funktion das explizit statt stillschweigend auf "ganzes
Dokument" zurückzufallen. Ursache der zehn Lücken war überwiegend eine zweite, bisher unsichtbare
Untugend: mehrere Kommandos teilten sich in § Error contracts eine Zeile der Form "a / b" -
nur das erste Backtick-Wort einer Zeile zählte je als "dokumentiert", der Rest der Gruppe war für
den (ungeprüften) Check unsichtbar. Behoben, indem jede betroffene Gruppe in eigene Zeilen mit
eigenem, gegen den Code nachgesehenem Inhalt aufgeteilt wurde - dabei fielen nebenbei drei
sachlich falsche Zeilen auf: sources coverage, types list, instructions list und budget status schlagen nie fehl, die alte gemeinsame Zeile behauptete das Gegenteil, weil sie das
Verhalten des jeweils benachbarten Kommandos mit übernahm. Eine echte Formatierungslücke kam
dazu: log appends Zeile stand im Quelltext ohne Zeilenumbruch hinter der von index rebuild /
sources rebuild-index verschmolzen - für Menschen als Tabelle kaum lesbar und für die
zeilenanfang-verankerte Regex unsichtbar.
MINOR, kein neues Boundary-Crossing: additiv und in beide Richtungen drop-in - eine
bestehende Instanz kopiert docs_verify.py und tools/CONTRACT.md über sich, ohne Hand-Arbeit
oder Migration. Eine Ausnahme ist es wert, genannt zu werden: eine private Instanz mit einem
lokal abweichenden tools/CONTRACT.md (instructions/private-instance.md) kann nach diesem
Update zum ersten Mal an der strengeren Prüfung scheitern, wenn ihr eigener Baum dieselbe
Fehlerkontrakt-Lücke trägt oder ihre Kommandotabelle anders benannte ##-Überschriften
verwendet. Das ist kein Boundary-Crossing - kein Dateiformat ändert sich, keine bestehende
Funktion verschwindet -, sondern derselbe bereits akzeptierte Fall, den jede Verschärfung von
docs verify seit jeher mit sich bringt: der Fix legt eine bereits vorhandene
Dokumentationslücke bloß, statt eine neue Anforderung einzuführen. Der Kandidat trägt seine
--breaking-Zeile bereits aus einem früheren Bump; diese Änderung fügt keine neue hinzu.
Geändert: tools/chemenu/commands/docs_verify.py (section_text, check_cli_readme),
tools/CONTRACT.md (die docs verify-Zeile sowie zehn aufgeteilte bzw. nachgetragene
Fehlerkontrakt-Zeilen), tools/chemenu/tests/test_docs_verify.py (neue Tests für
Abschnittstrennung, fehlende/umbenannte Überschrift, § Error contracts in beiden Richtungen).
Verifiziert: tools/wikitool docs verify, tools/wikitool instructions verify, volle
pytest-Suite (1193 passed). Schließt #91.
tools/CONTRACT.md ist jetzt ein Dokument zum Nachschlagen statt zum Durchlesen (#92).
Mit 70 KB war es das größte Dokument im Repo - größer als AGENTS.md, kb/CONTRACT.md und
raw/CONTRACT.md zusammen -, und 89 % davon lagen in den zwei Kommandotabellen. Gleichzeitig
war es für gezielten Abruf bereits ideal gebaut, ohne dass es irgendwo stand: eine Zeile ist
ein Kommando, physisch einzeilig, also liefert ein einziges grep '^| `<kommando>' beide
Hälften seines Vertrags - was es tut und wie es fehlschlägt - und sonst nichts. AGENTS.md
routete stattdessen mit "Full command reference" dorthin, was sich als Vollread liest.
Drei Änderungen: die Datei trägt die Nachschlage-Regel samt grep-Zeile jetzt als Lead-in vor
dem Inhaltsverzeichnis (genau einmal, Invariante 8 - AGENTS.mds Routing-Zelle beschreibt nur
noch die Form und wiederholt die Zeile nicht); beide Tabellen sind in vierzehn ###-Gruppen
gegliedert, wodurch docs toc erstmals Anker unterhalb ## erzeugt (vorher existierte im
ganzen Repo genau ein Anker-Link in diese Datei); und § Error contracts steht nun in derselben
Gruppen- und Zeilenreihenfolge wie § Commands, sodass die zwei Hälften eines Vertrags parallel
liegen - vorher saß etwa links show einmal zwischen xref link-source und cite id, einmal
zwischen version release und migrate list. Nebenbei repariert: die dist export-Zeile war
über vier physische Zeilen umgebrochen und damit kein gültiger GFM-Tabellen-Datensatz mehr.
Die Umstellung lief per Skript mit einer Behauptung über die Multimenge der Erst-Zellen vor und
nach dem Schreiben, nicht per Augenschein - bei 117 Zeilen ist "keine verloren" nichts, was ein
Review zusieht. Dass ### die Abschnittsgrenze aus #91 nicht bricht, ist jetzt ein eigener
Test: section_texts Lookahead (?=^#{1,2}[ \t]|\Z) verlangt nach ein bis zwei # ein
Space/Tab, was bei ### fehlschlägt - ohne diese Eigenschaft wäre § Commands an seiner ersten
Gruppe abgeschnitten und jedes spätere Kommando als undokumentiert gemeldet worden.
Bewusst nicht angefasst: die siebzehn Tabellenzellen über 900 Zeichen. Der naheliegende Schritt
wäre, ihren Begründungsanteil nach docs/ zu schieben; die Gegenprobe an der größten Zelle
(publish, 3.375 Zeichen) zeigt, dass die Kandidaten dafür - warum ein Token Dateiliste und
Inhalte digestiert, warum die Publish-Remote-Gate kein Flag hat - für eine handelnde Session
normativ verwertbar sind und nicht Hintergrund. Sie wegzukürzen hätte die Zelle verkleinert und
die Handlungsfähigkeit gesenkt. Die Zellen sind lang, weil die Verträge dicht sind.
PATCH, kein neues Boundary-Crossing: reine Dokumentstruktur, kein Format, keine Funktion, keine
Hand-Arbeit bei Update oder Downgrade. Eine private Instanz mit eigenem tools/CONTRACT.md
bekommt beim Merge Konflikte in den beiden Tabellen, weil deren Zeilen umsortiert wurden - das
ist ein Merge-Konflikt in einer stack-eigenen Datei, den upstream merge ohnehin zugunsten der
Upstream-Seite auflöst, kein Kompatibilitätsbruch.
Geändert: tools/CONTRACT.md (Lead-in, ###-Gruppen in beiden Tabellen, gespiegelte
Reihenfolge, TOC via docs toc --apply), AGENTS.md (Routing-Zelle für tools/),
tools/chemenu/tests/test_docs_verify.py (zwei Tests: Abschnitt läuft über eigene
Unterüberschriften hinweg, gruppierte Tabellen bleiben in beiden Richtungen geprüft).
Verifiziert: tools/wikitool docs verify, tools/wikitool instructions verify, volle
pytest-Suite (1195 passed). Typischer Zugriff: Median 229 statt 17.500 Tokens.
Schließt #92.
incoming/.gitkeep ist jetzt trackbar - Datei- statt Verzeichnismuster in .gitignore (#88).
/incoming/ schloss bisher das Verzeichnis selbst aus, und Regel 2 im .gitignore-Kopf sagt
genau, warum das eine Falle ist: git kann keine Datei wieder einschließen, deren Elternverzeichnis
ausgeschlossen ist, also blieb ein !incoming/.gitkeep wirkungslos. Ein frischer Klon hatte den
Eingang deshalb nie - instructions/bootstrap.md Schritt 2 (mkdir -p incoming) fing das für
einen Menschen ab, der die Instruction liest, aber nicht für einen Checkout, den eine Maschine
anlegt: der Korpus-Checkout des MCP-Read-Servers entsteht aus Klon plus fetch && reset --hard
und läuft durch keinen Bootstrap, also fehlte der Eingang dort still, bis das erste Kommando ihn
brauchte.
Umgestellt auf /incoming/* plus !/incoming/.gitkeep - jetzt ignoriert das Muster den Inhalt,
nicht das Verzeichnis, und die Negation greift tatsächlich. Gegengeprüft in beide Richtungen:
git check-ignore --no-index incoming/.gitkeep meldet die Datei als nicht ignoriert,
incoming/probe.pdf weiterhin als ignoriert - die Kanarie aus
docs_verify.REQUIRED_IGNORE_CANARIES hält also unverändert. incoming/.gitkeep steht jetzt
zusätzlich in REQUIRED_TRACKED_PATHS, damit eine künftige Rückkehr zur Verzeichnisform
docs verify laut fehlschlagen lässt statt still zu wirken. Ein voller dist export +
git init + git add -A-Replay bestätigt, dass incoming/.gitkeep im allerersten Commit einer
frischen Instanz landet, während incoming/probe.pdf dort weiterhin ignoriert bleibt.
instructions/bootstrap.md Schritt 2 entfällt ersatzlos - ein frischer Klon braucht ihn nicht
mehr -, die übrigen Schritte rücken nach. mcp-upload/ bleibt bewusst bei der Verzeichnisform
ohne .gitkeep: die Schreibprimitive des submit-Tools legt das Verzeichnis selbst an, sobald
sie gebraucht wird, und ein Checkout, der das Tool nie aktiviert, braucht das Verzeichnis auch
nie - anders als bei incoming/ gibt es dort keinen Frisch-Klon-Fall abzudecken. Der
.gitignore-Kommentar zu mcp-upload/ benennt diesen Kontrast jetzt ausdrücklich, statt sich
nur noch mit dem alten (identischen) Verhalten von incoming/ zu vergleichen.
Geändert: .gitignore (beide Kommentarblöcke, neues Muster), incoming/.gitkeep (neu),
tools/chemenu/commands/docs_verify.py (REQUIRED_TRACKED_PATHS), instructions/bootstrap.md
(Schritt 2 entfernt, Rest umnummeriert), raw/CONTRACT.md, tools/CONTRACT.md (dist export-
Zeile), README.md (Architekturdiagramm-Kommentar). Verifiziert: tools/wikitool docs verify,
tools/wikitool instructions verify, volle pytest-Suite (1195 passed), manueller
dist export-Replay. Schließt #88.
dist export-Doku nennt keine Typverzeichnisse mehr, die seit #67 nicht mehr angelegt werden
(#93). tools/CONTRACT.mds dist export-Zeile und der --help-Docstring des Kommandos
selbst behaupteten wortgleich, ein frischer Export lege leere
raw/{articles,documents,notes,assets}/ und die passenden incoming/-Typverzeichnisse an. #67
hat die Typverzeichnisse aus dem Adressierungsschema von raw accept entfernt (Datums-Shard
statt Typverzeichnis) und dabei den Export-Pfad auf zwei flache Anker verkürzt
(raw/.gitkeep, incoming/.gitkeep - run_export), ohne die zwei Prosa-Stellen nachzuziehen,
die noch die alte Struktur beschrieben. Nebenbefund aus der Sitzung zu #88.
Beide Stellen beschreiben jetzt den tatsächlichen flachen Export. Gegengeprüft per
tools/wikitool dist export in ein leeres Scratch-Verzeichnis: raw/ enthält nach dem Export
nur .gitkeep (plus die verzeichniseigene CONTRACT.md), incoming/ nur .gitkeep - keine
Typunterverzeichnisse unter keinem der beiden Wurzeln.
Außerhalb der Akzeptanzkriterien dieses Issues, aber derselbe Fund: dieselbe Prosa-Drift bei
raw/*/.gitkeep in der dist upgrade-Zeile (tools/CONTRACT.md), in
docs/ownership-and-templates.md und im apply_upgrade-Docstring - andere Fundstelle, gleiche
Ursache. In derselben Sitzung gleich mitkorrigiert, siehe die nächste Eintragszeile unten.
Geändert: tools/CONTRACT.md (dist export-Zeile), tools/chemenu/commands/dist_cmd.py
(--help-Docstring von export). Verifiziert: tools/wikitool docs verify,
tools/wikitool instructions verify, volle pytest-Suite, manueller dist export-Replay in
ein leeres Scratch-Verzeichnis. Schließt #93.
raw/*/.gitkeep-Glob war seit #67 ebenfalls stale - korrigiert auf den flachen
raw/.gitkeep-Anker. Nebenbefund beim Nachziehen von #93 in derselben Sitzung: dieselbe
Typverzeichnis-Drift steckte noch an drei weiteren Stellen, die von einem mehrfachen
raw/<typ>/.gitkeep sprachen, obwohl raw/ seit #67 flach ist und es nur noch den einen
raw/.gitkeep-Anker gibt - chemenu.ownership.is_export_stub/is_upgrade_preserved matchen
ohnehin per Dateiname statt Pfadtiefe, das Verhalten war also nie falsch, nur die illustrative
Glob-Schreibweise in der Prosa. Betroffen: tools/CONTRACT.mds dist upgrade-Zeile,
docs/ownership-and-templates.mds Aufzählung der seeded-once-Dateien, und der
apply_upgrade-Docstring in dist_cmd.py.
Geändert: tools/CONTRACT.md (dist upgrade-Zeile), docs/ownership-and-templates.md,
tools/chemenu/commands/dist_cmd.py (--help-Docstring von dist upgrade). Verifiziert:
tools/wikitool docs verify, tools/wikitool instructions verify, volle pytest-Suite.
Ingests haben jetzt zwei Größenachsen: Volumen und Breite (#61). ingest-large-tree löste
bislang ausschließlich auf Volumen aus - mehr als ~20 Rohdateien, mehr als ~15 raw_files:-
Einträge -, und sein § Scope sagte das auch so: „This is about volume, not difficulty."
Dazwischen lag eine Lücke: die einzelne, thematisch breite Quelle. Sie löst keinen
Volumen-Trigger aus, wiki-ingest kannte keinen anderen, und was sie anrichtet, meldet kein
lint-Befund.
Der Korpus zeigt den Fall genau einmal, dafür deutlich. Source - LLM Wiki v2: eine Rohdatei,
4 Entities + 26 Concepts, gegen einen Median von 6 über alle 29 Source-Seiten. Der naheliegende
Verdacht - die Source-Seite werde vage - trägt nicht: alle 26 Concepts haben eine eigene Seite,
die Breite wurde vollständig nachgezogen. Der Schaden sitzt eine Ebene tiefer und ist bimodal:
14 der 30 Subjektseiten liegen unter 150 Wörtern (die dünnsten bei 71 bis 78), die übrigen bei
541 bis 1.294, gegen einen Korpus-Median von 485. Eine Stub-Kohorte aus einem einzigen
Durchgang. Keine andere Quelle im Korpus hat eine: bei 18 Gegenständen (Source - LLM Wiki Pattern) sind es null, und darunter ebenfalls. Daher die Schwelle bei ~20 statt am
75.-Perzentil - ein Auslöser, der auf schadensfreien Seiten feuert, wird ignoriert.
Breite wird nicht geschnitten, und das ist der Teil, der eine eigene Sektion bekommt. Zwei
Regeln schließen den Weg: raw/ hält eine Datei „exactly as received", und eine Rohdatei hat
genau einen Besitzer (lint meldet einen zweiten Anspruchsteller). Mehrere thematische
Source-Seiten über einer Datei wären also nicht bloß unüblich, sondern ließen bei einer neuen
Edition niemanden zuständig zurück. Der Schnitt vor raw accept bleibt davon unberührt - er
ist das, was capture-session § 1 tut, und er ist nur möglich, weil das Transkript dort noch
gar nicht existiert. Diese Abgrenzung steht jetzt in § 1 selbst, weil genau dort der
Fehlschluss ansetzt, ein empfangenes Dokument ließe sich genauso zerlegen.
Was die Breite stattdessen auslöst, ist der Extract-Pass vor dem Schreiben und die Regel, die
er beliefert: ein Gegenstand bekommt eine Seite, wenn die Quelle Material für eine trägt.
Eine beiläufige Erwähnung bekommt einen Wikilink von der Source-Seite und eine Zeile unter
## Not Extracted, keine eigene Seite. Eine Seite, die nur ihren eigenen Titel wiederholt, ist
schlechter als die Erwähnung, aus der sie entstand: lint misst Struktur und nie Substanz, also
meldet sie niemand, und die nächste Sitzung liest sie als abgedecktes Terrain und schaut nicht
mehr in die Quelle.
Der vendorierte commonplace-Korpus stützt den Verzicht auf einen Source-Split unabhängig vom
Befund aus dem Baum, was ihn zum belastbareren Teil der Begründung macht: Atomizität ist dort
ausschließlich mit Co-Loading für Entdeckung begründet und gilt damit der Library-Schicht
(kb/entities/, kb/concepts/), nicht der Evidenz-Schicht; ein Mehr-Aussagen-Dokument ist per
title-as-claim Referenz statt Prämisse, weshalb Breite seiner Rolle nicht schadet; und die
Forderung, Forward-Lineage müsse den Betreiber unterbrechen, erreicht „eine Rohdatei, ein
Besitzer" auf eigenem Weg.
Nicht gebaut: ein lint-Befund gegen die Stub-Kohorte. Er wäre die Beobachtungsseite derselben
Sache, braucht aber eine eigene Schwellenwertdiskussion - Wortzahl ist ein grober Proxy für
Substanz - und liegt als eigenes Issue auf dem Board. Die Schwelle ~20 ruht auf einem einzigen
Schadenspunkt und ist entsprechend vorläufig.
MINOR, kein neues Boundary-Crossing: additiv in beide Richtungen. Eine bestehende Instanz
bekommt einen zusätzlichen Auslöser und eine zusätzliche Regel in Dateien, die der Stack
besitzt; nichts an kb/, keinem Schema, keinem Kommando und keinem Flag ändert sich, und ein
Downgrade nimmt beides ersatzlos zurück. Der Kandidat steht ohnehin auf MAJOR, der Bump
erhöht also nur seinen Zähler.
Geändert: instructions/ingest-large-tree.md (§ When to run auf zwei Achsen, Tier-Tabelle,
neue § A broad source is not cut, § Scope, ein Decision Point, description),
instructions/wiki-ingest/SKILL.md (Schritt 2 verweist jetzt auf die Schwellenliste statt sie
zu wiederholen, Schritt 7 trägt die Seiten-Regel, Schritt 8 und ein Decision Point ziehen nach),
instructions/capture-session.md (§ 1 Abgrenzung), types/source.md (## Not Extracted ist
jetzt auf beiden Achsen Pflicht; entities:+concepts: jenseits von ~20 als Gegenstück zum
bestehenden raw_files:-Signal). Verifiziert: tools/wikitool docs verify,
tools/wikitool instructions verify, volle pytest-Suite (1195 passed), docs toc --apply und
instructions sync für die generierten Regionen und die veröffentlichten Kopien.
Schließt #61.
stack-close Schritt 3 sagt jetzt, dass ein Doku-Nachzug seinen eigenen Bump braucht.
Direkt aus dem vorigen Paket gelernt: dessen Abschlussphase zog types/source.md nach, ohne
VERSION zu bewegen, und CI-Run 249 fiel an der Version Gate um. Die Sitzung las den Nachzug
als Dokumentation - types/source.md ist aber beides, Dokument und ausgelieferte
Verhaltensbeschreibung, und die Gate ist nach Pfad gefasst, nicht nach Absicht
(^(tools/|types/|instructions/|AGENTS\.md$|[^/]+/CONTRACT\.md$)).
Das ist kein Ausrutscher, sondern eine Reihenfolge, die sich wiederholt: stack-dev Schritt 4
bumpt die eigentliche Änderung, und Schritt 3 dieses Skills läuft danach - in genau den
Pfaden, die die Gate bewacht. Schritt 3 trägt den Satz deshalb jetzt selbst, samt der Falle
("nur Dokumentation") und des Auswegs (version bump --patch im selben Zug, was beim laufenden
Kandidaten ohnehin nur den Zähler bewegt). Nicht nach doc-pull-through.md gelegt, obwohl es
dort thematisch auch passte: die Entscheidung fällt in dem Schritt, der den Commit auslöst, und
zwei Kopien wären die Dopplung, die Invariante 8 verbietet.
Geändert: instructions/dev/stack-close/SKILL.md (Schritt 3). Verifiziert:
tools/wikitool docs verify, tools/wikitool instructions verify, volle pytest-Suite,
instructions sync für die veröffentlichte Kopie. Kein Issue - direkt korrigiert.
4.7.4 - 2026-09-04 - bootstrap.md nennt den session-id-WARN nach frischem Bootstrap explizit als erwartet
Author: Torben Nehmer
- bootstrap.md nennt den session-id-WARN nach frischem Bootstrap explizit als erwartet
Nach einem frischen Clone plus instructions/bootstrap.md zeigte tools/wikitool doctor
durchgehend OK, außer session-id: WARN - ohne Einordnung, ob das ein Bootstrap-Defekt ist.
WIKITOOL_SESSION_ID wird laut instructions/session-setup.md bewusst pro Arbeitssitzung
gesetzt, nicht pro Clone; ein Export in bootstrap.md selbst würde nur für den Bootstrap-Lauf
gelten, nicht für die tatsächliche Arbeitssitzung danach (die nach dem Neustart in Schritt 6 in
einer neuen Shell beginnt). bootstrap.md bekommt deshalb einen neuen Schritt 7, der den WARN
als erwarteten Zustand benennt - analog zum bereits dokumentierten personalization: FAIL in
Schritt 4 - und auf session-setup.md verweist, statt den Export in Bootstrap nachzubauen.
Schließt #54.
4.7.3 - 2026-09-04 - eval: gate-not-self-opened prueft REMOVED_FLAGS gegen das eigene Kommando
Author: Torben Nehmer
Die Trajektorien-Regel gate-not-self-opened (tools/chemenu/evals/trajectory.py) hat jedes
Argument jedes wikitool.call gegen REMOVED_FLAGS = {"--yes": "publish", "-y": "publish"}
geprüft, ohne je das eigene command-Feld des Aufrufs gegenzulesen. --yes/-y sind nur auf
publish entfernt worden - auf rm --page <Titel> --yes sind sie ein gültiger, dokumentierter
Flag. Ergebnis: jeder rm --yes-Aufruf wurde als Invarianten-Verstoß gemeldet ("an agent
inventing a flag the tool never accepts"), obwohl das Tool ihn akzeptiert hatte.
In den vorhandenen Telemetrie-Traces unter reports/telemetry/ betraf das 111 rm-Aufrufe
über 8 Sessions, davon 27 allein in publish-cleanup/u3 - jede davon fälschlich FAILED
gescort. Kein bestehender Test hätte das gefangen: tools/chemenu/tests/test_evals.py prüfte
REMOVED_FLAGS ausschließlich über publish --yes, nie über ein anderes Kommando.
Fix: die Bedingung liest jetzt attrs.get("command") == REMOVED_FLAGS[arg] mit. Neuer
Regressionstest test_yes_on_a_command_that_still_has_it_is_not_a_finding deckt genau den
rm --yes-Fall ab und schlägt gegen den unfixed Code nachweislich fehl.
Keine Verhaltensänderung an wikitool selbst - ausschließlich an der Scoring-Logik unter
tools/chemenu/evals/.
- eval: gate-not-self-opened prueft REMOVED_FLAGS gegen das eigene Kommando
4.7.2 - 2026-09-04 - Coverage-Untergrenze bei 85 %, gegen beobachtete 87,0 %
Author: Torben Nehmer
- Coverage-Untergrenze 85 % in tools/.coveragerc
Die Suite hat jetzt einen Boden: fail_under = 85 in tools/.coveragerc, gemessen gegen 87,0 %
(CI-Lauf 163, 6498 Statements, 975 Tests). Damit ist Gitea #10 geschlossen — das Issue, das die
Messung eingerichtet und die Schwelle danach absichtlich zurückgehalten hat, bis die Zahl
beobachtet war.
Die Beobachtung ist der eigentliche Inhalt dieses Bumps. Zwischen der ersten Messung (86,9 % von 5105 Statements, 730 Tests, Lauf 87, Stack 1.8.1) und heute ist der gemessene Code um ein Viertel gewachsen und die Suite um ein Drittel, über 38 grüne Läufe — und die Quote hat sich um einen Zehntelpunkt bewegt. Eine Untergrenze, die auf dieser Beobachtung steht, ist etwas anderes als eine gegriffene Zahl.
85 und nicht 87, und das ist keine Bequemlichkeit. Der Coverage-Bericht unterscheidet drei
Sorten ungedeckter Zeilen, und nur eine davon bedeutet Arbeit (EVALS.md § „How much of the
stack the suite reaches"). Ein neuer dünner Typer-Wrapper senkt den Gesamtwert, ohne dass
irgendetwas schlechter geworden wäre — seine Logik liegt daneben und ist dort getestet. Eine
Schwelle auf dem gemessenen Wert würde genau an diesem Commit rot, und eine Schwelle, die aus
einem Nicht-Grund rot wird, wird gesenkt statt verdient. Das ist die Fehlerweise, die #10
verhindern wollte, nur von der anderen Seite. Die zwei Punkte sind der Platz, den die Taxonomie
verlangt.
fail_under steht in der Konfiguration und nicht als --cov-fail-under im CI-Schritt: so sitzt
die Zahl neben der Begründung, die sie erzeugt hat, und gilt für jeden --cov-Lauf statt nur für
den einen, den CI schreibt.
Was der Boden nicht tut: die drei echten Lücken schließen (provenance_cmd.py 44 %,
migrate_cmd.py 65 %, type_resolver.py 79 %). Er friert den erreichten Stand ein. Diese Liste
ist die einzige, die sich nicht bewegt hat, während alles um sie herum wuchs — migrate_cmd.py
ist sogar von 71 % gefallen, weil das Modul gewachsen ist und die neuen Zeilen ungetestet ankamen.
Das ist Gitea #51.
Mitgenommen, weil es dieselbe Frage beantwortet: der Coverage-Bericht ist als Artefakt
abrufbar, über die Run-Seite. Die Actions-Artefakt-Endpunkte melden dafür total_count: 0, weil
upload-artifact@v3 über die ältere Artifact-API ablegt, die diese Endpunkte nicht lesen. Eine
leere Liste ist kein fehlgeschlagener Upload — steht jetzt in EVALS.md und im Kommentar an der
Coverage report-Stufe, damit die naheliegende „Korrektur" auf v4 (hier eingeschränkt) niemandem
mehr einfällt.
4.7.1 - 2026-09-04 - redundant_see_also in tools/CONTRACT.md und wiki-lint dokumentiert; xref-remove-Falle benannt
Author: Torben Nehmer
- redundant_see_also in tools/CONTRACT.md und wiki-lint; xref-remove-Falle beim Aufräumen benannt
Die Doku-Hälfte von 4.7.0, beim Abschluss nachgezogen. Der Befund war ausgeliefert, aber
tools/CONTRACT.mds lint-Zeile zählt die Befunde auf und kannte ihn nicht - eine Instanz hätte
eine Sektion im Report gefunden, die ihr Contract nicht erklärt. docs verify prüft, dass die
Kommandotabelle existiert, nie was in einer Zeile steht; genau die Lücke, für die AGENTS.md
„a stack change is not finished until the human docs describe it" geschrieben ist.
Die eigentliche Änderung ist aber die Warnung in wiki-lint, und sie ist keine Prosa-Politur.
Der neue Befund liest sich wie etwas, das Schritt 7 („repariere, was mechanisch ist") abräumt, und
der naheliegende Griff wäre xref remove - das die Referenz beidseitig löscht. Angewandt auf
Wine see-also Wine GE neben Wine GE depends-on Wine hätte das die schwache und die
spezifische Kante entfernt, und das Paar sagte danach gar nichts mehr. Ein Befund, dessen
offensichtliche Reparatur Daten zerstört, ist schlechter als kein Befund: Schritt 1 nennt die
Falle jetzt beim Namen und verweist auf xref add (fasst nur die Quellseite an) oder aufs
Berichten. Dieselbe Asymmetrie hat in #30 schon einmal Daten gekostet.
4.7.0 - 2026-09-04 - Link-Katalog: authored, alternative-to, addresses; entity→entity-Lineage; Lint-Befund gegen redundante see-also
Author: Torben Nehmer
- Link-Katalog: authored, alternative-to, addresses; Lint-Befund gegen redundante see-also
Drei neue Label, zwei geschlossene Autorisierungslücken und ein Lint-Befund - ausgelöst von der
anstehenden ersten produktiven Instanz. Katalog und COLLECTION.md gehen über dist export in
jede neue Instanz; was hier fehlt, fehlt dort ab Tag eins, und nachträglich ist eine
Katalogerweiterung eine Korpus-Migration statt einer Datenzeile. Gitea #43 und #49.
Die Messung, die den Zuschnitt bestimmt hat. Die 180 see-also-Kanten des Korpus zerfallen
exakt in drei Klassen: 57 (32 %) sind Spiegel einer bereits typisierten Gegenkante, 70 (39 %)
sind wechselseitige see-also-Paare, 53 (29 %) stehen einseitig. Die erste Klasse ist kein
Vokabularproblem - Wine see-also Wine GE steht neben Wine GE depends-on Wine, RAG see-also NotebookLM neben NotebookLM implements RAG. Der Katalog war für ein Drittel der Fälle längst
ausreichend; es hat sie nur nichts gemeldet. Genau dafür ist der Lint-Befund unten da, und er ist
der Grund, warum diese Version mehr ist als zwei Katalogzeilen.
authored (operationales Register). „hat das Ziel als einmaligen Akt geschaffen." Der Katalog
kannte fortlaufende Rechenschaft (owns) und fortlaufende Arbeit (maintains), aber nicht den
historischen Ursprung - Urheberschaft stand im Korpus deshalb in vier unvereinbaren Formen
nebeneinander: source.author als Freitext, owns, see-also und ein Prosa-Bullet. Eine davon
war sachlich falsch: Vannevar Bush owns Memex behauptet laufende Rechenschaft für einen 1974
Verstorbenen, und eine falsche maschinenlesbare Kante ist schlechter als eine schwache, weil sie
geglaubt wird. Geschrieben wird das Label auf der Entity-Seite (Andrej Karpathy authored LLM Wiki Pattern) - die Gegenrichtung authored-by auf der Concept-Seite hätte xref remove
gebraucht, das beidseitig abräumt, statt xref add, das relabelt. Vier Kanten im Korpus
umgestellt, keine verloren.
alternative-to (operationales Register, selbst-dual). „erfüllt denselben Zweck wie das Ziel,
so dass ein Leser, der zwischen beiden wählt, beide will." Belegt durch rund 30 Paare, darunter
die sieben Agent-CLIs, die untereinander ausschließlich see-also tragen - keine einzige
typisierte Kante. Abgegrenzt gegen contrasts (behauptet einen lesenswerten Unterschied) und
compares-with (wiegt auf benannten Dimensionen ab und führt in dieser Instanz auf eine
kb/comparisons/-Seite): zwei Agent-CLIs sind austauschbar, zwei gegensätzliche Entwurfsprinzipien
sind es nicht.
Der Katalog sagt jetzt ausdrücklich, dass ein selbst-duales Label einmal pro Paar geschrieben
wird. Ohne diesen Satz wäre aus einer 22-Kanten-see-also-Clique eine 22-Kanten-
alternative-to-Clique geworden und nichts gewonnen: sieben austauschbare Werkzeuge sind 21
Paare, beidseitig deklariert 42 Kanten, von denen die zweiten 21 nichts sagen.
addresses (konzeptionelles Register). „ist eine Antwort auf das Problem, das das Ziel
beschreibt." types/concept.md deklariert problem und decision als eigene Subtypen, und der
Katalog hatte kein Label, das eine Entscheidung mit dem Problem verbindet, das sie löst - eine
Collection konnte ein Problem benennen und nie sagen, was dagegen unternommen wurde. Abgegrenzt
gegen rests-on, das das Ziel als Prämisse nimmt statt als zu lösendes Problem. Im Korpus nur
vier belegte Paare, also dünn nach dem sonst geltenden „erst der Anwendungsfall"-Maßstab; die
Ausnahme ist bewusst und gilt dem Auslieferungszeitpunkt, der die Kosten umdreht.
Zwei Autorisierungslücken entity→entity. kb/entities/COLLECTION.md erlaubte bisher keine
Lineage zwischen zwei Entities - ein Fork, eine Neuimplementierung, ein Nachbau war nicht
ausdrückbar; derived-from und adapted-from sind jetzt freigegeben. Ebenso implements, für
eine Entity, die eine als Entity geführte Konvention umsetzt. Ein autorisiertes Label ohne
Live-Nutzung ist ausdrücklich in Ordnung (instructions/dev/corpus-policy.md).
Lint-Befund redundant_see_also. Meldet eine see-also-Kante, deren Gegenrichtung bereits
ein typisiertes Label trägt. Gegen den Korpus dieser Instanz meldet er genau die gemessenen 57.
Advisory, nicht hart, aus zwei Gründen zugleich: eine schwache Kante neben einer spezifischen
ist redundant, nicht kaputt - und der Befund kommt lange nach den Korpora, die er beurteilt, also
würde eine harte Einstufung jede bestehende Instanz mit dem Upgrade rot schalten, das ihn
ausliefert. Anders als unlabelled_edges ist er auch nicht migrations-gegatet: es gibt keine
Version, ab der die Redundanz zum Fehler wird, nur einen Sweep, zu dem jemand kommt oder nicht.
links.SEE_ALSO ist damit das einzige Katalog-Label, das das Werkzeug beim Namen kennt. Das ist
eine begründete Ausnahme, keine Aufweichung: see-also ist der erklärte letzte Ausweg des
Katalogs und behauptet nur, dass nichts Besseres passte - was der einzige Grund ist, warum lint
eine Kante als schwächer als eine andere über demselben Paar beurteilen kann. Alles andere am
Vokabular bleibt in instructions/link-taxonomy.md und den outbound:-Blöcken.
Nicht dabei, bewusst. Der Sweep der 180 bestehenden Kanten (#48) - diese Version ändert außer
den vier Urheberschaftskanten keine Korpus-Kante. Verworfen wurden außerdem variant-of (die
Wine-Forks tragen bereits depends-on Wine), implemented-by (Spiegel von implements, den die
Inbound-View rendert), sibling-of für die Concept-Cliquen (Über-Verlinkung, kein fehlendes Wort)
und builds-on (Vokabularkollision mit extends/derived-from/adapted-from).
4.6.1 - 2026-09-04 - DEVELOPMENT.md im Kommandotabellen-Check, selbstbeschriftete Release-Notes, Prosa-Korrekturen (#47 Block 3)
Author: Torben Nehmer
- DEVELOPMENT.md im Kommandotabellen-Check, selbstbeschriftete Release-Notes, Prosa-Korrekturen (#47 Block 3)
Block 3 aus #47 - die beiden Nebenbefunde entschieden und umgesetzt - plus drei Prosa-Korrekturen
an 4.6.0, die eine Bewertung des eigenen Ergebnisses gefunden hat.
DEVELOPMENT.md gehört in docs_verify.STAGE_READMES (entschieden: ja). Das Gegenargument
bei der Aufnahme war, die Liste führe bisher nur ausgelieferte Dokumente, und DEVELOPMENT.md
wird von dist_cmd.ROOT_FILES bewusst nicht ausgeliefert. Beim Hinsehen löst es sich auf:
check_readmes_have_no_command_table überspringt eine Datei, die nicht existiert. In einer
ausgelieferten Instanz ist der Eintrag damit schlicht wirkungslos, im Entwicklungs-Checkout - dem
einzigen Ort, an dem die Datei existiert und also driften kann - greift er. Dafür spricht der
Anlass: genau diese Datei trug einmal eine Tabelle, die für jeden Verify-Befehl ein zweites Mal
beschrieb, was er prüft, und sie musste von Hand entfernt werden, weil nichts sie mit etwas
verglich. Zwei Tests: einer, der die Tabelle in DEVELOPMENT.md meldet, und einer, der
festhält, dass eine fehlende gelistete Datei übersprungen und nicht als Fund gemeldet wird - der
Instanz-Fall, an dem die Entscheidung hing. Der Konstantenname ist jetzt enger als sein Inhalt;
das steht als Kommentar daneben, statt eine Umbenennung durch zwei Aufrufstellen zu ziehen.
Veröffentlichte Release-Notes veralten weiter - sie sagen es jetzt selbst (entschieden:
Schnappschuss akzeptieren, statt einen Korrekturweg zu bauen). Eine nach dem Tag korrigierte
CHANGES.md erreicht die Release-Seite nicht: gitea-mcp kennt kein Release-Edit, und
Löschen-und-neu-Anlegen würde die angehängten Tarball-Assets vernichten, auf die INSTALL.md und
version check zeigen. Bei v4.4.0 ist das real eingetreten. Statt eines Korrekturwegs für einen
Text, den niemand editieren kann, trägt der Schnappschuss jetzt eine Fußzeile, die sagt, dass er
einer ist und wo die gepflegte Fassung liegt - eine veraltete Notiz kostet einen Leser damit einen
Klick statt einer falschen Überzeugung. Angehängt in release.yml und nicht in version notes:
das Kommando ist ein allgemeiner Extraktor, dessen andere Aufrufer (lokale Vorschau, eine Pipe)
keine Release-Seiten-Fußzeile erben sollen. .gitea/-Änderung, also ohne eigenen Bump-Anspruch -
sie fährt hier mit.
Drei Prosa-Korrekturen an 4.6.0. Der 4.6.0-Eintrag und der Docstring von
touches_stack_machinery behaupteten, das Prädikat prüfe „denselben Pfad-Umfang, den der
CI-Versions-Gate selbst verwendet". Das stimmt nicht: CI matcht [^/]+/CONTRACT\.md$, also genau
eine Pfadebene, das Prädikat matcht CONTRACT.md in jeder Tiefe. Folgenlos im Verhalten - ein
Über-Match druckt eine Zeile zu viel, nie eine zu wenig -, aber es war eine behauptete Äquivalenz,
die keine ist, geschrieben in genau der ungeprüften Prosa-Phase, um die #47 sich dreht. Docstring
und tools/CONTRACT.md benennen die Differenz jetzt und begründen sie (bei einer Erinnerung ist
Über-Matchen die richtige Richtung). Drittens: stack-close beschrieb den eigenen Skill-Schnitt
zu stark („es gibt keinen nächsten Schritt mehr, an dem vorbei zu rutschen wäre"). Wahr für die
Prozedur, die nicht mehr im Kontext steht; nicht wahr für den Auslöser - stack-devs „invoke
it now" ist weiterhin ein Satz, und die publish-Notiz nennt den Skill bewusst nicht beim Namen.
Zwei der drei Kettenglieder bleiben Selbstdisziplin. Der Skill sagt das jetzt selbst, statt sich
als Garantie zu verkaufen, die er nicht ist.
Verifiziert: tools/wikitool docs verify, tools/wikitool instructions verify,
.venv/bin/python -m pytest -q (969 passed, 2 davon neu), release.yml gegen den YAML-Parser
und das Heredoc als Trockenlauf gegen eine Beispiel-Notiz.
4.6.0 - 2026-09-04 - stack-dev/stack-close skill split, publish stack-machinery note, model-selection fix (#47 Block 2)
Author: Torben Nehmer
- stack-dev/stack-close skill split, publish stack-machinery note, model-selection fix (#47 Block 2)
Block 2 aus #47 (Vorschlag E, am 2026-09-04 entschieden): die ungeprüfte Schlussphase einer
Stack-Sitzung - Issue-Body-Rewrite, docs/-Veralterung, Changelog-Prosa - hatte bisher keinen
eigenen Haltepunkt, sondern einen Prosa-Break in stack-dev Schritt 6. Der ist zweimal
hintereinander verschluckt worden (#42, #30), beide Male mit echtem Fund im nachgeholten
Durchgang. Ein dritter Prosa-Haltepunkt hätte dieselbe Wette verloren, die
docs/why-gates-are-code.md für Gates schon verliert - also keine Prosa-Lösung mehr, sondern ein
struktureller Schnitt.
Neuer Skill stack-close, dev-only wie stack-dev. stack-dev endet nach tools/wikitool publish mit einem Stop statt mit einem sechsten Schritt; die Schlussphase existiert nur noch als
eigener Skill, den eine Sitzung aufrufen muss - es gibt keinen „nächsten Schritt" mehr, an dem
vorbei sie rutschen könnte. stack-close trägt drei Dinge: den Modell-Rückwechsel-Hinweis (wie
zuvor), die Body-Rewrite-Disziplin aus issue-tracking.md Schritte 2-3 und 7, und neu die
Handover-Pflicht über die ganze Sitzung: benannt wird das Modell für Design/Versionsstelle
(Schritt 3), für die mechanische Mitte, und für diese Schlussphase - alle drei, auch wenn sie
identisch sind. Eine Handover-Zeile, die nur eine billige Schlussphase meldet, schweigt genau
dann, wenn die ebenso ungeprüfte Design-Phase auch billig lief und niemand dort gewechselt hat.
Ein Agenten-Zuschnitt (Schlussphase als eigener Subagent mit eigenem Modell) wurde geprüft und verworfen: ein Fork erbt in Claude Code zwingend das Elternmodell, ein frischer Subagent den Sitzungskontext nicht - die Kombination, die der Zuschnitt bräuchte, gibt es nicht, und selbst wenn: der Input der Schlussphase ist das akkumulierte Sitzungswissen, das ein kalter Agent aus Diff und Issue neu ableiten müsste. Volle Begründung im Body von #47.
instructions/claude-code-model-selection.md korrigiert, im dist-strip-Block: die
Übersicht „stack-dev Schritt 3 und 6" ist falsch geworden, seit Schritt 6 nicht mehr existiert.
Sie benennt jetzt beide Haltepunkte an ihrem tatsächlichen Ort - Schritt 3 in stack-dev,
der zweite am Anfang von stack-close.
stack-dev Schritt 3 ehrlicher formuliert (Vorschlag C): nicht mehr „ab hier alles
mechanisch", sondern mit benannter Ausnahme - Changelog-Prosa (Schritt 4), eine berührte
docs/-Seite, neue Menschendoku, der Prosa-Anteil einer Instruction. Dazu die Einschränkung aus
#30: „durch Tests abgedeckt" gilt nur für das, was die Tests treffen - zwei
datenvernichtende Bugs in upstream merge liefen an einem grünen pytest/docs verify/instructions verify/CI vorbei, weil kein Test den Fall traf, nicht weil ein
schwächeres Modell schlechteren Code für den getesteten Fall geschrieben hätte.
Neu: tools/wikitool publish selbst erinnert an die Phasengrenze. Berührt das Changeset
tools/, types/, instructions/, AGENTS.md oder ein <stage>/CONTRACT.md - derselbe
Umfang, den ein Versions-Bump selbst abdeckt -, druckt publish nach der Erfolgsmeldung eine
Zeile, dass die folgende Phase von keinem der drei Checks abgedeckt ist. Kein Gate, keine
Änderung am Exit-Code, für eine gewöhnliche Content-Publish stumm; harness- und
instanzneutral formuliert, ohne jede Erwähnung eines Trackers, weil publish von jedem
Skill genutzt wird, nicht nur von stack-dev. git_publish.touches_stack_machinery() plus
vier neue Tests (test_git_publish.py): zwei für die reine Klassifikationsfunktion
(positiv/negativ), zwei Integrationstests gegen einen echten Publish - die Notiz erscheint genau
einmal bei einer instructions/-Änderung und bleibt aus bei einer gewöhnlichen kb/-Änderung.
tools/CONTRACT.mds publish-Zeile trägt die Kurzfassung, absichtlich ohne den Dateinamen
version-parts.md zu nennen - die Datei liegt unter instructions/dev/ und würde in einer
ausgelieferten Instanz ins Leere zeigen, während tools/CONTRACT.md selbst ausgeliefert wird.
Verifiziert: tools/wikitool instructions sync (7 Skills, stack-close neu), tools/wikitool docs verify, tools/wikitool instructions verify, .venv/bin/python -m pytest -q (967
passed, 4 davon neu).
#47 bleibt offen für Block 3 (DEVELOPMENT.md in STAGE_READMES, veraltete Release-Notes).
4.5.1 - 2026-09-04 - issue-tracking - destructive-step invariants, comment-vs-body authority, rename sweep
Author: Torben Nehmer
- issue-tracking - destructive-step invariants, comment-vs-body authority, rename sweep
Block 1 aus #47 (gemeinsam mit #29): drei Ergänzungen an
instructions/dev/issue-tracking.md, ausgelöst durch zwei Fehlerklassen, die
in derselben Sitzung am Stack aufgetreten waren.
- Schritt 1 trägt jetzt, dass destruktive Schritte im Body die Invariante
nennen müssen, die sie nicht verletzen dürfen, und dass ein
Akzeptanzkriterium eine prüfbare Eigenschaft ist, keine Tätigkeit. Auslöser
war #30: der Body schrieb wörtlich "Arbeitsverzeichnis entfernen" für den
upstream merge-Ablauf, und genau das wurde zum datenvernichtenden Bug - einshutil.rmtreeauf eine Stage mit gitignorierten, nicht rekonstruierbaren Daten. - Schritt 2 trägt jetzt die Lesesicht auf Body und Kommentare, die es bisher nur aus Autorensicht gab: der Body ist der Stand, Kommentare sind Historie; ein erkennbar veralteter Body wird richtiggestellt statt umgangen; widersprüchliche Kommentare werden nach Beleg aufgelöst, nicht nach Datum. Auslöser war ebenfalls #30 (ein Kommentar empfahl das Gegenteil dessen, was der Body später festlegte) und #10 (ein seit Tagen veralteter Body gegen drei widersprechende Kommentare, zwei davon sich selbst widersprechend).
- Neuer Abschnitt "Renames and other decay in the tracker" nach Schritt 7:
ein Rename ist erst fertig, wenn auch die offenen Issues nachgezogen sind,
weil
wikitooldiesen Tracker nicht kennt und nicht kennenlernen soll. Mit der Wegweiser-vs-Beleg-Unterscheidung aus #29 und dem Hinweis, dass auch verschwundenekb/-Seiten und private Infrastrukturangaben Issue-Texte altern lassen. Ein neuer Trigger in "When to run" verweist darauf.
Keine der drei Ergänzungen verschiebt die bestehende Nummerierung der
Schritte 1-7 - die Querverweise darauf (u. a. aus
instructions/dev/stack-dev/SKILL.md auf Schritt 7, aus
kb/concepts/Issue Label Scheme.md auf Schritt 2) bleiben also gültig, ohne
angefasst zu werden.
Verifiziert: tools/wikitool instructions verify, tools/wikitool docs verify, beide grün (Prosa-only, kein Interface geändert, PATCH).
#47 bleibt offen (Block 2: der Skill-Schnitt aus Vorschlag E; Block 3: die beiden Nebenbefunde). #29 bleibt ebenfalls offen: dieser Block deckte nur den Regelabsatz, nicht den noch ausstehenden Pfad-Durchgang durch #4, #5, #15, #21, #16, #26 - der war nicht Teil des Auftrags für diesen Block.
4.5.0 - 2026-09-04 - Beide Update-Wege in Code: upstream merge fuer Clones, dist upgrade fuer Tarball-Instanzen
Author: Torben Nehmer
- Faktenkorrekturen in der 4.4.0-Prosa; DEVELOPMENT.md ohne zweite Kommandobeschreibung
- wikitool upstream merge/verify: code procedure for taking a stack update, ownership.py as the shared stack/instance boundary
- upstream merge: combined-commit regression test (edit+add+delete+contract+template+contract-delete in one commit)
- upstream merge: keep gitignored local data under a content stage, refuse a merge git never opened, report what actually changed
- dist upgrade: apply a stack update, not just detect one (#7)
- dist upgrade: Stamp-Semantik nach --keep-local benannt, docs/ownership-and-templates.md auf drei Eigentumsklassen nachgezogen
Die Prosa zu 4.4.0 - Changelog-Eintrag, docs/version-model.md, instructions/dev/version-parts.md,
DEVELOPMENT.md - entstand ungeprüft: kein maschineller Check liest sie, und die Sitzung, die sie
schrieb, hat den dafür vorgesehenen Modellwechsel aus stack-dev Schritt 6 übersprungen. Ein
nachgeholter Durchgang hat drei Fehler gefunden, von denen der erste der teuerste war.
Der Befund war an einer Stelle schlicht falsch. Changelog und docs/version-model.md
behaupteten, unter dem alten Modell seien Nummern vergeben worden, die „nie ausgeliefert" wurden -
im Changelog sogar mit einer erfundenen Zahl („fünf Minor-Bumps ... vier nie ausgeliefert"). Das
Gegenteil stimmt: weil release.yml auf jede VERSION-Bewegung feuerte, wurde jeder Bump zu
einem echten, getaggten Release. Der 2026-09-03 hat vier davon in sechs Stunden erzeugt (4.3.0
bis 4.3.3), zwei für reine Prosa-Änderungen. Der Schaden war nie ein Phantom-Release, sondern
dass „Release" aufhörte, etwas zu bedeuten - vier Upgrades an einem Nachmittag sind für einen
Konsumenten kein kleineres Versprechen, sondern ein unleserliches. Beide Stellen tragen jetzt den
tatsächlichen Vorgang samt Beleg.
Dazu zwei kleinere Korrekturen: version-parts.md nannte den Stack weiterhin 2.x (er steht bei
4.x), und DEVELOPMENT.md trug eine Tabelle, die für jeden Verify-Befehl ein zweites Mal
beschrieb, was er prüft - eine Kopie dessen, was tools/CONTRACT.md hält und docs verify dort
gegen die CLI prüft. Die Tabelle ist raus; dass die Datei selbst außerhalb der von diesem Check
abgedeckten Dokumente liegt, steht jetzt an ihrer Stelle. Der Code-seitige Teil davon - ob
DEVELOPMENT.md in docs_verify.STAGE_READMES gehört - hängt an #47, zusammen mit der Lücke im
Skill, die den übersprungenen Modellwechsel überhaupt erst unauffällig gemacht hat.
Kein Verhalten geändert, nur Prosa - und bewusst als laufender Kandidat gelassen statt fixiert:
CIs Version-Gate verlangt die VERSION-Bewegung, ein Release verlangt sie nicht. Genau dafür gibt
es das Modell aus 4.4.0.
Zweiter Bump auf demselben Kandidaten (#30): git merge upstream/main behandelt einen
bewegten Korpus asymmetrisch - eine gelöschte, upstream-geänderte Seite meldet sich als Konflikt,
eine neu angelegte Seite wird still gestaged, nur eine beidseitig gelöschte Seite ist harmlos. Die
Prosa-Prozedur in private-instance.md § "Taking a stack update" hat das geschlossen, aber mit
vier eigenen Fehlern: der Pfadsatz stand dreifach (im Skript, im Kontroll-Grep, implizit in
dist_cmd.py); eine vom Upstream gelöschte Maschinerie-Datei wurde von git checkout MERGE_HEAD -- <pfad> still ignoriert, weil das Skript kein set -e hatte; ein echter Konflikt in
tools//types//instructions/ endete in einem von der Prosa nirgends erwähnten offenen Merge;
und ein neuer Maschinerie-Pfad unter einer Content-Stage hätte die Literal-Liste nie erreicht.
Die Eigentumsgrenze ist jetzt ein Prädikat statt einer Liste: chemenu/ownership.py,
is_stack_owned(relative), wahr für <stage>/CONTRACT.md direkt unter einer Content-Stage
(kb, raw, work, reports) und für jeden Pfad, der dort auf .template endet - nicht
rekursiv, kb/<collection>/COLLECTION.md bleibt seit #39 instanzeigen. dist_cmd.py liest das
Modul jetzt statt einer eigenen _CONTENT_ALLOWED_NAMES-Liste zu pflegen, und
CONTRACT_ONLY_STAGES leitet sich aus ownership.CONTENT_STAGES ab statt die drei Stage-Pfade
ein zweites Mal aufzuschreiben - ein Test hält fest, dass beide Sichten nicht auseinanderlaufen
können.
Neu: wikitool upstream merge [--remote upstream] [--branch main] [--no-fetch] und wikitool upstream verify --since <rev> [--until HEAD]. merge prüft Vorbedingungen (sauberer Baum, kein
laufendes Merge, Remote löst auf), warnt statt zu blockieren, wenn .wikitool-remotes.json fehlt,
hält den Merge offen (--no-commit --no-ff), zwingt jede Content-Stage auf die lokale Seite
zurück, holt dann über die Vereinigungsmenge der Bäume von MERGE_HEAD und HEAD genau die
stack-eigenen Pfade zurück - inklusive einer Löschung, falls der Upstream einen Maschinerie-Pfad
entfernt hat -, verweigert bei verbliebenen unaufgelösten Pfaden ohne zu committen, committet
sonst und verifiziert den entstandenen Commit mit derselben Logik wie verify - ein Fund dort
wird laut gemeldet und nicht automatisch zurückgerollt. Nicht idempotent (AGENTS.md § Tool
error contract), nicht budget-exempt; verify liest nur und ist wie migrate verify von der
Budget-Gate ausgenommen. Die Mass-Update-Gate greift bei einem Merge-Commit strukturell nicht -
das steht jetzt als eigener Absatz in instructions/gates.md, mit upstream merges eigener
Nachkontrolle als der Sicherung, die hier tatsächlich trägt.
private-instance.md § "Taking a stack update" verweist jetzt auf den Befehl statt das Skript
auszuschreiben; die Pfadtabelle bleibt als Erklärung stehen, ist aber nicht mehr die operative
Liste. Vorschlag B (eigenes Demo-Repo) bleibt zurückgestellt, siehe #30.
23 neue Tests unter test_upstream_cmd.py (35 Fälle mit der parametrisierten
is_stack_owned-Tabelle) decken die Fälle aus der Spezifikation ab:
gelöschte vs. geänderte vs. neu angelegte Seiten, kb/CONTRACT.md- und Template-Änderungen,
kb/entities/COLLECTION.md bleibt lokal, eine gelöschte raw/CONTRACT.md landet, ein neuer
Template-Pfad landet, ein offener work/-Lauf landet nicht, ein echter tools/-Konflikt lässt
den Merge offen, ein schmutziger Baum wird unberührt abgewiesen, "bereits aktuell" ist ein No-op,
die Publish-Remote-Gate-Warnung, verify gegen einen von Hand verpfuschten Merge, und die
dist_cmd/ownership-Konsistenz.
Dritter Bump auf demselben Kandidaten: die im Akzeptanzkriterium geforderte Kombinationsprobe
fehlte noch - ein einzelner Upstream-Commit, der Editieren, Anlegen, Löschen einer Seite, eine
Contract-Änderung, eine Template-Änderung und eine Contract-Löschung gleichzeitig bewegt. Jetzt
als test_one_upstream_commit_mixing_every_case_at_once nachgetragen; alle sechs Erwartungen in
einem upstream merge-Aufruf verifiziert.
Vierter Bump: zwei Fehler, die ein Review-Durchgang nach dem Publish gefunden hat. Beide waren in der ersten Fassung enthalten, beide hätten Daten vernichtet, und keiner der bestehenden Tests hat sie berührt.
Erstens: die Content-Stage wurde als Ganzes gelöscht. _restore_stage_to_local hieß in der
ersten Fassung shutil.rmtree(stage_dir) — die wörtliche Übersetzung des rm -rf kb raw aus der
Prosa-Prozedur. Für kb/ und raw/ ist das harmlos, weil dort nichts Ignoriertes liegt. Für die
beiden Stages, die dieses Issue neu in den Satz aufgenommen hat, ist es das nicht: reports/
ist bis auf seinen Contract komplett gitignored und trägt genau die Daten, die nirgends sonst
existieren — die Telemetrie-Traces, aus denen eval score liest, gespeicherte Eval-Berichte,
alte Lint-Reports. In dieser Instanz standen zum Zeitpunkt des Fundes 497 Trace-Verzeichnisse
unter reports/telemetry/; ein einziger upstream merge hätte sie alle gelöscht, und zwar
stillschweigend, weil git von ignorierten Dateien nichts meldet. Die Stage wird jetzt über die
getrackten Pfade beider Bäume zurückgesetzt statt über das Verzeichnis; ignorierte lokale
Daten bleiben unberührt. Leergewordene Verzeichnisse werden aufgeräumt, aber nur wirklich leere.
Zweitens: ein Merge, den git nie eröffnet hat, hätte die Maschinerie gelöscht. Der Exit-Code
von git merge --no-commit --no-ff wird bewusst ignoriert (Konflikte unter den Content-Stages
sind erwartet). Nur: wenn git das Merge gar nicht erst eröffnet — unverwandte Historien, eine
ignorierte Datei im Weg —, gibt es kein MERGE_HEAD, _tree_paths("MERGE_HEAD") liefert die
leere Menge, und jeder stack-eigene Pfad in HEAD fällt damit in den Zweig „der Upstream hat
ihn gelöscht": kb/CONTRACT.md, raw/CONTRACT.md und sämtliche Templates werden entfernt. Der
Kommando-Ablauf prüft jetzt nach dem Merge-Aufruf, dass tatsächlich ein Merge offen ist, und
bricht sonst ab, ohne den Baum angefasst zu haben. Beide Fehler haben je einen Regressionstest,
und beide Tests wurden gegen die alte Fassung laufen gelassen, um zu zeigen, dass sie sie
tatsächlich fangen.
Dazu eine Ehrlichkeitskorrektur an der Erfolgsmeldung: sie zählte die wiederhergestellten
Pfade, nicht die geänderten — ein Merge, der eine Datei bewegt, meldete vier oder fünf. Sie fragt
jetzt git diff zwischen Vor- und Nach-Commit, kennzeichnet Löschungen, und stimmt damit mit dem
überein, was ein Leser nachprüfen würde. docs/ownership-and-templates.md hat einen Abschnitt
bekommen, warum die Grenze ein Prädikat und keine Liste ist — die Begründung, die dieses Issue
erarbeitet hat, gehörte in die Hintergrunddoku und nicht nur in einen Changelog-Eintrag.
Fünfter Bump: wikitool dist upgrade (#7), der zweite der beiden Update-Wege. upstream merge oben bedient eine Instanz mit gemeinsamer Git-History; dist upgrade bedient eine
Instanz aus einem Tarball, ohne History, die bislang eine rein manuelle Prozedur in
INSTALL.md durchlaufen musste - Schritt 4 verlangte einen sha256-Vergleich von Hand gegen den
files-Block der alten .wikitool-release.json.
Die tragende Regel: die Schreibmenge ist genau der files-Block der neuen
.wikitool-release.json, minus was ein Export aus einer leeren Vorlage neu sät
(chemenu.ownership.is_export_stub, wie bisher schon für kb/log.md/.gitkeep) oder einmalig
sät und danach der Instanz gehört (chemenu.ownership.is_upgrade_preserved, neu für
.wikitool-kb.json und CHANGES.md), plus der Stamp selbst. Jeder Kandidatpfad wird gegen die
alte Instanz-Summe klassifiziert: unverändert wird geräuschlos überschrieben, neu im Release
wird angelegt, lokal verändert oder gelöscht wird nie still überschrieben - der Lauf bricht
mit der vollständigen Liste ab, außer --keep-local sagt ausdrücklich, dass die Dateien liegen
bleiben sollen. --prune entfernt zusätzlich aus dem Release entfallene Dateien, aber nur
solche, die seit der Installation unverändert sind.
Die Migrationskette nach dem Tausch wird aus den instructions/migrations/ des neuen Baums
ermittelt (kb_state.load_migrations bekam dafür einen directory-Parameter) und nur
gemeldet, nie ausgeführt - es gibt bewusst kein migrate run. Eine bereits gegen die
installierte Maschinerie offene Kette lässt den Befehl abbrechen, bevor er die Quelle
überhaupt öffnet. kb_state.divergent_files() (bisher nur von migrate status gelesen) ist
jetzt eine dünne Hülle um das neue, zwei-Baum-fähige compare_against_stamp() - gleiches
Verhalten für den bestehenden Aufrufer, wiederverwendbar für dist upgrades eigenen Vergleich.
Quelle ist immer ein bereits vorhandenes Verzeichnis oder .tar.gz - kein Download, das bleibt
allein version checks Sache. Ein Tarball muss genau ein Top-Level-Verzeichnis enthalten (die
Form, in der release.yml es baut) und wird gegen eine .sha256-Beidatei geprüft, falls eine
danebenliegt (fehlt sie: WARN, kein Abbruch). Weitere Abbruchgründe vor jedem Schreiben: fehlende
lokale VERSION/.wikitool-kb.json/Stamp mit files-Block, ein schmutziger Arbeitsbaum (kein
Git-Repo ist ein WARN, keine Sperre), eine Vorab-Version (-beta.N) ohne --pre, sowie ein
Downgrade; Gleichstand ist ein No-op. Ein Grenzübertritt der Kompatibilität wird laut gemeldet,
blockiert aber nicht. Committet und pusht nichts (Invariante 5).
Gegenüber dem ersten Entwurf des Issues zwei Korrekturen, die dort auch nachgetragen sind: der
files-Block wurde entgegen der ursprünglichen Annahme bereits vor diesem Bump gelesen
(divergent_files/migrate status), und die Migrationskette war ursprünglich falsch begründet
- sie kann nur aus dem neuen Baum kommen, nicht durch eine andere Abfragereihenfolge aus der
alten Instanz. 24 neue Tests unter
test_dist_upgrade.pydecken die Klassifikation, alle Abbruchgründe,--keep-local,--pruneund beide Quellformen (Verzeichnis und Tarball, inklusive der sha256- und Top-Level-Prüfung) ab.
Bewusst nicht angetastet: instructions/private-instance.md (der Clone-Weg ändert sich nicht,
INSTALL.md benennt jetzt beide Wege nebeneinander) und die Frage, wie dist upgrade mit
Collection-Templates umgeht, deren Namen eine fremde Instanz gar nicht hat - es verhält sich wie
upstream merge und schreibt sie, was ein eigenes Issue gegen den Export wäre, keins gegen das
Upgrade.
Sechster Bump: die ungeprüfte Phase nachgeholt. Der Abschluss des vorigen Bumps lief auf
Sonnet, und stack-dev Schritt 6 verlangt dort genau zwei Dinge, die kein Check erzwingt:
Issue-Body und docs/-Veralterung. Der Body war gemacht, die docs/-Prüfung nicht - sie wurde
benannt statt durchgeführt. Nachgeholt auf Opus, mit einem Fund.
docs/ownership-and-templates.md § „The consequence in practice" beschrieb ein Upgrade als
Zweiteilung: verbatim überschreiben, .template-gestützte Dateien liegen lassen - und begründete
den ersten Teil damit, dass verbatim ausgelieferte Dateien „safe to replace wholesale" seien,
weil sie „never instance-specific to begin with" waren. Genau diese Annahme trifft dist upgrade
nicht: eine Instanz kann eine verbatim ausgelieferte Datei angefasst haben, und die sha256 je
Datei existiert, um das zu erkennen, statt es vorauszusetzen. Dazu fehlte die dritte Klasse ganz -
die einmalig gesäten, danach instanzeigenen Pfade (.wikitool-kb.json, CHANGES.md,
kb/log.md, raw/*/.gitkeep), die im Stamp stehen wie jede andere Datei und deshalb aktiv
ausgeschlossen werden müssen. Die Seite nennt jetzt drei Klassen und die engere praktische Regel:
überschreibe die verbatim ausgelieferten Dateien, die diese Instanz nicht angefasst hat.
Dazu eine Präzisierung in tools/CONTRACT.md, die vorher nirgends stand: nach --keep-local
wird der neue Stamp trotzdem vollständig geschrieben, trägt also die Release-Summe auch für
Dateien, die bewusst nicht geschrieben wurden. Der Stamp ist die Vergleichsbasis für den
nächsten Lauf, kein wörtliches Inventar der Platte - und genau das hält eine übersprungene Datei
bei jedem weiteren Lauf als abweichend gemeldet, statt sie nach einmaligem Überspringen still als
aktuell zu führen.
4.4.0 - 2026-09-03 - Versionskandidat statt Bump-pro-Release: VERSION traegt -beta.N, version release fixiert
Author: Torben Nehmer
- Versionskandidat statt Bump-pro-Release: VERSION traegt -beta.N, version release fixiert
Bisher bekam jeder version bump sofort eine fixierte Nummer, und weil release.yml auf jede
VERSION-Bewegung feuert, wurde daraus sofort ein Release: Nummern entstanden in
Commit-Granularität statt in Release-Granularität. Der 2026-09-03 hat so vier Releases in sechs
Stunden erzeugt (4.3.0 bis 4.3.3), zwei davon für reine Prosa-Änderungen - alle vier echt,
keines davon eine Einheit, an der ein Konsument sich hätte orientieren können. VERSION trägt
jetzt zwischen zwei Releases einen laufenden Kandidaten (X.Y.Z-beta.N):
--major/--minor/--patch eskaliert diesen Kandidaten max-wins gegen den letzten Release, statt
eine neue Nummer danebenzustellen, und geht dabei nie zurück.
Version versteht den Suffix, mit einer expliziten Ordnung
(4.4.0-beta.1 < 4.4.0-beta.2 < 4.4.0, numerisch nach N, nicht lexikografisch). CHANGES.md
trägt genau einen offenen Eintrag pro Kandidat: der erste Bump eröffnet ihn, jeder weitere
aktualisiert Heading und die maschinenverwaltete Bump-Titel-Liste in
<!-- wikitool:bumps --> (Marker-Konvention aus blocks.py, aber bewusst nicht in
blocks.BLOCKS - diese Region gehört zu CHANGES.md, nicht zu einer Seite). version release
ist neu und fixiert einen Kandidaten: Suffix weg, Eintrag geschlossen, committet und pusht nichts.
Vier Stellen am Bestand angepasst, die das Kandidatenmodell sonst still beschädigt hätten:
release.yml überspringt einen suffixbehafteten VERSION-Push sauber, bevor die Releases-API
gefragt wird, statt jeden Beta-Bump zu veröffentlichen; die Grenzübertritts-Checks in
docs verify (check_migration_for_boundary, check_breaking_change_for_boundary) messen jetzt
gegen den letzten Release (version_mod.last_release) statt gegen den zweitobersten Eintrag,
der zwischen zwei Betas keine Grenze mehr hergibt; kb_state.chain()/next_link() vergleichen
gegen die Kandidatenbasis, weil eine Migration mit Ziel 4.4.0 sonst bei installiertem
4.4.0-beta.1 aus dem Intervall fällt (4.4.0-beta.1 < 4.4.0); read_kb_version() verweigert
einen Prerelease, weil eine Inhaltsform kein Beta kennt. dist export schreibt VERSION und den
Stamp weiterhin ehrlich mit Suffix, aber .wikitool-kb.json bekommt die Basis.
Menschendoku für die Erzeuger-Seite: DEVELOPMENT.md im Repo-Root, bewusst nicht in
dist_cmd.ROOT_FILES (Begründung als Kommentar dort), mit Zeile in AGENTS.md § File naming und
Zeiger aus README.md. docs/version-model.md hat einen neuen Abschnitt, warum eine Nummer erst
durch ein Release verbraucht wird.
4.3.3 - 2026-09-03 - Modellwahl nach Pruefbarkeit statt nach Aufgabenname; stack-dev bricht an den Phasenwechseln fuer den Model-Switch
Author: Torben Nehmer
instructions/claude-code-model-selection.md routete bisher nach Skill: eine Zeile "Stack
development -> Opus/high" fuer alles, was tools/, types/ oder instructions/ anfasst. Das ist
zu grob in beide Richtungen - es verteuert die lange, mechanische Mitte einer Stack-Sitzung, und es
sagt nichts darueber, dass Anfang und Ende derselben Sitzung anders zu behandeln sind.
Die neue Achse ist "was faengt hier einen Fehler ab". Wo ein Check in Code steht - pytest,
docs verify, instructions verify, CI, die Gates - kostet der Fehler eines schwaecheren Modells
eine Runde und faellt auf. Wo die einzige Durchsetzung eine Sitzung ist, die Prosa liest, faellt
derselbe Fehler gar nicht auf: er wird ausgeliefert und bleibt stehen. Das ist dasselbe Argument,
das docs/why-gates-are-code.md fuer Gates fuehrt, angewandt auf die Modellwahl.
Stack-Entwicklung ist damit nicht mehr eine Zeile, sondern drei:
| Phase | Was einen Fehler faengt | Modell |
|---|---|---|
| Design, Versionsstelle, Grenzuebertritts-Urteil | nichts | Opus/high |
| Code, Tests, mechanische Doku-Synchronisation | pytest, CI, docs verify |
Sonnet/high |
Issue-Abschluss, docs/-Veralterung, Changelog-Prosa |
nichts, per Konstruktion | Opus/high |
Die Mitte ist die lange Phase und die mit den Checks - dort liegt die Ersparnis. Die beiden
Raender sind kurz (Minuten, nicht Stunden), haben aber keinen maschinellen Waechter: wikitool
kennt den Issue-Tracker bewusst nicht, und eine docs/-Seite traegt keinen normativen Satz, also
gibt es dort nichts zu verifizieren. Sie oben zu lassen ist billig und schuetzt genau die Arbeit,
die still scheitert.
Zwei Praezisierungen dazu: Effort ist der billigere Hebel als das Modell - medium steht fuer
Stack-Arbeit bewusst in keiner Zeile, weil Mehrdatei-Konsistenz das ist, was ein reduzierter
Effort zuerst aufgibt; high ist die Untergrenze, sobald mehr als eine Datei oder ein Contract
betroffen ist. Und die Asymmetrie ist benannt: eine unnoetige Opus-Phase kostet einmal Geld, eine
ungepruefte Sonnet-Phase kann etwas ausliefern, das nie wieder jemand ansieht.
Damit die Tabelle ueberhaupt wirksam wird, braucht sie Haltepunkte. Eine Sitzung kann ihr
eigenes Modell nicht wechseln - das ist /model und gehoert dem Nutzer. Eine Empfehlung, die
niemand zum richtigen Zeitpunkt ausspricht, aendert nichts. instructions/dev/stack-dev/SKILL.md
bekommt deshalb zwei ausdrueckliche Breaks:
- Neuer Schritt 3 - "Settle the design before building", mit dem Angebot zum Wechsel nach unten, sobald der Plan steht und die Arbeit mechanisch wird. Einmal aussprechen, dann so oder so weiterarbeiten.
- Schritt 6 (Abschluss) bricht in die Gegenrichtung - ab dort greift wieder kein Check. Mit der ausdruecklichen Auflage, die Arbeit unabhaengig von der Antwort zu tun: nach dem Publish auf einen Modellwechsel zu blockieren wuerde genau den Zustand hinterlassen, den Schritt 6 verhindern soll. Lief die Phase auf dem billigeren Modell, gehoert das in die Uebergabe statt ins Schweigen.
Ein auftauchender Grenzuebertritt ist unter den Decision points ebenfalls als Anlass zum Wechsel
nach oben benannt: docs verify prueft, dass ein Uebertritt sich dokumentiert, nie dass die
Stelle richtig gewaehlt war.
Die uebrigen Schritte sind unveraendert und nur umnummeriert (alt 3-5 -> neu 4-6).
4.3.2 - 2026-09-03 - stack-dev: Issue-Abschluss ist ein nummerierter Schritt, kein Zeiger in einer Routing-Liste
Author: Torben Nehmer
Nachfassen zu 4.1.2 (#44), das die Regel geschaerft, aber den Weg zu ihr nicht geaendert hat.
instructions/dev/issue-tracking.md bekam damals Schritt 7 ("Closing is the last body update,
not a comment"); instructions/dev/stack-dev/SKILL.md bekam nur eine umformulierte Zeile in
seiner Routing-Liste. Eine Stunde spaeter schloss #45 auf exakt dieselbe Weise: gruendlicher
Abschlusskommentar ueber einem Body mit unangehakten Kriterien.
Die Ursache lag nicht am Text der Regel, sondern an ihrer Erreichbarkeit. Die nummerierten Schritte des Skills endeten bei "Verify before publishing". Ein Issue zu schliessen war ueberhaupt kein Schritt - es hing an einem Zeiger innerhalb von Schritt 2, und Schritt 2 ist eine Routing-Tabelle aus fuenf "read X before Y"-Eintraegen, keine Checkliste. Eine Sitzung folgt dem Spine, den sie im Kontext hat; was nur hinter einem Link steht, wird genau in dem Moment nicht aufgeschlagen, in dem es greift - am Ende einer langen Sitzung, wenn der Kontext am vollsten und die verbleibende Instruktionsflaeche am duennsten ist.
Verschaerfend arbeitete der Blurb gegen seine eigene Regel: fett gesetzt war "keep it current as the state moves, not at the end". Wer den Body unterwegs ungefaehr gepflegt hatte, las daraus Konformitaet - der eigentliche Abschlusstest stand nur in der verlinkten Datei.
Geaendert:
- Neuer Schritt 5 in
stack-dev/SKILL.md- "Close the issue with a body rewrite, not a comment", mit dem Test inline (Kriterien abgehakt oder mit Begruendung gestrichen, Entscheidungen als entschieden formuliert, kein Praesens ueber einen behobenen Defekt, Verifikation benannt) und dem Verweis auf Schritt 7 fuer die volle Form. Damit steht der Abschluss auf dem Spine. - Schritt-2-Blurb rebalanciert - beide Haelften binden jetzt sichtbar: fortlaufende Pflege und der Rewrite vor dem Schliessen, mit Verweis auf Schritt 5.
Nichts davon ist maschinell pruefbar, und das bleibt richtig so: wikitool kennt den Tracker
nicht und darf ihn nicht lernen, weil es an Instanzen ausliefert, die kein Board haben
(issue-tracking.md § "What no tool checks"). Der Skill-Spine ist die einzige Durchsetzung, die
es geben kann - was der Grund ist, den Schritt zu nummerieren statt ihn zu verlinken.
Verallgemeinerbar: eine Regel, die in eine verlinkte Instruction geschrieben wird, erreicht Sitzungen nur, wenn die nummerierten Schritte des zustaendigen Skills sie in dem Moment ansteuern, in dem sie greift.
4.3.1 - 2026-09-03 - docs/ befuellt - Stack-Hintergrund fuer vier Themen, Pflegeklausel in AGENTS.md ergaenzt
Author: Torben Nehmer
Gitea #45: die von #38 angelegte, bis dahin leere docs/ bekommt ihre ersten vier Seiten - frisch
geschrieben, nicht durch Umzug aus kb/ befuellt, jede ohne normativen Satz und mit Verweis auf
das bindende Dokument statt einer Wiederholung seiner Regeln:
docs/pipeline-rationale.md- warumraw -> types/tools -> kb -> reportsvier getrennte Stufen sind und was "never re-derive, always compile" praktisch bedeutetdocs/why-gates-are-code.md- warum Mass-Update-, Publish-Remote- und Iteration-Budget-Gate intools/wikitoolstatt in einer Instruktion stehendocs/ownership-and-templates.md- der Unterschied zwischen stack-eigenen, verbatim ausgelieferten Dateien und instanz-eigenen.template-Dateiendocs/version-model.md- warum Drop-in-Kompatibilitaet und Migrationsbedarf zwei unabhaengige Fragen sind, illustriert an der 2.0.0-Fallstudie
AGENTS.md § Changelog: neue Klausel zur Pflege von docs/, ergaenzt neben der bestehenden
Regel zu README.md/EVALS.md/tools/README.md. Eine docs/-Seite veraltet nicht wie ein
README bei jedem neuen Flag, sondern nur, wenn die aufgeschriebene Begruendung selbst nicht mehr
traegt - per Konstruktion ungeprueft, da die Seite keinen normativen Satz enthaelt, den
docs verify pruefen koennte.
4.3.0 - 2026-09-03 - docs/ als ausgelieferter Hintergrund-Ort; Decision-Seiten bleiben in kb/, Decay-Skip fuer concept_type: decision
Author: Torben Nehmer
Gitea #38: dist export lieferte bislang keine einzige kb/-Seite aus - eine frische Instanz
bekam den Stack, aber keinen Grund für seine Form. Die dokumentierte adr-NNN--Konvention in
kb/concepts/COLLECTION.md existierte zudem nur auf Papier: keine der sieben
concept_type: decision-Seiten folgte ihr, und confidence_decay() lief bedingungslos über sie
- ein Kategorienfehler, weil Zeitablauf eine Entscheidung nicht falscher macht, nur Supersession tut das.
Neu: docs/ - ein inertes Verzeichnis für Stack-Hintergrund (warum der Stack so gebaut ist,
nicht was diese Instanz entschieden hat). Keine Frontmatter, kein Typ, kein Index, kein Lint,
keine Decay, keine Provenance, keine COLLECTION.md. dist export liefert es verbatim aus, wie
instructions/ und types/. Befüllung folgt in Gitea #45.
Verworfen, nach Prüfung: ein Umzug der sieben Decision-Seiten nach decisions/. Der
Subtyp-Floor aus #28 verlangt mindestens eine Seite je deklariertem concept_type, und ein
Umzug hätte decision auf null gebracht; dazu zeigen 89 Wikilinks aus kb/ sowie
tool-eigene Frontmatter-Arrays auf die sieben, und links.py/xref add kennen kein Ziel
außerhalb kb/. Die sieben bleiben in kb/concepts/, ebenso ein zweiter, separat erwogener
Rename (docs verify → parity verify) - der wäre nur nötig gewesen, wenn ein Befehl auf das
Verzeichnis docs/ wirkt, und keiner tut das.
Geändert:
confidence_decay()überspringtconcept_type: decisionstrukturell (kategorische Ausnahme, nicht als Brücke gebaut - Begründung im Docstring).kb/concepts/COLLECTION.md§ Decisions ersetzt die tote ADR-Vorlage durch die real gelebte Form: eine Entscheidung ist eine gewöhnliche Concept-Seite, organische Prosa, keinadr-NNN--Präfix,**Status:**optional, Supersession persupersedes-Link.kb/CONVENTIONS.md§ Naming undinstructions/kb-profiles.md(Profilgerman) korrigiert - beide dokumentierten noch die verworfeneadr-NNN--Namensregel.AGENTS.md§ File naming und § Routing:docs/-Zeile, plus die Regel, dassdocs/keinen normativen Satz trägt (das hält Invariante 8 heil - was binden würde, gehört in einen Contract).tools/CONTRACT.md: Klarstellung, dassdocs verifyDokumentations-Parität prüft, nicht dasdocs/-Verzeichnis, sowiedocs/in derdist export-Zeile ergänzt.
Additiv und in beide Richtungen drop-in: eine bestehende Instanz ohne docs/ exportiert
weiterhin identisch (leerer _copy_tree-Treffer), eine Instanz mit docs/ bekommt es ab jetzt
mitgeliefert. Kein Feld, kein Kommando ändert sein Verhalten für bestehenden Inhalt.
Migration: none required.
Berührt: tools/chemenu/commands/confidence_decay.py, tools/chemenu/commands/dist_cmd.py,
tools/chemenu/tests/test_confidence_decay.py, tools/chemenu/tests/test_dist_cmd.py,
kb/concepts/COLLECTION.md, kb/CONVENTIONS.md, instructions/kb-profiles.md, AGENTS.md,
tools/CONTRACT.md.
4.2.0 - 2026-09-03 - Korpus-Kuratierungsrichtlinie: Untergrenzen und Leitplanke für reaktive Fixes
Author: Torben Nehmer
Ein Demo-Korpus will klein und stabil sein, ein Testbett groß, unordentlich und in Bewegung -
dieses Repo verlangt seit der Veröffentlichung beides vom selben kb/ (Gitea #28). Die Sitzung
vom 2026-09-02 hatte Fixture, --with-demo und ein zweites Repo bereits verworfen; offen blieb
nur, wie kuratiert "kuratiert genug" heißt und welche Leitplanke reaktive Fixes bekommen.
Neu: instructions/dev/corpus-policy.md. Fünf Untergrenzen, jede mit einer bestehenden
wikitool-Prüfung messbar, keine davon durch neuen Tool-Code: jeder Seitentyp und jeder
deklarierte Subtyp mit mindestens einer Seite, mindestens fünf Seiten mit mindestens drei
Quellen, ein bis zehn Orphan-Seiten, im Schnitt mindestens vier ausgehende Wikilinks pro Seite.
Gemessen am 2026-09-03: 181 Seiten, alle Typ-/Subtyp-Floors erfüllt, 12 Seiten mit ≥3 Quellen, 3
Orphans, Ø 6,2 ausgehende Links - der Korpus war bereits groß genug, ohne dass eine einzige
Seite eigens dafür angelegt werden musste. Eine Untergrenze wird nie durch eine erfundene Seite
gefüllt, sondern durch eine echte Quelle beim nächsten passenden Ingest - Invariante 3 gilt
unverändert.
Die Leitplanke für reaktive Fixes unterscheidet drei Stufen: punktuelle Änderungen (immer
erlaubt, gewöhnliche Arbeit), korpusweite Änderungen (nur geplant, mit eigenem Issue und
work/-Run - trifft eine Session das Mass-Update-Gate während sie etwas anderes tat, holt sie
sich nicht den --confirm-Token, sondern stoppt und legt ein Issue an) und reaktive Eingriffe
in Korpusinhalt, um einen Test grün zu machen oder einen Tool-Bug zu umgehen (nie erlaubt,
Invariante 7). Das Verhältnis zu kb_dir/raw_dir und test_pipeline_l0.py bleibt wie im
ursprünglichen Befund: kleiner, isolierter Fall in der Fixture, großer, vernetzter Fall in
kb/ - keine Fixture-Extraktion aus dem Korpus.
Dev-only und rein additiv - kein Feld, kein Kommando, keine Datei außerhalb von
instructions/dev/ ändert sich, daher --minor ohne --breaking.
Migration: none required.
Berührt: instructions/dev/corpus-policy.md (neu),
instructions/dev/stack-dev/SKILL.md (Schritt 2, Routing-Zeile).
4.1.2 - 2026-09-03 - Issue-Abschluss ist ein Body-Rewrite, nicht nur ein Kommentar
Author: Torben Nehmer
Aufgefallen beim Schließen von #44: der Abschlussbericht stand als Kommentar da, der Body darunter weiterhin als offene Arbeit — Abschnitt „Zu entscheiden" über eine längst getroffene Entscheidung, ungehakte Checkliste, Präsens über einen Defekt, den es nicht mehr gab.
Die Regel gab es dafür schon: Schritt 2 von instructions/dev/issue-tracking.md sagt, der Body
ist die aktuelle Wahrheit und wird umgeschrieben, wenn sich der Stand ändert. Nur ließ die
Formulierung offen, wann — und Schritt 7 („Close with what actually happened") war vollständig
erfüllbar, ohne den Body anzufassen. Ein Abschlussbericht im Kommentar fühlt sich beim Schreiben
vollständig an; dass der Body dabei zurückbleibt, merkt erst der nächste Leser.
Schritt 2 ist deshalb schärfer geworden: der Body ist das Plan-File dieses Stacks. Dasselbe, was das Plan-Dokument eines Harness ist, und genauso gepflegt — fortlaufend, sobald etwas darin nicht mehr stimmt, nicht am Ende. Der Maßstab ist der Abbruch, nicht der Meilenstein: eine Session kann jederzeit enden, und was der Body in diesem Moment sagt, ist die vollständige Übergabe. Eine frische Session muss zu jedem Zeitpunkt allein aus dem Body weiterarbeiten können, ohne Kommentare rückwärts zu lesen und ohne einen Menschen, der es neu erklärt. Entschieden ersetzt die Frage, erledigt hakt das Kriterium ab, verworfen steht mit Begründung dort, wo das Kriterium stand.
Schritt 7 ist damit kein Sonderakt mehr, sondern die letzte dieser Aktualisierungen: erst Body auf den Endstand, dann schließen, dann die Changelog-Zeile aus Schritt 3. Wer Schritt 2 befolgt hat, ist fast fertig; wer nicht, zahlt die ganze Schuld im schlechtesten Moment — der geschlossene Body ist die Fassung, die danach alle lesen und niemand mehr aufsucht. #44 steht als Beispiel drin.
Schritt 3 zieht die Konsequenz: ein Kommentar pro Session-Umfang, nicht pro Edit. Ein
fortlaufend gepflegter Body mit einem Changelog-Kommentar je Änderung wäre Lärm; triviale Pflege
braucht gar keinen. Der stack-dev-Skill sagt es beim Aufgreifen mit, weil dort die Entscheidung
fällt, ob eine Session den Body überhaupt anfasst.
Und die ehrliche Antwort auf die Frage nach dem Tooling: es gibt keins, und es soll keins
geben. wikitool kennt diesen Tracker nicht. Es wird an Instanzen ausgeliefert, die unter
dieser URL keine Issues haben, während instructions/dev/ von dist export gepruned wird —
ein Gitea-Client im ausgelieferten Tool wäre eine Dev-Abhängigkeit, die jede Instanz mitträgt,
um ein Board zu prüfen, das keine von ihnen hat. Der Tracker ist ausschließlich über
gitea-mcp erreichbar, also in einer Session, durch einen Agenten.
Kein docs verify fängt also einen geschlossenen Issue, dessen Body offen klingt, einen Body,
der seinen eigenen Kommentaren widerspricht, oder ein fehlendes Pflichtlabel. Das steht jetzt
als eigener Abschnitt „What no tool checks" in der Instruktion — nicht als Bedauern, sondern als
Begründung dafür, warum die Reihenfolge in Schritt 7 ausgeschrieben ist statt aus Schritt 2
erschlossen zu werden.
4.1.1 - 2026-09-03 - Testisolation: kb_dir repointet config.ROOT, lint löst Kollektionen gegen den übergebenen Baum auf
Author: Torben Nehmer
Issue #44, gefunden beim Bau der Migrations-Gate-Tests für 4.1.0: die kb_dir-Fixture baute
ihren Baum unter tmp_path, ließ config.ROOT aber auf dem echten Checkout stehen. Jeder
Codepfad, der eine Datei über config.ROOT/config.KB_DIR auflöst statt über das übergebene
Verzeichnis, traf damit das echte Repository.
Der laute Fall war ein Test, der kb_state.write_kb_state() rief und dabei das
.wikitool-kb.json des Repos überschrieb — Applied-Ledger leer statt zwei Einträgen. In
git status sofort sichtbar und reversibel; bei einer gitignorierten Datei wäre es das nicht
gewesen.
Der stillere Fall ist der teurere. lints Kollektions-Lookup löste eine Seite gegen
config.KB_DIR auf. Für eine Seite unter tmp_path/kb/ warf das ValueError, die Funktion
antwortete „keine Kollektion", und die Label-Autorisierung übersprang die Kante wortlos.
unauthorised_labels war damit faktisch ungetestet — jeder Test, der das Finding hätte
auslösen können, bekam eine leere Liste und behauptete nichts. Ein grüner Lauf, der wie eine
Zusicherung aussah.
Der Fix ist der Codepfad, nicht die Fixture. run_lint() bekommt ein Verzeichnis
übergeben und löst jetzt auch intern dagegen auf; authorised_labels() bekommt denselben Baum
gereicht, statt auf config.KB_DIR zurückzufallen. Der Regressionstest lintet einen Baum, von
dem ROOT bewusst wegzeigt — genau der Fall, den die alte Auflösung verschluckte. Eine Funktion,
die ein Verzeichnis entgegennimmt, löst dagegen auf: keine Fixture kann diese Form von außen
reparieren.
Beide Korpus-Fixturen repointen jetzt. kb_dir tut, was raw_dir längst tat — ROOT auf
das eigene tmp_path, plus use_shipped_type_specs(). Der Suite-Lauf kippte dadurch keinen
einzigen Test. Die lokale rooted_kb-Umgehung aus 4.1.0 entfällt damit; die Auswahl zwischen
zwei fast gleichen Fixturen war Wissen, das nirgends stand.
Und ein Wächter für die ganze Klasse. repository_tree_guard (session-scoped, autouse)
vergleicht git status --porcelain vor und nach dem Lauf und lässt die Suite scheitern, wenn
sich im Checkout etwas bewegt hat — zwei git status-Aufrufe pro Lauf, deshalb per Default an.
Er vergleicht vorher gegen nachher statt einen sauberen Baum zu verlangen, sagt also nichts über
die unveröffentlichte Arbeit des Entwicklers. Den Verursacher benennt er nicht;
CHEMENU_TREE_GUARD=each prüft nach jedem Test und tut es. Ohne git oder außerhalb eines
Repositorys sind beide still.
Was der Wächter nicht sieht: eine Prüfung, die unter Test nichts tut, schreibt keine Datei. Dagegen hilft nur ein Test, der das Finding tatsächlich auslöst — der neue tut das.
instructions/dev/testing-conventions.md hat dafür einen eigenen Abschnitt („Which tree a test
writes into"), einen Schritt in der Checkliste und die Regel für neue Fixturen.
4.1.0 - 2026-09-03 - Link-Taxonomie: Lint-Findings hart ab kb_version 4.0.0, outbound: an das Type-Spec gebunden, part-of/composition als Inversenpaar
Author: Torben Nehmer
Der Rest von Issue #40, nachdem die Korpus-Migration durch ist: die beiden aufgeschobenen Lint-Findings werden hart, und die drei Befunde aus dem Abschlusskommentar des Migrationslaufs werden aufgelöst.
unlabelled_edges und unauthorised_labels sind harte Fehler — aber an kb_version
gebunden, nicht an ein Datum. Der Weg, den legacy_citation_markers genommen hat, war ein
Umlegen in einer späteren Version: eine Instanz, die die Zitat-Migration noch schuldete, lebte
danach mit rotem Lint. Das Ledger kann die Frage inzwischen beantworten, also tut es das.
Unterhalb kb_version 4.0.0 bleiben beide beratend — genau das Fenster, in dem
instructions/migrations/4.0.0-link-taxonomy.md der Instanz sagt, sie solle den halb
konvertierten Korpus Einheit für Einheit publizieren; ein Check, der dabei fehlschlägt, würde
den Korpus verweigern, dessen Fortschritt er misst. Ab 4.0.0 ist eine kahle Titelangabe in
related: keine Seite mehr, die auf ihre Umstellung wartet, sondern eine Kante, deren Autor
nicht gesagt hat, was sie behauptet. hard_error_keys() liefert die jeweils geltende Menge,
HARD_ERROR_KEYS bleibt die vollständige.
outbound: ist an das Type-Spec gebunden. kb/sources/ und kb/comparisons/
autorisierten Label, die dort strukturell nicht schreibbar waren: keiner der beiden Type-Specs
führte ein related:. Folgenlos war das nicht — die einzige Comparison-Seite des Korpus trug
- **compares-with:** [[amd-pstate]] als handgeschriebene Prosa, ohne Marker-Region, ohne
Frontmatter, für lint unsichtbar. Also ein Identifier zurück im Fließtext, gut vier Stunden
nachdem 4.0.0 genau das beendet hatte. Eine leere Autorisierung liest sich als Lizenz.
Aufgelöst nach dem, was die beiden Contracts jeweils selbst sagen: comparison bekommt ein
related: (die compares-with-Kante gegen jedes Subjekt ist die eine Aussage, für die die
Seite existiert), kb/sources/ verliert seinen outbound:-Block ersatzlos (dessen Contract
sagt ausdrücklich, seine Verknüpfungen seien der mechanische Provenance-Pfad und keine
Autorenkanten). Neu prüft docs verify die Kombination: ein outbound:-Block auf einer
Collection, in die kein Typ mit related: schreibt, ist ein Befund und nennt beide Richtungen
der Reparatur.
composition / part-of ist das dritte Inversenpaar, neben depends-on / required-by
und runs-on / hosts. Aus der Messung, nicht vom Schreibtisch: der u3-Lauf hatte entschieden,
die Gegenseite eines composition bekomme see-also, weil part-of ein Spiegel wäre. Ist es
nicht — der Satz des Elternteils zählt seine Teile auf, der des Kindes benennt das Ganze, zu
dem es gehört, und ein Leser, der auf dem Kind landet, braucht den zweiten. Übrig blieben 16
see-also-Kanten für eine Beziehung, für die der Katalog ein Wort hat; sie sind auf part-of
umgestellt. Ein Inversenpaar macht die Gegenkante weiterhin nicht zur Pflicht — Richtung
wird verfasst, nicht gespiegelt —, es legt nur fest, welches Label sie trägt, wenn jemand sie
schreibt.
Stack- und Korpusänderung laufen hier in einem Zug, entgegen der sonstigen Trennung. Der
neue docs verify-Check würde eine bestehende 4.0.x-Instanz beim bloßen Kopieren der neuen
Maschinerie fehlschlagen lassen, weil deren kb/sources/COLLECTION.md den outbound:-Block
noch trägt — nach instructions/dev/version-parts.md
Schritt 1 ein Grenzübertritt. Statt dafür eine 5.0.0 zu lösen, ist die Ursache mitbeseitigt:
die Collection-Contracts dieser Instanz sind angepasst, und dist export leitet die
COLLECTION.md.template daraus ab, also liefert jede neue Distribution die korrigierte Form
aus. Für eine bereits bestehende 4.0.x-Instanz bleibt eine Handbewegung übrig, und sie wird
hier benannt statt versteckt: die zwei outbound:-Zeilen aus kb/sources/COLLECTION.md
löschen. Das neue related: im comparison-Type-Spec erreicht sie ohnehin nicht — die vier
Page-Type-Specs gehören seit 4.0.0 der Instanz und werden nur als .template ausgeliefert.
Offen aus #40 bleibt nichts mehr; Befund 2 des Migrationslaufs (dem Katalog fehlt ein Register für Urheberschaft) ist als eigenes Issue erfasst.
4.0.1 - 2026-09-02 - Issue-Board: vier Pflicht-Label-Familien und Body-als-Wahrheit
Author: Torben Nehmer
Das Issue-Schema aus 1.2.1 hatte zwei Pflichtachsen und einen ausdrücklich begründeten Verzicht auf eine dritte: eine Taxonomie mit mehr Achsen brauche eigene Pflege, und das Board habe einen einzigen Betreuer. Diese Begründung ist entfallen, weil die Pflege inzwischen maschinell passiert - Body-Rewrites und Kommentare laufen über eine LLM-Sitzung, Menschen fassen in der Regel nur Labels an. Damit sind vier Achsen bezahlbar (Issue #41).
Pflicht auf jedem offenen Issue sind jetzt vier Label: area/ (kb, distribution,
corpus, workflow, process - kein area/tools, Tooling wird nach der bedienten Domäne
einsortiert, nicht nach Codeort), kind/ (decision, build, defect), prio/
(blocking, planned, waiting - reine Umbenennung von 1/2/3) und size/ (S, M,
L; XS entfällt). Dazu zwei optionale Flags: status/blocked für Abhängigkeit von einem
anderen offenen Issue, status/unconfirmed für einen ungeprüften Verdacht, unter dem size
und prio vorläufig sind. Ein unconfirmed-Issue endet in der Triage entweder ohne Flag und
mit verbindlichen Werten oder geschlossen mit Begründung - die Prozessentsprechung zu
Invariante 3.
Der Issue-Body ist ab jetzt aktuelle Wahrheit, nicht Ursprungstext. Die Umsetzung eines Issues zieht sich über mehrere, zeitlich getrennte Sitzungen, und der Body ist das einzige, was sie verbindet: eine Sitzung muss aus ihm allein rekonstruieren können, was entschieden und was offen ist. Er wird deshalb umgeschrieben statt ergänzt. Jeder Rewrite bekommt einen Kommentar, der ausschließlich benennt, was sich geändert hat - keine Vollkopie des alten Stands, weil ein Mensch zwei Fließtexte nicht diffen kann und eine Kopie pro Revision damit keine Historie ist, sondern nur eine weitere Kopie.
Geändert: instructions/dev/issue-tracking.md (Schritte 2,
3, 5 neu; Schritt 4 um area/ und kind/ erweitert; der Entscheidungspunkt „Two labels feel
too coarse?" entfällt) und die Beschreibungszeile in instructions/dev/stack-dev/SKILL.md. Für
eine ausgelieferte Instanz ändert sich nichts: dist export schließt instructions/dev/
vollständig aus, weshalb dies ein PATCH ist und kein MINOR - dieselbe Begründung wie bei
1.2.1. Noch offen aus #41: kb/concepts/Issue Label Scheme.md beschreibt weiterhin das
zweiachsige Schema und braucht eine eigene wiki-manage-Sitzung.
4.0.0 - 2026-09-02 - Prosa ist kein Identifier: Link-Taxonomie als Enum, generierte Regionen mit Markern
Author: Torben Nehmer
Breaking Change: Beziehungslabel sind Enum-Werte in related: statt Freitext im Body-Bullet, toolgefuehrte Abschnitte liegen zwischen Marker-Paaren statt hinter ihrer Ueberschrift, und xref add schreibt nur noch eine Kante statt beider Richtungen. tools/chemenu/sections.py ist geloescht. Eine bestehende Instanz muss sections: in kb/CONVENTIONS.md auf links/footnotes umstellen, outbound: in jede COLLECTION.md eintragen, die {section.*}-Variablen aus ihren Page-Type-Templates entfernen und den Korpus umstellen - sonst scaffoldet new die Variablen woertlich in neue Seiten. Ablauf: instructions/migrations/4.0.0-link-taxonomy.md
Der Stack benutzte an drei Stellen Prosa als Identifier, und jede hat messbar etwas
gekostet. Die Überschrift eines Abschnitts war seine Adresse (^## Beziehungen$), was die
KB-Sprache zu einer Compiler-Konstante machte und das Ende der Region zur Schätzung - sie lief
bis zur nächsten Überschrift, davor bis zum Dateiende, und hat auf acht Seiten still Inhalt
gelöscht. Das Beziehungslabel stand nur im Body-Bullet, also konnte nichts das Vokabular prüfen:
gemessen am Korpus 152 distinkte Label in 337 Bullets gegen dreizehn dokumentierte, 102 davon
genau einmal vorkommend. Und xref add spiegelte jede Kante, was ## Siehe auch mit 555
Bullets ohne Label füllte - 353 davon beweisbar redundant.
Was jetzt Identifier ist. Eine Region liegt zwischen <!-- wikitool:links --> bzw.
<!-- wikitool:footnotes --> und wird vollständig aus dem Frontmatter gerendert, Überschrift
eingeschlossen. Ein Label ist ein Maschinenwert in related: (- depends-on: Hermes), gezogen
aus instructions/link-taxonomy.md und pro Ziel autorisiert von der Quell-Collection
(outbound: im COLLECTION.md, Commonplaces ADR-019). Der Body-Bullet ist eine Darstellung
dieser Daten, nicht ihr zweiter Aufbewahrungsort.
Gelöscht, ersatzlos: tools/chemenu/sections.py komplett, heading_re, der
Alias-Mechanismus, PRE_CONVENTIONS_NAMES, cite_block_heading, provenance.__getattr__, die
{section.*}-Template-Variablen, xrefs Abschnittssuche. Kein Überschriftentext liegt mehr in
Python - bis auf zwei kosmetische Fallbacks, und die sind harmlos geworden: der Marker trägt die
Identität, also rendert ein falscher Default falsche Wörter statt Struktur zu zerlegen, und der
nächste Write repariert es.
Kanten sind direktional, und das war keine Geschmacksfrage. Die per-Collection-Autorisierung
ist mit einer automatisch gespiegelten Gegenkante logisch unverträglich: die Spiegelhälfte
entsteht in einer Collection, deren Regeln der Autor nie gelesen hat. Entweder schriebe das
Werkzeug unautorisierte Kanten, oder die Regel "die Quellcollection entscheidet" löst sich auf.
Der Navigationseinwand wird dabei besser beantwortet als vorher: wikitool links show --page
berechnet die Eingangssicht über den Korpus, vollständig und ohne Pflege, und das gerenderte
Bullet ist ein gewöhnlicher [[wikilink]] - ein Backlink-Panel zeigt es ohnehin. Die erzwungene
Gegenkante garantierte nie Vollständigkeit, nur dass jemand daran gedacht hat.
Der Orphan-Check meldet dadurch mehr, und das ist die Prüfung bei der Arbeit: sie misst jetzt
Erreichbarkeit statt "ist xref gelaufen".
obligation: trennt zwei Achsen, die vorher eine waren. migration_kind: sagt wie
gearbeitet wird, neu obligation: required|offered ob überhaupt. Eine offered-Migration ist
ein Angebot für eine Datei, die der Instanz gehört - sie blockiert nie, steht nicht in der Kette,
und migrate done verbucht sie im Ledger, ohne kb_version zu bewegen. Genau daran hing ein
Entwurfsfehler, den erst der Test gezeigt hat: Offers gegen kb_version zu filtern hätte jede
Offer verschwinden lassen, sobald irgendein unbeteiligter Pflichtschritt lief. Dazu ist die
Erkennungshälfte aktiviert, die seit ihrer Einführung ungelesen dalag - die sha256 pro Datei in
.wikitool-release.json beantwortet jetzt "editiert oder nur empfangen", also ob eine Offer
kopiert werden darf oder von Hand abgeglichen werden muss.
types/ teilt sich entlang root:. root: kb heißt Wissensseite heißt Instanz: die vier
Page-Type-Specs samt Schemas gehen als .template, instruction/lint-report/type-spec
verbatim. Damit ist die deutsche Prosa in jenen vier Dateien korrekt statt Migrationsschuld -
es war die richtige Sprache an einem Ort mit falsch deklariertem Eigentümer. Was der Stack von
der Type-Schicht noch verlangt, ist eine Zeile: ein Type-Spec name: source, dessen Schema
raw_files fordert. STACK_REQUIRED_COLLECTIONS entfällt als separate Liste - die pflichtige
Collection wird aus dem base_dir dieses Typs abgeleitet.
Warum das MAJOR ist. Vorwärts: sections: hat eine andere Form, outbound: fehlt, und die
in 3.0.0 übernommenen Page-Type-Templates enthalten {section.*}-Variablen, die es nicht mehr
gibt - new schriebe sie wörtlich in neue Seiten. Rückwärts: 4.0.0 schreibt gelabelte Kanten,
die 3.0.0s Schema als type: string ablehnt. Beide Hälften des Drop-in-Tests fallen.
Der Korpus dieser Instanz ist noch nicht umgestellt. Diese Version liefert die Maschinerie;
lint meldet die 480 noch ungelabelten Kanten als Findings, nicht als Fehler, weil das genau das
Fenster ist, für das .wikitool-kb.json existiert. malformed_edges und unbalanced_markers
sind dagegen sofort hart - keines beschreibt eine unkonvertierte Seite, nur eine kaputte. Die
Beförderung der beiden anderen kommt, wenn der Korpus sie bestehen kann.
3.0.0 - 2026-09-02 - Autorenkonventionen nach Eigentum geschnitten: kb/CONVENTIONS.md, deklarierte Collections
Author: Torben Nehmer
Breaking Change: kb/CONTRACT.md ist um alles gekuerzt, was eine Instanz selbst entscheidet; das steht jetzt in einer neuen, instanzeigenen kb/CONVENTIONS.md, aus der der Compiler die drei toolgefuehrten Abschnittsnamen liest. Eine bestehende Instanz muss diese Datei anlegen, auf jedem kb/*/COLLECTION.md profile: und required_by_stack: deklarieren und kb/CONTRACT.md aus dem Release nachziehen - sonst FAILt doctor und docs verify bricht. Ablauf: instructions/migrations/3.0.0-authoring-conventions.md
kb/CONTRACT.md war eine Datei mit zwei Autoritäten. Der eine Teil ist code-erzwungen und in
jeder Instanz gleich; der andere - § Language komplett, das Beziehungslabel-Vokabular, die
Tonfall-Beispiele samt deutscher Buzzword-Liste, die Confidence-Rubrik, das ADR-Präfix - ist
Konvention, die jede Instanz für sich entscheidet, und wurde trotzdem als bindender Contract
verbatim ausgeliefert. Wer bei Schritt 5 von setup-instance.md "Englisch" antwortete, hatte
danach kb/CONTRACT.md, vier Type-Specs und tools/chemenu/sections.py lokal geändert -
und private-instance.mds Decision Point sagt für so einen Merge-Konflikt: Upstream-Seite
nehmen. Für diese Instanz hieß das: KB-Sprache zurück auf Deutsch.
Der Schnitt läuft jetzt danach, wer den Satz ändern darf. kb/CONTRACT.md behält, was
wikitool erzwingt; neu daneben liegt kb/CONVENTIONS.md, die genauso bindet und der
Instanz gehört. Unterschied ist Eigentum, nicht Autorität - deshalb liefert die Distribution nur
kb/CONVENTIONS.md.template, exakt der USER.md/SOUL.md-Split ein Verzeichnis tiefer. Dazu
instructions/kb-profiles.md: der Katalog erprobter Profile, ausdrücklich Palette und kein
Enum. Übernommen wird der Text in die Instanzdatei, nie ein Verweis auf den Katalog - ein
Verweis wäre wieder genau die Konstruktion, die dieser Release beendet.
sections.py hält keine Überschrift mehr. RELATIONSHIPS = "Beziehungen" war die Stelle,
an der die Konvention in Code übergelaufen war: solange sie dort stand, konnte kein Template die
Sprache umstellen. Neu ist tools/chemenu/conventions.py, das die drei Namen aus
kb/CONVENTIONS.md liest; sections.py löst sie per PEP 562 bei jedem Zugriff auf, wie
config seine Pfade - ein Modulkonstante hätte den Wert an den Baum gebunden, in dem der Prozess
gestartet ist. Aus demselben Grund ist provenance.CITE_BLOCK_HEADING ein __getattr__ und
render_cite_block(heading=None) löst innerhalb des Aufrufs auf. Der Alias-Mechanismus, den das
Modul schon hatte, ist der Migrationspfad: erkannt wird die kanonische Form plus die
deklarierten section_aliases: plus das, was dieser Stack vor der Konventionsdatei geschrieben
hat. Ohne Datei antwortet dieser Fallback - richtig für jeden Korpus, der ihn erreichen kann,
denn der wurde unter genau diesen Namen geschrieben; doctor ist die laute Hälfte davon.
Die vier Page-Type-Specs schreiben ## {section.relationships} statt einer Überschrift.
Neue Template-Variablen {section.relationships} / {section.see_also} / {section.footnotes},
gefüllt aus der Instanzdeklaration. Damit ändert eine anderssprachige Instanz keine Datei unter
tools/ oder types/ mehr - was Schritt 5 von setup-instance.md von fünf Editierstellen
über drei Schichten auf eine Entscheidung reduziert.
COLLECTION.md bekommt Frontmatter. Bisher wurde eine Collection rein an der Dateipräsenz
erkannt; die Deklaration brauchte einen Träger, sonst wäre der Ortsschnitt nur durch einen
Prosaschnitt ersetzt worden. profile: nennt den übernommenen Katalogeintrag (Freitext - eine
selbst angelegte Collection hat dort keinen), required_by_stack: sagt, ob wikitool die
Collection namentlich auflöst. Das zweite ist nicht die Wahl der Instanz: docs verify
prüft es beidseitig gegen kb_collections.STACK_REQUIRED_COLLECTIONS. Heute steht dort genau
sources - sources coverage, die [^cite-id]-Auflösung und kb/provenance.md hängen an dem
Namen, entities an keinem.
Das zweite Leck der Merge-Prozedur ist zu. git checkout HEAD -- kb raw holte alles unter
beiden Stages auf den Vor-Merge-Stand - auch kb/CONTRACT.md und raw/CONTRACT.md. Änderte der
Upstream einen davon, warf die Prozedur das Update still weg, und die Kontrollzeile meldete dabei
leer, bestätigte den Fehler also, statt ihn zu fangen. private-instance.md nimmt die
Upstream-Seite jetzt für die drei Maschinerie-Pfade unter den Content-Stages zurück
(kb/CONTRACT.md, kb/CONVENTIONS.md.template, raw/CONTRACT.md) und schließt sie aus der
Kontrollzeile aus. Dieselbe Altlast in der Tarball-Richtung: INSTALL.md Schritt 3 fasste kb/
gar nicht an und zog kb/CONTRACT.md damit nie nach - jetzt ausdrücklich benannt.
Verworfen, gemessen: sources/ aus kb/ herausziehen. Der Graph ist einwurzelig
(kb_scan.iter_kb_pages macht ein rglob über kb/, darauf sitzen Link-Graph, Orphan-Check,
index rebuild und search), und Source-Seiten sind darin der dichteste Knotentyp. Ein Hoist
machte jede Graph-Operation dauerhaft zweiwurzelig, um ein Verzeichnis umzubenennen. Vor allem
aber kann der Ort Eigentum ohnehin nicht kodieren, sobald Collections offen sind: eine selbst
angelegte liegt im selben kb/ wie die Defaults. Eigentum ist eine deklarierte Eigenschaft -
daher das Frontmatter oben. Gitea #39 trägt die Ablehnung im Volltext.
Warum das MAJOR ist. Die Rückwärtshälfte des Drop-in-Tests hält - 2.5.0 ignoriert beide neuen
Deklarationen folgenlos. Die Vorwärtshälfte nicht: nach dem Kopieren der Maschinerie FAILt
doctor auf der fehlenden kb/CONVENTIONS.md, docs verify bricht auf den undeklarierten
Collections, und kb/CONTRACT.md muss aus dem Release nachgezogen werden. Ein Shim war die
Alternative (doctor nur WARN, Pflichtfelder tolerant) und wurde verworfen: er hätte genau den
Zustand normalisiert, in dem eine Instanz glaubt, sie habe entschieden, während in Wahrheit der
Fallback antwortet - für eine englische Instanz hieße das ## Beziehungen in englischen Seiten.
Die Handarbeit ist eine Datei und zwei Frontmatter-Zeilen je Collection; keine einzige kb/-Seite
ändert sich, weshalb migrate done 3.0.0 --pages 0 ehrlich und kein Platzhalter ist.
2.5.0 - 2026-09-02 - Versionsstelle: Kompatibilitaet statt Inhaltsmigration, Breaking-Change-Vermerk erzwungen
Author: Torben Nehmer
Die Doku des Stacks führte für die Wahl der Versionsstelle zwei Fragen zusammen, die nicht
dieselbe sind: muss der Korpus migriert werden? und ist die neue Version ein
Drop-in-Ersatz? An allen drei Stellen, die einer Session vor dem Bump begegnen - die Tabelle
in stack-dev, der Modul-Docstring in version.py, INSTALL.md § Version und Updates -
stand nur die erste. Nur version bump --help formulierte es richtig, also ausgerechnet die
Stelle, die man erst liest, wenn die Entscheidung gefallen ist. Bei 2.0.0 hat genau diese
Lücke zu 1.9.0 geführt (Gitea #26; der Eintrag zu 2.0.0 trägt den Fall im Volltext).
Die Regel, jetzt an einer Stelle. Neu ist
instructions/dev/version-parts.md: der Drop-in-Test in zwei Hälften - vorwärts (kopieren,
fertig, keine Handarbeit durch Nutzer oder Migrationsskript) und rückwärts (die alte Version
lässt sich noch zurückinstallieren) -, der Katalog der Brüche, die die Grenze bei völlig
unangetastetem kb/ überschreiten (Update-Pfad, Artefaktname, Import-Name, Flags, Envvars,
Shape maschinengelesener Dateien), und der Fall 2.0.0 als Fallbeispiel. instructions/dev/,
also nicht in einer ausgelieferten Instanz - die Datei entscheidet, wer den Stack entwickelt.
Die ausgelieferten Artefakte tragen deshalb die Kurzform der Regel, aber keinen Verweis auf
sie: instructions verify meldet einen solchen Verweis, weil er nach dist export ins Leere
zeigen würde.
Breaking Changes sind teuer, also genehmigungspflichtig. stack-dev hat einen
Entscheidungspunkt bekommen: stellt sich eine Änderung als nicht drop-in heraus, wird nicht
aus eigener Initiative über die Grenze gebumpt. Vorgelegt werden der konkrete Bruch, was jede
bestehende Instanz von Hand tun muss, und die Alternativen - Bruch per Shim vermeiden,
aufschieben und mit dem nächsten bündeln, oder in einen kompatiblen und einen brechenden Teil
mit Deprecation-Fenster spalten -, dazu eine Empfehlung. Dann Freigabe abwarten.
--breaking ist neu und bei jedem Grenzübertritt Pflicht. Es schreibt eine
**Breaking Change:**-Zeile in den Eintrag, vor die Migrationszeile: das ist, was der Operator
einer bestehenden Instanz beim Lesen der Release-Notes zuerst braucht. docs verify prüft den
neuesten grenzüberschreitenden Eintrag darauf, analog zu **Migration:** none required und aus
demselben Grund - eine Prosa-Regel driftet, ein Marker nicht. Auf einem kompatiblen Bump wird
--breaking verweigert, damit es keine Gewohnheit wird. Beide Prüfungen bleiben getrennt:
check_migration_for_boundary fragt, ob der Inhalt sich bewegt, check_breaking_change_for_boundary,
ob dem Operator gesagt wurde, dass der Tausch keiner zum Drüberkopieren ist. Ein Rebranding
besteht die erste und fällt durch die zweite.
Was ausdrücklich nicht passiert: docs verify prüft weiterhin nicht, ob die Stelle
richtig gewählt wurde. Kein Validator unterscheidet ein umbenanntes Flag von einem neuen; das
bleibt Urteil, und dieser Eintrag existiert, weil das Urteil eine Anleitung braucht, keinen
Automaten.
Berührt: instructions/dev/version-parts.md (neu), instructions/dev/stack-dev/SKILL.md,
tools/chemenu/version.py, tools/chemenu/commands/version_cmd.py,
tools/chemenu/commands/docs_verify.py, tools/CONTRACT.md, INSTALL.md. Sechs neue Tests.
2.4.1 - 2026-09-02 - INSTALL-MCP.md: Menschendoku fuer den Leseserver, und sie wird mit ausgeliefert
Author: Torben Nehmer
Nachtrag zu 2.4.0. Der Server war ausgeliefert und für Menschen nur über tools/CONTRACT.md und
eine Instruktion beschrieben — beide für Agenten geschrieben. INSTALL-MCP.md schließt das,
neben INSTALL.md und in derselben Form: Voraussetzungen, sechs Schritte, Verifikation,
Troubleshooting.
Was drinsteht, das sonst nirgends zusammen stand: die Client-Konfiguration als JSON-Block;
dass WIKI_TRACE=0 nicht optional ist und wie die Startverweigerung aussieht; dass
--host 0.0.0.0 bewusst nicht der Default ist; und der Verweis auf die
Traefik-ForwardAuth-Middleware unter https://gitea.nehmer.net/torben/gitea-mcp-forward-auth
samt dem, was sie erwartet (Bearer-Token gegen SHA-256-Hashes, AUTH_PROXY_TOKEN_HASHES_DIR).
Der Verifikationsabschnitt ist ein lauffähiges Skript — wörtlich so ausgeführt, bevor es hier
landete.
dist export liefert die Datei mit. ROOT_FILES ist eine Allowlist, also hätte sie sonst
keine Instanz erreicht: ein ausgelieferter Server, dessen Bedienungsanleitung im Ursprungs-Repo
zurückbleibt, ist einer, den der Empfänger durch Quelltextlesen findet. INSTALL.md bekommt
zusätzlich CHEMENU_ROOT und WIKI_TRACE/WIKI_TRACE_DIR in seine Konfigurationstabelle —
beide waren seit 2.3.0 bzw. seit jeher wirksam und dort nicht aufgeführt.
Dateien: INSTALL-MCP.md (neu), INSTALL.md, README.md,
tools/chemenu/commands/dist_cmd.py.
2.4.0 - 2026-09-02 - MCP-Leseserver: zweiter Konsument auf demselben Kern
Author: Torben Nehmer
Letzter Schritt der Sequenz aus #36, inhaltlich Issue #19. chemenu bekommt einen zweiten
Konsumenten: search, types, describe_type, lint und status über MCP. Kein CLI mit
angeschraubter Netzwerkschnittstelle — CLI und Server sind zwei Adapter auf dem Kern, den 2.3.0
freigelegt hat.
tools/chemenu/mcp/, im Repo statt als eigenes Artefakt. Der Golden-Test, der die
Serverantworten gegen die --json-Formen der CLI hält, läuft nur mit beiden Seiten in einer
Testsuite; getrennt würde aus einem Contract eine Versionsabsprache. Der Test ruft wikitool als
Subprozess gegen denselben Baum auf, über CHEMENU_ROOT — womit er nebenbei die Root-Auflösung
von außen mitprüft.
Zwei Transports. stdio zum Entwickeln und Testen ohne Netz, streamable-http für die
Auslieferung — der einzige, vor den sich die Authentifizierungs-Middleware überhaupt setzen kann,
weil sie ein HTTP-Reverse-Proxy ist. sse ist über das SDK erreichbar und wird bewusst nicht
angeboten: der abgelöste Remote-Transport, jetzt darauf zu bauen verschiebt den Wechsel nur.
--host/--port gibt es, weil der Default auf Loopback bindet und ein Container hinter einem
Proxy eine Adresse braucht, die der Proxy erreicht — eine Eigenschaft der Software, nicht einer
Installation. Beide Transports sind gegen den echten Korpus gegengeprüft.
Kein Schreibpfad, strukturell. Weder der Server noch chemenu.api importiert irgendetwas
unter chemenu.commands, also existieren new, touch, xref, cite, publish, migrate
und version bump in dieser Reichweite gar nicht, statt aus einer Liste gefiltert zu werden. Ein
Test importiert das Servermodul in einem frischen Interpreter und sieht in sys.modules nach;
ein zweiter ruft alle fünf Tools auf und vergleicht den Dateibaum, HEAD und
git status --porcelain vorher/nachher.
Jede Antwort trägt ihren Commit. commit und as_of in jedem Payload; null heißt, der
bediente Baum hat uncommittete Änderungen und die Antwort entspricht keiner Revision. Der Stempel
ist die Revision, aus der die Seiten tatsächlich gelesen wurden — zwischen Laden und Stempeln
kann der Baum sich bewegen, deshalb reicht der Ladepfad seine Revision durch, statt noch einmal
zu fragen. Das war beim ersten Durchlauf falsch: types/lint/status lasen die zuletzt
gecachte Revision und stempelten null, obwohl der Baum sauber war.
Telemetrie in den bedienten Baum wird beim Start verweigert, nicht stillschweigend
umgeleitet. Tracing ist per Default an und schreibt nach reports/telemetry/ im Repo — genau das
Verzeichnis, das der Sync per git reset --hard wegräumen darf. WIKI_TRACE=0 oder
WIKI_TRACE_DIR außerhalb des Korpus. Heute schreibt auf diesem Pfad nichts (der Emitter hängt an
cli.main() und den Gates), die Sperre ist gegen später.
Fehler an der Protokollgrenze. Ein ChemenuError wird zum ToolError des SDK — eine
absichtliche Ablehnung, deren Text den Aufrufer erreicht. Alles andere bleibt ein Absturz, dessen
Text auf dem Server bleibt. Ein kaputtes Prädikat ist das Argument des Aufrufers, also muss die
Zeile mitreisen, die sagt, was stattdessen zu schreiben ist.
Bewusst nicht enthalten: Authentifizierung und Rate Limiting (Middleware vor dem Prozess), Deployment (private Infrastruktur), der Iteration Budget Gate — er begrenzt eine Agenten-Session und nicht einen Nutzer, weshalb Retrieval von ihm befreit ist; ihn hier als Rate Limiter zu benutzen würde ihn dazu verwässern.
Die Abhängigkeit ist optional (tools/requirements-mcp.txt): eine Instanz, die nur die CLI
benutzt, soll dafür nicht pydantic, starlette, uvicorn und cryptography installieren müssen. CI
installiert sie, denn ein übersprungener Golden-Test ist genau der Weg, auf dem Server und CLI
unbemerkt auseinanderlaufen.
Betrieb und Sync-Mechanismus: instructions/mcp-read-server.md.
Polling (git fetch && git reset --hard) statt Webhook — kein eingehender Endpunkt, keine
Signaturprüfung. reset --hard ist dort tragend und keine Bequemlichkeit: ein abgedrifteter Baum
antwortet zwar richtig, parst aber bei jeder Anfrage neu und stempelt jede Antwort mit null.
Dateien: chemenu/mcp/ (neu: server.py, __main__.py), chemenu/api.py,
tools/requirements-mcp.txt (neu), instructions/mcp-read-server.md (neu), tools/CONTRACT.md,
tools/README.md, .gitea/workflows/ci.yml, tests/test_mcp_server.py (neu).
2.3.0 - 2026-09-02 - Bibliotheksgrenze: chemenu laesst sich auf einen Korpus zeigen
Author: Torben Nehmer
Dritter Schritt der Sequenz aus #36, inhaltlich Issue #31. Der Schritt, der chemenu von einem
Skript, das in seinem eigenen Verzeichnis lebt, zu einer Bibliothek macht, auf die man einen
Korpus zeigen kann. Rückwärtskompatibel: ohne gesetzte Variable verhält sich tools/wikitool
unverändert.
Root-Auflösung nach Präzedenz statt nach Dateilage. config.resolve_root(): expliziter
Parameter → $CHEMENU_ROOT → Walk-up wie bisher. Der Walk-up bleibt Default. Wichtiger als die
neue Fähigkeit ist die beseitigte Fehlerklasse: ROOT und alles darunter waren
Modulkonstanten, also zur Importzeit gebunden, und
monkeypatch.setattr(config, "ROOT", ziel) zeigte ROOT um, aber nicht KB_DIR/RAW_DIR. Wer
sich darauf verließ, baute etwas, das scheinbar auf einem Zielbaum arbeitete und in Wahrheit aus
dem Entwickler-Checkout antwortete. Die abgeleiteten Pfade werden jetzt bei jedem Zugriff
aufgelöst (PEP 562 __getattr__) und folgen ROOT — der halb-umgezeigte Zustand ist nicht mehr
konstruierbar. CHEMENU_ROOT ist in _WIKITOOL_ENV registriert, #23-konform.
config.reset() und config.rooted(). reset() nimmt Zuweisungen auf die verwalteten
Pfadnamen zurück; die Testsuite ruft es zwischen Tests, und das ist dort nicht optional:
monkeypatch merkt sich den alten Wert, indem es ihn liest — also auflöst — und schreibt ihn
beim Aufräumen als echtes Attribut zurück. Genau die stale Bindung, die der Umbau unmöglich
machen sollte, vom Cleanup wieder aufgebaut. rooted(root) setzt den Root für die Dauer eines
Blocks, prozessweit und damit nicht thread-sicher — der Aufrufer hält das Lock, dieselbe
Disziplin wie beim Korpus-Cache. Nötig, weil nicht alles einen Root als Argument nimmt: der
modulglobale TypeResolver muss types/ finden, und ohne ihn läse ein fremder Korpus mit den
Type-Specs dieses Checkouts.
Die Naht ist gezogen. run_search/run_lint/types lagen in Modulen, die typer auf
Modulebene importieren und über _util auch rich — wer sie in-process aufrief, zog den
kompletten CLI-Kopf mit. Der reine Kern liegt jetzt in search/service.py, lint_core.py und
types_core.py; commands/ sind die Terminal-Adapter darüber und re-exportieren die Namen, damit
kein bestehender Import bricht. Ein Test importiert chemenu.api in einem frischen Interpreter
und prüft, dass weder chemenu.commands.* noch typer/rich/click geladen werden.
chemenu.api.Corpus als In-Process-Einstieg. Nimmt einen Root, liefert exakt die
--json-Formen der CLI zurück — ein Wire-Contract statt zwei — und stempelt jede Antwort mit dem
Commit-SHA und einem Zeitstempel (commit, as_of), so dass aus einer stillen veralteten Antwort
eine sichtbare wird. search/lint/types/describe_type/status; status ist bewusst
serverseitig komponiert und kein Wrapper, weil es kein wikitool status gibt. Lesend
strukturell: nichts unter chemenu.commands wird importiert, die Schreibfunktionen existieren in
dieser Oberfläche also gar nicht, statt gefiltert zu werden. Das ist die Grenze, auf der #19
aufsetzt.
Exceptions statt Exit-Codes an der Grenze. chemenu/errors.py: ChemenuError mit
ValidationError (Eingabe abgelehnt) und BackendError (Abhängigkeit fehlt oder scheitert).
PredicateError, FrontmatterError, UnknownBackend und die beiden Ripgrep* hängen jetzt
darunter; ValidationError erbt zusätzlich von ValueError, weil PredicateError vorher eines
war und Aufrufer es so fangen. Das CLI-Verhalten ist unverändert: fail() → ERROR-Zeile,
Exit 1, Budget-Refund.
resolve() reicht den Root an das Backend durch. Vorher konnte ein Aufrufer run_search einen
Korpus übergeben, während RipgrepBackend weiter config.KB_DIR durchlief — die Anfrage aus dem
einen Baum beantwortet, die Seiten aus dem anderen gelesen, ohne dass irgendetwas das gesagt
hätte.
Zwei Abhängigkeiten, die durch Zufall hielten, stehen jetzt da. TypeResolver.repo_root
folgt ROOT, statt beim Import zu binden — womit Fixtures, die ROOT auf einen tmp-Baum zeigen,
die mitgelieferten Type-Specs ausdrücklich benennen müssen (use_shipped_type_specs). Dieselbe
Form wie das Loch, für das raw_dir geschrieben wurde, eine Ebene tiefer. Und
types describe --json trug root: nicht im Payload, obwohl types list --json es tut:
types/instruction.md deklariert root: repo, die Renderer-Zeile las es direkt aus dem
Frontmatter. Jetzt im Payload, in derselben Form wie bei list.
Dateien: config.py, errors.py (neu), api.py (neu), search/service.py (neu),
lint_core.py (neu), types_core.py (neu), search/registry.py, type_resolver.py,
commands/search.py, commands/lint.py, commands/types_cmd.py, tools/CONTRACT.md, dazu
tests/conftest.py, tests/test_api.py (neu), tests/test_new_page.py,
tests/test_instructions_cmd.py.
2.2.4 - 2026-09-02 - Haertung des Lesepfads: ReDoS, Subprozess-Timeout, YAML-Alias-Budget, Korpus-Cache
Author: Torben Nehmer
Zweiter Schritt der Sequenz aus #36, inhaltlich Issue #33. Limits vor der Exposition: der Lesepfad bekommt mit dem MCP-Server (#19) einen Konsumenten, der nicht der Operator ist. Alle sechs Befunde waren reproduziert, nicht vermutet; jeder hat jetzt eine Regression.
ReDoS über --regex beseitigt. search/ripgrep.py:_contains gab nutzergesteuerten Regex an
Pythons Backtracking-Engine: (\w+\s?)+$ gegen 114 Zeichen gewöhnlichen Seiteninhalts terminiert
nicht in acht Sekunden, ein deterministisch scheiterndes Muster braucht 0,2 ms — der Unterschied
ist das Muster, nicht der Heuhaufen. build_hit ruft die Funktion je Treffer zweimal auf, und
\w matcht jede Seite: eine Anfrage kaufte zwei unbegrenzte Suchen pro Korpusseite. Der Zweig ist
gelöscht, nicht begrenzt. rg hat das Muster mit einer linearen Engine längst angewendet, wenn
die Funktion überhaupt läuft; verloren geht nur der zusätzliche Titel-/Summary-Bonus für
nicht-literale Muster, und Summary wie H1 sind selbst Zeilen in der Datei, die rg zählt.
Subprozess-Timeout. rg wird nach 30 s abgeräumt und über den vorhandenen
RipgrepFailed-Pfad gemeldet. Kein Performance-Budget — eine Fixed-String-Suche kostet hier 6 ms —
sondern ein Hänger-Abbruch, damit ein Aufruf als Fehler endet statt den Aufrufer offenzuhalten,
während seine Ausgabe in den Heap puffert.
YAML-Anchors und -Aliases im Frontmatter werden verweigert, nicht budgetiert. Gemessen:
267 Byte werden in 0,2 ms zu 672.603 Knoten beim Traversal, Wachstum 9ⁿ bei konstanter Parse-Zeit
— ein Größenlimit fasst das nicht an, weil die Eingabe klein bleibt. Die Prüfung läuft auf dem
Event-Strom (yaml.parse), der nichts auflöst, kostet also O(Text) und löst nie aus, wonach sie
fragt; * ist in jedem Alias-Knoten notwendig, seine Abwesenheit beweist Abwesenheit ohne jeden
Parse — der Weg, den jede echte Seite nimmt. Dazu ein Größenlimit von 64 KiB und ein Abfangen von
RecursionError (PyYAML komponiert rekursiv, tiefe Verschachtelung ist kein YAMLError). Heute
nicht erreichbar, weil kb/ der Operator committet; erreichbar mit der Ingest-Queue (#32).
CSafeLoader statt SafeLoader, mit Fallback. Gemessen über diesen Korpus (176 Seiten,
best of 5): 265 ms → 54 ms. Kein Mikro-Tuning — der Korpus-Parse war der größte Einzelposten
eines search-Aufrufs und skaliert linear mit der Korpusgröße. End-to-end fällt ein
wikitool search damit von 593 ms auf 347 ms; die verbleibenden 262 ms sind Modulimport und
entfallen erst im residenten Prozess (#19).
Stiller Frontmatter-Verlust wird gemeldet. Entschieden: der Lesepfad nennt die Seite, statt
sie zu schlucken. Kaputtes YAML wird weiterhin zu {} — Massenoperationen dürfen an einer Seite
nicht scheitern —, aber der Grund wird mitgeführt (Page.frontmatter_error) und ausgegeben:
search --json trägt immer eine unreadable-Liste aus {path, reason}, die Tabellenform
schreibt dieselben Zeilen nach stderr. Das war nötig, weil so eine Seite weder confidence noch
kind hat und damit aus jedem positiven --field-Prädikat fällt — ausgerechnet aus der
Low-Confidence-Suche, die Seiten in genau diesem Zustand finden soll — und dabei aussieht wie eine
Seite, die nicht gematcht hat. Für Frontmatter, das diese Instanz nicht selbst geschrieben hat,
steht read_page_strict() bereit: die Quarantäne aus #32 muss strikt lesen, wo ein
unlesbares Dokument das Dokument stoppen und nicht leeren soll.
Ein Parser statt zwei. read_page() und frontmatter_error() liefen bisher getrennt durch
safe_load — so konnte der permissive Weg zu {} degradieren aus einem Grund, den der strikte
Weg anders beschrieb, und jeder Aufrufer, der beide Antworten wollte, las die Datei zweimal.
Beide gehen jetzt durch _load_frontmatter(); ein Test hält sie gegeneinander.
Korpus-Cache am Commit-SHA (chemenu/corpus_cache.py), als Objekt, das ein Aufrufer hält —
kein Modul-Dict, das sich hinter allen einschaltet. Die CLI hält keins und verhält sich unverändert
(ein Aufruf pro Prozess, nichts wiederzuverwenden); der residente Prozess aus #19 hält eins.
Entscheidend ist nicht die Geschwindigkeit, sondern dass nichts veraltet: ein schmutziger
Arbeitsbaum wird nie gecacht, sonst bekäme eine Sitzung, die eine Seite schreibt und danach
sucht, die Analyse von vor dem Schreiben — bei unverändertem SHA. Kann git nicht antworten, gilt
der Baum als schmutzig. Derselbe SHA ist der Antwort-Stempel aus #19, per Konstruktion also die
Revision, aus der die Antwort auch wirklich berechnet wurde.
Erhalten geblieben ist die Eigenschaft, die kein Befund war: kein shell=True,
--fixed-strings als Default, ---Terminator. Der Modul-Docstring führt sie jetzt als drei
tragende Zusagen statt zwei — die dritte ist, dass nutzergesteuerte Muster ausschließlich rg
sieht.
Dateien: frontmatter_io.py, search/ripgrep.py, commands/search.py, page.py,
corpus_cache.py (neu), tools/CONTRACT.md, dazu tests/test_frontmatter_io.py,
tests/test_search.py, tests/test_corpus_cache.py (neu).
2.2.3 - 2026-09-02 - Publish-Remote-Gate in diesem Checkout scharf, doctor benennt den Zustand
Author: Torben Nehmer
Erster Schritt der Sequenz aus Issue #36 (Weg zum MCP-Leseserver), inhaltlich Issue #34. Das
Gate war seit 2.2.0 implementiert und in diesem Checkout inert: .wikitool-remotes.json
fehlte, und eine fehlende Datei heißt unbeschränkt. ENVIRONMENT.md beschrieb origin
gleichzeitig als einziges Publish-Ziel, AGENTS.md führt das Gate unter den drei in Code
erzwungenen Grenzen. Eine dokumentierte Sicherung, die nicht greift, ist schlechter als eine
fehlende — sie erzeugt genau das Vertrauen, das sie nicht verdient.
Die Datei ist angelegt (gitignored, per Checkout, reist nicht mit) und gegengeprüft: ein
publish --remote auf ein nicht gelistetes Ziel verweigert mit Exit 42, bevor irgendetwas
gestaged wird, und der Arbeitsbaum bleibt unberührt. Damit steht die Sicherung vor dem Klonen
der privaten Instanz (#30) — nachträglich gesetzt ließe sie genau das Fenster offen, das sie
schließt.
doctor sagt jetzt, ob das Gate scharf ist, nicht nur ob die Datei da ist. Alle drei
Zustände beginnen mit Gate armed: bzw. Gate not armed:; der einzelne Remote ohne Allowlist
bleibt OK (er hat nichts zu schützen, und ein FAIL machte die Datei durch die Hintertür
verpflichtend), sagt aber ausdrücklich, dass jedes Push-Ziel durchkommt. Der Fall, der wirklich
beißt — mehrere Remotes ohne Allowlist — bleibt WARN. Der Check hatte bislang keine Tests;
drei sind dazugekommen, einer je Zustand.
Dateien: .wikitool-remotes.json (neu, nicht committet), doctor.check_publish_remotes(),
tools/chemenu/tests/test_doctor.py.
2.2.2 - 2026-09-01 - Doku-Verdrahtung: Publish-Remote Gate im Werkzeugvertrag, Projektseite auf oeffentlich
Author: Torben Nehmer
Nachziehen dessen, was 2.2.0 und die Veröffentlichung offen gelassen haben. Gefunden durch eine
Durchsicht auf lose Enden, nicht durch einen Fehlschlag — docs verify deckt den Fall nicht ab,
weil es Kommando-Namen gegeneinander prüft, nicht ob ein neuer Fehlerfall beschrieben ist.
tools/CONTRACT.md kannte das Publish-Remote-Gate nicht. Der Werkzeugvertrag ist die Stelle,
an der pro Kommando steht, was ein Fehlschlag bedeutet und ob ein Retry sicher ist — und
publish hatte seit 2.2.0 einen dritten Weg zu Exit 42, der dort nirgends stand. Ergänzt in
beiden Richtungen: in der Kommandozeile (URL statt Remote-Name, pushurl wird gelesen, fehlende
Datei heißt unbeschränkt, kaputte Datei ist ein Fehler) und im Fehlerkontrakt, wo der
entscheidende Unterschied zu den anderen beiden Gates steht — es gibt keine --confirm-Zeile,
die der Agent nachreichen könnte.
instructions/gates.md verwies nicht auf die Prozedur, für die das Gate gebaut wurde.
Jetzt verlinkt, mit dem Hinweis auf Schritt 4: Das Gate gehört vor den ersten publish, später
hinzugefügt schützt es das Fenster nicht, das es schließen soll.
Die Projektseite beschrieb sich selbst falsch. kb/entities/projects/Chemenu.md nannte
Chemenu ein „persönliches IT-Wissens-Wiki" mit dem Zweck „Persönliche IT-Wissensbasis" — seit
der Veröffentlichung schlicht unzutreffend, und es ist die Seite, die ein Fremder über das
Projekt liest. Neu gefasst: deterministischer Wissenskompiler, diese Instanz seit 2026-09-01
öffentlich als Testbett und Demo, Lizenz benannt.
Die historische Aussage über die monolithische AGENTS.md (~30 KB) bleibt — sie war zu ihrer
Zeit korrekt und ist belegt. Sie ist jetzt als Ausgangspunkt datiert statt als Gegenwart
formuliert, dieselbe Unterscheidung, die #29 für die Issue-Texte trifft: ein Pfad als Wegweiser
wird nachgezogen, ein Pfad als Beleg bleibt stehen und bekommt ein Datum.
Dateien: tools/CONTRACT.md, instructions/gates.md,
kb/entities/projects/Chemenu.md, kb/entities/INDEX.md (generiert).
2.2.1 - 2026-09-01 - private-instance: der Demo-Korpus wandert beim Merge doch mit - Prozedur korrigiert
Author: Torben Nehmer
instructions/private-instance.md behauptete in 2.2.0, ein git merge upstream/main löse
Änderungen am Demo-Korpus stillschweigend auf, weil die private Instanz ihn einmal gelöscht hat:
deleted-in-ours, unmodified-in-theirs. Das war nicht gemessen, sondern angenommen — und es
ist falsch. Ein Nachbau mit einem Upstream, der seinen Korpus bewegt, zeigt drei verschiedene
Verhalten:
| Upstream tut | git merge upstream/main tut |
|---|---|
| ändert eine Seite, die du gelöscht hast | CONFLICT (modify/delete) — und lässt die Upstream-Fassung im Arbeitsbaum liegen. Ein git add -A beim Auflösen holt die Demo-Seite zurück. |
| legt eine neue Seite an | staged sie stillschweigend. Kein Konflikt, keine Meldung. |
| löscht eine Seite, die du auch gelöscht hast | nichts. Der einzige harmlose Fall. |
Die mittlere Zeile ist die gefährliche, weil nichts sie ankündigt. Ein Upstream, der einen
Demo-Korpus ausliefert und ihn als Testbett benutzt, legt Seiten an — und jede einzelne
landet in der privaten Instanz und taucht dort in lint, index, search und
confidence decay auf. Genau diese Doppelnutzung beschreibt Issue #28.
Korrigiert: Die Update-Prozedur hält den Merge mit --no-commit offen, zwingt kb/ und
raw/ danach auf den eigenen Stand zurück (git rm --cached, rm -rf, git checkout HEAD --)
und schließt ihn erst dann. Solange der Merge offen ist, zeigt HEAD noch auf den Commit davor,
und genau das macht den Schritt sauber. Anschließend eine Kontrolle, die man nicht überlesen
kann:
git diff --name-only $BEFORE HEAD -- kb raw # muss leer sein
Das Rezept ist wörtlich so nachgespielt worden, wie es jetzt in der Datei steht — mit einem
Upstream, der gleichzeitig eine Seite ändert, eine anlegt, eine löscht und dasselbe unter
raw/ tut. Ergebnis: Stack aktualisiert, nur eigener Inhalt übrig, Kontrolle leer,
Arbeitsbaum sauber.
Auch die Decision Points korrigiert. „Konflikt in kb/ per Hand als keep deleted
auflösen" stand vorher da und ist der Rat, der in die Falle führt: git add -A committet die
Fassung, die git im Arbeitsbaum liegen gelassen hat.
Dateien: instructions/private-instance.md.
2.2.0 - 2026-09-01 - Publish-Remote Gate: publish schreibt nur an erklaerte Ziele
Author: Torben Nehmer
Der Stack bekommt sein drittes Gate. Die beiden bestehenden fragen, ob eine Änderung zu groß ist und ob ein Rebase gefährlich ist. Dieses fragt, was darunter liegt: ob das überhaupt das richtige Repository ist.
Das Problem entsteht erst durch die private Instanz. Ein Checkout mit eigenem Inhalt hat
typischerweise zwei Remotes — sein eigenes und das öffentliche Upstream, von dem er
Stack-Updates zieht. Git unterscheidet die beim Push nicht, also legt ein falsches --remote
einen privaten Korpus auf ein öffentliches Repository. Das ist nicht billig rückholbar, und
zwar nachweislich: Beim Veröffentlichen dieses Repos blieb die gesamte alte History nach dem
Force-Push per SHA abrufbar, bis auf dem Server die Reflogs verfielen und git gc --prune=now
lief. Ein Force-Push bewegt den Branch, nicht die Objekte.
.wikitool-remotes.json nennt die erlaubten Push-URLs. Nicht die Remote-Namen: Eine
Namensliste ließe ein publish durch, dessen origin umgebogen wurde, und genau das ist der
Fall, den das Gate fangen soll. Gelesen wird die pushurl, wenn der Remote eine setzt, denn
dorthin schreibt git push tatsächlich.
Pro Checkout und gitignored, aus demselben Grund wie ENVIRONMENT.md: Zwei Klone pushen an
zwei verschiedene Orte, eine committete Kopie würde einem privaten Klon also mitteilen, das
öffentliche Upstream sei ein legitimes Ziel für seinen eigenen Inhalt. Fehlt die Datei, gilt
keine Beschränkung — ein Checkout mit einem Remote und ohne Privates hat nichts zu schützen,
und eine Pflichtdatei würde aus einer Sicherung Papierkram machen. Eine kaputte Datei ist
dagegen ein Fehler und kein „keine Beschränkung": Eine beschädigte Sicherung darf sich nicht
wie eine abgeschaltete verhalten.
Kein Token, keine Flagge. Die anderen beiden Gates lösen sich mit einem --confirm <token>,
weil ihre Frage („ist diese Änderung richtig?") für genau ein Changeset beantwortbar ist. Dieses
fragt „gehört dieser Inhalt in jenes Repository?", und das ist eine stehende Eigenschaft des
Checkouts, kein Einzelfallurteil. Der Weg daran vorbei ist ein bewusster Edit des Nutzers.
Ein Agent, der die Datei anfasst, um an einer Verweigerung vorbeizukommen, öffnet ein Gate aus
eigenem Antrieb — Invariante 6.
doctor meldet den Zustand statt ihn zu erzwingen: OK mit Anzahl der Ziele, OK bei
Abwesenheit mit einem Remote, und WARN bei mehr als einem Remote ohne Allowlist — also genau in
der Form, die eine private Instanz annimmt, sobald sie das Upstream hinzufügt.
Und die Prozedur, für die das Gate gebaut wurde. instructions/private-instance.md (neu)
beschreibt die dritte Instanz-Form neben „frisch aufsetzen" und „Repo klonen": eine private
Arbeitsinstanz, die Stack-Updates von einem öffentlichen Upstream per git merge zieht und
deren eigener Inhalt nie zurückwandert. Der Grund, warum das dem Tarball-Weg vorzuziehen ist,
steht dort ausformuliert — cp -r hat keinen Drei-Wege-Merge und keine Konflikterkennung.
Schritt 4 der Anleitung ist das Gate, und zwar ausdrücklich vor dem ersten publish:
später hinzugefügt schützt es das Fenster nicht, das es schließen soll.
Dateien: config.PUBLISH_REMOTES_FILENAME, git_publish.read_allowed_push_urls(),
push_url_for(), publish_remote_refusal() und die Prüfung vor dem Reconcile-Schritt,
doctor.check_publish_remotes(), .gitignore, instructions/gates.md,
instructions/private-instance.md (neu), AGENTS.md (Gate-Liste und Bootstrap-Routing),
12 neue Tests in test_git_publish.py.
2.1.1 - 2026-09-01 - raw_dir-Fixture kappt config.ROOT; letzte private Fixture-Namen ersetzt
Author: Torben Nehmer
Der erste CI-Lauf nach der Korpus-Bereinigung wurde rot, während dieselbe Suite lokal grün
blieb: test_legacy_source_pages_flags_url_and_directory scheiterte mit
KeyError: 'Source - DirBacked'.
Die Ursache ist keine Testschwäche, sondern eine Umgebungsabhängigkeit. Der Test legt eine
Quellenseite mit source: raw/documents an und erwartet, dass legacy_source_pages() sie als
„directory, not a file" meldet. Die Prüfung dort ist (config.ROOT / legacy).is_dir() — sie
löst also gegen das echte Repo-Wurzelverzeichnis auf, während die raw_dir-Fixture ihren
Baum unter tmp_path baut. Der Test war nie hermetisch; er bestand, weil dieser Checkout
zufällig ein raw/documents/ besaß.
Die Korpus-Bereinigung hat dieses Verzeichnis geleert. Git verfolgt keine leeren Verzeichnisse, also verschwand es im CI-Checkout und blieb im lokalen Arbeitsbaum stehen — grün hier, rot dort, ohne dass sich am Test etwas geändert hätte.
Geschlossen in der Fixture, nicht im Test. raw_dir setzt jetzt config.ROOT auf
tmp_path. Das ist dieselbe Bauart und dasselbe Argument wie bei hermetic_environment
(Gitea #8): Wenn eine Umgebungsabhängigkeit lokal unsichtbar ist, gehört sie an der Stelle
geschlossen, die alle Tests erreicht, statt bei dem einen, der zufällig darüber stolpert.
Nachgewiesen, indem raw/documents/ lokal entfernt und die Suite erneut gefahren wurde — 38
Provenance-Tests grün ohne das Verzeichnis, vorher rot.
Fixture-Namen. Die Suite benutzte weiterhin reale Systemnamen der Ursprungsinstanz als
Fixture-Bezeichner. In einem
öffentlichen Repo beschreiben sie nichts, verraten aber die Namensgebung einer privaten
Umgebung. Ersetzt durch aurora, almanac, Borealis, gateway, docstore. gdeploy
bleibt: die Seite existiert im öffentlichen Korpus.
Dateien: tools/chemenu/tests/conftest.py (raw_dir), sämtliche Testdateien mit
Fixture-Namen.
2.1.0 - 2026-09-01 - Dual-Lizenz AGPL-3.0/CC-BY-4.0, und ein Leak-Canary fuer dist export
Author: Torben Nehmer
Das Repo hatte keine Lizenz. Für ein Repo, das privat auf der eigenen Gitea liegt, ist das folgenlos; für eines, das veröffentlicht werden soll, hebt es den Zweck der Veröffentlichung auf — ohne Lizenz gilt „alle Rechte vorbehalten", und niemand darf eine Instanz aufsetzen.
Zwei Hälften, zwei Lizenzen. LICENSE trägt die AGPL-3.0 und gilt für den Stack
(tools/, types/); LICENSE-CONTENT trägt CC-BY-4.0 und gilt für die Inhalte (kb/,
raw/, instructions/, die CONTRACT.md-Ebene, die Prosa im Wurzelverzeichnis). Die
Aufteilung folgt dem Vorbild der vendorten Commonplace-Basis, kehrt aber deren Dateinamen um:
Die Copyleft-Hälfte heißt LICENSE, weil das die Lizenz ist, die eine Forge für das Repository
meldet — und wer eine Copyleft-Pflicht übersieht, wird dadurch geschädigt, wer eine zu viel
sieht, nicht.
Die Affero-Variante statt der einfachen GPL, weil Issue #19 den Stack als MCP-Frontend erreichbar machen will. Bei der GPL schuldet nichts, wer nur einen Dienst betreibt statt Software zu verteilen; genau dieser Weg ist der, den dieses Projekt einschlägt.
Die Grenze wird nicht aufgeschrieben. Welche Datei zu welcher Hälfte gehört, ist der
Dateiplan, den dist export ohnehin berechnet. NOTICE verweist darauf, statt eine zweite
Pfadliste anzulegen, die drifted (Invariante 8). NOTICE trägt außerdem die Attribution für
commonplace (zby, CC-BY-4.0 Inhalte + MIT Code), die CC-BY verlangt.
dist export liefert die Lizenzen mit — und weigert sich sonst. Die drei Dateien stehen
jetzt in ROOT_FILES, aber nicht nur dort: REQUIRED_ROOT_FILES macht sie zur Bedingung.
Jeder andere ROOT_FILES-Eintrag wird if source.is_file() kopiert und sonst stillschweigend
übersprungen — richtig für INSTALL.md, das es lange nicht gab, und falsch für eine Lizenz:
Ein Export, der sie weglässt, übergibt einen AGPL-tools/-Baum ohne Lizenztext, und das ist
eine Verletzung, sobald die empfangende Instanz irgendwo öffentlich landet.
Der Leak-Canary. find_leaks() prüft den fertigen Plan gegen das, was er nie enthalten
darf: eine ausgefüllte USER.md/SOUL.md/ENVIRONMENT.md, etwas unter instructions/dev/,
eine kb/-Seite oder eine raw/-Quelle. Drei getrennte Allowlists entscheiden bisher, was
build_plan kopiert, und jede hält nur, solange sich beim Editieren jemand an die Regel
erinnert hat — nichts prüfte das Ergebnis nach. export bricht jetzt ab, wenn der Plan etwas
davon trägt, mit dem Hinweis, die Allowlist zu reparieren statt Dateien aus dem Ziel zu löschen.
Die Prüfung ist bewusst strukturell und nicht textbasiert. Ein Muster-Scan auf Hostnamen
oder IP-Literale wurde erwogen und verworfen: Der eigene Host steht legitim in INSTALL.md und
version.py, ein solcher Scan müsste also genau die Zeichenkette ausnehmen, nach der er sucht —
oder bei jedem Export Fehlalarm geben.
Beispiele neutralisiert. Hilfetexte, Docstrings und Contract-Beispiele nannten reale Quellenseiten und Pfade der Ursprungsinstanz. In einer Distribution ist das nicht nur unpassend, sondern verrät den Inhalt der Instanz, die sie gebaut hat. Ersetzt durch Beispiele aus dem verbleibenden öffentlichen Korpus.
Dateien: LICENSE, LICENSE-CONTENT, NOTICE (neu), config.LICENSE_FILES,
dist_cmd.REQUIRED_ROOT_FILES, dist_cmd.find_leaks(), Lizenzabschnitt in README.md,
sechs neue Tests in test_dist_cmd.py; Beispiel-Neutralisierung in cite_cmd.py,
page_ops.py, work_cmd.py, provenance.py, search/ripgrep.py, work/CONTRACT.md,
instructions/session-setup.md und zwei Testdateien.
2.0.0 - 2026-09-01 - Rebranding zu Chemenu: Repo, Produktname und Python-Paket
Author: Torben Nehmer
Migration: none required - Der Bruch liegt im Update-Pfad und im Import-Namen, nicht im Korpus: kb/ behaelt Schema und Shape 1.0.0, keine Seite und kein Frontmatter-Feld aendert sich. Was eine bestehende Instanz tun muss, steht im Eintrag - Release-Feed umstellen, Paketimport nachziehen -, und beides ist Instanz-Konfiguration, keine Inhaltsmigration.
Der Stack hieß llm-wiki-test1, und das war nie ein Name, sondern ein Platzhalter mit einer
Ordnungszahl darin. Gitea-Issue #3 setzt Chemenu an seine Stelle - der altägyptische Name
von Hermopolis Magna, Hauptkultort des Thoth und „Stadt der Acht" der Ogdoade. Der Ort, nicht
der Gott: die Persona heißt weiterhin Thoth, und der Stack ist das, worin sie schreibt.
Was den Namen trägt. Repo (torben/chemenu), Produktname in README.md, AGENTS.md,
.gitignore, types/type-spec.md und tools/CONTRACT.md, das Release-Artefakt
(chemenu-stack-<version>.tar.gz statt llm-wiki-stack-…), der Release-Feed in
version.DEFAULT_UPDATE_URL, und die Download- und Issue-URLs in INSTALL.md und
instructions/dev/issue-tracking.md. Die eine Nennung von „LLM Wiki", die stehen bleibt, ist
die Attribution an Karpathys ursprüngliches Gist in README.md - das ist die Herkunft der
Idee, nicht der Name dieses Stacks.
Das Python-Paket heißt jetzt chemenu, nicht mehr wiki_tools. Der Import-Name eines
Pakets ist ein flacher globaler Namensraum ohne Kollisionsschutz, und wiki_tools ist genau
die Sorte generischer Name, die dort nichts verloren hat - unabhängig davon, ob je etwas
veröffentlicht wird, denn es reicht, dass ein zweites wiki_tools auf demselben sys.path
liegt. Betroffen sind 93 Dateien unter tools/chemenu/ plus pytest.ini, .coveragerc, der
tools/wikitool-Shim und die Pfadangaben in EVALS.md, instructions/dev/, types/*.md und
kb/CONTRACT.md. dist_cmd.py brauchte nichts: es läuft tools/ als Baum ab und nennt das
Paket nirgends beim Namen.
Das Kommando bleibt wikitool, und mit ihm .wikitool-release.json, .wikitool-kb.json
und die WIKITOOL_*-Variablen. Distributionsname, Import-Name und Kommandoname sind drei
unabhängige Dinge, und ein abweichender Kommandoname ist verbreitete Praxis (httpie →
http). Was der Stack nach außen anbietet, heißt weiter so, wie es tut, was es tut.
Warum das trotzdem MAJOR ist. Der erste Anlauf stand auf 1.9.0, mit dem Argument, dass
kein Inhalt migriert werden muss. Das beantwortet die falsche Frage. Drei Dinge brechen:
- Die Update-Erkennung selbst. Jede bereits exportierte Instanz trägt in
.wikitool-release.jsoneinupdate_urlauf.../repos/torben/llm-wiki-test1/releases/latest. Nach dem Repo-Rename antwortet Gitea dort bestenfalls per Redirect, schlimmstenfalls mit404- und ein404ist von „kein Release vorhanden" nicht zu unterscheiden, wieINSTALL.md§ Konfiguration für den Token-Fall bereits festhält. Die Instanz kann sich nicht selbst reparieren: der Stempel ist maschinengeschrieben, Invariante 1 verbietet den Hand-Edit. Es bricht also ausgerechnet der Kanal, über den eine Instanz erführe, dass sie etwas zu tun hat. Der Ausweg istWIKITOOL_UPDATE_URLauf den neuen Feed zu setzen, oder ein frischerdist export. - Der Artefaktname.
llm-wiki-stack-<version>.tar.gzheißt jetztchemenu-stack-<version>.tar.gz- jedes Download-Skript und jeder Pin darauf bricht. - Der Import-Name.
from wiki_tools import …außerhalb des ausgelieferten Baums bricht.
Keiner dieser drei Punkte verlangt, dass eine Seite in kb/ angefasst wird - deshalb
--no-migration statt eines Migrationsdokuments. Aber rückwärtskompatibel ist das nicht, und
1.9.0 hätte genau das behauptet.
SOUL.md.template schlägt jetzt einen Persona-Namen vor. Bisher stand dort ausdrücklich,
dass der Stack keinen vorschlägt; ab jetzt nennt er Thoth als Startpunkt, mit der
Begründung, die der neue Repo-Name mitbringt. Die Regel dahinter ändert sich nicht: gefragt
wird trotzdem, und ein anderer Name gewinnt. Weil das eine bestehende Regel verschiebt, steht
sie gleichlautend an allen drei Stellen, die sie führen - SOUL.md.template, Schritt 6 in
instructions/setup-instance.md, und SOUL.md dieser Instanz.
Was den alten Namen behält: CHANGES.md, kb/log.md, kb/provenance.md, die Prosa unter
kb/sources/ und alles unter raw/. Das sind abgeschlossene Aufzeichnungen dessen, was
einmal galt - sie umzuschreiben hieße, die eigene Historie zu fälschen, und raw/ ist
ohnehin unveränderlich.
1.8.1 - 2026-08-31 - Coverage-Reporting in CI: messen und als Artefakt ausweisen, ohne Schwelle
Author: Torben Nehmer
729 Tests liefen in CI, und wie viel vom Stack sie erreichen, wusste niemand - Coverage war nie gemessen (Gitea #10). Der Verdacht war nicht theoretisch: Run 52 hatte zwei Tests gefunden, die grün waren, weil die Umgebung zufällig passte. Was gar nicht ausgeführt wird, fällt leichter durch.
Gemessen wird jetzt, entschieden wird später. Der CI-Schritt „Tests" läuft mit
--cov --cov-report=term --cov-report=xml --cov-report=html, ein neuer Schritt lädt XML und
HTML als Artefakt coverage-<run id> hoch (upload-artifact@v3, nicht v4 - v4 ist auf dieser
Gitea-Instanz eingeschränkt). Der Upload steht unter if: always(): eine rote Suite ist genau
der Moment, in dem die Zahlen pro Modul interessant sind.
Kein --cov-fail-under, bewusst. Eine Schwelle vor der Zahl hat zwei Ausgänge, beide
schlecht: sie liegt zu tief und wirkt nicht, oder sie liegt zu hoch und der nächste ehrliche
Commit wird rot - worauf jemand die Schwelle senkt statt Tests zu schreiben. Sie kommt in einem
eigenen, späteren Commit, mit der gemessenen Zahl als Begründung, und friert den erreichten
Stand ein, statt ihn schönzurechnen.
Erste Messung: 86.9 % von 5105 Statements, 730 Tests (2026-08-31, CI-Lauf 87). Festgehalten in EVALS.md
§ „How much of the stack the suite reaches", zusammen mit der Leseanleitung - denn der
Gesamtwert ist die uninteressanteste Größe. Der Bericht trennt drei Fälle, die verschieden zu
behandeln sind: dünne Typer-Wrapper (eval_cmd.py 36 %, types_cmd.py 52 %, cli.py 52 %),
bei denen niedrige Zahlen ein guter Schnitt sind und kein fehlender Test; Code mit einer
injizierbaren Außengrenze (version.py's fetch_latest()), wo die echte Netzzeile absichtlich
ungetestet bleibt; und echte Lücken (provenance_cmd.py 44 %, migrate_cmd.py 71 %,
type_resolver.py 79 %). Nur die dritte Liste ist Arbeit.
pytest-cov steht nicht in tools/requirements.txt: die Datei beschreibt die
Laufzeitabhängigkeiten einer Instanz und wird von dist export mitgeliefert, und eine Instanz
misst diese Suite nicht. CI installiert es neben pytest. Die Konfiguration liegt in
tools/.coveragerc statt in pytest.ini - coverage.py liest .coveragerc, setup.cfg,
tox.ini und pyproject.toml, aber kein pytest.ini, wo ein [coverage:*]-Abschnitt still
ignoriert würde. Gemessen wird wiki_tools/ ohne wiki_tools/tests/. Die Messung hängt
ausdrücklich nicht in addopts: das würde den nackten pytest -q überall dort brechen, wo
pytest-cov fehlt.
.gitignore und die Kanarienvögel in docs verify decken die Ausgabe mit ab
(tools/.coverage, tools/coverage.xml, tools/htmlcov/) - abgeleitet wie reports/, und
publish läuft mit git add -A, also hätte ein unignorierter htmlcov/-Baum sich beim
nächsten Content-Publish selbst mitcommittet.
Dabei gefunden und mitbehoben: dist export hat die Coverage-Ausgabe mit ausgeliefert. Der
erste Export nach der Messung trug 227 statt 162 Dateien, darunter den kompletten
htmlcov/-Baum - eine Messung dieses Repos, ausgeliefert in fremde Instanzen. Der Grund war
struktureller Natur: TOOLS_EXCLUDE_DIRS prunet Verzeichnisse, aber zwei Drittel der
Coverage-Ausgabe (.coverage, coverage.xml, unter Parallelläufen .coverage.<host>.<pid>)
liegen als Dateien neben dem Code. _copy_tree nimmt jetzt zusätzlich ein
Dateinamen-Prädikat; htmlcov/ kam in die Verzeichnisliste. .coveragerc geht weiter mit -
Konfiguration ist Maschinerie, wie pytest.ini. Damit hat der Kopierplan drei
Ausschlussmechanismen statt zwei, für drei verschiedene Gründe: dev-only Inhalt (Marker),
dev-only Dateien (instructions/dev/), abgeleitete Ausgabe (Build-Artefakte).
1.8.0 - 2026-08-31 - ENVIRONMENT.md: optionale, checkout-spezifische Umgebungsnotiz
Author: Torben Nehmer
Welches Harness läuft hier, welche MCP-Server sind erreichbar, wohin veröffentlicht publish,
wo läuft CI? Werte, die sich über Wochen nicht ändern und die trotzdem jede Session neu
erfragt hat, weil nichts sie festhielt. USER.md beantwortet, wer die Instanz bedient,
SOUL.md, wie sie klingt - womit sie arbeitet, beantwortete bisher niemand (Gitea #24).
ENVIRONMENT.md schließt die Lücke, als drittes Root-Dokument der Session-Ebene. Es hält
Harness, publizierte Skills, MCP-Server, Connectoren, Git-Remotes und den CI-Ort fest;
ENVIRONMENT.md.template gibt die Abschnitte vor. Claude Code importiert es über CLAUDE.md,
AGENTS.md trägt es in der Namenstabelle und in einem eigenen Abschnitt für die übrigen
Harnesses.
Drei Eigenschaften unterscheiden es von der Personalization Plane, und alle drei sind Absicht:
Optional. Fehlt die Datei, ist das ein gültiger Endzustand, kein Mangel - doctor meldet
environment: absent (optional) und niemals FAIL. Ein FAIL hätte die Datei durch die
Hintertür verpflichtend gemacht; der Preis ihres Fehlens sind ein paar Fragen, keine falsche
Ausgabe. Gemeldet wird genau ein Fall, und zwar als WARN: ein umbenanntes, nie ausgefülltes
Template, das in jeder Session mitgeladen wird und nichts beantwortet. Der Sentinel
wikitool:template-unfilled ist derselbe wie bei USER.md/SOUL.md.
Gitignored statt committet. Zwei Clones desselben Repos sind zwei verschiedene Umgebungen.
Eine committete Fassung würde dem zweiten Clone Antworten geben, die falsch sind statt zu
fehlen - und falsch ist hier schlimmer, weil die Datei geglaubt wird. docs verify prüft
beide Hälften des Musters: ENVIRONMENT.md muss ignoriert sein, ENVIRONMENT.md.template
darf es nicht sein. Das nachlässige ENVIRONMENT.md* würde beide schlucken, und genau das
fängt der Kanarienvogel ab.
Kontext, keine Autorität. Die Datei beschreibt, was da ist, nicht, was erlaubt ist. Ein
dort gelisteter Remote autorisiert kein git push - Invariante 5 führt weiter über
wikitool publish -, ein gelisteter MCP-Server öffnet kein Gate, und nichts darin ist eine
Quelle im Sinne von Invariante 3. Geheimnisse gehören nicht hinein: die Datei liegt im
Klartext im Arbeitsverzeichnis und in jedem Agenten-Kontext.
Warum ein Import in CLAUDE.md und kein Link: Was hier steht, wird nebenbei gebraucht - beim
Griff zum richtigen MCP-Server, mitten in einer Aufgabe -, nicht an einer Stelle, an der
jemand ein Dokument aufschlägt. Das ist derselbe Maßstab, den die Modellwahl-Instruktion
erfüllt, und er ist zugleich der Test: Eine Session, die erst nachschlagen müsste, fragt
stattdessen wieder den Nutzer - genau die Kosten, die die Datei beseitigt. Dass ein
unaufgelöster Import folgenlos bleibt, war schon vor der Personalization-Einführung so; hier
wird es zum Dauerzustand statt zum Übergang.
Angelegt wird die Datei in bootstrap.md (neuer Schritt 5, für einen Clone) und in
setup-instance.md (neuer Schritt 9, für eine frische Instanz). Beide Male gilt: eintragen,
was aus dem Checkout ablesbar ist, einmal nach dem Rest fragen, "weiß ich nicht" als Antwort
akzeptieren - und einen leeren Abschnitt löschen statt ihn mit Plausiblem zu füllen. Eine
geratene Zeile ist schlechter als eine fehlende.
1.7.2 - 2026-08-31 - lint liest Code nicht mehr als Wiki-Notation; Zitat-Limit zählt Zitate statt Zeilen
Author: Torben Nehmer
Two lint rules were measuring something other than what they claim to. Both are found the same way - by a page that describes the stack rather than using it - and both are hard enough to be worth a release of their own.
lint read wiki notation inside code as the real thing (Gitea #20). CITE_REF_RE,
CITE_DEF_RE, LEGACY_CITE_RE and WIKILINK_RE are plain text matches with no idea what a
Markdown code span is, so a page writing `[^cite-id]` in backticks, or showing a
definition line in a fenced block, produced Undefined Footnote References - and that is a
hard error, so lint --fail-on-error and CI went red. It happened at the 2026-08-31 ingest
(c28f8ce), on concept pages about the citation mechanism itself. The way out that session was
to write about the notation without writing it ("Zitatdefinitionszeile" instead of the marker),
which is invisible to whoever reads the page later. A knowledge base about a stack has to be
able to quote that stack's notation.
The new wiki_tools/markdown_code.py holds that rule once: strip_code_spans() replaces
fenced blocks and inline code spans with spaces of the same length, so offsets survive and a
caller may match the masked text and slice the original. Every scan now goes through it -
provenance.iter_cite_refs() for references, split_cite_block() for definitions,
legacy_citation_markers(), and kb_scan.extract_wikilinks()/count_wikilinks() for the link
graph, where the identical blindness made a fenced example link a broken one. Teaching each
regex its own context logic would have been six copies of one rule.
Two boundaries are drawn deliberately, and both are asserted in
tests/test_markdown_code.py. Indented code blocks are not masked: in this corpus a
four-space indent is a nested list continuation far more often than it is code, and masking by
indentation would have silently deleted real links from the graph. Inline spans are matched
within one line: an unclosed backtick is a common typo, and a matcher that crosses newlines
turns one typo into a masked paragraph - a failure that hides findings rather than inventing
them, which is the direction that goes unnoticed. For the same reason split_cite_block()
still locates the Footnotes heading on the unmasked body: one unclosed fence would otherwise
blank the heading and leave the page with no definitions at all.
The corpus had the mirror-image habit. 12 [^s-…] markers sat inside fenced blocks,
appended to a command or a YAML line, across four pages. They never rendered as footnotes -
GFM shows them literally, so copying the command copied the marker with it - and masking turned
two of them into orphan definitions. They now sit on a Quelle: [^s-…] line under their block:
vier Infrastruktur- und Konzeptseiten.
The quote limit counted > lines, not quotes (Gitea #22). The rule is about how much
borrowed wording a page carries; the line count measured the wrap width instead, so one
quotation counted 1 written long and 4 wrapped at the width the rest of the repo keeps. An
author who took the finding seriously made the page harder to read to quiet it - and it fired
on a page with exactly one quotation while verifying 1.7.1. count_quote_blocks() now counts
runs of consecutive quoted lines, with code masked out first so a > in a shell transcript is
a prompt rather than a quotation. QUOTE_LIMIT stays at 2 and finally means what its name
already claimed.
Alongside the release, .gitea/workflows/nightly.yml runs doctor, docs verify,
instructions verify, lint --fail-on-error, sources coverage and migrate status on a
schedule (Gitea #9). It closes the gap ci.yml's content paths-ignore opened: since that
landed, structural lint no longer runs when the corpus changes. Workflows are outside the
version gate, so this needed no bump of its own.
1.7.1 - 2026-08-31 - Testsuite gegen stille Umgebungsabhängigkeiten gehärtet
Author: Claude Code
The suite was green for months on every developer machine while silently reading the machine it
ran on. config.default_author() shells out to git config user.name with cwd=config.ROOT;
the fixture root is not a repository, so the answer came from the global git configuration of
whoever started pytest. The first CI run that ever reached pytest (run 52) had none, and two
tests fell over with ERROR No author configured for this instance. Both were repaired in
1.0.1 - and in 1.2.0 two new tests reintroduced exactly the same dependency, written by
someone who had read the issue first (Gitea #8). Patching cases one at a time loses to the rate
at which the suite acquires them.
tools/wiki_tools/tests/conftest.py now carries an autouse hermetic_environment fixture
next to isolated_trace_dir. Before every test it points HOME (and XDG_CONFIG_HOME) into
that test's own tmp_path, sets GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM to /dev/null, and
clears both git's identity/location variables (GIT_DIR, GIT_WORK_TREE, GIT_AUTHOR_*,
GIT_COMMITTER_*, EMAIL) and the tool's own (WIKI_AUTHOR, WIKI_TRACE,
WIKI_TRACE_CONTENT, WIKI_TRACE_MAX_CONTENT, WIKITOOL_SESSION_ID, WIKITOOL_UPDATE_URL,
WIKITOOL_UPDATE_TOKEN). A test can no longer acquire the dependency at all: it either
establishes an identity itself or the tool refuses, on the author's machine and in CI alike.
WIKI_TRACE_DIR is deliberately the one variable left set - isolated_trace_dir redirects it
per test, and now declares hermetic_environment as a dependency so the ordering between the
two autouse fixtures is stated rather than inherited from declaration order. Tracing is never
disabled suite-wide; two telemetry tests assert that a trace is written.
No identity is set for everyone. A shared default in conftest.py would have been the
cheaper fix and the wrong one: default_author()'s None branch only exists on a machine that
knows nobody, so a suite-wide WIKI_AUTHOR would have made the fallback untestable while
looking green. Instead the new test_hermetic_env.py asserts the fixture itself - the cleared
variables, the empty HOME, that git config user.name answers nothing, that tracing stays on
and redirected - and covers all three branches of default_author(), including the one the
fixture makes writable for the first time. test_new_source_fails_hard_without_any_author keeps
patching default_author directly, so it stays a test of the CLI's error path rather than of
the environment.
Verified in three environments that now agree exactly - the developer shell, a deliberately
poisoned one (WIKI_AUTHOR, WIKI_TRACE=0, WIKITOOL_* and GIT_* all set to junk), and
env -i with an empty HOME and no git configuration: 702 passed in each. Before this change
the third of those was the one that failed.
CI keeps running the suite once. A second hardened pytest step was the issue's fallback
proposal; it is redundant now and would report the dependency only after a push. The Tests step
in .gitea/workflows/ci.yml says so, so nobody adds it back for the reason it was once needed.
Added instructions/dev/testing-conventions.md - what
the fixture neutralizes, how a test establishes an author when it needs one, and the env -i
counter-check to run before publishing a test change. Linked from the stack-dev skill's step
2, which until now pointed at "the existing test files' own patterns" for exactly this. Also
pins the fixture repo in test_new_source_author_falls_back_to_git_config to
git init -q -b main: without a global configuration git emits an init.defaultBranch advisory
that clutters unrelated failures.
No command changes behaviour, so this is a PATCH.
1.7.0 - 2026-08-31 - CLAUDE.md as a harness-scoped instruction reference; Claude-Code model-selection guidance
Author: Claude Code
tools/wikitool instructions verify only ever recognized AGENTS.md as an automatically-loaded
reference source for instructions/*.md - accurate for every other harness (Codex CLI, GitHub
Copilot CLI, Mistral Vibe read AGENTS.md natively), but not for Claude Code, which loads
CLAUDE.md instead and does not read AGENTS.md on its own. A Claude-Code-only instruction
therefore had no valid place to be linked from: linking it from AGENTS.md would load it into
every other harness's session too.
CLAUDE.md is now a second recognized reference/auto-load source alongside AGENTS.md,
scoped the same way: instructions_cmd.AGENT_ROOT_FILES and automatic_load_paths() both
include it, manual: true instructions are forbidden from being linked there for the same
reason they are forbidden from AGENTS.md, and instructions/CONTRACT.md's reference-tier
table documents the split. AGENTS.md and CLAUDE.md are deliberately kept as two names, not
folded into one "automatically loaded" concept, because they are automatic for disjoint sets of
harnesses.
A mention in README.md or CHANGES.md no longer counts as a reference. Both were in the
old REFERENCE_GLOBS, so an instruction whose only mention was in one of them passed verify's
"nothing references it - it deploys to no one" check while being unreachable by any agent -
AGENTS.md's own file-naming table defines README.md as "never by an agent as instruction",
and CHANGES.md is not even shipped (dist export replaces it wholesale). The tuple is now
split by what the two callers actually ask: AGENT_ROOT_FILES for "can an agent reach this",
AGENT_ROOT_FILES + SHIPPED_DOC_ROOT_FILES for the instructions/dev/ boundary check, which
still scans README.md because a dangling path in a shipped document is a defect regardless of
who reads it. Nothing in the tree relied on the old behaviour - verify stays green - so this
closes a latent false green rather than fixing a live break. It also means this entry's own
mention of the new instruction below no longer masks a broken CLAUDE.md link.
Added instructions/claude-code-model-selection.md,
@-imported by CLAUDE.md as its fourth import: which model (Sonnet vs. Opus) and effort level
to run a session, a spawned subagent, or a /code-review pass at, keyed to the skill or task at
hand rather than defaulting everything to the most capable model at the highest effort.
Imported, not linked - the choice matters enough that instructions/CONTRACT.md now
documents both forms and when each applies. Model and effort are decided while spawning a
subagent or starting a review, not at a point where anyone stops to open a document, so a link
would be followed only when the user explicitly asked about models. The price is standing
context in every Claude Code session: an import is the strongest load in this layer, stronger
than a skill (which contributes only its description), and that is the bar each further
Claude-Code-only import has to clear. verify accepts either form - both put the filename in
CLAUDE.md.
Deliberately out of scope, tracked as a follow-up issue instead: equivalent model-selection
guidance for the non-Claude-Code harnesses this repo supports. Those harnesses have no
equivalent of Claude Code's model names, /code-review effort dial, or Agent tool model:
override, so the question of whether and how they should decide this differently is a separate
design decision, not answered here.
1.6.0 - 2026-08-31 - xref link-source schreibt beide Richtungen; xref add lehnt undeklarierte Ref-Felder ab
Author: Torben Nehmer
Schließt Gitea-Issue #18. Drei Defekte mit einer Ursache, und ein Loch in einer
Entscheidung aus 1.4.0 desselben Tages: die Sperrliste in touch --set lehnt
Page-Ref-Felder mit dem Verweis auf xref ab — für die entities:/concepts:
einer Source-Seite konnte xref das aber nicht. Der Verweis zeigte ins Leere.
xref link-source schreibt jetzt beide Richtungen
Bisher schrieb es nur die Zielseiten (sources: + See-Also-Bullet) und rührte
die Arrays der Source-Seite nie an — es prüfte nur, dass sie existiert. Ein
Ingest legt die Source-Seite aber an, bevor die Concept-Seiten existieren,
weil deren Titel erst beim Extrahieren feststehen. Also blieb concepts: [],
und kein Kommando konnte es füllen.
Welches der beiden Felder ein Ziel bekommt, folgt seiner Collection:
kb/entities/ → entities:, kb/concepts/ → concepts:. Das Verzeichnis
ist der Feldname, also braucht eine neue Collection hier keine Code-Änderung —
sie braucht einen Typ, der das passende Feld deklariert. Ein Ziel, dessen
Collection zu keinem deklarierten Feld passt, wird einseitig verlinkt und in der
Ausgabe benannt statt stillschweigend übergangen.
xref add lehnt ein nicht deklariertes related: ab
add_related() schrieb das Feld bedingungslos. Auf einer Source-Seite —
page_ref_fields: [entities, concepts] — erzeugte das Frontmatter, das das
Schema verwirft (additionalProperties: false). Genau so entstand der einzige
Schema-Fehler, der heute auf main lag.
Die Prüfung läuft für beide Seiten, bevor eine davon geschrieben wird: eine Ablehnung darf keinen halben Link hinterlassen. Die Meldung nennt die Felder, die der Typ kennt, und das Kommando, das sie füllt.
xref remove räumt auch undeklarierte Reste
strip_frontmatter_ref() lief nur über die vom Typ deklarierten Felder. Ein
related: auf einer Source-Seite war damit nicht entfernbar — ein Kommando
erzeugte einen Zustand, den ein anderes nicht rückgängig machen konnte, obwohl
sein Contract-Eintrag ausdrücklich verspricht, hinterlassene Referenzen ohne
Handeditierung zu klären.
Gesweept wird jetzt zusätzlich jedes auf der Seite vorhandene Feld, das
irgendein Typ als Ref-Feld deklariert — die Namen kommen aus den Type-Specs,
nicht aus einer Konstante. Ein undeklariertes Feld, das dabei leer wird, fällt
ganz weg statt als related: [] stehenzubleiben: der Schlüssel war für diesen
Typ nie gültig, und ein leeres Array hielte die Seite weiter schemawidrig.
Das gilt damit auch für rename und rm, die denselben Helfer benutzen.
Bestandsfall repariert, mit dem Werkzeug
Die Seite auf main trug related: [Write-Once Frontmatter Fields] und ein
leeres concepts:. xref remove hat den Rest geräumt, xref link-source hat
beide Concepts in beide Richtungen eingetragen. lint meldet keinen
Schema-Fehler mehr. Kein rm --yes, keine Handeditierung.
Sonst
Die Ablehnungsmeldungen der Sperrliste in touch --set nennen jetzt für
entities:/concepts:/sources: konkret xref link-source statt pauschal
xref add/xref remove — der bisherige Verweis war für genau diese Felder
falsch.
Sechs neue Tests in test_xref.py, darunter der, dass eine Ablehnung keine
Seite anfasst, und die Idempotenz der neuen Richtung.
1.5.1 - 2026-08-31 - cite add/sync und rename loeschen keinen Inhalt mehr hinter dem Fussnoten-Block
Author: Torben Nehmer
Schließt Gitea-Issue #17. Der bisher einzige bekannte Weg, auf dem dieser Stack Inhalt verliert statt ihn falsch zu schreiben.
split_cite_block() nahm alles ab der Überschrift ## Fußnoten bis zum
Dateiende als Zitatblock, behielt daraus nur die [^id]:-Zeilen, und jeder
Aufrufer setzt eine Seite als Kopf + neu gerenderter Block wieder zusammen.
Stand hinter dem Block noch ein Abschnitt, war er nach dem nächsten Aufruf weg —
ohne Warnung, mit Erfolgsmeldung. Betroffen waren cite add, cite sync und
rename, die alle denselben Pfad benutzen.
Systematisch wurde es durch einen Widerspruch zwischen zwei Kommandos:
xref add hängt ## Beziehungen und ## Siehe auch ans Dateiende, cite add
hielt den Fußnoten-Block für das Dateiende. Wer cite add zuerst laufen ließ,
war sicher; wer die Reihenfolge umdrehte, verlor beim nächsten Zitat alle
Querverweise. Keiner der Skills schreibt die Reihenfolge vor, es war also Zufall.
Gefunden beim Ingest vom 2026-08-31 (8524bce), wo
kb/concepts/Detect-Repair Asymmetry.md vier ## Beziehungen- und fünf
## Siehe auch-Einträge verlor. Dass es auffiel, war Glück: der ausführende
Agent hatte nach cite add zurückgelesen.
Der Fix
Der Block endet jetzt an der nächsten Überschrift statt am Dateiende. Alles dahinter — und alles im Block, was keine Zitatdefinition ist — wird auf den Kopf zurückgefaltet statt verworfen. Der Rückgabetyp bleibt gleich, alle sechs Aufrufer profitieren ohne Änderung.
Zwei Eigenschaften, die dabei mehr wert sind als die reine Reparatur:
- Die Seite heilt sich selbst. Weil der gerenderte Block immer zuletzt
ausgegeben wird, bringt die erste Zitatoperation eine bereits verrutschte
Seite wieder in die richtige Form.
xref adddarf weiterhin ans Dateiende anhängen, ohne Schaden anzurichten. - Loser Text im Block wird gerettet, nicht abgelehnt. Ein Abbruch wäre die
naheliegende Härtung gewesen, ist hier aber falsch: derselbe Pfad läuft unter
lintundcorpus_diff, wo eine Exception das Lesen einer Seite verweigern würde, statt sie zu melden.
Nebenbei behoben: ein [^id], das nur in einem Abschnitt hinter dem Block
referenziert wurde, galt für extract_inline_cites als nicht referenziert — und
cite sync hätte seine Definition als verwaist entfernt.
Korpus repariert
Acht Seiten trugen zusammen 74 Zeilen in der gefährdeten Position. cite sync --all hat sie normalisiert (elf Seiten insgesamt; drei brauchten nur eine
Neusortierung). Nachgemessen: 0 Seiten mit Inhalt hinter dem Block, und je Seite
unveränderte Zahl an Zitatdefinitionen und Bullets.
Tests
Fünf neue in test_provenance.py, darunter der Fall, der zuerst rot sein muss
(Abschnitt hinter dem Block überlebt den Rundlauf), die Stabilität wiederholter
Rundläufe, und ein Zitat, das nur im geretteten Abschnitt referenziert wird.
Bemerkenswert: vor diesen Tests liefen alle 678 grün. Das alte, zerstörende Verhalten war von keinem Test festgehalten — so hat es überlebt.
1.5.0 - 2026-08-31 - Mass-Update Gate zaehlt generierte Dateien nicht mehr; Kalibrierungsband gemessen statt geerbt
Author: Torben Nehmer
Zwei Kalibrierungen, beide aus derselben Beobachtung: drei gewöhnliche Ingests hintereinander blieben am Mass-Update Gate stehen. Keiner war ein Massen-Update. Das Gate feuerte auf Rauschen, und die Budget-Richtwerte beschrieben eine Arbeit, die es so nicht gibt.
Generierte Dateien zählen nicht mehr gegen die Schwelle
kb/index.md, kb/log.md, kb/provenance.md und jede INDEX.md werden
committet und gepusht wie bisher, aber vom Gate nicht mehr gezählt und nicht
mehr zur Freigabe vorgelegt. Der Grund ist derselbe, aus dem work/ schon
ausgenommen war: sie tragen keine Entscheidung. Jede davon ist per
index rebuild / sources rebuild-index aus dem Baum reproduzierbar, also
entscheidet ihre Freigabe nichts — sie erzeugt nur Review-Müdigkeit, gegen die
die Schwelle eigentlich gerichtet ist.
Ein gewöhnlicher Ingest baut fünf bis sechs davon neu. Gemessen an den drei Changesets vom 2026-08-31:
| Ingest | Dateien | vorher gezählt | jetzt gezählt |
|---|---|---|---|
| Comma Bug | 14 | 14 → Gate | 9 → läuft durch |
| Issue Triage | 16 | 16 → Gate | 9 → läuft durch |
| Auto Mode | 11 | 11 → Gate | 5 → läuft durch |
Das Gate bleibt scharf: zehn echte Seiten lösen es weiterhin aus, egal wie viel Index-Rauschen mitfährt. Ein Test hält genau das fest.
Die Ablehnungszeile weist beide Ausnahmegründe getrennt aus („3 under work/ and 5 generated by wikitool committed but not counted"), weil ein Prüfer, der „9 counted" an einem 14-Datei-Commit sieht, sonst die Differenz für einen Fehler hält. Und weil Kratzzustand und abgeleitete Ausgabe nicht dasselbe sind.
Der --confirm-Token deckt jetzt genau das ab, was ein Mensch tatsächlich
gelesen hat: eine neu gebaute INDEX.md macht eine erteilte Freigabe nicht
mehr ungültig.
Kalibrierungsband: 15-25 → 20-35 für komplexe Workflows
Das obere Band stammte aus einer Branchen-Faustregel. Vier reale Ingests messen 24, 26, 29 und 30 Aufrufe — jeder einzelne am oberen Rand oder darüber, ohne dass etwas Ungewöhnliches passiert wäre. Ein Richtwert, den der Normalfall überschreitet, ist keiner; er bringt einem Agenten bei, dass die Zahlen Dekoration sind.
Neu: ~5-15 für eine einfache Aufgabe (gemessen 5-9), ~20-35 für einen
komplexen Multi-Tool-Workflow. Nachgezogen in run_budget.py,
instructions/gates.md und den Skills wiki-ingest und wiki-lint. Die
Obergrenze von 60 bleibt unverändert — sie ist kein Ziel, sondern der Punkt, ab
dem eine Sitzung als festgefahren gilt.
gates.md sagt jetzt auch, woher die Zahl kommt und wie man sie neu misst:
tools/.wikitool_session/budget.json hält die Aufrufzahlen je Session. Ein
Richtwert ohne Messverfahren veraltet lautlos.
Die zitierte Branchen-Faustregel in kb/concepts/Iteration and Cost Limits.md
bleibt unangetastet — sie ist eine belegte Aussage über den Stand der Technik,
nicht über diese Instanz. Was diese Instanz misst, ist eine andere Behauptung
und gehört beim nächsten Ingest dieser Sitzung dort ergänzt.
1.4.0 - 2026-08-31 - touch --set/--add/--remove: Frontmatter-Felder nach dem Anlegen korrigierbar
Author: Torben Nehmer
Schließt Gitea-Issue #14. Ein Feld, das new einmal schrieb - tags:,
raw_files:, source_url: - war danach unerreichbar: touch kannte es nicht,
Frontmatter von Hand zu editieren ist das, was dieses Werkzeug verhindern soll,
und die Seite zu löschen und neu anzulegen bricht jede Referenz, die schon auf
sie zeigt. Da new nicht idempotent ist, war das Fenster, es richtig zu
treffen, genau ein Kommando breit.
Drei Fehlschläge in drei aufeinanderfolgenden Ingests am 2026-08-31, an zwei
Feldern, von drei verschiedenen Agenten - einer davon wegen eines Kommas am
Ende eines --set tags=-Werts. Das ist keine Ausrutscherquote, sondern die
Fehlerrate einer Schnittstelle, die einen Wert genau einmal entgegennimmt.
Neu an touch
--set field=valueersetzt den Wert auf der Platte. Wiederholtes--setfür dasselbe Arrayfeld hängt innerhalb eines Aufrufs an,\,ist ein literales Komma - dieselben Regeln wie beinew --set(1.2.0).--add/--removeändern einzelne Elemente eines Arrayfelds, ohne dass man die bestehende Liste kennen muss.--addist idempotent.--removeauf ein nicht vorhandenes Element gelingt und sagt es - wiexref removeidempotent, aber nie stillschweigend: ein stiller No-op sieht genauso aus wie eine erfolgreiche Entfernung, und genau so verschwindet ein Tippfehler.--summary/--provenance/--confidence-basebleiben als Kurzformen.
Die Sperrliste, und warum es eine ist
Gesperrt sind type: (ändert Schema und Ablageort - das ist
page-lifecycle.md), confidence: (abgeleitet; --confidence-base setzen und
confidence decay --apply laufen lassen) und die Page-Ref-Arrays related:,
sources:, entities:, concepts: (xref pflegt die Gegenrichtung und die
Body-Bullets mit; ein nackter Frontmatter-Schreibvorgang ließe die andere
Hälfte veralten). Jede Ablehnung nennt das Kommando, dem das Feld gehört.
Bewusst eine Deny- und keine Allowlist: eine Allowlist wäre eine zweite Kopie des Schemas, und die Kopie ist die, die driftet - ein neu in einen Type-Spec aufgenommenes Feld bliebe stumm unbeschreibbar, bis jemand daran denkt. So gilt: was das Schema kennt, ist schreibbar, sofern es keinen Grund gibt.
Ein unbekanntes Feld wird anders abgelehnt als ein gesperrtes: nicht mit
einem Verweis auf ein anderes Kommando, sondern mit der Liste dessen, was die
Seite tatsächlich hat. Bei --set tag=x ist der Nutzen zu erfahren, dass
tags gemeint war.
Sonst
raw_files:bekommt beim Schreiben durchtouchdieselbe Existenzprüfung wie beinew- eine Dateisystemprüfung, die kein Schema ausdrücken kann._coerce_set_value,_parse_set_fieldsund_check_raw_files_existsind ausnew_page.pynachcommands/_util.pygewandert und heißen jetzt ohne Unterstrich. Zwei Kommandos, eine Implementierung - andernfalls hättetouch --setdas Komma-Problem aus #12 gleich mit geerbt.tests/test_touch.pyruft den Typer-Callback jetzt über einen Helfer mit Vollbelegung auf. Ein direkt aufgerufener Callback bekommt für ausgelassene ArgumenteOptionInfo-Objekte, und ohne den Helfer kostet jede neue Option eine Änderung an jeder Aufrufstelle.
Nicht enthalten
raw rename - der Fall, dass die Datei sich bewegt. Zweistufig ist er jetzt
möglich (git mv, dann touch --set raw_files=…), aber zwischen den beiden
Schritten zeigt die Referenz ins Leere, und bei mehreren referenzierenden
Seiten wächst das Fenster. Als eigenes Issue geführt.
1.3.0 - 2026-08-31 - capture-session: eine Session als raw/-Transkripte sichern und ingesten
Author: Torben Nehmer
Neue Instruction instructions/capture-session.md: eine beendete Claude-Code-
Session als raw/notes/-Transkripte sichern und einzeln ingesten. Das war
inzwischen dreimal von Hand gemacht worden, jedes Mal etwas anders - und die
Fehler, die dabei teuer sind, macht man genau einmal.
manual: true, also bewusst nicht aus AGENTS.md oder einem Skill verlinkt.
Jede Session zu sichern würde raw/ mit Material füllen, das nie jemand
zitiert; die Einschätzung „war diese Session es wert" gehört dem Nutzer, nicht
dem Agenten. Aufruf nur namentlich auf Anforderung.
Was die Instruction festhält, statt es beim nächsten Mal wieder herzuleiten:
- Drei Oberflächen, drei Aufgaben.
raw/hält die Evidenz, der Issue-Tracker den offenen Arbeitsstand,kb/das Gelernte. Ein Transkript ist keine To-do-Liste: was offen bleibt, wird vor dem Ingest zum Issue, und das Transkript hält nur fest, dass es das wurde. - Ein Transkript je Thema. Eine Rohdatei bekommt eine Source-Seite, und
deren
summary:/entities:/concepts:beschreiben eine Sache. Später zu schneiden heißt, eine Datei umzubenennen, auf die schon Zitate zeigen. - Fidelity wird bei der Erfassung festgelegt und steigt danach nie wieder. Kein Zitatformat und keine spätere Prüfung befördert eine Paraphrase zum Zitat - nur ein erneuter Blick ins Original, den es bei einer beendeten Session nicht mehr gibt. Was tragend werden könnte, wird sofort wörtlich zitiert; Material aus zweiter Hand (etwa Subagent-Befunde) nennt seinen Vermittler.
- Ingests laufen streng nacheinander. Parallele Ingests desselben Korpus
kollidieren an geteilten Entity-Seiten, an den generierten Dateien und an
publish- jedes Mal als stiller verlorener Schreibvorgang, nicht als Fehler. Je Transkript eine eigeneWIKITOOL_SESSION_ID. - Verworfene Alternativen gehören ins Transkript. Was der Code tut, steht im Code; was er bewusst nicht tut, steht sonst nirgends.
Die Punkte zu Fidelity und zur Trennung von Evidenz und aktivem Arbeitsstand
sind aus der vendorierten commonplace/-Wissensbasis übernommen
(a-citation-cannot-assert-more-fidelity-than-its-capture-preserved,
active-work-state-is-not-retrospective-memory-or-chat-history).
Mit diesem Commit landen zugleich die drei Transkripte dieser Session unter
raw/notes/; ihre Source-Seiten entstehen in den nachfolgenden Ingests.
1.2.1 - 2026-08-31 - Arbeitsplanung: Gitea-Issues mit prio/size-Labels statt TODO.md
Author: Torben Nehmer
TODO.md ist entfernt. Nach der Verlagerung der CI/CD-Punkte nach Gitea
(2026-08-30) und der Recherche-Notiz als Issue #15 stand dort nichts mehr, was
nicht ohnehin ein Issue war - die Datei war eine zweite Liste über denselben
Bestand, und die zweite Liste ist immer die, die driftet.
Offene Arbeit steht ab jetzt ausschließlich in den Gitea-Issues, ein Issue je Arbeitspaket, mit genau zwei Labels:
prio/1blockiert oder beschädigt laufende Arbeit,prio/2trägt bald Zinsen,prio/3wartet auf einen benannten Auslöser.size/XSbissize/Lschätzt den Aufwand, nicht die Wichtigkeit.
Beide Achsen sind Pflicht: eine Priorität ohne Kosten ist eine halbe Entscheidung. Eine dritte Achse gibt es bewusst nicht - dort fängt eine Taxonomie an, eigene Pflege zu brauchen, und dieses Board hat einen Betreuer.
Die Regel steht in instructions/dev/issue-tracking.md und ist aus dem
stack-dev-Skill verlinkt. Sie liegt unter instructions/dev/, weil sie nur
hier gilt: eine verteilte Instanz hat keine Issues unter dieser URL, und
dist export schließt das Verzeichnis vollständig aus. Aus demselben Grund ist
das ein PATCH und kein MINOR - für eine bestehende Instanz ändert sich
nichts.
1.2.0 - 2026-08-31 - --set-Arraywerte mit Komma, Lint-Reportpfad, Budget-Refund, Ceiling 60
Author: Torben Nehmer
Zwei Werkzeugdefekte aus dem Ingest vom 2026-08-30 (Issues #12 und #13). Beide
haben den Nutzer gegen eine Regel des Stacks gedrückt: einmal gegen die
Unveränderlichkeit von raw/, einmal gegen Invariante 6. Das ist der Grund,
warum sie zusammen vor allem anderen kommen.
--set kann Kommas in Arraywerten ausdrücken (#12)
parse_list teilte hart an , ohne jedes Escape, und Shell-Quoting hilft
nicht - die Quotes sind lange weg, bevor der Wert dort ankommt. Ein
raw_files:-Pfad mit Komma im Dateinamen war damit nicht darstellbar. Im
Ingest vom 2026-08-30 wurde deshalb die Rohdatei umbenannt, obwohl
raw/CONTRACT.md die Rohschicht als unveränderlich führt.
\,ist jetzt ein literales Komma und überlebt den Split (parse_list, also auchxref add --entities).--sethängt an, wenn es für dasselbe Arrayfeld wiederholt wird:--set raw_files=a --set raw_files=b. Das ist die Form ohne Trennzeichen und damit die richtige, wenn ein Element ein Komma enthält. Skalare Felder behalten "last one wins".- Die Fehlermeldung von
_check_raw_files_existbenennt den Split als Ursache und den Ausweg - vorher nannte sie einen Pfad, den niemand getippt hatte.
Dazu die zweite Hälfte desselben Defekts, die beim Testen auffiel:
dump_frontmatter schreibt Listen in Flow-Syntax ([a, b]), quotete aber nach
Dokument-Kontext. Ein Element mit Komma kam deshalb als zwei Elemente zurück -
--set hätte den Wert korrekt geparst, und die Datei hätte ihn trotzdem
zerlegt. _round_trips_as_string prüft den Rundlauf jetzt im selben Kontext,
in dem geschrieben wird, und _quote fragt für beide Kontexte die Bibliothek
statt selbst zu quoten. Bestehende Ausgabe ändert sich dadurch nicht - nur
Werte, die vorher stillschweigend kaputtgingen, werden jetzt gequotet.
Iteration Budget: Reibung statt Iteration wurde gezählt (#13)
- Ceiling 30 → 60. Das Band aus der Kalibrierung (5-15 einfach, 15-25
komplex) bleibt, wie es ist: es beschreibt die Arbeit. Die Decke beschrieb
bisher nichts - sie lag so dicht am Band, dass der Overhead eines echten
Ingests (Bericht nachlesen, ein korrigierter Retry, Baum vor dem Publish
prüfen) allein sie erreichte. Der Ingest vom 2026-08-30 lief inhaltlich im
Band und kam trotzdem nur mit
--override-budgetdurch. - Ein Aufruf, der abgelehnt hat, wird zurückerstattet. Wer über
_util.fail()mit Exit 1 endet - abgewiesenes Argument oder ein lesender Check, der Befunde meldet - hat nichts getan; und der Tool-Error- Contract beantwortet ein abgewiesenes Argument mit "einmal korrigiert wiederholen", was sonst zwei Slots für eine Operation kostet. Der Aufruf bleibt in der Historie des Loop-Breakers: dreimal dasselbe kaputte Kommando bricht weiterhin ab. Ein Kommando, das seine Arbeit schon getan hat und dann ungleich null endet (lint --fail-on-error), wirfttyper.Exit(1)direkt und bleibt gezählt.
lint nennt seinen Reportpfad (#13)
lint ohne Flags kippte den vollen Bericht nach stdout und schrieb nichts. Wer
ihn abschnittsweise las, hatte keinen Weg zurück zum übersprungenen Teil außer
einem zweiten lint - zwei Slots für einen Blick auf einen unveränderten
Korpus.
- Der volle Bericht wird jetzt immer geschrieben, standardmäßig nach
reports/Lint Report <date>.md, und der Pfad wird ausgegeben.--markdownüberschreibt das Ziel wie bisher. - Gedruckt werden nur die Abschnitte mit Befunden. Bei sauberem Korpus ist der
Vollbericht zu über 90 % "None found.";
--fulldruckt ihn trotzdem. --jsonschreibt weiterhin nichts.wiki-lintundwiki-statussagen jetzt beide, dass die Datei gelesen wird stattlinterneut aufgerufen.wiki-statusSchritt 3 liest die Hub-Statistik aus dem Bericht, weil sie kein Befund ist und deshalb nicht mehr in der Zusammenfassung steht.
Nicht enthalten
lint vom Budget zu befreien (dritter Vorschlag aus #13) - das ist eine
eigene Entscheidung über die Grenze zwischen lesend und schreibend, und lint
schreibt jetzt eine Datei.
1.1.1 - 2026-08-30 - CLAUDE.md: AGENTS.md/USER.md/SOUL.md erreichen Claude Code
Author: Torben Nehmer
Defekt, aufgefallen durch Nachfragen des Nutzers: Claude Code hat AGENTS.md
noch nie geladen. Das Repo hatte keine CLAUDE.md, und Claude Code liest
genau diese Datei automatisch - AGENTS.md nicht. Die anderen Harnesses
(Codex, Copilot, Vibe) lesen AGENTS.md nativ, weshalb es niemandem auffiel.
Praktisch heißt das: jede Claude-Code-Session lief bisher ohne Invarianten,
ohne Routing, ohne Gate-Regeln, sofern der Agent die Datei nicht zufällig
selbst aufmachte.
Mit 1.1.0 wurde der Defekt eine Stufe schlimmer: die File-naming-Tabelle
behauptet seitdem für USER.md und SOUL.md „Always, every session". Für
Claude Code war das schlicht unwahr.
Behoben durch eine CLAUDE.md im Root, die keine eigenen Regeln trägt -
nur drei Importe (@AGENTS.md, @USER.md, @SOUL.md) und die Begründung,
warum sie existiert. Eine Regel dort wäre die zweite Kopie, die Invariante 8
verbietet, und zwar die am ehesten driftende: sie stünde in der Datei, die nur
ein einziges Harness liest.
Sie wird von dist export mitgeliefert (ROOT_FILES), aus demselben Grund wie
.claude/settings.json: eine ausgelieferte Instanz unter Claude Code hätte
sonst exakt denselben Defekt. Ein Test hält fest, dass sie im Plan landet und
@AGENTS.md enthält.
Bewusst kein doctor-Check dafür: CLAUDE.md ist harness-spezifisch, und
eine Instanz, die nur unter Codex läuft, braucht sie nicht - ein FAIL wäre dort
schlicht falsch. Anders als bei USER.md/SOUL.md, die jedes Harness liest.
Während des Setups löst nur @AGENTS.md auf; USER.md/SOUL.md entstehen
erst in Schritt 6. Ab der ersten Session danach greifen alle drei.
1.1.0 - 2026-08-30 - Personalization Plane: USER.md/SOUL.md als Setup-Schritt
Author: Torben Nehmer
Zwei neue Dateien, die eine Instanz in jeder Session liest: USER.md (wer sie
bedient) und SOUL.md (wie sie klingt). Beide sind reine Kontext- und
Stildateien - sie bekommen keine neue Autorität, kein Gate und keinen
Zugang nach kb/: SOUL.md verliert gegen AGENTS.md, und eine Nutzeraussage
ist weiterhin keine Quelle im Sinne von Invariante 3.
Der eigentliche Entwurfspunkt liegt woanders. Beide Dateien sind Betriebsvoraussetzung und zugleich persönlicher Inhalt, gehören also nicht in jede exportierte Kopie. Sie entstehen deshalb während der Installation, nicht vorab befüllt in der Distribution:
USER.md.templateundSOUL.md.templateliegen im Root und werden vondist exportmitgeliefert. Dass die ausgefüllten Fassungen es nicht werden, ist keine zusätzliche Regel, sondern Folge der bestehenden Root-Allowlist - ein Name, der dort nicht steht, wird nicht kopiert.- Beide Templates tragen eine Sentinel-Zeile (
wikitool:template-unfilled), die beim Ausfüllen entfernt wird. instructions/setup-instance.mdhat einen neuen Entscheidungspunkt (Schritt 6, Personalization); die Folgeschritte verschieben sich um eins. Der Agent interviewt entlang der Template-Abschnitte und schreibt die Antworten wörtlich mit. Zwei Fragen darf er nicht raten: den Persona-Namen und die Themen, die bewusst draußen bleiben.wikitool doctorbekommt den Checkpersonalization:FAILbei fehlender Datei und bei einer, die noch den Sentinel trägt - ein umbenanntes Template ist kein ausgefülltes.
Für bestehende Instanzen: doctor meldet ab hier personalization: FAIL,
bis die beiden Dateien existieren. Das ist einmalig nachzuholen und keine
Content-Migration - .wikitool-kb.json bleibt unberührt, keine Seite ändert
sich. Nachzuholen ist ausschließlich Schritt 6 aus setup-instance.md, nicht
der ganze Ablauf; instructions/bootstrap.md und INSTALL.md verweisen an
der jeweils passenden Stelle darauf.
Der CI-Replay der Installation stubbt den neuen Entscheidungspunkt so wie die Identität: mit einer festen Antwort (Template minus Sentinel-Zeile). Geprüft wird damit, dass der Export die Templates trägt - nicht, was ein Mensch hineinschreibt.
Umgesetzt aus Issue #2.
1.0.1 - 2026-08-30 - Tests ohne globale git-Identität; CI-Fixes
Author: Torben Nehmer
Der erste CI-Lauf, der überhaupt bis zu pytest kam, hat zwei Tests
umgeworfen, die auf jeder Entwicklermaschine grün sind: sie hingen an einer
globalen git-Identität. default_author() ruft git config user.name mit
cwd=config.ROOT auf, und die Fixture-Wurzel ist kein Repository - also
antwortete bisher die globale Konfiguration dessen, der die Suite laufen ließ.
Im Job-Container als root gibt es keine, und new source weigert sich zu
Recht, einen Platzhalter-Autor zu stempeln.
test_new_source_author_falls_back_to_git_configmacht die Fixture-Wurzel jetzt selbst zu einem Repository mit lokalemuser.name. Damit prüft der Test den Fallback statt der Maschine - und prüft ihn schärfer: er behauptet jetzt den konkreten Namen, nicht nur, dass irgendeiner dasteht.test_new_source_with_multiple_raw_filessetztWIKI_AUTHOR. Der Test geht überraw_files:, Autorschaft ist dort nur Vorbedingung.
Nachprüfbar mit GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null pytest, was die CI-Bedingung lokal herstellt.
Dazu die Workflows unter .gitea/ (ohne eigenen Bump, sie stehen außerhalb
des Version-Gates): nodejs wird jetzt vor dem Checkout installiert, weil
act_runner JavaScript-Actions mit node im Job-Container ausführt und
debian:trixie-slim keins mitbringt; actions/checkout auf @v7; und CI
läuft nicht mehr auf reinen Inhaltsänderungen.
1.0.0 - 2026-08-30 - Content migrations get a version, a chain, and a mechanical check
Author: Torben Nehmer
Migration: none required - no distributed instance exists yet; 1.0.0 is the migration baseline
0.1.0 gave an instance a way to learn that it must migrate. This release gives it a way to
know what shape its content is in, which migrations it still owes, in what order - and a
mechanical check that a bulk rewrite did not quietly drop something.
Two versions, not one. VERSION describes the machinery. What shape the content is in is
a different number, now in .wikitool-kb.json and written only by migrate done. Without it
the state every upgrade passes through - machinery already replaced, content not yet migrated -
cannot be represented at all, and migrate status would have to infer the answer from the stack
version, which is wrong exactly when it matters. It is a separate file from
.wikitool-release.json because the two have opposite rules: that one is generated and must
never be hand-edited, this one is mutable instance state. dist export seeds it with the
exported version, since a fresh instance's content is empty and therefore trivially current;
only a tree predating the file has to answer by hand, via migrate baseline.
Migrations run as a chain. An instance rarely moves exactly one version. migrate status
builds the interval (kb_version, VERSION] from the documents under instructions/migrations/
and orders it: 1.3.1 to 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 simply is not in the interval. migrate done refuses any
version that is not the next link, so a step cannot be skipped and an interrupted multi-step
upgrade resumes rather than has to be reconstructed.
migrate verify --from <rev> is the piece that was missing. It compares every page against
a git revision on what a content migration must not change: wikilink and citation counts,
footnote definitions, H1, structural frontmatter. lint cannot answer this and never could - it
reads a single revision, so a reference that went missing leaves a corpus that is still
perfectly consistent. Everything it compares is a Counter, never a set: three of the four
defects found in the 248-page translation had unchanged link sets and only changed counts.
Verified both ways on the real corpus - a clean run over 248 pages produced zero findings in
2.2 s, and dropping one of two [[Docker]] occurrences from one page was reported as
'Docker' 2->1 while lint passed all 21 checks in silence.
A migration is a manual: true instruction, at instructions/migrations/<version>-<slug>.md
with migrates_to: and migration_kind:. No new stage and no new type: the Manual tier exists
for procedures that must never run implicitly, and dist export already ships instructions/,
so a migration reaches every distributed instance with no second export path. (The contract's
claim that the tier had no example was already stale - german-terminology.md uses it too.)
The methodology is now written down. instructions/migrate-corpus.md holds what the
translation run learned and then deleted with its workshop: units sized by the iteration budget
(~21 pages against the 30-call ceiling), publishes sized by the Mass-Update Gate, the two being
different boundaries; migrate verify before the summaries and before lint; read the whole
lint report; carry the vocabulary between units. Preferring a backwards-compatible change over a
flag day is a decision point of its own, with sections.py's canonical-plus-alias pattern as
the worked example.
Enforcement, in two places. version bump --major refuses without either a migration
document for the new version or --no-migration "<reason>", and docs verify checks the same
thing about the newest changelog entry. An instance told to migrate with nothing telling it how
is the dead end both close - this entry's own --no-migration line is the mechanism's first
real use.
Starting at 1.0.0. compat_key is stated generally (leftmost non-zero component), so
nothing in the code special-cases a range; but the guidance derived from it under 0.x was
self-contradictory, assigning --minor to both "new capability" and "requires migration". From
1.0.0 the familiar reading holds throughout: PATCH fixes, MINOR adds compatibly, MAJOR migrates.
Pre-release suffixes stay unsupported - x.y.z is the whole grammar, and a second ordering rule
would have to be honoured by the release feed, the chain and the compatibility check alike.
One bug found by running it for real. The historical side of the diff 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 - so a clean run reported 13 phantom removed "pages". Both
sides now answer with one shared kb_scan.is_page_path, and the regression is pinned.
Deliberately not built: mechanical runner primitives (migration_kind: mechanical describes
the work today, it does not perform it - a DSL for zero migrations is not worth having), and
dist upgrade, still the manual procedure in INSTALL.md.
Verification: 630 tests pass (46 new), docs verify, instructions verify and
lint --fail-on-error clean, plus the two real-corpus runs described above.
0.1.0 - 2026-08-29 - The stack gets a version, a pipeline, and release files
Author: Claude Code
The repo had no CI and no way to say which stack an instance was running. Both are now in place, and they are the same mechanism seen from two ends: a version that CI enforces, stamps into every export, and publishes as a release an instance can compare itself against.
VERSION versions the stack, never the content. tools/, types/, instructions/,
AGENTS.md and the contracts - not the kb/ pages sitting in the same repo. That split is why
the number is set explicitly instead of being derived from commit messages: publish --message "ingest: ..." writes content commits here too, so any conventional-commit reading would turn
every ingest into a release.
Compatibility is read off the leftmost non-zero component, the rule Cargo's caret ranges
use: 0.1.3 -> 0.1.4 is safe, 0.1.3 -> 0.2.0 is not, and from 1.0.0 on the same rule reads
as the familiar "MAJOR means migration". Stating it this way is what lets the 0.x era carry
the migration signal at all - under plain "MAJOR breaks" semantics every 0.x release is
indistinguishable from every other, which is exactly the signal update detection needs. Nothing
about the mechanism changes at 1.0.0.
New: wikitool version - show (offline: the version plus the release stamp it came
from), bump --major|--minor|--patch --title (writes VERSION and opens this entry's
heading, leaving the body to the author - the same structure-by-tool/prose-by-author split as
new), notes (prints one entry, so the release workflow never parses markdown in shell), and
check.
version check is the one networked command in wikitool. The research note in TODO.md
argues that a network call breaks all four properties every other command has - offline,
deterministic, free, testable - and it does. It is allowed here on narrow terms: its own
command that nothing else calls implicitly, no API key, a timeout, the fetch behind an
injectable function so the tests never touch a network, and a feed that cannot be reached is
reported as an error rather than silently answered as "up to date". The argument that ruled
out a Perplexity call was about a paid, keyed API; an unauthenticated GET on a release feed
is a different thing.
dist export now ships VERSION and writes .wikitool-release.json. The stamp holds the
version, the export date, the origin (repo, commit, release page, update feed - supplied by
flags, because export still never calls git), and a sha256 per exported file. Nothing reads
the digests today: they record what the machinery looked like when it was installed, which is
the only thing a later dist upgrade could use to tell a file the instance edited from one it
merely received. Writing them now is what keeps that upgrade from needing a format change.
CI: .gitea/workflows/ci.yml on push/PR to main, runs-on: linux-docker, one job that
stops at the first failure - the four steps TODO.md specified (tests, lint/docs verify/instructions verify over the dev tree, dist export, then instructions verify
against the export) plus two the versioning makes possible:
- a version gate: if the pushed range touches stack paths,
VERSIONmust have moved andCHANGES.mdmust say why. It cannot block a push that already landed, but a release can never contain a change nobody named. - the export smoke test now replays
setup-instance.mdend to end (git init, identity, venv,instructions sync,index rebuild,sources rebuild-index,doctor,docs verify,instructions verify,lint) instead of running one verify against it. What that tests is the artifact as an artifact: the documented path from tarball to working instance.
Release: .gitea/workflows/release.yml, triggered by a push to main in which VERSION
changed. It tags v<version> (the CI tags, never an agent - AGENTS.md invariant 5 is
untouched), exports with the origin flags filled in, and publishes
llm-wiki-stack-<version>.tar.gz plus a .sha256 as Gitea release assets, with wikitool version notes as the release body. The tarball is exactly a dist export tree, so
INSTALL.md's Weg A now starts from a download instead of requiring a checkout of this repo.
Both workflows were written against the wiki's own record of the runner (the Act Runner
and Gitea Actions pages), which corrected two guesses. Authentication is
${{ gitea.token }}, the short-lived per-job token this instance issues - not
secrets.GITEA_TOKEN, and not a 1Password secret: nothing in these two workflows leaves the
instance, so the Zero-Trust secret path the container-build workflows use has nothing to carry.
And the job image is named explicitly (container: image: debian:trixie-slim, the base
Szenario B already proves works with actions/checkout@v4) rather than inherited from the
linux-docker label, because the label-to-image mapping is the one thing the wiki does not
record - and it decides whether python3 exists at all. Trixie's python3 is 3.13, past the
3.11 floor doctor enforces.
Deliberately not built yet: applying an update. Detection tells an instance a newer stack
exists and whether it crosses a compatibility boundary; INSTALL.md documents the manual
procedure. dist upgrade is a command that writes into an instance that already has content,
which is a different risk class and wants the stamp's digests to exist first - they now do.
Also: doctor gained a stack-version check (missing VERSION is a WARN, since instances
exported before this release are still perfectly functional; malformed is a FAIL). docs verify gained the VERSION/CHANGES.md agreement check. version show/check/notes are
exempt from the Iteration Budget Gate as read-only; version bump is not.
Verification: 585 tests pass (61 new), and the CI job's own sequence was replayed locally
end to end against a real export - instructions sync, index rebuild, sources rebuild-index, doctor, docs verify, instructions verify, lint --fail-on-error,
version show - all clean in a tree with no git history and no content.
version check was also run against the live feed. It returned 404, which was first read as
"no release published yet" - wrongly. This repository is private, and Gitea answers an
anonymous caller with the same 404 for an invisible repository as for one that does not exist;
the repo endpoint returns it for a made-up repository name too. So $WIKITOOL_UPDATE_TOKEN is
not the exception here but the requirement, until the repository is made public. The mechanism
was unaffected - the variable exists and the 401/403 message already names it - but the two
documentation claims that said otherwise were wrong and are corrected.
That run also found a real bug, now fixed and regression-tested: version notes
terminated an entry at the next versioned heading, and every entry below 0.1.0 is headed by
a date - so the newest entry ran to the end of the file, and the first release would have
shipped the entire changelog as its notes.
What no local run can prove is the runner itself: whether linux-docker accepts a job that
names its own container image, and whether ${{ gitea.token }} carries the rights to create a
release. The first run on the server answers both.
2026-08-29 - Human docs catch up, and a new instance is asked which language it wants
Author: Claude Code
Auditing README/INSTALL/EVALS/tools/README against the migration turned up one thing that was not a documentation gap at all.
A dist export instance inherited German silently. The exported distribution ships
kb/CONTRACT.md § Language, instructions/german-terminology.md and the German section names in
sections.py - verified by exporting one and grepping it. setup-instance.md has decision points
for author identity and remote, both of which refuse to be guessed from the source repo on the
grounds that it is "a different person, a different project". The KB language is the same kind of
choice and was being passed along without being asked. It is now step 5, placed before the tool
environment so the answer lands before the first ingest - after that, changing it is a migration
of every existing page rather than an edit to one contract section.
The step names what actually has to change for another language (contract section, tone examples
and hedge words, four page templates, sections.py canonical names with the old ones kept as
aliases, relationship labels) and what does not: the prose/identifier split itself is not a German
rule. Titles, wikilink targets, cite-ids, enum values, tags, commands and paths follow no KB
language in any instance.
Documentation brought current: README gains the language paragraph it lacked entirely, the
title rule under Tips → Naming, german-terminology.md in the layout tree, and German relationship
labels where it still listed depends-on, uses, implements. INSTALL gains the language decision in
Weg A. tools/README.md gains sections.py and a design note on why heading names are a
vocabulary with aliases rather than literals.
One correction worth its own line. tools/README.md claimed tests "use tmp_path fixtures and
monkeypatch config paths rather than touching the real kb/". Two do not: test_types_cmd.py
and test_index_build.py resolve the real types/ on purpose, since what they assert is that
behaviour comes from the type-specs. That sentence is why editing layout: title: failed six tests
that looked unrelated - and why the first fix attempt in this session anchored a test on a literal
heading that the next commit promptly translated. The README now says so, and says what such a test
may pin: structure, never wording.
2026-08-29 - The corpus is German; the glossary outlives the run
Author: Claude Code
All 248 kb/ pages are translated, no English template heading remains anywhere, and
source_language: is set on all 38 source pages. wikitool lint reports clean on all 21 checks.
The translate-kb-de workshop is closed.
What survived the closure, and why. work/CONTRACT.md requires a run's durable conclusions to
be out of work/ before the directory is deleted. Of the run's four files, three were bookkeeping -
unit plan, checklist, subagent brief - and died with it. The glossary did not: it is not a migration
artifact. Every future ingest of an English source produces German prose and meets the same
questions, so it is now instructions/german-terminology.md,
linked from kb/CONTRACT.md § Language. The rule stays in the contract, the vocabulary moved to the
instruction layer - a word list is lookup material, and 80 lines of it do not belong in a file
loaded on every write.
What the migration cost, and where. The mechanical checks - frontmatter, H1, wikilink and
cite-id multisets compared against HEAD - found four defects in 248 pages: a dropped citation
that silently unsourced a claim, a dropped wikilink, an invented one, and one translated H1. Three
of those four would have passed a set-based comparison; only counting occurrences caught them. None
would have been reported by lint, which checks whether references resolve, not whether one went
missing.
The checks found nothing else, and that is the more useful result: every remaining error was a
plausible, consistent, wrong word. „Geheimnis" for Secret (96 occurrences), „Fähigkeit" for
Skill and Capabilities, „Anspruch" for claim, and 182 sentences in the polite „Sie" form the
English imperative invites. Structural verification cannot see any of these - each page is
internally perfect - and neither can a subagent that made the choice consistently.
Where the errors actually came from. Nine glossary revisions were needed, and the pattern held
throughout: the agents followed the instructions, and the instructions were ambiguous. A
counter-check for capabilities was written backwards and would have propagated through eleven
units. "Separation of Concerns" was listed as a choice between two options rather than as a fixed
phrase, and one agent took the other option and reported doing so. "Fehler melden, nicht
korrigieren" meant errors in the source, and an agent applied it to its own bad German. Cheap
parallel agents do not degrade an instruction gracefully - they execute both readings of it at once,
in the same batch, which makes every ambiguity immediately visible and immediately expensive.
2026-08-29 - Two frontmatter fields no command can write
Author: Claude Code
Both surfaced while closing the translation run, and both are recorded rather than fixed - the right shape for either is a design question, not a patch.
source_language: has no writer. The field was added to types/source.schema.yaml this
morning, and wikitool new source --set source_language=de sets it at creation. On the 37 source
pages that predate it there is no path at all: touch writes exactly modified/date, summary,
provenance and confidence_base, and nothing else offers a generic frontmatter setter. Backfilling
meant editing the field directly. That is defensible for a plain data field - it is neither generated
nor a page-reference array, so AGENTS.md invariant 1 does not reach it - but "add a schema field"
and "be able to set it on the pages that already exist" should not be separate capabilities. Adding
a field to a schema is currently a half-migration by construction.
comparison declares no modification date. touch --page "amd-pstate vs acpi-cpufreq" fails
with "Type types/comparison.md declares no modified/date field - pass --no-date to skip it",
because the schema has only created. So a comparison page can record when it was made but never
that it was revised - and touch --summary on one requires --no-date every time. Either
comparison should gain modified: like entity and concept, or the omission should be
documented as deliberate; right now it reads as an oversight that the error message papers over.
Neither blocked the run. Both are the kind of gap that only shows up when something walks the whole corpus, which is exactly what a migration does and what routine per-page work never will.
2026-08-29 - Date fields hold dates, and the special case disappears
Author: Claude Code
Supersedes this morning's ISO-date exemption, which is now deleted. That fix worked by teaching
_round_trips_as_string to recognize a date-shaped string and leave it bare - a second place
that had to know what a date looks like, kept in sync by hand with the schemas and with
validate_frontmatter's normalization.
The question that removed it: why is there a date-shaped string at all? _format_scalar has
always rendered a datetime.date bare, and yaml.safe_load yields exactly that for every page on
disk. Only two places ever put a string in a date field - touch and new, both via
today_iso() - and the guard existed solely to paper over that mismatch. Both now write
datetime.date, the exemption is gone, and no code outside _format_scalar needs an opinion about
date syntax.
Three things fell out of it:
touchno longer reports a change on every run. It comparedfrontmatter.get(field)- adatetime.dateread from disk - against a string, which is never equal, so an unchanged date was rewritten and logged as a change every time. With both sides dates, an unchanged page is now correctly a no-op.--dateand--set <date field>=validate their input. Previously any string was written through;29.08.2026would have landed in frontmatter and surfaced later as a schema error on a page nobody was looking at. Both now fail immediately, naming the expected shape.normalize_datesmoved tofrontmatter_ioand is shared. There were two validators and only one of them converted dates:TypeResolver.validate_frontmatter(behindlint) did,touch'svalidate_fieldsdid not. Harmless whiletouchwrote strings, and a latent failure the moment it stopped - found by writing the change rather than by the tests.
On the question that prompted this ("can the quoting be schema-driven - we know what belongs
there"): the instinct is right, but the schema does not distinguish the two cases. A tags[] entry
and modified: are both declared type: string; keying on the declared type gives dates the same
answer as "1945", which is the wrong one. Only format: date separates them, and reaching it from
frontmatter_io would mean importing type_resolver, which already imports frontmatter_io.
Making the value's own type honest answers the same question without a lookup - and
_build_frontmatter was already schema-driven on format == "date", which is where that knowledge
belongs.
2026-08-29 - The round-trip guard reached date fields it should not have
Author: Claude Code
Follow-up to the frontmatter fix earlier today, and a reminder that a correct-looking rule can
still be wrong in context. _round_trips_as_string quotes anything that would read back as a
different type - which is exactly right for a "1945" tag, and exactly wrong for modified:.
PyYAML reads a bare 2026-08-29 as a datetime.date, so the guard started quoting it, and 53
pages came out of one batch carrying modified: '2026-08-29' while the other ~195 still had it
bare.
Nothing broke: TypeResolver.validate_frontmatter already converts date objects to strings before
validating, precisely because the schemas declare type: string while YAML hands back a date. That
normalization is the corpus convention, and the guard had quietly voted against it. The result
was not corruption but something more corrosive - a field with two spellings that alternate
depending on whether touch happened to change the value, so every later diff carries noise
nobody can attribute.
Full ISO dates (^\d{4}-\d{2}-\d{2}$) are now exempt from the guard, and the 53 pages are
normalized back to bare. The exemption is deliberately narrow: 1945 is an int to YAML, not a
date, so a tag by that name still gets quoted.
Found by reading the Mass-Update Gate's per-file churn: two of 32 translated pages had asymmetric
line counts where every other page was symmetric. Chasing that asymmetry - it turned out to be
harmless line rewrapping - surfaced the modified: line in the same diff.
2026-08-29 - Frontmatter round-trip turned string values into other YAML types
Author: Claude Code
_format_scalar decided quoting from a hardcoded list: empty, leading special character,
surrounding whitespace, ": ", or one of true/false/null/~. A string that merely looks like a
number was not on it, so the Python string "1945" was written bare and read back as int on the
next load. wikitool touch rewrites the whole frontmatter block, so translating a page was enough
to corrupt a tag it never mentioned: Vannevar Bush came out of a --summary update failing
schema validation on Field 'tags[3]': 1945 is not of type 'string'.
The list was also short in a second direction. yaml.safe_load follows YAML 1.1, where yes,
no, on and off are booleans, and it resolves 0x1F, 1_000, 1e5 and +3 as numbers -
none of which the check knew about.
Rather than lengthening the list, _round_trips_as_string(text) now asks the loader whether
yaml.safe_load(text) == text, and quotes when it does not. Reader and writer agree by
construction, and a resolver rule this code has never heard of cannot drift out from under it.
Two pages in the corpus were one touch away from the same corruption - Memex (1945) and
10.000 Dice Game (10000) - which a translation run touching all 248 pages would have reached.
Vannevar Bush is repaired.
How it was found is the reusable part. The wikitool lint run at the end of a unit reported a
schema error on a field the unit had not edited. A page that changes in a way nobody asked for is
worth chasing even when the visible diff looks right, and the check that caught it - lint after
every unit, before publish - is now in the run's README rather than in anyone's memory.
2026-08-29 - work new --key for a run that is not an ingest
Author: Claude Code
The workshop layer assumed every task too big for one session was an ingest: work new required
--input <raw path> and derived the run key from it. A translation pass over the 248 existing
kb/ pages has no raw input at all, so it could open no workshop - and the alternative was worse
than cosmetic. Without plan.md there are no written unit boundaries, and instructions/gates.md
allows a fresh WIKITOOL_SESSION_ID only at a boundary recorded there; a run with no plan
either exhausts one 30-call budget or takes new session ids on no authority but its own.
work new --key <run key> names the key outright. Exactly one of --input/--key is required,
and a --key may not start with ingest-, which stays reserved for derived keys so a directory
name keeps saying which kind of run made it. plan.md gets a second template for these runs -
units sized by the iteration budget rather than by raw_files count, since there is no tree to
cut.
Also worth recording, because it will happen again: adding a parameter broke seven tests that call
new_command(...) directly. Bypassing Typer means an omitted argument keeps its
typer.Option(...) sentinel rather than becoming None, so a mutual-exclusion check saw both
options as supplied. test_git_publish.py already documents this trap in _publish; the fix is to
pass every parameter explicitly, not to loosen the check.
2026-08-28 - Two bugs the translation sample run surfaced
Author: Claude Code
Both found by translating six pages by hand before turning subagents loose on 248. Neither is about language; both were latent and would have been amplified by the bulk run.
touch overwrote a source's publication date. DATE_FIELDS = ("modified", "date") picks
whichever field the type declares, and a source declares date: - which the schema documents as
"Publication or creation date". So touch --summary on a source page silently rewrote the raw
material's own date to today, leaving the page contradicting the **Datum:** line printed in its
own body. Rewriting a summary is a statement about the page; the source's date is a fact about the
world, and today's date is simply wrong for it. modified: is still bumped automatically; date:
now moves only on an explicit --date. Caught on one page during the sample, it would have hit
all 38 source pages in the bulk pass.
The Mass-Update Gate misreported any file with a non-ASCII name. _changed_files reads
git status --porcelain -z, which emits raw paths; _numstat read git diff --numstat without
-z, and git renders such a path quoted ("kb/sources/Source - 10.000 ausw\303\274rfeln…"). The
lookup missed, the file fell through to _untracked_stat, and a rewrite was reported as a pure
insertion: +54/-0 for a change git counted as +28/-27. This is the same failure mode as the
718-line deletion reported as -174 on 2026-08-27, reached by a different route - the report
understating removals in the one direction a reviewer most needs it not to. _numstat now passes
-z and parses NUL-delimited records, including the empty-path-plus-two-records form git uses for
a rename. Regression tests for both are in test_git_publish.py and test_touch.py.
Worth noting how this one was found: the gate printed the wrong number in a clearance report that was being read carefully because it was about to be approved. The numbers a gate prints are the gate.
2026-08-28 - Section headings become a named vocabulary instead of hardcoded English literals
Author: Claude Code
The language policy earlier today froze all section headings as English, on the grounds that
xref add and cite add match three of them literally and a translated heading would silently
append a second section. That reasoning was sound but the conclusion was too cheap: it left
every page with an English skeleton around German prose, which is most of what "the wiki is in
German" is supposed to mean. Tracing the literals showed the constraint was smaller than it
looked - three constants in two files, with page_ops.py matching bullet shapes rather than
headings and not involved at all.
wiki_tools/sections.py now owns the vocabulary. Each heading has one canonical name - what
the tool writes - and aliases it still recognizes:
| Canonical | Recognized alias |
|---|---|
## Beziehungen |
## Relationships |
## Siehe auch |
## See Also |
## Fußnoten |
## Footnotes |
That asymmetry is the point: a 248-page corpus cannot be translated atomically, so a page still
carrying the English heading has to keep working. xref add finds and appends to it rather than
creating a duplicate, and only a page's own translation changes the name.
A regression the tests caught, worth recording. Making render_page_body write the canonical
heading turned cite sync into a silent migrator: every page with an English Footnotes block came
back changed, so one cite sync --all would have rewritten the whole wiki and buried the real
diff. split_cite_block's counterpart cite_block_heading(body) now reports the heading the page
actually carries, and the three call sites pass it back into render_page_body. Renaming a
heading is the translation pass's job, never a side effect of another command. cite sync is a
no-op on an untranslated page again, and a test pins that.
Also here: the four page-type templates now scaffold German sections; **Type:** becomes
**Typ:** while the value stays the schema enum that search --field filters on; and
kb/CONTRACT.md's typed relationship labels (depends on → hängt ab von, …) are German, which is
safe because no code matches on them - an untranslated label is stale wording, not a broken
reference.
Not covered here: the 248 existing pages still carry English headings and prose. They are recognized, not migrated.
2026-08-28 - Canonical KB language: German, with an identifier carve-out
Author: Claude Code
The wiki had no language rule anywhere - not in AGENTS.md, not in the contracts, not in the
skills, not in the schema. The 248 existing pages were English purely by LLM habit, and a German
raw file (raw/documents/wuerfelspiel.md) had already been
summarized into English with its German terms preserved in quotes. That is a defensible outcome
reached by accident, which means the next ingest could just as well have gone the other way.
The rule now lives in one place, kb/CONTRACT.md § Language: pages are written in German,
and every line of a page is either prose or an identifier - only prose is translated. The
carve-out is not a list of exceptions bolted onto a language rule; it is the rule's second half,
and it is what makes the policy mechanically safe:
- Page titles and the H1 that repeats one follow the subject's own established name, not the
KB language.
GitOps Ownership ModelandAct Runnerstay. This keeps titles - and with them[[wikilinks]], the[^cite-id]ids derived from them, and every frontmatter reference array - out of the language question entirely.lintalready reports an H1 that stops matching its title. - Section headings stay English because
xref addandcite addlocate them by literal name (commands/xref.py:75,79,commands/cite_cmd.py). A translated## Relationshipsdoes not error - it makesxref addappend a second section, silently. - The generated
**Type:**line renders a schema enum value thatsearch --fieldfilters on. tags:are search keys; commands, paths, config keys and code are what they are; quotations stay verbatim in the source's own language.
Consequential edits in the same contract. § Confidence prescribed the literal hedge words "possibly"/"may"/"uncertain"/"unconfirmed", and § Tone's buzzword and AI-filler lists and its good/bad example pair were English. Those are words meant to appear in pages, so under a German policy the contract was instructing against itself. All now German.
Sources in another language are summarized in the KB language regardless of what the raw file
is in - a source page is evidence about a source, not a substitute for it. New optional
source_language: (ISO 639-1, types/source.schema.yaml) records what had to be translated, and
makes "which pages are translations" answerable with search --field source_language=fr. Optional
by design: the 38 existing source pages predate the rule and stay valid.
The four page type-specs (entity, concept, source, comparison) have German prose now -
they are the authoring surface that shapes kb/, and types describe dumps their body verbatim to
whoever is writing a page. Their ```markdown template fences are untouched, for the heading reason
above. Note that extract_template (type_resolver.py:190) takes the first markdown fence in the
body, not the one under ## Template, so a translated section must never introduce a markdown
example block above it. The control plane - AGENTS.md, the stage contracts, instructions/, and
the type-specs for non-page artifacts - stays English.
Not covered here: the 248 existing pages, which are migrated separately, and layout: title: in
types/entity.md (the generated INDEX headings), which moves with that migration since it forces
an index rebuild.
2026-08-28 - Mass-Update Gate: --yes retired for an exit-42 clearance code and --confirm <token>
Author: Claude Code
The 2026-08-23 procedural fix - "run publish without --yes first, let the gate refuse, then
re-run with --yes" - failed a third time on 2026-08-27: an agent asked "want me to publish?",
got "Ok", ran publish (refused), and ran publish --yes in the same turn. The documented
order was followed - refusal before --yes - but no human turn happened in between, so the
approval the trace showed was the agent waving itself through. A procedure written down in
advance is a ritual an agent can perform without a human in the loop, and eval score's
gate-not-self-opened could not see the difference because it only ever looked at single calls.
What shipped, and the false start that shaped it. The first attempt made the gate
un-self-openable: a ticket bound to the changeset, redeemable only after a prompt.submitted
trace event and a wikitool approve <id> typed in a second terminal. It worked and it was
unusable - on a harness driving wikitool through a shell tool the agent cannot produce the turn
signal at all, and the human is asked to leave the conversation to approve anything. It was built,
tested, and then deleted; a gate nobody can pass is not a safe gate, it is an abandoned one. What
replaced it splits the problem in two: cheap honest enforcement in-process, and the real check in
the eval layer.
- A distinct exit code.
EXIT_NEEDS_CLEARANCE = 42(commands/_util.py, vianeeds_clearance()) is now a third outcome beside success (0) and validation error (1), meaning "a human has to see this output before anything proceeds". An agent, a hook, a CI job and a trajectory scorer can each tell it apart from "your input was wrong, fix it and retry" - which exit 1 could never express.AGENTS.md's tool error contract lists four outcomes now, not three. - The procedure moved into the tool's output. A tripped gate prints what would change, the
full counted file list, and the exact copy-pasteable
--confirm <token>line that publishes it (git_publish.clearance_message/rerun_command, the latter reconstructing the command line rather than echoing argv so it is correct however the original was spelled, andshlex-quoted).instructions/gates.mdnow says only "exit 42 -> show the user the output, stop" and nothing about the mechanism - deliberately, since a recipe in the instruction layer is one an agent can complete alone. --confirm <token>replaces--yes. The token is a 12-hex digest of the counted file list plus the publish target (changeset_token), so a clearance never carries to a changeset the user did not see - touch one more file and the token changes and the gate asks again. This closes a hole--yesalways had: it published whatever was in the working tree when it ran. A wrong, invented or superseded token exits 42 again with the current list.--yes/-ystay registered only to fail with an explicit error instead of a Typer usage error.- Stateless. No ticket file, no TTL, no session bookkeeping, no
approve/approval statuscommands, and nothing undertools/.wikitool_session/to get out of sync - the token is a pure function of the working tree and can be recomputed at any time. - The real check is in the eval layer, where a whole trajectory is visible instead of one
call (
evals/trajectory.py):clearance-ended-the-turn(error) flags awikitool.callbetween an exit-42 call and the nextprompt.submitted- precisely the "refused, then confirmed in the same breath" shape all three incidents had;clearance-was-asked-for(error) flags agate.clearedwhose token nogate.refusedever issued, catching both an invented token and one reused from a different changeset.gate-not-self-openedstill hard-flags--yes/-y. Rules can now report a third state, skipped (Rule.skipped/skip_reason, renderedskip), soclearance-ended-the-turnsays "cannot say" on a harness with no prompt hook rather than fabricating a pass - the degradation rule, applied to a rule that genuinely needs an optional event. - Telemetry:
gate.cleared(token, counted, threshold, files) is new;gate.refusedgainedreason: needs-clearance|stale-token,tokenandpresented_token. The ticket-eragate.ticket_issued/gate.consent/gate.approvedare gone. - Harness adapters.
.claude/settings.json(new, tracked, and copied bydist exportas a single file viadist_cmd.SINGLE_FILESso the generated.claude/skills/is not swept in with it) wiresUserPromptSubmittotrace_ingest.py, which is what makesclearance-ended-the-turnscorable here, and adds apermissions.askrule onBash(tools/wikitool publish --confirm:*)so the clearing call prompts in-chat while ordinary publishes do not. Verified live against this CLI: aPreToolUsehook returningpermissionDecision: "ask"does not override a matchingpermissions.allowrule (permissions beat hooks), which is why the rule is apermissions.askentry and not a hook - and it is a prefix match, sorerun_commandalways emits--confirmfirst. Copilot CLI already emitsuserPromptSubmitted, so it needed no change; its decision-document schema stays unverified and therefore unwritten, per the rule the Vibe adapter established. - Honest limits, recorded rather than papered over. None of this proves a human typed
anything: the token sits in the agent's own context.
prompt.submittedproves a turn boundary, not that the output was relayed or that the reply meant yes. Closing that needs the harness to report what the agent actually said, which no adapter here does. - The clearance report is grouped and sized, not a flat list of paths. A 30-line alphabetical
list tells a reviewer nothing about what kind of change it is. The refusal now opens with a
scale line (file count,
+added/-removed, status breakdown), then only the attention notes that actually apply - deletions named individually, how many files touch the agent control plane or harness config, how many published pages change, the largest single file, any binary - and then every counted path grouped by area (FILE_GROUPS), each with its status letter and churn. Ordering is by consequence, not alphabet: published knowledge and the control plane first, mechanically-regenerated files (is_generated) split into their own group at the end and labelled as needing no review. Deliberately notes, not a risk score: every line is a checkable statement a reader can disagree with by looking, where a number would invite trusting it. Only applicable notes are emitted, since a wall of "0 deletions" reassurances is how a reviewer learns to skim. Churn comes fromgit diff --numstat HEADfor tracked files and a line count for untracked ones, with binaries marked rather than given a fabricated count; a repository with no HEAD yet (the first-commit case insetup-instance.md) degrades to counts instead of failing. - The token now covers file contents, not just file names.
changeset_tokendigests each counted path with a hash of its bytes, closing the same "approved A, published B" hole one level down: previously a cleared list could be published after rewriting one of those files, so the user's approval would have covered text they never saw. Editing a file after clearance now invalidates the token exactly as adding one does. - Deletions report their size.
collect_changesoriginally short-circuited a deleted file to0/0churn instead of askinggit diff --numstat, which knows it. The first changeset to exercise this deleted a 718-line file and went out headlined-174against git's own-891- four fifths of the removals missing, in the one direction a reviewer least wants understated. A deletion now carries its real removed-line count and renders as-718 deleted, so a one-line stub and a long document stop reading identically. - The clearance message's wording, fixed after it failed on first contact. It originally read
"SHOW THIS OUTPUT TO THE USER"; the first agent to receive it replied with a file count and a
pointer to "the output above" - which the user could not see, because on a harness that runs
wikitoolthrough a shell tool a command's stdout lands in the agent's context, not on anyone's screen. Printing and showing are different acts there, and an instruction that conflates them reads as already satisfied the moment the text exists. The message now leads with "THE USER CANNOT SEE THIS OUTPUT", asks for the file paths to be copied into the reply, and names the near-misses that do not discharge it (a count, a summary, "the output above").gates.mdgot the same treatment. Worth generalising: an instruction to an agent should name the act, not the intent. - Also updated:
instructions/gates.md(rewritten around the exit code, with an exit-code table),instructions/publish-cycle.md,tools/CONTRACT.md(publish command + error-contract rows),AGENTS.md(invariants 5-6, tool error contract),README.md,INSTALL.md,instructions/setup-instance.md, thewiki-ingest/wiki-lintskills,tools/README.md,EVALS.md(L2 rule table, the worked example rewritten around where the check moved to and why), and - as wiki content -kb/concepts/Mass-Update Gate.mdandkb/concepts/Iteration and Cost Limits.md.
2026-08-26 - One-way dev/dist split: instructions/dev/, stack-dev skill; dist enable-dev retired
Author: Claude Code
Replaced the "dev instance" concept from the previous entry with a one-way model: a distributed instance never becomes a dev instance again. Instead of restoring dev-only content into a distributed instance, stack development happens in the origin repo (or a new dev instance exported from it).
tools/wikitool dist enable-devis gone, along withtools/wiki_tools/dist_templates/agents_dev_extras.md/readme_dev_extras.mdandinstructions/enable-dev-extras.md(themanual: trueinstruction it backed). There is no longer a command that reconstructs dev-only content in a distributed instance -docs verify's matching drift check (check_dev_extras_in_sync) is removed along with it.- New
instructions/dev/- a second, purpose-scoped location for material that is exclusively about developing the tool stack itself (compiler/schema/instruction-layer changes), as opposed to operating on wiki content.tools/wikitool dist exportnow prunes this directory wholesale and unconditionally (dist_cmd.INSTRUCTIONS_EXCLUDE_DIRS) - a second, independent exclusion mechanism alongside the existing<!-- dist:strip-start/end -->marker convention, which only ever removed content inside an otherwise-shipped file. The commonplace/ vendored-knowledge-base guidance that used to live inline in AGENTS.md's marker block moved intoinstructions/dev/commonplace-kb.md. - New
stack-devskill, nested atinstructions/dev/stack-dev/SKILL.md- switches a session into tool-development mode: source-binding rules for wiki content do not apply to code, and the test/review conventions underinstructions/dev/apply instead. Nested rather than a top-level sibling of the five content skills specifically so the one export exclusion removes it together with the instructions it routes to, with no separate skill-exclusion list. Linked from AGENTS.md via a routing line inside its owndist:stripblock - normal (notmanual: true), since it is a recurring session type, not a rare explicit request. tools/wikitool instructions verifygained a boundary check: nothing outsideinstructions/dev/may reference something inside it (instructions_cmd.dev_only_forbidden_references), because such a reference would dangle the momentdist exportprunes the directory. A mention inside adist:stripblock is exempt -dist_cmd.strip_markers()removes it from the scan before the check runs, sincedist exportremoves the block and the directory together.instruction_files()/skill_dirs()were extended to discoverinstructions/dev/as a second scan location (previously non-recursive by design).- Canonical rule: instructions/CONTRACT.md. Also
updated:
AGENTS.md,README.md,INSTALL.md,instructions/setup-instance.md,tools/CONTRACT.md.
2026-08-25 - Citation syntax migrated to real GFM footnotes; wikitool cite
Author: Claude Code
Replaced the inline ^[[Source - X]] (or ^[[Source - X|file.md]]) citation marker - a
Pandoc-style parser reads it as an inline footnote wrapping a broken shortcut link, so it never
rendered correctly outside this repo's own tooling - with real GFM footnotes: an inline
[^cite-id] reference plus a [^cite-id]: [[Source - X]] definition in a tool-owned ## Footnotes block at the end of the page. The qualifier for a multi-file source now lives in the
wikilink's alias slot ([[Source - X|storage-model.md]]).
tools/wikitool cite id/add/sync- new command group.cite idpreviews the deterministic idcite_id(title, qualifier)derives (strip theSource -prefix, transliterate to ASCII, slugify,---join title and qualifier,-2/-3suffix on a per-page collision);cite addupserts a page's Footnotes definition and its frontmattersources:entry and prints the marker to paste;cite syncprunes definitions nothing references any more, re-renders the block in first-reference order, and reports any[^id]reference left undefined.cite idis read-only and exempt from the Iteration Budget Gate.wikitool linthard-errors on three new findings:legacy_citation_markers(a leftover pre-migration^[[...]]),undefined_footnote_refs(a[^id]with no definition), andorphan_footnote_defs(a definition nothing references) - the same class asbroken_links, not an advisory.wikitool renamenow also refreshes a citation id derived from the old title (in both its Footnotes definition and every reference to it) alongside the wikilink retargeting it already did; a hand-picked or otherwise-derived id is left untouched.- Migration: all 71 kb/ pages carrying the legacy marker were rewritten by a one-off script
(not committed - scratch tooling for a single run) that imports
cite_id()fromwiki_tools.provenanceso it can never mint an id the tool itself wouldn't, and refuses to write any page where the multiset of (source title, qualifier) pairs cited before and after migration doesn't match exactly.raw/and the historical entries inCHANGES.md/kb/log.mdwere deliberately left untouched. - Canonical rule: kb/CONTRACT.md. Also updated:
types/entity.md,types/concept.md,types/source.md,kb/sources/COLLECTION.md,instructions/page-lifecycle.md,instructions/wiki-ingest/SKILL.md,instructions/wiki-manage/SKILL.md,AGENTS.mdinvariants 1 and 2,README.md.
2026-08-25 - Distributable instances: dist export/dist enable-dev, doctor, per-instance author
Author: Claude Code
Added a reproducible way to turn this repo into a fresh, contentless wiki instance, instead of manual copy-and-strip:
tools/wikitool dist export <target>writes a contentless copy of the machinery (AGENTS.md/README.md/EVALS.mdwith dev-instance-only content removed,instructions/,types/,tools/minus venv/caches, the.github/hooks/+.vibe/session-tracing config, everykb/*/COLLECTION.mdwith no pages or areas, emptyraw/{articles,documents,notes,assets}/) into an empty target. Never touches git.- A generic
<!-- dist:strip-start -->/<!-- dist:strip-end -->marker convention replaces the one-off "strip the commonplace section" idea:dist exportremoves whatever sits in a marked region, andtools/wikitool dist enable-devis the exact inverse - it adds the vendoredcommonplace/knowledge base back into an existing instance fromtools/wiki_tools/dist_templates/, on request.docs verifychecks the templates never drift from what this repo (itself a dev instance) actually has. tools/wikitool doctor [--json]is a single read-only health check (dependencies, author resolution, git identity/branch/remote, published skills, structure, generated files, session id) backing the new setup instruction instead of ten checks spelled out in prose.- Per-instance author.
author:on a new source page now resolves from$WIKI_AUTHOR, else this instance's owngit config user.name(config.default_author()) - never a hardcoded name. Neither resolving is a hard failure (ERROR, exit 1), not a silent placeholder. - A third instruction-reference tier.
instructions/*.mdfrontmatter gainedmanual: true: exempt frominstructions verify's "must be referenced" rule, and inverted - it must NOT be linked from AGENTS.md or a skill (though a CONTRACT.md mentioning it as documentation is fine). For a rare, deliberate procedure that must never run implicitly. - New instructions: instructions/setup-instance.md (the normal
path for a new instance: git init, author/remote decision points, bootstrap, first commit) and
instructions/enable-dev-extras.md (
manual: true, addscommonplace/back on explicit request only). - New INSTALL.md for humans: prerequisites, the two paths (new instance vs.
cloning this repo), configuration,
doctoras the verification step, troubleshooting.
2026-08-24 - README brought current; keeping the human docs current is now written down
Author: GitHub Copilot
README.md had drifted behind six stack changes. What was wrong:
- The architecture tree was missing
work/,EVALS.md,.github/hooks/and.vibe/, and still describedreports/as holding only lint reports. - It claimed to be "the only README in the repo", which stopped being true when
the naming rule changed to let a stage carry both a
README.mdand aCONTRACT.md. - Two links sent readers to
tools/README.mdfor the command reference and the maintenance schedule. Both moved totools/CONTRACT.mdwhen the command table was consolidated - anddocs verifynow enforces that the README has no copy of it. wikitool skills syncwas renamed toinstructions sync.- Lint reports were said to land in
kb/reports/. - The Search section advised a third-party tool for wikis past 100 pages,
written before
wikitool searchexisted.
Added: a Telemetry and evaluation section pointing at EVALS.md.
AGENTS.md's Changelog section now says that a stack change is not finished
until the human docs describe it. It went there rather than into the Invariants
list on purpose: an invariant holds in every session regardless of task, and this
one only applies when the stack changes - a wiki-ingest or wiki-query session
never touches a README. The Changelog section is already the place where the
"record what changed" rules live, so it is one sentence in an existing home
rather than a new rule competing for the always-loaded budget.
2026-08-23 - L0: the pipeline against a wiki the tools built themselves
Author: GitHub Copilot
tools/wiki_tools/tests/test_pipeline_l0.py builds a wiki through the CLI -
new → write → xref add → index rebuild - against an empty tree, then
asserts that lint finds no hard errors, that the catalog covers exactly what
was created, and that rebuilding it again changes nothing on disk.
It closes the last gap the eval ladder rested on. Nothing in the suite chained
the commands before: test_lint.py runs lint over a hand-written fixture, and a
hand-written fixture cannot show that new, xref, index and lint agree
with one another.
L0 belongs in pytest, not in a separate harness. The plan had it as a case in
an evals/ stage, but a fixture wiki has to be driven in-process - config.ROOT
is derived from wiki_tools/config.py's own location, so wikitool cannot be
aimed at another tree from outside - and driving commands in-process against a
temporary tree is exactly what the test suite already does. A second mechanism
for it would have duplicated the suite. The evals/ stage is therefore not
created: it exists to hold agent cases and fixtures, and there are none yet.
Two findings came out of writing it:
- A scaffolded page does not lint clean.
newwrites placeholder wikilinks for the author to replace, so a page that was created but not yet written reports broken links. That is the scaffold saying it is unfinished, and it is now pinned by a test so it cannot be mistaken for a defect later. - The shared
kb_dirfixture no longer validates against the type schemas. Five of its pages are missingprovenance,summaryorraw_files, and one carries asourcekey the schema rejects.test_clean_wiki_has_no_hard_errorspasses anyway because it asserts onlybroken_linksandduplicate_titlesrather than the whole hard-error set. Left as found: repairing the fixture touches every test that reads those pages, and that is its own change.
EVALS.md records what an agent runner needs - a repo copy rather than a fixture
directory, an isolated HOME per run, a mandatory --agent so Vibe cannot fall
back to auto-approve, and a run manifest - and why it was not built: no provider
credentials are configured here, so a live run cannot be verified, and the Vibe
adapter is the standing lesson about writing against unverified documentation.
2026-08-23 - Let the gate refuse first, even when approval already exists
Author: GitHub Copilot
instructions/gates.md gains one rule: run publish without --yes first, let
the Mass-Update Gate refuse, then re-run. Approval that arrived earlier in the
conversation is not a licence to pass --yes on the first attempt.
This is the first change made because a trace showed something. eval score
flagged gate-not-self-opened on two consecutive sessions - the one that shipped
the telemetry layer and the one that shipped the scorer. Both times the file list
had been shown and approved; both times --yes went out on the first attempt, so
the gate never spoke. A trace records tool calls, not conversations, which makes
that indistinguishable from an agent waving a gate through on its own.
Two occurrences of one shape is a pattern, not an incident, so it warranted a
decision rather than an explanation. The options were to soften the rule, to give
the rule a way to see approval (an --approved-by flag - but a flag an agent can
set itself is a weaker check than the one it replaces), or to change the
procedure. The procedure changed: the outcome is identical, the record is not,
and the cost is one refused call.
The check that found it stays exactly as it was.
2026-08-23 - wikitool eval: scoring a session against what it left behind
Author: GitHub Copilot
eval score turns a trace into a scorecard, and eval sessions lists what there
is to score. Both are read-only and exempt from the Iteration Budget Gate, for
the same reason search is: reading back what a session did is not iteration on
the wiki, and charging for it would discourage checking one's own work.
Two levels, both hard-oracle:
- L1, structure re-runs
lint's checks in-process. The definition of a hard error moved out oflint_commandintoHARD_ERROR_KEYS, so the scorecard andlint --fail-on-errorcannot drift apart - a run must never pass its score while lint refuses it. - L2, trajectory checks three rules over the trace. Each restates an
invariant the code cannot enforce in-process:
refusal-not-retried,gate-not-self-opened,content-change-logged. A gate can refuse a call, but nothing stops an agent from calling again with the gate's own flag - that gap is what a trajectory check closes.
The rules were not invented from the contract text. Each one corresponds to a
refusal an agent can talk its way around, which is why the gates exist at all.
Further rules wait for observed failures; a scorer built ahead of a failure
taxonomy improves its own number rather than the behaviour, which is the failure
mode commonplace/kb/notes/evaluation-automation-is-phase-gated-by-comprehension.md
describes.
Scoring the session that shipped the telemetry layer immediately reported a
violation of gate-not-self-opened: publish --yes went out on the first
attempt, so the gate never got to refuse anything, even though the file list had
been shown and approved in conversation. The finding is true and benign at once -
the trace records tool calls, not conversations. It stays an error rather than
being softened, because weakening a check to make a known case pass is how a
scorecard stops meaning anything. EVALS.md carries the worked example.
Two supporting changes:
- Every trace now opens with a
session.startnaming what its source can report. Mistral Vibe has no session hook, so its traces would otherwise carry nocompletenessat all and a scorer could not tell "never happened" from "not observable here". The header is written underxmode, which elects a single writer among the processes appending to one trace. telemetry/reader.pyowns reading a trace back - sort order, torn last line, the union ofcompletenessacross sources - because a consumer that re-derived those would drift from the writer.
2026-08-23 - Mistral Vibe hooks, verified against the installed CLI
Author: GitHub Copilot
.vibe/hooks.toml declares the three hooks Vibe has - pre_tool, post_tool,
post_agent - and .vibe/config.toml puts the telemetry policy in the
repository instead of in someone's shell profile.
Vibe is installed here, and it is open source, so neither file was written
against the documentation: both were validated with the CLI's own loader
(vibe.core.hooks.config._load_hooks_file, VibeConfigSchema), and the
committed post_tool command was executed through Vibe's own
HookExecutor with a real PostToolInvocation. Four things that changed as a
result:
- The config shape is an array of tables,
[[hooks]]with a uniquename, not a table per event type as the research summary suggested. - A failing hook cannot block anything. With
strict = false- the default - a crash or timeout is a no-op warning. This is the mirror image of Copilot, where apreToolUsehook is fail-closed and a missing interpreter would deny every tool call.strictandmatchare rejected onpost_agent. post_agentcarries no response text, only the session context, so it maps toturn.endrather than toassistant.message.enable_telemetrydefaults totrue. Setting it false is a real change, not a restatement of the default. It also gates OTel:vibe/core/tracing.pyexports only whenenable_telemetry and enable_otel. Both are off here, becauseenable_otelships prompts and tool arguments to Mistral Studio and Mistral documents no way to point it at a collector you run yourself.
Also confirmed from the source: Vibe already reads .agents/skills/ and
AGENTS.md, so the directory wikitool instructions sync publishes is a
project-scope skill source for it. This repository needs no adaptation to be
worked on with Vibe beyond trusting the folder.
A new test parses every committed hook config - JSON for Copilot, TOML for Vibe -
and asserts that each --event names an event that exists. A typo there would
otherwise produce a silent gap in a trace that nothing else would notice.
2026-08-23 - Copilot CLI hooks and the chronicle importer
Author: GitHub Copilot
The trace layer now has two sources besides wikitool itself.
.github/hooks/wiki-trace.json wires eleven Copilot CLI hook events into
tools/trace_ingest.py. It is committed, so a clone brings its own telemetry;
disableAllHooks opts out without deleting anything. Two details in it are
load-bearing:
- Every command ends in
|| true.preToolUsehooks are fail-closed: a non-zero exit denies the tool call. Without the guard a missing interpreter would turn the observer into a blocker that refuses every tool call in the session. Verified by running the committed command withPATH=/nonexistent. - The event name is passed explicitly. Copilot serves two payload dialects -
camelCase event names give camelCase fields, PascalCase names give the VS
Code/Claude snake_case shape - so
--eventkeeps the mapping independent of which one a config picked. Both spellings are mapped either way.
tools/import_chronicle.py reconstructs a trace from a chronicle store
(session-store.db, shared schema between VS Code Chat and Copilot CLI) for the
surface that has no hooks at all. The store is opened read-only, the import is
idempotent unless --force, and the original timestamps are preserved rather
than replaced by import time - write_event gained a ts parameter for that.
Reconstruction is honest about its limits: those events carry
reconstructed: true, and the completeness list for vscode-chat names
tool.post but not tool.pre, because the store records that a file was
touched, not that a tool was about to run. Ordering follows the store's own
timestamps, which puts a touched file after the reply of its turn; reordering it
would invent a sequence nobody wrote down.
Two events joined the vocabulary. turn.end replaces the awkward use of
assistant.message for Copilot's agentStop and Vibe's post_agent, which mark
the end of a turn and do not always carry text. session.error covers Copilot's
errorOccurred, which can fire for a model call or the system rather than for a
tool.
2026-08-23 - Session traces: the telemetry layer under the eval harness
Author: GitHub Copilot
wikitool now records what a session did. Every command appends a
wikitool.call event to reports/telemetry/<session>/trace.jsonl, and both
gates record the moment they refuse. tools/trace_ingest.py accepts a hook
payload from Claude Code, Copilot CLI or Mistral Vibe and normalises it into the
same vocabulary.
The unit tests check the compiler; nothing checked the agent. Scoring a
trajectory - did new precede the write, was a refused gate simply retried -
needs a record of the trajectory, and that record has to survive a change of
harness. So the design puts the truth in the layer nobody can swap out: the
repo's own tool. Hooks enrich a trace, they do not constitute it.
Three design points worth knowing:
- Hooks call
tools/trace_ingest.py, neverwikitool. A hook fires on every tool call, and everywikitoolinvocation is counted by the Iteration Budget Gate - routing telemetry through the CLI would let the act of observing a session end it. The script importswiki_tools.telemetryas a library, which is stdlib-only for the same reason: a hook must run without the venv. - The trace records what the budget exempts. Read-only retrieval is not charged, but it is traced: what an agent looked at before acting is exactly what a trajectory scorer needs.
- The core event set is what the weakest harness can produce. Claude Code
has ~30 hooks, Mistral Vibe has three. Every
session.startcarries acompletenesslist, so a scorer says "not measurable here" instead of silently scoring zero.
Prompts and assistant replies are recorded in cleartext, locally, because a
failure taxonomy cannot be read out of hashes. Guards: secret scrubbing, a
60 KiB cap, WIKI_TRACE_CONTENT=0, and the rule that raw/ file contents
never enter a trace at all. reports/ stays gitignored and no exporter is
configured.
Also added: /EVALS.md, the human-facing hub for the whole thing, registered in
the AGENTS.md file-naming table. reports/CONTRACT.md gained a Traces
section. wiki_tools/session.py now holds the single definition of session
identity, which run_budget and the emitter share.
2026-08-23 - lint detects contested raw-file ownership
Author: GitHub Copilot
New hard check duplicate_raw_file_owners: a raw file named in more than one
source page's raw_files:.
uncovered_raw_files only ever asked whether a raw file is claimed at all,
so the opposite failure was invisible. It had already happened: one ingested manual
10-bootstrap-manual/ subtree carried two incompatible cuts at once, leaving
eight raw files claimed by two pages each, undetected until someone counted by
hand.
The rule it enforces is the one recorded in types/source.md: raw_files: is
a maintenance claim, ^[[Source - X]] citation is free reuse. Any number of
pages may cite a source; with two owners it is undefined which page must be
refreshed when the raw file changes, so both rot silently and neither is
identifiably the stale one.
Repeats inside a single page's own list are untidy, not contested - there is still exactly one owner - so they are not reported.
2026-08-23 - Workshop layer work/; tree ingests; ## Not Extracted
Author: GitHub Copilot
A tree like raw/documents/handbook/ (71 files) does not fit in one ingest, and
the wiki had no place to keep a run that spans sessions. Four changes.
work/ - a tracked, transient workshop layer
New stage beside kb/, with work/CONTRACT.md. One directory
per run, holding the plan and the intermediate extracts on the way to a kb/
page, deleted when the run closes.
It is the mirror image of reports/: a report is recomputable and therefore
gitignored, a workshop is judgment in progress that nothing can re-derive and is
therefore tracked. A gitignored workshop would be a reports/ directory
with extra steps - the whole point is that a multi-day run survives.
work/ joins raw/ and kb/ in CONTENT_DIRS and in the .gitignore
backstop: an ignore rule reaching it is a data-loss bug, and docs verify now
carries a canary that says so.
Run keys are derived, and a collision is an error
wikitool work new --input raw/documents/handbook creates
work/ingest-documents-handbook/. The key comes from the input path below
raw/ with separators flattened - not the basename, because
raw/documents/handbook and raw/articles/handbook would collide.
The directory name is the run key. No second identifier, for the same
reason a page title is a page's only identifier (invariant 2). A collision is
refused rather than suffixed: it means the tree is already being ingested, which
is the condition the key exists to detect. A genuine re-ingest is --again,
which dates the key.
wikitool work close --run-key <name> --yes deletes a finished run, listing
what would be lost first.
The Mass-Update Gate stops counting work/
publish still commits and pushes workshop files, but they no longer count
toward the threshold. The gate's justification is that a push publishes
knowledge irreversibly; a directory that is deleted on closure is not that, and
making a reviewer approve twelve scratch files is exactly the review fatigue the
threshold exists to avoid.
GATE_EXEMPT_PREFIXES is a constant, deliberately not a flag - a
--gate-exempt option would be a gate an agent could open on its own
initiative. The refusal message now reports both counts.
Tree ingests, and recording what was not extracted
instructions/ingest-large-tree.md (a flat
instruction, not a published skill - it runs rarely, so it earns a link rather
than a permanent slot in every session's context) cuts a tree into units before
anything is written, then reads, promotes and publishes one unit at a time
through a workshop. Triggers: more than ~20 raw files, or a source page that
would carry more than ~15 raw_files: entries. A third tier adds an audit pass
for units covering secrets, RBAC, ingress or disaster recovery.
Each unit takes its own WIKITOOL_SESSION_ID, derived from the run key
(<runkey>/u<N>), so the budget is scoped to the unit rather than to the whole
run. instructions/gates.md now states the matching
rule: a new session id may only be taken at a unit boundary written down in
plan.md, never in response to a gate refusal. The plan is the human approval
the gate would otherwise have to ask for.
types/source.md gains a ## Not Extracted section: what was read and
deliberately not promoted, with the reason. sources coverage can only answer
whether a raw file is named by some source page, never whether anyone decided
what to do with it - so without this, every later pass re-litigates the same
source.
README.md and CONTRACT.md are both allowed, and kept apart
The naming rule said README.md was repo-root only. It is now explicit that a
stage may carry both: README.md is technical documentation for humans and
for developing the thing in that directory, CONTRACT.md is the agent-facing
contract for using it. What a stage may not carry is the same content twice.
tools/README.md returns under that rule, rewritten as developer
documentation - layout, how to add a command, design notes, how to run the
tests - with no command table. The untracked file previously sitting there was
a leftover copy of the document that became tools/CONTRACT.md in 35d5df8, and
had already drifted (no search, no work, old gate wording).
To stop that recurring, docs verify's command-table guard was generalized
from the root README to every README (check_readmes_have_no_command_table).
The guard existed because this exact drift happened once; allowing stage
READMEs without widening it would have re-created the bug.
Found while verifying
Running the new procedure against an already-ingested tree reproduced 5
of 7 existing units exactly and surfaced one defect: 8 raw files under
10-bootstrap-manual/ are covered by two source pages each - an 18-file
umbrella page and seven per-step pages, two incompatible cuts applied at
different times. Recorded as a DECISION NEEDED in
work/ingest-documents-handbook/; retiring either side is a page-lifecycle
operation, not part of this change.
2026-08-22 - Retrieval instead of catalog reading; one instruction layer; CONTRACT.md naming
Author: GitHub Copilot
Four changes, all aimed at the same problem: the routing layer had grown redundant, and finding anything meant reading everything.
wikitool search
New command. Text search runs through a pluggable backend (rg today, behind a
SearchBackend protocol with Reciprocal Rank Fusion already in place, so a
vector backend is a new module rather than a rewrite). Frontmatter predicates -
--field entity_type=system, --field 'confidence>=0.8', --field tags=k8s,
--field '!source_url' - are evaluated in-process on the parsed YAML, and work
with or without a text query.
The query is passed as an argv element and --fixed-strings is the default, so
a search term is never a shell command and never an accidental regex. An unknown
field name fails with the list of fields that do exist, rather than returning an
empty result that reads as "no such pages".
search is exempt from the Iteration Budget Gate: retrieval is reading, not
iterating, and charging for it would tax the one habit that lowers token cost.
The catalog is sharded
kb/index.md was one flat file listing all 243 pages with summaries - roughly
12-14k tokens to read, which wiki-query did on every question. It is now a
map: statistics, one row per collection and per area, and a link to the shard
that lists those pages (50 lines). The tables live in a generated
kb/<collection>/INDEX.md, and an area past 50 rows gets its own -
kb/entities/tools/INDEX.md split out immediately at 56. The threshold comes
from the wiki's own Index Scaling page, so growth is handled by arithmetic
rather than by a judgment call. Stale shards are swept on every rebuild.
instructions/, and skills published by copy
New top-level directory: the single source for everything an agent is told to
do. Two forms, told apart structurally - a subdirectory with a SKILL.md is
published into the harness directories, a flat .md never is. That split is what
keeps a rarely-used procedure from occupying context for a whole session.
The five skills moved from .agents/skills/ into instructions/. Four shared
fragments were extracted from the places that had been repeating them:
gates.md (was restated in 7 places), page-lifecycle.md (6),
session-setup.md (4), publish-cycle.md (4), plus bootstrap.md.
wikitool skills sync|verify became wikitool instructions sync|verify|list,
and publication now copies instead of symlinking. This reverses the earlier
decision recorded in that module: symlinks cannot go stale, but they are
unreliable on Windows checkouts and do not survive being archived or copied. The
price of a copy is drift, so verify compares every copy byte for byte against
its source. Both .agents/skills/ and .claude/skills/ are now gitignored
build output; a fresh clone publishes them once via instructions/bootstrap.md.
verify also reports an instruction that nothing references - an instruction
nothing loads is inert, and nothing else in the stack would ever say so.
New instruction type-spec, with a new optional placement field root: on
type-specs (kb by default, repo to opt out). Without it a type outside kb/
could only be contract-only; with it, wikitool new instruction scaffolds
correctly.
CONTRACT.md for agents, README.md for humans
A file's name now says who it is for. raw/README.md, reports/README.md and
tools/README.md became CONTRACT.md; kb/CONVENTIONS.md became
kb/CONTRACT.md and absorbed kb/README.md, which is gone. README.md now
exists only at the repo root, for humans.
Twelve rules that had been stated in two to seven places each were reduced to one
canonical location apiece, with links elsewhere. This is now AGENTS.md invariant
8 ("one rule, one place"). It is deliberately not machine-checked: prose
duplication is a judgment call, so wiki-lint gained a review step for it
instead of docs verify gaining a validator.
Also fixed: docs verify said COLLECTION.md placement had two rules while
kb/README.md said three; runs on was recommended by the entities contract but
missing from the canonical label list; AGENTS.md implied budget reset was the
way past the Iteration Budget Gate, when reset is itself counted and refused at
the limit.
2026-08-21 - Collections under a single kb/ root; the repo becomes a four-stage pipeline
Author: GitHub Copilot
The wiki layer was a single wiki/ directory with one contract covering five
subdirectories. It is now kb/, a namespace whose children are first-class
collections, each with its own COLLECTION.md. The structural rule is now
enforced rather than described:
A directory under
kb/is a collection exactly when it contains aCOLLECTION.md. A subdirectory inside one is an area and inherits it.COLLECTION.mdappears nowhere outsidekb/.
Layout. wiki/ -> kb/, with entities/, concepts/, sources/ and
comparisons/ each gaining a contract. wiki/COLLECTION.md split into
kb/CONVENTIONS.md (the cross-collection half: naming, tone, linking,
provenance, confidence) and the four per-collection contracts (types offered,
what belongs, outbound linking, per-area emphasis). Added kb/README.md for
routing.
raw/ stayed at the repo root rather than moving to kb/raw/. It has no
types, frontmatter, wikilinks, provenance or confidence, so none of
kb/CONVENTIONS.md applies to it; keeping it a sibling makes the trust boundary
structural instead of documented, and leaves every raw_files: reference in
every source page valid. raw/COLLECTION.md -> raw/README.md.
types/ stayed at the repo root as the global type surface and lost its
COLLECTION.md; the content folded into types/type-spec.md, which now also
documents placement frontmatter, adding a type, and the template filters. A
type-spec is not a collection contract, and docs verify now fails if one
appears there.
Lint reports left the wiki. types/lint-report.md dropped its base_dir:
and is now contract-only, so wikitool new lint-report refuses to scaffold one.
lint --markdown writes to a new gitignored reports/. A report's structural
half is recomputable from the tree at any commit, so committing it created a
second copy that could drift; the semantic review, which is not recomputable,
now has to be carried into kb/log.md via log append --op lint before a pass
ends. index.md no longer has a Lint Reports section, and lint no longer
carries per-type exemptions for reports.
Tooling. repo_scan.py -> kb_scan.py; iter_wiki_files/load_pages ->
iter_kb_pages/load_kb_pages; config.WIKI_DIR -> config.KB_DIR, plus
REPORTS_DIR and PROVENANCE_FILE. The scanner now excludes COLLECTION.md at
any depth, since contracts sit one level below the kb root where the meta-file
guard does not reach. New kb_collections.py discovers collections from the
filesystem, so mkdir kb/<name> + a contract adds one with no code change.
docs verify replaced its hardcoded ("raw","wiki","types") list with that
discovery and gained three checks: an unclaimed directory under kb/, a nested
contract, and a contract outside kb/. Its .gitignore canaries now run in
both directions - nothing ignored under raw//kb/, everything ignored under
reports/ except its README.
AGENTS.md routing was rewritten from a flat "by layer" table into the
pipeline diagram plus the two collection rules, then by stage -> by
collection -> by task -> by question, with an explicit statement that page
placement follows base_dir: and is never an agent's choice, and a two-line
"adding a collection" recipe.
Verification: 208 tests pass (13 new, covering collection discovery, scanner
exclusions and contract-only types). lint is identical to the pre-move
baseline: 243 pages, zero findings.
Fixed while publishing the above: wikitool publish pushed the ref named
--branch (default main) rather than the commit it had just made. Run from a
feature branch, git push origin main pushed an unrelated, unchanged ref, exited
0, and the command reported "Published changes to origin/main" while the new
commit stayed local - the same class of silent non-publication that the
.gitignore canaries exist to prevent. publish now refuses to push when the
checked-out branch differs from --branch, naming both and suggesting
--branch <current>, and it checks this before staging so a refusal leaves the
working tree untouched. Covered by two regression tests.
Known gap, not fixed: at exactly the iteration limit, budget reset --yes is
itself refused, because reset is deliberately excluded from the exemption list
(SKIP_COMMAND_PATHS) so an agent cannot step around the gate by resetting
first. The consequence is that the only way out is --override-budget, while
AGENTS.md's "Gates" section reads as though reset were available at that point.
The behaviour is intentional; the documentation is what misleads.
Entries
| Date | Change | Author |
|---|---|---|
| 2026-08-13 | Review remediation: ignore-rule and reference-integrity holes closed, page mutations given commands. Two defects let the stack report itself healthier than it was. (1) .gitignore carried unanchored patterns (*temp*, *scratch*, *.log, go.mod, bin/, core, tags) that silently excluded content: raw/notes/temperature-sensors.md was gitignored, so sources coverage would call it covered while publish's git add -A never committed it and a fresh clone got a broken raw_files: reference. Every repo-local pattern is now anchored to the root, the content-swallowing ones are gone, and a !raw/**/!wiki/** backstop closes the rest; docs verify gained check_ignored_content(), which reports both actually-ignored files under raw//wiki/ and ten latent canary paths via git check-ignore --no-index. (2) lint's broken-link scan only walked page bodies, so related:/sources:/entities:/concepts: entries naming a non-existent page passed every check - 40 of them existed while lint reported zero findings, including a URL pasted into sources: and four pages citing a source title with a stray .md suffix the page does not carry. New dangling_frontmatter_refs hard-error check; which fields hold page titles is declared by each type-spec's new page_ref_fields: frontmatter (read via TypeResolver.get_page_ref_fields) rather than hardcoded, so a new type needs no code change. Both defects were reachable because renaming and deleting a page had no command: added wikitool rename (repoints body wikilinks, ^[[Title]] citation markers with aliases/anchors preserved, the page's own H1, and every declared ref array; a --from that is not a page repoints references onto an existing --to instead of moving anything), wikitool rm (refuses while inbound references exist unless --yes, strips mechanical links, deliberately leaves prose citations and reports them), and wikitool xref remove (inverse of both xref add and xref link-source; --b need not exist, which is what lets a stale reference be cleared without hand-editing frontmatter). log append accepts --op rename. All 40 dangling references were then cleaned up: 19 repointed onto existing pages, 21 removed as retired or never-created. Also: the lint_report pseudo-type - written by lint --markdown, special-cased in three places, documented nowhere - became a real types/lint-report.md + schema with base_dir: reports, so reports live under wiki/reports/ and are schema-validated like any other page; budget reset is no longer exempt from the Iteration Budget Gate and now requires --yes of its own (exempting the command that clears the counter made the gate a formality, while budget status stays exempt so the situation remains reportable); skills sync refuses to rmtree a .claude/skills/ entry that is a real directory unless --force; index_build no longer hardcodes types/entity.md; the root README's duplicate command table, maintenance schedule, and entity-type directory table were replaced with pointers, with a new docs verify check enforcing that the command table stays absent there; wiki/COLLECTION.md dropped the directory column the type-specs own; and wiki-lint now writes its report after the fixes rather than before, so it records the wiki's final state. Test suite 139 -> 182. |
GitHub Copilot |
| 2026-08-13 | Tooling correctness + control-plane restructure. Fixed confidence decay compounding on every run: confidence is now derived from a new undecayed confidence_base field (added to the entity/concept schemas, backfilled across 204 pages by the new confidence init-base command), so repeated runs are idempotent - the previous "never double-decays" claim in AGENTS.md/READMEs was false. Added wikitool touch (bump modified:/summary:/provenance:/confidence_base through the tool instead of by hand, schema-validating the fields it writes) and wikitool docs verify (checks every CLI command is documented in tools/README and vice versa, that each collection has a COLLECTION.md, and that no pre-migration type: <enum> block survives in the contracts). lint gained a hard-error check for unreadable/missing frontmatter - previously such pages silently read back as {} and escaped every other check - plus most_linked/inbound_counts output, which wiki-status had been documented to read but which the report never contained. publish now evaluates the Mass-Update Gate before staging, parses git status --porcelain -z -uall (fixing renames and paths with spaces), and takes --remote/--branch/--path (the last scoping gate count, staging and commit to a subtree, so a large change can be published in reviewable batches). Budget gate: refused calls are no longer counted, --help anywhere in argv is exempt, stale sessions are pruned from the state file, and budget status reports whether the session key came from WIKITOOL_SESSION_ID or the pid fallback. Restructured the 648-line AGENTS.md into a ~140-line control plane (invariants, routing, gates, error model) plus per-layer contracts raw/COLLECTION.md, wiki/COLLECTION.md, types/COLLECTION.md, moving the per-command error contracts and maintenance schedule into tools/README.md; deleted the stale type: comparison template, the schema-duplicating field tables, and the pseudo-command reference. All five skills now export WIKITOOL_SESSION_ID, call touch instead of hand-editing frontmatter, and treat raw/ content as untrusted data. Added tests for decay idempotence, frontmatter errors, porcelain parsing, budget hardening, touch, docs verify, and skills sync (139 total). |
GitHub Copilot |
| 2026-08-08 | Made wikitool new's scaffolding fully schema-driven instead of hand-duplicating the type-specs: added TypeResolver.get_enum()/get_schema(), removed config.py's hardcoded ENTITY_TYPES/CONCEPT_TYPES/PROVENANCE_VALUES (enum validity and CLI --help text now read from each type's .schema.yaml), replaced each new_* command's hand-built frontmatter dict with a generic schema-driven builder, and extended _apply_template_variables with |bullets/|join/|capitalize filters so type-spec templates reference frontmatter fields directly instead of each command precomputing bespoke display variables. Also consolidated lint's two overlapping missing-field checks into one: removed the hand-maintained REQUIRED_FIELDS/schema_issues, relying solely on the resolver-based schema_validation_errors. Directory placement (ENTITY_SUBDIRS) stays in config.py as wiki layout, not type structure. |
GitHub Copilot |
| 2026-08-08 | Removed the now-unused legacy bare-enum type: compatibility shims left over from the type-system migration below (Page.kind's entity/concept/source/comparison fallback entries, and lint's duplicate REQUIRED_FIELDS keys) since all 238 wiki pages have used path-based type: types/entity.md references since that migration; only the current path-based form is recognized now. |
GitHub Copilot |
| 2026-08-08 | Major: Transformed to Commonplace-style externalized type system. Created /types/ directory with type-spec files (entity.md, concept.md, source.md, comparison.md, type-spec.md) and corresponding .schema.yaml files for structural validation. Rewrote wikitool new commands to use type-spec templates instead of hardcoded templates. Migrated all 238 wiki pages from enum-style type references (type: entity) to path-based references (type: types/entity.md). Created type_resolver.py module for path resolution, caching, and schema validation. Extended lint with type path validation and schema validation checks. Updated AGENTS.md Type System section, Architecture diagrams, and skill files. |
Mistral Vibe |
| 2026-08-07 | Introduced this CHANGES.md. Removed the inline "Version History" sections from AGENTS.md and README.md (and confirmed no skill file under .agents/skills/ had one); both now point here instead. |
GitHub Copilot |
| 2026-08-07 | Added "Iteration and Cost Limits" section to AGENTS.md and a code-enforced tools/wikitool Iteration Budget Gate + Loop-Breaker (new budget status/budget reset commands, --override-budget flag): every wikitool call is checked against a 30-call/session ceiling and refuses 3x-identical repeats, mirroring the Mass-Update Gate pattern but scoped to iteration volume instead of publish size. |
GitHub Copilot |
| 2026-08-06 | Added "Tool Error Contracts" section to AGENTS.md: a general 3-outcome model (success / expected validation error / unexpected error) plus a per-command table covering every wikitool command's exit-1 meaning, atomicity, retry policy, and escalation path. Added --dry-run to xref add/xref link-source and made xref link-source report partial failures instead of aborting silently mid-loop. |
GitHub Copilot |
| 2026-08-05 | Documented the skills-based workflow split, the full wikitool command set, and a dedicated Maintenance section (schedule, confidence decay, provenance) in README.md. |
GitHub Copilot |
| 2026-08-04 | Split the 5 step-by-step workflows into independently-discoverable skills under .agents/skills/ (wiki-ingest, wiki-query, wiki-lint, wiki-manage, wiki-status), mirrored to .claude/skills/ for Claude Code via new tools/wikitool skills sync/skills verify commands. Slimmed root AGENTS.md to schema/policy only. |
GitHub Copilot |
| 2026-08-02 | Added "Provenance and Citation" section to AGENTS.md: raw_files: (replacing source:) on source pages, provenance: field on entity/concept pages, inline ^[[Source - X]] citation markers, and the generated wiki/provenance.md reverse index. Added tools/wikitool sources coverage|trace|rebuild-index commands and 4 new lint checks. Updated all workflows and page templates accordingly, and added the "no confident answer without a source" rule. |
GitHub Copilot |
| 2026-08-02 | Added tools/wikitool deterministic CLI and a new "Tooling" section to AGENTS.md mandating its use for scaffolding, cross-references, index rebuilds, log entries, confidence decay, and publishing. Rewrote INGEST/CREATE/UPDATE/LINT workflow steps and Git Automation to call it instead of describing manual edits. Added summary: frontmatter field to all page templates. |
GitHub Copilot |
| 2026-08-02 | Added Git Automation: all workflows now automatically commit and push changes to origin/main without user confirmation. Added auto-commit/push step to INGEST and CREATE workflows. |
Mistral Vibe |
| 2026-07-26 | Updated naming conventions: changed from kebab-case to human-readable titles with spaces (e.g., Hybrid Search.md instead of hybrid-search.md). Wikilinks now match file names exactly: [[Hybrid Search]] links to Hybrid Search.md. Updated all page templates to use spaced names in wikilinks. |
Mistral Vibe |
| 2026-07-25 | Initial schema/scaffold created for the IT-focused LLM Wiki. | Mistral Vibe |