229 Commits
Author SHA1 Message Date
4db294e1bc Follow-up builds (#23)
A build definition may declare afterSuccessOf, running whenever the named
build of the same branch turns green — the deployment path chosen over a
deployCommand or a separate deploy section.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #23
2026-09-05 20:02:32 +02:00
mhoennig 0f9f119687 init: keep dots in detected repository names 2026-09-05 15:15:12 +02:00
miandmhoennig 5c64391f5f Feature/remote authenticated clone (#24)
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #24
2026-09-05 14:06:58 +02:00
mhoennigandClaude Opus 5 cd6f915231 init reads its configuration from the repository root, and says so when it cannot (#22)
`init --systemd` generated the systemd unit, the .htaccess and the maintenance
page from a configuration it never actually read reliably: the load swallowed
every exception and fell back to a default `ServerConfig`, and it resolved the
layers from the process's current directory instead of the git top level every
other file of the command goes through.

A broken `.werkator.yml` was therefore indistinguishable from an unconfigured
`publicBaseUrl` — the host integration was skipped without a word — and running
`init` from a subdirectory read a foreign configuration or none, which also
defeated `--apply`'s promise that the fragment reaches the generated unit.

The fallback stays, but the exception message is printed, once per run, and the
repository root is passed down like every other caller of `ConfigLoader.load`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 19:38:41 +02:00
092183ca30 Clone the watched repository from Gitea by default (#21)
The repository moved to git.javagil.de, but repo-init still defaulted to
the GitHub mirror, which is no longer updated.
A freshly cloned instance therefore watched a stale main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #21
2026-09-04 13:33:08 +02:00
mhoennig 0761a274db Merge main into the deflake-maxconcurrent-test branch 2026-09-04 12:46:25 +02:00
mhoennigandClaude Opus 5 5c73c4cc21 docs(prs): the deflake-maxconcurrent-test PR-doc
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 08:53:00 +02:00
16543f038b the sandbox config section is werkdock, not bwrap (#19)
`bwrap` named the mechanism one layer below the tool that actually runs it: since
v1.0.0 Werkator does not invoke bwrap at all, it shells out to the werkdock CLI —
which made `bwrap.werkdock` a key naming its own executor.

The section is `werkdock` now and that key is `werkdock.binary`; BwrapConfig,
BwrapOverrides and BwrapBuildRunner follow the name. A file still writing `bwrap`
is read as before and warned about once per file, in `renameLegacySandbox` on the
raw map of every layer before merging — so nothing downstream knows two names, and
the old name is not a way around the pinning either. Renaming rather than refusing,
because the section lives in the machine configuration of every webspace instance,
which no repository tracks; the hard refusal belongs to the release that sets
ConfigVersions.FORMAT_BROKE_IN, where a file declaring no version can be caught
by name at all.

WERKATOR_SANDBOX in tools/remote follows, and still accepts `bwrap`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #19
2026-09-03 20:41:18 +02:00
3ccc901d1b tools/remote drives any host layout (#18)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #18
2026-09-03 20:37:58 +02:00
mhoennigandClaude Fable 5.1 11f6f9bd26 docs(rfcs): RFC 0001 proposes the Instrument Panel web UI redesign
Six explored directions, the chosen one (E) worked out in a light and a dark
teal palette, with a repository strip previewing every served repository,
the ten-repository and the phone case, and the backend it needs (/api/repos).
Renderings of all artboards live next to the RFC; AGENTS.md registers docs/rfcs/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 20:15:04 +02:00
mhoennigandClaude Opus 5 9221550a2a test(build): gate the maxConcurrent-1 test instead of racing a sleep
The build of branch-a slept one second while the test asserted, without
any synchronization, that branch-b was still PENDING.
Under CPU contention on the mih09 host the sleep could elapse first, so
branch-b was already RUNNING or SUCCESS when the assertion ran.
branch-a now blocks until the test creates a gate file, and the test
first waits for branch-a to be RUNNING; the PENDING assertion no longer
depends on timing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 19:32:22 +02:00
mhoennigandClaude Opus 5 3b095fd04a docs(prs): PR#17 verified live on mih09
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 18:00:07 +02:00
mhoennigandClaude Sonnet 5 6521c47092 Bump version to 1.1.2 for the maintenance-page deployment
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 17:54:23 +02:00
mhoennigandClaude Sonnet 5 ed89911871 docs(prs): the maintenance-page PR-doc
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 17:49:21 +02:00
mhoennigandClaude Sonnet 5 ca9c23d55b Show a static maintenance page while the service restarts
A deployment's ErrorDocument-covered downtime window looked dead: Apache's
default 502/503/504 error page, or a hung request, while instance-update
briefly restarts the systemd unit. init --systemd now also writes a static
werkator-maintenance.html next to the generated .htaccess, and the .htaccess
maps a refused connection straight to it — served by Apache alone, no
change needed to Werkator itself since it is exactly the process that is
down.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 17:48:06 +02:00
mhoennigandClaude Sonnet 5 06967c551e docs(prs): PR#16 verified live on mih09
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 17:40:07 +02:00
mhoennigandClaude Sonnet 5 108fabeae6 Bump version to 1.1.1 for the quota-aware disk metrics deployment
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 17:35:25 +02:00
mhoennigandClaude Sonnet 5 61c235313f Implement quota-aware disk metrics (PR#16)
The system page showed the disk of the host volume, not the budget the
instance can actually fill — on a Hostsharing Managed Webspace that is a
group quota, tighter than the volume by an order of magnitude, so the
warn/critical highlighting could never fire before a build failed with
"Disk quota exceeded".

DiskQuota parses `quota -u -g --no-wrap --raw-grace` and picks the
tightest of the user quota, the group quota, and the volume itself;
SystemMetricsCollector reports whichever binds, resets the disk
min/max/avg when the binding source changes, and the system page names
the source in its info line. A host without a binding quota (Docker
hosts, developer machines) renders exactly as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 17:23:39 +02:00
mhoennigandClaude Sonnet 5 be965617cd docs(prs): das PR-Dokument der Quota-Anzeige trägt seine echte Nummer — PR#16
Die alte GitHub-PR#14 kollidiert mit der Gitea-Nummerierung, die nach dem
Umzug bei 1 neu beginnt; PR#16 ist die tatsächliche Nummer auf git.javagil.de.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 17:04:27 +02:00
mhoennigandClaude Fable 5.1 7eac1c342d docs(prs): das PR-Dokument der Quota-Anzeige trägt seine echte Nummer — PR#14
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 17:04:27 +02:00
mhoennigandClaude Fable 5.1 623266e060 docs(prs): PR#13 — die Anzeige nimmt den kleinsten Spielraum aus User-Quota, Gruppen-Quota und freiem Plattenplatz
Die Volume-Zahlen sind nicht mehr nur Rückfall, sondern dritter Kandidat: Gebunden
ist, was am wenigsten Platz lässt; eine später eingeführte User-Quota geht ohne
Änderung in den Vergleich ein.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 17:04:27 +02:00
mhoennigandClaude Fable 5.1 22a1b28824 docs(prs): Plan für die quota-bewusste Plattenplatz-Anzeige der Systemübersicht — PR#13
Auf mih09 zeigt /system die 71-GiB-Platte des Hosts, gemessen bindet aber die
Gruppen-Quota des Pakets (8 GiB soft, 12 GiB hard, 1,04 GiB belegt). Das PR-Doc
plant: quota(1) per CLI lesen, die bindende Zeile (User/Gruppe, Dateisystem des
Repos) als Disk-Budget nehmen, ohne Quota unverändert der File Store, Serien-Reset
bei Quellenwechsel, Budget in der Info-Zeile benannt. Nur Plan, kein Code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 17:04:27 +02:00
mhoennigandClaude Sonnet 5 4d76a93927 Add a script to mirror main to GitHub after the move to Gitea
Werkator's canonical repository now lives on git.javagil.de; GitHub stays
around as a public mirror. This keeps its main branch in sync without
touching any other branch or the repo's other history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 16:34:44 +02:00
mhoennigandClaude Sonnet 5 4f2ad3b244 Fail the build when the project version and the release notes disagree
A version bump is manual and easy to forget on a deployment — it happened
twice on 2026-09-03, leaving a running instance whose footer/--version
claimed a version stale by two deployments. ReleaseVersionConsistencyTest
now compares build.gradle.kts against the top entry of releases.html so a
forgotten bump, or notes written for a version nothing was built as, fails
before an artifact can even be produced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 16:02:11 +02:00
mhoennigandClaude Sonnet 5 7028ca8ca3 Bump version to 1.1.0 for the multi-repo registry deployment
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 15:50:00 +02:00
mhoennigandClaude Fable 5.1 0012c17cb7 UI: die Repository-Auswahl sitzt mittig zur Überschrift
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 15:34:13 +02:00
mhoennigandClaude Fable 5.1 fe11a339ad UI: das Repository wird in der Seitenüberschrift per Drop-Down gewählt
Die Link-Leiste neben dem Menü entfällt. An ihrer Stelle steht dort, wo
bisher der Repository-Name stand, eine Auswahlliste der bedienten
Repositories; werkator.js hängt die aktuelle Ansicht (branches, history,
current) an, damit der Wechsel auf derselben Seite bleibt. Mit nur einem
Repository bleibt der Name wie bisher reiner Text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 15:26:29 +02:00
mhoennigandClaude Opus 5 e61adfbbaa docs: nachgemessen, wann die entfernte Build-Definition wirklich verschwindet
Die offene Frage im PR-Dokument sagte „löst sich beim Merge". Das Log sagt
es genauer: Am Merge-Commit wurde `werkdock` noch eingereiht, erst am
nächsten nicht mehr. Die Projektschicht ist die Datei im Arbeitsbaum des
beobachteten Klons — die Definition überlebt, bis dieser Baum an der
Entfernung vorbei ist, der Merge selbst bekommt also einen letzten
Geister-Build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:22:10 +02:00
mhoennigandClaude Opus 5 c0fd1de954 docs: das PR-Dokument der Werkdock-Herauslösung trägt seine echte Nummer
Geschrieben wurde es als #14 — vergeben wurde beim Öffnen #15. Die
Namenskonvention (docs/prs/README.md) will die Nummer aus dem Tracker,
sonst zeigt später nichts mehr aufeinander.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:18:14 +02:00
Michael HönnigandClaude Opus 5 ac53b5f211 step 21e werkdock moved to own repo (#15)
* refactor: werkdock/ verlässt dieses Repository

Die neun Commits unterhalb von werkdock/ sind per `git subtree split`
historieerhaltend nach https://git.javagil.de/mi/werkdock gehoben; hier
bleibt nichts davon zurück.

Mit dem Verzeichnis geht die Build-Definition `werkdock`: Sie beschrieb ein
Go-Modul, das der Kotlin-Build um sie herum nicht berühren kann — jeder
Werkator-Branch baute es dennoch als `<branch>@werkdock` mit. Drüben ist
sie die einzige und heißt `default`.

Die Kopplung bleibt, wie Sitzung C sie gemacht hat: Werkator ruft das
`werkdock`-Binary über PATH (`bwrap.werkdock`, weiterhin gepinnt), so wie
es `git` ruft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(remote): das werkdock-Binary kommt aus einem eigenen Checkout

Bisher baute das Skript es aus dem Unterverzeichnis. Jetzt nennt
WERKDOCK_REPO den Checkout (Vorgabe: ein Geschwister dieses Repositories —
die übliche Ablage, wenn man an beidem arbeitet), WERKDOCK_BINARY das
gebaute Binary darin; beides ist wie jeder Transportwert aus der Umgebung
übersteuerbar.

Fehlt der Checkout, nennt die Meldung die Klon-URL, statt bloß „file not
found" zu sagen: Das Skript kann die Lage nicht mehr selbst beheben, also
sagt es, was sie beheben würde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: Sitzung E — Werkdock hat ein eigenes Repository

Plan-Schritt 21 bekommt die Sitzung, die die Herauslösung beschreibt, samt
Abnahmekriterium; Plan-Index und Architektur-Skill sagen nicht mehr, sie
stehe noch bevor, und der Deployment-Leitfaden verlinkt das Repository
statt eines Verzeichnisses, das es hier nicht mehr gibt.

Dazu das PR-Dokument nach Hausregel (docs/prs/README.md).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(remote): repo-add wendet das Instanz-Fragment an

`repo-add` rief `werkator init` ohne `--apply`, `repo-init` dagegen mit.
Folge: Ein so aufgenommenes Repository bekam die Sandbox-Politik nicht —
`bwrap.rootfs` und `bwrap.werkdock` fehlten, und der erste Build lief auf
dem nackten Host statt in der Sandbox. Gefunden beim Aufnehmen von
Werkdock: `gofmt: command not found`, weil die Go-Toolchain nur im
Sandbox-Image liegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(remote): repo-add erkennt einen ~/-Registry-Eintrag, und der Kommentar sagt die Wahrheit

Die Registry-Prüfung suchte nur den absoluten Pfad und meldete deshalb
„not registered yet“, obwohl der Eintrag als `path: ~/werkdock` dasteht —
eine Einladung, ihn ein zweites Mal einzutragen. Gegriffen wird jetzt
lokal, damit beide Schreibweisen ohne zweite Schicht Remote-Quoting
treffbar sind.

Dazu der Kommentar über dem Laden der Env-Datei: Er behauptete, die
ausdrückliche Umgebung gewinne. `set -a; source` weist unbedingt zu — die
Datei gewinnt. Beim Nachziehen des Werkdock-Fragments hat mich genau das
eine Runde gekostet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:15:15 +02:00
mhoennigandClaude Opus 5 06f4052848 Merge PR #13: Routen, UI und Artefakt-Links tragen das Repository
/repos/<name>/… und /api/repos/<name>/… neben den bisherigen Pfaden; die
Kopfzeile bekommt einen Repo-Umschalter, sobald mehr als ein Repository
registriert ist, und die Gitea-Statusmeldung trägt eine dauerhafte URL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 14:26:56 +02:00
mhoennigandClaude Opus 5 f11b53bdb4 Merge PR #12: Repo-Registry — mehrere Repositories je Instanz
Die Instanz liest ihre Repositories aus ~/.werkator.yml (ADR 0009); Watcher,
Build-Ausführung und Artefakte arbeiten je Repository. Bisher lief sie auf
einem Vorab-Build dieses Branches — mit dem Merge entspricht main wieder dem,
was produktiv läuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 14:26:56 +02:00
mhoennigandClaude Opus 5 c1629cace3 feat(remote): repo-add bereitet ein weiteres Repository für die Registry vor
`tools/remote` konnte genau ein beobachtetes Repository einrichten
(`repo-init`, fest auf $WERKATOR_PATH/werkator). Für eine Instanz, die eine
Registry bedient (ADR 0009), fehlte der Weg, ein zweites hinzuzunehmen —
und das braucht der Rollout unabhängig davon, welche Repositories es am Ende
sind.

`repo-add <url> [name]` klont daneben, führt `init` darin aus und PRÜFT, ob
die Registry den Pfad schon nennt — geschrieben wird sie nicht. `~/.werkator.yml`
ist die Datei der Instanz: Port, globale Nebenläufigkeit, womöglich geteilte
Zugangsdaten. Ein Skript, das sie in place umschreibt, verändert die
Konfiguration des Betreibers hinter dessen Rücken. Klonen und Initialisieren
ist mechanisch, Registrieren ist eine Entscheidung — also druckt es den
Eintrag und sagt, dass die Registry beim Start gelesen wird.

Der Name muss ein Pfadsegment sein: Er wird das Routen-Segment /repos/<name>.
docs/deployment.md verweist im Registry-Abschnitt darauf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:59:24 +02:00
mhoennigandClaude Opus 5 80a14a7467 feat(gitea): der Commit-Status verlinkt endlich die Artefaktseite — repo-bezogen
Der letzte offene Punkt der Sitzung D („Gitea status links use the repo-scoped
URLs"). Beim Nachsehen war es kein Scoping-Problem, sondern ein fehlendes
Feature: `server.publicBaseUrl` ist als „used for all links posted to Gitea"
dokumentiert, der Executor postete aber `targetUrl = null` — es gab überhaupt
keinen Link. Jetzt zeigt der Status auf die Artefaktseite des Builds, also auf
die Logs, um die es geht. Ohne `publicBaseUrl` bleibt er null: ein relativer
Link in einem Gitea-Status wäre schlimmer als keiner, er löste gegen die Forge
auf.

Die Präfix-Regel wohnt jetzt an einer Stelle (`RepoLinks`), weil drei
dasselbe damit tun: die Seiten, die API-Antworten und diese Ziel-URLs — und
eine dreimal ausgeschriebene Regel driftet. Die Slice-Tests binden die echte
Komponente per @Import ein, statt die Regel im Test nachzubauen.

Ein neuer Test (mit publicBaseUrl trägt der Status die Artefaktseite; die
vorhandenen Tests halten fest, dass ohne sie weiterhin null gepostet wird).
499 Tests grün, ktlint sauber. Plan und Architektur-Skill nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:53:51 +02:00
mhoennigandClaude Opus 5 91e1dd47a3 docs(deployment): wie eine Instanz mehrere Repositories bedient
Der aus Sitzung C nach E übertragene Doku-Punkt: docs/deployment.md
beschreibt die Registry jetzt betrieblich — klonen, vorbereiten, in
~/.werkator.yml eintragen, neu starten. Dazu die Regeln, die beim Eintragen
weh tun, wenn man sie nicht kennt: Der Name ist Routen-Segment und
Umschalter-Eintrag und muss eindeutig sein (ein Doppel bricht den Start ab,
ebenso ein Eintrag, der kein git-Repository ist), eine unlesbare Konfiguration
überspringt nur ihr Repository, geteilte Zugangsdaten können einmal im
defaults-Block liegen, und die Unit behält ihren Namen — sie gehört der
Instanz, nicht einem Repository.

Dazu, was die Adressen tun: mit einem Repository bleibt jede URL, wie sie
war; mit mehreren tragen Seiten und API das Repository, und die unscoped
Pfade meinen weiterhin das bediente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:41:26 +02:00
mhoennigandClaude Opus 5 c82f2a965c feat(server): Routen, Seiten und Artefakte tragen das Repository — /repos/<name>/… (Sitzung D)
Zweite Hälfte von Sitzung D (docs/plan/22-multi-repo.md, PR #13): Der Server
bediente bisher genau ein Repository der Registry. Jede Route — API, Seiten,
Artefakt-Dateien — arbeitete auf `registry.current()`; ein zweites
registriertes Repository wurde gebaut und gepollt, war aber unsichtbar und
unerreichbar.

Jeder Controller löst sein Repository jetzt je Anfrage auf, statt das
bediente als Bohne zu halten; jede Route ist zweimal gemappt. Die unscoped
Form ist kein Übergangs-Alias, sondern dauerhaft die Art zu sagen „das
bediente Repository" — Lesezeichen und die nach Gitea geposteten Links
kennen kein Segment.

Entschieden gegen die Repo-Spalte: Die Seiten bleiben je Repository, die
Navigation bekommt einen Umschalter. Die Aktionen einer Zeile brauchen das
Repository ohnehin, Branches kommen von einem origin und Artefakte aus einem
Store — und bei dem einen Repository, das die meisten Installationen haben,
wäre eine Spalte nur Rauschen.

Das Link-Präfix folgt der ZAHL der bedienten Repositories, nicht dem Weg, über
den eine Seite erreicht wurde: mit einem behält die Installation ihre
bisherigen URLs (Abnahmekriterium der Sitzung), mit mehreren benennt jeder
Link sein Repository. werkator.js liest das Präfix einmal aus einem
`werkator-repo-base`-Meta. `BranchPermalinks.permanentUrl` bekommt es
ebenfalls — der permanente Schlüssel ist ein Hash des Build-Namens allein,
zwei Repositories mit `main` teilten sich sonst eine permanente URL.

Fünf neue Tests, Gegenprobe per Mutation gezogen (Präfix fest auf leer →
genau der Mehr-Repo-Test fällt). 498 Tests grün, ktlint sauber. PR-Dokument
docs/prs/2026-09-03-PR#13-…, Plan, Architektur-Skill und AGENTS.md
nachgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:39:09 +02:00
mhoennigandClaude Opus 5 4304dd7c4b feat(server): die Builds-API trägt das Repository im Pfad — /api/repos/<name>/builds/…
Sitzung D, erste Hälfte (docs/plan/22-multi-repo.md): Jede Route der
Builds-API gibt es jetzt zweimal — repo-benannt und unscoped. Die unscoped
Form bleibt dauerhaft und meint das bediente Repository
(`RepoRegistry.current`): Lesezeichen, die alte Oberfläche und die bereits
nach Gitea geposteten Links kennen kein Repo-Segment, und eine CI, die ihre
eigenen alten Links bricht, ist eine CI, der niemand traut.

Der Controller hält kein `BuildResultRepository`, keinen `ArtifactStore` und
keinen `RepoContext` mehr als Bohne, sondern löst je Anfrage über die
Registry auf. Ein unbekannter Name beantwortet sich in der Form dieser API
(404 mit `error`) über einen eigenen ExceptionHandler statt über Springs
Problem-Detail.

Dabei gefunden und mitbehoben: `cancel` griff über Repository-Grenzen. Der
Executor bricht per Schlüssel instanzweit ab — eine Route, die ein
Repository benennt, darf damit kein fremdes erreichen. Sie prüft jetzt
zuerst, ob der Schlüssel in den Ergebnissen DIESES Repositories steht; ein
wartender oder laufender Build hat dort immer sein PENDING/RUNNING.

Drei neue Tests: die repo-benannte Route antwortet, ein unbekannter Name
gibt 404 mit Meldung, und `cancel` erreicht keinen fremden Build. 496 Tests
grün, ktlint sauber.

Noch nicht scoped: die Oberfläche und die Artefakt-Routen — die kommen mit
der zweiten Hälfte von Sitzung D.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:04:52 +02:00
mhoennigandClaude Opus 5 6ca67a6a37 feat(build): every running build knows its repository — die laufenden Builds und das Worktree-Aufräumen unterscheiden Repositories
Der letzte Übertrag aus Sitzung C: `RunningBuild` trug kein Repository, der
Executor ist aber instanzweit. Zwei Folgen, beide gemessen und jetzt behoben:

- Das Worktree-Aufräumen schützte die Worktrees ALLER Repositories. Ein
  laufender Build eines anderen Repositories auf einem gleichnamigen Branch
  hielt hier einen Worktree fest, dessen Branch längst von origin weg war.
- Die Current-Builds-Ansicht und `/api/builds/current` zeigten die Builds
  aller Repositories, schlugen ihren Status aber in den Ergebnissen NUR
  dieses Repositories nach — ein fremder Build fiel auf RUNNING zurück und
  zeigte einen Zustand, den niemand aufgezeichnet hat. Dasselbe beim
  Live-Log: der Schlüssel eines fremden Builds wurde beantwortet.

`RunningBuild.repo` ist der `RepoContext` selbst, verglichen wird per
Referenz — der Kontext ist die Identität (ADR 0009), und der Executor hat ihn
in `startBuild` ohnehin zur Hand. Watcher, API und UI filtern damit auf ihr
eigenes Repository.

Drei neue Tests, Gegenprobe per Mutation gezogen: Nimmt man die drei Filter
wieder heraus, fallen genau diese drei und sonst keiner. 493 Tests grün,
ktlint sauber.

Architektur-Skill und Plan (docs/plan/22-multi-repo.md) nachgezogen — die
Aussage „RunningBuild carries no repository" stimmte nicht mehr. Was von
Sitzung D bleibt: die repo-bezogenen Routen und die Oberfläche, die weiterhin
nur `registry.current()` bedienen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 12:36:07 +02:00
mhoennigandClaude Opus 5 6fba43c627 docs(prs): das PR-Dokument der Repository-Registry trägt seine echte Nummer — PR#12
Die Datei lag als `PR#000`-Platzhalter, wie es die Konvention in
docs/prs/README.md vorsieht: Die Nummer gibt es erst, wenn der Pull Request
offen ist. Er ist es — #12. Umbenannt samt der fünf Szenario-IDs
(`Scenario#000.01` … `.05` -> `Scenario#12.01` … `.05`), wie es Schritt 21
für PR #4 vorgemacht hat; sonst zeigen die IDs auf eine Nummer, die es nicht
gibt. Kein `#000` bleibt in der Datei, keine andere Datei verweist auf den
alten Namen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 12:35:53 +02:00
mhoennigandClaude Fable 5.1 58799ed0ed Step 22 C: metrics over the registry, documentation, PR-doc
The metrics page sums the registered repositories' sizes; the instance
configuration is documented in docs/configuration.md, the architecture skill,
AGENTS.md and the step 22 plan (session C ticked, fairness decided FIFO).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 09:06:13 +02:00
mhoennigandClaude Fable 5.1 b66d26a03a Step 22 C: --repo selects a registered repository in build, retry, and status
The RepoOption mixin resolves the name through the registry; without it a
command means the current directory when served, else the first entry. An
unknown name is a usage error naming the registered repositories.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 09:03:07 +02:00
mhoennigandClaude Fable 5.1 a5027acfaf Step 22 C: the watcher multiplexes the registry
start takes the served repositories; one poll cycle polls each in its own guard
and aggregates the reports into WatcherState, which gains a per-repository list.
With one repository the top-level fields read exactly as before; with several,
every message is prefixed by the repository name. Log lines carry the name too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 09:00:52 +02:00
mhoennigandClaude Fable 5.1 b16c9c2e27 Step 22 C: the repository registry
RepoRegistry opens one RepoContext per entry of the home configuration, or the
current directory without one. An entry that is no git repository or a name
used twice aborts the start naming the home file; a repository whose config
must not be read is skipped with an error and the others are served. The
current-repo bean now comes from the registry (the cwd when served, else the
first entry), and the pre-rename state-dir migration runs per opened repository.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 08:58:04 +02:00
mhoennigandClaude Fable 5.1 ab2a4133e8 Step 22 C: the instance configuration in ~/.werkator.yml
ConfigLoader reads the home file (WERKATOR_HOME overrides the location): its
defaults block merges below every repository's layers, and once it exists the
server section, executor and watcher.pollInterval come from it alone — a
repository file still carrying them is told so once, naming both files.
InstanceConfig binds the registry entries; nothing reads them yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 08:55:59 +02:00
Michael HönnigandClaude Fable 5.1 096cce3659 step 22b: repo context (#11)
* Step 22 B: RepoContext over the current repository

A RepoContext bundles a repository's primary checkout with the state that lives
inside or is keyed by it (results, artifact store) and carries its name. Today
there is exactly one, opened over the current working directory; the result and
artifact-store beans now come from it, so nothing else changes yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Step 22 B: the watcher polls a RepoContext

start/poll/recoverOnStartup take the context instead of a working directory and
read results and artifacts from it; the per-repository poll memory (logged fetch
error, deprecation warning, cached branch definitions) moves into a RepoWatch
keyed by context, so the next session can iterate contexts without one
repository's outage silencing another's. The shared WatcherState is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Step 22 B: the executor runs builds of a RepoContext

startBuild takes the context first; builds serialize per (context, branch) and
share the global maxConcurrent cap across repositories, results and artifacts go
to the build's own context. ConsoleBuildRunner, the build/retry commands and the
builds API restart pass the current repository's context along.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Step 22 B: the UI and the branch listing read their RepoContext

UiController and BuildsApiController take the current repository's context
instead of a settable working directory; BranchListing lists the branches of a
context and reads the results from it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Step 22 B: document the RepoContext, PR-doc for PR #11

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 08:49:21 +02:00
mhoennig 713edf77d6 Merge pull request #10 from mhoennig/22-multi-repo
ADR 0009 and step 22 plan: one Werkator instance serves a set of repositories

# Conflicts:
#	docs/plan/22-multi-repo.md
#	docs/prs/2026-09-01-PR#6-werkdock-bootstrap.md
2026-09-02 07:22:59 +02:00
mhoennig a1de3c861f Merge pull request #9 from mhoennig/init-apply-config
Step 23 sessions A-C: init --apply, applied instance fragment, tools/remote wraps
2026-09-02 07:12:19 +02:00
mhoennig 717cd65ea1 Merge pull request #8 from mhoennig/23-init-owns-the-files
Plan step 23: init owns the files, tools/remote wraps
2026-09-02 07:12:12 +02:00
mhoennig 3af8ac20ef Merge pull request #7 from mhoennig/werkator-consumes-werkdock
BwrapBuildRunner delegates to werkdock; webspace install path replaces the self-build prototype
2026-09-02 07:12:07 +02:00
mhoennig ce4b44b2fc Merge pull request #6 from mhoennig/werkdock-extraction
Werkdock bootstrap: a docker-shaped bwrap sandbox CLI, grown in werkdock/
2026-09-02 07:12:01 +02:00