8.1 KiB
REPORT — Beta finish: 525 / 522 / 495 (2026-08-29 15:30 CST)
Repo: ~/projects/meta/MOPAC/harness (main). All work in the digest-pinned
Docker builder via ./dev.sh. ./dev.sh check (build + vet + test ./...)
ran GREEN immediately before EVERY commit below. Branch pushed:
19d61a0..f9fb729 on origin/main. Working tree clean at handoff.
Note: no Redmine tickets were written from this session (PMO owns tickets), per instructions.
Commit map
| Commit | Part | Subject |
|---|---|---|
e8f50d3 |
env chore | deploy: add host docker group to the loop container (uncommitted WIP found in tree; prerequisite for the Docker builders) |
3927b38 |
525 | loop: retry Redmine writebacks with backoff, verify, and park on failure |
5784698 |
522 | notify: native SMS notifier for loop events via stdlib SMTP |
f9fb729 |
495 | brief: wire the 0630 pipeline - journal note, sections, daemon schedule |
Net: 24 files changed, +2732 / -112.
525 — retry/backoff on status/note writes
Compile fix (first): the loop turn's WIP did not build —
retry.go:74 field/method collision (Backoff), redmine.go:186-210
Attempts used as a value, and the [redmine] writeback_* config fields
did not exist yet. All fixed (method renamed BackoffFor; config knobs
added to RedmineConfig + TOML parsing).
Finished behavior
- Bounded retries with exponential backoff + ±25% jitter on the note and
status writes:
[redmine] writeback_retries = 3,writeback_backoff_secs = 2,writeback_max_backoff_secs = 30. Retryable: 5xx, 429, transport (dial/reset/EOF). Never retried: 4xx, caller context cancel. Retried PUTs replay an identical body; every retry logs one audit line to the loop stdout. - Verification re-read after the writes: one GET confirms the status
transition actually landed and advances the dedup marker in the same
round trip (
IssueState; supersedes the old refresh-only read). - Park on final failure: when the outage outlives the bound the task
PARKS — issue status left exactly as-is (New stays New), one
parkevent in loop.jsonl pins the dedup marker (no hot-loop), heartbeatstatus.jsongainslast_writeback_error+ aparkedcounter (cleared by the next verified writeback), and the next PMO update of the issue re-releases the task. A paid-for REPORT is never silently stranded.
Tests (all green, fake-server based)
internal/writeback— 14 tests total incl. new: 502-then-success (replayed body + audit line asserted), bounded give-up after exactly N attempts, 401 one-shot, verify re-read shape, RetryPolicyFrom defaults/overrides, BackoffFor jitter band, Retryable classification.internal/loop— 502-flap recovery at the daemon level, park path (no note, no status move, no hot-loop, PMO update re-dispatches),last_writeback_error/parkedsurfaced in status.json.
522 — native SMS notifier
internal/notify (rewritten from the WIP sketch) + [notify] config
- Stdlib-only send (
net/smtpviaNewClient+DialTimeout, deadline- bounded so a black-holed relay cannot stall the sequential loop; optional PLAIN AUTH from refs). Recipients = carrier email-to-SMS gateway addresses, resolved from one ref. - Events wired in the daemon: turn complete, turn failed (incl. parked writebacks), quota/resource deferral, scan error, daemon start/stop.
- Per-kind rate limit with collapsed repeats: repeats inside the window are counted and ride along on the next send ("+N suppressed repeat(s)"); no phone spam during a flapping scan.
- Offline queue on disk (one JSON file per event, FIFO by name, bounded with oldest-dropped, corrupt-entry tolerant) drained after every scan and at daemon stop.
- Redaction rules applied BEFORE queueing (so secrets are never at
rest either): credential-shaped assignments (
token=,password:, bearer/api-key/secret/auth) and key refs (env:/file:/literal:/mpk:/bw:values). - Disabled
[notify]yields a nil notifier; all methods are no-ops, so call sites never gate.oncenever notifies (daemon-only surface).
Tests — 7 in internal/notify against a local fake SMTP server
(real SMTP dialogue: EHLO/AUTH/MAIL/RCPT/DATA/QUIT): send + headers +
recipients, rate-limit collapse + follow-up count, offline queue drain,
bounded queue oldest-drop, redaction (no secret bytes on the wire), nil
no-op, AUTH path. Plus a loop-level wiring test (turn_complete +
daemon_start fire and queue while SMTP is dead).
495 — 0630 briefing pipeline (surface existed at 064aed0; now wired)
- Render grows the 0630 sections: one-line executive summary (shipped/moved/incoming/open · review chain · decisions needed · quota pulse), per-vertical open-now backlog table (window- independent, capped at 15 rows) alongside the existing shipped/moved/ incoming window tables, decisions-needed (open issues in Feedback/ Needs-Decision statuses), review-chain line (open PR count + oldest age), quota line from quota state (unchanged section + exec pulse), and system health (loop heartbeat status.json: running/scans/ dispatched/reports/parked/last_scan_error + host load/mem/disk).
- Delivery: journal note on
[brief] redmine_issue(the SoR copy, e.g. 495 — benefits from 525's retry on the note write) + Discourse topic when creds are present; missing creds = graceful logged skip (never a failed briefing when the SoR note landed). Legacy discourse-with-fallback path preserved when no redmine_issue is set. Marker recordsredmine/discourse/redmine+discourse. - Daemon schedule hook: the loop fires the brief daily at
[brief] schedule(default 06:00 America/Chicago — 06:00 render for the 0630 read; configurable HH:MM + timezone; "" = off; catch-up on late start). Injected vialoop.DailyHookfrom the CLI to avoid an import cycle (brief already reads loop state). Idempotent per calendar day via the existing brief.jsonl marker;harness briefunchanged as the one-shot/cron surface.
Tests — 16 in internal/brief (9 new): render section coverage
(exec line / open-now / decisions / review chain / quota / health),
classify open-backlog + decisions, health-file reading, Redmine-primary
delivery (note content + graceful discourse skip), redmine+discourse
both-land marker, redmine-fails-discourse-saves, full render against the
fake Redmine; plus loop-side daily-hook tests (timezone math table +
fires-exactly-once-per-day under a live daemon loop).
Smoke evidence
./dev.sh checkgreen before each commit (build + go vet + go test ./... — 12 packages ok, no failures; final run at commitf9fb729).- Live read-only smoke of the 495 render (dry-run, no writes to
Redmine, no marker): authenticated GET of the real scope at
projects.knownelement.com (fixed_version_id=35, status New — genuinely
empty; the live loop heartbeat's "29 scans / 0 dispatched" corroborates),
live system-health section reading this host's running daemon
status.json. Transcript:
SMOKE-20260829-brief-dryrun.txtbeside this report. - 525/522 behaviors are exercised in-test against fake Redmine/SMTP servers (see test lists above); no live 502-injection or SMS send was performed against production systems.
Deferred / open
- Live Discourse send (495): vendored client is wired and fake-tested,
but no live topic has been created — awaiting the 1900 CST creds
session Charles offered (discourse key ref + category). Config slot is
ready (
[brief] discourse_url/discourse_key_ref/category_slug). - Live SMS send (522): fake-SMTP-tested; a real relay + carrier
gateway addresses are needed to enable
[notify]in production. - Quota pulse depth (495): quota line currently reads "gate off"
until
[quota]is enabled per instance (existing 490 surface, opt-in). - gofmt drift exists across the repo (pre-existing, incl. at 19d61a0); the check gate is build+vet+test, unchanged.
Handoff
git statusclean;origin/mainatf9fb729.- New config surfaces documented in
harness.toml.example([redmine] writeback knobs, [notify], [brief]) and README tables/usage.