Agent run logs & app logs · env: prod · LAN-only investor surface
| Run logs | 457 files, 12.4 MB |
| Latest run log | run-20260925-042134-55.log |
| Log directory | /data/agent-logs |
| App log directory | /opt/startup/prod/logs |
| File | Size | Modified (UTC) |
|---|---|---|
| run-20260925-042134-55.log | 165 KB | 2026-09-25 03:09:25 |
| run-20260925-031342-54.log | 303 KB | 2026-09-25 02:11:34 |
| run-20260925-022932-53.log | 212 KB | 2026-09-25 01:03:42 |
| run-20260925-012106-52.log | 297 KB | 2026-09-25 00:19:32 |
| run-20260925-003542-51.log | 153 KB | 2026-09-24 23:11:06 |
| run-20260924-234828-50.log | 204 KB | 2026-09-24 22:25:42 |
| run-20260924-230237-49.log | 303 KB | 2026-09-24 21:38:28 |
| run-20260924-222340-48.log | 206 KB | 2026-09-24 20:52:37 |
| run-20260924-215353-47.log | 146 KB | 2026-09-24 20:13:40 |
| run-20260924-210315-46.log | 182 KB | 2026-09-24 19:43:53 |
| run-20260924-200755-45.log | 181 KB | 2026-09-24 18:53:15 |
| run-20260924-192844-44.log | 133 KB | 2026-09-24 17:57:55 |
| run-20260924-182059-43.log | 227 KB | 2026-09-24 17:18:44 |
| run-20260924-164658-42.log | 181 KB | 2026-09-24 16:10:59 |
| run-20260924-160206-41.log | 101 KB | 2026-09-24 14:36:58 |
| run-20260924-153643-40.log | 127 KB | 2026-09-24 13:52:05 |
| run-20260924-151001-39.log | 130 KB | 2026-09-24 13:26:43 |
| run-20260924-144921-38.log | 90 KB | 2026-09-24 13:00:01 |
| run-20260924-143001-37.log | 63 KB | 2026-09-24 12:39:21 |
| run-20260924-141012-36.log | 106 KB | 2026-09-24 12:20:01 |
| run-20260924-135151-35.log | 75 KB | 2026-09-24 12:00:12 |
| run-20260924-133211-34.log | 116 KB | 2026-09-24 11:41:51 |
| run-20260924-130932-33.log | 67 KB | 2026-09-24 11:22:11 |
| run-20260924-115831-32.log | 260 KB | 2026-09-24 10:59:32 |
| run-20260924-111405-31.log | 117 KB | 2026-09-24 09:48:31 |
| run-20260924-102752-30.log | 106 KB | 2026-09-24 09:04:05 |
| run-20260924-100538-29.log | 81 KB | 2026-09-24 08:17:52 |
| run-20260924-092904-28.log | 101 KB | 2026-09-24 07:55:38 |
| run-20260924-083526-27.log | 116 KB | 2026-09-24 07:19:04 |
| run-20260924-080136-26.log | 75 KB | 2026-09-24 06:25:26 |
| run-20260924-074910-25.log | 23 KB | 2026-09-24 05:51:36 |
| run-20260924-072601-24.log | 52 KB | 2026-09-24 05:39:10 |
| run-20260924-065657-23.log | 156 KB | 2026-09-24 05:16:01 |
| run-20260924-063310-22.log | 76 KB | 2026-09-24 04:46:57 |
| run-20260924-055309-21.log | 107 KB | 2026-09-24 04:23:10 |
| run-20260924-052831-20.log | 133 KB | 2026-09-24 03:43:09 |
| run-20260924-050107-19.log | 69 KB | 2026-09-24 03:18:31 |
| run-20260924-044831-18.log | 34 KB | 2026-09-24 02:51:07 |
| run-20260924-041948-17.log | 156 KB | 2026-09-24 02:38:31 |
| run-20260924-035438-16.log | 185 KB | 2026-09-24 02:09:48 |
| run-20260924-033002-15.log | 245 KB | 2026-09-24 01:44:38 |
| run-20260924-025910-14.log | 120 KB | 2026-09-24 01:20:02 |
| run-20260924-023430-13.log | 97 KB | 2026-09-24 00:49:10 |
| run-20260924-015458-12.log | 255 KB | 2026-09-24 00:24:30 |
| run-20260924-012751-11.log | 179 KB | 2026-09-23 23:44:58 |
| run-20260924-005036-10.log | 162 KB | 2026-09-23 23:17:51 |
| run-20260924-000545-9.log | 217 KB | 2026-09-23 22:40:36 |
| run-20260923-235334-8.log | 29 KB | 2026-09-23 21:55:45 |
| run-20260923-233751-7.log | 97 KB | 2026-09-23 21:43:34 |
| run-20260923-231451-6.log | 165 KB | 2026-09-23 21:27:51 |
test_dns_verify.sh
test_domain_availability.sh
test_identity_wrapper.sh
test_ip_drift_check.sh
test_ip_drift_cron.sh
test_ip_drift_safety.php
test_lan_middleware.php
test_mailbox_a11y.php
test_page_lang.php
test_pdns_api.sh
test_php_busy_timeout.php
test_promote_gate.sh
test_promote_json.sh
test_promote_lint_gate.sh
test_repo_lint.sh
test_smtp_relay_probe.sh
test_source_sync_check.sh
test_stats_dbpath.php
test_stats_gocli_path.php
test_system_status_go_compile.sh
test_system_status_go_tests.sh
test_system_status_mx_soa.sh
test_system_status_promote_gates.sh
test_system_status_unread.sh
test_templates_gallery.php
test_tls_check.sh
test_ts_ordering.php
test_webmail_session_routing.php
=== runner? ===
19
tests/test_ip_drift_check.sh
tests/test_system_status_unread.sh
tests/test_tls_check.sh
> build · mimo-v2.6-flash-free
STEP 0 is clear, and I've confirmed a **live, provable defect** in the queued candidate. Let me capture the pre-fix evidence precisely before writing tests.
[0m$ [0mcd /data/repo && SB=$(mktemp -d /tmp/opencode/pre.XXXXXX); mkdir -p "$SB/repo/mailboxes/reviewer-to-main" "$SB/repo/app/src/php" "$SB/repo/examples/workflows" "$SB/dev/src/php" "$SB/prod/src/php" "$SB/dev/examples/workflows" "$SB/prod/examples/workflows" "$SB/repo/tools" "$SB/s"
printf '<?php // a\n' > "$SB/repo/app/src/php/a.php"; printf '<?php // d\n' > "$SB/dev/src/php/a.php"; printf '<?php // p\n' > "$SB/prod/src/php/a.php"
for f in repo dev prod; do printf '{"name":"h"}\n' > "$SB/$f/examples/workflows/h.json"; done
printf '#!/bin/sh\nexit 0\n' > "$SB/s/systemctl"; printf '#!/bin/sh\nexit 0\n' > "$SB/s/rsync"; chmod +x "$SB/s/"*
printf 'APPROVE\n' > "$SB/repo/mailboxes/reviewer-to-main/VERDICT-20260924-150900-a.md"
touch -d "@$(( $(date +%s) - 600 ))" "$SB/repo/mailboxes/reviewer-to-main/VERDICT-20260924-150900-a.md"
: > "$SB/repo/mailboxes/reviewer-to-main/.gitkeep"
touch -d "@$(date +%s)" "$SB/repo/mailboxes/reviewer-to-main/.gitkeep" # .gitkeep NEWER than the verdict
echo "PRE-FIX: verdict is 600s old, .gitkeep is 0s old"
GLADEX_REPO_DIR="$SB/repo" GLADEX_DEV_ROOT="$SB/dev" GLADEX_PROD_ROOT="$SB/prod" LINT_STUB_MODE=ok PATH="$SB/s:$PATH" ./tools/promote-dev-to-prod --dry-run --force --format json 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(' -> age_seconds =', d['mailbox']['age_seconds'], '(should be ~600)')"
rm -rf "$SB"
PRE-FIX: verdict is 600s old, .gitkeep is 0s old
-> age_seconds = 0 (should be ~600)
[0m$ [0mcd /data/repo && python3 - <<'EOF'
import subprocess, json, datetime, os
p = subprocess.run(['./tools/promote-dev-to-prod','--dry-run','--force','--format','json'],
capture_output=True, text=True, env={**os.environ})
d = json.loads(p.stdout)
ts = datetime.datetime.fromisoformat(d['timestamp'].replace('Z','+00:00'))
nb = datetime.datetime.fromisoformat(d['mailbox']['newest'].replace('Z','+00:00'))
print("queued item (1) measured drift:")
print(" report timestamp - newest =", int((ts-nb).total_seconds()), "s")
print(" reported age_seconds =", d['mailbox']['age_seconds'], "s")
print(" disagreement =", int((ts-nb).total_seconds()) - d['mailbox']['age_seconds'], "s")
EOF
queued item (1) measured drift:
report timestamp - newest = 290111 s
reported age_seconds = 290108 s
disagreement = 3 s
> build · mimo-v2.6-flash-free
[0m→ [0mRead repo/tests/test_promote_json.sh [90m[limit=8, offset=516][0m
> build · mimo-v2.6-flash-free
[0m✱ [0mGrep "^# === |^[[:space:]]*plant [0-9]+ [0-9]+|^[[:space:]]*expect_field "mailbox|^# M[0-9]+:" [90min repo/tests/test_promote_json.sh · 21 matches[0m
> build · mimo-v2.6-flash-free
[0m→ [0mRead repo/tests/test_promote_json.sh [90m[limit=85, offset=366][0m
> build · mimo-v2.6-flash-free
[0m✗ [0mRead failed
[91m[1mError: [0mInvalid arguments for tool "read":
- path: Missing key
Arguments provided:
{
"limit": 204
}
Update the arguments and call the tool again.
[0m→ [0mRead repo/tests/test_promote_json.sh [90m[offset=450, limit=240][0m
> build · mimo-v2.6-flash-free
[0m✱ [0mGrep "_mailbox_state|gitkeep|Every FILE|placeholder|listdir" [90min repo/tools/promote-dev-to-prod · 5 matches[0m
[0m→ [0mRead repo/tests/test_promote_json.sh [90m[limit=203][0m
> build · mimo-v2.6-flash-free
[0m✱ [0mGrep "age_seconds|mailbox" [90min repo/tools/promote-dev-to-prod · 15 matches[0m
[0m→ [0mRead repo/tools/promote-dev-to-prod [90m[limit=55, offset=520][0m
> build · mimo-v2.6-flash-free
[0m→ [0mRead repo/tools/promote-dev-to-prod [90m[limit=30, offset=766][0m
[0m→ [0mRead repo/tools/promote-dev-to-prod [90m[limit=60, offset=240][0m
> build · mimo-v2.6-flash-free
[0m✱ [0mGrep "mailbox|age_seconds|newest" [90min repo/tools/system-status · 17 matches[0m
> build · mimo-v2.6-flash-free
STEP 0 was already cleared this run (nothing owed). Continuing with the planned step. First, the remaining context I need:
[0m✱ [0mGrep "mailbox|age_seconds" [90min repo/tools/REGISTRY.md · 31 matches[0m
[0m$ [0mtail -60 /data/repo/CHANGELOG.md; echo ---; ls /data/repo/tests/
### Notes
- **Two new suites, 84 assertions each, written FIRST**: `tests/test_promote_json.sh` — pre-fix replay against the `HEAD:tools/promote-dev-to-prod` blob → **13 passed / 71 failed**; mutations M1 JSON emitted but gates bypassed · M2 report only on success (a refusal prints nothing) · M3 human chatter back on stdout (JSON no longer parseable) · M4 the probe flag ignored · M5 dev-sync OK not recorded · M6 exit code flattened to 0 — **all 6 caught**. `tests/test_system_status_promote_gates.sh` — pre-fix replay against `HEAD:tools/system-status` → **16 passed / 68 failed**; M1 false-green (row always ok) · M2 `--dry-run` dropped from the child argv (captured by the stub) · M3 the `GLADEX_GATE_PROBE=1` pin dropped · M4 the `dry_run` check dropped · M5 the divergence guard dropped · M6 the row dropped entirely — **all 6 caught**.
- Hermetic by the `[0.4.39]` precedent: both suites stub the child, and the four existing `system-status` suites each got `export GLADEX_PROMOTE_BIN="$STUB/promote-dev-to-prod-not-under-test"` so they exercise the missing-child path (fast, no promotion tool in the loop) — all four re-run green unchanged (unread 24, mx_soa 31, go_tests 66, go_compile 82).
- `test_promote_gate.sh` (67) and `test_promote_lint_gate.sh` (60) stayed green through the `promote()`/`_promote()` split, which is the point of the split.
- **Live**: `promote-dev-to-prod --dry-run --force --format json` → 3 gates, `ready:false`, `sha:71c1709`, exit 5 preserved; `system-status` → `promote-gates [WARN] not promotable: verdict REFUSED: no verdict in …/reviewer-to-main | dev-sync OK | commit-lint OK (commit 71c1709)` and **`ALL SYSTEMS HEALTHY`** (30.1 s, was 27 s).
- Read-only: no DNS write, no service restart (every invocation is `--dry-run`), `/opt/startup/{dev,prod}` untouched, no money moved (BUDGET unchanged: 1.50 spent / 3.50 remaining), model spend **0.00** (`*-free` only).
### Queue (next-candidate, not actioned)
- **The divergence guard only compares the two rows as *this run* sees them**; a check that `system-status`'s `go_compile` row and the promote gate's commit-lint gate agree *across* runs (same recorded sha, same child invocation) would pin the stronger claim.
- The `promote-gates` row could also surface **how stale** the promote decision is (e.g. minutes since the newest mailbox entry), so a dashboard read shows not just "not promotable" but "not promotable since …".
### Placement (deliberate — same as `[0.4.29]`–`[0.4.40]` which sit immediately above this one)
Parked at the bottom of an otherwise newest-first file on purpose. Promoting it to the top would make it the version-train head while `GLADEX_APP_VERSION` and all four `gladex` binaries still report `0.4.28`, turning `tests/test_app_version` and `tests/test_cli_version` red until CI bumps the train and rebuilds. That bump is main-loop work and is not part of this fix.
## [0.4.42] - 2026-09-25 — the divergence guard asserted "same child, same sha" while neither row recorded a sha, the queued next-candidate from [0.4.41]
### Fixed
- **The `promote-gates` divergence guard compared no commit at all.** Its whole thesis was a sentence nobody had measured: `SS_GCC_STATUS` was the only input the guard ever read from the other row, while its comment and its printed detail claimed the two rows ran "the same repo-lint on the same sha". Both children resolve HEAD **themselves** — the `go-compile` row with `repo-lint --sha HEAD`, the promote gate with its own `_head_sha()` — and one dashboard run lasts ~30 s, so a commit landing between them made that sentence false in the direction that matters: a **drift artefact** (two checks of two different commits) was diagnosed as a structural disagreement about one commit. The mirror failure existed too: a contradiction with no readable sha on either side still claimed "same sha".
- **Evidence, not assertion**: the `go-compile` row now exports the commit it measured (`repo-lint`'s `sha_resolved`; `""` when the child recorded none) as `sha` on its row JSON and hands it to the guard as `SS_GCC_SHA`. The guard normalises both that and the report's `sha` through `measured()` — empty or the symbolic `HEAD` is **not** a commit and reads as "unknown", so two blanks can never compare equal (a match on two unread values would be the same unmeasured claim in new clothes). Only then does it decide, and the detail states which case it found: **same commit** → `both measured commit <sha>, so the two children differ (binary or env)` — a real divergence; **different commits** → `the two checks measured DIFFERENT commits (go-compile <a>, commit-lint <b>): HEAD moved between them, drift not disagreement`; **unread** → `the commit each check measured is unknown (go-compile ?, commit-lint ?)`. All three remain `warning` (the guard still never fails the tool or counts an error), all three name both commits they had, and the guard still fires **only** on a genuine contradiction — two green checks on two commits invent no divergence. `same child` is dropped from the wording on purpose: neither row records the child *binary*, so it is the one part still unmeasured and the one part no longer claimed.
- **`--help`**: the `cannot verify` bullet now documents the evidence contract (`the commit each check recorded is compared first: same commit = the two children differ, different commits = HEAD moved between them, unknown = cannot compare`).
### Notes
- **Suite `tests/test_system_status_promote_gates.sh` grew 84 → 120 assertions, 6 → 12 mutations**: section 14b drives the four verdicts the guard can now produce (same-commit divergence; drift in *both* contradiction directions; an unread `sha_resolved`; a report without a sha) plus the no-false-alarm case (drift without contradiction → ordinary `promote-ready`), each pinned by what it must **not** say. Both children became scenario knobs — `STUB_RL_SHA` (`""` → `sha_resolved: null`) and `STUB_PG_SHA` (`NULL` → no sha) — so "real" and "mutant" always run the *same* scenario.
- **Pre-fix replay**: `SYSTEM_STATUS_BIN=<blob of HEAD:tools/system-status, md5 cca0ab15713e91db875def74a46e8121> bash tests/test_system_status_promote_gates.sh` → **99 passed / 21 failed / 0 skipped** — the old guard's `same child, same sha` where the suite now demands a measured commit, no drift/unknown diagnosis anywhere, no evidence contract in `--help`, and none of the six new plants matching a line (0 hits each). After the fix → **120/120**.
- **Mutations M7–M12** (planted on copies — the real tool is never edited — precondition-asserted by grepping the search string for exactly one occurrence, `bash -n`-validated, each run against the one scenario it targets): M7 the drift branch never taken (drift re-reported as a same-commit divergence) · M8 the unknown-commit branch dropped (two blanks read as a match) · M9 `SS_GCC_SHA=""` at the call site (the guard goes blind by construction) · M10 `"sha": ""` in the row (same blindness, produced one step earlier) · M11 the reverse contradiction missed — **false green**: a commit that does not compile reports `promote-ready` · M12 `if contradiction and g_sha == r_sha` (drift skips the guard and the row silently falls back to its ordinary verdict). **All 6 caught.** Two of them (M9, M10) fail on the *same* assertion by design: the hand-off and the row are one chain, and either link breaking must show up as "the commit was never recorded".
- **Full regression: 34 suites, 1960 assertions, 0 failed** (19 shell = 1479: budget 49, dns 40, domain 140, identity 28, ip-drift-check 133, ip-drift-cron 93, pdns 175, promote 67, promote-json 84, promote-lint 60, repo-lint 120, smtp 38, source-sync 30, go-compile 82, go-tests 66, mx_soa 31, **promote-gates 120**, unread 24, tls 99 + 15 PHP = 481) — **+36 over the 1,924 baseline**, no other suite moved. `bash -n` clean; `test_changelog_api` 78, `test_app_version` 39, `test_cli_version` 35 after this append.
- **Live**: `system-status --format human` → `go-compile [OK] … (commit e83aa62)` next to `promote-gates [WARN] not promotable: … commit-lint OK (commit e83aa62)` — the two rows naming the **same** commit, which is exactly the pair the guard compares — under **ALL SYSTEMS HEALTHY**, exit 0, 30.5 s.
- Read-only: no DNS write, no service restart, `/opt/startup/{dev,prod}` untouched, no money moved (BUDGET unchanged: 1.50 spent / 3.50 remaining), model spend **0.00** (`*-free` only).
### Queue (next-candidate, not actioned)
- **Actioned the same day, both of them, in `[0.4.43]` below** — the stale-decision
date and the guard's "which child" gap are no longer queued here; `[0.4.43]`
carries the queue that replaced them.
### Placement (deliberate — same as `[0.4.29]`–`[0.4.41]` which sit immediately above this one)
Parked at the bottom of an otherwise newest-first file on purpose. Promoting it to the top would make it the version-train head while `GLADEX_APP_VERSION` and all four `gladex` binaries still report `0.4.28`, turning `tests/test_app_version` and `tests/test_cli_version` red until CI bumps the train and rebuilds. That bump is main-loop work and is not part of this fix.
## [0.4.43] - 2026-09-25 — the dashboard said "not promotable" without saying for how long, and blamed "the two children" without naming either — both queued verbatim in [0.4.42]
### Fixed
- **The `promote-gates` row could not say how stale its own refusal was.** `not promotable: verdict REFUSED: …` reads identically whether the reviewer mailbox changed four seconds or four days ago, so a dashboard reader cannot tell "we are waiting on a human" from "this decision has been dead since Tuesday". Nothing on the promote side recorded the age either: the verdict gate's whole evidence is a file in `mailboxes/reviewer-to-main`, and *when* that file landed was never measured. The promote report now carries **`mailbox`** — `{"dir": "mailboxes/reviewer-to-main", "newest": <UTC ISO, seconds>, "age_seconds": <int|null>}`, computed by `_mailbox_state()` in `promote()` **next to `sha`, before any gate runs**, so an *early* refusal (the case where a reader most needs the age) still carries it. Every file in the mailbox counts, not only `VERDICT-*`: an empty mailbox is itself a state worth dating, and `null` is never a guessed age.
- **The row dates its refusal with it, honestly, in four states**: `not promotable (newest reviewer-mailbox entry 2026-09-21T18:29:21Z, 4771m old): …` · `not promotable (reviewer-mailbox holds no entries): …` · `not promotable (reviewer-mailbox age not recorded by this child): …` (the child recorded no `mailbox` at all — an older promote binary, or a report written by hand) · `not promotable (reviewer-mailbox entry <ts>, age not recorded): …`. The header is a **prefix, not a suffix**, deliberately: the detail is re-extracted from JSON through `clean()`, which truncates at 400 chars, so anything appended after a long verdict message is exactly what gets cut. Minutes (`int(round(age / 60.0))`) is the unit because the row is read at a glance; an age nobody measured is never printed in any state. The header appears **only on a refusal** — a `promote-ready` row gains no mailbox decoration.
- **The divergence guard blamed "the two children" while neither row named one.** `[0.4.42]` had already made the guard *measure* the commit, but its same-commit branch still ended `both measured commit <sha> - the two children differ (binary or env)`: a guess between two hypotheses, offered because **neither row recorded the path of the `repo-lint` it ran**. The unmeasured claim was the sentence's entire content, and the mirror was worse — a branch where `same child` had never been compared. The promote report now records **`gates[].child`** — the binary the gate *executed*, written by `_record_gate(..., child=…)` **only after `subprocess.run` returns** (`commit-lint` → `repo-lint`, `dev-sync` → `source-sync-check`; a timeout or a missing helper records nothing, because a child that never came back did not identify itself; the verdict gate runs no child and records none) — and the `go-compile` row exports its own as `bin`, handed over as `SS_GCC_BIN`. The guard then states only what it can show: **same path** → `both ran <path> - the same child, so the difference is in the environment`; **different paths** → `they ran DIFFERENT children (go-compile …, commit-lint …)`; **one or both unrecorded** → `the child each check ran is not BOTH recorded (go-compile …, commit-lint ?)` with a literal `?` where the path is unknown. `binary or env` is gone on purpose: with both paths in hand, "or env" is no longer a guess, and when only one is in hand the honest answer is the question mark, not a claim of sameness.
### Notes
- **Both suites grew, written assertions-first against the `HEAD:` blobs.** `tests/test_promote_json.sh` **84 → 108 assertions, 6 → 8 mutations** (section L pins `mailbox`'s `dir`/`newest`/`age_seconds` and the `child` field's presence-and-absence rules); `tests/test_system_status_promote_gates.sh` **120 → 155 assertions, 12 → 18 mutations** (two new scenario knobs — `STUB_PG_CHILD` and `STUB_PG_MB` — so "real" and "mutant" always run the *same* mailbox/child scenario; section **14c** drives all four child-identity outcomes, section **18b** all three mailbox states plus the no-header-on-a-pass negative). Both stubs keep the suites hermetic: no suite run ever executes the real promote tool, reads a live message DB, or starts a service.
- **Pre-fix replay (this step, final suites)**: `PROMOTE_TOOL=/tmp/opencode/pre-fix/promote-dev-to-prod bash tests/test_promote_json.sh` → **91 passed / 16 failed**; `SYSTEM_STATUS_BIN=/tmp/opencode/pre-fix/system-status bash tests/test_system_status_promote_gates.sh` → **130 passed / 25 failed / 0 skipped** — both against blobs byte-identical to `HEAD` (md5 `ef86ad55534293064f6eac219ab48a61` and `1e4b5d04ed472d029c2abd1902baff3b`): no `mailbox` field is read, no `gates[].child` is written, the same-commit branch still says `binary or env`, the old undated `not promotable:` header is all the row can print, `--help` documents neither evidence, and none of M7/M8 or M13–M18 can be planted (0 matching lines each).
- **Mutations M7/M8 (promote) and M13–M18 (dashboard)**, planted on copies — the real tools are never edited — precondition-asserted by grepping the search string for exactly one occurrence, `bash -n`-validated, each run against the one scenario it targets: M7 the gate's `child` never recorded · M8 the `mailbox` state never recorded · M13 the refusal loses its date (header reverted to a bare `not promotable:`) · M14 two *differing* binaries claimed as one child · M15 the `go-compile` row's binary never handed off (`SS_GCC_BIN=""`) · M16 the report's `commit-lint` child never read (`r_bin = ""`) · M17 an unrecorded child reported as `the same child (unmeasured)` · M18 the age rendered in **seconds** instead of the queued minutes. **All 8 caught.** M15/M16 deliberately fail on the *same* assertion: row→hand-off and report→guard are one chain, and either link breaking must read as "the child was never recorded".
- **Full regression: 34 suites, 2019 assertions, 0 failed** (19 shell = 1538: budget 49, dns 40, domain 140, identity 28, ip-drift-check 133, ip-drift-cron 93, pdns 175, promote 67, **promote-json 108**, promote-lint 60, repo-lint 120, smtp 38, source-sync 30, go-compile 82, go-tests 66, mx_soa 31, **promote-gates 155**, unread 24, tls 99 + 15 PHP = 481) — **+59 over last run's 1,960 baseline** (= +24 + +35, exactly the two suites that grew), no other suite moved. `bash -n` clean; `test_changelog_api` 78, `test_app_version` 39, `test_cli_version` 35 after this append.
- **Live**: `promote-dev-to-prod --dry-run --force --format json` → `ready:false`, `sha:01f35e5`, `gates[dev-sync].child=/data/repo/tools/source-sync-check`, `gates[commit-lint].child=/data/repo/tools/repo-lint`, verdict with no `child`, `mailbox.newest=2026-09-21T18:29:21Z`, `mailbox.age_seconds` in the 10⁵ range; `system-status` → `promote-gates [WARN] not promotable (newest reviewer-mailbox entry 2026-09-21T18:29:21Z, 4771m old): verdict REFUSED: no verdict in /data/repo/mailboxes/reviewer-to-main … | dev-sync OK | commit-lint OK (commit 01f35e5)` under **ALL SYSTEMS HEALTHY**, exit 0. Promotions remain refused in practice (exit 5, `reviewer-to-main/` holds no `VERDICT-*`) and nothing was promoted: every invocation is `--dry-run`.
- Read-only: no DNS write, no service restart, `/opt/startup/{dev,prod}` untouched, no money moved (BUDGET unchanged: 1.50 spent / 3.50 remaining), model spend **0.00** (`*-free` only).
### Queue (next-candidate, not actioned)
- **The age is recomputed at read time, not recorded at refusal time.** `mailbox.age_seconds` is measured when the *report* is generated, so `not promotable (…, 4771m old)` re-ages on every dashboard run even though nothing changed — and a cached or replayed report ages silently. The honest fix is to date the refusal from the report's own `timestamp` (which the tool already writes) or from the newest entry's *mtime vs. the run*, so "old" means "old when this report was made".
- **`4771m old` is arithmetic a reader must do.** Above a day the minutes figure stops being glanceable (`2d 23h` / `31h` would be); the unit choice is pinned by a mutation (M18) today, so changing it means changing the contract, which is why it is queued rather than slipped in.
- **Only one of the three gates' children is ever compared.** `dev-sync`'s `source-sync-check` path is now recorded too but nothing reads it — a divergence guard between the promote gate's dev-sync verdict and `source-sync-check`'s own output would extend the same "one check seen twice" principle to the second pair.
### Placement (deliberate — same as `[0.4.29]`–`[0.4.42]` which sit immediately above this one)
Parked at the bottom of an otherwise newest-first file on purpose. Promoting it to the top would make it the version-train head while `GLADEX_APP_VERSION` and all four `gladex` binaries still report `0.4.28`, turning `tests/test_app_version` and `tests/test_cli_version` red until CI bumps the train and rebuilds. That bump is main-loop work and is not part of this fix.
---
test_app_version.php
test_budget_show.sh
test_changelog_api.php
test_chat_a11y.php
test_chat_frontend.php
test_cli_version.php
test_dns_verify.sh
test_domain_availability.sh
test_identity_wrapper.sh
test_ip_drift_check.sh
test_ip_drift_cron.sh
test_ip_drift_safety.php
test_lan_middleware.php
test_mailbox_a11y.php
test_page_lang.php
test_pdns_api.sh
test_php_busy_timeout.php
test_promote_gate.sh
test_promote_json.sh
test_promote_lint_gate.sh
test_repo_lint.sh
test_smtp_relay_probe.sh
test_source_sync_check.sh
test_stats_dbpath.php
test_stats_gocli_path.php
test_system_status_go_compile.sh
test_system_status_go_tests.sh
test_system_status_mx_soa.sh
test_system_status_promote_gates.sh
test_system_status_unread.sh
test_templates_gallery.php
test_tls_check.sh
test_ts_ordering.php
test_webmail_session_routing.php
[0m→ [0mRead repo/tools/system-status [90m[limit=30, offset=770][0m
> build · mimo-v2.6-flash-free
Now I have the full picture. Let me check for every doc that repeats the "every file counts" claim:
[0m✱ [0mGrep "Every file counts|every FILE counts|not only VERDICT|Every file in the mailbox" [90min repo · 3 matches[0m
[0m→ [0mRead repo/tools/REGISTRY.md [90m[limit=55, offset=300][0m
> build · mimo-v2.6-flash-free
[0m→ [0mRead repo/tools/REGISTRY.md [90m[limit=45, offset=410][0m
> build · mimo-v2.6-flash-free
Select another run log from the list above. Only files matching run-YYYYMMDD-HHMMSS-N.log are readable.
[Fri Sep 25 04:32:23 2026] 127.0.0.1:51804 Accepted [Fri Sep 25 04:32:23 2026] 127.0.0.1:51804 Closing [Fri Sep 25 04:32:23 2026] 127.0.0.1:51816 Accepted [Fri Sep 25 04:32:23 2026] 127.0.0.1:51816 Closing [Fri Sep 25 04:32:23 2026] 127.0.0.1:51828 Accepted [Fri Sep 25 04:32:23 2026] 127.0.0.1:51828 Closing [Fri Sep 25 04:43:38 2026] 127.0.0.1:39662 Accepted [Fri Sep 25 04:43:38 2026] 127.0.0.1:39662 Closing [Fri Sep 25 04:43:38 2026] 127.0.0.1:39678 Accepted [Fri Sep 25 04:43:38 2026] 127.0.0.1:39678 Closing [Fri Sep 25 04:43:38 2026] 127.0.0.1:39684 Accepted [Fri Sep 25 04:43:38 2026] 127.0.0.1:39684 Closing [Fri Sep 25 04:43:38 2026] 127.0.0.1:39696 Accepted [Fri Sep 25 04:43:38 2026] 127.0.0.1:39696 Closing [Fri Sep 25 04:43:38 2026] 127.0.0.1:39702 Accepted [Fri Sep 25 04:43:38 2026] 127.0.0.1:39702 Closing [Fri Sep 25 04:43:39 2026] 127.0.0.1:39718 Accepted [Fri Sep 25 04:43:39 2026] 127.0.0.1:39718 Closing [Fri Sep 25 04:43:39 2026] 127.0.0.1:39728 Accepted [Fri Sep 25 04:43:39 2026] 127.0.0.1:39728 Closing [Fri Sep 25 04:43:39 2026] 127.0.0.1:39736 Accepted [Fri Sep 25 04:43:39 2026] 127.0.0.1:39736 Closing [Fri Sep 25 04:43:39 2026] 127.0.0.1:39752 Accepted [Fri Sep 25 04:43:39 2026] 127.0.0.1:39752 Closing [Fri Sep 25 04:43:39 2026] 127.0.0.1:39768 Accepted [Fri Sep 25 04:43:39 2026] 127.0.0.1:39768 Closing [Fri Sep 25 04:43:39 2026] 127.0.0.1:39780 Accepted [Fri Sep 25 04:43:39 2026] 127.0.0.1:39780 Closing [Fri Sep 25 04:43:39 2026] 127.0.0.1:39786 Accepted [Fri Sep 25 04:43:39 2026] 127.0.0.1:39786 Closing [Fri Sep 25 04:43:40 2026] 127.0.0.1:39796 Accepted [Fri Sep 25 04:43:40 2026] 127.0.0.1:39796 Closing [Fri Sep 25 04:43:40 2026] 127.0.0.1:39802 Accepted [Fri Sep 25 04:43:40 2026] 127.0.0.1:39802 Closing [Fri Sep 25 04:43:40 2026] 127.0.0.1:39816 Accepted [Fri Sep 25 04:43:40 2026] 127.0.0.1:39816 Closing [Fri Sep 25 04:43:40 2026] 127.0.0.1:39818 Accepted [Fri Sep 25 04:43:40 2026] 127.0.0.1:39818 Closing [Fri Sep 25 04:43:40 2026] 127.0.0.1:39824 Accepted [Fri Sep 25 04:43:40 2026] 127.0.0.1:39824 Closing [Fri Sep 25 04:43:40 2026] 127.0.0.1:39836 Accepted [Fri Sep 25 04:43:40 2026] 127.0.0.1:39836 Closing [Fri Sep 25 04:43:41 2026] 127.0.0.1:39852 Accepted [Fri Sep 25 04:43:41 2026] 127.0.0.1:39852 Closing [Fri Sep 25 04:43:41 2026] 127.0.0.1:39866 Accepted [Fri Sep 25 04:43:41 2026] 127.0.0.1:39866 Closing [Fri Sep 25 04:49:34 2026] 127.0.0.1:60332 Accepted [Fri Sep 25 04:49:34 2026] 127.0.0.1:60332 Closing [Fri Sep 25 04:49:35 2026] 127.0.0.1:35230 Accepted [Fri Sep 25 04:49:35 2026] 127.0.0.1:35230 Closing [Fri Sep 25 04:55:53 2026] 127.0.0.1:51770 Accepted [Fri Sep 25 04:55:53 2026] 127.0.0.1:51770 Closing [Fri Sep 25 05:02:26 2026] 127.0.0.1:33794 Accepted [Fri Sep 25 05:02:26 2026] 127.0.0.1:33794 Closing [Fri Sep 25 05:07:18 2026] 127.0.0.1:37504 Accepted [Fri Sep 25 05:07:18 2026] 127.0.0.1:37504 Closing [Fri Sep 25 05:09:45 2026] 127.0.0.1:60822 Accepted [Fri Sep 25 05:09:45 2026] 127.0.0.1:60822 Closing [Fri Sep 25 05:09:45 2026] 127.0.0.1:60834 Accepted
Generated 2026-09-25 03:09:45 UTC · Gladex.de