brydup@mac ~/cowork % cat 2026-05-19-tech-brief.md

# 2026-05-19 — Tech Brief

## Session arc

Morning status check on overnight cron jobs surfaced a chronic cowork-backup.sh failure (3 of last 4 nights, exit 128, fatal: not a git repository). Confirmed via both #cron-alerts (Slack) and "Pi Cron Alerts" (Discord) — discovered Discord as a parallel alerting surface, saved as memory.

Diagnosed Pi-side ~/cowork/.git missing. Verified the GitHub remote BryanDuplantis/cowork existed (private) but contained 12 subdirs from a prior unscoped git add -A. Ran a content-secrets sweep on the 5 700-mode subdirs (01_too_important through 05_lean_mean) — clean. Chose Path C: re-init local .git from origin via git init -b main + git fetch origin + git reset --mixed FETCH_HEAD; tightened wrapper to explicit pathspec allowlist; one-time git rm --cached -r cleanup commit to untrack the 12 subdirs from main's tip.

During the diff review caught a 32-line regression in gotchas.md — Pi's working tree had the 5/14 8,664-byte version, canonical had the 5/16 14,968-byte version with four hard-won entries Bryan had added that morning. Restored via git checkout HEAD -- gotchas.md. Pushed cleanup + recovery as 22f4168 and 8c6c029.

Pivoted to root-cause: WHY does .git keep getting deleted and gotchas.md keep rolling back? Inventoried Pi-side automation (no syncthing, no rsync daemon, no systemd timer touching cowork, no relevant cron). Found the git lineage's 2353910 initial import 2026-05-16: cowork tree at v1 was a separate root commit — implicates a second machine. journalctl -u sshd window 5/17 03:00–5/18 03:00 EDT showed 8 short SSH sessions from 10.0.0.240 between 16:22 and 17:20 EDT on 5/17. Traced the IP to Mac LAN. Inspected Mac's ~/Library/LaunchAgents/: com.bryanduplantis.cowork-sync.plist (WatchPaths=~/cowork + StartCalendarInterval 06:00 daily, runs=40, last exit code=0). Read ~/bin/sync-cowork-to-pi.sh: rsync -az --delete ~/cowork/ pi:~/cowork/ with no --exclude='.git' and a Mac side that has no .git. Three-way gotchas.md size check confirmed the asymmetry: Mac local 8,664 ≠ canonical 14,968 ≠ Pi local 14,968 (post-restore).

Disabled the LaunchAgent (launchctl bootout gui/$UID ...) + renamed plist to .disabled for reboot persistence. Inventoried Mac's ~/cowork/ vs canonical via git hash-object SHA compare: 11 of 13 canonical files byte-identical, gotchas.md stale (8,664→14,968 needed), .gitignore missing. Ran Path C on Mac: HTTPS remote (Mac has no GitHub SSH key, uses gh credential helper), git init + fetch + git reset --mixed origin/main + git checkout HEAD -- .gitignore gotchas.md. End state Mac/Pi/GitHub all at 8c6c029.

Drafted new gotchas.md entry documenting the rsync -a --delete failure mode, committed (4eb1166), pushed from Mac, git pull on Pi to propagate.

Ran /security_review on the broader Pi backup infrastructure (subagent dispatch). Result: 0 critical, 3 high, 5 medium, 4 low. Remediated H1, H2, H3, M1, M2, M3, M5, L1 in a single Pi+Mac pass. Skipped M4 (false positive — no actual double-log; cowork-backup.sh and brain-backup.sh don't internally write to $LOG, cron redirect is sole writer). Skipped L2/L3 (Mac rsync script cosmetics, disabled anyway). Verified L4 clean (no push.verbose, no credential helpers).

Captured to brain (2026-05-19-session-cowork-sync-diagnose-and-harden). Now drafting this tech brief.

## Shipped artifacts

**Pi-side** (brydup@brain-mcp.local:/home/brydup):

- ~/cowork-backup.sh — rewrote git add -Agit add -A -- MEMORY.md CLAUDE.md gotchas.md .gitignore '2026-*-tech-brief.md' '2026-*-tech-brief.html' '2026-*-shipping-log.md' '2026-*-inbox-triage-handoff.md'. Added umask 077, set -euo pipefail. Original at .bak-2026-05-19-pre-allowlist. Purpose: nightly 03:00 EDT auto-commit + push of curated cowork files; no longer sweeps subdirs.
- ~/brain-backup.sh — same allowlist treatment: git add -A -- '*.md' '.gitignore'. Added umask 077, set -euo pipefail. Original at .bak-2026-05-19-pre-allowlist.
- ~/cowork/.gitignore — new (didn't exist locally before). Denylists 00_Resources/ through 31_jukebox_hero/, *.pdf, *.{jpg,JPG,jpeg,png,PNG}, *.skill. Belt-and-suspenders to the wrapper allowlist.
- ~/brain/.gitignore — expanded from 13 B (inbox/, *.tmp) to 18 patterns including *.env*, *.key, *.pem, *.crt, *.token, secrets/, credentials*, .venv/, node_modules/.
- ~/cowork/.git — re-initialized from scratch (git init -b main + git fetch origin main + git reset --mixed FETCH_HEAD). Per-repo config: user.name, user.email, core.sshCommand="ssh -i $HOME/.ssh/id_ed25519_cowork -o IdentitiesOnly=yes".
- ~/bin/slack-alert.sh — full rewrite. M3 fix: python3 -c json.dumps(...) with shell-interpolated $CHANNEL and $TEXTjq -n --arg channel --arg text '{...}'. H3 fix: optional 4th positional --no-tail arg → includes log path but not log content. Tail cap reduced 15 lines/2500 chars → 5 lines/500 chars. Original at .bak-2026-05-19-pre-jq. chmod 700.
- ~/bin/morning-briefing.sh — added STATE_DIR="$HOME/.local/state/morning-briefing" + mkdir -p "$STATE_DIR" + --add-dir "$STATE_DIR" \ on the claude -p invocation (M2). Added umask 077. Wired --no-tail into both slack-alert.sh calls (timeout branch + non-zero-non-timeout branch). Original at .bak-2026-05-19-pre-statedir.
- ~/bin/inbox-triage.sh — added umask 077; wired --no-tail into slack-alert calls.
- ~/jukebox-sync.sh — added umask 077.
- ~/bin/wrapper-template.sh — chmod 600 (was 644).
- 9 Pi log files chmod 600 (were 644): cowork-backup.log, brain-backup.log, morning-briefing.log, inbox-triage.log, jukebox-sync.log, yt-weekly-synth.log, yt-subs/logs/{fetch-and-score,cron,cluster-daily}.log.
- ~/cowork/gotchas.md — restored from canonical (108 → 116 lines after appending new entry); now includes new 2026-05-19 entry.

**Mac-side** (bryanduplantis@<mac>:/Users/bryanduplantis):

- ~/cowork/.git — new. git init -b main, HTTPS remote https://github.com/BryanDuplantis/cowork.git, local user config. No core.sshCommand (Mac has no GitHub SSH key; relies on gh credential helper).
- ~/cowork/.gitignore — new (didn't exist locally before, even though canonical had one).
- ~/cowork/gotchas.md — refreshed from canonical via git checkout HEAD -- gotchas.md. Was Mac's stale 5/14 8,664-byte version; now 14,968 / 116 lines / 12 entries.
- ~/Library/LaunchAgents/com.bryanduplantis.cowork-sync.plist.disabled — renamed from .plist. Reboot-persistent disable; launchctl bootout gui/$UID ~/Library/LaunchAgents/com.bryanduplantis.cowork-sync.plist already unloaded the current session.
- ~/bin/sync-cowork-to-pi.sh — UNCHANGED on disk. The triggering plist is disabled. Kept around as forensic reference for the gotchas entry.
- ~/Library/Logs/sync-cowork-to-pi.log and .launchd.log — chmod 600 (were 644).

**GitHub commits pushed today:**

- BryanDuplantis/cowork@main: 0262b4f22f4168 stop tracking subdirs; tighten .gitignore (going-forward allowlist only)8c6c029 auto backup 2026-05-19T12:52:53Z (5/17 + 5/18 tech briefs) → 4eb1166 gotchas: rsync --delete mirror clobbers destination-side state (incl. .git)
- BryanDuplantis/brain@main: 847733884bb124 auto backup 2026-05-19T13:44:12Z (.gitignore expansion + first commit under new wrapper)

## Cron schedule changes

None. All 8 Pi crontab entries unchanged:
```
0 2 * * *  brain-backup.sh                            (unchanged schedule, new wrapper content)
0 3 * * *  cowork-backup.sh                           (unchanged schedule, new wrapper content)
0 8 * * *  bin/morning-briefing.sh                    (unchanged schedule, new wrapper content)
0 18 * * 0 bin/yt-weekly-synth.sh                     (unchanged)
0 * * * *  yt-subs/fetch-and-score.py                 (unchanged)
0 22 * * * yt-subs/cluster-daily.sh                   (unchanged)
0 7 * * *  bin/inbox-triage.sh                        (unchanged schedule, new wrapper content)
0 4 * * *  jukebox-sync.sh                            (unchanged schedule, new wrapper content)
```

Mac LaunchAgent removed from active scheduling:
- com.bryanduplantis.cowork-sync — was WatchPaths=~/cowork (60s throttle) + StartCalendarInterval Hour=6 Minute=0 daily. runs=40 since 5/13 plist mtime. Now bootout'd and renamed .disabled.

## Decisions

- **GitHub origin/main is the canonical source of truth for ~/cowork/.** Mac, Pi, and GitHub all participate bidirectionally via git pull/git push. Rsync mirror retired.
- **Mirror sync + downstream-editable filesystem = category error.** If both endpoints get edited, the source of truth has to be a shared remote. rsync --delete from src to a dst that has its own state will silently destroy that state.
- **Allowlist > denylist for git add in backup wrappers.** Explicit pathspec list scoped to -- <patterns> in cowork-backup.sh and brain-backup.sh. Defensive .gitignore is belt-and-suspenders, not primary defense.
- **12 cowork subdirs stay local-only on each machine, no longer mirrored.** FoWR briefings, IRS log, jukebox catalog, etc. Living with the divergence rather than designing complex bidirectional sync now.
- **LaunchAgent disabled via rename, not deleted.** mv plist plist.disabled is reversible; rm is not.
- **slack-alert.sh gets PII-aware.** --no-tail flag opt-in for Claude-session jobs (morning-briefing, inbox-triage) where log content may include email subjects, calendar events, Slack message fragments — sensitive even on internal #cron-alerts.
- **Default slack-alert.sh tail cap tightened** 15 lines/2500 chars → 5 lines/500 chars. Tradeoff: less debug context in alert, smaller PII surface even for "safe" tails.
- **Skipped M4 cron double-log fix** as false positive — cowork-backup.sh and brain-backup.sh don't internally >> "$LOG"; cron's >> log 2>&1 redirect is the sole writer. No interleaving.
- **Skipped L2/L3** (Mac rsync script tilde expansion + StrictHostKeyChecking) — the script is disabled, cosmetic only.

## Incidents

**Incident 1: cowork-backup.sh cron alarmed daily 5/18 → 5/19.** Symptom: exit 128, fatal: not a git repository. Pi's ~/cowork/.git directory missing. Trigger: Mac LaunchAgent com.bryanduplantis.cowork-sync fired 5/17 06:00 EDT (its StartCalendarInterval), ran rsync -az --delete against a Mac side with no .git → Pi's .git deleted. Discovered 5/19 morning. Resolved by re-initializing .git from origin + tightening wrapper + disabling Mac LaunchAgent. Mean time to detection: ~30 hours (one cron cycle of silent alarms before investigation).

**Incident 2: Silent rollback of gotchas.md** between 5/16 19:01 UTC and 5/19 morning. Pi's local file was the 5/14 8,664-byte version, mtime regressed to 2026-05-14 16:30; canonical and Pi's index had 14,968 bytes with the four 5/16 entries (claude -p schema deferral, sandbox writes, rc=0 masking, gdrive dupes). Same root cause as Incident 1 (rsync -a preserves source mtime, so the regression looks like the file was *always* the older version). If the Pi cron had succeeded silently instead of alarming, those entries would have been pushed to GitHub at 5/19 03:00 with the smaller content — irrecoverable except via local Pi backups. **The cron failure was protective.** Recovered via git checkout HEAD -- gotchas.md.

**Incident 3: §8 of ~/.claude/CLAUDE.md has a misleading "this one was caught" entry** for 2026-05-16. The actual sequence: 5/16 12:54 EDT Mac pushed an "initial import: v1" commit (separate root); 5/16 15:01 EDT Pi pushed 0262b4f auto backup (genuine cron-style push); 5/17 06:00 EDT Mac LaunchAgent deleted Pi .git for the first time. The "caught" entry conflates "alarm fired and was investigated" with "underlying mechanism identified and fixed permanently." The mechanism (Mac rsync --delete) wasn't identified until 5/19. Candidate amendment to §8 to clarify.

## Gotchas added

**New entry in ~/cowork/gotchas.md** (pushed as commit 4eb1166):

> **2026-05-19 — rsync -a --delete mirror clobbers destination-side state (including .git)**
> **Symptom:** dest .git keeps vanishing; working-tree files appear to "roll back" with *mtime preserved* so git status shows nothing time-anomalous — looks like the file was *always* the older version. Dest cron alarms with "fatal: not a git repository." Source machine looks fine. Lasts for days before anyone connects the dots.
> **Root cause:** unidirectional rsync -a --delete with no --exclude='.git'. Source has no .git--delete removes dest's .git. rsync -a preserves source mtime → overwrites dest's newer file with source's older content AND regresses mtime. Dormant when content-identical; surfaces the first time dest diverges from source.
> **Fix:** Two layers. (a) If unidirectional mirror is genuinely wanted: --exclude='.git' non-negotiable; treat --delete as "authorize destroying all dest-only state on every run". (b) Better: shared remote (GitHub, S3) + bidirectional git pull/push. Mirror sync + downstream-editable dest is a category error.
> **Detection helper:** if dest cron alternates "no changes" → "fatal: not a git repository" overnight, suspect a mirror from another host — grep ~/Library/LaunchAgents/, ~/bin/, crontab -l on every machine that talks to dest for rsync.*<dest-hostname>.
> **Where it bit:** Mac LaunchAgent com.bryanduplantis.cowork-sync (WatchPaths + 06:00 daily) calling ~/bin/sync-cowork-to-pi.sh (rsync -az --delete, no exclude). Dormant 5/13–5/16; surfaced 5/17 06:00 when Pi-side gotchas.md first diverged from Mac. Plist disabled launchctl bootout + rename to .plist.disabled. Architecture switched to bidirectional git via GitHub.

**Implicit gotchas discovered today** (not yet written into gotchas.md but worth noting):

- launchctl bootout is session-scoped only; plist auto-reloads on next login from ~/Library/LaunchAgents/. Rename to .disabled is the reboot-persistent fix.
- git reset --soft <commit> after git init leaves the index empty; files in HEAD show as both "staged deletion" AND "untracked." Use git reset --mixed (default). Hit twice today (Pi restore + Mac restore).
- Mac has no GitHub SSH key; git@github.com: URLs fail with Permission denied (publickey). Use HTTPS + gh credential helper.
- Subshell PATH on this Mac sometimes drops /usr/bin/ in Bash tool invocations; git falls back to "command not found." Use absolute path /usr/bin/git as workaround.
- gh api repos/.../contents/<path> --jq .sha returns the **git blob SHA** (formatted blob hash, not raw content hash). Mac-local equivalent: git hash-object <file>.

## Verification matrix

**Tested (positive signal observed in-session):**
- ~/cowork-backup.sh manual run → "no changes — skipping commit" + clean rc=0.
- ~/brain-backup.sh manual run → committed .gitignore expansion → "pushed" → rc=0.
- ~/bin/slack-alert.sh test-job 0 /tmp/nonexistent → rc=0 short-circuit (no Slack post emitted).
- Mac→GitHub HTTPS push working (commits 8c6c029, 4eb1166).
- Pi→GitHub SSH push working (commits 22f4168, 8c6c029 via wrapper).
- Pi git pull of Mac-pushed 4eb1166 → fast-forwarded cleanly.
- bash -n syntax pass on all 5 modified wrappers + new slack-alert.sh.
- All chmod 600 verified via stat -c '%a'.
- Three-way gotchas.md content check (Mac/canonical/Pi) → all aligned at 14,968 bytes / 116 lines post-restore + entry append.

**Trusted-untested (logic looks right but hasn't fired in production):**
- slack-alert.sh new jq payload construction for non-zero rc (rc=0 short-circuit doesn't exercise the payload path; no real failure has triggered the new code yet).
- slack-alert.sh --no-tail flag behavior in production (no actual job failure since the rewrite).
- morning-briefing.sh new STATE_DIR + --add-dir (next firing is 5/20 08:00 EDT).
- umask 077 in wrappers (logs already exist at 600 from chmod sweep; umask only matters for new file creation, e.g., on rotation or after deletion).
- Bidirectional git workflow on Mac (push + pull demonstrated, but day-to-day editing flow not yet exercised).

**Wait-for-event (external trigger required to verify):**
- 5/20 02:00 EDT brain-backup.sh cron — first production run under new wrapper. Should emit "no changes — skipping commit" or "pushed", no alert.
- 5/20 03:00 EDT cowork-backup.sh cron — first production run under new wrapper. Same shape. No Slack alert. No Discord alert.
- 5/20 07:00 EDT inbox-triage.sh cron — first production run with --no-tail wired into failure alerts.
- 5/20 08:00 EDT morning-briefing.sh cron — first run with STATE_DIR + --add-dir + --no-tail.
- Mac reboot — verify renamed .plist.disabled is not auto-loaded by launchd at login.

## Open items

- 5/20 cron passive verification (above wait-for-event bucket).
- **Cross-machine subdir sync** (FoWR briefings, IRS log, jukebox catalog, etc.): currently local-only on each machine, no longer mirrored. Mac and Pi will diverge over time. Decision deferred until divergence becomes a problem. Possible designs: narrow rsync excluding .git + tracked files; or one-side-canonical model where one machine owns each subdir.
- **cluster-daily.sh and yt-weekly-synth.sh umask coverage** — not touched today (out of scope). Worth applying the same umask 077 pattern when next edited.
- **Stale cowork-backup.log content** (5 fatal entries from 5/16–5/19) — not rotated; will be appended to by future cron runs, naturally aged out. Cosmetic.
- **~/Library/Logs/sync-cowork-to-pi.log on Mac** — orphaned now that the LaunchAgent is disabled. No log rotation. Cosmetic.
- **~/.claude/CLAUDE.md §8 "this one was caught" entry** — misleading per Incident #3 above; candidate amendment to distinguish "alarm fired and was investigated" from "underlying mechanism identified and fixed."
- **Today's 2026-05-19-tech-brief.md itself** is now in the Pi-canonical allowlist (matches 2026-*-tech-brief.md), so the Pi's 5/20 03:00 cron will pick it up and push it to GitHub if it's been synced down from Mac via git pull on the Pi by then. Manual git pull on Pi to confirm before tomorrow's cron.

## Brain refs

- 2026-05-19-session-cowork-sync-diagnose-and-harden (today's /capture)
- [[2026-05-18-session-prompt-alerts-hook-tuning]] — yesterday, ~/.claude config edits (Notification hooks)
- [[2026-05-16-session-pi-cron-trifecta-wiring]] — original ~/.claude/CLAUDE.md §8 source for the "this one was caught" entry that turns out to be misleading
- [[2026-05-14-session-pi-hardening-cowork-backup]] — initial Pi backup wiring; baseline for today's tightening
- ~/.claude/CLAUDE.md §8 — failure modes list; candidate amendment per Incident #3
- ~/cowork/gotchas.md (post-4eb1166) — new entry covers the rsync --delete failure mode
- Commits: BryanDuplantis/cowork@22f4168, @8c6c029, @4eb1166; BryanDuplantis/brain@84bb124
- Backup files for rollback: ~/cowork-backup.sh.bak-2026-05-19-pre-allowlist, ~/brain-backup.sh.bak-2026-05-19-pre-allowlist, ~/bin/slack-alert.sh.bak-2026-05-19-pre-jq, ~/bin/morning-briefing.sh.bak-2026-05-19-pre-statedir (all Pi-side)

---

# Session arc 2 — Slack → Discord migration

## Session arc

Pivoted from morning's cowork-sync hardening into a planned migration of all Pi cron alerts + content delivery from Slack to Discord. Plan-mode workflow: discovery agent audited the five wrappers I'd touched that morning (brain-backup, cowork-backup, jukebox-sync, morning-briefing, inbox-triage), confirmed ~/bin/discord-alert.sh already existed mirroring slack-alert.sh's contract, with cowork-backup.sh + jukebox-sync.sh already dual-posting. Three known unmigrated callers from that set: brain-backup.sh, morning-briefing.sh, inbox-triage.sh. Content-delivery path for briefing/triage was the real work — both use claude -p with mcp__claude_ai_Slack__slack_send_message; no Discord MCP equivalent existed, so delivery had to move to a wrapper-level shim.

Decisions locked via AskUserQuestion before plan finalization: three Discord channels (mirrors Slack's #cron-alerts / #morning-briefings / #inbox), three webhooks; hard cutover after one verified test; OAuth revoke + token rm after test passes. Built ~/bin/discord-post.sh (chunked-by-##-section content delivery), added discord-alert dual-post to the three Slack-only wrappers (Phase A of cutover), modified morning-briefing.sh + inbox-triage.sh --allowedTools to drop slack_send_message and add Write, rewrote workflow doc ~/cowork/01_too_important/CLAUDE.md to instruct the model to write $STATE_DIR/*.md instead of calling Slack tool.

Test A (failure alerts) verified end-to-end against both Slack and Discord — synthetic rc=99 fire from slack-alert.sh + discord-alert.sh directly, Bryan visually confirmed both channels rendered correctly.

Test B (content delivery) hit two snags. First snag: first DRY-RUN burned $2.50 budget cap (pre-existing failure mode amplified by stale last-run from 5/16 → 3-day window). Verified file write worked (31 KB triage produced, posted manually via discord-post.sh against the existing multipart-attachment path). Bryan flagged that Discord previews .md attachments as raw ## source — not readable inline. Rewrote discord-post.sh to chunk-by-##-section with 0.4s rate-limit headroom and 25-chunk cap. Bumped last-run to today 11:00 UTC, re-ran DRY-RUN: full success path exercised in ~4 min for $0.50, 7.7 KB triage produced, wrapper picked it up, posted as 6 chunked messages. Bryan visually confirmed: native Discord markdown rendering, bold/italic/bullets working, no raw markers.

Cutover (Phase 1): stripped slack-alert.sh from all 5 audited wrappers. Renamed ~/bin/slack-alert.sh → .disabled. Immediately caught a scope miss — three more callers existed: ~/bin/yt-weekly-synth.sh, ~/yt-subs/cluster-daily.sh, ~/yt-subs/fetch-and-score.py (Python subprocess). The hourly yt-subs fetch would silently lose failure alerts within 60 min. Restored slack-alert.sh immediately, then added discord-alert dual-post to all three yt-* scripts as Phase 2 prep (full Slack strip + OAuth revoke deferred to a follow-up session after dual-post soak).

Two live security incidents during webhook setup: Bryan leaked two Discord URLs in succession via ~/bin/save-webhook.sh's read -rs prompt. First leak: pasted into Claude Code's ! bash, stdin was captured to chat transcript regardless of terminal echo. Second leak: pasted in native Terminal.app, but bracketed-paste mode echoed the URL into terminal scrollback before read -rs consumed it. Each required Discord webhook revoke + recreate. Built Mac-side ~/bin/save-discord-webhook-from-clipboard.sh — clipboard → pbpaste → ssh stdin → Pi file. Third attempt succeeded cleanly.

Bryan asked for two product additions mid-session: (1) Gmail thread URLs on every triage entry — rolled into the workflow doc as part of this session (~0 cost, just include in output); (2) AI per-email summaries — captured to backlog with cost analysis ($60-150/month additional Claude API spend for ~50 threads/day if full-body summaries; cheap version uses Gmail snippet at $0 extra). Bryan also requested no silent email skipping (every email enumerated in some section) and a Discord-based feedback loop for rescuing false positives from Would-archive — first implemented now (workflow doc), second captured to backlog with 4 implementation paths.

## Shipped artifacts

**Pi-side** (brydup@brain-mcp.local:/home/brydup):

- ~/bin/discord-post.sh — **new**. Chunked-by-##-section content delivery to Discord webhook. Mirrors discord-alert.sh's --channel <name> convention; resolves to ~/.config/discord-webhook-<channel>. awk splits content at ##  boundaries, sub-splits long sections at line boundaries to keep each chunk under 1700 chars. jq -n --arg c payload construction (no shell-interpolation injection). 0.4s sleep between POSTs (Discord rate limit: 5/2s per webhook). 25-chunk cap as safety. Hard cap defense: truncate any single chunk over 1990 chars. umask 077, set -euo pipefail, chmod 700.
- ~/.config/discord-webhook-morning-briefings — **new**, chmod 600, 121 bytes.
- ~/.config/discord-webhook-inbox — **new**, chmod 600, 121 bytes.
- ~/brain-backup.shalert() function now Discord-only (slack call removed at cutover). Original at .bak-2026-05-19-pre-discord and .bak-2026-05-19-pre-cutover.
- ~/cowork-backup.shalert() function now Discord-only. Backups same naming pattern.
- ~/jukebox-sync.sh — same.
- ~/bin/morning-briefing.shALLOWED_TOOLS dropped mcp__claude_ai_Slack__slack_send_message, added Write. Added Discord post-step on rc=0 (pickup briefing-*.md from $STATE_DIR, call discord-post.sh --channel morning-briefings). Failure branches dual-post (Slack call stripped at cutover, now Discord-only).
- ~/bin/inbox-triage.sh — same pattern, channel inbox, file triage-*.md.
- ~/bin/yt-weekly-synth.sh — added discord-alert.sh dual-post to both failure branches. **Slack call still present** (Phase 2 strip pending soak). Backup at .bak-2026-05-19-pre-discord.
- ~/yt-subs/cluster-daily.sh — same. Backup at .bak-2026-05-19-pre-discord.
- ~/yt-subs/fetch-and-score.py_slack_alert function modified to fan out to both slack-alert.sh and discord-alert.sh via two subprocess.run calls. Name kept for callsite compatibility; can be renamed _alert in a later pass. Backup at .bak-2026-05-19-pre-discord.
- ~/cowork/01_too_important/CLAUDE.md — workflow doc rewritten. Identity line, cron-mode constraints, tool-call discipline, morning-briefing Step 7, inbox-triage Steps 6/7, Slack-format-skeleton → Discord-markdown-skeleton, dry-run wording. Added URL specification (Gmail thread URLs in every enumerated entry). Added "no silent skipping" — every email surfaces in Decisions / topical / Would-archive (fully enumerated, not summarized). 229 lines (was 195). Backup at .bak-2026-05-19-pre-discord.
- ~/cowork/01_too_important/inbox-triage-ideas-backlog.md — **new**, 3.7 KB, 51 lines. Captures AI per-email summaries + Discord feedback-loop options for future decision. Pi-only (gitignored subdir).
- ~/.local/state/inbox-triage/triage-2026-05-19.md — produced by test runs; first artifact of the new content-delivery architecture.
- ~/.local/state/inbox-triage/last-run — manually bumped to 2026-05-19T11:00:00Z for the Test B re-run; tomorrow's 7am cron should successfully bump it (state-file write was previously failing since 5/16; the migration's Write allowlist addition fixes this as a side effect).
- ~/bin/slack-alert.sh — **still live** (Phase 2 will rename to .disabled after yt-* strip). Briefly renamed to .disabled mid-session, then restored when yt-* dependency caught.

**Mac-side** (bryanduplantis@MacBook-Air:/Users/bryanduplantis):

- ~/bin/save-discord-webhook-from-clipboard.sh — **new**, chmod 700, 1.6 KB. Reads URL from macOS clipboard via pbpaste, validates against canonical Discord webhook URL regex, pipes via ssh to write ~/.config/discord-webhook-<channel> on Pi with chmod 600. Replaces the unsafe read -rs path on the Pi-side save-webhook.sh for new webhook entry.
- ~/cowork-mac-mirror-2026-05-19-inbox-triage-ideas-backlog.md — mirror copy of the Pi backlog file (since Pi version is gitignored). Kludgy filename; durable cross-machine strategy is an open item.

**Memory updates** (~/.claude/projects/-Users-bryanduplantis/memory/):

- reference_pi_slack_token.md — updated status to "in transition." Lists who still depends on the token (3 yt-* scripts) and who's been cut over (5 wrappers). Notes Phase 2 deferred work.
- reference_pi_alerting_surfaces.md — updated current state. 5 wrappers Discord-only, 3 yt-* dual-post, Slack reads preserved for signal sources.
- reference_pi_discord_webhooks.md — **new**. Three webhooks, channel mapping, helper scripts, secure entry workflow.
- MEMORY.md — updated Pi Infrastructure section to add Discord webhooks reference + reword alerting-surfaces description.

**Global standards** (~/.claude/CLAUDE.md):

- §8 Known Failure Modes — two new entries: read -rs defeated by Claude Code stdin capture + macOS bracketed-paste; migration audits must grep -RIl across entire $HOME, not just the wrapper set in scope.
- §9 Session Log — today's entry added at the top (5/19 Pi alerting migration), pushing the 5/18 hooks tuning entry down. No rotation needed (oldest still within 30 days).

**Brain captures:**

- 2026-05-19-session-pi-cron-discord-migration — this afternoon's session recap (just captured).
- 2026-05-19-session-cowork-sync-diagnose-and-harden — morning session recap (captured earlier today).

## Cron schedule changes

None. All 8 Pi crontab entries unchanged. Cron now hits new code paths but same firing schedule.

## Decisions

- **Three Discord channels, three webhooks** — mirrors Slack's prior structure (#cron-alertsPi Cron Alerts, #morning-briefings, #inbox). Audience separation preserved.
- **Hard cutover after one verified test** for the 5 audited wrappers; **dual-post + soak** for yt-* (caught late, can't ratify with the same Test A/B path).
- **Content delivery architecture: model writes file → wrapper posts → Discord** (not model-calls-webhook-directly). Decouples generation from delivery, mirrors the failure-alert helper pattern, keeps --allowedTools scoped to Write (not Bash for arbitrary curl).
- **Chunked-by-section inline messages**, not file attachments. Discord renders .md attachments as raw source, defeating the purpose of "readable in Discord." Tradeoff: more messages per day (typical 4-6, can hit 12-15 on heavy days).
- **Reuse --channel <name> convention** between discord-alert.sh and discord-post.sh. Channel name maps to ~/.config/discord-webhook-<name> file automatically; adding a new channel = 30 seconds (new Discord webhook + one save-webhook call).
- **Defer OAuth revoke + rm slack-bot-token** until yt-* migration soak completes. Per 5/14 lesson, OAuth revoke is the load-bearing security step; deleting local file alone leaves a valid credential floating.
- **Gmail thread URLs in inbox-triage entries** — Bryan's request, ~0 cost (thread IDs already in search results), pure UX win. Implemented this session.
- **No silent email skipping in inbox-triage** — workflow doc rewritten to enumerate every email in Decisions / topical / Would-archive. Tradeoff: bloats output (more chunks), may push past --max-budget-usd on heavy days. Acceptable cost for Bryan's "no false-positive blind spots" requirement.
- **AI per-email summaries DEFERRED to backlog** — cost analysis written ($60-150/month additional spend), no decision yet on snippet-only vs full-body-read.
- **Discord-based feedback loop DEFERRED to backlog** — 4 implementation paths documented; recommended MVP = Gmail label (Path A) for lowest-lift Day-N+1 honoring.
- **save-webhook.sh not removed** despite known read -rs defeat — kept on Pi for reference / non-Claude-Code use cases. Mac-side save-discord-webhook-from-clipboard.sh is the new canonical path.

## Incidents

**Incident 1: Two Discord webhook URLs leaked in succession during setup.** Both required revoke + recreate.
- *URL #1:* Bryan ran ssh brydup@brain-mcp.local '~/bin/save-webhook.sh morning-briefings' via Claude Code's ! bash interface. Pasted URL at the read -rs hidden prompt. Claude Code's stdin capture wrote the URL into the chat transcript regardless of terminal-side echo suppression. URL became permanently embedded in conversation history (and any future /capture export).
- *URL #2:* Same script in native Terminal.app to avoid Claude Code's capture. macOS bracketed-paste mode echoed the URL into terminal scrollback before read -rs consumed it. Bryan then pasted the terminal output back to Claude for confirmation, re-leaking it.
- *Resolution:* Built Mac-side ~/bin/save-discord-webhook-from-clipboard.sh that uses pbpaste → ssh stdin pipeline. URL never echoed, never typed, never enters chat. Third attempt succeeded cleanly. Both leaked webhooks revoked via Discord Server Settings → Integrations → Webhooks → Delete; new webhooks created and stored via the Mac-side helper.
- *Root cause:* read -rs is not a reliable secret-entry mechanism in modern terminal stacks. Both failure paths predate this session — it's just that nobody had stressed-tested the secret-entry path under Claude Code's bash interface or under macOS bracketed-paste before.

**Incident 2: Scope miss on yt-* slack-alert.sh callers.** Renamed slack-alert.sh.disabled as part of Phase 1 cutover. Realized within ~60 seconds (mid-thinking about Phase 2 token disposition) that yt-* scripts were unaudited. Quick grep -l slack ~/bin/yt-*.sh ~/yt-subs/*.{sh,py} confirmed three more callers: yt-weekly-synth.sh, cluster-daily.sh, fetch-and-score.py. The hourly fetch-and-score.py cron would have lost failure alerts within 60 min when slack-alert.sh execution failed silently (the || true at each callsite swallowed the file-not-found). Restored slack-alert.sh immediately (renamed .disabled back). Then added discord-alert dual-post to all three as Phase 2 prep.

**Incident 3: Budget cap hit on first Test B inbox-triage DRY-RUN.** Error: Exceeded USD budget (2.5) after 7 minutes. Root cause: pre-existing last-run was 3 days stale (since 5/16), so the email window was ~3× normal volume (~400 unread). Same pre-existing failure mode that hit on 5/15 12:09 — not migration-induced. Despite budget exhaustion, model HAD written the 31 KB triage-2026-05-19.md before being cut off — full content survived. Resolved by bumping last-run to today 11:00 UTC for the re-test (small 4-hour window).

## Gotchas added

**New ~/.claude/CLAUDE.md §8 entries:**

> **read -rs hidden-input is NOT hidden when run through Claude Code's interactive bash, OR under macOS terminal bracketed-paste mode.** Bryan leaked two Discord webhook URLs in succession on 2026-05-19 using save-webhook.sh (which uses read -rs). First leak: pasted at hidden prompt from Claude Code's ! bash interface — stdin was captured into the chat transcript regardless of terminal echo. Second leak: same script in native Terminal.app, but bracketed-paste mode echoed the URL before read -rs consumed it. Fix: for secret entry to Pi, use Mac-side ~/bin/save-discord-webhook-from-clipboard.sh <channel> — URL flows clipboard → pbpaste → ssh stdin → Pi file, never echoed, never typed, never enters chat. The general principle: read -rs is not a reliable secret-entry mechanism in modern terminal stacks; pipe via clipboard or vault instead.

> **Migration audits must grep -RIl <callsite> ~/ across the full home tree, not just the obvious wrapper set.** Today's Slack→Discord migration audited the 5 named wrappers (briefing/triage/3-backup) and missed 3 yt-* callers of slack-alert.sh (yt-weekly-synth.sh, cluster-daily.sh, fetch-and-score.py). Disabling slack-alert.sh would have silently broken the hourly yt-subs cron's failure alerts. Caught after the disable, restored within minutes. Fix: discovery agent prompts must explicitly say "enumerate ALL callers across $HOME, not just the wrappers in scope" — a narrow audit is the same class of mistake as a narrow git add pathspec.

**Implicit gotchas not yet written to gotchas.md:**

- grep -c <pattern> returns exit 1 when count is 0 — short-circuits && chains during verification scripts. Cosmetic; rerun in two steps.
- 01_too_important/ (and other numbered subdirs) gitignored on cowork repo per today's morning architecture decision. Files there don't get auto-backed-up to GitHub. Affects today's 01_too_important/CLAUDE.md workflow edits and new inbox-triage-ideas-backlog.md — both Pi-local-only. Durable cross-machine strategy for workflow docs is an open item.

## Verification matrix

**Tested (positive signal observed in-session):**
- discord-post.sh syntax + dry-run OK locally and on Pi.
- Test A: slack-alert.sh + discord-alert.sh both fired with synthetic rc=99; Bryan visually confirmed Slack #cron-alerts and Discord Pi Cron Alerts both received correctly formatted alerts.
- Test B (first attempt): claude -p produced 31 KB triage-2026-05-19.md even after budget exhaustion; manually posted via discord-post.sh --channel inbox → Discord rendered file as multipart attachment (success at HTTP level, but UX flagged by Bryan as unreadable).
- Test B (chunked rewrite, manual): re-ran discord-post.sh against the 7.7 KB triage file → 6 chunks delivered; Bryan visually confirmed native Discord markdown rendering (bold names, italic synthesis, hyphen bullets, section headings, no raw markers).
- Test B (re-run, full wrapper path): bumped last-run, re-ran DRY_RUN=1 ~/bin/inbox-triage.sh end-to-end → rc=0, model wrote file under budget ($0.50, 4 min), wrapper picked up via ls -t ... | head -1, called discord-post.sh with new chunking, returned discord-post: 6 chunks delivered to #inbox.
- 5 wrappers: bash -n clean after each edit cycle; grep audit shows 0 residual slack-alert references in the cutover-target set.
- 3 yt-* dual-post wrappers: bash -n clean; py_compile clean on fetch-and-score.py; grep audit shows discord-alert count of 2/1/1 per file.
- ~/bin/discord-post.sh chmod 700 on Pi.
- ~/.config/discord-webhook-{morning-briefings,inbox} chmod 600, 121 bytes each.
- ~/cowork/01_too_important/CLAUDE.md deployed (229 lines, 7 every/mail.google.com references confirming no-skip + URL changes landed).

**Trusted-untested (logic looks right, hasn't fired in production):**
- Wrapper success-branch glue in morning-briefing.sh — identical structure to inbox-triage.sh which IS verified; tomorrow's 8am cron is the first production firing.
- discord-alert.sh dual-post on yt-* — calls the same discord-alert.sh helper that's verified in Test A, but never exercised from yt-* scripts specifically. Next yt-* failure (could be hours or days) is the natural test.
- Workflow doc changes (URL spec, no-skip language) — model behavior change, only visible in tomorrow's cron output.
- discord-post.sh chunking with larger content (~12+ chunks for a 30 KB briefing) — algorithm is correct on paper, hasn't been exercised at the 25-chunk cap.

**Wait-for-event (external trigger required to verify):**
- 5/20 02:00 EDT brain-backup.sh — first production run as Discord-only. Expected: silent success, no alert.
- 5/20 03:00 EDT cowork-backup.sh — same shape.
- 5/20 04:00 EDT jukebox-sync.sh — same shape.
- 5/20 07:00 EDT inbox-triage.sh — first production firing of the new workflow. Expected: claude -p writes ~/.local/state/inbox-triage/triage-2026-05-20.md, wrapper posts ~4-8 chunks to Discord #inbox, state-file last-run finally bumps successfully (first time since 5/16).
- 5/20 08:00 EDT morning-briefing.sh — first production firing of new workflow. Expected: claude -p writes ~/.local/state/morning-briefing/briefing-2026-05-20.md, wrapper posts ~3-6 chunks to Discord #morning-briefings. No Slack post in either channel.
- Top-of-hour fetch-and-score.py — silent on success, dual-alerts on failure. Confirms yt-* discord-alert wiring on first real failure.

## Open items

- **Phase 2 cutover (Task #8):** strip slack-alert.sh calls from yt-weekly-synth.sh, cluster-daily.sh, fetch-and-score.py after dual-post soak. Then mv ~/bin/slack-alert.sh → .disabled. Bryan revokes Slack OAuth via admin (load-bearing per 5/14 lesson). Then rm ~/.config/slack-bot-token (requires per-command approval per Bryan's auto-mode policy).
- **AI per-email summaries decision** — backlog item with cost analysis. Open questions: which sections deserve depth (Decisions only? Decisions + top-3-per-topic?), snippet vs full read+summarize, bloat tolerance.
- **Discord-based feedback loop for false-positive rescue** — backlog item with 4 implementation paths. Decide A (Gmail label MVP) vs B (Discord reactions, more ergonomic but more lift).
- **Cross-machine durability for 01_too_important/ workflow docs** — currently Pi-local-only per today's morning architecture decision (subdirs gitignored). Workflow CLAUDE.md edits and ideas-backlog.md don't reach GitHub. Either change the gitignore strategy, force-add specific files, or accept local-only with periodic backup. Decision deferred.
- **yt-* failure-alert end-to-end test** — discord-alert dual-post is bash -n clean and parallels working pattern, but not exercised on yt-* specifically. Either wait for an organic failure or fire a synthetic test (discord-alert.sh --rc 1 --job TEST-yt-weekly-synth).
- **No-skip workflow cost** — bloats output, may push past --max-budget-usd on heavy days (>150 unread). If 5/20 morning triage hits the cap, revisit caps OR add a smarter filtering tier that still enumerates but compresses very-low-signal items.

## Brain refs

- 2026-05-19-session-pi-cron-discord-migration — this afternoon's /capture (just stored)
- 2026-05-19-session-cowork-sync-diagnose-and-harden — morning session, same day
- [[2026-05-18-session-prompt-alerts-hook-tuning]] — yesterday, ~/.claude config edits
- [[2026-05-14-session-pi-hardening-cowork-backup]] — origin of the "OAuth revoke is the load-bearing step" lesson cited in Phase 2 deferral
- ~/.claude/CLAUDE.md §8 — two new entries on read -rs defeat and audit-scope-miss
- ~/.claude/CLAUDE.md §9 — 2026-05-19 entry added
- ~/cowork/01_too_important/CLAUDE.md — workflow doc post-rewrite (229 lines)
- ~/cowork/01_too_important/inbox-triage-ideas-backlog.md — new, Pi-local-only
- Backup files for rollback (all Pi-side, all .bak-2026-05-19-pre-discord and .bak-2026-05-19-pre-cutover):
  - ~/brain-backup.sh.bak-{pre-discord,pre-cutover}
  - ~/cowork-backup.sh.bak-pre-cutover
  - ~/jukebox-sync.sh.bak-pre-cutover
  - ~/bin/morning-briefing.sh.bak-{pre-discord,pre-cutover}
  - ~/bin/inbox-triage.sh.bak-{pre-discord,pre-cutover}
  - ~/cowork/01_too_important/CLAUDE.md.bak-pre-discord
  - ~/bin/yt-weekly-synth.sh.bak-pre-discord
  - ~/yt-subs/cluster-daily.sh.bak-pre-discord
  - ~/yt-subs/fetch-and-score.py.bak-pre-discord

---

# Session arc 3 — Anthem MRX 300 calibration

## Session arc

Evening session, off-infra, hands-on hardware. Two related complaints solved in one walkthrough: (1) routing the receiver's front-panel L/R RCA input to the CD source button so an external device could play through the system without re-wiring, and (2) chronic boomy bass that had survived years of fiddling. No TV connected to the receiver, so all navigation done via the 2-line 16-char front-panel display — every menu transition was photo-confirmed by Bryan since the abbreviated labels (Aud/Vid Setup, Spkr Config, Fnt Analog, SBW) aren't readable in any documentation.

Walked the full Setup Menu tree from the front panel: Setup → Aud/Vid Setup → Main Src Setup → CD → Main Audio In = Fnt Analog. Source routed, audio flowing through external amp + KRK 10s sub setup. Master volume safety pass to -30 dB before play.

Then bass calibration. Bryan reported sub blasting at the low end. First-pass fix was to verify bass routing was correct via Spkr Config → Bass Management: all speakers Sm 80Hz in Movie config, sub on. Routing was clean — the issue wasn't doubled bass.

Next: cable trace. The MRX 300's SUB W pre-out is a **single mono RED RCA in the same column as CENTER** (white = Center, red = Sub W). Visually misleading — the red color suggests "R" of a stereo pair but it's the dedicated subwoofer output. Confirmed Bryan's braided cable was correctly in SUB W, not in a misrouted FRONT R or SB/VH R jack. Cabling clean.

Diagnosed the actual root cause as **slope-stacking**: the KRK 10s has its own internal low-pass filter (knob on back, 50-130 Hz range), the receiver crosses over at 80 Hz, and both were set near each other → two filters in series creating a lump in the response right at the crossover region. Cure: open the sub's filter as wide as possible (130 Hz fully clockwise) so the receiver's filter dominates alone. KRK volume centered to 0 dB, phase left at 0°.

Receiver-side level trim: Lev Calibration → Movie Sub and Music Sub stepped down from 0 dB to -10 dB in 3 dB increments by Bryan, listening between. Landed at -10/-10. Result: bass sits under the music instead of competing with it. Boom fight over — a years-long complaint resolved.

Bryan tested across multiple streaming services as a final check — source mastering variance confirmed (loudness normalization differs per platform), but receiver-side settings now reliable across all of them.

Produced the learning guide as deliverable (Sections 1-14: system diagram, full Setup Menu tree, bass management concepts, slope-stacking explanation, level vs tone vs sub-volume disambiguation, phase, source assignments, the SUB W jack confusion, ARC overview, calibration discipline rules, current settings snapshot, troubleshooting reference, glossary, keyboard-style menu paths). Ran through ~/bin/tech-brief-to-html for the styled HTML sibling. Then uploaded to Google Drive root as raw markdown (conversion disabled to preserve ASCII diagrams + code blocks) per §7 canonical-surface rule (long-form docs → Drive).

Captured to brain (2026-05-19-session-anthem-mrx-300-calibration + 2026-05-19-session-mrx-300-guide-html-styling).

## Shipped artifacts

**Mac-side** (bryanduplantis@<mac>:/Users/bryanduplantis):

- ~/cowork/anthem-mrx-300-learning-guide.md — **new**, 14-section reference doc. Purpose: persistent calibration knowledge + troubleshooting reference. Will be ingested by next Pi cowork-backup cycle (matches no *-tech-brief* allowlist pattern — see "Open items").
- ~/cowork/anthem-mrx-300-learning-guide.html — **new**, 17,810 bytes, 10 styled code spans. Produced by ~/bin/tech-brief-to-html. Terminal-look styling for browser viewing.

**Google Drive** (root, owner duplantis@gmail.com):

- Anthem MRX 300 — Calibration Learning Guide.md — **new**, ID 1TJfhpPZOJdxuy74fKsX6nxYqI-BoznAB, MIME text/markdown. Uploaded with disableConversionToGoogleType=true so Drive doesn't auto-convert to Google Docs and break ASCII diagrams. Renders natively in Drive preview / iOS Drive app / claude.ai. Cross-surface read path for the long-form doc.

**Brain captures:**

- 2026-05-19-session-anthem-mrx-300-calibration — main session recap.
- 2026-05-19-session-mrx-300-guide-html-styling — follow-up note marking the HTML deliverable.

**Anthem MRX 300 settings written to non-volatile receiver memory:**

- Source CD → Main Audio In = Fnt Analog (was likely default Coax 2 or Analog 2).
- Speaker Config → Bass Management → Movie: Front/Center/Surround = Sm 80Hz (assumed already, confirmed).
- Lev Calibration → Movie Sub: -10 dB (was 0 dB).
- Lev Calibration → Music Sub: -10 dB (was 0 dB).

**KRK 10s rear-panel knob settings:**

- Low-Pass Freq: 130 Hz (fully clockwise — was somewhere around 50-80 Hz).
- Volume: 0 dB (centered — was approximately +3 dB).
- Phase: 0° (unchanged, untested A/B against 180°).
- Input: single RCA into LINE IN (unchanged, verified correct).

## Cron schedule changes

None. No infra touched.

## Decisions

- **Slope-stacking is the load-bearing diagnostic for "boomy sub" on systems with crossovers on both sides.** Cure: open the downstream filter (sub) as wide as possible so the upstream filter (receiver) dominates. This single change usually fixes more bass complaints than any amount of level trimming. Logged into the learning guide as the most important section (§4).
- **Sub gain knob set once, never touched.** All future sub-volume adjustments happen at the receiver's Lev Calibration. Keeps the system reproducible. Logged as calibration discipline rule §1 in the guide.
- **Receiver does all bass management; sub's internal crossover stays wide open.** Logged as rule §2.
- **All speakers Sm @ 80 Hz** unless specific reason otherwise (THX standard, works for almost any speaker smaller than floorstanders). Logged as rule §3.
- **Movie and Music configs match** until deliberate reason to differ. Logged as rule §4.
- **Trust the printed jack labels, not the color coding.** Standalone mono outputs (Center, Sub) break the white-L/red-R convention. SUB W on the MRX 300 is a red RCA sharing the column with the white CENTER jack. Logged as rule §6.
- **Long-form reference doc → Google Drive root** per ~/.claude/CLAUDE.md §7 canonical-surface rule. No new folder structure created (Bryan's "do not change the flow" directive). Filed flat at Drive root with descriptive title for search.
- **Skipped phase A/B test** — Bryan switched music services mid-test before reporting which sounded tighter. Phase left at 0°. Logged as open item for future verification.
- **Skipped Music config bass management verification** — confirmed Movie config at 80 Hz on all channels, assumed Music matches. Open item.
- **Did not run ARC.** Requires Anthem mic + Windows PC; both potentially available but not located this session. ARC is the long-term answer for any remaining bass irregularity (parametric EQ that cancels room modes, not just level trim). Open item.

## Incidents

**Incident 1: Doubled "system reminders" about TaskCreate during a non-task-tracked walkthrough.** The hook fired multiple times suggesting TaskCreate/TaskUpdate use during an interactive hardware-calibration session where task tracking would have been overhead — wrong tool for the work shape. Ignored per the reminder's own "ignore if not applicable" guidance. No remediation needed but worth noting: the reminder cadence is biased toward multi-step code work; interactive walkthrough sessions shouldn't trigger it.

**Incident 2: ManualsLib + RetailSpecs PDF both image-scanned, not text-extractable.** First-pass attempt to fetch the MRX 300 manual via WebFetch returned binary JPEG data inside the PDF wrapper. Pivoted to ManualsLib's HTML preview for the table-of-contents only, then drove the rest of the session from front-panel photos Bryan sent + Claude knowledge of the MRX 300's actual menu structure. HiFi Engine returned 403 (login required). No remediation — just a reminder that ~2010-era consumer audio gear manuals are often scanned, not text PDFs.

**Incident 3: Misidentified SUB W jack location on first pass.** Earlier guidance suggested the sub cable might be misrouted into a SURR R or SB/VH R jack. Once Bryan unplugged and read the printed label, the jack was clearly labeled SUB W in the column shared with CENTER. Cabling was correct from the start; the diagnostic confusion came from the red coloring + my unfamiliarity with the MRX 300's specific layout convention. Captured into the learning guide §8 so future-anyone hits it once and never again.

## Gotchas added

**None added to ~/.claude/CLAUDE.md §8 today** — the gotchas here are domain-specific (audio gear), not engineering/infra. They live in the learning guide instead. Worth noting in case they recur in another consumer-electronics calibration session:

- **Slope-stacking when both upstream and downstream have crossovers.** Receiver at 80 Hz + sub at 80 Hz = lump + steeper rolloff. Cure: open the downstream filter.
- **Color coding on receiver rear panels is unreliable for mono outputs.** Anthem MRX 300's SUB W is a red RCA next to a white CENTER. Trust labels, not colors.
- **Image-scanned PDFs are common for ~2010-era consumer electronics manuals.** WebFetch returns binary; fall back to ManualsLib HTML preview or product photos + LLM domain knowledge.
- **Streaming service mastering creates non-trivial bass variance.** A/B between services when diagnosing speaker/sub problems — your speakers may be fine and the source may be the variable.

## Verification matrix

**Tested (positive signal observed in-session):**
- Bryan confirmed audio routing from external device through CD source button → external amp → speakers + sub.
- Bryan confirmed bass-no-longer-boomy on multiple streaming services after the KRK Low-Pass → 130 Hz + Movie/Music Sub → -10 dB changes.
- Bryan visually confirmed all menu navigation steps via photos before / after each change.
- Mac → Google Drive upload succeeded (file ID returned, MIME preserved as text/markdown).
- tech-brief-to-html ran clean on the learning guide source (17,810 bytes output, 10 code spans styled).
- open of the HTML sibling rendered in browser (Bryan confirmed visually).

**Trusted-untested (logic looks right, hasn't been exercised):**
- Drive-side rendering on iOS / claude.ai web (uploaded but Bryan hasn't pulled it from those surfaces yet to confirm rendering quality).
- Pi-side mirror of ~/cowork/anthem-mrx-300-learning-guide.md via cowork-backup. The current Pi allowlist (MEMORY.md CLAUDE.md gotchas.md .gitignore '2026-*-tech-brief.md' '2026-*-tech-brief.html' '2026-*-shipping-log.md' '2026-*-inbox-triage-handoff.md') does NOT match anthem-mrx-300-learning-guide.md. The file will live on Mac only (and Drive) unless the allowlist is amended OR the file is renamed to match a pattern. See Open items.

**Wait-for-event (external trigger required):**
- 5/20 03:00 EDT Pi cowork-backup.sh cron — will surface whether today's tech brief itself (2026-05-19-tech-brief.md) gets picked up by the allowlist as expected. Independent of the learning-guide allowlist gap.
- Future bass-anomaly scenario — verify the learning guide's §12 troubleshooting reference is actually useful when consulted under stress.
- Phase A/B test — flip KRK Phase 0° ↔ 180°, listen for tightness change. Deferred.
- ARC run — long-term. Requires mic + Windows PC.

## Open items

- **Learning guide cross-machine durability.** The file lives on Mac + Drive but won't auto-mirror to Pi under current cowork-backup.sh allowlist. Three options:
  1. Add a new pattern to the allowlist (e.g., 'anthem-*-learning-guide.md' or '*-learning-guide.md') — pollutes the allowlist with project-specific entries.
  2. Rename the file to match an existing pattern (no clean fit — it's not a tech-brief / shipping-log / triage-handoff).
  3. Accept Mac + Drive as the canonical surfaces; Pi gets it via the next manual cowork sync if/when one happens. Drive is already the cross-surface read path per §7, so this is arguably fine.

  Recommended: option 3 (do nothing), since Drive already provides cross-surface read access and the file isn't expected to change often. Revisit if more topic-specific learning guides accumulate and want a shared pattern.

- **Music config bass management verification.** Movie config confirmed at 80 Hz across the board; Music config assumed same but not stepped through. Verify next time receiver is powered on with a few minutes to spare.

- **KRK phase A/B test.** 0° vs 180°, listen for which sounds tighter on a known reference track. Cheap test, just need a quiet moment.

- **ARC run.** Long-term fix for any room-mode-driven bass irregularities. Requires the Anthem ARC mic (check original packaging) + a Windows PC running ARC1 software. 30-60 min process across 4-5 listening positions.

- **CD source rename.** Source Setup → Name → rename "CD" to match the actual external device now plugged into the front-panel input (e.g., "Phone", "Turntable", "AUX FP"). Cosmetic; the button works regardless.

- **Source level offset.** If the new front-panel device is materially louder/quieter than other sources, a per-source level offset in Adv Src Setup → CD → Level can balance them. Not done this session because nothing was outputting a baseline-louder signal yet.

## Brain refs

- 2026-05-19-session-anthem-mrx-300-calibration — main session recap (calibration walkthrough).
- 2026-05-19-session-mrx-300-guide-html-styling — follow-up capture for the HTML styling pass.
- ~/cowork/anthem-mrx-300-learning-guide.md — 14-section reference doc, Mac-canonical.
- ~/cowork/anthem-mrx-300-learning-guide.html — styled HTML sibling.
- Google Drive: file ID 1TJfhpPZOJdxuy74fKsX6nxYqI-BoznAB, title "Anthem MRX 300 — Calibration Learning Guide.md", at Drive root.
- ~/.claude/CLAUDE.md §7 — canonical-surface rule (long-form docs → Google Drive). Applied this session.
- ManualsLib reference URL: https://www.manualslib.com/manual/644892/Anthem-Mrx-300.html (TOC readable, page bodies not extractable via WebFetch).

---

# Session arc 4 — FoWR Discord migration + workflow restructure

## Session arc

Extended arc 2's Pi cron Slack→Discord migration to cover Bryan-triggered FoWR briefings (flash + full). Discovery: FoWR skills live at ~/.claude/skills/fowr-{flash,full,end-to-end}-briefing/SKILL.md; both flash and full are Bryan-triggered (not cron-scheduled) and run on the Mac (not Pi). Built/copied Mac-side discord-post.sh from Pi (same script, different ~/.config/discord-webhook-<channel> resolution per host). Extended save-discord-webhook-from-clipboard.sh with a --local flag — clipboard → pbpaste → local Mac file (chmod 600) instead of ssh-to-Pi. Added Discord post-step to both flash and full briefing skills; modified flash-briefing's Step 6 from "Optional archive" to "Always archive + post" since Discord delivery needs a file to pick up and flash archives also have delta-tracking value.

End-to-end test: Bryan provided today's NYT seed article ("Middle East on Edge After Trump Says He Delayed Attack on Iran," May 19, 2026). Documented the NYT-seed pattern in both skills as a one-line addition to Step 2 — "Bryan opens a flash prompt with a single NYT article as the anchor; rest of source pile builds outward via parallel verification." Invoked /fowr-flash-briefing skill, ran the full workflow: anchor (Day 81 = 2026-05-19), tracker + memory read (parallel), 5 parallel verification subagents on load-bearing claims, baseline conflict reconciliation, drafted ~8 KB flash, posted to Discord #fowr as 6 chunks, updated tracker with Day 81 entries + new watch items (Russia HEU transfer destination, Iraqi gov response to UAE quasi-attribution, Artesh institutional escalation signal).

Bryan flagged: "this dossier seems rather, brief. i expected a full fowr briefing, not a flash analysis." Pivoted to /fowr-full-briefing workflow using the same NYT seed + the 5 already-completed verifications (no re-verify spend). Read briefing_style.md (required for full briefings, skipped by flash). Drafted full briefing per style contract: orientation italic, MASTER BLUF with framing-lead + 4 labeled bullets + Watch-for close, 6 numbered sections (Kinetic Posture / Diplomatic Track / Nuclear Track / Alliance Friction / Insurance Market Mechanics / Information Operations) each with ⚡ SIGNAL ledes, section bottom lines, MASTER BOTTOM LINE, 12-item italic deep-dive menu, closing italic footer. 34 KB output. Rendered HTML via python3 ~/bin/fowr-md-to-html.py (42 KB). Posted to Discord #fowr as 28 chunks via the new long-paragraph splitter. Opened HTML in browser.

Bryan attempted to paste .md into Substack per the workflow doc's "paste the .md (clean rendering)" instruction. **Workflow doc was wrong** — Substack's editor did NOT parse markdown on paste; literal **, >, ##, --- characters rendered. Pivoted: copy from the rendered browser HTML (Cmd+A → Cmd+C captures rich-text via DOM rendering) → paste into Substack body. Worked cleanly: bold, italic, headings, blockquote, links all preserved. Substack strips FoWR visual-identity CSS (Geist fonts, ⚡ pills, confidence chips) but semantic structure survives.

While reviewing the first publish render, Bryan flagged structural cleanups for ALL future briefings: (a) drop the inline "About this series" disclaimer block (info now lives in Substack profile); (b) drop the redundant Date/Day/Operation body line (Substack title carries it); (c) standardize title FoWR Day N — Month D, YYYY + subtitle Evening Brief: <theme1>, <theme2>, <theme3>. Updated three source docs permanently (briefing_style.md, fowr-full-briefing/SKILL.md, cowork/02_fog_of_war/CLAUDE.md). Updated today's Day 81 .md to match: removed disclaimer block + Date/Day/Operation line, added YAML frontmatter (time_utc: 21:40, sources_summary: 24 sources verified // 0 fragmentary // 0 disputed, editorial_state: Released). Re-rendered HTML.

Ran security review on today's afternoon changes via subagent (the /security-review skill failed-to-load because cwd was ~/bryanduplantis, not a git repo — file-system change needed in skill to support arbitrary cwd or take a --path arg). Found: 1 HIGH (ssh command injection via $CHANNEL in save-discord-webhook-from-clipboard.sh default mode — typo-class footgun), 1 MEDIUM (path traversal via $CHANNEL in both helper scripts), 1 MEDIUM (webhook URL briefly in ps during curl — deferred, needs refactor to curl --config). Fixed HIGH + first MEDIUM with single allowlist regex [A-Za-z0-9_-]{1,32} in both helper scripts; verified with negative test (rejects ../etc/passwd) and positive test (accepts fowr). Re-deployed fixed discord-post.sh to Pi. Committed tech brief expansion (session arcs 2 + 3) as 0e94ac7, pushed.

Captured the FoWR Discord arc to brain as 2026-05-19-session-fowr-discord-migration. Now drafting this tech brief addition as the night's wrap-up.

## Shipped artifacts

**Mac-side** (/Users/bryanduplantis):

- ~/bin/discord-post.sh — **new on Mac** (mirror of Pi version + additional fixes). Chunked-by-##-section content delivery to Discord webhook. Resolves --channel <name> to ~/.config/discord-webhook-<name>. Same script works on both Mac and Pi (the only host-difference is which ~/.config/ it reads from). chmod 700. Two improvements landed today vs. earlier Pi version:
  - **Long-paragraph splitter** — when a buffered chunk exceeds 1900 chars (typically a dense MASTER BLUF), split at last ". " boundary within trailing 300 chars; fallback to raw character split. Prevents content loss on dense paragraphs. Max chunk size on Day 81 full briefing (would have hit 2281-char truncation): now 998 bytes.
  - **MAX_CHUNKS=40** (up from 25) to accommodate FoWR full briefings which run 20-35 chunks. Cap-exceeded path fails loud with suggested workaround.
  - **Channel-name allowlist regex** ^[A-Za-z0-9_-]{1,32}$ — fixes security review H1 (ssh injection) and M1 (path traversal). Negative test rejects ../etc/passwd; positive test accepts fowr / morning-briefings / cron-alerts.
- ~/bin/save-discord-webhook-from-clipboard.sh — **extended with --local flag**. Default mode still does pbpaste → ssh stdin → Pi file. New --local mode does pbpaste → local Mac file (chmod 600). Same allowlist regex added. Backup at ~/bin/save-discord-webhook-from-clipboard.sh.bak-pre-channel-allowlist (if preserved on disk).
- ~/.config/discord-webhook-fowr — **new**, chmod 600, 121 bytes. Mac-local. Used by both FoWR briefing skills.
- ~/cowork/02_fog_of_war/FoWR_Day81_2026_05_19_flash.md — **new**, 8.3 KB, internal-only flash briefing for Day 81. Produced via /fowr-flash-briefing skill end-to-end.
- ~/cowork/02_fog_of_war/FoWR_Day81_2026_05_19.md — **new**, 34 KB, full briefing for Day 81 with YAML frontmatter for HTML header (time_utc, sources_summary, editorial_state). Substack-bound.
- ~/cowork/02_fog_of_war/FoWR_Day81_2026_05_19.html — **new**, 41 KB, designed-canonical HTML render with FoWR visual identity. Used as the source for Substack copy-paste.
- ~/cowork/02_fog_of_war/fowr_tracker.md — Day 81 entries added (Trump postponement, UAE Iraqi-territory attribution, Russia HEU specificity, Artesh "new fronts" signal). 3 new watch items, 2 watch items partially triggered + refocused.
- ~/.claude/skills/fowr-flash-briefing/SKILL.md — Step 2 documented NYT-seed pattern. Step 6 changed from "Optional archive" to "Archive + Discord post" (always-archive now; new convention from this session).
- ~/.claude/skills/fowr-full-briefing/SKILL.md — Step 2 documented NYT-seed pattern. Step 6 added Discord post + limitation note for end-to-end flow. Step 5 dropped "About this series" disclaimer block requirement and Date/Day/Operation body line. Substack title/subtitle convention added at top of Step 5.
- ~/cowork/02_fog_of_war/fog_of_war_resources/briefing_style.md — removed verbatim "About this series" block + section heading; removed Date/Day/Operation from structural skeleton; added Substack title/subtitle convention + per-removal explanatory notes.
- ~/cowork/02_fog_of_war/CLAUDE.md — updated document-structure description (drop disclaimer + Date/Day/Operation, add title/subtitle); updated AI-authorship disclosure note.

**Pi-side** (brydup@brain-mcp.local:/home/brydup):

- ~/bin/discord-post.sh — **re-deployed twice today** (afternoon): once with the long-paragraph splitter, once with the channel-name allowlist regex. Same content as Mac. chmod 700.

**Brain captures (today, afternoon FoWR session):**

- 2026-05-19-session-fowr-discord-migration — this session arc's /capture (just stored).

**Pi-side artifacts no longer present (cleanup):**

- ~/.local/state/inbox-triage/triage-2026-05-19.md.pre-test-b — earlier Test B artifact from arc 2; not from this arc but worth noting it lives at that path if needed for rollback comparison.

**GitHub commits pushed today (this arc):**

- BryanDuplantis/cowork@main: 0ca9db70e94ac7 2026-05-19 tech brief: append Slack→Discord migration session arc (covered arcs 2 + 3; arc 4 would be the next commit).

## Cron schedule changes

None. FoWR briefings are Bryan-triggered (not cron-scheduled). No Pi cron entries touched in this arc.

## Decisions

- **FoWR Discord delivery is single-channel** (#fowr) for both flash and full briefings, not split. Bryan's pick when offered topology options.
- **Manual trigger only** — no Pi cron for FoWR briefings. Discord post is part of the Bryan-triggered skill execution.
- **Always-archive flash briefings** (changed from "optional archive"). Reason: Discord post needs a file to pick up; flash archives also have delta-tracking value Bryan was implicitly using.
- **Chunked inline delivery for FoWR content** — same architecture as inbox-triage migration (arc 2). Discord renders native markdown inline; .md attachments preview as raw ## source.
- **Long-paragraph splitter** in discord-post.sh — split at sentence boundaries when buffer > 1900 chars. Prevents MASTER BLUF truncation. MAX_CHUNKS bumped 25 → 40 to accommodate full briefings.
- **Drop "About this series" disclaimer block** from all future briefings — info lives in Bryan's Substack profile. Orientation italic at top still discloses AI authorship.
- **Drop Date/Day/Operation body line** — Substack title FoWR Day N — Month D, YYYY carries that info; redundant in body.
- **Title/Subtitle convention standardized:** FoWR Day N — Month D, YYYY + Evening Brief: <theme1>, <theme2>, <theme3>. Flash variant uses Midday Flash: prefix.
- **YAML frontmatter always present** for publish-bound .md files (time_utc, sources_summary, editorial_state). HTML renderer falls back to placeholder defaults if frontmatter is missing — visible to readers.
- **Substack publish is HTML-paste-via-browser**, not .md paste. Workflow doc was wrong about .md clean rendering. HTML preserves bold/italic/headings/blockquote/links when copied from rendered browser DOM; Substack strips FoWR visual-identity CSS.
- **Channel-name allowlist regex** [A-Za-z0-9_-]{1,32} in both helper scripts. Fixes security review H1 + M1.
- **Defer M2** (webhook URL briefly in ps) — needs curl --config refactor; not blocking.
- **Don't re-post Day 81 to Discord** after each cosmetic header iteration. 28-chunk reposts spam the channel for non-substantive changes.

## Incidents

**Incident 1: Substack .md paste failed.** Workflow doc said pasting .md into Substack's editor would render cleanly. Empirically false. Substack treated all markdown syntax as literal text: **bold** showed as **bold**, > blockquotes showed as > characters, ## showed as ##. Pivot: open the HTML render in browser, Cmd+A → Cmd+C (captures rich-text via DOM, not raw HTML markup), paste into Substack body. Works cleanly. Updated workflow doc.

**Incident 2: Bryan flagged dossier-shape mismatch.** I ran /fowr-flash-briefing per his earlier choice of option (b) ("Real flash briefing (~5 min, ~$1): you trigger /fowr-flash-briefing for today's flash"). After the flash landed in Discord he said: "this dossier seems rather, brief. i expected a full fowr briefing, not a flash analysis." Recovery: pivoted to /fowr-full-briefing reusing the same NYT seed + 5 already-completed verifications (no API spend on re-verify). Full briefing drafted, rendered, and posted to Discord within ~3 min. Both flash + full now coexist as today's archive (acceptable per Day 80 baseline — flash has delta-tracking value).

**Incident 3: /security-review skill failed to load.** The skill executes git status on load to scope its review to pending changes on the current branch. Failed in current cwd ~/bryanduplantis ("fatal: not a git repository"). Pivot: dispatched a security-review subagent manually with explicit scope (Mac scripts + Pi mirror + skill files + cowork repo pending changes). Functional outcome was equivalent. Recommended fix to skill: support --path arg or auto-cd to nearest git repo.

**Incident 4: Long-paragraph silent truncation in original discord-post.sh.** Day 81 full briefing's MASTER BLUF is 2281 chars in one paragraph. Original chunker's hard-cap defense truncated at 1990 chars + appended "…(truncated)" — would have lost ~300 chars of the briefing's most load-bearing summary. Caught during dry-run preview of Day 80 archive (37 chunks, largest at 2281 B). Fixed before posting Day 81 with awk-level sentence-boundary splitter. Post-fix: largest chunk dropped to 998 B; total chunk count went 37 → 38 (one extra chunk from BLUF split).

**Incident 5: Security review found channel-name injection.** HIGH severity in save-discord-webhook-from-clipboard.sh default mode (ssh remote command builds cat > $DEST && chmod 600 $DEST with unquoted ${CHANNEL} — typo like CHANNEL='foo;rm -rf ~' would execute on the Pi). MEDIUM in both scripts via path traversal (CHANNEL='../../../tmp/pwned' resolves outside ~/.config/). Attacker model is "Bryan typo'd the channel arg" — no untrusted input path today — but still a footgun. Fixed with allowlist regex ^[A-Za-z0-9_-]{1,32}$ in both scripts. Negative test confirmed rejection of ../etc/passwd (exit 64); positive test confirmed fowr passes through to normal operation.

## Gotchas added

**To capture (not yet written to ~/.claude/CLAUDE.md §8 or ~/cowork/gotchas.md):**

- **Substack's editor does NOT parse markdown on paste, despite our workflow doc claim.** The literal **, >, ##, --- characters render as text. Fix: paste from rendered HTML (browser DOM), not raw .md. Workflow doc updated to remove the false claim — but the gotcha itself belongs in ~/cowork/gotchas.md for cross-project relevance (this is true for any Substack publishing).
- **Channel-name and other "config token" string args are command-injection / path-traversal surfaces if interpolated unsafely into shell commands or file paths.** Allowlist-then-interpolate beats sanitize-then-interpolate beats interpolate-and-hope. Pattern: [[ "$VAR" =~ ^[A-Za-z0-9_-]{1,32}$ ]] || { echo "FAIL: ..."; exit 1; }. Apply to all future Bash helpers that take config names as args.
- **claude -p skills that execute shell on load require cwd to be a git repo if they grep git state.** Failure mode: skill fails to load with shell error rather than gracefully prompting. Possible fix patterns: (a) skill takes --path arg, (b) skill auto-cds to nearest git repo via git rev-parse --show-toplevel, (c) skill emits a helpful error pointing to the cwd problem.
- **HTML renderers with placeholder defaults will silently leak the placeholders to publish destinations.** The FoWR fowr-md-to-html.py renderer fills 12:00 UTC / "Source basket per briefing body" / "Draft" when no frontmatter is present — and those placeholders ride along into any Substack paste. Pattern: always require frontmatter for publish-bound renders, or have the renderer refuse to emit defaults without an explicit --allow-defaults flag.

**Skill files updated (counts as gotcha documentation):**

- ~/.claude/skills/fowr-flash-briefing/SKILL.md and ~/.claude/skills/fowr-full-briefing/SKILL.md — both Step 2 sections now document the NYT-seed pattern.
- ~/.claude/skills/fowr-full-briefing/SKILL.md Step 5 — disclaimer block + Date/Day/Operation line removed with explanatory notes preserved inline.

## Verification matrix

**Tested (positive signal observed in-session):**
- Mac discord-post.sh dry-run + live POST against ~/cowork/02_fog_of_war/FoWR_Day80_2026_05_18.md (Day 80 archive, 44 KB) → 38 chunks correctly chunked; largest 998 B after long-paragraph splitter fix.
- Mac discord-post.sh live POST against today's Day 81 flash → 6 chunks delivered to #fowr (rc=0).
- Mac discord-post.sh live POST against today's Day 81 full → 28 chunks delivered to #fowr (rc=0).
- /fowr-flash-briefing skill end-to-end (anchor → tracker read → source ingest → 5 parallel verifications → draft → archive write → Discord post → tracker update → handoff). Total time ~15 min, cost ~$1.50.
- /fowr-full-briefing skill drafting + archive + HTML render + Discord post (re-used verifications from flash run; no re-verify spend). Total additional time ~5 min, cost ~$0.50.
- HTML render via python3 ~/bin/fowr-md-to-html.py — produced 42 KB / 41 KB HTML siblings for full and flash respectively.
- Negative security test: ~/bin/discord-post.sh --channel ../etc/passwd rejected with exit 64.
- Positive security test: ~/bin/discord-post.sh --channel fowr ... accepted, proceeds to normal operation.
- Pi discord-post.sh re-deployed via scp; bash -n clean; channel-allowlist regex present.
- File perms 600 verified on ~/.config/discord-webhook-fowr (Mac) + ~/.config/discord-webhook-{cron-alerts,morning-briefings,inbox} (Pi).
- Cowork commit 0e94ac7 pushed to GitHub origin/main successfully.

**Trusted-untested (logic looks right, hasn't fired in production):**
- Full briefing's end-to-end /fowr-end-to-end skill — uses full-briefing internally so Discord post fires before the review gate. If Bryan revises during the gate, Discord retains the pre-review version. Known limitation documented in skill Step 6.
- Substack publish to bryanduplantis.substack.com — Bryan paused before hitting Publish; will publish manually outside session.
- save-discord-webhook-from-clipboard.sh --local fowr was used today and worked; the --local flag's negative paths (writing to existing file, perms recovery, etc.) not exhaustively tested.
- Channel-allowlist regex on save-discord-webhook-from-clipboard.sh default (ssh) mode — fix is symmetric with --local, but the ssh-injection vector specifically wasn't replayed post-fix.
- HTML renderer with YAML frontmatter — first time time_utc / sources_summary / editorial_state were exercised together; the render output looks correct in browser but full test of all three values would benefit from a unit-style check.

**Wait-for-event (external trigger required to verify):**
- Bryan's Substack publish + the 5-min launch Note ritual + the 7:30 PM standalone Note ritual.
- Tomorrow morning's first FoWR briefing under the new title/subtitle convention — confirm the format produces clean publish output without manual cleanup.
- Whether Russia publicly acknowledges or rejects the HEU transfer destination role (Day 81 watch item).
- Whether the Iraqi government addresses UAE's quasi-attribution to Iraqi territory (Day 81 watch item).
- Whether any named US principal (Witkoff, Kushner, Vance, Rubio) articulates a public response to Iran's amended package inside Trump's "two or three days" window (Day 81 watch item).

## Open items

- **Discord #fowr channel has stale Day 81 chunks** with the disclaimer block + redundant Date/Day/Operation line. Deferred per Bryan ("Discord channel clutter not worth it for cosmetic header changes; will be fixed tomorrow").
- **M2 security finding** — webhook URL briefly visible in ps during curl. Refactor to curl --config file. Lower priority than today's H1/M1; open task.
- **/security-review skill needs cwd-tolerance** — either accept --path arg or auto-cd to nearest git repo. Open task for skill maintainer.
- **Substack publish ritual** (launch Note within 5 min, 7:30 PM standalone Note) — Bryan's standing manual ritual until Phase 4 Substack publish skill is built. Not part of any current automation.
- **Phase 2 cutover (Task #8 from arc 2)** — strip Slack from yt-* after dual-post soak, revoke OAuth, rm slack-bot-token. Carries over.
- **anthem-mrx-300-learning-guide.md/.html files** in ~/cowork/ are still untracked. Bryan added arc 3 to this tech brief about them; the actual files are not yet committed. Decide whether they belong in the cowork repo or stay local-only (similar question to 01_too_important/ files).
- **Backup-allowlist for cowork-backup.sh** — earlier in arc 2 I attempted to add 01_too_important/*.md to the wrapper's git allowlist; reverted because the subdir is gitignored. The architectural question (where do workflow CLAUDE.md files live durably?) is still open from arc 2.

## Brain refs

- 2026-05-19-session-fowr-discord-migration — this arc's /capture (just stored)
- 2026-05-19-session-pi-cron-discord-migration — arc 2 capture
- 2026-05-19-session-cowork-sync-diagnose-and-harden — arc 1 capture (morning)
- [[2026-05-18-session-prompt-alerts-hook-tuning]] — yesterday
- ~/.claude/skills/fowr-flash-briefing/SKILL.md — Step 2 (NYT seed), Step 6 (Discord post)
- ~/.claude/skills/fowr-full-briefing/SKILL.md — Step 2 (NYT seed), Step 5 (structural cleanups), Step 6 (Discord post + end-to-end limitation note)
- ~/cowork/02_fog_of_war/fog_of_war_resources/briefing_style.md — body skeleton (disclaimer + Date/Day/Operation removed; title/subtitle convention added)
- ~/cowork/02_fog_of_war/CLAUDE.md — document structure updated
- ~/cowork/02_fog_of_war/fowr_tracker.md — Day 81 entries
- ~/cowork/02_fog_of_war/FoWR_Day81_2026_05_19.md + .html — today's full briefing (Substack-bound)
- ~/cowork/02_fog_of_war/FoWR_Day81_2026_05_19_flash.md — today's internal flash
- ~/bin/discord-post.sh (Mac + Pi mirror), ~/bin/save-discord-webhook-from-clipboard.sh (Mac) — channel-allowlist security fix
- Commit 0e94ac7 on BryanDuplantis/cowork@main — tech brief expansion (arcs 2 + 3); this arc 4 expansion is currently uncommitted pending wrap-up commit.


brydup@mac ~/cowork %