Files
mopac-pmo/archive/REPORT-20260829-1530-beta-finish.md

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 park event in loop.jsonl pins the dedup marker (no hot-loop), heartbeat status.json gains last_writeback_error + a parked counter (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/parked surfaced in status.json.

522 — native SMS notifier

internal/notify (rewritten from the WIP sketch) + [notify] config

  • Stdlib-only send (net/smtp via NewClient + 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. once never 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 records redmine / 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 via loop.DailyHook from the CLI to avoid an import cycle (brief already reads loop state). Idempotent per calendar day via the existing brief.jsonl marker; harness brief unchanged 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 check green before each commit (build + go vet + go test ./... — 12 packages ok, no failures; final run at commit f9fb729).
  • 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.txt beside 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 status clean; origin/main at f9fb729.
  • New config surfaces documented in harness.toml.example ([redmine] writeback knobs, [notify], [brief]) and README tables/usage.