Files
felhom.eu/REPORT.md
T
admin ebdc04601d
gates / gates (push) Successful in 15s
docs(hub v0.108.0): the delivery grain, the cooldown ruling, and gate 11's first subject
The alarm ladder gains §6.2 - which events are per-app, per-run, per-tier or
coarse, and why the default is coarse. CONTEXT records two rulings: the grain is
allow-listed rather than inferred from the payload, with crossdrive_failed as
the proof that a payload rule would have been wrong; and a finding recorded only
in REPORT.md has a lifetime of one session.

R-389 closed and compressed, keeping its rules and naming the commit whose
git show returns the full text. R-390 and R-391 left open.

REPORT.md is gate 11's first real subject and passes: six observations, two
FILED, four NOT-A-FINDING with their reasons. Three of those declarations are
things a tidier report would have omitted - the gate's own spec would have
passed the item it was built to catch, the burst has no ceiling, and ArgoCD
said "successfully rolled out" while still running the old image.

STATUS carries forward the one thing outstanding: the controller floor still
reads 0.222.0 while the golden reads 0.223.0.
2026-08-23 14:12:23 +02:00

14 KiB

REPORT — hub v0.108.0 (R-389), gate 11, and the instruction that invited the gap

Session 2026-08-23. felhom.eu is the subject; the controller and agent were touched only to register the shared gate. No controller release — no golden bake, no vouch, no floor. No halt condition fired. Nothing was dropped.

1. Baselines, and the hub's four numbers as read

Repo at start at end
felhom.eu 2f7c9a6 (hub v0.107.0) hub v0.108.0 deployed
felhom-controller 1da2c9c (v0.223.0) unchanged — runner registration only
felhom-agent 40d857b (v0.130.0) unchanged — runner registration only

Hub's four numbers, live from GET /configuration before starting:

Field Value
golden_version 0.223.0
agent_version 0.130.0
min_agent 0.129.0
controller floor (min_controller_version) 0.222.0

The task predicted the floor at 0.223.0 and it reads 0.222.0. The operator vouched the golden but has not yet raised the floor — the last step of the previous release, which STATUS.md says to do "last, in its own save". Carried forward as item 1 there. Not a halt; the correct reading is simply different from the prediction.

The golden-currency gate stayed green throughout and no bake was needed, exactly as §1 said it should be: the controller CHANGELOG's newest entry and the newest baked golden are both 0.223.0 and this session moved neither.

2. Documents read

hub/internal/notify/dispatcher.go (cooldownTierSuffix + cooldownRunSuffix docstrings in full, processOperator and its R-182 suppression-logging block, operatorOnlyEvents), scripts/repo_gates.py (whole docstring, including "WHY 10 IS HERE" and "WHY 7 IS HERE"), scripts/due_checks_gate.py, documentation/PROMPT-TEMPLATE.md §15, and the alarm ladder at documentation/architecture/08-alarm-ladder.md — extended here with §6.2, the delivery grain.

3. The AppDetails emitter count, measured

Three, exactly as §3 said. grep -rn "AppDetails{" --include=*.go over the controller, excluding tests:

Emitter Event Severity Reaches the operator leg?
notifier.go:502 app_deployed info no — info is dropped by severityNotifies
notifier.go:563 app_start_failed warning yes
notifier.go:661 app_removed info no

No fourth emitter. No halt.

But the sweep found something the AppDetails question could not: stack_name is also carried by a different struct, CrossDriveDetails (notifier.go:151-157), used by crossdrive_failed (severity error, so it does reach the operator leg) and crossdrive_completed. This is what makes the allow-list load-bearing in fact rather than in principle — a payload-shape rule would have split a backup-family event per app and silently undone R-182. It is Scenario C's live subject.

4. Files, commits, CI

Commit Repo Contents
f751aea felhom.eu R-389 filed — alone, before any code (Phase 1)
2fc4a15 felhom.eu the suffix + allow-list + tests, gate 11, template fix, R-390/R-391
45659bd felhom.eu hub v0.108.0 CHANGELOG + manifest bump
f8c9390 felhom-controller gate 11 registered; its REPORT's observations marked up
058b945 felhom-agent gate 11 registered

CI runs confirmed BY ID — and the listing was checked for truncation rather than trusted, since a silently truncated listing has already cost this arc a false claim:

Commit Repo CI id run_number Result
f751aea felhom.eu 412 263 success
2fc4a15 felhom.eu 413 264 success
45659bd felhom.eu 416 265 success
f8c9390 felhom-controller 414 90 success
058b945 felhom-agent 415 55 success

The API reported total_count 265 / 91 / 55 against 3 rows shown in each case — i.e. the pages were known-partial and the newest rows are the ones quoted, not the whole set.

5. Red-proofs — two planted, both seen failing

# Mutation Layer, and why that layer Observed
1 cooldownStackSuffix dropped from the key expression processOperator's KEY — where the collapse physically happens 2 apps down inside the hour produced 1 operator mail(s), want 2 (suppressed=1), and the suppression row reads key=c1:app_start_failed — the live shape reproduced in a unit test
2 the allow-list check removed from the suffix the REGISTER — the fence that keeps the backup family coarse crossdrive_failed … = ":bookstack", want "", plus app_deployed, app_removed, backup_failed — the fence convicting exactly the types it was written for

Both mutations asserted their pre-fix text was present before rewriting and printed MUTATION APPLIED. Neither passed first time, and the check for that was explicit after yesterday's inert mutation.

Gate 11 carries its own controls rather than a mutation, because the gate is the guard: ten cases in §Part 3 of the drill record, including the historical red-proof against yesterday's real file.

6. Test counts

Repo Before After
felhom.eu hub 709 716

go build ./... && go vet ./... && go test ./... in the hub → exit 0, zero failures. python3 scripts/repo_gates.py --fast → 12/12 OK in felhom.eu; controller and agent runners both OK with gate 11 registered.

7. Deployed hub version, and the manifest commit

gitea.dooplex.hu/admin/felhom-hub:0.108.0, ArgoCD Synced / Healthy, rolled out. Deployed by 45659bd, which bumped manifests/hub.yaml:128. The image was pushed to the registry before that commit landed, so a sync could never have pointed at a missing tag. Never kubectl set image.

One thing worth stating because it looked like success and was not: the first refresh=hard + sync reported successfully rolled out while the deployment still read 0.107.0 and the app read OutOfSync — ArgoCD had synced a pre-push revision. A second hard refresh took it to Synced rev=45659bd and the image then read 0.108.0. The rollout message alone would have been a false confirmation; the image tag is the observable that settles it.

8. The live walk

All counts filtered created_at >= T0 (2026-08-23 11:56:06Z) so yesterday's two inert Scenario H probe rows cannot contaminate them.

Step 1 — Scenario A: two different apps, four minutes apart ✅

sent    operator  Telepített alkalmazás nem fut: OpenGist        2026-08-23 11:56:57
sent    operator  Telepített alkalmazás nem fut: Calibre-Web A   2026-08-23 12:00:57
                                                    sent: 2   suppressed: 0

Yesterday, the identical shape: bookstack sent 09:27:51, privatebin suppressed 09:31:51 under key=demo-hp:app_start_failed.

Step 2 — Scenario B: each app again inside the hour ✅

suppressed  OpenGist      operator cooldown 1h, key=demo-hp:app_start_failed:opengist     12:02:48
suppressed  Calibre-Web   operator cooldown 1h, key=demo-hp:app_start_failed:calibre-web  12:02:48

One sent and one suppressed per app — the hour is unchanged — and the two keys differ by the app, against v0.107.0's single shared key. The hub records the key only on a suppression, which is why this step is what exposes it.

Method note: the repeats were posted through /api/v1/event, the exact endpoint the controller invokes, with the controller's own AppDetails payload. The controller's own event is edge-triggered per app, so a down→down cycle is silent by design and cannot re-fire from the box.

Step 3 — Scenario C: a backup-family event carrying stack_name ✅

sent        opengist
suppressed  calibre-web   operator cooldown 1h, key=demo-hp:crossdrive_failed   12:03:44

Byte-identical to v0.107.0's key, with no app suffix. The "before" value was obtained two independent ways, both stated: derivation from the v0.107.0 expression (which has no app term), and TestR389_NoOtherEventTypeKeyChanges, which models that expression inline for 12 event types and carries a positive control proving it can see a key change before reporting that none occurred.

Step 4 — Part 2's burst ✅ (see §9)

Step 5 — Part 3's gate ✅ all controls plus the historical red-proof (see §10)

9. Part 2's three counts, and the judgement

Three apps stopped in one scan, after checking none carried a live cooldown — a stale one would have halved the count and made the answer look better than it is:

attempted 3
sent 3
suppressed 0

Plain judgement: per-app is the right grain and this volume is acceptable. The reference box has 8 deployed apps, so a total outage is 8 mails; the boot grace (90 s), the quiesce grace (180 s) and the per-app edge trigger absorb reboots, backup cycles and persistently-dead apps. No burst-digest row was filed, and the condition that would reopen it is recorded rather than left implicit: the volume scales linearly with app count and has no ceiling, so a box large enough that a total outage is unreadable is the point at which the answer becomes a digest with a customer message — not a wider cooldown.

10. Which repos gate 11 is registered in

Repo Registered Note
felhom.eu yes — gate 11 its own REPORT.md is the gate's first real subject
felhom-controller yes already had SHARED_* constants; one constant + one GATES line
felhom-agent yes same; no observations section today, so it passes quietly
app-catalog-felhom.eu NO filed as R-391

Why not the catalog. catalog_gates.py has no shared-gate mechanism at all: run_gate joins every entry against its own scripts/ directory, so it cannot invoke a sibling repo's script; and the loop appends --all to every gate unconditionally, which the observations gate would read as a path. Registering there needs run_gate's contract widened and its argument handling changed — a refactor of a runner whose shape is deliberately different, in a repo this task marked out of scope. Exposure today is nil (that repo's REPORT.md has no observations section, and the gate passes quietly on that), but a future session could write one. Filed rather than left as a sentence in a report, which is the exact failure this session exists to fix.

11. Evidence

documentation/audits/DRILL-cooldown-grain-2026-08-23/evidence/ — 17 files: 2 red-proof transcripts, 3 gate-11 control files covering 10 cases, 11 live-walk files, and a 1803-line controller-log window pulled off before the apps were restored.

12. Teardown, three layers

  1. Guest 9201 / apps — nothing provisioned. Five apps stopped across the walk (opengist, calibre-web, kimai, romm, paperless-ngx); all restarted and confirmed healthy, 17 containers up. The three retained subjects (docmost, bookstack, privatebin) were not touched. No app rebuilt, redeployed or restored.
  2. No VM, no bake — this session built no golden and started no drill VM.
  3. Hub-side, stated explicitly. The hub was written: deployed to v0.108.0 via the manifest, and six probe events POSTed for Scenarios B and C (two app_start_failed, two crossdrive_failed, plus yesterday's two, left in place deliberately). They are inert event rows for demo-hp, named here rather than left to be found, and every count in this report is created_at-filtered so they cannot contaminate it. Nothing else: no appliance registered, no customer created, no artifact manifest changed, floor untouched.

13. Register size

File Before After
OPEN-ITEMS.md 328,132 B 331,024 B
CLOSED-ITEMS.md 74,642 B 76,855 B

R-389 filed, then closed and compressed into CLOSED-ITEMS.md in the same session. R-390 (the golden-bake runbook's missing pveam update) and R-391 (the catalog runner) filed open.

14. Observations

Gate 11's first real subject is this section. Each item carries FILED: R-NNN naming a row opened this session, or NOT-A-FINDING: with its reason.

  1. The gate's specification would have passed the item the gate exists to catch. It said an observation may "cite an R-NNN that resolves" — but yesterday's lost item cites R-182, which resolves, as an analogy rather than as its own row. No parser can tell citation-as-precedent from citation-as-filing by reading prose, so the marker is explicit instead. The discrepancy is recorded in the gate's docstring and proven by EDGE 7. NOT-A-FINDING: this is a design decision taken and documented inside the deliverable itself, not a defect left behind — the gate ships with the stricter rule and its reasoning, so there is nothing outstanding for a row to track.
  2. The burst has no ceiling. Three apps in one scan produce three mails; the reference box's worst case is 8, and it scales linearly with app count. NOT-A-FINDING: measured and judged acceptable at today's scale in §9, with the reopening condition stated there; filing a row for a digest would queue work the operator has not asked for and that needs a customer message and their call on volume.
  3. ArgoCD reported "successfully rolled out" while still running the old image. The first sync ran against a pre-push revision; only a second hard refresh moved it. The rollout message alone was a false confirmation and the image tag was the observable that settled it. NOT-A-FINDING: the existing runbook already says to verify the image tag after a sync, and this run followed it and caught the discrepancy — the procedure worked; recording the near-miss here is the appropriate weight.
  4. The golden-bake runbook still omits pveam update, and its failure names the wrong cause. FILED: R-390
  5. Gate 11 is registered in three runners, not four — catalog_gates.py cannot invoke a sibling script and appends --all to every gate. FILED: R-391
  6. Deliberately left open, untouched: R-102, R-359, R-385, R-387, R-388's redesign. NOT-A-FINDING: a pointer to rows that already exist, carried so their absence from this session reads as deliberate rather than forgotten.