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

# Tech Brief — 2026-05-18

Synthesized from session captures:
- 2026-05-18-session-global-standards-setup-reconstructed (backfill of the uncaptured 5/17 morning standards build)
- 2026-05-18-session-brain-backfill-pi-hardening (morning)
- 2026-05-18-session-prompt-alerts-hook-tuning (afternoon)
- 2026-05-18-session-claude-code-stop-hook-wired (late afternoon)

## Session arc

Three connected threads stretched across the day, all converging on ~/.claude/ as a load-bearing-but-opaque surface.

**Morning** opened with a review of yesterday's three captures, which surfaced the dangling [[2026-05-17-session-global-standards-setup]] wiki-link in CLAUDE.md §9 — the morning 5/17 build of the global standards file had never invoked /capture. Reconstructed it as an explicit backfill ((reconstructed) title suffix, Provenance section in body, fidelity gap documented), then updated §9's source line to point at the new capture-date ID. Attempted to scp the updated CLAUDE.md to Pi; the command hung silently because the Pi went offline mid-transfer and OpenSSH has no ConnectTimeout. Killed the stuck process tree, fell through to Tailscale IP (verified host key match against existing known_hosts before appending), completed sync. Codified the hang as a new §8 Known Failure Mode (-o ConnectTimeout=10 + pre-flight ping). Discovered while debugging mDNS that the Pi's canonical hostname is brain-mcp.local, not raspberrypi.local — the old name had been "working" purely via stale Mac mDNSResponder cache. Updated CLAUDE.md §8 and MEMORY.md's Active Projects table to use the correct hostname.

**Afternoon** pivoted to an in-flight Claude Code prompt-alerts thread that had *no* brain capture (and that's the whole lesson). Brain search for "prompt alerts" returned zero matches; the actual state was discoverable only by reading ~/.claude/settings.json mtime + contents directly. Existing Notification hooks fired sounds on both permission_prompt and idle_prompt but only idle_prompt had a banner — asymmetry that made permission gates easy to miss with a backgrounded terminal. Fixed by adding a banner to permission_prompt and splitting sounds (Funk=permission, Ping=idle). Captured.

**Late afternoon** trimmed CLAUDE.md (cut the entire §6 Long-horizon-tasks subsection; both bullets failed the "would removing this cause a mistake?" test) and added the 5/18 §9 entry. Then Bryan noted he didn't hear an alert when I finished cooking — diagnosed as a missing Stop hook (the Notification matchers cover *waiting* events, not end-of-turn). The claude-code-guide subagent earlier in the day had claimed Stop/UserPromptSubmit/PreCompact don't exist; WebFetch of the authoritative docs at code.claude.com/docs/en/hooks proved all three do. Wired Stop with Glass.aiff + "Claude finished" banner. Bryan heard Glass twice (manual preview + auto-fired hook at end-of-turn) — live wiring confirmed, no restart needed.

---

## Shipped

| Artifact | Path | Purpose |
|---|---|---|
| §8 Known Failure Mode: Pi-bound ssh/scp hangs | ~/.claude/CLAUDE.md §8 | New entry codifying the morning's hang. Fix layers: (1) -o ConnectTimeout=10 always; (2) pre-flight ping -c1 -W2 brain-mcp.local (mDNS first, Tailscale IP fallback); (3) host-key verification against existing known_hosts before appending Tailscale IP — same Pi → same ECDSA key, reject StrictHostKeyChecking=accept-new as an antipattern. Includes hostname correction: brain-mcp.local is canonical, raspberrypi.local is retired phantom resolved only via stale Mac cache. |
| Hostname correction (mDNS) | ~/.claude/CLAUDE.md §8 + ~/.claude/projects/-Users-bryanduplantis/memory/MEMORY.md Active Projects table | raspberrypi.localbrain-mcp.local propagated. grep confirmed no other memory file referenced the old name. |
| §9 5/18 entry | ~/.claude/CLAUDE.md §9 | Theme: "~/.claude config edits are their own surface." Body: prompt-alerts wiring + the discoverability gap (brain returned zero matches when asked to "pick up where we left off"). How-to-apply: capture before close *and* name the file path in the recap so semantic search can land it next session. Same lesson as 5/15, applied one layer down. Source link: [[2026-05-18-session-prompt-alerts-hook-tuning]]. |
| §6 trim | ~/.claude/CLAUDE.md §6 | Cut the entire Long-horizon-tasks subsection (header + 2 bullets, 4 lines). Both bullets failed the load-bearing test: "compaction → NOTES.md → multi-agent" was narrative; "preserve [X]" was a discoverable trick already partly covered by §2. CLAUDE.md: 201 → 197 lines. |
| Notification hook: permission_prompt upgrade | ~/.claude/settings.json | Added osascript banner ("Claude needs permission"). Sound switched PingFunk.aiff to differentiate from idle_prompt. Was sound-only and trivially missable when terminal backgrounded. |
| Notification hook: idle_prompt (unchanged behavior, retained for record) | ~/.claude/settings.json | Banner "Claude is waiting for input" + Ping.aiff. |
| Stop hook | ~/.claude/settings.json | New event wiring. osascript banner "Claude finished" + Glass.aiff. No matcher (Stop doesn't support one — fires on every turn). Third distinct sound across event classes: Funk=permission, Ping=idle, Glass=done. |
| Reconstructed 5/17 morning capture | brain: 2026-05-18-session-global-standards-setup-reconstructed | Backfill for the uncaptured 5/17 morning standards build. Title suffix (reconstructed), Provenance section in body, fidelity gap acknowledged (headline decisions preserved from §9 self-summary; reasoning trail lost). §9 source line in CLAUDE.md updated to point at this capture's date-mismatched ID. |
| Pi sync of updated ~/.claude/CLAUDE.md | Pi /home/brydup/.claude/CLAUDE.md | Completed via Tailscale IP fallback after mDNS path hung mid-transfer. Re-synced cleanly via mDNS post-hostname-correction. |
| This tech brief | ~/cowork/2026-05-18-tech-brief.md | Per §7 ritual. End-of-session synthesis. Mac copy first; Pi sync deferred to manual scp if Bryan wants tomorrow's 3am cron to push it. |

---

## Cron schedule changes

None today. Always-on Pi stack unchanged: 2am brain-backup, 3am cowork-backup, 4am jukebox-sync, 7am inbox-triage, 8am morning-briefing, hourly yt-subs, 6pm Sun yt-weekly-synth, 10pm cluster-daily. The 3am cowork-backup tomorrow will pick up today's tech brief and the updated ~/.claude/CLAUDE.md (assuming both are in scope of the backup). Expect silence in #cron-alerts.

---

## Decisions made

- **Reconstructed brain captures get an explicit (reconstructed) title suffix and a Provenance section in the body.** Fidelity gap documented up front so future readers don't mistake the reconstruction for the original. Headline decisions preserved from the original session's §9 self-summary; reasoning trail acknowledged as lost.
- **Backdating brain capture IDs is not supported by the schema** (no created or id param — auto-generated from capture time + type + title slug). When reconstruction is needed, accept the capture-date ID and update the referencing file's wiki-link to match.
- **Pi-bound ssh/scp gets -o ConnectTimeout=10 as a rule, not a per-call choice.** Codified in CLAUDE.md §8. Pre-flight ping (mDNS first, Tailscale IP fallback) is part of the same pattern — fail fast and visibly instead of hanging.
- **When falling back to a Tailscale IP, host key MUST be verified against the existing known_hosts entry before appending.** Same Pi → same ECDSA key. Rejected StrictHostKeyChecking=accept-new as a security antipattern that papers over the verification step.
- **Pi's canonical mDNS hostname is brain-mcp.local.** raspberrypi.local retired; was resolving only via stale mDNSResponder cache on the Mac.
- **Layer 3 (~/bin/pi-sync wrapper with retry loop + #cron-alerts escalation via slack-alert.sh) is deferred.** Layers 1+2 (ConnectTimeout + pre-flight ping) cover interactive sessions cleanly; Layer 3 is for unattended cron-mode and can be built when a cron job actually depends on Pi reachability beyond cowork-backup.
- **Notification permission_prompt gets a banner** (was sound-only). A faint Ping with the terminal backgrounded is not a real interrupt.
- **Distinct sounds per event class: Funk=permission, Ping=idle, Glass=done.** Identifiable by ear without looking.
- **Stop fires on every turn, no gating yet.** If ding-per-turn cadence grates, gating path is well-defined: timestamp stash in UserPromptSubmit, conditional ding in Stop if elapsed > N seconds (~10–15s). Build only if needed.
- **terminal-notifier upgrade deferred.** Installed at /opt/homebrew/bin/terminal-notifier, would give click-to-focus-Terminal + app icon + -group claude-code dedup. osascript works for now.
- **§6 Long-horizon-tasks subsection cut entirely** rather than compressed. Both bullets failed "would removing this cause a mistake?" — narrative content vs. load-bearing rule.
- **When a subagent's claim contradicts prior knowledge, fetch the authoritative docs.** claude-code-guide subagent claimed Stop/UserPromptSubmit/PreCompact don't exist; docs proved all three do. Default to docs over subagent claims on platform-feature facts.

---

## Incidents

- **Pi-bound scp hung silently mid-transfer this morning.** Root cause: OpenSSH has no connect timeout by default. The Pi went offline (reboot or Wi-Fi blip) after the TCP socket was established; the command sat indefinitely with no exit, no notification, no progress output, and queued every subsequent ssh behind it. Recovery: ps auxww | grep raspberrypi to surface the stuck process, kill it, fall through to Tailscale IP (verified ECDSA host key match before appending to known_hosts). Codified as §8 entry. **Background bash output files stay empty while the command is hung** — peek-via-Read returned no content for both scp and ssh-verify until I checked the process table directly. Empty output ≠ done.
- **Claude Code Stop hook went unwired until Bryan flagged "didn't hear an alert when cooking done."** The afternoon prompt-alerts capture had the warning baked in already: "End-to-end positive signal not yet observed — manual osascript + afplay was only a component verification." Then exactly that failure surfaced. Root cause: claude-code-guide subagent gave wrong info, claimed Stop event doesn't exist. WebFetch of code.claude.com/docs/en/hooks (after 301 from docs.claude.com) confirmed Stop is documented with a no-matcher schema. Wired live, fired on next end-of-turn — Bryan heard Glass twice (manual preview + Stop hook). Validation loop closed.
- **claude-code-guide subagent unreliable for hook-event facts.** Documented as a subagent-reliability data point. When a subagent's claim contradicts prior half-knowledge of a documented platform feature, fetch the docs.

---

## Gotchas added

- **§8 Known Failure Mode: Pi-bound ssh/scp hangs forever without ConnectTimeout.** Default OpenSSH behavior; surfaces as a silent indefinite hang on any reachability issue mid-session. Fix: -o ConnectTimeout=10 always + pre-flight ping. Tailscale fallback adds complexity (host-key verification step is non-skippable). Now lives in ~/.claude/CLAUDE.md.
- **§9 5/18 entry: ~/.claude/* config edits are their own surface.** Settings, hooks, skills, custom agents — all opaque to brain-mcp unless explicitly captured. When editing load-bearing config mid-session, capture before close *and* name the file path so semantic search lands it next session.
- **raspberrypi.local was a phantom.** Resolved on Mac for months via stale mDNSResponder cache from before the brain-mcp v1 deploy renamed the host. avahi-daemon on the Pi was healthy the whole time, just advertising a different name.
- **tailscale status hostname is independent of the Pi's mDNS hostname.** Tailscale shows raspberrypi (its own machine-setup assignment); avahi advertises brain-mcp.local. Don't conflate them.
- **Tailscale-routed SSH banner is literally SSH-2.0-Tailscale** — Tailscale intercepts the handshake at its edge before forwarding to target. Pi's actual ECDSA host key still comes through underneath; key verification still works correctly.
- **ping brain-mcp.local can return "No route to host"** while ssh over the same address works. Likely transient ARP/power-save weirdness. ICMP failing doesn't mean TCP unreachable.
- **Background bash output files stay empty while a command is hung** — peek-via-Read returns nothing. Check ps auxww directly when output is suspiciously silent.
- **claude-code-guide subagent gave wrong info about Claude Code hook events.** Subagent-reliability data point: not authoritative for documented platform features.
- **WebFetch doesn't auto-follow 301** from docs.claude.comcode.claude.com. Requires a second fetch call.
- **Claude Code re-reads settings.json hooks dynamically.** No /clear or session restart needed after hook edits — next event picks up the new config live.
- **Hand-applied the rule I'd just written, then almost did it again.** The 5/17 backfill session captured "checking one surface and declaring absent ≠ confirmed missing" as a real-time gotcha. Today I diagnosed "mDNS is broken" without checking what hostname the Pi actually advertises. Same failure class, different surface.

---

## Verification matrix

| Surface / behavior | State | How verified |
|---|---|---|
| ~/.claude/CLAUDE.md 197 lines, §8 ConnectTimeout entry present, §9 5/18 entry present, §6 trimmed | ✓ tested | wc -l post-edits + Read confirmation |
| MEMORY.md Active Projects table uses brain-mcp.local | ✓ tested | Edit + grep for old name returned no remaining references |
| ~/.claude/settings.json JSON valid post-edits | ✓ tested | python3 -c 'import json; json.load(open(...))' returned OK after each edit |
| Sound files exist (Funk.aiff, Ping.aiff, Glass.aiff) | ✓ tested | ls /System/Library/Sounds/*.aiff |
| osascript banner + afplay fire correctly in isolation (all three sounds) | ✓ tested | Manual invocation, Bryan confirmed banners + distinct sounds heard |
| Stop hook fires live at end-of-turn | ✓ tested | Bryan heard Glass twice on the capture turn — manual preview + auto-fired hook. **Live trigger observed in the wild, validation loop closed.** |
| permission_prompt hook live | ⏸ wait-for-event | Components verified in isolation; first real permission gate inside Claude Code is the true test. |
| idle_prompt hook live | ⏸ wait-for-event | Components verified in isolation; no real idle has triggered the hook path live yet. |
| Tomorrow 3am cowork-backup pushes today's CLAUDE.md + tech brief to GitHub | ⏸ wait-for-event | Pi has the CLAUDE.md (synced via Tailscale → re-synced via mDNS); tech brief still Mac-only at write time. Manual scp if Bryan wants it pushed tomorrow. |
| Pi mDNS hostname brain-mcp.local resolves cleanly | ✓ tested | ssh -o ConnectTimeout=10 brydup@brain-mcp.local succeeded post-correction |
| Tailscale-IP fallback path with host-key verification | ✓ tested | This morning's actual recovery used this path; ECDSA key matched existing known_hosts entry before append |
| terminal-notifier install present at /opt/homebrew/bin/terminal-notifier | ✓ tested | which + Homebrew Cellar check; not yet wired into hooks |
| agentPushNotifEnabled / inputNeededNotifEnabled toggle behavior | ⊘ trusted-untested | Both true in settings.json; presumed Claude Code's built-in notification path, parallel to hook system. Interaction with hooks unverified. |
| Other Notification matchers (auth_success, elicitation_dialog, etc.) | ⊘ documented-not-wired | Per docs WebFetch; not used in current hook config. |

---

## Open items

- **Layer 3 ~/bin/pi-sync wrapper** — ping → scp → verify, 3×30s retry loop, escalate to #cron-alerts via slack-alert.sh. Worth building before any cron depends on Pi reachability beyond cowork-backup. Not blocking.
- **Stop-hook gating** — timestamp stash in UserPromptSubmit, conditional ding in Stop only if turn elapsed > ~10–15s. Build only if ding-per-turn cadence becomes annoying in rapid back-and-forth.
- **~/.claude/CLAUDE.md is 47 over the 150 cap** (197 lines after today's trim). Next compression candidates: §8's ConnectTimeout entry is now the longest single bullet (split Tailscale-fallback detail to a sub-reference); §9 entries older than ~14 days could be pre-rotated to ~/.claude/session-log-archive.md rather than waiting for the 30-day cutoff.
- **Other documented Notification matchers** (auth_success, elicitation_dialog, elicitation_complete, elicitation_response) — low-value but documented.
- **agentPushNotifEnabled + inputNeededNotifEnabled semantics still unverified.** Worth understanding the interaction with hooks if Bryan ever wants to turn one off independently.
- **terminal-notifier upgrade** still deferred. Installed; not wired. Pull forward if banners feel underweight.
- **Cross-device push (Pushover/ntfy)** still deferred. iPhone alerts for sessions running while Bryan is away from Mac. Needs token + helper script.
- **5/17 morning session reconstruction is medium-fidelity.** Reasoning trail (rejected alternatives, format debate, gotchas during original build) is permanently lost. Higher fidelity would require Bryan to edit the brain doc from personal memory.
- **Three stale 5/17 Drive uploads** from yesterday's HTML bake-off still need manual cleanup (Drive MCP has no delete_file). IDs are in [[2026-05-17-session-tech-brief-html-pipeline]] "Still open."
- **5/15 tech brief still genuinely missing** — not on Desktop, not in cowork, not in Drive. Could be backfilled from the four 5/15 session captures if Bryan wants.
- **brain-mcp project CLAUDE.md** at ~/Library/Application Support/Claude/local-agent-mode-sessions/.../outputs/brain-mcp-CLAUDE.md still hasn't been landed at ~/Projects/brain-mcp/CLAUDE.md. Carry-over from 5/17 and 5/18.
- **Glass/Ping disambiguation closed.** Bryan said "alert sounds are working not sure about a miswire" — colloquial naming, no actual miswire. Dropped.
- **Four research threads queued for next session**: Slack vs Discord/Telegram for agent workflow; Claude Design for professional briefings; Claude iOS/macOS app + claude.ai vs all-terminal; Ghostty terminal as default. Fresh context per §2.

---

## Brain references

- Today (4 captures): 2026-05-18-session-global-standards-setup-reconstructed, 2026-05-18-session-brain-backfill-pi-hardening, 2026-05-18-session-prompt-alerts-hook-tuning, 2026-05-18-session-claude-code-stop-hook-wired
- Adjacent yesterday: 2026-05-17-session-global-claudemd-lessons-backfill, 2026-05-17-session-tech-brief-html-pipeline, 2026-05-17-note-yt-week-of-2026-05-17
- Source for §9 5/15 lesson (cross-surface): 2026-05-15-session-atcq-dossier-surface-gap
- Hook-event docs source-of-truth: code.claude.com/docs/en/hooks (after 301 from docs.claude.com)

---

# Session 2 — Afternoon & Evening (2026-05-18)

Synthesized from session captures:
- 2026-05-18-session-discord-migration-phase-1 (Discord migration Phase 1: alert helpers shipped on Mac + Pi)
- 2026-05-18-idea-multi-surface-claude-continuity (research thread parked: cross-surface state via brain-mcp + Cloudflare Tunnel architecture)
- 2026-05-18-idea-fowr-designed-outputs-svg-first + 2026-05-18-note-substack-svg-verification-result (Claude Design research thread + Substack-SVG-doesn't-work correction)
- 2026-05-18-idea-fowr-field-style-header-block (intel-brief header treatment as FoWR's anchor visual element)
- 2026-05-18-session-fowr-day-80-html-pipeline (this session — Day 80 briefing shipped through new HTML pipeline)

## Session arc

Four major workstreams stretched across the afternoon and evening, each connecting to the others through a single thread: **moving load-bearing infrastructure out of opaque local state and onto durable, queryable, versioned surfaces.**

**Discord migration Phase 1** — picked the first of four research threads queued from the morning brief (Slack vs Discord/Telegram for agent workflow). After scoping ("full agent-ops surface", "pure exploration"), recommended Discord over Slack and Telegram. Discord wins on retention (no 90-day paywall), bot-UX parity with Slack, server model maps to workstation taxonomy, and indie-dev ecosystem fit. Built ~/bin/discord-alert.sh (mirrors slack-alert.sh contract, §8 rc=0 short-circuit), ~/bin/save-webhook.sh (read-rs based webhook capture with regex validation, 0600 perm enforcement, GNU-stat-first probe). Created private Discord server "Bryan Agent Ops" with #cron-alerts channel and two webhooks ("Cron Alerts" for Mac, "Pi Cron Alerts" for Pi) so origin labeling is visible in author name as well as host field. Verified end-to-end via live smoke tests on both machines. Then advanced Phase 1 step 5: wired jukebox-sync.sh (4am Pi cron) for parallel-fire (alert function pulling from both slack-alert.sh and discord-alert.sh), and Phase 1 extension: wired cowork-backup.sh (3am Pi cron) on the same pattern. Both with .bak-pre-discord rollback artifacts. Tomorrow morning is the first natural test window for parallel-fire on both crons.

**Research threads — three more picked off.** (a) **Multi-surface Claude continuity**: scoped to "full multi-surface continuity, pure exploration." Conclusion: structurally not achievable today — Claude Code is CLI-only, the Claude apps are a separate product. Achievable target is cross-surface *state* (not session) via brain-mcp exposed through Cloudflare Tunnel + Zero Trust. Parked as half-day weekend project; idea captured to brain. (b) **Claude Design for professional briefings**: scoped to "Claude-produced designed outputs." Surveyed format candidates (SVG diagrams, PDF dossier, designed HTML email, social cards, audio narration, infographics). Recommended SVG diagrams as first build for editorial alignment. WebSearch-verified Substack does NOT support inline SVG or custom HTML — fallback path is SVG-source-via-Claude → rasterize to PNG → upload via Substack image embed. Captured as parked idea + correction note. Bryan then dropped the Substack constraint entirely, signaled HTML-canonical format direction, asked for "a cool design element." Picked the field-style header block (intel-brief framing — project mark stripe + day counter + datetime + source basket + editorial state stamp). Captured as forward-build idea. (c) **Ghostty as default terminal**: surveyed quickly (bracketed-paste UX is the real win; GPU rendering modest; modern macOS app feel). Bryan adopted directly.

**FoWR HTML pipeline — built and shipped.** Visual identity baseline doc at ~/cowork/02_fog_of_war/fog_of_war_resources/visual_identity.md codifying the design DNA (bone/ivory + charcoal + oxblood, restrained palette, mono+sans pairing, Tufte-discipline). Initial font pick was Inter + IBM Plex Mono; switched to **Geist + Geist Mono** (sibling pair from Vercel) after a WebSearch confirmed Geist is the current font-trend pick with matched x-heights and weights, free on Google Fonts. Built two HTML mockups (header-block-v1.html, briefing-mockup-v1.html) in fog_of_war_resources/mockups/. Then the pipeline: briefing-template.html (token-placeholder page shell with embedded CSS), ~/bin/fowr-md-to-html.py (Python converter using python-markdown plus FoWR-specific post-processing — confidence chips for HIGH/MEDIUM/LOW + their compound forms, ⚡ SIGNAL paragraph → oxblood pill, **Bottom line.** paragraph → weight-bumped class, blockquote-with-⚠️ → disclaimer wrapper, deep-dive menu label + italic-paragraph item styling, hr→removed in favor of H2-top-border treatment). SKILL.md Step 6 updated to call the converter after writing the .md; Step 8 handoff block updated to report both files. Validated by rendering existing Day 67 briefing — counts looked right (6 SIGNAL pills, 15 confidence chips, 13 deep-dive items, 6 bottom-line treatments).

**Day 80 evening briefing through the new pipeline.** Read tracker, MEMORY, and briefing_style in parallel. Confirmed today is Day 80 (Day 1 = 2026-02-28 + 79 days). Read the morning RSS candidate pile (100 items, filtered out 2016 CSIS noise, Premier League, etc.). Dispatched 6 parallel verification subagents — Bryan rejected approval-per-call overhead; switched to 10 direct WebSearches in two parallel batches of 5. Verified Trump exact Truth Social quote, UAE Barakah strike specifics (3 drones from western border, 2 intercepted, generator hit outside inner perimeter, no UAE attribution), Iran amended 14-point details (2-month ceasefire, explicit Lebanon coupling, Gharibabadi quote), IEA cumulative losses (1B barrels, 14+ mb/d shut in), Iran-Oman mechanism (Baghaei statement), cable-toll proposal (Tasnim, $10T/day transactions, $15B revenue claim, UNCLOS Article 79 obstacle), Stimson "indispensable channel to ignored option" framing, UN-verified 32 political executions. Drafted ~6K-word briefing, 5 sections (Kinetic, Diplomatic, Economic, Alliance Friction, Humanitarian), 14 confidence-flagged claims, 12-item deep-dive menu, MASTER BLUF labeled-tracks structure, disclaimer block included. Bryan's review caught **two distinct failure modes** that required fixes: (1) Section 4 Trump-Merz block was recap-not-delta — missed the May 18 Merz "humiliated" upgrade to German students, Trump's "thinks it's OK for Iran to have a Nuclear Weapon" Truth Social rejoinder, and the operationalization of ~5,000 US troop reduction from Germany. Re-WebSearched, rewrote Section 4 with Day 80 deltas, updated SIGNAL lede and Master Bottom Line, added US-troop-cut as Day 80 locked baseline in tracker. (2) Section 1 referenced "the Day 66 framing held that 'Iran calibrates each engagement to land below...'" — fabricated attribution, since Day 66's briefing is not in archive (only 64, 65, 67) and the quote was synthesized from tracker entry. Rewrote as own analytical line anchored to tracker-verified events (April 7 ceasefire, post-announcement UAE/Kuwait launches, ADNOC tanker hit). Added a formal rule to SKILL.md Step 5 banning this failure mode for future sessions.

**Version control — ~/.claude and ~/bin under git.** Question raised: should the user-level config and scripts be versioned? Yes — cowork was already covered via Pi 3am backup, but ~/.claude/ (skills, settings, slash commands, engineering standards) and ~/bin/ (alert helpers, briefing renderer, RSS ingest, etc.) had zero version control. Ran security sweep using grep -lr (file paths only, no content echo) for webhook URLs, API key patterns, secret-keyword assignments, bearer tokens, URL-embedded credentials, suspicious filenames. Clean across both directories. The lone suspect (store-slack-bot-token.sh) was verified as a setup helper (osascript dialog → keychain). Wrote tight .gitignore files (excluding 54MB projects/ auto-memory, telemetry, caches, session state, runtime artifacts, Python bytecode, **/state/ dirs). Initialized both repos with git init -b main. Created private GitHub repos BryanDuplantis/claude-config and BryanDuplantis/bin via gh repo create --private. Pushed initial commits with detailed messages. Both live and tracking origin/main.

---

## Shipped (Session 2)

| Artifact | Path | Purpose |
|---|---|---|
| discord-alert.sh | ~/bin/discord-alert.sh (Mac + Pi) | Discord webhook alerter mirroring slack-alert.sh contract: §8 rc=0 short-circuit, embeds with exit code + host + optional stderr tail. Requires jq. GNU stat-c first, BSD stat-f fallback. Validated by Discord-side smoke test on both machines. |
| save-webhook.sh | ~/bin/save-webhook.sh (Mac + Pi) | Webhook URL capture helper: read -rs from stdin (no echo, no shell history), writes 0600 file at ~/.config/discord-webhook-<channel>, validates content against canonical Discord URL regex before exit. Built to break the chicken-and-egg clipboard-pollution failure mode that bit three prior capture attempts. |
| Discord server "Bryan Agent Ops" + 2 webhooks | discord.com (private server) | #cron-alerts channel with "Cron Alerts" webhook (Mac origin) and "Pi Cron Alerts" webhook (Pi origin). Author-name disambiguation gives visible per-machine labeling beyond just the host field. |
| ~/.config/discord-webhook-cron-alerts | Mac + Pi | 121-byte canonical URLs, 0600 perms, captured via stdin (no echo). Separate URLs per machine. |
| jq install on Pi | Pi /usr/bin/jq | Was missing pre-deploy. Installed via passwordless sudo apt. |
| jukebox-sync.sh parallel-fire wiring | /home/brydup/jukebox-sync.sh (Pi) | Alert function pulling from both Slack and Discord helpers, called from trap ERR. .bak-pre-discord rollback artifact preserved. |
| cowork-backup.sh parallel-fire wiring | /home/brydup/cowork-backup.sh (Pi) | Same pattern as jukebox-sync — second cron in the parallel-fire pilot. Different failure path (git-push at 3am vs rclone at 4am) means tomorrow morning surfaces two independent test windows. |
| visual_identity.md | ~/cowork/02_fog_of_war/fog_of_war_resources/visual_identity.md | Visual identity baseline for FoWR designed outputs: color tokens, typography pairing (Geist + Geist Mono), type scale, stroke/spacing, component conventions (header block, ⚡ SIGNAL pill, confidence chips, source links, deep-dive menu). Anchor doc for all future design work. |
| briefing-template.html | ~/cowork/02_fog_of_war/fog_of_war_resources/briefing-template.html | Page shell with embedded CSS + 8 token placeholders ({{day_counter}}, {{date_iso}}, {{time_utc}}, {{sources_summary}}, {{editorial_state}}, {{orientation}}, {{briefing_title}}, {{body_html}}). Consumed by the converter. |
| fowr-md-to-html.py | ~/bin/fowr-md-to-html.py | Python converter: filename parsing (FoWR_DayNN_YYYY_MM_DD.md → day + date), optional YAML frontmatter parsing, H1/H2/orientation extraction, body Markdown → HTML via python-markdown, FoWR-specific post-processing (chips, SIGNAL pills, bottom-line class, disclaimer wrap, deep-dive menu, hr suppression), template substitution. Tolerant of missing structural elements. |
| Mockups | ~/cowork/02_fog_of_war/fog_of_war_resources/mockups/{header-block-v1,briefing-mockup-v1}.html | Standalone HTML mockups for taste calibration before pipeline wiring. Geist font swap was tested here first. |
| SKILL.md Step 6 update | ~/.claude/skills/fowr-full-briefing/SKILL.md | Step 6 now writes .md AND renders .html via the converter; Step 8 handoff block reports both files. YAML frontmatter convention documented for optional header-block metadata override. |
| SKILL.md Step 5 failure-mode rule | ~/.claude/skills/fowr-full-briefing/SKILL.md | New rule: never fabricate prior-briefing quote attributions. Either read the briefing in-context or rewrite as own analytical line anchored to tracker events. Includes the Day 80 incident as canonical failure example. |
| Day 80 FoWR briefing | ~/cowork/02_fog_of_war/FoWR_Day80_2026_05_18.{md,html} | First briefing through the new pipeline. ~6,085 words, 5 sections, 14 confidence chips, 12 deep-dive items, MASTER BLUF with labeled tracks, MASTER BOTTOM LINE, disclaimer block, Day 80 anchor + Substack-ready prose. editorial_state: Draft pending Bryan's review. |
| fowr_tracker.md update | ~/cowork/02_fog_of_war/fowr_tracker.md | First live update through the file as canonical (post-reconstruction). 5 new locked baselines (Barakah strike, amended 14-point, US standing demands, IEA cumulative loss, US troop reduction from Germany), 1 TRIGGERED item (Iranian nuclear ultimatum response → resolved by amended 14-pt), 6 new open watch items, Lebanon item updated with Day 80 ground-strain note, Day 80 added to anchor reference. |
| Private GitHub repo claude-config | github.com/BryanDuplantis/claude-config (private) | 23 files: CLAUDE.md, STYLE.md, settings.json, settings.local.json, skills/, commands/, scripts/, session-log-archive.md, .gitignore. Excluded: 54MB auto-memory (projects/), 4MB telemetry/plugins, image-cache, paste-cache, file-history, sessions/, shell-snapshots, history.jsonl, runtime caches. |
| Private GitHub repo bin | github.com/BryanDuplantis/bin (private) | 12 files: discord-alert.sh, save-webhook.sh, fowr-md-to-html.py, fowr-rss-ingest, store-slack-bot-token.sh, sync-cowork-to-pi.sh, tech-brief-to-html, claude-snapshot, claude-workspace, olleewatch, yt-transcribe.py, .gitignore. Excluded: __pycache__, *.pyc, **/state/, logs, OS artifacts. |
| This tech-brief append | ~/cowork/2026-05-18-tech-brief.md (this file) | Session 2 arc + shipped artifacts + decisions + incidents + verification + open items. Closes out the day's synthesis. |

---

## Cron schedule changes (Session 2)

No new cron entries. Existing Pi crontab unchanged. Two existing wrappers gained parallel-fire wiring:
- 0 3 * * * /home/brydup/cowork-backup.sh (3am — now alerts to both Slack #cron-alerts AND Discord #cron-alerts on rclone-or-git failure)
- 0 4 * * * /home/brydup/jukebox-sync.sh (4am — same parallel-fire pattern)

Both wrappers preserve .bak-pre-discord rollback artifacts. Tomorrow morning is the first natural test window for both — expect either silence (success path) or duplicated alerts (failure path, both surfaces fire).

---

## Decisions made (Session 2)

- **Discord over Slack and Telegram** for full agent-ops surface. Reasoning: Slack free tier's 90-day retention paywall makes it bad as canonical ops log; Discord has bot-UX parity (slash commands, buttons, modals, threads) with no retention cost; server model maps cleanly to workstation taxonomy; indie-dev ecosystem fit. Telegram beats Discord on raw iOS push but threading is weak — deferred as future split if mobile reach becomes dominant pain.
- **Separate webhook per origin** (Mac vs Pi) rather than shared. Costs ~60s of clicking; gains visible author-name disambiguation on every alert. Worth it for infra whose whole job is telling Bryan what failed.
- **save-webhook.sh as stable helper** rather than ad-hoc one-liners. Three capture attempts failed to terminal-wrap + clipboard-pollution issues; dedicated script eliminates the failure class.
- **HTML becomes canonical for FoWR briefings**; .md stays as Substack-paste sidecar. Markdown is the working format; HTML is the published artifact.
- **Visual identity DNA**: bone/ivory + charcoal + oxblood (single accent used surgically), mono+sans pairing only (no serifs), Tufte-discipline (no shadows/gradients/animation), single 1.5x type scale.
- **Geist + Geist Mono** font system over Inter + IBM Plex Mono. Current font-trend pick per May 2026 design publications. Sibling pair from Vercel (2024), matched x-heights/weights, free on Google Fonts.
- **Renderer architecture: skill drafts Markdown, build script handles MD→HTML conversion** with FoWR-specific post-processing. Keeps skill prose light, lets design iterate independently of skill, and preserves both .md and .html outputs.
- **Optional YAML frontmatter** for header-block metadata (time_utc, sources_summary, editorial_state) — tolerant defaults if absent. Frontmatter stripped before body rendering so it doesn't leak into either output.
- **GNU stat -c before BSD stat -f** in dual-platform perm-check probe. Reverse order silently succeeds-with-garbage on Linux.
- **§8-style failure-mode rule added to SKILL.md**: never fabricate prior-briefing quote attributions. Either read the briefing in-context (archive paths follow ~/cowork/02_fog_of_war/FoWR_DayNN_YYYY_MM_DD.md) or rewrite as own analytical line anchored to tracker-verified events.
- **Pi-side ~/bin and Mac-side ~/.claude need version control**. Two new private GitHub repos: BryanDuplantis/claude-config and BryanDuplantis/bin. Cowork already covered via existing 3am Pi backup. Strict .gitignore excludes auto-memory, caches, session state, runtime artifacts.
- **Substack-format target deprioritized**. HTML-canonical means Substack becomes one consumer of many possible (copy-paste into editor for the subset that survives sanitization), not the canonical format constraint.
- **Phase 1 step 5 + cowork-backup wired** as the parallel-fire pilot. Two crons covering two different failure paths is better verification surface than just one.

---

## Incidents (Session 2 — all recovered)

- **Clipboard pollution during first Mac webhook capture** (pre-helper-script). Pasting my pbpaste command from chat to terminal *was* the clipboard write; pbpaste then read its own command text back into the file (223 bytes of bash). Diagnostic: structural counts (slashes, ampersands, dots, whitespace) of the file content without leaking; revealed the file contained the command, not a URL. Fix: built save-webhook.sh to break the chicken-and-egg cycle via read -rs from stdin.
- **Terminal wrap fractured && chains** on three separate one-liner attempts. Bryan's terminal wrapped at ~100 cols, splitting && chains across literal newlines so the shell saw them as separate commands. chmod got no arg, ls -la ran with no path (dumped entire home dir), and the webhook file path got treated as an executable command ("permission denied"). Fix: helper script with a short invocation that doesn't wrap.
- **Cross-platform stat bug surfaced on first Pi smoke test**. Mac-side script worked fine (BSD stat -f); Pi failed loudly because GNU stat -f means "filesystem-mode" and doesn't error like BSD's unknown flag. Error message embedded multi-line filesystem dump inside the perm-check failure message — distinctive enough to spot immediately. Fix: reverse stat order (GNU first, BSD fallback).
- **Recursive SSH attempted by accident**. Bryan was already SSH'd to Pi when I gave him an ssh brydup@brain-mcp.local command intended for Mac terminal. Result: Pi attempted to SSH to itself, hit host-key-not-known prompt. Aborted before any nested-ssh weirdness; ran script directly on Pi instead.
- **Wrong webhook URL captured on Pi initially**. Bryan SSH'd to Pi without re-copying the second webhook URL — clipboard still held the first webhook from Mac capture. Smoke test landed but author was "Cron Alerts" instead of "Pi Cron Alerts" (host: brain-mcp confirmed Pi-origin, so disambiguation still worked via host field). Recovery: re-copy correct URL, re-run save-webhook.sh on Pi, re-smoke.
- **Substack does NOT support inline SVG** — verified via WebSearch on official Substack support docs. Supported image formats are avif, gif, jpg, jpeg, png, webp. SVG explicitly absent. No custom HTML/CSS in the post editor. Invalidated the "SVG inline, no new infra" path in the original FoWR design idea. Captured as a verification correction note (2026-05-18-note-substack-svg-verification-result). Bryan then dropped the Substack constraint entirely, making the SVG path viable for an HTML-canonical workflow.
- **Recap-vs-delta failure mode in Day 80 Section 4**. I padded the Trump-Merz block with April 28 baseline content instead of digging for the May 17-18 deltas. Bryan caught it: "what's new in the trump merz beef? i didn't see a new update; felt like a recap." WebSearched, found the real Day 80 deltas (Merz "humiliated" upgrade to German students, Trump's "nuclear weapon endorsement" Truth Social rejoinder, ~5,000 troop cut operationalization), rewrote Section 4 with deltas, updated SIGNAL lede + Master Bottom Line, added US-troop-cut as Day 80 locked baseline in tracker. Briefing_style rule "Density is acceptable; recap is not" was being violated. Default behavior going forward: "what's new in the last 24 hours" before reaching for prior-baseline scaffolding.
- **Fabricated continuity quote in Day 80 Section 1**. Attributed a quoted framing to "the Day 66 briefing held that 'Iran calibrates each engagement to land below whatever the U.S. ambiguous threshold is...'" — but Day 66 is not in the archive (only Days 64, 65, 67 are). Synthesized plausible-sounding quote from the tracker entry → false attribution. Bryan caught it after I self-flagged during the source-pile audit. Rewrote as own analytical line anchored to tracker-verified events (April 7 ceasefire, post-announcement UAE/Kuwait launches, ADNOC tanker hit, Fujairah fire). Formalized as a SKILL.md Step 5 failure-mode rule to prevent recurrence.
- **Agent tool dispatches required per-call approval** in the current harness when 6 parallel verification subagents were dispatched. Bryan rejected the approvals: "no approvals necessary to connect to internet resources. pls continuye." Switched to direct WebSearch in parallel batches (2 sets of 5) as cleaner verification workflow. Result was actually faster than the subagent route would have been.
- **stat -f on GNU produced filesystem-info dump captured as "perms" value**, which then failed the [[ "$PERMS" == "600" ]] comparison with a confusingly multi-line error message. Diagnostic was instant because the error message embedded the entire stat -f filesystem dump. Fix: reverse stat order, GNU stat -c first.

---

## Gotchas added (Session 2 — six new §8 candidates)

These are accumulated for the next CLAUDE.md compression pass. None landed yet in §8 — Bryan can apply as a grouped edit.

1. **Pasting a command from chat to terminal IS a clipboard write.** Any subsequent pbpaste in the command's execution reads the command's own text. For credential capture, never combine "copy command from chat" with "pbpaste in command" — use a stable helper script with read -rs instead.
2. **Terminal wrap fractures && chains** when commands exceed terminal width (~100 cols here). Shell sees each wrapped line as a separate command. For multi-step chains involving secrets, use a script not a one-liner.
3. **stat -f on GNU coreutils does NOT error** the way -c does on BSD. The dual-platform pattern stat -X first 2>/dev/null || stat -Y file only fails-over cleanly if the first form errors on the wrong platform. Order matters: GNU stat -c first, BSD stat -f fallback. Reverse fails silently with filesystem-info dump captured as the "perms" value.
4. **Discord webhook author name is a free differentiator** for multi-origin alerting. One webhook per machine costs minutes to set up but makes Mac-vs-Pi origin visible in the channel without reading embed fields.
5. **Recap-vs-delta failure mode in briefing drafting**. When a story thread carries forward from prior briefings, default to "what's new in the last 24 hours" before reaching for prior-baseline scaffolding. Briefing_style rule is explicit: "Density is acceptable; recap is not." Failure mode: padding a section with April content when May 18 has its own deltas.
6. **Fabricated-continuity-quote failure mode**. When invoking continuity from prior briefings, the rule is: either (a) actually have the prior briefing in the read context and quote it verbatim, or (b) rewrite as own analytical line anchored to specific events from the tracker baseline. Tracker = persistence layer; prior briefings off the tracker are unread context. Failure mode now formalized in ~/.claude/skills/fowr-full-briefing/SKILL.md Step 5.

---

## Verification matrix (Session 2)

| Surface / behavior | State | How verified |
|---|---|---|
| discord-alert.sh (Mac + Pi) — rc=0 short-circuit, no false alerts | ✓ tested | Direct Discord-side eyeball + script exit codes |
| discord-alert.sh (Mac + Pi) — rc=1 fires red embed | ✓ tested | Smoke test posts visible in #cron-alerts (3 from Mac, 2 from Pi during debugging) |
| save-webhook.sh — perm enforcement, regex validation | ✓ tested | Live capture on both machines + intentional fail tests |
| jq install on Pi | ✓ tested | apt install + which jq confirmation |
| Pi-side ConnectTimeout + pre-flight ping pattern | ✓ tested | Used throughout the session; no hangs |
| jukebox-sync.sh parallel-fire wiring | ✓ syntax-tested; ⏸ live-fire wait-for-event | bash -n post-swap + diff verification + .bak-pre-discord rollback artifact preserved. First real test: 4am tomorrow. |
| cowork-backup.sh parallel-fire wiring | ✓ syntax-tested; ⏸ live-fire wait-for-event | Same pattern as jukebox-sync. First real test: 3am tomorrow. |
| fowr-md-to-html.py converter — Day 67 dry-run | ✓ tested | Real Day 67 briefing rendered successfully: 6 SIGNAL pills, 15 confidence chips, 13 deep-dive items, 6 bottom lines |
| fowr-md-to-html.py converter — Day 80 live | ✓ tested | Day 80 briefing rendered successfully: 6 SIGNAL pills, 14 confidence chips, 12 deep-dive items, 5 bottom lines, 1 disclaimer block. HTML opens cleanly in browser. |
| visual_identity.md baseline | ✓ documented; ⊘ no automated test | Reference doc, not executable. Bryan reviewed during the iteration cycle. |
| Geist + Geist Mono font swap | ✓ tested | Visual diff in browser confirmed correct family loading; no fallback chain activated |
| ~/.claude/.gitignore filtering — only 23 load-bearing files staged | ✓ tested | git add -An dry-run output verified before commit |
| ~/bin/.gitignore filtering — only 12 files (no __pycache__, no state/) | ✓ tested | Same dry-run check; updated gitignore once to exclude **/state/ and __pycache__/ |
| GitHub repos claude-config + bin created + pushed | ✓ tested | Both pushed to origin/main, gh repo create returned URLs |
| Security sweep on ~/.claude and ~/bin | ✓ tested | grep -lr (file paths only, no content echo) for webhook URLs, API keys, secret-keyword assignments, bearer tokens, URL-embedded creds. Zero hits across both directories. |
| Day 80 briefing — content load-bearing claims verified | ✓ tested | 10 parallel WebSearches verified Trump quotes, UAE strike specifics, IEA inventory figures, Iran 14-point amended terms, Iran-Oman talks, cable-toll mechanics, Pakistan mediation limits, Trump-Merz Monday escalation, UN-verified executions, Saudi-Iran call (last one MEDIUM confidence, NEEDS URL flag remains for Saudi-side English-wire) |
| Day 80 briefing — Trump-Merz recap-vs-delta fix applied | ✓ tested | Bryan-reviewed correction; Section 4 SIGNAL + Master Bottom Line updated; tracker baseline for US-troop-cut added |
| Day 80 briefing — fabricated-Day-66-quote fix applied | ✓ tested | Section 1 rewritten as own analytical line anchored to tracker events; SKILL.md Step 5 rule added |
| fowr_tracker.md — Day 80 baselines locked, watch items added, TRIGGERED moves | ✓ tested | 5 new baselines + 6 new watch items + 1 TRIGGERED resolution committed |
| editorial_state: DraftReleased flip (post-publish) | ⏸ wait-for-event | Bryan-side review + frontmatter edit + re-render — manual workflow until Phase 4 |
| Substack publish of Day 80 .md | ⏸ wait-for-event | Manual paste at bryanduplantis.substack.com section "Fog of War Room" (ID 371665) |

---

## Open items (Session 2)

- **Tomorrow 3am cowork-backup + 4am jukebox-sync** — first natural test window for Discord parallel-fire pilot. Expect silence in success path, duplicated alerts in both #cron-alerts (Slack + Discord) on failure. Either outcome closes one verification loop.
- **Phase 1 step 6 — one-week parallel-fire window** — sign-off criterion: zero Discord misses across N=∼14 cron firings (7 days × 2 crons). After clean window, proceed to step 7 cutover.
- **Phase 1 step 7 — cutover**: flip all wrappers to discord-alert.sh only, rename ~/bin/slack-alert.sh.deprecated on Pi, update ~/.claude/CLAUDE.md reference table.
- **FoWR Day 80 review + publish**: Bryan reviews .md/.html, flips frontmatter editorial_state to "Released", re-renders via python3 ~/bin/fowr-md-to-html.py ~/cowork/02_fog_of_war/FoWR_Day80_2026_05_18.md, then pastes the .md into Substack.
- **2 NEEDS URL flags in Day 80 .md** — both about Saudi-side English-wire confirmation of the May 18 Iran-Saudi FM phone call. Currently only IRNA/Mehr have reported it; if Saudi side surfaces a readout, resolve.
- **Day 80 watch items** (added to tracker today): Barakah strike attribution decision by UAE; US response to amended 14-point package (next 72h); Iran-Oman mechanism finalization; cable-toll concept progression from Tasnim trial-balloon; IEA Hormuz resumption assumption tracking in June OMR; carrier rotation in CENTCOM (Ford home → replacement?).
- **§8 CLAUDE.md grouped update** — six new gotchas accumulated today across both sessions (clipboard pollution, terminal wrap, stat portability, Discord webhook author labeling, recap-vs-delta drafting, fabricated continuity quotes). Worth a single grouped edit during the next CLAUDE.md compression pass.
- **Pi-side ~/.claude and ~/bin convergence** — clone the new repos to Pi, have Pi push its additions (slack-alert.sh on Pi, pi-cron-alerts webhook config, etc.) to the same repos. Cross-machine git convergence replaces per-file scp for these dirs.
- **Optional auto-commit cron for ~/.claude** — mirror cowork-backup.sh pattern: nightly commit + push if changes detected. Means Bryan never has to manually commit a skill edit again.
- **READMEs for claude-config and bin repos** — not load-bearing but would make them self-explanatory in the GitHub web UI.
- **~/.claude/CLAUDE.md size** — 197 lines pre-session; not edited in Session 2 directly, but the six new gotchas accumulated suggest the next compression pass is due. Currently 47 over the 150 cap.
- **Phase 2 — Discord bot for two-way control**: discord.py on Pi via systemd, slash commands (/status, /run, /approve), gateway not interactions endpoint. Half-day separate session when wanted.
- **Three remaining 2026-05-18 morning research threads** — actually all four have now been picked off today: Discord (executed), multi-surface Claude continuity (captured as parked idea), FoWR Claude Design (built and shipped via the HTML pipeline + Day 80 briefing), Ghostty (adopted directly). Tomorrow's tech brief opens fresh on what's next.

---

## Brain references (full day, updated)

- Morning (4 captures): 2026-05-18-session-global-standards-setup-reconstructed, 2026-05-18-session-brain-backfill-pi-hardening, 2026-05-18-session-prompt-alerts-hook-tuning, 2026-05-18-session-claude-code-stop-hook-wired
- Afternoon / Evening (5 captures): 2026-05-18-session-discord-migration-phase-1, 2026-05-18-idea-multi-surface-claude-continuity, 2026-05-18-idea-fowr-designed-outputs-svg-first, 2026-05-18-note-substack-svg-verification-result, 2026-05-18-idea-fowr-field-style-header-block, 2026-05-18-session-fowr-day-80-html-pipeline
- Adjacent yesterday: 2026-05-17-session-global-claudemd-lessons-backfill, 2026-05-17-session-tech-brief-html-pipeline, 2026-05-17-note-yt-week-of-2026-05-17
- Source for §9 5/15 lesson (cross-surface): 2026-05-15-session-atcq-dossier-surface-gap
- Hook-event docs source-of-truth: code.claude.com/docs/en/hooks (after 301 from docs.claude.com)
- GitHub repos created this session: github.com/BryanDuplantis/claude-config (private), github.com/BryanDuplantis/bin (private)
- FoWR archive (today): ~/cowork/02_fog_of_war/FoWR_Day80_2026_05_18.{md,html} + updated fowr_tracker.md
- FoWR resources (created today): ~/cowork/02_fog_of_war/fog_of_war_resources/{visual_identity.md, briefing-template.html, mockups/header-block-v1.html, mockups/briefing-mockup-v1.html}


brydup@mac ~/cowork %