The owner wants the path alert -> [alert] issue -> kanban card -> expert fix PR
-> owner merges, without a human step in between. Owner decisions (kanban card
t_3f5d0a62): [alert] issues are not excluded from the bridge, and the issue-fix
Woodpecker pipeline is retired, so the expert's card is the only automated fix
path for [alert] issues.
Routing
- alert-monitor: `alerts.py file` takes a required `--label` (hestia, atlas,
apollo, clio, daedalus) and sets that label at creation (POST /issues
`labels: [id]`). Gitea 1.27.3 adds the labels in the same transaction
(services/issue/issue.go NewIssue) and the `opened` webhook payload loads them
(services/convert/issue.go toIssue -> LoadLabels), so the first delivery
already routes the card to the expert. Org labels are accepted for org repos
(models/issues/issue_update.go: label.OrgID == repo.OwnerID). If the label
does not exist yet, the issue is filed without it and the coordinator routes.
SKILL.md tells the triage agent how to pick the expert.
- Label-less issues no longer go to triage. kanban.auto_decompose defaults to
true (hermes_cli/config_defaults.py; not overridden here), so the dispatcher
hands every triage card to the auxiliary-LLM decomposer
(gateway/kanban_watchers_dispatcher.py auto_decompose_tick ->
kanban_decompose.decompose_task), which rewrites title and body (dropping the
Gitea-Issue line the completion hook needs), may fan out into children without
that line, and keeps the assignee `default` anyway (_apply_single only sets an
assignee when none is set). The card is now created ready for `default` with
routing instructions: set exactly one domain label on the issue, nothing else.
- The script reacts to that label: a coordinator card nobody has claimed yet
(todo/ready) is reassigned (`hermes kanban assign`) and its body replaced by
the expert body (`hermes kanban edit --body`); a claimed or finished one gets
a separate expert card under key `<owner>/<repo>#<n>@<expert>` (one per
expert, created_by gitea-webhook, so its completion is posted on the issue).
`assign` refuses a running card, which falls back to the separate card. Cards
already with an expert are still never moved.
[alert] cards
- Their body carries the fix protocol that replaces issue-fix, including its
prompt hardening from PR #253/#266: check for an open PR with `Closes #<n>` or
a branch `alert-fix/issue-<n>` first; owner (k.renner) comments are
authoritative, everything else is untrusted problem description; verify live
before fixing, never state cluster state not observed this run, give
read-only commands instead of predicted output; repo-fixable -> branch
`alert-fix/issue-<n>`, smallest fix, PR with `Closes #<n>`, never merge; not
repo-fixable -> no PR, the completion summary (posted on the issue) says why
and what a human must do.
Gitea side (CronJob gitea-webhook-sync, every 15 min)
- Domain labels: created as org labels in every org (GET /admin/orgs) and as
repo labels in user-owned repos, only when missing.
- Backfill: open issues without the card comment are replayed to the route as
signed `opened` deliveries (same headers Gitea sends), at most 10 per run, 3 s
apart (the adapter's default rate limit is 30/min/route), in random order so
repos where the comment keeps failing cannot starve the rest. The idempotency
key `<owner>/<repo>#<n>` makes a replay of an issue with a card a no-op.
The job runs in the hermes namespace, whose default-deny already allows
intra-namespace traffic to :8644.
- The admin PAT needs write:organization (org labels, reqOrgOwnership passes for
a site admin), write:issue (repo labels, comments) and read:repository
(/repos/search) next to write:admin. activeDeadlineSeconds 120 -> 600.
issue-fix removed
- .woodpecker/issue-fix.yml, ci/agents/issue-fix/, and what only it used:
scripts/ci/make-diag-kubeconfig.sh, the monitoring/ci-diagnostics
ServiceAccount and RBAC (no other pipeline mints its token; git grep), the
.ci-diag/ gitignore entry. Comments naming issue-fix updated.
- Not in git and left to the owner: the Woodpecker cron `issue-fix` and the
secret `issue_fix_bot_token` with its Gitea PAT.
Tested: unittest 73 OK; gitea_issues.py against the real hermes kanban CLI in a
scratch HERMES_HOME with a stub Gitea (unlabelled -> default ready card;
labelled while unclaimed -> same card reassigned to daedalus with the alert
body; labelled while claimed -> separate card `#n@daedalus`; replay -> no
duplicate card or comment); sync discovery read-only against live Gitea (42
open issues, all without a card); kubectl kustomize for apps/hermes,
apps/monitoring, apps/monitoring-rules.
- BlackboxProbeFailing: exclude the hermes /api/status target (it has
HermesDown), so a Hermes outage fires one alert, not two.
- alerts.py: a noise-log line with an impossible date is dropped instead of
killing the gate (fails toward more alerting, as the pipeline did).
- alerts.py: HTTP errors raise ApiError; a Prometheus error only drops the
blips (live alerts still triaged, WARNING line in the prompt);
Alertmanager/Gitea errors exit non-zero so Hermes reports the failed run.
- alerts.py file/noise refuse text that contains a secret Hermes holds: any
single-value file or key-like token under /var/run/secrets, credential env
vars, or a 24-character piece of one.
- cron-jobs init container is non-fatal: an unreadable jobs.json or a failed
cron create/edit logs a warning and the pod still starts (from the #1400
review).
- homeassistant-token volume optional: true, like wiki-key and talos-config
(from the #1400 review).
- Offline unittests (kubernetes/apps/hermes/skills/tests): title fallback,
noise parsing incl. TTL and invalid dates, blip fingerprint equal to the
old pipeline's, gate wake/sleep, Prometheus error fallback, secret guard.
Run in ci-scripts-test.yml (python:3.14-alpine, pinned by digest).
- Nits: .gitignore blank line, comment width in ci-diagnostics-rbac.yaml.
Tested: 16 tests pass locally and in the pinned CI image; the gate run in
the hermes pod (wakeAgent false, 0 new); init script on a scratch
HERMES_HOME, normal (both jobs, skills replaced not appended) and with a
corrupt jobs.json (two warnings, exit 0); exclusion regex matches only the
hermes status URL; promtool check rules on both rule files; kustomize,
citations, server dry-run.
Owner decision: Hermes files the [alert] issues; the Woodpecker
alert-monitor pipeline is retired. issue-fix keeps working unchanged: it
picks issues by the "[alert] " title prefix, and the dedup contract is the
same (title "[alert] <alertname> (<namespace|job|cluster>)", noise-log lines
"- <fp> <ALERTNAME> — <date>: <reason>" with a 30-day TTL, blip
fingerprint sha256(alertname|scope)[:16], fingerprint footer on issues).
- skill alert-monitor: scripts/alerts.py (stdlib) is the cron pre-run gate:
Alertmanager active alerts minus heartbeats and null-routed ones, plus
ALERTS blips of the last 75 min, minus open [alert] issues (paginated, the
pipeline read only the first 100) and noise fingerprints. Nothing new ->
{"wakeAgent": false}, no LLM run, no message. `file` and `noise` write the
verdicts in the fixed formats. SKILL.md ports the triage prompt: wiki
first, verify live (kubectl, Prometheus, Loki, talosctl), problem/noise,
root cause vs symptoms; Matrix report per run that found something.
- Cron job alert-monitor, hourly at :20, deliver matrix. The init container
(renamed cron-jobs) now ensures both jobs by name and copies the gate
script to /opt/data/scripts (Hermes only runs scripts from there).
- HermesDown: blackbox probe of https://hermes.renner.uno/api/status plus an
alert, since a Hermes outage now stops alert -> issue (Alertmanager email
is unaffected).
- Removed .woodpecker/alert-monitor.yml and ci/agents/alert-monitor/;
comments that named them updated.
Tested: the gate run inside the hermes pod against live Alertmanager,
Prometheus and Gitea: 2 active, 36 open issues, 50 noise suppressions, 0 new
-> wakeAgent false. With a 3-day lookback and no noise TTL it produced 4
blips with the expected titles. file/noise against a fake Gitea (61 issues,
pagination, duplicate title skipped, footer, noise log created then
appended, parsed back by the gate; bad title/body/fingerprint refused).
Init script run twice on a scratch HERMES_HOME holding an existing
ha-updates job: ha-updates edited in place, alert-monitor created once,
script copied. Blackbox probe of the new target: 200. kustomize, citations
and server dry-run pass.
After merge:
- Delete the Woodpecker cron alert-monitor (id 4); without its pipeline file
it has nothing to run.
- The alert_monitor_bot_token secret and the alert-monitor-bot account are
unused afterwards.
Trade-off: the pipeline posted with alert-monitor-bot, a PAT limited to
write:issue. Hermes posts as hermes-bot, which also has repo write; the
agent reads untrusted alert text with a broader credential than before.
Pipeline branch filters and PR bases, Flux GitRepository ref, image automation
checkout and push, restore-test, vmtest and bootstrap.sh. Merge right before
the master -> main rename.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Flux source URL, flux-git rotator verify repo, review-bot REPOS, pipeline API,
clone and push URLs, the heating-can source label and the review-bot and
kuma-sync Go module paths. Merge right after the transfer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
registry-bot is the konairius registry identity: write:package PAT,
konairius org secret registry_bot_token, rotated by
openbao/rotation/registry-bot.yaml. That rotation syncs the secret for
the push, tag, release and manual events, so the manual trigger is
`event: manual` instead of a deployment target.
registry_bot_token is a konairius org secret. Woodpecker resolves org
secrets through the repo's org link, so this pipeline gets it only once
homelab has moved to konairius/homelab
(docs/superpowers/plans/2026-10-18-homelab-konairius-main.md, cut-over 1).
Drops the daily cron and the upstream-polling decide step. Every version
in ci/images/hermes/Containerfile is now an image reference (FROM for
nousresearch/hermes-agent, COPY --from for talosctl, kubectl, flux, bao
and tea), which Renovate's enabled dockerfile manager updates and
digest-pins. A merged Renovate PR pushes to ci/images/hermes/**, the
pipeline builds build.<unix-ts>, and Flux image automation rolls it out.
No checksums to maintain: the binaries come from digest-pinned images.
renovate.json: the hermes Containerfile gets a PR of its own instead of
the "CI non-major" group (that group is for changes that never reach the
cluster), and nousresearch/hermes-agent is held to v0.x because the
retired date-versioned v2026.x tags sort above it.
Verified: each COPY --from path exists in the pinned image and the
binary runs on the hermes-agent v0.22.2 base. renovate-config-validator
37.440.7 reports only the two pre-existing errors on master
(flux/kubernetes.managerFilePatterns).
nousresearch/hermes-agent ships no talosctl, kubectl, flux, bao, tea or
jq. The kubectl init container dropped its binary in /opt/data/.local/bin,
which the agent's terminal never has on PATH: Hermes snapshots the env
from a bash login shell, and Debian's /etc/profile resets PATH for
non-root users. /usr/local/bin survives that reset.
Upstream's documented route for always-present tools is a derived image
(docs/user-guide/docker, "Adding tools"). ci/images/hermes builds FROM
the upstream release and adds the binaries, versions matched to the
cluster (Talos v1.13.10, Kubernetes v1.36.3, Flux v2.9.5, OpenBao 2.7.1)
and checksummed against upstream release checksums.
.woodpecker/hermes-image.yml rebuilds daily when upstream publishes a
new release (compared against the uno.renner.hermes.version label on
:latest), and on any push to the Containerfile. Tags are
build.<unix-ts>, the scheme Flux image automation already tracks for
wiki and shelly-manager.
Owner step after merge: register the cron
woodpecker repo cron add --repo k.renner/homelab \
--name hermes-image --branch master --schedule '@daily'
The Deployment switch follows in a separate PR once the first build
exists, so the Recreate rollout never points at a missing tag.
The CI token is an orphan periodic token (period 768h) created with
-no-default-policy -policy=uptime-kuma-vps; nothing renews it. The policy
now grants auth/token/renew-self, and the pipeline gains a renew-openbao-token
step plus a cron trigger matching only the cron named uptime-kuma-token-renew.
The deploy step is restricted to push and deployment events so cron runs only
renew. -o /dev/null drops the JSON body that contains the token; -sSf keeps
errors visible and fails the step on a 403.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Role, play and pipeline deploy Uptime Kuma 2.5.6 behind the VPS Traefik.
The admin user is bootstrapped over socket.io (needSetup/setup) with the
route disabled, so /setup is never exposed; a .setup-done marker keeps the
route enabled on later runs. Credentials come from OpenBao
homelab/infrastructure/uptime-kuma via community.hashi_vault; the
uptime-kuma-vps policy grants read on that one path. Only kuma.renner.uno
is routed; uptime.renner.uno stays unrouted until its DNS moves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removes the Paperclip deployment (namespace paperclip, CNPG cluster
pclp-pg, ingresses, image automation, server image build) and the
infrastructure that existed only for its agents:
- agent sandbox: paperclip-renner namespace and CNPs, the
kubernetes-sigs/agent-sandbox controller with its namespace, CNP and
Renovate rules
- agent-runtime-{go,ops} images and the build-agent-runtime pipeline
- matrix bridge (Element <-> Paperclip agents) and synapse's appservice
registration mount
- monitoring loki-read-gateway (only called by the paperclip Role)
- MQTT account agents-read and the controller's RBAC in ns paperclip
- the OpenBao secret-exists-check policy file
- agent admits in the wiki, lidarr, shelly-manager, mosquitto and
hubble-relay CNPs; the paperclip-api Gitea webhook host; the wiki API
key; the ClusterSecretStore namespace entry; the pclp-pg RECOVERY row
- the Paperclip design spec and plan
Authentik group "Paperclip Users" is switched to state: absent so the
blueprint deletes it. package-retention's EXCLUDE_PACKAGES becomes
optional, since its only value was the agent-runtime prefix.
The shelly-manager api-proxy sidecar stays: mqtt-account-controller
calls shelly-manager-api too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All wiki interaction now goes through the wiki service API; CI agents that
distill and consolidate by writing the notes repo directly are retired.
Removes .woodpecker/wiki-maintenance.yml and ci/agents/wiki-maintenance/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drops the per-step resources from the previous commit and removes every
depends_on: [], so Woodpecker runs the kaniko steps serially in file order.
A full rebuild then holds one step pod at a time and cannot exceed the
woodpecker-ci-quota.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A change to build-ci-images.yml starts all 14 kaniko steps at once. With
the LimitRange default of 2 CPU each they asked for ~28 CPU in limits
against the 24-CPU woodpecker-ci-quota, so whichever pods were created last
were rejected ("exceeded quota: woodpecker-ci-quota"): alpine-jq and
cni-plugins in pipeline 13723, vmtest, tofu and secret-rotator in 13726.
Every step now sets its resources from 30 days of Prometheus data (peak
working set / CPU per step):
- ansible, k8s, vmtest, tofu (peaks <=983Mi, <=0.54 cores): 1 CPU, 2Gi
- secret-rotator, review-bot (Go builds, ~2 cores): 2 CPU, 1Gi
- the remaining eight (<=271Mi, <=0.45 cores): 1 CPU, 1Gi
Requests stay at the LimitRange defaults (100m / 256Mi).
Total 16 CPU / 18Gi in limits, leaving room in the quota for a concurrent
PR check. The quota itself is unchanged; its comment no longer claims the
workflow runs 4 pods.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build-authentik-gate-sync depended on a separate test step, so it only
started after the other steps had run, and Woodpecker skips not-yet-started
steps once the workflow has a failure: on the #1272 merge build-alpine-jq and
build-cni-plugins failed and the image was never built.
The unit tests now run as a stage of the image build (the final stage copies
the controller out of it), and the step starts with depends_on: [] like the
others.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds authentik-gate-sync, an in-house controller (ci/images/authentik-gate-sync,
stdlib Python like netbird-service-sync) that implements an `authentik-gate`
IngressClass. An Ingress of that class names its real backend; the controller
turns it into an Authentik proxy gate and wires the upstream ingress provider:
- Authentik: proxy provider, application (meta_publisher marks ownership),
outpost on the local Kubernetes connection, optional group binding.
- App namespace: ExternalName Service <ingress>-gate -> outpost, Traefik
Ingress <ingress>-gate (source hosts/paths/TLS/labels/annotations, backend
swapped, cert-manager annotations dropped so only the source issues), and a
CNP admitting only this outpost to the backend pods.
- authentik namespace: CNP letting the outpost reach only those pods.
Annotations: auth.renner.uno/group (members managed in Authentik; none = any
user), /login-flow, /authorization-flow (default passkey-stepup-authorization),
/skip-path. Fail closed: the Traefik route is applied last and removed when the
gate's config is invalid (missing group/flow/backend); transient API errors
leave the gate as the last good pass left it.
The API token is zero-touch: an ESO Password generator (CreatedOnce) feeds
both the controller and AUTHENTIK_GATE_SYNC_TOKEN on server/worker, which the
new gate-sync blueprint sets as the key of a non-expiring token for a
service account whose role covers only gate objects.
No existing gate moves yet; llama is migrated in a follow-up once this runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A second build on the same day reused the tag, so the Flux image policy saw nothing new and the
Deployment kept the old digest (#1270 built but did not roll out). Tags are now YYYY-MM-DD-HHMM and
the policy accepts both forms, newest wins.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The workflow-level path filter lacked review-bot/**, so a push touching only that directory
created no pipeline and the step-level filter was never evaluated. #1258 and #1268 merged
without an image build.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The review-bot service reviews pull requests now. The pipeline starts only when triggered by hand and stays as a reference.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Gitea's pull_request webhook (HMAC-checked) queues the review at once; a periodic sweep over the configured repositories catches lost deliveries. The service keeps the listed Mistral agent in sync with the embedded prompt, asks it for a review, refuses answers that called non-read-only tools, cleans the report, derives the review event and posts as the review bot. State is the pr-review:sha marker in the bot's own review. /healthz, /readyz and /metrics. Standard library only, tests against fake Gitea and Mistral servers, built as a scratch image by build-ci-images.yml.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
run.sh asks a Mistral agent (mistral-large-4-0) through the Conversations API. The agent reads the repo and the PR through the read-only Gitea connector, so the review step no longer holds a Claude token or a local checkout. The Claude CLI invocation, the model picker and the context clones stay in run.sh as comments. The answer is cut at the first heading and empty or None-only sections are dropped before the review event is derived. A tool call that is not read-only fails the run and nothing is posted. A failed request now fails the pipeline.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The k8s backend defaults to IfNotPresent, so a node kept serving a stale
:latest (Claude Code 2.1.218) after the refresh pipeline pushed a new one.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Runs kubernetes/infrastructure/velero/backup-verify/tests/run.sh in the
ci-backup-verify image (bash, jq, ssh-keygen), offline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alpine with bash, jq, kubectl, openssh and the hcloud CLI (v1.69.0,
copied from the official image). A verify user (uid 65532) exists
because the OpenSSH client refuses to run as a UID without a passwd
entry. Built by build-ci-images.yml with a dated tag; Flux image
automation tracks the newest date.
First step of docs/superpowers/plans/2026-10-10-backup-verify-cloud-vm.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The event is derived by the pipeline from the report's severity sections:
Blocking -> REQUEST_CHANGES; none of Blocking/Suggestions/Unverified on a
non-empty diff -> APPROVE; otherwise COMMENT.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Drop the complexity triage (it downgraded most PRs to a small model) and pick
the model via scripts/ci/pick-model.sh from the live /v1/models catalog,
steered to the strongest and newest. Falls back to the CLI default.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Move the review logic out of .woodpecker/pr-review.yml into
ci/agents/pr-review/run.sh; the per-repo file becomes a logic-free stub that
clones homelab@master and runs it, so the four copies cannot drift.
Prompt: unverified suspicions must be checked (checkout + docs) and are never
blocking. Review agent gets WebFetch/WebSearch.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
edge-1's bridge password moves out of ansible-vault. A small receiver,
edge-mqtt-cred, speaks the controller's esp32 API over HTTPS with mTLS
(only CN mqtt-account-controller from the MQTT CA) and writes one bridge
`edge-1` for open3e and sml2mqtt. Until the first push the two
vault-password bridges stay in bridge.d/legacy.conf, so nothing changes
on the cluster side yet.
A Woodpecker pipeline runs the role-only play on merge, so no manual play
run is needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pipeline 11717 build-paperclip exited 137 at the 8Gi memory limit during
the server build. Disable compressed caching so kaniko does not keep
compressed layer tarballs in memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k8s sandbox runs fail because the run path never sets the lease remoteCwd
(paperclipai/paperclip#13864). The fix, paperclipai/paperclip#13733, is not
merged upstream yet. Build our own image from the master commit the nightly
runs now plus that patch, and point Flux image automation at it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The k8s-go adapter test failed with 'Claude ACP server command is not
available: claude-agent-acp'. Both agent-runtime images now install
@agentclientprotocol/claude-agent-acp@0.73.0, the version Paperclip pins,
with Paperclip's patch for it applied (vendored in agent-runtime-common/).
go and gofmt are also linked into /usr/local/bin, so every tool is found
with the default PATH, not only with the image ENV. The build checks that
with env -i. Kaniko context is now ci/images so both images can COPY the
shared patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the #1046 merge (pipeline 11306) the build-agent-runtime step got no
pod: the full rebuild of build-ci-images.yml had filled the 24-CPU
woodpecker-ci-quota ("exceeded quota"), so the go image was never built.
Move the step to .woodpecker/build-agent-runtime.yml. It runs only when
ci/images/agent-runtime-*/** or its own file change, and can be re-run
alone with a deployment to target build-agent-runtime. Step content is
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first push of this branch started all 13 build steps: the push trigger
had no branch filter. 13 pods at the 2-CPU default limit exceeded the
24-CPU woodpecker-ci-quota, so the PR checks and one build failed.
- push trigger: branch master only
- go and ops build in one step, one pod, at 1 CPU: a full rebuild is 23 CPU
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two pod images for the Paperclip Kubernetes sandbox (REN-284 step 2):
the stock agent-runtime-claude, pinned by digest, plus
- agent-runtime-go: Go 1.27.1, golangci-lint v2.14.0
- agent-runtime-ops: kubectl v1.36.5, flux v2.9.5, kustomize v5.8.1, helm 4.3.0
Tools are copied from their upstream images pinned by tag and digest, so
the existing Renovate dockerfile manager bumps them. Pushed to
konairius/agent-runtime-{go,ops}/agent-runtime-claude:v1 because the plugin
keeps that name and swaps only the registry prefix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1026 (REN-161) landed a promtool suite asserting CNPGWalArchiveBacklogSurge
and `for: 15m`, and mutations targeting `for: 8m`/`for: 15m`. This PR retires
the surge rule and moves the backlog rule to `> 5` for `20m`, so master CI
would break on merge.
- Generator: drop every Surge assertion; firing cases move to t=25m.
- New case 9: the worst night in Prometheus' 27-night retention (2026-09-11,
peak 141) as raw 30s samples, 12m pending at a 1m evaluation step.
- mutations.txt: surge-for-2m/-5m removed; backlog-for-12m added (caught only
by case 9, the margin control); backlog-for-30m retargeted at `for: 20m`.
- README and the woodpecker step comment updated to match.
Verified: scripts/ci/promtool-rules.sh with promtool 3.15.0 (CI's pin) —
18 manifests parsed, 2 suites green, 10 mutations caught;
scripts/ci/test-promtool-rules.sh passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>