The bug class (caught + fixed 2026-05-04)
Symptom: Operator clicks Dismiss on an approval card. Button changes to "Retry" and stays stuck. Clicking again reproduces. Card never goes away.
Root cause: queue items written to pending_*.jsonl without an id field. build_pwa.py rendered them as <button class="approval-dismiss" data-id="" data-channel="">. POST /api/dismiss returned 400 "missing id or channel". UI fell back to "Retry" — but every retry re-fails identically.
Affected: 24+ cards across pending_drafts (14/29 missing id), pending_smartlead_actions (24/24 missing id), pending_hs_updates (39/62 missing id) at the time of discovery.
The fix architecture
build_pwa.py now has _synthesize_card_id(item, channel_id) which produces a stable 12-char content hash for any item missing an id. IDs follow pattern <channel>-auto-<12hex>. Same content → same ID across rebuilds, so a dismiss recorded yesterday still hides the card today.
render_approval_item(item, channel_id="") accepts channel from the loop (the queue file determines channel — items don't always carry it). NEVER renders with empty data-id or data-channel.
Four layers of permanence
Layer 1 — Build-time assertion (build_pwa.py)
Before writing index.html, the build script greps the HTML for <button class="approval-dismiss" data-id="">. If ANY are found, raises RuntimeError and aborts the build. The PWA never ships with the regression.
Layer 2 — Gate-proof anti-regression (gate_proof_runner.sh PROOF 14)
4 gates verify the fix is intact every time the gate-proof fires:
1. _synthesize_card_id helper present in build_pwa.py
2. render_approval_item signature accepts channel_id parameter
3. Rendered pwa/index.html has zero cards with empty data-id
4. Rendered pwa/index.html has zero cards referencing closed-lost lead names (Tim Beegle, Sara Schulze, Fred Surganty)
Layer 3 — Nightly stale-drain (pwa_stale_drain.py)
Wired into nightly-consolidation Section G0 (runs BEFORE the dismiss-queue reconciliation). Drains:
- Items referencing closed-lost lead names (full-name word-boundary match — no false positives)
commitment-trackerentries older than 2 days (likely fulfilled — freshness rule's backstop)do_not_send=true+confidence=lowitems older than 7 days- Items whose ID matches a CF KV
dismissed/key
Archives to dismissed_archive.jsonl with _archived_reason metadata. Idempotent.
Layer 4 — This memory file
Future agent sessions reading this memory know: 1. The bug class exists and is documented 2. The fix is layered and any single layer compromise is detected by the others 3. Modifyingrender_approval_item or _synthesize_card_id requires updating PROOF 14 gates accordingly
4. Adding a new queue type requires ensuring write-time IDs OR reliance on the synthesis fallback
What NOT to do (regression vectors to avoid)
- Do NOT remove the
channel_idparameter fromrender_approval_item— items don't reliably carry channel; the loop must pass it - Do NOT accept
item.get("id", "")as the rendered id — the empty-string fallback is what causes the regression. Always synthesize if missing. - Do NOT delete the build-time assertion — it's the last line of defense against a stale-bug shipping
- Do NOT loosen the closed-lost matcher to substring (was tried 2026-05-04 — false-positived 'tim' in 'estimate'). Keep it word-boundary on full names.
- Do NOT allow
pwa_stale_drain.pyto run without backups — it doesshutil.copy2per-queue before pruning. Backups land indata/*.bak.stale-drain-<ts>.
Joseph's verbatim feedback (the trigger)
> "PWA is a mess and I'm still not sure what dismiss does when I hit it and most still say retry when I hit the dismiss button. Fully fix the PWA today"
> "Make sure all fixes are permanent going forward and regression in anyway is prevented."
Files that need to stay in sync
~/Library/Application Support/SkyRun/build_pwa.py—_synthesize_card_id+render_approval_item(item, channel_id)+ build assertion~/Library/Application Support/SkyRun/gate_proof_runner.sh— PROOF 14 gates~/Library/Application Support/SkyRun/pwa_stale_drain.py— drain rules~/.claude/scheduled-tasks/nightly-consolidation/SKILL.md— Section G0 wiring~/Desktop/adam-bd-bootstrap/package-snapshots/— franchise template inheritance (runpackage-sync.shafter any change)
If any of these drifts (e.g., a refactor of build_pwa.py removes the synthesizer), PROOF 14 gates fail next gate-proof run → fix_queue gets a manual_fix entry → operator surfaces the regression before customers notice.