QWU Backoffice User Manual
This is the public, redacted version of the QWU Backoffice User Manual. Sensitive data (IPs, credentials, project IDs, personal names) has been replaced with descriptive placeholders like <VM_IP> or [Member Name]. The structure and educational content are preserved for transparency and Missing Pixel student training.
Generated: 2026-11-05 07:52 | Source version: 7.02
QWU Backoffice User Manual
Version: 6.76 | Started: 251223 | Updated: 261103
A comprehensive guide to the QWU Backoffice agent workspace, covering architecture, daily operations, automation, and development workflows. These notes serve both as operational documentation and educational curriculum for Missing Pixel students.
Table of Contents
- [[#Environment Overview]]
- [[#Getting Started]]
- [[#Daily Operations]]
- [[#One-Tap Mobile VM Control via n8n ⭐]]
- [[#Running Agent Jobs]]
- [[#Overnight/Long-Running Jobs]]
- [[#Mobile Access via Termux (Android) ⭐]]
- [[#Obsidian + GitHub Integration: Unified Workspace]]
- [[#Sparse Checkout: Filtering Files on Azure VM]]
- [[#Syncing Workflow]]
- [[#Claude Code: Modes and Commands]]
- [[#Quick Reference Sheets]]
- [[#DOE Architecture & Skills System]]
- [[#Morning Briefing System ⭐]]
- [[#Daily Summary System ⭐]]
- [[#Google Calendar Integration]]
- [[#Google Docs Sync System]]
- [[#Video Content Pipeline]]
- [[#chaplaintig.com Personal Site]]
- [[#Voice Profiles]]
- [[#Expert Intelligence System ⭐]]
- [[#Wisdom Synthesis System ⭐]]
- [[#Canonical Datetime System]]
- [[#Azure Cost Tracking]]
- [[#Lead Generation System]]
- [[#Communications Architecture]]
- [[#Data Architecture]]
- [[#Project Organization]]
- [[#YAML Frontmatter Standards]]
- [[#Docker Fundamentals: Running Isolated Tasks]]
- [[#Docker Sandbox Security]]
- [[#Meeting Intelligence System ⭐]]
- [[#App Login Email Guard (
audit_qwf_auth_mail.py) ⭐ NEW]] - [[#Outlook Email Processing ⭐]]
- [[#SuiteDash CRM Integration ⭐]]
- [[#Transcript Extraction System (Planned)]]
- [[#Ez/Ezer Mascot]]
- [[#Ez Terminal (Scheduler) ⭐ NEW]]
- [[#Chat Leak Prevention System ⭐ NEW]]
- [[#Troubleshooting]]
- [[#Resources]]
- [[#BNI Member Dossier System]]
- [[#[Networking Chapter] Mentor Program (QNT) ⭐ NEW]]
- [[#BNI Meeting Recap System ⭐ NEW]]
- [[#System Architecture Audit ⭐ NEW]]
- [[#EPIC Appointment Intelligence System v2.0 ⭐ NEW]]
- [[#Ezer Aión Assistant System ⭐ NEW]]
- [[#Strategic Goals Framework ⭐ NEW]]
- [[#QWU Cosmic Style Guide ⭐ NEW]]
- [[#Content Calendar System ⭐ NEW]]
- [[#Daily Journal Command Center ⭐ NEW]]
- [[#Supervisor Observability System (SOS) ⭐ NEW]]
- [[#Relationship Intelligence Layer ⭐ NEW]]
- [[#Parallel Execution System ⭐ NEW]]
- [[#Ezer Universal Interface ⭐ NEW]]
- [[#Project System Status Files ⭐ NEW]]
- [[#Supervisor Architecture]]
- [[#HQ Command Center ⭐ NEW]]
- [[#QWR SEO Intelligence ⭐ NEW]]
- [[#Supporter System Safeguards ⭐ NEW]]
- [[#[Supporter Organization] Command Center ⭐ NEW]]
- [[#Pocket Ez Companion App ⭐ NEW]]
- [[#Public Manual Generation System ⭐ NEW]]
- [[#QWR Audience Intelligence System ⭐ NEW]]
- [[#QKN Quietly Knocking ⭐ NEW]]
- [[#QSP Quietly Spotting ⭐ NEW]]
- [[#QNT Quietly Networking ⭐ NEW]]
- [[#QWR Content Performance Intelligence ⭐ NEW]]
- [[#QWR Press Release Service ⭐ NEW]]
- [[#Cost Intelligence System ⭐ NEW]]
- [[#QWR Reverse Benchmarking Intelligence ⭐ NEW]]
- [[#QWR Content Strategy System ⭐ NEW]]
- [[#QWR Preparation Workbook ⭐ NEW]]
- [[#QWF Ecosystem Landing Section ⭐ NEW]]
- [[#Auto-Remediation System ⭐ NEW]]
- [[#QTR Quietly Tracking ⭐ NEW]]
- [[#QWF Ecosystem Widget ⭐ NEW]]
- [[#QWR Team Accounts System ⭐ NEW]]
- [[#QWF Documentation Standard ⭐ NEW]]
- [[#Weavy Creative Production System ⭐ NEW]]
- [[#WHL WHELHO App ⭐ NEW]]
- [[#Cloudflare & DNS Management ⭐ NEW]]
- [[#Puzzle Operations Blueprint ⭐ NEW]]
- [[#Trails AI SOP + Video Pair ⭐ NEW]]
- [[#QWF App Registry ⭐ NEW]]
- [[#Testimonial Intelligence Pipeline ⭐ NEW]]
- [[#QSP Local Growth Engine ⭐ NEW]]
- [[#TWL Preload Hook ⭐ NEW]]
- [[#HQ Issues Preload Hook ⭐ NEW]]
- [[#Date/Time Output Validation Hook ⭐ NEW]]
- [[#QWB Quietly Webbing ⭐ NEW]]
- [[#L4G Locals 4 Good Platform ⭐ NEW]]
- [[#Living Roadmap Delivery Pipeline ⭐ NEW]]
- [[#WP Legacy Sites on Cloudflare Pages ⭐ NEW]]
- [[#Security Plan and Guards ⭐ NEW]]
- [[#Session Log]]
Environment Overview
What is the QWU Backoffice?
The QWU Backoffice is an AI agent workspace running on Microsoft Azure, designed to automate operations for The Quietly Working Foundation. It provides a secure, sandboxed environment where Claude Code agents can execute tasks, process data, and manage workflows.
Architecture: Azure VM + Docker + VS Code Remote
┌─────────────────────────────────────────────────────────────────┐
│ YOUR DEVICES │
├─────────────────────────────────────────────────────────────────┤
│ Windows PC │ Android Phone │ Any Browser │
│ VS Code + SSH │ Termux + SSH │ Azure Portal │
└──────────┬───────────┴──────────┬───────────┴─────────┬─────────┘
│ │ │
└──────────────────────┼─────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ AZURE CLOUD │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Ubuntu VM (claude-dev-vm) │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ Docker │ │ qwu_backOffice/ │ │ │
│ │ │ (isolated │ │ ├── .claude/ │ │ │
│ │ │ containers) │ │ ├── 005 Operations/ │ │ │
│ │ └─────────────────┘ │ │ ├── Directives/ │ │ │
│ │ │ │ └── Execution/ │ │ │
│ │ │ └── [obsidian vault] │ │ │
│ │ ┌─────────────────┐ └─────────────────────────────┘ │ │
│ │ │ Claude Code │ │ │
│ │ │ (AI agent) │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
VM Details
| Property | claude-dev-vm | qwu-n8n |
|---|---|---|
| Resource Group | <RESOURCE_GROUP> | <RESOURCE_GROUP> |
| Region | West US 2 | West US 2 |
| Size | Standard E8as_v7 (8 vCPUs, 64 GB RAM, 256 GiB NVMe Premium ZRS; since 2026-10-29, was D8as_v6 with 32 GB and 128 GiB) + 128 GB Standard SSD storage disk at /mnt/storage (added 2026-10-11, $9.60/mo) | Standard B2s (2 vCPUs, 4 GB RAM, ~$30/mo) |
| IP | <VM_IP_CLAUDE_DEV> (static) | <VM_IP_N8N> (static) |
| Image | Ubuntu Server 24.04 LTS | Ubuntu Server 24.04 LTS |
| Swap | 4 GB | 6 GB (2GB + 4GB added 2026-02-23) |
| Operation Mode | 24/7 always-on | 24/7 always-on |
| Purpose | Backoffice agent workspace | n8n workflow engine + Caddy + Postgres |
Infrastructure Monitoring & Backups
The QWU infrastructure is monitored at three layers, all documented in 005 Operations/Directives/infrastructure_monitoring.md.
External Monitoring (Betterstack)
- Plan: AppSumo lifetime deal (2 stacked codes) ... 200 monitors, 10 status pages, 5 members
- 23 HTTP monitors (updated 2026-04-12): 10 WPMU sites, 6 QWF apps (QNT, QQT, QKN, QSP, L4G, QTR), 5 infrastructure services, QWR app, [Supporter Organization] supporter site
- 4 heartbeat monitors: claude-dev, n8n, WPMU VM, [Supporter Organization] VM (all every 6 hours)
- Alerts: phone call + SMS + email + push notification
- Outgoing webhook (ID 80218) fires on incident events → triggers auto-remediation (see [[#Auto-Remediation System ⭐ NEW]])
- Status page: status.quietlyworking.org ... single section "QWU Infrastructure" (all QWF sites + apps + infra + heartbeats). Supporter monitors ([Supporter Organization]) run internally for alerting but are not displayed on the public status page ... supporters access their status via SuiteDash portal embeds.
- WPMU and [Supporter Organization] Lightsail CPU alarms deleted (2026-03-11) ... they flapped due to WP-Cron bursts and xmlrpc attacks; Betterstack uptime monitoring is more useful
Internal Health Checks
check_vm_health.pyruns every 6 hours via cron on both VMs- Collects: disk, memory, load, Docker containers, n8n workflow failures
- Posts health embed to Discord
#system-status - Always pings Betterstack heartbeat regardless of health status (v1.1.0). The heartbeat proves "VM is alive and monitoring is running." Service-level issues are reported via Discord alerts separately. A missing heartbeat should only mean "VM is unreachable."
- Thresholds: 80% warning, 90% critical for disk/memory
HQ header strip: disks, containers and space (updated 2026-10-27, Session 827; resize done 2026-10-29)
report_vm_health.py(every 5 minutes) feeds the strip. Every disk of 10GB or more has its own daily trend line and its own warning, which now comes at 45 days to full (was 30), so a warning always leaves more than a month to act. A one-time gain bigger than a tenth of a disk (data moved off, a big clean-up, a resize) is taken out before the trend is fitted; before this, the Oct 11 storage-disk move made a filling disk read "flat or recovering".- A stopped Docker container turns a machine red only when its restart policy says it should be running. A throwaway test database (restart "no") shows as a grey chip.
- The weekly Sunday trim (
prune_disk_caches.py) clears an app copy's downloaded libraries after 7 idle days, and removes a whole app copy (agent-lane worktrees included) after 30 idle days, only when every byte of it is on GitHub and no note names it. - qwu-n8n: an n8n upgrade now removes old versions itself (keeping the rollback one), and its system log is capped at 500MB.
- Done 2026-10-29: TIG grew the main disk from 128GB to 256GB (+$24.96 a month) and moved the machine to Standard_E8as_v7 (8 vCPUs, 64 GB, +$83.22 a month) from Azure Cloud Shell, walked through by his desktop Claude (
002 Projects/_QWU Backoffice Automation/Kickoff Prompts/claude-dev-disk-resize-walkthrough-kickoff-2026-10-27.md), and raised the interactive memory limit from 26G to 56G. Azure grows a system disk only while the machine is stopped, so it was off for a few minutes. Afterwards/read 247G with 162G free, the strip took the gain out of the trend as a one-off and read 103 days to full, and/pulseread green. The night's safety snapshot and three older ones (two of this disk, one of qwu-n8n) were deleted on 2026-10-31, after a full day of normal running and a clean first weekly restart on the new setup (the memory limit still read 56G). No snapshot of either machine is left; the snapshot cost line reads $0. - The cost checks know when the setup changed (2026-10-29):
cost_constants.AZURE_VM_MONTHLY_SINCEnames the day. The daily drift check skips that day's bill and the next one, and the month-to-date and invoice checks report the month it landed in as "not compared", because those bills still hold the old machine. Without it the morning after a resize recommends putting the old price back. A typo in the date never switches a check off: an unreadable or future date reads as no date. - If VS Code shows "reconnecting" while the machine is healthy, try Reload Window first. On 2026-10-27 a full stop and start was used for what turned out to be a client-side break: the machine's logs showed it idle and healthy throughout.
- Systemd services monitored:
sms-webhook,digital-twin,qnt-webhook,caddy
claude-dev Disk Layout & Weekly Upkeep ⭐ NEW (2026-10-11)
- Two disks. The fast NVMe OS disk holds everything live: the vault, live SQLite, Docker, sessions, app copies. The cheaper storage disk (
claude-dev-vm-storage-1, E10, mounted at/mnt/storageby UUID withnofail) holds cold data behind symlinks at the old paths:~/backups(website media mirror, website DB dumps, SQLite spares),~/call_intel/archive,~/call_intel/inbox/drive_exports. No script path changed.df /no longer counts that data. backup_wpmu.shv1.1.0 measures free space on the disk the mirror lives on, in MB (df -BGrounds up and hid a sub-15 GB reading).- Sundays 03:30 Pacific:
tidy_vault_git.shpacks the vault's loose git objects with a capped, non-aggressivegit gc(gc.autostays 0; auto-gc mid-session froze the VM twice in July). First run:.git4.8 GB to 958 MB in 3 minutes. - Sundays 03:40 Pacific:
prune_disk_caches.py --applyremoves only what rebuilds itself: the npm download cache, superseded VS Code server builds, vault.tmp/entries untouched 30 days, andnode_modulesof app copies untouched 30 days. Never logs, backups, test fixtures, anything the vault or a script names, anything a running process uses, or Docker. Dry run is the default; every deleted path is logged to.tmp/logs/disk_prune.log. If an old app copy says a module is missing, runnpm install(ornpm ci) in it. - Result 2026-10-11:
/went from 88% / 16 GB free to 60% / 50 GB free. Before this,/grew a steady 8-9 GB a month, mostly new app working copies (~0.4 GB each) and regrowing caches.
MP Training Opportunities (from the 2026-10-28 resize-night pre-checks, Session 834)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Answer a runbook's pre-checks from the live system, read-only | The desktop Claude asked three questions before a change that stops the machine. Each answer came from a read-only command against the machine or Azure, never from memory or old notes. Teaches evidence before an action you cannot undo | Intermediate |
| A past event can be your rehearsal | "Will the memory limit exist before anyone logs in?" was answered by the boot log of the previous restart, which showed it created before the first login. Teaches reading what already happened instead of predicting | Intermediate |
| Check the parachute before you jump | A disk restored from a snapshot must carry the same disk-controller tag as the original, or it cannot boot on the new size. The runbook now reads the restored disk back before swapping it in. Teaches verifying a rollback path, not just having one | Advanced |
| Reconcile what you pay for against what you think you pay for | Three old safety snapshots were still billing while the cost record counted one. Teaches inventory-versus-ledger checks | Beginner |
| A cascade that carries numbers may not carry names | The public cost page takes its prices from the cost file but types the machine names itself, so a resize would have shown new prices beside old names. Teaches tracing every reader of a change, not only the documented ones | Intermediate |
Most valuable MP exercise: give a student a resize runbook and read-only access to a practice subscription, and ask them to answer three pre-checks with commands, quoting the output line that proves each answer.
MP Training Opportunities (from QCM's second locks and consent repairs, 2026-11-03 and 04, Session 903)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Break your own change on purpose | Each broken copy of a migration has to fail its rehearsal, or the rehearsal proves nothing. 27 of 27 were caught, and that is what makes REHEARSAL_OK mean something. Teaches testing the test. | Intermediate |
| Ask the reader's question, not the writer's | A consent page read back "stopped" while the mail kept going: the writer was right and the sender read a different record. Teaches proving a change from where it is used. | Intermediate |
| Hand your work to a reviewer who starts cold | A first review found two real faults the author's own rehearsal had passed; a second, on the fixes, found nothing serious. Teaches treating review as a tool, and arguing with the code rather than the reviewer. | Beginner |
| Commit only your own lines | On a shared working copy, another person's unsaved lines sit in the same file. Teaches git's index, and why git add -A is a hazard. | Advanced |
| Make the alarm reach a person, then watch the alarm | A nightly check whose failure nobody saw ran for months. Pushing it to a phone, with a monitor behind it, teaches that an alert only counts once it is seen. | Intermediate |
Most valuable MP exercise: give a student a small database function and its test, and ask them to write three broken versions the test would miss, then fix the test.
MP Training Opportunities (from the L4G artwork choice walk, 2026-11-04, Session 901)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Walk a path as a first-time person, then say what happens in plain words | Pressing each button on a page after paying showed a choice that saved nothing and a "Welcome Back" sign-in for someone who had never signed in. Reading the code named the same facts, and walking it showed how they feel. Teaches testing from the person's side | Beginner |
| Test a signed-in page with stand-in data | The preview talks to the real database, so a real form press would hold a real space. A private copy with made-up data ran the real page safely. Teaches knowing what a test environment is really connected to | Intermediate |
| Trace every reader before turning a value on | A field empty for months had five readers waiting. One would have sent 12 emails to a 12-month donor-partner, because it counts rows, not payments. Teaches asking "what wakes up when this stops being empty?" | Intermediate |
| Ask one reader without showing your answer | A second reader, given only the facts, agreed on the main call and disagreed on one point worth weighing. Teaches getting a view your own answer has not shaped | Beginner |
| Look at the page at both widths before trusting the order | On a phone the question comes right before the pay button. On a computer the pay button stays in view and the question can sit below the fold, so a person could pay without seeing it. Teaches that layout changes what a person reads | Beginner |
Most valuable MP exercise: give a student a live flow and a made-up account, and ask them to write what happens in five plain sentences, then name one promise on the page that nothing keeps.
MP Training Opportunities (from a presentation app ship and a document comparison, 2026-11-04, Session 899)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Is the app wrong, or the check? | A browser check failed mid-ship on a page nobody had changed. Pressing one button at a time, slowly, about 1,400 times, then logging presses against page turns, showed the app right and the check racing ahead of it. Teaches testing the fault before blaming the test, and never re-running until green | Intermediate |
| Prove a check can still fail | After the fix, a walk forced to stop short had to turn the check red. Teaches that a check nobody has seen fail proves little | Beginner |
| Search with the other side's words | A search for "deposit" reported that an app had none; the app's word was "down payment". A price list looked empty for a product filed under its category, not its item names. Teaches that a claim of absence needs both vocabularies | Beginner |
| Compare documents with citations | Ten product sheets against a price list, live words and a contract, every claim tied to a file and page, then handed to a fresh reader to break. Teaches telling two real answers apart from two wordings of one | Intermediate |
MP Training Opportunities (from the 2026-10-29 resize night and its after-checklist, Session 847)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Verify a change from both sides | The disk, size and memory limit were each read twice: from inside the machine (df, nproc, free, the cgroup's memory.max) and from Azure with the machine's own identity. Teaches that one witness is a claim and two are a check | Beginner |
| Change the baseline and the alarms that read it together | Raising the expected Azure cost the same night would have made the next morning's audit recommend the old price back, because yesterday's bill still held the old machine. A dated "setup changed on" value lets each check skip what it cannot fairly compare. Teaches that a monitor is only as good as the baseline it compares against | Intermediate |
| Find when space went, then what took it | The pulse history pinned a "10 GB in a day" to two hours of one afternoon; a find -newermt window and docker image inspect (LastTagTime) then named the folders and the image. Teaches narrowing by time before searching by size | Intermediate |
| Fail open toward a visible alarm | A typo in the change date reads as "no date", so the checks compare and may raise a false gap someone will look at, instead of going quiet. Teaches choosing which way a safety check fails | Intermediate |
| Know what your check actually reads (added 2026-10-31, Session 860) | Before deleting the last rollback snapshots, the disk-error check came back with two "hits": both were the kernel's own boot settings (nvme_core.io_timeout=240), not errors, and journalctl -k had read only the newest boot, missing the day that mattered. Rerun per boot with the boot line dropped, it was clean. Teaches checking that a check looks where you think, before an action you cannot undo | Intermediate |
| Let someone try to break it | An independent reviewer broke the first draft of the cost-check change four ways (a malformed date silently turned checks off; the 1st of the month read the wrong day). Teaches adversarial review and tests that would fail without the fix | Advanced |
| Dates on a UTC machine | The walkthrough's shell step stamped "2026-10-30" for a 2026-10-29 Pacific change. Teaches TZ=America/Los_Angeles date +%F | Beginner |
Most valuable MP exercise: give a student a cost check that compares bills with a fixed monthly figure, change the figure mid-month, and ask them to predict every false alarm the next 40 days will raise, then write the tests that prove their fix.
Application Health Checks
check_calendar_health.pyruns every 2 hours via cron on claude-dev- Validates HQ Command Center's
google-calendar-eventsSupabase edge function - Auto-redeploys known-good source via Supabase CLI if broken
- Posts to Discord
#system-status(healthy/healed/still-broken)
SEO Data Health Checks ⭐ NEW (2026-05-27)
check_seo_silent_death.pyruns daily at 13:05 PT via cron on claude-dev- Deliberately VM-cron, not n8n ... the alerter must be independent of n8n itself, so an n8n outage can never silence the alarm about the n8n outage
- Three detector families: (1) data freshness ... alerts when QWR/QSP SEO tables (
seo_position_history,seo_citation_tracking,qsp_seo_health,qsp_analytics_traffic) miss their expected cadence; (2) DataForSEO balance floor ... alerts when the account drops below $10; (3) active-but-empty ... flags any n8n workflowactive=Truewhose target table is stale or empty (gated on table freshness so n8n's pruned execution history can't false-positive) - Posts to Discord
#system-statusonly when something trips; self-events land inintegration_events(the alerter is itself observable) - Built after the 2026-05-27 audit found three live silent failures (position tracker dead 11 weeks on a DataForSEO 402, Vista Social gateway NXDOMAIN since March, GA-sync deployed-but-not-feeding). Operationalizes the "active/deployed is NOT producing ... probe the output table" rule (
memory/feedback_active_deployed_not_producing_probe_output.md).
Agent Memory Health Checks
audit_memory_health.pyruns daily at 5 AM PT via cron on claude-dev- Checks: line budget violations (50-line cap per topic file, 180-line MEMORY.md), orphaned/missing index entries, frontmatter integrity, broken vault file references, version claim mismatches, stale date sections (>30 days)
- Dream Score: 0-100 composite across 5 categories (integrity, budget, paths, freshness, index). Healthy >= 80, Warning >= 50, Critical < 50
- Posts to Discord
#system-statusonly on warning/critical (no noise on healthy days) - Safe auto-fix mode (
--fix): adds missing frontmatter, removes trailing whitespace, syncs index. Never deletes content. - Deep analysis: invoke
/dreamskill for Claude-powered intelligent review (contextual staleness, semantic duplication, refactoring recommendations)
Database Backups
- Nightly at 3 AM UTC:
backup_n8n_postgres.shon n8n VM - Local rotation: 7 daily + 4 weekly (~8MB compressed per backup)
- Offsite: uploaded to Azure Blob Storage (
<STORAGE_ACCOUNT>account,n8n-backupscontainer) - Restore:
az storage blob download ... && gunzip -c restore.sql.gz | docker exec -i n8n-postgres psql -U n8n n8n
Getting Started
Accessing the Workspace (VS Code Connection)
- Start the VM (Azure Portal or one-tap mobile script)
- Open VS Code
- Click green button (bottom-left) → Connect to Host →
claude-dev - You're in!
First-Time Setup for New Team Members
- Generate SSH key on your machine
- Send public key to admin
- Admin adds to VM's
~/.ssh/authorized_keys - Configure SSH in
~/.ssh/config:
Host claude-dev
HostName <VM_IP_CLAUDE_DEV>
User <VM_USER>
IdentityFile ~/.ssh/your-key.pem
Understanding the Dev Container
The .devcontainer/ folder contains configuration for isolated development:
Dockerfile- Container image definitiondevcontainer.json- VS Code settingsinit-firewall.sh- Network security rules
"Reopen in Container" ... When to Click vs Dismiss
When you connect via VS Code Remote SSH, you may see a popup asking to "Reopen in Container." Here's when to use each option:
Click "Reopen in Container" when you want to:
- Work inside the sandboxed Claude Code agent environment
- Develop/test in the isolated Docker container
- Run agent operations that need that specific secure environment
Click "Don't Show Again" or dismiss when you want to:
- Work directly on the VM itself
- Access the Obsidian vault, check logs, manage Docker containers from the host
- Run tmux sessions for overnight agent monitoring
- General system administration
Typical daily workflow: For most day-to-day work ... checking on agents, syncing the vault, running n8n-triggered operations ... you probably want to stay on the SSH connection to the VM host rather than reopening inside the container.
Switching between them:
- To enter container: Command Palette (
Ctrl+Shift+P) →Dev Containers: Reopen in Container - To return to VM host: Command Palette →
Remote-SSH: Connect to Host→claude-dev
Daily Operations
Starting Your Work Day
Option 1: One-Tap Mobile (Recommended)
Tap qwu-start.sh widget on phone → auto-connects when ready
Option 2: Manual Start
- Azure Portal → Find
claude-dev-vm - Click Start
- Wait ~60 seconds
- VS Code → Connect to
claude-dev
Stopping the VM When Done
Option 1: One-Tap Mobile
Tap qwu-stop.sh widget
Option 2: From VS Code Terminal
sudo shutdown now
Option 3: Let Auto-Shutdown Handle It VM stops at 7 PM Pacific automatically.
Checking Costs in Azure Portal
- Azure Portal → Cost Management
- Filter by resource group:
<RESOURCE_GROUP> - Review daily/monthly spend
One-Tap Mobile VM Control via n8n ⭐
Start and stop your Azure VM from your phone with a single tap... no Azure Portal needed.
Architecture
Phone (Termux Widget)
→ n8n Webhook (cloud, always on)
→ Azure API (start/stop/status)
→ VM responds
→ Termux polls for SSH
→ Auto-connects when ready
Components
| Component | Purpose |
|---|---|
| Azure Service Principal | Scoped credentials for VM control only |
| n8n Workflow | Handles Azure auth + VM commands |
| Termux Scripts | One-tap triggers + SSH connection |
| Global Variable | Single location for secret rotation |
Azure Service Principal Details
Created via Azure Cloud Shell:
az ad sp create-for-rbac \
--name "n8n-vm-automation" \
--role "Virtual Machine Contributor" \
--scopes "/subscriptions/{sub-id}/resourceGroups/<RESOURCE_GROUP>/providers/Microsoft.Compute/virtualMachines/claude-dev-vm"
| Field | Value |
|---|---|
| Client ID | <AZURE_CLIENT_ID> |
| Client Secret | Stored in n8n Variables |
| Tenant ID | <AZURE_TENANT_ID> |
| Subscription ID | <AZURE_SUBSCRIPTION_ID> |
| Resource Group | <RESOURCE_GROUP> |
| VM Name | claude-dev-vm |
n8n Webhook Endpoints
| Action | URL |
|---|---|
| Start | https://n8n.quietlyworking.org/webhook/vm-control?action=start |
| Stop | https://n8n.quietlyworking.org/webhook/vm-control?action=stop |
| Status | https://n8n.quietlyworking.org/webhook/vm-control?action=status |
Termux Scripts
~/.shortcuts/qwu-start.sh - Start VM + wait + connect:
#!/data/data/com.termux/files/usr/bin/bash
# Version: 1.0.0
# QWU Start + Connect
WEBHOOK_URL="https://n8n.quietlyworking.org/webhook/vm-control?action=start"
VM_IP="<VM_IP_CLAUDE_DEV>"
MAX_ATTEMPTS=24
echo "🚀 Starting QWU Backoffice..."
curl -s "$WEBHOOK_URL"
echo "⏳ Waiting for VM..."
ATTEMPTS=0
until nc -z -w 5 $VM_IP 22 2>/dev/null; do
ATTEMPTS=$((ATTEMPTS + 1))
if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then
echo "❌ VM didn't respond in time"
exit 1
fi
echo " Attempt $ATTEMPTS/$MAX_ATTEMPTS..."
sleep 5
done
echo "✅ Connecting..."
ssh qwu
~/.shortcuts/qwu-stop.sh - Deallocate VM (saves money):
#!/data/data/com.termux/files/usr/bin/bash
# Version: 1.0.0
# QWU Stop
WEBHOOK_URL="https://n8n.quietlyworking.org/webhook/vm-control?action=stop"
echo "🛑 Stopping QWU Backoffice..."
curl -s "$WEBHOOK_URL"
echo "✅ Shutdown initiated"
Setup in Termux
# Create shortcuts directory
mkdir -p ~/.shortcuts
# Create and edit scripts
nano ~/.shortcuts/qwu-start.sh
nano ~/.shortcuts/qwu-stop.sh
# Make executable
chmod +x ~/.shortcuts/qwu-start.sh
chmod +x ~/.shortcuts/qwu-stop.sh
# Install netcat if needed
pkg install netcat-openbsd
Then add Termux:Widget to your home screen.
Credential Rotation
The Azure client secret expires after 12 months. To rotate:
- Azure Portal → Microsoft Entra ID → App registrations → n8n-vm-automation
- Certificates & secrets → New client secret (12 months)
- Copy the new secret value
- n8n → Variables → Update
azure_client_secret - Delete the old secret in Azure
Calendar reminder set for 2 weeks before expiration (December 2026).
Self-Hosted n8n Instance (n8n.quietlyworking.org) ⭐ NEW
As of January 2026, we migrated from n8n Cloud to a self-hosted instance to reduce costs.
| Property | Value |
|---|---|
| URL | https://n8n.quietlyworking.org |
| VM | qwu-n8n (Azure B1ms, Ubuntu 22.04) |
| API Endpoint | https://n8n.quietlyworking.org/api/v1 |
Environment Variables (in .env):
N8N_API_URL=https://n8n.quietlyworking.org/api/v1
N8N_API_KEY=<API_KEY> # Full key in .env
API Usage (from QWU Backoffice VM):
# List all workflows
curl -s "$N8N_API_URL/workflows" -H "X-N8N-API-KEY: $N8N_API_KEY" | jq '.data[] | {name, id, active}'
# Import a workflow
cat workflow.json | jq '{name, nodes, connections, settings}' | \
curl -s -X POST "$N8N_API_URL/workflows" \
-H "X-N8N-API-KEY: $N8N_API_KEY" \
-H "Content-Type: application/json" -d @-
# Activate a workflow
curl -s -X POST "$N8N_API_URL/workflows/{workflow_id}/activate" \
-H "X-N8N-API-KEY: $N8N_API_KEY"
Naming Convention for .env:
- API keys should follow:
SERVICE_API_KEYformat (e.g.,N8N_API_KEY,SUITEDASH_SECRET_KEY) - Use UPPERCASE_WITH_UNDERSCORES, not lowercase-with-hyphens
- Include a comment above explaining the key's purpose
SSH Access to n8n VM (from QWU Backoffice):
ssh qwu-n8n # Connect to n8n VM
ssh qwu-n8n "docker ps" # Run single command
Managing n8n Environment Variables:
The self-hosted Community Edition uses server environment variables ($env.VARIABLE_NAME) instead of the Cloud Pro Variables UI ($vars).
# View current variables
ssh qwu-n8n "cat ~/n8n/.env"
# Add new variable (example)
ssh qwu-n8n "echo 'NEW_VAR=value' >> ~/n8n/.env"
# Also add to docker-compose.yml environment section, then restart:
ssh qwu-n8n "cd ~/n8n && docker compose up -d"
Available Discord Webhook Variables:
| Variable | Purpose |
|---|---|
DISCORD_WEBHOOK_AGENT_LOG | #agent-log notifications |
DISCORD_WEBHOOK_INBOX_ALERTS | #inbox-alerts for errors |
DISCORD_WEBHOOK_DAILY_DIGEST | Daily summaries |
DISCORD_WEBHOOK_L4G_LEADS | Lead generation alerts |
DISCORD_WEBHOOK_SYSTEM_STATUS | System status updates |
DISCORD_WEBHOOK_TIG_BOOKING | Booking notifications |
DISCORD_WEBHOOK_CONTENT_REVIEW | Content review channel |
DISCORD_WEBHOOK_CONTENT_QUEUE | Content queue reminders |
DISCORD_WEBHOOK_NEWSLETTER_REVIEW | Newsletter review |
DISCORD_WEBHOOK_RESEARCH_QUEUE | Research queue items |
Usage in n8n Workflows:
{{ $env.DISCORD_WEBHOOK_AGENT_LOG }}
Active Ezer Workflows:
| Workflow | Schedule | Purpose |
|---|---|---|
| SMS Approval Webhook | Always-on | Route incoming SMS to dev VM |
| Ezer Morning Health Check-in | Daily 7 AM PT | Proactive health reminder |
| Ezer Discord DM Poller | Every 3 min | Process Discord DM voice messages |
Migrating Workflows from Cloud Pro to Self-Hosted ⭐ MP TRAINING
This section documents how to migrate n8n workflows from Cloud Pro (or any exported JSON) to the self-hosted instance. This is an excellent MP Student training opportunity (Contributor tier+).
Key Differences: Cloud Pro vs Self-Hosted
| Aspect | Cloud Pro | Self-Hosted Community |
|---|---|---|
| Variables syntax | $vars.variable_name | $env.VARIABLE_NAME |
| Variables UI | Built-in Settings → Variables | Server environment variables |
| SSH credentials | May have different ID | Credential ID: <SSH_CREDENTIAL_ID> |
| SSH credential name | Varies | "QWU Backoffice SSH - 20251224a" |
| SSH credential key | Varies | n8n's OWN key (n8n-ssh@qwu-n8n), made on the n8n server and used only over the private network, since 2026-10-11. Never a person's key |
Step-by-Step Migration Process
1. Export the workflow from Cloud Pro:
# In Cloud Pro: Workflow → ⋮ menu → Export
# Save as JSON file
2. Identify changes needed:
Run this check on the exported JSON:
# Check for $vars references (need to change to $env)
grep -o '\$vars\.[a-z_]*' workflow.json | sort -u
# Check SSH credential references
grep -A5 '"sshPrivateKey"' workflow.json
3. Convert variable references:
| Find | Replace With |
|---|---|
$vars.discord_webhook_agent_log | $env.DISCORD_WEBHOOK_AGENT_LOG |
$vars.discord_webhook_* | $env.DISCORD_WEBHOOK_* (UPPERCASE) |
Any $vars.lowercase | $env.UPPERCASE_UNDERSCORES |
4. Update SSH credentials:
Replace any SSH credential block with:
"credentials": {
"sshPrivateKey": {
"id": "<SSH_CREDENTIAL_ID>",
"name": "QWU Backoffice SSH - 20251224a"
}
}
5. Clean the JSON for API import:
The n8n API rejects certain properties. Remove these before import:
# Create clean version for API
cat workflow.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
for key in ['staticData', 'triggerCount', 'tags', 'id', 'meta']:
data.pop(key, None)
print(json.dumps(data))
" > workflow-clean.json
6. Deploy via API:
# Source environment (or use literal values)
source .env
# Create workflow
curl -s -X POST \
-H "X-N8N-API-KEY: $N8N_API_KEY" \
-H "Content-Type: application/json" \
-d @workflow-clean.json \
"$N8N_API_URL/workflows"
# Note the returned workflow ID
7. Activate the workflow:
curl -s -X POST \
-H "X-N8N-API-KEY: $N8N_API_KEY" \
"$N8N_API_URL/workflows/{WORKFLOW_ID}/activate"
8. Verify deployment:
# List all workflows and check status
curl -s -H "X-N8N-API-KEY: $N8N_API_KEY" "$N8N_API_URL/workflows" | \
python3 -c "import json,sys; data=json.load(sys.stdin); \
[print(f\"{w['id']}: {w['name']} (active: {w['active']})\") for w in data.get('data',[])]"
Common Pitfalls
| Issue | Solution |
|---|---|
$vars not found | Self-hosted uses $env - convert all variable references |
| SSH authentication fails | Ensure authentication: "privateKey" is set in SSH node parameters |
| API returns "additional properties" | Remove staticData, triggerCount, tags from JSON |
| Webhook doesn't trigger | Known API bug - see Troubleshooting section; import via UI instead |
| Wrong credential ID | Use ID <SSH_CREDENTIAL_ID> for all SSH nodes |
Save the Workflow Locally
Always save the migrated workflow JSON to version control:
# Standard location for workflow JSON files
005 Operations/Workflows/your-workflow-name.json
MP Student Learning Objectives
Students completing this module will learn:
- JSON editing and transformation
- API authentication patterns
- Environment variable conventions
- CI/CD concepts (deploy → activate → verify)
- Debugging API responses
- Version control for infrastructure
MP Training Opportunities (from the 2026-10-11 n8n key move, Session 720)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Which key a client REALLY uses is in the server's log, not the key's label | A key labelled for the automation server had never been used; the automation logged in with a person's admin key. Only Accepted publickey ... SHA256: lines told the truth. Teaches evidence over labels | Intermediate |
| Prove the undo on a throwaway before touching the real thing | The rollback was rehearsed on a dummy login (change, restore byte-identical, delete) before the real swap. Teaches rehearsal as a safety habit, not a formality | Intermediate |
| Test a credential the way the real client connects | The new key was tried from inside the container with the automation's own SSH library, in both directions (allowed and refused). Teaches fidelity: a test that uses a different client proves a different thing | Advanced |
| A check that cannot fail is not a check | The audit pinned the wrong key and would have gone green. The fix makes it read the login log itself, and mutation tests prove each rule can fail. Teaches mutation testing in one afternoon | Advanced |
Most valuable MP exercise: give a student an authorized_keys file with three lines, a week of auth.log, and the question "which key does the automation use?" The labels point one way and the log points the other.
n8n Version Management ⭐ NEW
The self-hosted n8n instance uses pinned version tags for stability.
Current Version: 2.45.5 (upgraded 2026-10-28 9 PM Pacific from 2.44.6, unattended on the armed schedule and verified at the source that night: ~20s down, 5 migrations, every workflow and timer intact, every timed workflow firing on 2.45's new durable scheduler. Upgrade history: n8n_version_management.md changelog. The HQ masthead strip tracks installed vs published vs vulnerable continuously.)
| Property | Value |
|---|---|
| Image | docker.n8n.io/n8nio/n8n:2.45.5 (rollback image 2.44.6 kept) |
| Location | ~/n8n/docker-compose.yml on qwu-n8n |
| Monitor | monitor_n8n_releases.py hourly cron :41 on backoffice VM (live version via SSH + GHSA advisories; replaced the retired "n8n Version Monitor" workflow 2026-07-21) |
| Port Binding | 127.0.0.1:5678 (localhost only, Caddy proxies HTTPS) |
The advisory light only shows what is really open (2026-10-28, Session 840): n8n writes one fix per release line it patched, plus a 3.0.x entry since its 3.0 pre-releases. When none names our line, check_n8n_version.py v1.2.0 clears a warning only if an earlier line shipped the fix a day or more before our line was cut (the previous minor's first npm release, since n8n cuts the next minor when it ships one), or if a person-written check in 005 Operations/Data/n8n_advisory_code_checks.json finds the fix in the live container (re-run on every strip refresh, so an upgrade that drops the fix turns the light red again). The hourly watcher shares the rule and re-judges a cleared advisory when n8n edits it or we roll back. Validated on n8n's whole history: 0 wrong clears in 9,501 cases where the listed line was removed and re-asked.
MP Training Opportunities (from the 2026-10-28 n8n upgrade checkup and advisory-light fix, Session 840)
| Task | Skills taught | Prerequisites | Time | Level |
|---|---|---|---|---|
| Verify an unattended upgrade at the source (log verdict, running version, database counts, per-workflow run counts before and after) | SQL against a live Postgres, reading a deploy log critically, docker exec | SSH, basic SQL | 2 h | Intermediate |
Find a security fix in compiled JavaScript inside a container (and learn why BusyBox grep --include silently finds nothing) | grep/find, reading minified Node code, tool limits | Linux shell | 1.5 h | Intermediate |
| Test a rule that clears warnings by deleting the evidence it replaces (counterfactual validation) | data validation design, Python, thinking like an adversary | Python, JSON | 3 h | Advanced |
| Write a test that reproduces a reviewer's finding before fixing it | pytest, monkeypatching, test-first repair | Python | 1 h | Intermediate |
The weekly package advisory note (2026-10-30, TIG's "guard it" and "today too"): once a week (Mondays at 06:30 Pacific), watch_dependency_advisories.py reads Quietly Networking main's package.json and package-lock.json and runs npm audit on the lockfile only (never an install, never a build gate). One record note goes to #agent-log every week, even when nothing is new: each NEW critical or high advisory in plain words (package, how it comes in, title and link, production tree or development tools only, and whether a same-major fix at least 14 days old exists), then the totals in GitHub's count and npm's. A new critical or high in the production tree also goes to TIG's phone the same day (TODAY), at most once a week. A week that cannot read says so; two in a row send a TODAY alert. Betterstack heartbeat 521772 texts TIG if a week passes without a clean run (security texts go at any hour). Status: watch_dependency_advisories.py --status. Repo list: 005 Operations/Data/dependency_advisory_watch_repos.json (QNT only; another repo joins after its own triage). Detail: QNT System Status, Known Issues, "Dependency security alerts"; github_tool_wisdom.md.
MP Training Opportunities (from the 2026-10-30 package advisory watch)
| Task | Skills taught | Prerequisites | Time | Level |
|---|---|---|---|---|
Read npm audit --json safely (the exit code is not the verdict; the counts must add up; one advisory = one package and GHSA pair) | JSON, defensive parsing, npm | Python basics | 1.5 h | Intermediate |
| Write a semver range evaluator and check it against npm's own library | parsing, test design, checking against a reference | Python | 3 h | Advanced |
| Build a weekly watcher that never reads a failed check as a quiet week (retries, one time budget, a dead-man heartbeat) | reliability design, cron, alert tiers | Python, cron | 3 h | Advanced |
| Prove an alert at the far end (read the Discord channel back; a throwaway heartbeat for the text path) | REST APIs, verification habits | REST basics | 1 h | Beginner |
MP Training Opportunities (from the 2026-11-04 server login lock-down, Session 897)
| Task | Skills taught | Prerequisites | Time | Level |
|---|---|---|---|---|
| Prove a firewall rule with data, not a bare connection: run throwaway listeners, one that speaks first and one that waits, and compare both ends with the vendor's log | networking, TCP, test design, reading what a result really says | Python sockets | 2 h | Intermediate |
| Find out what an API key can do without changing anything: an invalid request answers 403 when the right is missing and 400 when it is there; an empty list proves nothing | REST APIs, least privilege, safe probing | REST basics | 1 h | Beginner |
| Write a guarded tool for a file that can lock you out: a backup, a temp file, a re-read just before the rename, and the system's own parser as a second opinion | file safety, atomic writes, test design | Python | 3 h | Advanced |
| Write one hands-on walkthrough where every step has an expected result and a branch for when it differs | technical writing, operations | None | 2 h | Beginner |
Security Hardening (2026-04-04):
- UFW firewall enabled: ports 22 (SSH), 80 (HTTP), 443 (HTTPS) only
- fail2ban installed: SSH brute-force protection via
sshdjail - Unattended-upgrades enabled: automatic OS security patches
- Port 5678 restricted to localhost (was previously exposed on all interfaces)
- Docker CE updated 29.1.4 → 29.3.1
Update Procedure:
# 1. SSH to n8n VM
ssh qwu-n8n
# 2. Backup database
cd ~/n8n
~/scripts/backup_n8n_postgres.sh
docker exec n8n-postgres pg_dump -U n8n n8n | gzip > backups/backup_pre_upgrade_$(date +%Y%m%d).sql.gz
# 3. Update version in docker-compose.yml (sed with .bak for instant rollback)
# Change: image: docker.n8n.io/n8nio/n8n:<current>
# To: image: docker.n8n.io/n8nio/n8n:X.Y.Z
# 4. Pull new image and restart
docker compose pull n8n && docker compose down && docker compose up -d
# 5. Verify version
docker exec n8n n8n --version
# 6. Verify all workflows still active
docker exec n8n n8n list:workflow --active=true --onlyId | wc -l
Version Monitoring (v2.0.0 - Release Intelligence):
- The "n8n Version Monitor" workflow checks GitHub releases every Monday 9 AM
- Parses release notes for QWU-relevant changes using node inventory
- Color-coded notifications by severity:
- 🚨 Major (X.0.0) - Red, high priority
- 🔄 Minor (X.Y.0) - Orange, medium priority
- 📦 Patch (X.Y.Z) - Green, routine
- Skips pre-release versions (e.g., 2.5.x)
- Update the
currentVersionconstant in the workflow after each upgrade
QWU Node Inventory (what the monitor tracks):
| Node | Usage | Notes |
|---|---|---|
| httpRequest | 63 | Discord webhooks, API calls |
| code | 59 | JavaScript parsing |
| ssh | 56 | Running Python scripts |
| if | 54 | Conditional logic |
| scheduleTrigger | 47 | Cron scheduling |
| noOp | 39 | No-op endpoints |
| webhook | 11 | Incoming webhooks |
| respondToWebhook | 10 | Webhook responses |
Directive: See 005 Operations/Directives/n8n_version_management.md for full process
Workflow Publishing (n8n 2.x): n8n 2.x uses a publish/unpublish model instead of activate/deactivate:
# Publish a workflow (after import)
docker exec n8n n8n publish:workflow --id=<workflow_id>
# Unpublish (disable)
docker exec n8n n8n unpublish:workflow --id=<workflow_id>
# Restart required after publishing
cd ~/n8n && docker compose restart n8n
Workflow Archiving:
n8n has a native archive feature via the isArchived database column:
# Archive an old workflow
docker exec n8n-postgres psql -U n8n -d n8n -c \
"UPDATE workflow_entity SET \"isArchived\" = true WHERE id = '<workflow_id>';"
Archived workflows are hidden from the main list but preserved for reference.
Running Agent Jobs
tmux for Persistent Sessions ⭐
Why it matters: When you disconnect from the workspace, running processes normally stop. tmux keeps your agents running even when you're not connected.
Essential Commands:
# Start a new named session
tmux new -s agents
# Detach from session (process keeps running)
Ctrl+B, then D
# List all sessions
tmux ls
# Reattach to a session
tmux attach -s agents
# Kill a session when done
tmux kill-session -s agents
Multiple Windows Inside tmux:
Ctrl+B, then C # Create new window
Ctrl+B, then N # Next window
Ctrl+B, then P # Previous window
Ctrl+B, then 0-9 # Jump to window by number
Ctrl+B, then , # Rename current window
Best Practice for Overnight Jobs:
- Start a tmux session with a descriptive name:
tmux new -s lead-scraper - Run your agent script
- Detach:
Ctrl+B, then D - Go to sleep 😴 (VM runs 24/7, no shutdown concerns)
- Reconnect next day:
tmux attach -s lead-scraper
Overnight/Long-Running Jobs
24/7 Operation Mode
As of January 2026, the QWU Backoffice VM runs 24/7. Auto-shutdown has been disabled to enable:
- Continuous n8n workflow execution
- Ezer email response handling at any hour
- Overnight batch processing jobs
- True autonomous operations
Cost Trade-off: ~$50-85/month (vs. ~$35-40 with auto-shutdown). Worth it for always-on automation.
Server Maintenance
With 24/7 operation, we follow a maintenance schedule:
| Frequency | Task | How |
|---|---|---|
| Every 5 min | Memory pressure monitor | /etc/cron.d/qwu-memory-pressure-monitor runs monitor_memory_pressure.py ... SMS+Discord on RED, Discord on YELLOW |
| Every 6 hr | Health check | check_vm_health.py cron ... Discord #system-status + Betterstack heartbeat |
| Weekly (Sat) | Scheduled restart | /etc/cron.d/qwu-weekly-reboot ... fires 0 4 * * SAT Pacific, shutdown -r +5 (5-min warning, cancellable via sudo shutdown -c) |
| Monthly | Security updates review | Manual check of applied patches |
| Quarterly | Deep maintenance | Disk cleanup, Docker prune, log rotation |
Reboot cadence updated 2026-05-17 (Session 349) from the old Sun 3 AM schedule to the new Sat 04:00 PT schedule, after a VM crash where a single login scope reached 28.3 GB peak following 22 days of uptime. The memory pressure monitor + /pulse skill catch accumulation BEFORE it locks the VM; the weekly reboot is the periodic reset.
Monitoring model ... v2.4.0 (2026-05-19 base, corrected through 2026-08-02): The pulse skill (.claude/skills/pulse/SKILL.md) and memory pressure monitor (005 Operations/Execution/monitor_memory_pressure.py) share core logic via Python import from pulse.py ... they cannot drift out of sync. v2.1.0 dropped two v2.0 thresholds that caused false alerts:
- The 8-session count cap ... counts treat all sessions as equal weight; 12 simple chats are safer than 2 multi-agent fan-outs. Replaced with per-PID classification (chat / medium / fan-out) and spawn-budget verdicts (Simple ✅ / Medium ✅ / Fan-out ❌).
- The 16 GB scope-size threshold ...
cgroup.memory.currentincludes reclaimable kernel page cache (VSCode + MCP servers can sit at 20 GB without any real pressure). Replaced with PSI memory.pressure (kernel-honest stall percentages).
v2.3.0 (2026-07-15) retired resident swap SIZE as a RED/YELLOW trigger in both tools. Linux parks cold pages in swap even with ~28 GB RAM free and PSI = 0, so swap_used > 500 MB pinned the monitor to RED and re-texted every 30 minutes, each time naming an innocent ~200 MB chat session that holds no swap. Swap is now context-only; PSI is the honest signal.
v2.4.0 (2026-08-02) closed a blind spot that had been hiding 93% of the slice. pulse.list_claude_processes matched only claude --output-format, so during a real freeze it tracked 1,104 MB of a 15,790 MB slice and the RED alert named a 265 MB chat session while ~15 GB sat in a non-Claude process nothing was looking at. New additive slice_processes field records the top in-slice processes by RSS regardless of cmdline (binary name + hash only ... never argv, because pulse_history.jsonl is tracked in git). The /pulse report gained §3b, and the Discord embed + SMS now name the real hog and flag when it is not a Claude session.
Current RED conditions (any one triggers SMS): available RAM < 4 GB, top Claude process > 3 GB, PSI memory.pressure some > 30% OR full > 5%, max growth slope > 200 MB/min. Swap size is not a trigger (see v2.3.0 above). SMS names the offending PID + classification, or the largest in-slice process when that is bigger: [QWU VM RED] PSI some=99.0 ... Biggest: pid 265363 node 2402 MB (NOT a claude session).
Alerting is forensics, not defense. In all three 2026 livelock incidents the balloon completed inside a single 5-minute sampling interval, so the alert necessarily arrived after the box was already frozen. What actually protects the VM is the cgroup
MemoryMaxenforcement layer (see §Infrastructure Monitoring andinfrastructure_monitoring.md§Tier-1). Do not respond to a recurrence by tightening these thresholds.
Background heartbeat: cron */5 * * * * writes 005 Operations/Data/pulse_history.jsonl so slopes (Δ RSS / Δ time per PID) populate within 10 minutes of any session starting.
MP Training Opportunities ⭐ (from the 2026-08-02 livelock diagnosis)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
"It crashed" is a hypothesis, not a fact ... check last -x, who -b, and the shutdown path before diagnosing | The single highest-value habit in ops. Here the box had NOT crashed: it was a clean graceful poweroff, and everything downstream of "crash" would have been wrong | Beginner |
Reading sar history to reconstruct an incident after the fact | sysstat silently records every 10 min; most people never learn it exists. %system 99% vs %user 99% points at completely different culprits | Beginner |
| Distinguishing livelock from crash from OOM kill | Three failure modes that all look like "the machine died" but have opposite fixes. Evidence: oom_kill 0 + no panic + 99% %system + PSI full 99% = livelock | Intermediate |
Free RAM does not mean healthy ... read PSI (memory.pressure) instead | The box had 10.8 GB free while frozen solid. Teaches that the obvious metric can be the misleading one | Intermediate |
Verifying a kernel symbol's origin in /proc/kallsyms instead of guessing from its name | high_work_func sounds like the Hyper-V balloon driver on an Azure VM. It isn't. Checking neighbours in the symbol table settled it in one command | Advanced |
cgroup v2 memory.high vs memory.max ... one throttles forever, only one can END an episode | A genuinely counter-intuitive distinction that cost three production freezes. Excellent systems-thinking lesson: a "safe, soft" limit was the dangerous one | Advanced |
| Auditing your own instrument's coverage before trusting its output | The monitor tracked 1,104 MB of a 15,790 MB slice and confidently named the wrong process. Teaches "what is this tool blind to?" as a standing question | Advanced |
Never writing argv into a git-tracked log | A script invoked with a token in its arguments leaks that secret into history permanently. Store a hash instead | Intermediate |
Most valuable MP exercise: the crash-that-wasn't. Give a student the screenshot (CPU pegged at 100%, memory dropping) and the VM, and ask them to determine what happened. The instinct is "it ran out of memory." The evidence says the opposite, and every step of the correction is teachable: boot history → sar → %system vs %user → PSI → cgroup limits → kallsyms. It ends with a real, shipped fix and a genuinely humbling lesson: the safety mechanism was the thing causing the outage.
Operating discipline (replaces the v2.0 "8 sessions max" cap): one session per task, close it when done. Don't think in session count ... think in classification mix. Watch slope, not just RSS. Trust swap and PSI over free RAM.
See server_maintenance.md directive for full procedures.
Mobile Access via Termux (Android) ⭐
Access your Azure VM from your phone... check on overnight agent runs from anywhere.
Why This Matters
When you kick off a long-running agent job in tmux and go to bed, you can check on it from your phone without getting out of bed. 📱
Installation (One-Time Setup)
Important: Don't use the Google Play Store version... it's outdated.
- Install F-Droid from https://f-droid.org
- Open F-Droid and install these four apps:
- Termux (main terminal)
- Termux:API (clipboard, notifications)
- Termux:Widget (home screen shortcuts)
- Termux:Styling (customize appearance)
Initial Configuration
Open Termux and run:
# Update packages
pkg update && pkg upgrade -y
# Install essentials
pkg install openssh git termux-api nano curl netcat-openbsd -y
# Grant storage access
termux-setup-storage
Generate SSH Key
ssh-keygen -t ed25519 -C "termux-mobile"
Press Enter for defaults. View your public key:
cat ~/.ssh/id_ed25519.pub
Add Key to Azure VM
From another terminal connected to your VM, add the public key:
echo "YOUR_PUBLIC_KEY_HERE" >> ~/.ssh/authorized_keys
Configure SSH Shortcut
Create the config file:
nano ~/.ssh/config
Add this (update IP if it changes):
Host qwu
HostName <VM_IP_CLAUDE_DEV>
User <VM_USER>
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 60
ServerAliveCountMax 3
Save (Ctrl+O, Enter, Ctrl+X) and set permissions:
chmod 600 ~/.ssh/config
Daily Use
| Command | What It Does |
|---|---|
ssh qwu | Connect to Azure VM |
tmux ls | Check running sessions |
tmux attach | Jump into existing session |
tmux attach -t agents | Attach to specific session |
Ctrl+B, D | Detach (leaves session running) |
exit | Disconnect from VM |
Keyboard Tips
Termux shows an extra keys row. Useful shortcuts:
- Volume Up + Q = ESC
- Volume Up + E = CTRL
- Swipe left from right edge for drawer with more keys
Troubleshooting
"Connection timed out"
- Rare with 24/7 operation, but could indicate network issue or VM restart. Check Azure Portal for status.
"Connection refused"
- Check if VM IP changed (it can change when stopped/started). Update
~/.ssh/configwith new IP.
"Permission denied (publickey)"
- Your key wasn't added to the VM. Re-run the
echocommand on the VM to add it.
Obsidian + GitHub Integration: Unified Workspace
The QWU Backoffice uses a unified workspace architecture where the Obsidian vault and agent codebase live in the same repository. This allows you to edit agent code (directives, execution scripts) from Obsidian on any device.
Architecture Overview
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Obsidian │ │ GitHub │ │ Azure VM │
│ (Windows) │◄───►│ (Full Repo) │◄───►│ (Filtered) │
│ Full vault │ │ │ │ Text files only│
└─────────────────┘ └─────────────────┘ └─────────────────┘
| Location | What it contains |
|---|---|
| Obsidian (Windows) | Everything - full vault with all files |
| GitHub | Everything - complete repository |
| Azure VM | Filtered - only what agents need (no images, canvas files, etc.) |
Local Vault Location
C:\Users\<USERNAME>\qwu_backOffice
This folder serves as both:
- Your Obsidian vault (open in Obsidian as a vault)
- Your git repository (syncs to GitHub)
Obsidian Git Plugin Settings
The obsidian-git community plugin handles automatic sync:
| Setting | Value |
|---|---|
| Auto commit-and-sync interval | 10 minutes |
| Auto pull interval | 10 minutes |
| Pull on startup | ON |
| Push on commit-and-sync | ON |
| Pull on commit-and-sync | ON |
Manual Sync Commands
In Obsidian, use the Command Palette (Ctrl+P):
- Obsidian Git: Commit all changes - Save current changes
- Obsidian Git: Push - Upload to GitHub
- Obsidian Git: Pull - Download from GitHub
.gitignore Configuration
Located at the root of the vault, excludes files that shouldn't sync:
# Obsidian workspace cache & plugins
.obsidian/workspace.json
.obsidian/workspace-mobile.json
.obsidian/plugins/
# Smart Connections plugin data
.smart-env/
.smart-connections/
smart-chats/
# Trash & temporary files
.trash/
.tmp.driveupload/
.tmp/
# Supervisor escalation outputs (legacy safety net — now routed to ___Supervisor_Escalations/)
000 Inbox/___Tasks/SUPERVISOR-*.md
000 Inbox/___Supervisor_Escalations/
# Environment and credentials (NEVER commit)
.env
.credentials/
# Node.js
node_modules/
# Python
.venv/
__pycache__/
*.pyc
# IDE & OS
.vscode/
.DS_Store
Thumbs.db
Obsidian CLI (v1.12+, Feb 2026)
Obsidian released an official CLI in v1.12. It's a remote control for the running GUI app ... Obsidian must be running (or it auto-launches on first command). Currently Early Access ($25 Catalyst License, will go free).
Key commands: obsidian read, create, append, rename, search, search:context, daily, daily:append, files, unresolved, plugin:reload, dev:screenshot.
Impact on QWU:
- Does NOT help server-side (claude-dev VM): Headless Linux, no GUI = CLI won't work
- Useful on TIG's Windows workstation for quick captures and daily note appends from PowerShell
obsidian renameis the strongest use case ... handles backlink updates across the vault (our Pythonos.rename()doesn't update[[wikilinks]])obsidian unresolvedbeats our regex-basedextract_unresolved_links.pyfor accuracy (understands aliases, embeds, block refs)
Full details: See TWL at 005 Operations/Directives/obsidian_tool_wisdom.md
Future watch: If Obsidian ships a headless/daemon mode, this changes everything for server automation.
Sparse Checkout: Filtering Files on Azure VM
The Azure VM uses sparse checkout to only download files that agents need. This excludes images, canvas files, and other non-text content.
How It Works
Git's sparse checkout feature tells the VM: "Only pull these types of files." The full repo stays on GitHub, but the VM only sees what's relevant for agent work.
Sparse Checkout Configuration File
Located at: ~/qwu_backOffice/.git/info/sparse-checkout
Current configuration:
# Include everything by default
/*
# ===================
# EXCLUDE: Obsidian visual/UI files
# ===================
!*.canvas
!*.excalidraw
!/Excalidraw/
# ===================
# EXCLUDE: Obsidian plugin data (not needed by agents)
# ===================
!/.smart-connections/
!/.smart-env/
!/smart-chats/
!/www.help.obsidian.md/
!/{{savePath}}/
# ===================
# EXCLUDE: Large media files (agents work with text)
# ===================
!*.jpg
!*.jpeg
!*.png
!*.gif
!*.webp
!*.mp4
!*.mp3
!*.pdf
# ===================
# EXCLUDE: Misc
# ===================
!/Omnivore/
!/Clippings/
Editing Sparse Checkout Rules
To add or remove exclusions:
nano ~/qwu_backOffice/.git/info/sparse-checkout
After editing, apply changes:
cd ~/qwu_backOffice
git sparse-checkout reapply
Sparse Checkout Syntax
| Pattern | Meaning |
|---|---|
/* | Include everything |
!*.canvas | Exclude all .canvas files |
!/Excalidraw/ | Exclude the Excalidraw folder |
!*.jpg | Exclude all .jpg files |
The ! prefix means "exclude this pattern."
Verifying Sparse Checkout
Check what's included:
ls -la ~/qwu_backOffice
Verify specific files are excluded:
find ~/qwu_backOffice -name "*.canvas" 2>/dev/null
# Should return nothing if working correctly
Syncing Workflow
Your Daily Workflow (Obsidian)
- Open Obsidian (it auto-pulls on startup)
- Edit notes, directives, execution scripts
- Auto-commits every 10 minutes, or manually via Command Palette
- Changes sync to GitHub automatically
Agent Workflow (Azure VM)
Before agents read/write:
cd ~/qwu_backOffice
git pull
After agents write outputs:
git add .
git commit -m "Agent work log: $(date +%Y-%m-%d)"
git push
These outputs will appear in your Obsidian vault on the next sync!
Handling Sync Conflicts
If you edit on multiple devices simultaneously, you may get merge conflicts.
To resolve:
git status # See conflicted files
git diff # See the conflicts
nano <conflicted-file> # Edit to resolve
git add <conflicted-file>
git commit -m "Resolved merge conflict"
git push
Prevention tip: Let auto-sync run before switching devices.
Claude Code: Modes and Commands
Claude Code is a command-line AI assistant that can read, write, and execute code directly on your system.
Starting Claude Code
cd ~/qwu_backOffice
source .env
claude
Operating Modes
| Mode | How to activate | What it can do |
|---|---|---|
| Normal | Default when you start | Read files, write files, run commands, full power |
| Plan Mode | Type /plan | Think and plan only, NO file changes or commands |
| Auto-accept | Start with claude --dangerously-skip-permissions | Runs without asking permission for each action |
Normal Mode (Default)
Claude asks permission before:
- Writing/editing files
- Running terminal commands
- Installing packages
Best for: Learning, careful work, when you want to review each step.
Plan Mode
Toggle on/off by typing /plan inside Claude Code.
Claude will:
- ✅ Read files
- ✅ Think through problems
- ✅ Create detailed plans
- ❌ NOT write any files
- ❌ NOT run any commands
Best for: Complex problems where you want to think before acting. Architecture decisions. Reviewing what SHOULD happen before doing it.
Auto-accept Mode
claude --dangerously-skip-permissions
Claude executes without asking "May I run this command?" each time.
Best for: Trusted, repetitive tasks. Overnight agent runs. When you've already validated the workflow.
⚠️ Use with caution - it will do exactly what it decides to do!
Useful Commands Inside Claude Code
| Command | What it does |
|---|---|
/help | Show all commands |
/plan | Toggle plan mode |
/clear | Clear conversation history |
/compact | Summarize conversation to save context |
/cost | Show token usage and cost |
/quit or Ctrl+C | Exit Claude Code |
Tips for Effective Use
- Start in Normal Mode until you're comfortable with how Claude operates
- Use Plan Mode before tackling complex refactors or new features
- Be specific in your requests - Claude works better with clear goals
- Review changes before approving in Normal Mode
- Use Auto-accept only for well-tested, repetitive workflows
Running Several Claude Subscriptions (Capacity Routing) ⭐
Since 2026-09-09 the backoffice holds more than one Claude Max subscription (tig@ = #1, claude002@ = #2, each in its own ~/.claude* config directory) and a small system decides which one every NEW session starts on. Nothing ever moves a RUNNING session.
The one rule (corrected 2026-10-06): a Max subscription is three limits with three reset clocks (5-hour session window, weekly all-models, weekly scoped to one model). Routing ranks on the limit Anthropic itself marks binding for that account right now, only among accounts alive on every window. Ties fall through to the fullest limit, then total load, then the account name ... never to list order.
⚠ Why that sentence used to name Fable, and why it must never name a limit again. Until 2026-10-06 the rule was "rank on the weekly limit scoped to Fable." True when written ... Fable was the binding limit at 90%+ through September. Then both Fable windows rolled over, every account read 0% on the only dimension being compared, the ranking went flat, and the tie fell to list order. Ten live sessions ran on one paid subscription and zero on the other for three days, while the capacity card beside it correctly named the idle account as the one with the most room. A hand-named dimension cannot report that it has gone stale; it just reads 0, which is the most attractive value in the ranking. Pinned by
tests/test_claude_account_picker_ranks_on_binding_limit.py... 8 cases, each run with the account list both ways, because an order-dependent answer is what a dead ranking signal looks like from the outside.
Since 2026-10-30 the plan is called Ultra, and the 5-hour window is going away. Anthropic renamed Max 20x to Ultra (same price, same weekly limit; Max 5x is now plain "Max") and is removing the 5-hour window from Ultra accounts one at a time, each once Anthropic has verified the account. #1 lost its window on 2026-10-30 at 6:24 PM Pacific; #2 was still waiting that night. An account without the window simply reports no 5-hour limit, and the router ranks it on its weekly limits alone. Anthropic emails the account's own address when its window comes off. Both addresses land in TIG's one mailbox and Outlook shows tig@ as the recipient on every copy, so read the email's last line ("This email was sent to ...") to see which account it was for. A 5-hour row reading 0% with no reset time is an idle window, not a removed one.
| Piece | Script | Cadence | What it does |
|---|---|---|---|
| Gauge | poll_claude_account_limits.py | cron */20 | Reads every login's real limits from Anthropic (percentages, fail-loud, three states: ok / no room / cannot read); upserts hq_claude_account_limits and deletes any limit Anthropic stops reporting (v1.9.0, so the card never shows a removed window); writes .tmp/claude_capacity.json |
| Card | HQ /subscriptions + dashboard widget + mobile | live | "Most room now: |
| Brain | pick_claude_account.py | on demand | Prints the config dir a new session should use; --explain shows every account's standing; --resume <id> pins a conversation to the directory that holds it; any doubt = default account |
| Terminal door | the bare claude command (~/.local/bin/claude) | every terminal session | Shim in front of the CLI's own version binary, resolved numerically at run time. Added 2026-10-06 ... until then this path was a plain symlink, so every terminal session skipped the picker entirely and landed on the default account |
| Terminal door, explicit | launch_claude_session.sh (1, 2, claude002, or --explain) | on demand | Forces a named account, or asks the picker. Sets CLAUDE_CONFIG_DIR, which both shims honor without picking twice |
| Editor door | install_claude_switchboard.py | once | Launcher in front of the VS Code extension's bundled binary (all panels share ONE extension host, so this is the only seam); --status / --uninstall |
| Repair | repair_claude_switchboard.py | cron */10 | Restores both doors and says which. A VS Code extension update removes the editor one; a claude update rewrites the terminal symlink. Two different updaters, same silent outcome. Records repairs AND failed repairs to hq_claude_runtime |
| Config sync | sync_claude_account_config.py | cron */10 | Symlinks the user layer (settings, skills, output styles, plugins, status line) into each secondary account; credentials + history stay separate |
| Keep-alive | keep_claude_tokens_alive.py | cron */10 | Runs the official client once (one word, Haiku) on any login within 15 min of expiry; a live-but-idle session gets 15 min past expiry, then is renewed underneath |
| Verify | verify_claude_account.py | after every login | Two directories, two account uuids ... or you have one subscription signed in twice |
Adding subscription three: log in from a browser that has NEVER signed in to Claude (private windows share a session), with CLAUDE_CONFIG_DIR=~/.claude-claude003 claude, using a real claude003@ alias (not a plus-address). Run verify_claude_account.py. That is the only human step; the poller, sync, and keep-alive discover the new directory on their own.
When something looks wrong: pick_claude_account.py --explain first ... it prints every account's standing, which limit is binding, and the rank each one scores. An account showing "unreadable" on the card for more than one 10-minute upkeep cycle is a real fault (login gone, or the undocumented usage endpoint changed), not idleness. If Anthropic refuses an account outright (a past-due payment answers 403 oauth_not_allowed_for_organization), signing in again will not help; the router stops sending new sessions there (v1.3.0) and takes it back by itself within 20 minutes of the first good read. The HQ capacity card says so in plain words ("payment is past due ... signing in again will not fix that"), and its "Most room now" line ranks accounts exactly as the router does. After the payment clears, expect the card to stay unreadable until the account's next login renewal (at most 8 hours); nothing needs doing. Since 2026-10-12. And if one subscription is carrying everything while the card says another has room, believe the card ... it reads the live limits, the router reads a ranking, and the router is the thing that can be stale.
Not the same thing as OpenRouter. Scripts in the execution layer call any model per step through OpenRouter via model_config.py tiers. This system is for the interactive development sessions, which run on subscriptions.
MP Training Opportunities ⭐ (from the 2026-10-06 flat-ranking diagnosis)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
Read the running process, not the config ... /proc/<pid>/environ tells you what a program ACTUALLY got | The config said sessions were being routed. Ten processes said otherwise, in one command. Teaches the whole ground-truth habit in a single concrete move | Beginner |
| A default value has a direction ... ask "if this field went missing, does the gap make this option look better or worse?" | A missing percent became 0, and 0 meant "maximum room left". A sensible-looking default was a fail-open switch in disguise. One of the most transferable code-review questions there is | Beginner |
| Test the same input in BOTH orders | The bug was not "picks the wrong one", it was "the answer depends on list order". Teaches that the right assertion often tests a PROPERTY, not a value | Intermediate |
| A number that reads 0 can mean "none used" or "no longer measuring anything" | Same digit, opposite meanings, and nothing on screen distinguishes them. The clearest teachable example of why a metric needs provenance, not just a value | Intermediate |
| Count the entry points before trusting a control | The router was installed on VS Code panels and not on the terminal command, so half of all launches skipped it. A control on one door is a sample, not a control | Intermediate |
| Have someone try to break YOUR finding before you report it | An independent adversarial agent confirmed the fault AND corrected the root cause from "a 13-hour tie" to "a 3-day stale dimension". Teaches that being right about the symptom is not being right about the cause | Advanced |
Depth: 005 Operations/Directives/claude_tool_wisdom.md §Running several Max subscriptions (gotchas, the three silent faults). Deployed state: 002 Projects/_HQ Command Center/HQ-System-Status.md §Claude Capacity Card. Public write-up with generalized code: Built from Broken Vol. 10 (transparency site).
MP Training Opportunities (from the 2026-10-30 Ultra rename, Session 853)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Read an email's raw headers to learn which address it was really sent to | Two accounts' mail lands in one mailbox, and Outlook (and Graph's recipient list) showed tig@ on both. The raw To: line and the sender's own footer told the truth. Teaches going one layer below the display to the source | Beginner |
| A mirror table must forget what the source forgets | The usage poller only added and updated rows, so a limit Anthropic removed stayed on the HQ card, frozen at 2%, looking live. Teaches asking "what happens when an item disappears?" of every sync | Intermediate |
| Prove a test can fail | The new test was run against a copy of the poller with the fix switched off, and the two cases that need a delete failed, as they should. Teaches that a passing test only counts once you have seen it fail without the change | Intermediate |
Most valuable MP exercise: give a student a small table that a script fills by upsert, remove one item from the source, and ask them to find why the old item never leaves, then fix it so an empty or failed read never wipes the table.
MP Training Opportunities ⭐ (from the 2026-10-05 Vista Social entitlement diagnosis)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Before accepting "it used to work," grep the log for ONE success | Nine months of history, 51 responses, zero successes. One command settled a premise that two people believed. Teaches that a premise is checkable, and checking is cheaper than reasoning about it | Beginner |
| The three-way credential probe ... real key / known-bad key / no key | A dead credential and a plan-entitlement refusal read identically until the known-bad key answers DIFFERENTLY. Ten seconds, zero cost, and it redirected the whole fix from "rotate the key" to "call the vendor." The single most reusable move in this session | Beginner |
| Always carry a control when probing an external surface | The LinkedIn probe was only trustworthy because a known-real company returned 200 beside the four 404s. The TikTok probe reported not-found for TikTok's OWN account, which is how we knew to throw that result away instead of reporting it | Beginner |
| "Disproven" and "unchecked" are different words, and the difference matters | Bot walls beat every Instagram/Facebook/TikTok probe. Writing "those profiles do not exist" would have been a fabricated finding. Teaches reporting the shape of your own ignorance | Intermediate |
| A permanent configuration error can wear a transient costume | A wrapper pointed at a hostname that never resolved failed as Max retries, which every reader filed as a network blip. Five months of total outage produced no signal. Teaches that error CLASSIFICATION, not error presence, is what alerting depends on | Intermediate |
| The change that improves your error message looks exactly like the change that broke you | Correcting the base URL revealed the real wall and got blamed for building it. Teaches a specific, nasty failure of causal reasoning, and why "revert the last change" can un-fix a diagnosis | Advanced |
| Read a document's own changelog before trusting its table | The profile list said every program had active accounts. Its own first changelog entry said "verify actual connections," and nobody had, for 22 months. Teaches that documents carry their own confidence level if you look | Beginner |
Depth: 005 Operations/Integration-Specs/Vista-Social-Integration-Spec.md (status, probe output, incident timeline). Memory: feedback_the_fix_that_revealed_the_wall_gets_blamed_for_it.md. Program impact: 002 Projects/_Locals 4 Good/L4G-System-Status.md §the social-channel Known Issue.
Agent Memory Architecture
Claude Code maintains persistent memory across conversations through a layered system:
Always loaded (every conversation):
CLAUDE.md... Operating manual, conventions, infrastructure access (~500 lines)MEMORY.md... High-consequence behavioral rules, mission/language standards, topic file index (~104 lines, limit 200)
On-demand (loaded when domain work starts via Domain Start Protocol):
| Topic File | Trigger | Purpose |
|---|---|---|
memory/l4g.md | L4G work | Fundraiser behavioral notes, vault pointers |
memory/bni.md | BNI work | Pre-flight checklist, roster rules |
memory/email_patterns.md | Email tasks | Outlook draft rule, TIG's style |
memory/content_pipeline.md | Video/YouTube | Script inventory, output locations |
memory/lovable.md | App building | Vite gotchas, placeholder key warning |
memory/llm_api.md | Model config | OpenRouter parameter mapping |
memory/web_design.md | Visual design | RevSlider, Spline, frontend stack |
memory/wordpress.md | WPMU/Divi | System Status pointer |
memory/tool_wisdom_libraries.md | TWL work | Standard overview, coverage stats |
Key principles:
- MEMORY.md contains behavioral rules (what I get wrong). Topic files contain domain-specific notes + vault pointers.
- Topic files reference vault files (System Status, Entities, Directives) but never copy their content.
- Line budgets enforced at session wrap-up: MEMORY.md < 180 lines, topic files < 50 lines each. Also audited daily by
audit_memory_health.py(5 AM PT cron, Dream Score 0-100). /dreamskill runs mechanical audit + Claude-powered intelligent analysis (contextual staleness, semantic duplication, coverage gaps). Invoke manually between session wrap-ups.- Domain Start Protocol: read System Status file + topic file before beginning domain-scoped work.
- Multi-session safe: sessions are domain-scoped, topic files are domain-scoped, parallel writes hit different files.
- One store for both Claude accounts (since 2026-10-05). Memory lives per Claude config directory, so the second subscription (
~/.claude-claude002) had quietly built its own store of 42 memories the main account never saw. They were merged into~/.claude/projects/-home-<VM_USER>-qwu-backOffice/memory/, and the second account's memory folder is now a symlink to it (old folder kept asmemory.pre-merge-2026-10-05). Any future config directory gets the same link on day one.
Context window best practice: Manually clear conversations at natural breakpoints (task completion, domain switches) rather than waiting for auto-compaction. Use /session-wrap-up before clearing. The persistent file system is designed for clean breaks.
QCM ... QWU Context Manager
QCM is a homegrown context management system (v2.2.1, Session 378) that drives intentional fresh-session handoff before compaction AND recovers working state if compaction happens anyway. Built as 7 Python hook scripts with zero external dependencies (no npm packages, no MCP servers).
v2.2.0+ operating model: PRIMARY goal is to AVOID compaction entirely by triggering session handoff at 90% context fill. Compaction is lossy summarization that silently degrades agent recall (specific code, line numbers, exact decisions, rejected approaches, the reasoning chain that led to a decision all get fuzzed). Long context also degrades attention quality even within the stated window. A fresh session at 100% capacity outperforms a compacted session every time. The Stop-hook snapshot system is now the SECONDARY safety net for cases where the handoff discipline fails.
What it solves: During long sessions, Claude Code's context window fills. v2.2.0+ triggers an intentional handoff at 90% (via /session-wrap-up) so the next session starts fresh with full reasoning capacity. If compaction fires anyway, the v2.1.0 snapshot system still recovers state.
Architecture:
| Component | Hook Event | Purpose |
|---|---|---|
qcm_event_logger.py | PostToolUse, UserPromptSubmit | Logs every tool call and user message to SQLite, classified by priority (P1-P4). Tracks Agent launches + Skill invocations as P2 |
qcm_snapshot_builder.py | Stop | Builds a <=3KB priority-budgeted markdown snapshot with Session Goal pinning, file dedup, and git status |
qcm_session_restore.py | SessionStart | Injects the snapshot as additionalContext after compaction |
qcm_output_compressor.py | PostToolUse (Bash) | Compresses large outputs (>3KB) ... saves full output to disk, returns summary to context |
qcm_redundancy_detector.py | Stop (after snapshot) | Detects repeated file reads and searches within a session, tracks per-session and cross-session metrics |
qcm_threshold_monitor.py (v2.2.0) | PostToolUse | Fires 70/90/95% context-fill alerts using ground-truth token counts from transcript JSONL usage blocks. Drives intentional handoff before compaction |
qcm_precompact_handler.py (v2.2.0) | PreCompact | Defensive fallback: clears threshold_alerts + wrap_up_completions if compaction fires anyway (signal that handoff discipline failed) |
Priority tiers:
| Priority | What gets captured | Snapshot budget |
|---|---|---|
| Session Goal | First user intent (pinned, never ages out) | 200 bytes (guaranteed) |
| P1 (critical) | User requests, project focus, errors | 800 bytes |
| P2 (high) | Decisions, script runs, directive reads, agent launches | 600 bytes |
| P3 (medium) | File edits (deduplicated ... same file edited 5x = 1 entry) | 400 bytes |
| Git status | Uncommitted changes ("in-flight" work) | 500 bytes |
| P4 (low) | File reads, searches | Dropped from snapshot |
Threshold ladder (v2.2.0+) ... handoff before compaction:
| Threshold | Hook Alert | Agent Action |
|---|---|---|
| 70% | Awareness | Note marker; identify a clean commit boundary in next stretch. No new long-running background sub-agents past this point ... they may not return before 90% handoff |
| 90% | HANDOFF MODE | Wrap up at next safe point. No new work. Safe = no uncommitted + no in-flight sub-agents + not mid-response. Run /session-wrap-up when safe |
| 95% | EMERGENCY | Compaction imminent. Run /session-wrap-up --scope=essential (Tier 1 only; Tier 2 deferred to fresh session via Step 7 kickoff prompt) |
Token counting uses ground-truth from the Anthropic API (transcript JSONL usage block: input_tokens + cache_read_input_tokens + cache_creation_input_tokens). No estimation. Context window detected via QCM_CONTEXT_WINDOW env var > hook stdin context_window field (future-proof) > DEFAULT_CONTEXT_WINDOW = 1_000_000 constant. Per-session cache in session_context_window table.
Recommended all-day workflow ... handoff at 90%, not marathon sessions:
- Let the threshold monitor drive session boundaries; trust the 90% alert
- Run
/session-wrap-upat the alert to write Step 7 kickoff prompt → fresh session continues with full capacity - Git commits + Step 7 kickoff prompt = the handoff (more reliable than any snapshot)
- Tier 1 / Tier 2 split:
--scope=essentialprotects only non-reconstructable work (commits, memory, System Status, kickoff, sentinel) at 95%; Tier 2 (user manual, transparency sync, AAR) deferred and listed for fresh session - Snapshot system remains as safety net for cases where handoff discipline fails (PreCompact hook fires = that signal)
- Why handoff beats compaction: Compaction preserves "what" (files, scripts, commits) but destroys "why" (reasoning chains, tradeoff analysis, discovered constraints). After 3+ compactions, Claude operates on summaries of summaries with false confidence. Fresh sessions at 100% capacity outperform degraded sessions every time. v2.2.0 operationalizes this preference rather than relying on user discipline.
File locations:
- Hook scripts:
.claude/hooks/qcm_*.py - Session events DB:
.tmp/context/session_events.db(SQLite, WAL mode) - Snapshots:
.tmp/context/snapshots/snapshot_{session_id}.md - Compressed outputs:
.tmp/context/compressed/{hash}.txt - Redundancy reports:
.tmp/context/redundancy/redundancy_{session_id}.md(per-session) andcross_session_patterns.md(cross-session "engram candidates") - DB tables:
sessions,events,compressed_outputs,redundancy_metrics,threshold_alerts(v2.2.0),wrap_up_completions(v2.2.0),session_context_window(v2.2.1) - Hook configuration:
.claude/settings.json(hooks section) - Directive:
005 Operations/Directives/context_management.md
Sibling system: Parallel-Session Worktrees (v1.0.0, Session 378) ... structural fix for the parallel-session-git-collision class of bug. When 2+ Claude sessions work the same repo simultaneously, broader git add operations in one session can sweep uncommitted edits from another into the wrong commit. Helper: 005 Operations/Execution/qcm_worktree_spawn.py (create/list/info/cleanup/prune subcommands). Each parallel session spawns at /home/<VM_USER>/qwu_backOffice_worktrees/<label>/ on throwaway branch session/<label> (separate working tree + index, shared .git store). Full SOP: 005 Operations/Directives/parallel_session_worktrees.md. Trigger criteria: any planned parallel session >15 min, any orchestrator-spawned sub-agent that will commit, any time two terminals are working the repo concurrently.
Redundancy detection (v2.1.0): Inspired by DeepSeek's Engram paper ("Conditional Memory via Scalable Lookup"), the redundancy detector measures how often the agent re-reads the same file or re-runs the same search within a session ... wasted "compute" that could be served from cache. Baseline (52 sessions): 29.2% average redundancy ratio, ~9,471 wasted tokens/session. Files read in 5+ distinct sessions are flagged as "engram candidates" for potential persistent caching. Metrics sync daily to HQ Supabase via sync_hq_agent_efficiency.py and display on the HQ Command Center dashboard.
TWL Preload Hook (v1.0.0, Session 195): A separate UserPromptSubmit hook (.claude/hooks/twl_preload.py) that scans user messages for domain keywords and injects reminders to read relevant Tool Wisdom Libraries. Not part of QCM proper but follows the same hook architecture pattern. 32 domains mapped (28 TWLs on disk). See [[#TWL Preload Hook ⭐ NEW]] for details.
Security design: Zero external dependencies (Python stdlib only), no network access, no credential access, fail-open (never blocks Claude), all data in .tmp/ (ephemeral). Built in-house after security analysis rejected context-mode (npm supply chain risk with 30+ API keys on the VM).
Debugging:
# Check events
python3 -c "import sqlite3; c=sqlite3.connect('.tmp/context/session_events.db'); [print(f'P{r[0]} [{r[1]}] {r[2]}') for r in c.execute('SELECT priority, category, summary FROM events ORDER BY id DESC LIMIT 10')]"
# Check snapshot
cat .tmp/context/snapshots/snapshot_*.md
# Nuclear reset (safe)
rm -rf .tmp/context/
Quick Reference Sheets
Printable reference cards for efficient terminal work. These are designed to be printed and kept nearby while learning.
Terminal Copy/Paste (The Big One!)
Coming from Windows, this is the #1 gotcha:
| Action | Shortcut | Why Different? |
|---|---|---|
| Copy | Ctrl+Shift+C | Ctrl+C kills processes |
| Paste | Ctrl+Shift+V | Matches copy shortcut |
| Paste (alt) | Shift+Insert | Works everywhere |
Line Editing (Readline)
Navigate and edit commands without arrow keys:
| Action | Shortcut |
|---|---|
| Start of line | Ctrl+A |
| End of line | Ctrl+E |
| Back one word | Alt+B or Ctrl+← |
| Forward one word | Alt+F or Ctrl+→ |
| Delete word before cursor | Ctrl+W |
| Delete word after cursor | Alt+D |
| Delete to end of line | Ctrl+K |
| Delete to start of line | Ctrl+U |
| Undo | Ctrl+_ |
| Clear screen | Ctrl+L |
Command History
| Action | Shortcut |
|---|---|
| Previous command | ↑ or Ctrl+P |
| Next command | ↓ or Ctrl+N |
| Search history (reverse) | Ctrl+R then type |
| Cancel search | Ctrl+G |
| Run last command | !! |
| Run last command with sudo | sudo !! |
| Last argument of previous | !$ |
Process Control
| Action | Shortcut |
|---|---|
| Cancel/interrupt | Ctrl+C |
| Suspend (background) | Ctrl+Z |
| End input / exit | Ctrl+D |
| Resume suspended | fg |
tmux Quick Reference
| Action | Shortcut |
|---|---|
| New session | tmux new -s name |
| Detach | Ctrl+B, then D |
| List sessions | tmux ls |
| Attach | tmux attach -s name |
| Kill session | tmux kill-session -s name |
| New window | Ctrl+B, then C |
| Next window | Ctrl+B, then N |
| Previous window | Ctrl+B, then P |
| Window by number | Ctrl+B, then 0-9 |
| Rename window | Ctrl+B, then , |
| Split horizontal | Ctrl+B, then " |
| Split vertical | Ctrl+B, then % |
| Switch panes | Ctrl+B, then arrow |
Claude Code Commands
| Command | What it does |
|---|---|
/help | Show all commands |
/plan | Toggle plan mode |
/clear | Clear conversation |
/compact | Summarize to save context |
/cost | Show token usage |
/quit or Ctrl+C | Exit |
Nano Editor (Quick Edits)
| Action | Shortcut |
|---|---|
| Save | Ctrl+O, then Enter |
| Exit | Ctrl+X |
| Cut line | Ctrl+K |
| Paste | Ctrl+U |
| Search | Ctrl+W |
| Go to line | Ctrl+_ |
Git Essentials
| Command | What it does |
|---|---|
git status | See what changed |
git pull | Get latest from remote |
git add . | Stage all changes |
git commit -m "msg" | Commit with message |
git push | Send to remote |
git log --oneline -5 | Recent commits |
git diff | See unstaged changes |
Em-Dash Pre-Commit Gate (safety net)
A deterministic Layer-3 gate keeps our-voice em-dashes (the ABSOLUTE ban ... use the ellipsis) from drifting into a commit, so the rule no longer relies on memory alone.
| Piece | Path | Role |
|---|---|---|
| Detector | 005 Operations/Execution/check_emdash_staged.py | Scans staged files (git diff --cached); exit 1 if any our-voice em-dash found, 0 if clean. Read-only. |
| Hook | .claude/hooks/emdash_precommit_gate.py | PreToolUse (Bash) scoped to git commit; warn-and-confirm (exit 2 blocks, stderr shown so the agent fixes). |
| Reference | memory/reference_emdash_precommit_gate.md | Design + hardening backlog. |
Asymmetric rule it honors (per memory/feedback_two_rule_asymmetric_em_dash.md): the ban is a VOICE rule (author = us), not a data-handling rule. Exempt (preserve, never flag): any .../Reviews/ path (reviewer/audit artifacts), supporter-source subtrees (folders with a _SUPPORTER-SYSTEM.md marker + known supporter projects ... that content is the supporter's voice), a file whose head contains the literal em-dash-exempt opt-out, and the character-as-label case (a line spelling "em dash" while mentioning the character). Enforced everywhere else our voice is staged: canonical/publishable/internal docs, our-voice code comments, memory files, drafts.
Scope note: the hook fires ONLY when the agent runs git commit via Bash. It never touches the vault auto-committer (which commits outside the agent tool loop), so vault sweeps are untouched. Bypass a genuine missed-exemption with the standard git commit --no-verify.
VS Code Remote Shortcuts
| Action | Shortcut |
|---|---|
| Command Palette | Ctrl+Shift+P |
| Terminal | Ctrl+` |
| New terminal | Ctrl+Shift+` |
| File search | Ctrl+P |
| Find in files | Ctrl+Shift+F |
| Close tab | Ctrl+W |
Termux (Android) Tips
| Action | Method |
|---|---|
| ESC key | Volume Up + Q |
| CTRL key | Volume Up + E |
| Extra keys | Swipe left from right edge |
| Tab completion | Tab key on extra row |
DOE Architecture & Skills System
The QWU Backoffice uses a 3-layer DOE (Directive-Orchestration-Execution) architecture.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ LAYER 1: DIRECTIVE (What to do) │
│ Natural language instructions in Markdown │
│ Location: 005 Operations/Directives/ │
├─────────────────────────────────────────────────────────────────┤
│ LAYER 2: ORCHESTRATION (Decision making) │
│ AI agent reads directives, makes routing decisions │
│ Handles errors, asks for clarification, updates directives │
├─────────────────────────────────────────────────────────────────┤
│ LAYER 3: EXECUTION (Doing the work) │
│ Deterministic Python scripts │
│ Location: 005 Operations/Execution/ │
└─────────────────────────────────────────────────────────────────┘
| Layer | Location | Purpose |
|---|---|---|
| Directive | 005 Operations/Directives/ | SOPs in Markdown defining goals, inputs, tools, outputs |
| Orchestration | AI Agent | Intelligent routing, error handling, learning |
| Execution | 005 Operations/Execution/ | Deterministic Python scripts for API calls, data processing |
Key Principles
- Check for tools first - Before writing a script, check if one exists
- Self-anneal when things break - Fix, test, update directive
- Update directives as you learn - Directives are living documents
- Deliverables in cloud - Use Google Sheets, Slides, etc. for outputs
- Local files for processing only - Everything in
.tmp/is temporary
Skills System
Skills provide domain-specific knowledge and capabilities that agents reference. Each skill has a SKILL.md with YAML frontmatter (name + description) that controls when Claude triggers it.
QWF Program & Voice Skills:
| Skill | Purpose | Dependencies |
|---|---|---|
qwf-brand-voice | Voice profiles for all QWF communications (TIG, WOH, L4G, etc.) | None |
qwf-programs | Program context, audience, sensitivities for all QWF programs | None |
Operations & Audit Skills:
| Skill | Purpose | Dependencies |
|---|---|---|
system-audit | Comprehensive system architecture audit | None |
session-wrap-up | End-of-session documentation sync checklist | None |
process-zoom | Meeting Intelligence Pipeline for Zoom recordings | FFmpeg |
capture-triage | GTD-style inbox triage for Master Capture | None |
vista-social | Social media management via Vista Social API ... 🔴 non-functional: the subscription does not entitle API access (verified 2026-10-05). Posting is by hand until a Vista rep or a plan change enables it. | Python |
tool-wisdom | Query tool-specific wisdom from wisdom.db | None |
dream | Memory health audit (staleness, broken refs, line budgets) | Python |
qa | Browser-based QA testing for CF Pages apps via Playwright | Playwright |
canary | Post-deploy canary monitoring via Betterstack + HTTP probes | Python |
freeze | Restrict edit scope to specific files/directories during focused work | None |
pulse | Quick VM health check ... CPU, RAM, swap, disk, active Claude sessions | None |
Lead Generation Skills:
| Skill | Purpose | Dependencies |
|---|---|---|
lead-generation | Multi-source lead generation (LinkedIn, Maps, Apollo, etc.) | Python |
linkedin-scraping | LinkedIn Sales Navigator scraping | Apify |
gmaps-scraping | Google Maps business scraping | Apify |
apollo-scraping | Apollo.io lead scraping | Apify |
lead-enrichment | Enrich lead lists with emails, reviews, company data | Python |
email-enrichment | Email lookup via Anymail Finder | API key |
review-enrichment | Google reviews + AI sentiment analysis | Python |
friendly-name-enrichment | Clean up formal company names to brand names | LLM |
Visual & Creative Skills (adapted from Nate Herk / AI Automation Society):
| Skill | Purpose | Dependencies | Cost |
|---|---|---|---|
excalidraw-diagram | Editable .excalidraw JSON diagrams | None | Free |
excalidraw-visuals | Hand-drawn PNG images via Kie.ai API | KIE_AI_API_KEY, Node.js | ~$0.02-0.09/image |
nano-banana-images | Hyper-realistic photos via Kie.ai Nano Banana 2 | KIE_AI_API_KEY, Python | ~$0.04-0.09/image |
frontend-design | Anti-AI-slop design guidelines for distinctive UIs | None | Free |
video-to-website | Scroll-driven animated websites from video files | FFmpeg | Free |
.claude/skills/
├── qwf-brand-voice/ # Voice profiles (TIG, WOH, L4G, etc.)
├── qwf-programs/ # Program context and audience
├── system-audit/ # System architecture audit
├── session-wrap-up/ # End-of-session checklist
├── process-zoom/ # Meeting intelligence pipeline
├── capture-triage/ # Inbox triage
├── vista-social/ # Social media management
├── tool-wisdom/ # Wisdom database queries
├── lead-generation/ # Multi-source leads
├── linkedin-scraping/ # LinkedIn Sales Navigator
├── gmaps-scraping/ # Google Maps businesses
├── apollo-scraping/ # Apollo.io leads
├── lead-enrichment/ # Lead data enrichment
├── email-enrichment/ # Email lookup
├── review-enrichment/ # Google reviews
├── friendly-name-enrichment/ # Company name cleanup
├── excalidraw-diagram/ # Editable diagrams (JSON)
├── excalidraw-visuals/ # Hand-drawn PNGs (Kie.ai)
├── nano-banana-images/ # Hyper-realistic photos (Kie.ai)
├── frontend-design/ # Design guidelines
├── video-to-website/ # Scroll-driven animated sites
├── dream/ # Memory health audit
├── qa/ # Browser QA (Playwright)
├── canary/ # Post-deploy monitoring
├── freeze/ # Edit scope restriction
└── pulse/ # VM health check
Agent Templates
Agents live in .claude/agents/:
.claude/agents/
├── qwf-master-router.md # Routes incoming work
├── qwf-creative-director.md # Oversees creative production
├── qwf-writer.md # Content creation
└── qwf-visual-designer.md # Graphics execution
MCP Server Configuration
The workspace uses MCP (Model Context Protocol) servers for tool access. MCP servers are configured at the project level in .mcp.json (gitignored, contains tokens) and are opt-in per session to conserve memory.
File: .mcp.json (project root, NOT committed to git)
{
"mcpServers": {
"discord-mcp": {
"type": "stdio",
"command": "npx",
"args": ["-y", "discord-mcp@latest"],
"env": { "DISCORD_BOT_TOKEN": "..." }
},
"apify": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@apify/actors-mcp-server"],
"env": { "APIFY_TOKEN": "..." }
}
}
}
Settings: .claude/settings.local.json has enableAllProjectMcpServers: false ... new sessions do NOT auto-spawn MCP servers. Enable per-session via /mcp when needed.
Why opt-in: Each MCP server spawns ~3 Node.js processes (~160 MB). With 5 concurrent sessions, auto-spawning Discord MCP alone consumed 786 MB. Most sessions don't need Discord access, so opt-in saves significant memory on the 16 GB VM.
To enable Discord MCP in a session: Use the /mcp command within the session to connect.
Supervisor Architecture
The QWU Backoffice uses five domain-specific supervisors that orchestrate automated script execution across the entire system. Each supervisor owns a domain, runs its scripts on a schedule via n8n, logs results to SQLite, and escalates failures to Discord.
Why Supervisors Exist
Before supervisors, a single router agent handled all automation. As the system grew to 60+ scripts, the router became a bottleneck ... errors compounded, scheduling conflicted, and failures in one domain could block others. The supervisor architecture splits responsibility into five independent domains that run in parallel.
The Five Supervisors
| Supervisor | Domain | Schedule | Scripts |
|---|---|---|---|
| Operations | System health, briefings, audits | 6 AM + 6 PM Pacific | 7 |
| Lead Intelligence | Lead scraping, enrichment, BNI | Daily 6 AM (check mode) | 24 |
| Content Pipeline | Content queue, expert monitoring, YouTube, wisdom | Every 30 min (queue) + Daily 7 AM (full) | 21 |
| Relationship Intelligence | Email, calendar, meetings, Ezer AI, BNI 1-2-1 | Every 15 min + Daily 9 PM + Thu 12 PM | 36 |
| Student Programs | MP tracker, program inquiries | Weekly Mon 9 AM | 2 |
How It Works
┌──────────────┐ ┌────────────────────┐ ┌──────────────────┐
│ n8n │────▶│ Supervisor .py │────▶│ Scripts │
│ (schedule) │ │ (orchestrator) │ │ (deterministic) │
└──────────────┘ └────────────────────┘ └──────────────────┘
│
┌────────┴────────┐
▼ ▼
┌──────────┐ ┌──────────────┐
│ SQLite DB │ │ Discord Alert │
│ (logging) │ │ (escalation) │
└──────────┘ └──────────────┘
- n8n triggers the supervisor on schedule (SSH to backoffice VM)
- Supervisor checks the cross-domain queue for tasks from other supervisors
- Supervisor runs its pipeline scripts in sequence
- Each script result is logged to the supervisor's SQLite database
- On failure, the supervisor retries (up to 2x), then escalates to Discord
#supervisor-alerts - On completion, the supervisor logs the run summary and notifies Discord
Key Files
| File | Purpose |
|---|---|
005 Operations/Execution/supervisor_base.py | Abstract base class all supervisors inherit from |
005 Operations/Execution/operations_supervisor.py | Operations & Admin supervisor |
005 Operations/Execution/lead_intelligence_supervisor.py | Lead Intelligence supervisor |
005 Operations/Execution/content_pipeline_supervisor.py | Content Pipeline supervisor |
005 Operations/Execution/relationship_intelligence_supervisor.py | Relationship Intelligence supervisor |
005 Operations/Execution/student_programs_supervisor.py | Student Programs supervisor |
005 Operations/Execution/generate_supervisor_dashboard.py | Health dashboard generator |
005 Operations/Execution/error_handling.py | Centralized error handling: Discord alerts + escalation file creation (v2.1.0) |
005 Operations/Dashboards/Supervisor-Health.md | Generated health dashboard |
005 Operations/Workflows/supervisor-rollback.md | Emergency rollback procedures |
Databases
Each supervisor maintains its own SQLite database in 005 Operations/Data/:
| Database | Tables |
|---|---|
005 Operations/Data/operations_supervisor.db | supervisor_runs, script_executions |
005 Operations/Data/lead_intelligence_supervisor.db | Same schema |
005 Operations/Data/content_pipeline_supervisor.db | Same schema |
005 Operations/Data/relationship_intelligence_supervisor.db | Same schema |
005 Operations/Data/student_programs_supervisor.db | Same schema |
Escalation Routing
When a supervisor detects a failure requiring human attention, it escalates via two channels:
- Discord
#supervisor-alerts... Immediate visibility (viaescalate_to_discord()) - Escalation file ... Audit trail in
000 Inbox/___Supervisor_Escalations/(viaescalate_to_task()inerror_handling.pyv2.1.0)
These escalation files are intentionally kept separate from ___Tasks/ so they don't pollute the user's HQ Command Center task list. As defense-in-depth, sync_hq_tasks.py v1.5.0 also skips any files with a SUPERVISOR- prefix.
Running Supervisors Manually
# Run with JSON output (used by n8n)
python "005 Operations/Execution/operations_supervisor.py" --json
# Run specific pipeline
python "005 Operations/Execution/content_pipeline_supervisor.py" --pipeline queue --json
# Dry run (shows what would execute)
python "005 Operations/Execution/lead_intelligence_supervisor.py" --dry-run --json
# Run a single script through the supervisor
python "005 Operations/Execution/content_pipeline_supervisor.py" --script newsletter_monitor.py --json
Cross-Supervisor Queue
Supervisors can create tasks for each other via 000 Inbox/___Supervisor_Queue/. Tasks are YAML-frontmatter Markdown files with from, to, and priority fields. The receiving supervisor picks up tasks on its next run and moves completed tasks to 000 Inbox/___Supervisor_Completed/.
Health Dashboard
Generate a health dashboard at any time:
# Quick summary
python "005 Operations/Execution/generate_supervisor_dashboard.py"
# Full markdown dashboard
python "005 Operations/Execution/generate_supervisor_dashboard.py" --markdown
# JSON for programmatic use
python "005 Operations/Execution/generate_supervisor_dashboard.py" --json
n8n Workflows
Each supervisor has a corresponding n8n workflow:
| Supervisor | Workflow ID | Webhook Trigger |
|---|---|---|
| Operations | <WORKFLOW_ID> | (schedule only) |
| Lead Intelligence | <WORKFLOW_ID> | POST /webhook/lead-intelligence-trigger |
| Content Pipeline | <WORKFLOW_ID> | POST /webhook/content-pipeline-trigger |
| Relationship Intelligence | <WORKFLOW_ID> | POST /webhook/rel-intel-trigger |
| Student Programs | <WORKFLOW_ID> | POST /webhook/student-programs-trigger |
Troubleshooting
Dashboard shows "CRITICAL" but supervisors are running:
Supervisors with ON_DEMAND_SCRIPTS sets (Content Pipeline v1.1.0+, Lead Intelligence v1.1.0+) automatically skip scripts that need user-provided inputs during automated runs. If you still see failures, check for scripts missing from the ON_DEMAND_SCRIPTS set ... they may need required arguments (--sheet-url, --json-file, etc.) that the supervisor can't provide. Fix: add the script to ON_DEMAND_SCRIPTS in the supervisor.
A supervisor isn't running on schedule:
- Check n8n:
ssh <VM_USER>@qwu-n8n "docker exec n8n n8n list:workflow" - Verify the workflow is published (not just active)
- Check recent executions in n8n UI at https://n8n.quietlyworking.org
Script failures escalating to Discord too often: The supervisor retries each script up to 2 times before escalating. If a script consistently fails, either fix the script or remove it from the supervisor's script list until it's ready.
Cross-supervisor tasks not being picked up:
Check 000 Inbox/___Supervisor_Queue/ for stuck tasks. Verify the to: field in the YAML frontmatter matches the receiving supervisor's domain_name.
Rolling back to legacy workflows:
Follow 005 Operations/Workflows/supervisor-rollback.md for emergency rollback procedures. Legacy workflows are deactivated (not deleted) and can be re-enabled with a single command.
Morning Briefing System ⭐
The Morning Briefing is an automated daily summary that surfaces what matters when you begin work.
What It Shows
| Section | Source |
|---|---|
| Today's Schedule | Google Calendar (Main + Alerts & Reminders calendars) |
| Priority Tasks | ___Tasks/ files with priority: critical or high |
| Due Today | Tasks with due: matching today's date |
| Overdue | Tasks past their due date |
| Blocked | Tasks with status: blocked or blocked_by: set |
| Needs Decision | Items in ___Review/ folder |
Running the Briefing
Manual (via Claude Code):
Let's do a morning briefing
Script (direct):
cd ~/qwu_backOffice && source .env
python "005 Operations/Execution/morning_briefing.py"
Options:
--dry-run # Preview without writing
--discord # Also post to Discord (default: daily note only)
--no-daily # Skip daily note append
--json # Output JSON for n8n
Output Locations
| Output | Location |
|---|---|
| Daily Note | 001 Daily/YYYY/YYYYMMDD.md (appended) |
| Discord | #daily-digest channel (if --discord flag used) |
| Log | .tmp/logs/YYYY-MM-DD.log |
Project Task Scanning
The briefing also scans all active projects for tasks:
- Active Tasks section - Items in
## Active Taskswithin project_Overview.mdfiles - Incomplete checkboxes - Any
- [ ]items in project files
Project tasks inherit the project's priority level.
Day Boundary Feature
Late-night work handling: If you're working past midnight (before 4 AM), the briefing logs to "yesterday's" date. This prevents late-night sessions from bleeding into the next day's records.
2:00 AM on Jan 8 → logs to Jan 7's daily note
5:00 AM on Jan 8 → logs to Jan 8's daily note
This feature is powered by the Canonical Datetime System via the effective_date() function in qwu_datetime.py.
Daily Summary System ⭐
End-of-session summaries capture completed work, decisions, and next actions for institutional memory.
When to Use
Run the summary when wrapping up a work session:
Let's summarize this session
or
Wrap up for the day
What Gets Captured
| Element | Purpose |
|---|---|
| Tasks Completed | What was accomplished with goal alignment |
| Strategic Context | How work fits larger plans |
| Key Decisions | Decisions made with reasoning and implications |
| Files Changed | Git diff summary |
| Blockers & Resolutions | Issues hit and how they were resolved |
| Apify Costs | 30-day Apify spend with per-actor breakdown (via collect_apify_costs.py) |
| Open Questions | Unresolved items for future sessions |
| Proposed Next Actions | Prioritized by goal alignment |
Output Format
## Session Summary - 2026-01-08
### What Was Accomplished
- Implemented morning briefing calendar integration - *Advances: M5 milestone*
- Fixed inbox processing bug - *Advances: Automation reliability*
### Key Decisions Made
| Decision | Reasoning | Implications |
|----------|-----------|--------------|
| Use day boundary | Prevents late-night date confusion | Work before 4am counts as yesterday |
### Proposed Next Actions
1. **[High]** Complete user manual update *(Advances: Documentation)*
2. **[Medium]** Test calendar integration edge cases
Integration Points
The summary system can optionally read:
002 Projects/_Goals and Priorities.md- For goal alignment002 Projects/_Current Roadmap.md- For roadmap context
Google Calendar Integration
The backoffice integrates with Google Calendar for morning briefings and scheduling awareness.
Configured Calendars
| Calendar | Purpose | Environment Variable |
|---|---|---|
| Main | Appointments, day blocking | GOOGLE_CALENDAR_MAIN |
| Alerts & Reminders | Alerts, reminders, payment schedules | GOOGLE_CALENDAR_ALERTS |
| Timeslots | Future: goal time blocks | GOOGLE_CALENDAR_TIMESLOT |
Setup Requirements
- Google Cloud Service Account with Calendar API enabled
- Credentials JSON file stored securely on VM
- Calendar sharing with service account email
Environment Variables
GOOGLE_CALENDAR_CREDENTIALS="/path/to/service-account.json"
GOOGLE_CALENDAR_MAIN="<ADMIN_EMAIL>"
GOOGLE_CALENDAR_ALERTS="[email protected]"
GOOGLE_CALENDAR_TIMESLOT="[email protected]"
Testing Calendar Integration
cd ~/qwu_backOffice && source .env
python "005 Operations/Execution/calendar_events.py" --dry-run # Validate credentials
python "005 Operations/Execution/calendar_events.py" # Fetch today's events
python "005 Operations/Execution/calendar_events.py" --json # JSON output
Two calendar identities: the service account, and one person's consent (2026-09-23)
Everything above runs as the service account, and that is still the default for every calendar write in the backoffice. It has exactly one thing it cannot do:
403 forbiddenForServiceAccounts... "Service accounts cannot invite attendees without Domain-Wide Delegation of Authority."
It creates, moves and deletes events on a shared calendar all day. It cannot put an attendee on one. Discovered live while building the QNT mentor calendar invitation, on a throwaway event that was deleted.
Domain-Wide Delegation was rejected and should stay rejected by default. It would let one key act as ANY person in the domain for the granted scope, which is the opposite of the narrow-identity discipline the calendar-only readers already follow. The narrow path instead:
| Identity | Credential | Used for |
|---|---|---|
| Service account | GOOGLE_CALENDAR_CREDENTIALS | Every calendar read and write that carries NO attendee. Unchanged. |
| One person's consent | .credentials/google-calendar-oauth-token.json (mode 600, scope calendar.events only) | ONLY a call that carries an attendee. |
Set up with 005 Operations/Execution/setup_calendar_oauth.py, which is two steps because the VM has no
browser: --url prints the consent link, the browser lands on a localhost page that fails to load (this
is correct), and --code "<that whole URL>" exchanges it. --check says whether the token works.
calendar_events.get_calendar_service_as_user() reads it, and calendar_booking.py (v1.8.0+) takes
as_user=True on create_appointment / update_appointment / cancel_appointment / get_event.
Callers pass it only when the call actually carries an attendee ... never as a blanket default.
Proving an invitation works: never invite the organizer to their own event. Google sends no invitation email to yourself, so a self-invite proves the mechanism and says nothing about delivery. Use a different address that still reaches only the tester (plus-addressing works), and expect three emails across create, move and delete.
MP Training Opportunities (from the 2026-10-04 Day-and-Focus build)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
undefined is not the same as "empty" | Google omits the attendees key entirely from an event nobody else is on, so a solo event and a half-fetched event look IDENTICAL to a guard. The fix is to make the caller assert it did a full read, so the ambiguity is resolved by the one place that can know it. A student who learns this stops writing if (!x.length) on data they did not fetch themselves | Intermediate |
| A fail-closed rail is tested by naming what must be REFUSED | 22 fixtures, and almost none of them check the happy path: a truthy-but-not-true flag, a malformed list, the attendee shape Google verbatim returns, a value trying to break out of its fence. Teaches that a security test is an enumeration of refusals, not a demonstration that the feature works | Intermediate |
| Shape-based secret detection cannot see an opaque token | Every rule in the scanner anchored on a vendor prefix. A refresh token is twelve lowercase characters with no prefix, so it was invisible ... and it was the MORE dangerous of the two, because it never expires. The fix is to stop reading the value and read the field name it was filed under. Teaches threat modelling by failure mode rather than by pattern | Advanced |
| Redaction has to scale with the secret | Showing 4 leading + 4 trailing characters is safe on a 40-character key and discloses two thirds of a 12-character one. A rule that was correct became wrong when the data changed underneath it, with no code edit. Teaches that a security control has preconditions, and they expire | Advanced |
| Sizing type by visual angle, not by taste | x-height read from the actual font binary (0.600 for the display face, not the assumed 0.5 ... a 20 % error), WCAG contrast computed and asserted per token, and a test that re-derives every ratio so a COMMENT can never drift into a lie. Crosses design and engineering in one exercise | Intermediate |
| An absolute-pixel layout has never met a window | The same design was correct on paper and cut in half on a real monitor, because the page is a browser window and not the panel it was measured for. Teaches the difference between a reference install and a deployment target | Intermediate |
| A green test suite can be wrong about the thing that matters | A type-check, 75 unit tests and a pixel-diff of a printed page were ALL green while the live screen was visibly broken in two different ways. Teaches the test pyramid honestly: each layer proves something real and none of them proves the user can use it | Advanced |
| Proving an exemption still holds after you change the rule | After widening the secret filter, the question is not "does it catch more?" but "does it still let the PUBLIC keys through?" Sixteen real public keys in eight contexts, plus twenty-one secret ones, both directions asserted. Teaches regression thinking as a two-sided obligation | Advanced |
Most valuable MP exercise: give a student the write path and the attendee rail, and a calendar
containing one event with a guest on it. Ask them to add a private note to every event. The unit
tests will pass. Then ask them to prove, against the live calendar, that the one event with a guest
was left untouched ... and then to break their own guard by passing attendeesRead: 'true' as a
string. Every step is a real lesson, and it ends on the habit that matters most here: a rail you
have not tried to defeat is a rail you have not tested.
MP Training Opportunities (from the 2026-10-04 em-dash sweep and first Claude Code mod)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| A regex for a delimited block must be ANCHORED | ```.*?``` is not anchored to line starts, so it paired the wrong fences and quietly reclassified 213 lines of prose as code. The script then reported those as deliberately preserved. A perfect, self-contained lesson in why lazy matching is not the same as correct matching | Beginner |
| Verify at the OUTPUT, never at the tool's own report | Four source sweeps all returned "clean" while the live page still showed the glyph. Re-running the script would never have found it; fetching the rendered page did it in one command. The habit generalises to every pipeline a student will ever build | Beginner |
| A file in a repo is not a source of truth because the code imports it | Editing the site's data file changed nothing, because the deploy workflow overwrites it at build time from an API. Teaches students to read the build before deciding where content lives | Intermediate |
| A literal-character search misses the escaped form | The same character was written \u2014 in JSON, so a search for it returned zero while a browser rendered it perfectly. Teaches encoding-awareness concretely | Intermediate |
| Never run a text transform over a file that is entirely code | A blind find-and-replace hit a regex character class and turned it into a syntax error. The build rejected it, which is its own lesson: your CI is a real safety net and you should want it strict | Intermediate |
| Adding prevention upstream can silently blind an existing detector | A new filter rewrote the value before the old scanner read it, so the scanner matched nothing and alerted never, while both halves reported perfectly healthy. The only check that can see this asks "did the PAIR still catch it?" | Advanced |
| Event-driven middleware, via a real Claude Code mod | ($, e, next) is the same shape as Express, Rack and Django middleware. Observe, rewrite, or answer. A student who understands this one function signature understands a pattern that is everywhere | Advanced |
| Reading the threat model of an extension system before installing anything | A mod is unsandboxed and bypasses every file-permission rule in the host config. Teaches students to ask "what can this reach?" before "what can this do?" | Advanced |
Most valuable MP exercise: the sweep that kept lying. Give a student a source file with 200 em dashes, a cleaning script with the unanchored fence regex, and the live URL. Ask them to make the published page clean. The script will tell them they succeeded. The page will disagree. Every step of the correction is teachable, and it ends on the habit that matters most: the tool's report is a proxy; the rendered output is the truth.
MP Training Opportunities (from the 2026-09-23 calendar-invitation build)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Differential testing of two implementations against ONE fixture ... the same logic written in Python and TypeScript, pinned byte-for-byte, then fuzzed against each other | The highest-value lesson of the session. Five happy-path fixture cases looked like proof and were not: a fuzz over 143 inputs found six real divergences. Teaches that a passing test suite is a claim about the tests. | Advanced |
| Reading an API's actual error instead of reasoning about the design | The whole feature was structurally impossible with the identity it ran as, and no amount of code review would have said so. One throwaway call to the real API did. | Intermediate |
| OAuth 2.0 consent on a machine with no browser (auth URL, the loopback redirect that fails on purpose, exchanging the code, storing a refresh token at mode 600) | Every ops engineer meets this and most of them copy a snippet without understanding which half runs where. | Intermediate |
| Least-privilege identity choice under pressure ... Domain-Wide Delegation was fewer clicks and was refused | Teaches that "it works" and "it is the right permission" are different questions, and how to say why in one sentence. | Intermediate |
| Two clocks are not one clock ... a script-stamped column compared with a database-trigger-stamped one | A bug that had been latent for weeks and would have emailed a real person every night. Teaches reading a condition for who writes each side of it. | Advanced |
| RFC 5545 by hand (CRLF, escaping, folding at 75 OCTETS without splitting a codepoint, UID and SEQUENCE semantics) | A real spec, small enough to read end to end, with a visible result in the student's own calendar. | Beginner to Intermediate |
Portfolio value: high. The artifact is small, self-contained, testable, and the student can see the result land on their own phone.
MP Training Opportunities (from the 2026-09-23 auto-save + walk-through-page build)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Debounced auto-save as a state machine, not a timer ... one save in flight, one more queued behind it, a status line that describes the SCREEN rather than the last request | The strongest unit of the session. The first version passed its own tests and still had two reachable bugs a fresh reviewer found: a success handler that printed "All changes saved." over an edit made while the request was in the air, and a validity gate that let one tap on a trash icon commit an empty list. Teaches that "the save succeeded" and "the screen is saved" are different claims. | Advanced |
| Writing a rule so the code cannot contradict the words on screen ... the page says "Max is the most meetings within that particular window", so both booking engines were changed to count per window | Most students learn to make the copy match the code. This is the other direction, done deliberately and only when the words are right. Teaches reading a sentence as a specification. | Intermediate |
| Capturing product documentation from a demo lane with Playwright (screens, a short clip, the notice rendered from the real composer with sample names) so no real person's data is ever in a public asset | A privacy rail a student can see and test: the same page, two data sources, one of which may never be published. | Intermediate |
Verifying a generated clip's first AND last frame before publishing (ffmpeg -ss / -sseof / -frames:v 1) | A two-second habit that caught an over-scrolled empty ending which the file size, the duration and the poster all called fine. | Beginner |
| Rendering someone else's blessed words verbatim ... placeholders substituted, spelling fixed and disclosed, nothing "improved" | A professional discipline more than a technical one, and the one most students get wrong first. Pinned here by a test that asserts the rendered body equals the blessed string. | Beginner |
| Ground truth for a flag is the value the code reads, never a status document | Learned the hard way this session: a status row said a feature was off while the environment and the code said it was on, and a person was offered a choice built on the wrong answer. | Intermediate |
Portfolio value: high. Every unit is small, has a visible result, and the auto-save one is a real interview question with a real wrong answer.
MP Training Opportunities (from the 2026-10-03 silent-reader diagnosis)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Diagnosing a system that failed by saying nothing ... the only evidence was a log line repeating every 15 minutes | The hardest class of bug to teach, because there is no stack trace, no red test and no alert to start from. The entry point was a human noticing the absence of an email. Teaches that "nothing happened" is a symptom. | Advanced |
| Executing the check instead of reading the code ... import the real function, feed it the real message bodies, print what it returns | The regex was read correctly and the conclusion was still worth nothing until it was run against all nine real emails (six parsed, three did not). One command turned an argument into evidence. Teaches the difference between a plausible explanation and a verified one. | Beginner |
| Asking someone to break your own finding ... an independent reviewer briefed to attack, not confirm | It worked: the reviewer agreed the parser was broken but proved the parser was NOT why nobody got an email (the applicant's report had never been written for an unrelated reason). The correction changed what was reported to a decision-maker. Teaches that confirmation from yourself is worth less than opposition from someone else. | Intermediate |
| Making a failure path reachable before making it loud | The alert could not simply be added: the early return for "no work" sat above the notifier, so the alarm was unwritable. Teaches reading control flow for what it forbids, not just what it does. | Intermediate |
Spotting a date filter that hides unfinished work ... meeting_date >= today on a to-do list | Un-hiding it revealed four people stranded for weeks. Teaches that a due date orders work but does not discharge it, and that the backlog a fix reveals is often the real finding. | Beginner |
| A green test suite that proves nothing ... 37 passing tests, all written against the format the counterparty had just abandoned | A genuinely uncomfortable lesson and a memorable one. Teaches that tests encode the past, and that a reader of someone else's format needs a fixture added the moment a real input is rejected. | Intermediate |
Portfolio value: very high. This is the session to show a student who thinks debugging means reading an error message. There was no error message, and the cost was a person.
MP Training Opportunities (from the 2026-10-21 calendar-words change, Session 765)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Checking both layers before agreeing with a bug report ... the system state AND the words on screen | TIG reported that picking a time should count as saying yes. One database read showed it already did; only the calendar entry's wording was wrong. Saying "the logic is broken" would have sent someone to fix code that worked. | Beginner |
| Words in a saved artifact must stay true for its whole life | A calendar entry, a PDF or a printed sheet is a snapshot that outlives the moment it was made. The fix was an always-true sentence ("Need a different time?"), not wording that depends on the member's answer, which would have gone stale for anyone who added the entry before saying yes. | Beginner |
| Changing a value that two implementations share ... edit the reference (Python), regenerate the shared fixture from it, copy it byte-identical, and let the other side's tests prove agreement | The same sentence lives in a Python email builder and a TypeScript page/server builder. The fixture is the contract, so the student never hand-edits an expected string. | Intermediate |
| Prove before and after on the live system ... read the real calendar file before deploying (old words), deploy, read it again (new words), and compare the live app bundle byte-for-byte with the local build | A deploy that "succeeded" is a claim; a before/after read is evidence. | Beginner to Intermediate |
Portfolio value: medium. Small and complete; the strongest unit is the first one, because it is a habit, not a tool.
Google Calendar API Timestamp Gotcha (RFC3339)
The Google Calendar API requires RFC3339 timestamps with explicit timezone offset for timeMin/timeMax parameters ... not bare ISO 8601 timestamps. This is a subtle but critical distinction:
| Format | Example | Works? |
|---|---|---|
| RFC3339 with offset | 2026-02-28T00:00:00-08:00 | Yes |
| RFC3339 with Z (UTC) | 2026-02-28T00:00:00Z | Yes |
| Bare ISO 8601 | 2026-02-28T00:00:00 | No ... 400 Bad Request |
When building Supabase Edge Functions (TypeScript/Deno) that call Google Calendar API, always include the timezone offset. For Pacific time, calculate the offset dynamically to handle DST:
// Determine Pacific offset (PST = -08:00, PDT = -07:00)
const jan = new Date(now.getFullYear(), 0, 1).getTimezoneOffset();
const jul = new Date(now.getFullYear(), 6, 1).getTimezoneOffset();
const stdOffset = Math.max(jan, jul);
const isDST = now.getTimezoneOffset() < stdOffset;
const tzOffset = isDST ? '-07:00' : '-08:00';
// Build RFC3339 timestamp
const timeMin = `${year}-${month}-${day}T00:00:00${tzOffset}`;
This was the root cause of the HQ Command Center calendar edge function failure (Feb 2026). AI code generators (including Lovable) tend to produce bare timestamps without offsets.
Google Docs Sync System
The backoffice supports two-way synchronization between Obsidian markdown files and Google Docs, enabling external collaboration while preserving Obsidian as the primary editing environment.
Use Case
Share documents with non-technical collaborators (advisors, board members, partners) via Google Docs while maintaining source-of-truth in Obsidian. Format conversion ensures readers see native Google Docs formatting ... not raw Markdown or YAML frontmatter.
Architecture
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Obsidian │ push │ Google Docs │ edit │ Collaborator │
│ (Markdown) │───────►│ (Native fmt) │◄───────│ (Browser) │
│ │◄───────│ │ │ │
└─────────────────┘ pull └─────────────────┘ └─────────────────┘
Key Design Decisions:
- Bidirectional sync via
revisionIdtracking (v1.1.0) ... auto-detects remote edits and pulls them - Google Docs wins conflicts (Obsidian backed up before overwriting)
- YAML frontmatter stripped from Google Docs (readers never see it)
- Obsidian syntax converted to readable format (wiki-links → plain text)
- 15-minute automated sync via n8n
Enabling Sync on a File
Add YAML frontmatter to opt-in:
---
title: "QWU Backoffice User Manual [PUBLIC]"
google_doc_sync: true
---
After first sync, additional fields are auto-populated:
---
title: "Document Title"
google_doc_sync: true
google_doc_id: 1Abc123XyZ... # Auto-populated
google_doc_last_sync: '2026-01-10T10:30:00-08:00' # Auto-updated
---
Format Conversion
Push: Obsidian → Google Docs
| Obsidian | Google Docs |
|---|---|
# Heading | Native Heading 1 |
**bold** | Bold text |
*italic* | Italic text |
- bullet | Native bullet list |
[[Page Name]] | Plain text "Page Name" |
#tag | Removed |
> [!callout] | Bold header |
| YAML frontmatter | Stripped (hidden) |
[link](url) | Native hyperlink |
Pull: Google Docs → Obsidian
| Google Docs | Obsidian |
|---|---|
| Heading 1 | # Heading |
| Bold | **text** |
| Hyperlink | [text](url) |
| Original YAML | Preserved from backup/state |
Running Sync Manually
cd ~/qwu_backOffice && source .env
# Dry run - see what would sync
python "005 Operations/Execution/sync_gdocs.py" --dry-run
# Sync all enabled files
python "005 Operations/Execution/sync_gdocs.py"
# Sync specific file
python "005 Operations/Execution/sync_gdocs.py" --file "path/to/file.md"
# Force push local changes
python "005 Operations/Execution/sync_gdocs.py" --force push
# Force pull from Google Docs
python "005 Operations/Execution/sync_gdocs.py" --force pull
# JSON output for automation
python "005 Operations/Execution/sync_gdocs.py" --json
Automated Sync via n8n
Workflow: 005 Operations/Workflows/gdocs-sync-workflow.json
Schedule (15min) → SSH execute script → Parse JSON →
├── Has Activity? → Post to #agent-log
│ └── Has Errors? → Post to #inbox-alerts
└── No Activity → Skip (silent)
Posts to Discord only when there's actual sync activity (pushes, pulls, creates, or errors).
Environment Variables
# Reuses calendar service account (needs Docs API scope enabled)
GOOGLE_DOCS_CREDENTIALS=/path/to/service-account.json
# Default Drive folder for new synced documents
GOOGLE_DOCS_FOLDER_DEFAULT=<drive-folder-id>
# Directories to scan for sync-enabled files (comma-separated)
GOOGLE_DOCS_SYNC_DIRS=100 Resources,002 Projects,003 Entities
Google Cloud Setup
-
Enable APIs in Google Cloud Console:
- Google Docs API
- Google Drive API (if not already)
-
Service account setup:
- Can reuse existing calendar service account
- Needs scopes:
documents,drive.file
-
Create Drive folder:
- Create folder in Google Drive
- Share with service account email (Editor permission)
- Copy folder ID from URL
Execution Scripts
| Script | Purpose |
|---|---|
sync_gdocs.py | Main sync orchestrator |
gdocs_converter.py | Bidirectional format conversion |
State and Backups
| Location | Purpose |
|---|---|
.tmp/sync_state/gdocs_sync.json | Tracks sync state per file |
.tmp/backups/gdocs/YYYY-MM-DD/ | Backups before overwrites |
.tmp/logs/YYYY-MM-DD.log | Sync operation logs |
n8n Environment Variables Used
$env.DISCORD_WEBHOOK_AGENT_LOG - Sync activity notifications
$env.DISCORD_WEBHOOK_INBOX_ALERTS - Error alerts
Note: Self-hosted n8n uses $env.VARIABLE_NAME syntax (not $vars).
Video Content Pipeline
Transform YouTube videos into content assets (articles, social snippets, quotes, intelligence) using Gemini transcription, Claude analysis, and voice profile application.
Architecture
URL → Gemini 2.5 Transcribe → Claude Analyze → Voice Profile → Discord Review → Distribute
(multimodal) (structure) (brand voice) (approve/edit)
Processing a Video
# Full pipeline with Discord review
.venv/bin/python "005 Operations/Execution/process_video_content.py" "https://youtube.com/watch?v=xxx"
# Skip Discord review (just generate drafts)
.venv/bin/python "005 Operations/Execution/process_video_content.py" "https://youtube.com/watch?v=xxx" --skip-discord
# Force frame extraction (override auto-detection)
.venv/bin/python "005 Operations/Execution/process_video_content.py" "https://youtube.com/watch?v=xxx" --frames
# Force skip frames (override auto-detection)
.venv/bin/python "005 Operations/Execution/process_video_content.py" "https://youtube.com/watch?v=xxx" --no-frames
# Dry run (preview without saving)
.venv/bin/python "005 Operations/Execution/process_video_content.py" "https://youtube.com/watch?v=xxx" --dry-run
Output Structure
All generated content is saved to 000 Inbox/___Content/{uid}/:
| File | Contents |
|---|---|
_metadata.json | Source info, status, timestamps, visual_richness assessment |
article.md | Full article draft with voice profile applied |
social.md | Platform-specific social snippets (Twitter, LinkedIn, Instagram) |
quotes.md | Key quotes extracted from video |
intel.md | Internal intelligence summary (for knowledge base) |
transcript.md | Raw Gemini transcription with visual context |
frames/ | Extracted key moment frames (auto or --frames) |
Frame Extraction (v2.2.0)
The pipeline auto-detects whether a video has visual value worth capturing as frames. Claude assesses visual_richness from Gemini's transcript visual descriptions:
| Level | When | Auto-Action |
|---|---|---|
high | Visuals ARE the content (nature docs, demos, animations) | Contact sheet + key moment frames |
low | Some visual value (code on screen, slides) | Key moment frames only |
none | Static setup (talking head, interview, panel) | YouTube thumbnails only |
Requirements: yt-dlp + ffmpeg (installed in .venv). Cloudflare WARP Docker container (warp-socks on port 1080) masks Azure IP as residential ... auto-detected by extract_video_frames.py. Falls back to YouTube auto-generated thumbnails if download fails.
CLI overrides: --frames forces extraction, --no-frames forces skip. No flag = auto-decide.
Standalone frame extraction:
.venv/bin/python "005 Operations/Execution/extract_video_frames.py" "https://youtube.com/watch?v=xxx" --timestamps "0:01 2:15 4:42"
Content Review (HQ Command Center)
As of v1.5.0 (Apr 2026), all content review happens in HQ Command Center. Discord receives transparency notifications only.
- Content pipeline creates draft → writes to HQ Supabase
hq_action_queue - TIG reviews in HQ Action Queue (title, topics, routing suggestions, WP preview link)
- TIG approves/rejects in HQ →
hq_action_logentry created - n8n write-back pipeline (every 5 min) processes: route → adapt → publish → distribute
- Discord receives outcome notification ("Published! 25 posts scheduled across 6 programs")
Legacy: Discord commands (approve/reject/edit) are deprecated in process_content_review.py v1.3.0. CLI --approve/--reject flags still work for direct vault operations.
Pipeline Orchestrator Bug Fix (v1.6.0)
Fixed in Session 218 (April 12, 2026): tig_video_pipeline_orchestrator.py v1.5.0 → v1.6.0.
Root cause: run_content_router() was called at step 2.6 (before the hq_video_articles row was created at step 5). The Supabase PATCH silently matched zero rows because the article record did not yet exist. Result: content_atoms and program_routing fields were never populated in HQ Content Review ... all 153 existing articles were missing this data.
Fix: Moved run_content_router() to step 5.5 (immediately after HQ record insert). All future videos now correctly populate routing data at the right pipeline stage.
Backfill: Full backfill of all 153 existing hq_video_articles rows completed ... zero failures. content_atoms and program_routing now present on all articles in HQ Content Review.
Unified Content Distribution System (v1.0.0)
After approval, content flows through 4 automated phases:
| Phase | Script | Purpose |
|---|---|---|
| P2: Route | route_content_programs.py | Score 7 QWF programs for relevance, generate per-program "Big Why" statements |
| P3: Adapt | adapt_content_voice.py | Voice-adapted social posts per program per platform (tig-standard, woh-combat, l4g-b2b) |
| P4: Distribute | distribute_content_social.py | Schedule 15-25 posts across Vista Social over 14 days |
Content Atoms model: Each video produces atoms (core insight, quotable moments, key stats, visual assets, Big Why statements). Programs assemble their own posts from these atoms. See 005 Operations/Directives/content_distribution.md for full architecture.
Big Why Rule: Every cross-program share must explain why THIS content matters to THAT audience. Never generic "our founder posted this."
Complete Architecture Reference: 004 Knowledge/Articles & Research/content_intelligence_system_architecture.md ... a comprehensive 1,500-line public-facing document covering the full pipeline (all 10 stages), the feeder network (6 automated monitors), tool wisdom libraries, gotchas/learnings, implementation recipes, and cost analysis. Includes Excalidraw visual flow diagram. Created Session 192 for the QWU Public Transparency Project.
Content Lifecycle
000 Inbox/___Content/{uid}/ → Draft (pending_review)
_metadata.json → Status, topics, content_atoms, program_routing
social_variants.json → Voice-adapted posts per program/platform
distribution_log.json → What was posted where and when
000 Inbox/___Approved/{uid}/ → Approved (moved by sync_from_hq)
Article Connection Engine (v1.0.0) ... Interconnected Article Universe
The connection engine builds a semantic knowledge graph across all chaplaintig.com articles, enabling interactive concept maps and cross-article discovery. Every article becomes a node in a living constellation that readers can explore visually.
Script: 005 Operations/Execution/tig_connection_engine.py v1.0.0
Four capabilities in one script:
- Semantic tag extraction ... Claude analyzes each article and extracts 5-8 abstract concept tags (e.g., "resilience", "identity", "trust-building"). These are concepts, not topics ... enabling cross-domain connections (a photography article and a relationships article can both touch "presence").
- Article edge computation ... Computes pairwise similarity scores between all articles using shared semantic tags. Weighted by tag specificity (rare shared tags score higher than common ones). Produces directed edges stored in
article_edgestable. - Constellation graph generation ... Per-article subgraph of the 5-7 most-connected neighbors, pre-rendered as a baked inline SVG with zero runtime cost. No D3.js or CDN dependency on article pages ... the constellation is a static image that only looks dynamic.
- Quote threading ("Echoes") ... Pulls thematically related quotes from other articles based on semantic overlap. Displayed in the article's Section 5 as a "Voices from the Vault" thread.
Database: 005 Operations/Data/tig_graph.db
| Table | Contents |
|---|---|
article_nodes | Post ID, title, URL, semantic tag list, processed timestamp |
semantic_tags | Tag vocabulary with frequency counts |
article_edges | Source post ID, target post ID, similarity weight, shared tag list |
concepts | Concept definitions and cross-domain linkage metadata |
Semantic tags are abstract concepts, not topics. This is the key design decision. A topic tag ("photography") only connects photography articles. A concept tag ("presence") connects photography, relationships, parenting, and mindfulness articles ... creating the cross-domain discovery that makes the constellation feel surprising and alive.
Integration with Article Builder (v1.2.0):
tig_article_builder.py v1.2.0 reads tig_graph.db to embed the constellation and Echoes in every new article:
| Section | Content | Implementation |
|---|---|---|
| Section 2 (2-column) | Left col: Key Takeaways / Right col: Constellation preview | Pre-baked inline SVG, no runtime cost |
| Section 5 | "Echoes" ... thematically related quotes from other articles | Quote thread pulled from article_edges + quote store |
2-column Section 2 layout: Left column shows 3-5 key takeaways as a scannable list. Right column shows the article's constellation ... a small star map of connected articles with the current article as the center node. Clickable nodes link to the connected article's search page (not direct URL ... routes through /search/?semantic=tag to show the discovery path).
Constellation rendering (pre-baked SVG, zero runtime cost): The constellation is computed once when the article is built and embedded as inline SVG in the Divi et_pb_code module. No D3.js loaded on the article page. This approach:
- Zero CDN dependency (no D3 script tag on article pages)
- Zero runtime computation in the reader's browser
- Renders identically across all devices
- No
<br>injection risk from WordPresswpautop
Search discovery via clickable nodes: Clicking a constellation node does NOT go directly to the connected article. It goes to /search/?semantic=tag_name, which shows all articles sharing that concept tag ... revealing the full cluster, not just a single connection, and rewarding exploration.
WordPress content update pattern for Divi (critical):
When pushing complex Divi content (multi-column layouts, inline SVG, embedded JS) to WordPress via SSH, use the SCP file → WP-CLI pattern. NOT PHP eval-file with heredoc string escaping.
# CORRECT: SCP content file → wp post update
scp content.html bitnami@<WP_SERVER_IP>:/tmp/content.html
ssh bitnami@<WP_SERVER_IP> "chmod 644 /tmp/content.html && \
sudo wp post update <id> /tmp/content.html --path=/opt/bitnami/wordpress && \
rm /tmp/content.html"
# WRONG: PHP eval-file with string escaping (breaks on complex Divi content)
# PHP heredoc escaping corrupts quotes, SVG attributes, and JS template literals
# This pattern works for simple PHP updates but fails on Divi shortcode-heavy content
Why string escaping breaks: Divi content contains hundreds of escaped quotes in shortcode attributes, inline SVG path data, and JS template literals. Escaping these for PHP string injection produces cascading corruption. The SCP pattern treats content as a raw file ... no escaping needed. Discovered Session 163 (260328).
Running the connection engine:
# Process all articles and rebuild the full graph
.venv/bin/python "005 Operations/Execution/tig_connection_engine.py" --rebuild
# Process a single article (by WordPress post ID)
.venv/bin/python "005 Operations/Execution/tig_connection_engine.py" --post-id 29578
# Dry run (compute edges, don't write to DB or WordPress)
.venv/bin/python "005 Operations/Execution/tig_connection_engine.py" --dry-run
New article builder features (v1.2.0 vs v1.1.0):
- 2-column Section 2 (Key Takeaways left + Constellation preview right)
- Section 5 "Echoes" ... thematically related quotes from other articles
- Semantic tags passed to
process_video_content.pyLLM prompt (v2.5.0) for consistent extraction - Watch/Read toggle (video embed vs. article text) ... carried from v1.1.0
- Timestamped chapter navigation ... carried from v1.1.0
- TIG Izm pull-quotes ... carried from v1.1.0
- SEO VideoObject JSON-LD ... carried from v1.1.0
Social Video Clip Generation (Remotion)
Automated pipeline generating branded social media video clips from chaplaintig.com articles at scale. Each clip is a 30-60 second kinetic typography animation designed as a teaser linking back to the full article. Target: 791+ articles ready for batch rendering.
Project Location: /home/<VM_USER>/chaplaintig-video-clips/ (outside the vault ... Node.js project)
Directive: 005 Operations/Directives/chaplaintig_video_generation.md ... comprehensive brand generation guide with pacing rules, scene types, color/font tokens, wonder effects, and parameterized input schema.
How it works:
Article content files → generate_creative_brief.py (FLAGSHIP + thinking, ~$0.08/brief)
→ ArticleClipSchema JSON props → Remotion ArticleClip composition
→ Headless Chrome render → MP4 + PNG thumbnail
Component Library (src/):
- Scenes:
HookScene,QuoteScene,TurnScene,LandingScene,CloseScene... each accepts props, auto-calculates internal timing - Shared:
RevealText,AccentLine,BreathingBackground,Particles,SceneLayout - Wonder effects:
EllipsisHeartbeat(auto-detects "..." ... TIG's signature punctuation),ConstellationPulse(thesis reveal) - Schema:
ArticleClipSchema(Zod) ... interface contract between Python orchestrator and React components - Compositions:
ArticleClip-Reels(1080x1920),ArticleClip-Square(1080x1080),ArticleClip-Landscape(1920x1080),ArticleClip-Thumbnail(PNG) - Semantic Color Shift:
accentColorprop flows through all components. WHELHO realm routing: Work Hard =#d4782c, Enjoy Life =#33e8d8, Help Others =#9b3d8f
Brand Rules (locked in):
- Pacing: 10-13 seconds per scene (1-3 lines), 15-17 seconds (4+ lines), 2-3 second gaps between text reveals
- Colors: Dark bg
#0a0a1a, Aurora Teal accent#33e8d8, Stardust text#e8e4f0 - Fonts: PT Serif (quotes, headlines), PT Sans (labels, CTAs) ... loaded via
@remotion/google-fonts - Animation: Spring physics for all text reveals;
random('seed')for deterministic randomness - Mood: "Late night conversation about things that matter" ... contemplative, cinematic, not flashy
Python Orchestration (005 Operations/Execution/):
generate_creative_brief.py... AI-powered quote selection, scene assembly, WHELHO color routingrender_social_clip.py... Brief caching, Remotion render, quality gate (ffprobe), thumbnailbatch_render_clips.py... Resume-safe batch processing with progress tracking
Rendering:
# Single article via orchestrator
python render_social_clip.py --article-uid "20250601-ping-pong-bryant-park"
# Batch (791 articles ready)
python batch_render_clips.py --limit 10
# Direct Remotion render
cd /home/<VM_USER>/chaplaintig-video-clips
npx remotion render ArticleClip-Reels --props='{"hookLine":"...","articleUid":"..."}' --output out/clip.mp4
Render time: ~90 seconds for a 60-second clip on claude-dev VM (2 concurrent threads).
HeyGen AI Avatar Videos (SHELVED)
Status: Evaluated 2026-04-13. Quality not brand-ready ... shelved until rendering improves.
What it is: AI avatar video generation via API. A text prompt produces a complete video with avatar presenter, motion graphics, voiceover, and captions. 1,282 stock avatars available.
Plan: Creator ($24/mo web) + API pay-as-you-go credits (1,371 remaining, 12-month expiry).
Integration:
- API key:
HEYGEN_API_KEYin.env(agent type, namedqwu-backoffice-agent) - 11 Claude Code skills installed at
~/.claude/skills/(create-video, avatar-video, ai-video-gen, text-to-speech, video-translate, video-edit, video-download, video-understand, faceswap, visual-style, heygen) - MCP server configured in
.mcp.json(OAuth ... requires browser, untested from VM) - TWL:
005 Operations/Directives/heygen_tool_wisdom.md
Key findings from evaluation:
- Over-specifying avatar descriptions causes infinite retry loops (wasted 65 credits)
- Let Video Agent pick avatars ... simplified prompts work, detailed ones don't
- API credits are separate from web plan credits (not obvious)
- Actual cost: ~1 credit/second (not ~2 credits/min as docs claim)
- Total generation time: ~11 minutes for a 64-second video
- Output quality: adequate for generic content, not for QWF brand standards
Future exploration: Custom avatar as branded mascot (different quality expectations), Remotion + HeyGen compositing (kinetic typography + avatar segments), video translation (12+ languages).
Environment Variables
GOOGLE_AI_STUDIO_API_KEY="xxx" # Gemini API key
DISCORD_CHANNEL_CONTENT_REVIEW="xxx" # Channel ID for review
DISCORD_WEBHOOK_CONTENT_REVIEW="xxx" # Webhook URL for notifications
Gemini API Billing
The video content pipeline uses Google's Gemini API for video transcription. Understanding the billing tiers is important for production use.
Free Tier Limits:
- 20 requests/day for Gemini 2.5 Flash
- Sufficient for testing and occasional manual processing
- Will hit
429 RESOURCE_EXHAUSTEDerrors when exceeded
Enabling Billing (Production Use):
- Go to Google AI Studio
- Create a new project (or use existing)
- Settings → Billing → Link a billing account
- Create new API key in the billing-enabled project
- Update
GOOGLE_AI_STUDIO_API_KEYin.env
Pricing (Gemini 2.5 Flash):
- Input: $0.30 per 1M tokens
- Output: $2.50 per 1M tokens
- Estimated cost per video: ~$0.003-0.015 (depending on length)
- Daily monitoring of ~20 videos: ~$0.10/day
Cost Tracking: Monitor usage at Google AI Studio → Activity.
Transcript Fallback: Apify Actor
When the primary Gemini transcription path fails (frame limit exceeded on long videos, or yt-dlp/youtube-transcript-api blocked by YouTube on Azure IPs), use the Apify karamelo/youtube-transcripts actor as a fallback:
# Via Apify MCP (when MCP enabled)
# Actor: karamelo/youtube-transcripts
# Input: { "urls": ["https://www.youtube.com/watch?v=VIDEO_ID"] }
When to use: Gemini 2.5 Flash has a frame limit (~30 min of video at default sampling). For longer videos or when direct YouTube access is blocked from the Azure VM IP, Apify's transcript actor retrieves the YouTube auto-generated or manual captions without needing direct video download.
Limitations: Returns text-only transcript (no visual context that Gemini multimodal provides). Best for talk-heavy content (lectures, interviews) where visual richness is low.
Discovered: Session 159 (260326). Azure VM IP is blocked by YouTube for yt-dlp and youtube-transcript-api. Apify actors run on Apify infrastructure, bypassing the block.
Related Files
- Directive:
005 Operations/Directives/process_video_content.md - Scripts:
005 Operations/Execution/process_video_content.py(v2.7.0),extract_video_frames.py(v1.2.0),process_content_review.py,tig_connection_engine.py(v1.0.0),tig_article_builder.py(v1.2.0),youtube_video_utils.py(v1.1.0 ... landscape gate helper),youtube_monitor.py(v1.3.0) - Database:
005 Operations/Data/tig_graph.db(semantic_tags, article_edges, concepts tables) - Workflow:
005 Operations/Workflows/content-review-workflow.json
chaplaintig.com Video-to-Article Pipeline (Session 160+)
Automated pipeline that transforms YouTube playlist videos into full Divi-formatted blog articles on chaplaintig.com. Every video becomes a draft article with video embed, chapters, inline frames, and attribution.
Architecture:
Cron (2 AM Pacific, 15 videos) OR Cron (8 AM/2 PM/8 PM Pacific, 5 videos)
→ tig_video_pipeline_orchestrator.py v1.3.0 [lock file guard — fcntl.LOCK_NB]
→ detect new playlist videos (source_published_at from YouTube metadata)
→ process_video_content.py (Gemini transcribe + Claude article + semantic tags)
→ tig_connection_engine.py (extract tags → compute edges → bake constellation SVG)
→ tig_article_builder.py (Divi template + 2-col Section 2 + Echoes + constellation)
→ WordPress draft (SCP content file → wp post update → rm) [backdated to source_published_at]
→ wisdom_indexer.py (auto-index new article into wisdom.db)
→ generate_constellation_map.py --deploy (auto-regen /map/ page with new graph data)
→ hq_action_queue row (type: video_article, status: pending)
→ HQ Dashboard Action Queue panel (Publish / Edit Draft / Reject)
→ hq_action_log → write_back_dirty_items.py
→ tig_publish_article.py (publish) or mark dismissed (reject)
Key Scripts:
| Script | Version | Purpose |
|---|---|---|
tig_video_pipeline_orchestrator.py | v1.3.0 | Daily entry point: detect → process → build → draft → wisdom index → constellation map regen. Lock file guard, source_published_at backdating |
tig_article_builder.py | v1.2.0 | Divi article with 2-col Section 2 (takeaways + constellation), Section 5 Echoes, Watch/Read toggle |
tig_connection_engine.py | v1.0.0 | Semantic tag extraction, edge computation, constellation SVG generation, quote threading |
generate_constellation_map.py | v1.0.0 | Full-universe D3.js constellation map: tig_graph.db → 85+ nodes → WordPress post 29573 (/map/). Auto-triggered by orchestrator |
generate_tier1_priority_list.py | v1.0.0 | Rank all YouTube videos by visitor value → select Tier 1 (50) across 10 clusters. Output: .tmp/tier1_priority_list.json |
process_video_content.py | v2.7.0 | Gemini transcribe + Claude article + semantic_tags in LLM prompt; step 1b Wisdom Library landscape gate rejects Shorts/vertical clips before Gemini call (--force overrides) |
tig_publish_article.py | v1.2.0 | Publish approved WP drafts + write back wp_post_id (social distribution handled by distribute_content_social.py) |
route_content_programs.py | v1.0.0 | Score content against 7 QWF programs, generate Big Why statements, extract content atoms |
adapt_content_voice.py | v1.0.0 | Voice-adapted social posts per program/platform (tig-standard, woh-combat, l4g-b2b) |
distribute_content_social.py | v1.0.0 | Schedule adapted posts across Vista Social profiles, spread over 14 days |
Frame Verification (Two-Phase):
- Phase 1 (Gemini watching video): Identifies 5-8 key visual moments with descriptions
- Phase 2 (Gemini Vision per frame): Verifies each frame, receiving Phase 1's context to prevent false rejections
Article Template Sections (v1.3.0): (1) Video Hero with Watch/Read toggle, (2) 2-column: Key Takeaways + Constellation preview with backlinks bar (pre-baked SVG), (3) Article body with sticky chapter nav + inline frames + clickable wiki links + "Read Next ↓" anchor in Chapters sidebar, (4) Read Next ... top 3 related articles with one-liners and shared tags, (5) "Echoes" ... authority-ranked wisdom from same expert, shared themes, AND full wisdom.db library (actionable wisdom gets gold #E8B833 border + lightning bolt; authority badges for vendor_official/expert_validated). Dark theme (#0a0a1a), PT Sans/PT Serif, #33e8d8 accent.
Attribution: "Original video by [Channel Name](channel URL) ... Watch on YouTube" with both linked.
Data Files:
- Playlist mapping:
003 Entities/Taxonomies/chaplaintig_playlist_categories.yaml(112 playlists, 10 clusters, zero unmapped) - Processed tracker:
.tmp/tig_processed_videos.json - Frame rules:
005 Operations/Directives/video_frame_extraction.md - Knowledge graph:
005 Operations/Data/tig_graph.db(article_nodes, semantic_tags, article_edges, concepts, social_posts) - Wisdom engine plan:
005 Operations/Directives/chaplaintig_wisdom_engine.md
Test Results: Photography (post 29578), WHELHO Relationships: WIRED body language (29579), NatGeo ping pong (29580), ABC4 dad/son (29581). Session 162: 4 articles published live via HQ Action Queue (first end-to-end pipeline test). Session 163: Interconnection layer deployed ... constellation + Echoes on every article. Session 164: FAA Part 107 (29586), Gaussian Splatting (29587), Houdini (29588) ... all backdated to original YouTube publish dates.
Playlist Mapping Status (Session 164): ALL 112 playlists mapped ... zero unmapped. 10 explore clusters total (explore-spatial-craft added as 10th, WP category ID 109). All Creative Production playlists now tagged with realm-mind.
Full Constellation Map (Session 165): Live at chaplaintig.com/map/ (WP post 29573). D3.js force-directed graph with 85 nodes (33 articles, 27 semantic tags, 4 experts, 10 realms, 11 clusters), 128 edges. Filter controls for realm/cluster/type. Drag, hover tooltips, click-to-navigate. Star field + glow effects. Auto-regenerated by orchestrator v1.3.0 after each pipeline run.
Tier 1 Priority List (Session 165): 46 videos selected from 1553 unprocessed across all 112 playlists. 814M total views, 43 unique playlists, 34 channels. Generated by generate_tier1_priority_list.py. Output: .tmp/tier1_priority_list.json.
Content-Driven Categorization (Session 167): WordPress categories are now assigned by actual video content, not playlist membership. The orchestrator (v1.2.0) runs a scoring algorithm at step 1.5 (after content processing, before article building) that scores topics, key_concepts (from intel.md), semantic_tags, summary_text, title, channel, visual_richness, and suggested_use against signal definitions in 003 Entities/Taxonomies/chaplaintig_content_category_signals.yaml. Playlist mapping is a +1 bonus and fallback only. Discovered 103+ miscategorized videos across 14 playlists (worst: Unreal Engine 37, DaVinci Resolve 25). All 35 existing WordPress articles recategorized. Validated against 5 known mismatches (100% correct).
Batch Processing Server Protection (Session 167): run_tier1_batch.py v1.1.0 adds three safeguards after a MariaDB OOM-kill from 37 back-to-back pipeline runs: (1) 30-second inter-video delay (configurable --delay), (2) default --max 10 batch cap, (3) pre-video memory check via SSH to WPMU ... if available RAM < 2GB, waits 60s and retries 3 times before skipping. 4GB swap file also added to WPMU server (Session 166).
Read Next Backfill & wpautop Fix (Session 172): wp_post_id was NULL for all 112 rows in tig_graph.db ... the column existed but was never populated because tig_publish_article.py didn't write it back after publishing. Matched 41 published WP posts to graph articles via _tig_pipeline_uid post meta. Backfilled wp_post_id in both tig_graph.db and 41 wp_article.json files. Fixed tig_publish_article.py (v1.0.1 → v1.1.0) to auto-write-back wp_post_id on publish. Fixed build_read_next_section() to skip unpublished articles (url=None). Discovered WordPress wpautop breaks <div> inside <a> tags ... creates empty card bars. Fix: use <span style="display:block"> instead of <div> for elements inside anchor tags. Rebuilt Read Next on 37 live articles (4 skipped ... no published neighbors). See memory/feedback_wpautop_gotcha.md.
Smart Linking & Cross-Referencing (Session 168): Five interconnection enhancements deployed across all 62 articles via tig_backfill_enhancements.py v1.0.0: (1) Clickable wiki links ... [[Concept]] resolves to article URL (/?p=ID) from tig_graph.db or falls back to WordPress search (/?s=...); teal for matched, dim for search. (2) WordPress tags ... semantic tags from tig_graph.db auto-created as WP post_tag taxonomy via wp_set_post_terms in PHP eval-file; 423 tags set across 62 articles. (3) Backlink awareness ... "Referenced by X articles" bar below constellation, queries bidirectional article_edges table. (4) wisdom.db-powered Echoes ... third source in build_quote_threads() queries wisdom_query.py by article topics, deduplicates, authority-ranks (vendor_official > expert_validated > community); actionable wisdom gets gold border (#E8B833) + lightning bolt prefix. (5) Read Next ... top 3 related articles as clickable cards with one-liners and shared tags, positioned as standalone Divi section between article body and Echoes; Chapters sidebar includes "Read Next ↓" anchor link. Backfill script fetches wp_post_id from HQ Supabase (not local JSON). Key gotcha: semantic tags live in tig_graph.db, NOT _metadata.json.
chaplaintig.com Personal Site
Architecture
- Platform: SvelteKit on Cloudflare Pages (migrated from WordPress/Divi)
- Repo:
/home/<VM_USER>/chaplaintig/→github.com/QuietlyWorking/chaplaintig.git - Live URL:
chaplaintig.pages.dev(DNS cutover from WordPress pending) - Supabase project:
rtpmnqehreecuccgamxl - Tables:
tig_articles(170),tig_izms(173),tig_wisdom(2,885),tig_graph(3,816 edges) - Design: Dark theme (#0A0A1A), PT Sans/PT Serif, WHELHO realm color system
Homepage Sections (13 total)
All components in src/lib/components/home/:
- HeroQuote ... tagline + Firefly quote + CTAs
- NdeCinematic ... "I died." cinematic with animated heartbeat SVG
- CbsInterview ... original 2014 CBS Radio Eye on the Community interview audio player + link to
/heritage/cbs-2014/transcript - IdentityCards ... 4 identity cards + weaknesses block
- RealmsPreview ... WHELHO 10-realm color grid
- TestimonialsWall ... masonry layout, all testimonials visible
- SpeakingSection ... 5 speaking topic cards + CTA
- BigDream ... dream statement, napkin sketch, 2035 goal
- JourneyTimeline ... 8 milestones (1978-2035), realm-colored
- BuiltFromBroken ... series callout, 4 volume cards
- LatestPosts ... server-loaded from Supabase (4 cards)
- PhotoGallery ... 5 clusters with filter tabs
- ConnectSection ... 9 social links + phone + email
Key Utilities
src/lib/utils/scroll-reveal.ts... IntersectionObserver-based scroll animations (Svelteuse:action). Includes tall-element threshold cap (April 2026) ... for elements taller than half the viewport, effective threshold falls back to ~200px visible so reveal triggers reliably regardless of section height. Gotcha captured insveltekit_tool_wisdom.md.src/routes/(public)/+page.server.ts... Server-side Supabase data loader for latest postssrc/routes/(public)/heritage/cbs-2014/+page.svelte... Canonical primary-source page for the 2014 CBS interview. Audio player + full transcript with pull-quotes. Source MP3 lives instatic/audio/cbs-eye-on-the-community-2014.mp3(re-encoded to mono 64kbps → 11.8MB, under CF Pages' 25MB per-file limit).
Deploy Process
Push to main → GitHub → CF Pages auto-deploys (~60s)
Remaining Tasks
- Photo gallery: placeholder URLs need real WP media images
- Testimonial quotes: verify against WordPress originals
- Built from Broken slugs: match to Supabase article slugs
- DNS cutover: point chaplaintig.com from WordPress to CF Pages
- Pipeline update: article builder needs to push to Supabase (currently WordPress only)
Voice Profiles
Voice profiles define how content should be written for specific personas or brands. They ensure consistent tone, style, and messaging across all generated content.
Location
Voice profiles live in 003 Entities/Voice Profiles/:
003 Entities/
└── Voice Profiles/
├── Chaplain TIG/
│ └── Brand Voice.md
└── [Supporter Organization]/
└── voice.md
Profile Structure
Each voice profile folder contains:
| File | Purpose |
|---|---|
voice.md or Brand Voice.md | Core voice guide with attributes, tone, examples |
| Supporting docs | Linked resources like wisdom libraries |
Available Profiles
| Profile | Path | Vault Version | QWR Supabase Version | Use Case |
|---|---|---|---|---|
| Chaplain TIG | 003 Entities/Voice Profiles/Chaplain TIG/ | v2.2 (260411) | v3 (260326) | Personal content, QWU, Missing Pixel |
| Ezer Aión | 003 Entities/Voice Profiles/Ezer Aión/ | ... | ... | QWU Backoffice assistant, automated outreach, verification |
| [Supporter Organization] | 003 Entities/Voice Profiles/[Supporter Organization]/ | ... | ... | Client: roofing/construction company |
QWR Supabase Voice Profiles
The Chaplain TIG voice profile is stored in QWR's Supabase brands.voice_profile JSON column (brand ID in Supabase). Version history is tracked in the voice_profile_versions table. Updates can be made via:
- Direct Supabase PATCH (preferred for precise, deterministic changes)
- Voice Coach webhook (
n8n.quietlyworking.org/webhook/voice-coach) ... AI-mediated merge, less predictable
v3 additions (260326): Gratitude email arc, evidence-based writing principles ("Show the receipts", "Honest over dramatic"), sentence structure patterns (single-sentence emphasis, stretched words, onomatopoeia), full 17-emoji palette, 23 preferred phrases, 7 anti-patterns. Derived from TIG's actual Nick Saraev thank-you letter.
Listing Available Profiles
# List all discoverable voice profiles
.venv/bin/python "005 Operations/Execution/wisdom_synthesizer.py" --list-voices
Output:
=== Available Voice Profiles ===
[Supporter Organization]
Type: folder
Path: .../Voice Profiles/[Supporter Organization]/voice.md
Chaplain TIG
Type: folder
Path: .../Voice Profiles/Chaplain TIG/Brand Voice.md
Using Voice Profiles
Voice profiles are used by both the video content pipeline and wisdom synthesizer:
# Video processing with default voice
.venv/bin/python "005 Operations/Execution/process_video_content.py" "https://..."
# Video processing with specific voice
.venv/bin/python "005 Operations/Execution/process_video_content.py" "https://..." --voice "Chaplain TIG"
# Wisdom synthesis with voice profile
.venv/bin/python "005 Operations/Execution/wisdom_synthesizer.py" \
--vertical nonprofit --topic ai_adoption --voice "Chaplain TIG"
Creating New Profiles
- Create folder:
003 Entities/Voice Profiles/{Profile Name}/ - Create
Brand Voice.mdwith:- Core voice attributes
- Tone spectrum
- Language preferences
- Format examples
- What to avoid
- Link to supporting resources (wisdom libraries, etc.)
Why Voice Profiles as Entities
Voice profiles are entities (not just templates) because:
- They have identity and evolve over time
- Can link to people:
linked_to: [[Person Name]] - Scale to multiple profiles per person or organization
- Keep operations clean (just reference the profile)
Expert Intelligence System ⭐
Monitor thought leaders and subject matter experts across four platforms (YouTube, Twitter/X, LinkedIn, Newsletters), automatically capturing their new content, indexing it to the wisdom database, and sending Discord notifications.
Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ EXPERT INTELLIGENCE PIPELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │YouTube Monitor│ │Twitter Monitor│ │LinkedIn Monitor│ │Newsletter Mon.│ │
│ │ (6 hours) │ │ (6 hours) │ │ (6 hours) │ │ (12 hours) │ │
│ │youtube_monitor│ │twitter_monitor│ │linkedin_monitor│ │newsletter_mon │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │ │ │
│ └─────────────────┴─────────────────┴─────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Raw Captures (Obsidian-readable) │ │
│ │ 000 Inbox/___Intelligence/{youtube,twitter,linkedin,newsletters}│ │
│ └──────────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Wisdom Indexer (Claude API) │ │
│ │ Classifies by vertical, topic, concern │ │
│ └──────────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ .tmp/wisdom.db (SQLite) │ │
│ │ Queryable database of classified insights │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Discord #intel-digest Notifications │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Missing Pixel Training Opportunity
The Expert Intelligence Pipeline is an excellent Tier 2 curriculum module covering:
- Multi-source data integration - YouTube, Twitter, LinkedIn, Newsletters into unified workflow
- n8n automation - Scheduled triggers, SSH execution, conditional logic, Discord notifications
- API integrations - YouTube Data API, Apify (Twitter/LinkedIn scraping), MS Graph (Outlook)
- Database design - SQLite for wisdom indexing, classification taxonomies
- Cost management - Understanding API billing (Gemini free tier vs paid)
- Troubleshooting - SSH authentication, rate limiting, API quotas
Students can shadow a working production system, then build their own expert monitoring pipeline for a topic of interest.
Expert Roster (Notable Additions)
| Expert | Fields | Priority | Added |
|---|---|---|---|
| Wes Bos | frontend dev, web dev, CSS, JavaScript | ... | 2026-04-12 |
| GaryVee | social media marketing, content strategy, entrepreneurship, brand-building, AI adoption | A | 2026-04-16 |
Wes Bos (003 Entities/Experts/Wes Bos.md) ... Frontend web developer, educator, and co-host of the Syntax podcast. Known for practical CSS/JS courses and tool tutorials. First captured via his "Pretext is a bigger deal than you think" video (UID: 20260412-200733), which triggered the Pretext TWL creation.
GaryVee (003 Entities/Experts/GaryVee.md) ... Gary Vaynerchuk, Chairman of VaynerX and CEO of VaynerMedia. Social media marketing evangelist, content strategy authority, and early-platform adopter (YouTube 2006, Twitter 2007, AI 2023+). Bulk-onboarded with 20 most recent landscape (non-Shorts) videos through the content pipeline, generating ~343 wisdom entries (Stop Wasting Money on Ads ... 22 entries; The Internet Changed... ... 22; 1 man VS 100 Billion AI Creators ... 21). Total wisdom DB footprint: 409 entries across 53 unique sources. Captures span content strategy, AI adoption urgency, brand-as-moat philosophy, social media future, and team/leadership frameworks ... directly applicable to QWR product development and QWF program communications.
Expert Profiles
Expert profiles live in 003 Entities/Experts/ with YAML frontmatter supporting multiple platforms:
---
name: Qiusheng Wu
status: active
priority: A
fields: [gis, geospatial, python, remote-sensing]
platforms:
youtube:
url: https://youtube.com/@giswqs
channel_id: <YOUTUBE_CHANNEL_ID>
frequency: weekly
last_checked: '2026-01-14T12:00:00'
twitter:
handle: giswqs
last_checked: '2026-01-14T12:00:00'
---
Managing Experts
# Add a new expert with YouTube
.venv/bin/python "005 Operations/Execution/expert_registry.py" add "Simon Sinek" \
--priority A --youtube "https://youtube.com/@simonsinek" --fields leadership,culture
# List all active experts
.venv/bin/python "005 Operations/Execution/expert_registry.py" list
# Get specific expert
.venv/bin/python "005 Operations/Execution/expert_registry.py" get "Simon Sinek"
Source 1: YouTube Monitoring
Monitor expert YouTube channels for new full landscape 16:9 videos, then process through the content pipeline. Shorts AND vertical clips (including 1-3 minute "long-Shorts") are automatically filtered out ... creators use these to promote pull-quotes from full content, so capturing them creates noise and duplicates in the Wisdom Library. Detection method: HEAD probe against youtube.com/shorts/<id> ... YouTube redirects landscape videos to /watch (3xx) and keeps Shorts/vertical clips at /shorts/ (200). No API quota used, ~100ms per probe, catches classic Shorts AND long-Shorts regardless of duration (v1.3.0). Direct process_video_content.py invocations are also gated (step 1b); --force overrides.
# Check all channels for new videos
.venv/bin/python "005 Operations/Execution/youtube_monitor.py" --check
# Check A-tier experts only
.venv/bin/python "005 Operations/Execution/youtube_monitor.py" --check --priority A
# Check and process new videos through content pipeline (transcribe + index)
.venv/bin/python "005 Operations/Execution/youtube_monitor.py" --check --process
n8n Workflow: expert-intelligence-workflow.json (every 6 hours)
Source 2: Twitter/X Monitoring
Monitor expert Twitter accounts for recent tweets using Apify.
# Check all experts with Twitter handles for recent tweets
.venv/bin/python "005 Operations/Execution/twitter_monitor.py" --check
# Check and process (capture + index to wisdom database)
.venv/bin/python "005 Operations/Execution/twitter_monitor.py" --check --process
# Check specific expert only
.venv/bin/python "005 Operations/Execution/twitter_monitor.py" --expert "Qiusheng Wu"
# Test specific handles directly
.venv/bin/python "005 Operations/Execution/twitter_monitor.py" --test-handles giswqs,deepseadawn
n8n Workflow: twitter-intelligence-workflow.json (every 6 hours)
Cost: ~$0.40 per 1,000 tweets via Apify (apidojo/tweet-scraper)
Source 3: Newsletter Monitoring
Monitor Outlook inbox for newsletters from whitelisted sources only.
# List whitelisted newsletter sources
.venv/bin/python "005 Operations/Execution/newsletter_monitor.py" --list
# Check for new newsletters (dry run first)
.venv/bin/python "005 Operations/Execution/newsletter_monitor.py" --check --dry-run
# Check and process new newsletters (capture + index)
.venv/bin/python "005 Operations/Execution/newsletter_monitor.py" --check --process
n8n Workflow: newsletter-intelligence-workflow.json (every 12 hours)
Whitelist: 003 Entities/Taxonomies/newsletter_intel_sources.yaml
sources:
nir_eyal:
name: Nir Eyal
email: [email protected]
match_type: email
priority: A
topics: [productivity, behavior-design, habits]
vp_land:
name: VP Land
domain: vpland.io
match_type: domain
priority: B
topics: [drone-mapping, surveying, geospatial]
Source 4: LinkedIn Monitoring
Monitor LinkedIn profiles of watched experts for new posts. LinkedIn has become the professional "town square" as many thought leaders shift from Twitter/X.
# List LinkedIn-enabled experts
.venv/bin/python "005 Operations/Execution/linkedin_monitor.py" --list
# Test with a specific profile
.venv/bin/python "005 Operations/Execution/linkedin_monitor.py" --test-profile satyanadella
# Check for new posts (dry run first)
.venv/bin/python "005 Operations/Execution/linkedin_monitor.py" --check --dry-run
# Check and process new posts (capture + index)
.venv/bin/python "005 Operations/Execution/linkedin_monitor.py" --check --process
n8n Workflow: LinkedIn Intelligence Monitor (mX6DOn0TrNwZO5dY) ... every 3 days at 1 AM Pacific. Changed from 6-hour schedule on 2026-04-04 to optimize Apify costs (~$60/mo savings). LinkedIn content persists on profiles for months, so 3-day polling captures everything without data loss. For time-sensitive checks, run manually: python linkedin_monitor.py --expert "Name" --process
Cost: ~$5 per 1,000 posts via Apify (apimaestro/linkedin-profile-posts)
Priority Tiers
| Priority | YouTube | Newsletters | Use Case | ||
|---|---|---|---|---|---|
| A | Every 6 hours | Every 6 hours | Every 6 hours | Every 12 hours | Core thought leaders |
| B | Every 12 hours | Every 6 hours | Every 6 hours | Every 12 hours | Important experts |
| C | Every 24 hours | Every 6 hours | Every 6 hours | Every 12 hours | Secondary sources |
Reviewing Captured Intelligence
Option 1: Browse Raw Captures in Obsidian
000 Inbox/___Intelligence/
├── youtube/ # Full video transcripts with metadata
├── twitter/ # Tweet captures organized by expert
├── linkedin/ # LinkedIn posts organized by expert
└── newsletters/ # Newsletter content with frontmatter
Option 2: Query the Wisdom Database
# See what's available
.venv/bin/python "005 Operations/Execution/wisdom_query.py" --list-filters
# Get wisdom from a specific expert
.venv/bin/python "005 Operations/Execution/wisdom_query.py" --expert "Qiusheng Wu"
# Filter by topic
.venv/bin/python "005 Operations/Execution/wisdom_query.py" --topic gis --limit 10
# Last 7 days only
.venv/bin/python "005 Operations/Execution/wisdom_query.py" --days 7
Environment Variables
GOOGLE_API_KEY="xxx" # YouTube Data API v3
APIFY_API_TOKEN="xxx" # Twitter + LinkedIn scraping via Apify
MSGRAPH_CLIENT_ID="xxx" # Outlook/newsletter access
MSGRAPH_CLIENT_SECRET="xxx"
MSGRAPH_TENANT_ID="xxx"
DISCORD_WEBHOOK_INTEL_DIGEST="xxx" # #intel-digest notifications
Related Files
Directives:
005 Operations/Directives/expert_intelligence.md005 Operations/Directives/twitter_intelligence.md005 Operations/Directives/linkedin_intelligence.md005 Operations/Directives/newsletter_intelligence.md
Scripts:
expert_registry.py- Expert profile managementyoutube_monitor.py- YouTube channel monitoringtwitter_monitor.pyv1.0.2 - Twitter/X monitoring via Apify (now logs WARNING for A/B experts with empty handles)linkedin_monitor.py- LinkedIn monitoring via Apifynewsletter_monitor.py- Outlook newsletter monitoringdigest_generator.py- Intelligence digest generationaudit_expert_platforms.pyv1.0.0 - Platform gap audit (--priority,--platform,--jsonflags)
Workflows:
expert-intelligence-workflow.json(YouTube)twitter-intelligence-workflow.json(Twitter)linkedin-intelligence-workflow.json(LinkedIn)newsletter-intelligence-workflow.json(Newsletters)
Wisdom Synthesis System ⭐
Aggregate insights from multiple thought leaders across YouTube, Twitter, LinkedIn, and Newsletters, classify by audience vertical and topic, then synthesize audience-specific content with proper attribution and brand voice.
Architecture
┌─────────────────────────────────────────────────────────────────────────────┐
│ WISDOM SYNTHESIS FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐│
│ │ YouTube Videos │ │ Twitter Tweets │ │ LinkedIn Posts │ │ Newsletters ││
│ │ (transcripts) │ │ (tweet text) │ │ (post text) │ │ (email content)││
│ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘│
│ │ │ │ │ │
│ └──────────────────┴──────────────────┴──────────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Wisdom Indexer │ │
│ │ (Claude API + SQL) │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ verticals │ │ topics │ │ concerns │ │
│ │ (audiences) │ │ (subjects) │ │ (pain points) │ │
│ │ nonprofit │ │ ai_adoption │ │ accuracy │ │
│ │ surveying │ │ trust │ │ cost │ │
│ │ healthcare │ │ quality │ │ compliance │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ Wisdom Synthesizer │ │
│ │ + Voice Profile │ │
│ │ + Format Templates │ │
│ └────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ linkedin_post │ twitter_thread │ newsletter │ │
│ │ talking_points │ email_outreach │ article │ │
│ └───────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Data Flow
- Capture: Content from YouTube (transcripts), Twitter (tweets), Newsletters (email body)
- Index:
wisdom_indexer.py(v1.9.0) extracts insights with per-quote speaker attribution and authority levels, classifies by vertical/topic/concern - Query:
wisdom_query.pyretrieves wisdom by audience + topic + expert - Synthesize:
wisdom_synthesizer.pygenerates attributed content with voice profile
Indexing Wisdom
# Index wisdom from processed video content (speaker attribution auto-parsed from quotes.md)
.venv/bin/python "005 Operations/Execution/wisdom_indexer.py" \
"000 Inbox/___Content/20260111-020429"
# View database statistics
.venv/bin/python "005 Operations/Execution/wisdom_indexer.py" --stats
# Dry-run to preview without writing to DB
.venv/bin/python "005 Operations/Execution/wisdom_indexer.py" \
"000 Inbox/___Content/20260320-222439" --dry-run
Multi-Speaker Attribution (v1.9.0)
For videos with multiple experts (interviews, panels), the pipeline now tracks per-quote speakers:
quotes.mduses> — *Speaker Name*lines after each quote for attributionwisdom_indexer.pyparses these into per-quotespeakerfield, falls back toyoutube_channelfrom_metadata.jsongenerate_wisdom_capture.py(v1.2.0) addsfeatured_expertsto frontmatter and shows per-quote speaker labels- Authority levels are inferred automatically: named guest →
expert, channel host →media, audience/unattributed →community
Querying Wisdom
# Find AI adoption wisdom for nonprofits
.venv/bin/python "005 Operations/Execution/wisdom_query.py" \
--vertical nonprofit --topic ai_adoption
# Get leadership quotes from specific expert
.venv/bin/python "005 Operations/Execution/wisdom_query.py" \
--expert "Simon Sinek" --topic leadership
# List available filters
.venv/bin/python "005 Operations/Execution/wisdom_query.py" --list-filters
Tool Coverage: is every tool we run actually documented? (Aug 2026)
Six systems each hold a partial answer to "what tools do we run," and they disagree. The roster is a JOIN computed on demand, deliberately not a seventh source of truth:
# Build the joined roster and publish it to a Google Sheet in the QWU Backoffice Drive
.venv/bin/python "005 Operations/Execution/build_tool_roster.py" # counts only
.venv/bin/python "005 Operations/Execution/build_tool_roster_sheet.py" # creates the sheet
.venv/bin/python "005 Operations/Execution/build_tool_roster_sheet.py" --file-id <ID> # update in place
# Which load-bearing tools have no Tool Wisdom Library?
.venv/bin/python "005 Operations/Execution/audit_tool_wisdom_coverage.py"
.venv/bin/python "005 Operations/Execution/audit_tool_wisdom_coverage.py" --wisdom-threshold 10
# Register entity profiles for tools that carry wisdom but have no record
.venv/bin/python "005 Operations/Execution/register_tool_entities.py" --dry-run
# Move discipline tags out of the TOOL dimension into topics
.venv/bin/python "005 Operations/Execution/reclassify_wisdom_tool_tags.py" # dry run
.venv/bin/python "005 Operations/Execution/reclassify_wisdom_tool_tags.py" --apply # writes
The six sources joined: The Tool Shed page (published, donor-facing) ·
vendor_registry.yaml (monitoring config) · 003 Entities/Tools/*.md (entity profiles) ·
005 Operations/Directives/*_tool_wisdom.md (TWLs) · wisdom.db wisdom_tools tags ·
the Puzzle Tools canvas (via 005 Operations/Data/puzzle_tools_snapshot.json, refreshed
by an agent through the Puzzle MCP because Puzzle has no static token and no REST API).
Two things the roster reports that are easy to get wrong:
- "Has TWL = YES" is not the same as covered. A TWL file existing says a file is on disk. The roster reports each library's last COMMIT date instead, and only calls one current inside the standard's own 90-day freshness window. A stale TWL is more dangerous than a missing one, because every coverage check goes green while the library quietly stops being true.
- "Active" means an operational trace (in the Tool Shed, monitored, has an entity profile, has a TWL, on the Puzzle canvas, or called by 2+ execution scripts). "Knowledge only" means we have indexed wisdom about a tool and no operational trace ... a claim about what we have READ, not what we RUN.
Why this exists: the coverage audit used to build its roster from entity files alone
and never opened wisdom.db, so a tool could carry hundreds of indexed wisdom entries and
be structurally invisible to the only check that asks whether a TWL exists. Measured
2026-08-27: 48 tool tags, 4,449 entries, zero entity files between them.
Synthesizing Content
# Generate LinkedIn post for nonprofits about AI adoption
.venv/bin/python "005 Operations/Execution/wisdom_synthesizer.py" \
--vertical nonprofit --topic ai_adoption --format linkedin_post
# Generate talking points for surveyors with [Supporter Organization] voice
.venv/bin/python "005 Operations/Execution/wisdom_synthesizer.py" \
--vertical surveying --topic quality --format talking_points \
--voice "[Supporter Organization]" --save
# List available content formats
.venv/bin/python "005 Operations/Execution/wisdom_synthesizer.py" --list-formats
Content Formats
| Format | Description | Max Length |
|---|---|---|
linkedin_post | Professional post with line breaks | 1300 chars |
twitter_thread | Numbered tweets | 280 chars/tweet |
newsletter_section | Long-form with storytelling | 4000 chars |
talking_points | Presentation bullet points | 2000 chars |
email_outreach | Cold/warm email | 1500 chars |
article_intro | Opening paragraphs with expert voices | 2500 chars |
Taxonomies
Located in 003 Entities/Taxonomies/:
| File | Contents |
|---|---|
verticals.yaml | 12 industry/audience categories (nonprofit, surveying, healthcare...) |
topics.yaml | 31 subject matter categories (ai_adoption, trust, quality, open_source, homebrew, maker_culture...) |
concerns.yaml | 15 pain points (accuracy, cost, job_displacement...) |
Output Location
Synthesized content saved to 000 Inbox/___Synthesis/:
000 Inbox/___Synthesis/
└── 20260111-025328-nonprofit.md
Each file includes:
- YAML frontmatter (format, vertical, topic, voice, timestamp)
- Generated content
- Source attribution with links
Wiki Links
Generated content includes Obsidian [[wiki links]] for knowledge graph building:
- Key concepts:
[[Explainable AI]],[[Software Architecture]] - Named people:
[[Simon Sinek]],[[Geoffrey Hinton]] - Organizations:
[[OpenAI]],[[Google DeepMind]]
Social snippets remain link-free for platform export.
Related Files
- Directive:
005 Operations/Directives/wisdom_synthesis.md - Scripts:
wisdom_indexer.py,wisdom_query.py,wisdom_synthesizer.py - Database:
005 Operations/Data/wisdom.db(SQLite) - Taxonomies:
003 Entities/Taxonomies/
Canonical Datetime System
The QWU Backoffice runs on an Azure VM in UTC timezone, but all user-facing operations use Pacific Time. The qwu_datetime module provides a single source of truth for date/time operations across all scripts.
Why This Matters
| Server Time (UTC) | User Time (PST) | Without qwu_datetime |
|---|---|---|
| Friday 6:00 AM | Thursday 10:00 PM | Shows "Friday" (WRONG) |
| Friday 6:00 AM | Thursday 10:00 PM | Shows "Thursday" (CORRECT) |
Without canonical timezone handling, morning briefings, calendar queries, and daily notes would display the wrong date for ~8 hours every day.
The Module: qwu_datetime.py
Location: 005 Operations/Execution/qwu_datetime.py
All QWU scripts MUST import from this module instead of using datetime.now() directly:
# CORRECT - Use canonical QWU datetime
from qwu_datetime import now, today, effective_date, format_date
current_time = now() # Timezone-aware datetime in PST
current_date = today() # Date in PST
log_date = effective_date() # Date for logging (handles 4am boundary)
# WRONG - Never use these directly
from datetime import datetime
bad_time = datetime.now() # Returns UTC on server!
Available Functions
| Function | Returns | Use Case |
|---|---|---|
now() | Timezone-aware datetime | Current time operations |
today() | date | Current date comparisons |
effective_date() | date | Logging (handles 4am boundary) |
effective_datetime() | datetime | Timestamps for logs |
format_date(d, style) | str | Consistent date formatting |
format_time(dt, style) | str | Consistent time formatting |
format_datetime(dt, style) | str | Combined formatting |
to_utc(dt) | datetime | Convert to UTC for APIs |
from_utc(dt) | datetime | Convert from UTC responses |
get_timezone_info() | dict | Debug/status information |
Format Styles
Date styles (format_date):
"iso"→2026-01-08"display"→January 8, 2026"file"→20260108"weekday"→Thursday, January 8, 2026"short"→Jan 8
Time styles (format_time):
"12h"→10:46 PM"24h"→22:46"full"→10:46:30 PM"log"→22:46:30
Day Boundary Feature
Work before 4 AM counts as "yesterday" for logging purposes:
2:00 AM on Jan 9 → effective_date() returns Jan 8
5:00 AM on Jan 9 → effective_date() returns Jan 9
This prevents late-night sessions from bleeding into the next day's records.
Environment Variables
# Canonical timezone for all QWU operations (IANA format)
QWU_TIMEZONE=America/Los_Angeles
# Day boundary hour (work before this counts as "yesterday")
QWU_DAY_BOUNDARY_HOUR=4
Testing the Module
cd ~/qwu_backOffice && source .env
python "005 Operations/Execution/qwu_datetime.py" # Human-readable status
python "005 Operations/Execution/qwu_datetime.py" --json # JSON output
Sample output:
============================================================
QWU DATETIME STATUS
============================================================
Timezone: America/Los_Angeles
Current offset: -0800 (PST)
DST active: False
Day boundary: 4:00 AM
------------------------------------------------------------
Now: January 8, 2026 at 10:51 PM
Today: Thursday, January 8, 2026
Effective date: Thursday, January 8, 2026
============================================================
Scripts Updated
All execution scripts have been updated to use qwu_datetime:
| Script | Version | Status |
|---|---|---|
qwu_datetime.py | v1.0.0 | Core module |
morning_briefing.py | v1.6.0 | ✅ Goals integration |
calendar_events.py | v1.2.0 | ✅ Updated |
summarize_session.py | v1.7.0 | ✅ Goals alignment + stale-tmp archive guard |
process_inbox.py | v2.6.0 | ✅ Duplicate detection |
api_logger.py | v1.1.0 | ✅ Updated |
api_rate_limiter.py | v1.1.0 | ✅ Updated |
vista_social_api.py | v1.2.0 | 🔴 Wire-up correct, but BLOCKED ... the Vista subscription does not entitle API access. Key proven VALID 2026-10-05 (do NOT rotate it); 0 of 51 logged responses have ever succeeded. See 005 Operations/Integration-Specs/Vista-Social-Integration-Spec.md. |
azure_costs.py | v1.1.0 | ✅ Updated |
ez_chat_handler.py | v2.2.0 | ✅ Updated |
calendar_booking.py | v1.1.0 | ✅ Updated |
extract_unresolved_links.py | v1.3 | ✅ Updated |
Important: Any new scripts MUST import from qwu_datetime instead of using datetime.now() directly. This ensures consistent timezone handling across the entire codebase.
Frontend Timezone Standard (Foundational Directive)
Promoted to Foundational Directive: February 13, 2026 · Renamed 2026-09-21 ... this was titled "Lovable Frontend Standard" when every QWF app was built in Lovable. Lovable was cancelled 2026-09-21 and no QWF app is hosted or edited there any more (QKN + QTR, the last two, moved to Cloudflare Pages that night). The standard below is unchanged and still mandatory ... it now applies to every QWF frontend, React and SvelteKit alike, on whatever platform. Read "Lovable project" below as "QWF frontend app".
The same timezone problem affects all QWF Lovable apps. JavaScript's new Date().toISOString() returns UTC. After 4 PM Pacific (midnight UTC), the UTC date flips to the next calendar day. Since Supabase stores dates in Pacific time, all Supabase date-boundary queries silently return wrong results for 8 hours every day. No errors are thrown ... dashboards just show empty/stale data.
Every QWF Lovable project must include src/utils/timezone.ts with 7 helper functions. This file is required in every Prompt 001 (foundation prompt) ... see CLAUDE.md QWF Frontend Development.
Canonical utility functions:
| Function | Returns | Use Case |
|---|---|---|
getPacificToday() | "2026-02-13" | Supabase .eq('date', today) queries |
getPacificDaysAgo(n) | "2026-02-06" | Date range filters ("last 7 days") |
getPacificMonthStart() | "2026-02-01" | Monthly financial summaries |
getPacificMonthsAgo(n) | "2025-09-01" | Sparkline/trend chart ranges |
getPacificYearMonth() | "2026-02" | usage_monthly.month matching |
isPacificToday(ts) | true/false | Display logic ("2:34 PM" vs "Feb 11") |
toPacificDate(ts) | "2026-02-13" | Chart grouping by Pacific day |
Wrong patterns (search and replace in every Lovable codebase):
new Date().toISOString().split('T')[0] // UTC — wrong after 4 PM Pacific
new Date().toISOString().slice(0, 10) // Same problem
new Date().toLocaleDateString() // Locale-dependent, unreliable
Correct pattern:
import { getPacificToday, getPacificDaysAgo } from '@/utils/timezone';
const today = getPacificToday(); // Always Pacific YYYY-MM-DD
const weekAgo = getPacificDaysAgo(7); // 7 days ago in Pacific
Why en-CA? The Canadian English locale formats dates as YYYY-MM-DD (ISO 8601), matching Supabase's date column format. Why America/Los_Angeles? IANA timezone name that handles PST/PDT transitions automatically ... never hardcode -08:00.
History: First identified in HQ Command Center (Feb 2026, calendar edge function showed tomorrow's events after 4 PM). Recurred in QQT (11 affected locations) and QMP (18 affected locations) before the convention was established. Root cause: the original timezone directive called frontend JS "Generally safe" ... this assessment was wrong for Supabase date queries and was corrected Feb 13, 2026.
3-layer defense:
- Prevention: Every Prompt 001 must include
timezone.ts(CLAUDE.md QWF Frontend Development) - Enforcement: Agent instructions require Pacific helpers in all Lovable prompt code examples (never raw
new Date()) - Remediation:
005 Operations/Prompts/timezone-fix-handover.mdfor auditing/fixing legacy apps built before the convention
Apps fixed: QQT (Prompt 009), QMP (Prompt 008), Pocket Ez (Prompt 007). New apps built after Feb 2026 should not need remediation.
Full directive: 005 Operations/Directives/timezone_standard.md
Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Timezone Bug Analysis | UTC vs local time, silent failures, date boundary math | Beginner |
Intl.DateTimeFormat API | Browser APIs, locale formatting, IANA timezones | Intermediate |
| Systematic Codebase Audit | Pattern matching, regex search, impact analysis | Intermediate |
| Prevention Architecture | Convention design, agent instruction, defense-in-depth | Advanced |
| SEO Silent-Death Detector (2026-05-27) | Cron jobs, Discord webhooks, Supabase PostgREST probes, "active ≠ producing" debugging mindset, freshness-gated alerts | Intermediate |
| Senior-Reviewer Multi-Agent Fan-Out | Sub-agent orchestration, independent fact-checking with probes, commit-protocol discipline (SR owns commits), ground-truth verification | Advanced |
| Verifying a Route on a Single-Page App (2026-08-20) | Why curl -w "%{http_code}" is worthless against a React/SvelteKit SPA (every path returns 200, including its own 404); designing a negative control (request a path you KNOW is fake) to prove an instrument can discriminate; choosing data-verification over link-verification when shipping email | Beginner |
| Tracing One Alert Back to Its Cause (2026-08-30) | Following a single SMS backwards through five independent systems ... Twilio's REST API, a SQLite queue, run logs, n8n's Postgres, and the Graph mailbox ... instead of trusting the first plausible story. The habit being taught: read the recipient's copy, not the send result. Two shipped bugs this session were caught only by re-reading the rendered artifact after the fix. | Beginner |
| Proving What an Automation Actually Does (2026-09-10) | Reading an orchestrator's per-execution node history instead of its diagram. A workflow ran every 5 minutes and reported success every time, and its JSON clearly showed the pipeline step ... but the step sat behind a "did new input arrive?" gate, so it fired on exactly one tick and never again. Listing which nodes actually ran per execution, and contrasting the tick around the event with its neighbours, settled in one pass what reading the design could not. The habit being taught: name the triggering event and the state change you care about out loud, and check they are the same thing. | Intermediate |
| Internal vs Outbound Data Boundaries (2026-08-30) | Recognizing that fields sitting side by side in one dict do not share an audience. An LLM analysis record held {speaker, quote, significance}; the first two were the person's, the third was our private assessment, and a template pasted all three into their inbox. Teaches trust boundaries, allowlist-not-blocklist rendering, and asking "would I say this to their face?" before interpolating. | Intermediate |
| Race Conditions and Atomic Claims (2026-08-30) | A status field meaning BOTH "discovered" and "claimed, in flight" let two overlapping jobs process the same record. Teaches state-machine design, BEGIN EXCLUSIVE / compare-and-set, lease expiry so a crashed worker cannot wedge a queue forever, and proving the fix with a concurrency test (8 threads, exactly 1 winner) rather than by reasoning. Directly transferable to any job queue. | Advanced |
| Idempotency by Design (2026-08-30) | Making a repeated run harmless instead of forbidding repeats: a composite primary key claimed inside a transaction, released on failure so a retry can pick it up. Core distributed-systems concept, high employer demand, and demonstrable in a 20-line SQLite example. | Intermediate |
| The Permission That Silently Does Nothing (2026-09-06) | A database row-security policy listed a role name the application had retired, so an update by a valid manager changed nothing and returned success with an empty result ... the button reported "saved" for a day. Teaches Postgres row-level security, reading pg_policies instead of trusting a policy's NAME, and the habit that closes it: any write whose success you are about to REPORT must return its rows and be checked by COUNT, never by the error flag. Also teaches preferring one shared helper function over the same role list copy-pasted into four policies. | Intermediate |
| Escaping Untrusted Text for Three Different Outputs (2026-09-06) | The same name a stranger typed on a public website goes into an HTML email, a text message and a chat webhook, and each one is unsafe in a different way: HTML needs escaping, a subject line must never carry a line break, a chat embed must not be allowed to mention everyone. Teaches input sanitisation, why "clean" and "HTML-safe" are two separate steps, and writing the proof that shows the angle brackets arriving as visible text. | Beginner |
| Guarding a Webhook with a Shared Secret (2026-09-06) | An automation webhook open to the whole internet, closed with a header check. Teaches shared-secret auth, why the check must happen BEFORE the server answers (the default mode replies 200 first, so the guard silently protects nothing), where a secret has to live to reach a container, and proving all three cases: no token refused, wrong token refused, right token accepted AND the work still happening. | Intermediate |
| Splitting Hot and Cold Data Across Two Disks (2026-10-11) | Measuring what fills a disk (size vs 30-day writes vs net daily history), deciding what needs fast storage, moving a live folder under its job's lock with checksum verification and a symlink so no script changes, and proving each writer once before deleting the old copy. Also: why a cache should be trimmed rather than moved, and how a "safe" cleanup protects anything a script or note still names | Intermediate |
| Reading Code for What Is CALLED, Not What EXISTS (2026-08-30) | A correct, well-documented locking function had been in the codebase for months with zero call sites ... and the data proved it (status='processing' never appeared in 94 rows). Teaches grep for call sites, verifying a safety mechanism by its fingerprints in the data, and the difference between "implemented" and "wired in." | Beginner |
| Consent-Gated Sending (2026-08-20) | Resolving a permission model before any send; fail-CLOSED vs fail-OPEN and why an unreachable gate is an UNKNOWN not a yes; structurally locked-on categories (a receipt can never be suppressed); why an ungated DRAFT is still a broken opt-out. Real data-governance work, not a toy | Advanced |
| PostgREST Query Literacy (2026-08-20) | select= returns ONLY named columns and omitting one fails silently; eq. on text is case-sensitive so a mixed-case key is invisible; ilike. vs indexing lower(value); auditing a permission store means comparing CASE, not just presence | Intermediate |
| "Green" Is Not "Written" (2026-08-20) | Insert-once/idempotent pipelines that return success while discarding the payload (idempotent_replay + payload_drift_detected); verifying the WRITE landed instead of trusting the return code; repairing without creating duplicate records | Intermediate |
| n8n Workflow Deploy + Cleanup | scp → docker cp → import:workflow → publish:workflow → restart pattern; isArchived=true over delete (reversible); export-before-archive for reversibility | Intermediate |
| Provisioning a QWF app end to end (2026-09-02) | Following a written SOP (provision_qwf_sveltekit_app.md) to take a domain to a live app: Supabase project + Auth config via the Management API, GitHub repo + per-repo deploy key + SSH alias, Cloudflare Pages project + custom domains + DNS, GitHub Actions secrets (sealed-box encryption), CLI edge-function deploy, registry entries. Order matters: secrets BEFORE the first push. | Advanced |
| One renderer, print parity (2026-09-02) | Why a printable sheet and a screen sheet must be ONE component (@page 17in 11in), how a Python reference generator becomes a golden fixture for a Playwright visual-diff instead of a second engine, and how to verify a PDF's page size from its MediaBox. | Intermediate |
| Reusable ops script from a one-off (2026-09-02) | Turning a project-specific script (qcm_create_betterstack_monitor.py) into a parameterized, idempotent, dry-run-by-default tool (create_app_uptime_monitor.py) that any app can call; inherit the proven API errata, add --apply, never print the token. | Beginner |
| Web Push end to end (2026-09-06) | Generating a VAPID keypair with cryptography, storing the halves in the three places that need them (.env, edge-function secrets, the app's public env), subscribing a real phone from the browser, and a service worker push handler that does real work and shows the one notification the browser requires. The whole chain is visible in a single afternoon. | Intermediate |
| A wait that never waited (2026-09-06) | page.wait_for_function(...) wrapped in except: pass raised while the page was reloading, returned instantly, and turned a 90-second wait into a zero-second one ... three 10-minute proof runs were spent chasing an app bug that did not exist. Teaches: never swallow a wait's exception, and a test that passes too fast is a symptom. | Beginner |
| Offline-first as a cache, not a feature (2026-09-06) | A service worker that pre-fetches tomorrow's pages so a page opens with no signal; why the fetch must refuse to cache a login redirect, why the jobs are fetched one at a time, and why the on-screen count reads the device's own record rather than the server's. | Intermediate |
| Grants are checked before RLS (2026-09-06) | A new Postgres table arrives with anon holding every privilege because of default privileges; adding a policy does not remove them. Teaches revoke ... from public, anon, authenticated then grant precisely, and writing a post-condition that refuses to commit the migration if the grant is still there (it fired twice this session and rolled both back). | Intermediate |
| Reading a vendor's breaking-change notice, then proving and applying it yourself (2026-10-21) | A database vendor's final notice said new tables would stop being reachable through its API on a set date. Teaches: reading the vendor's raw change page instead of a summary and finding what it leaves out (new views were covered too; one dangerous privilege stayed behind), checking every project's live settings instead of trusting the notice, scanning in-progress work for what would break, then applying the change ourselves one project at a time with a throwaway test that proves the before and the after and leaves nothing behind. Pairs with building a guard that refuses a bad migration when the file is written and again inside the database's own transaction, then asking a second reviewer to break it before trusting it. | Intermediate |
| Rehearse every door of a database guard (2026-10-21) | A Quietly Quoting price book could be approved while one of its packages had no price, so the package priced as its add-ons alone (a "$950 roof"). The fix was a database trigger that refuses the approval, rehearsed against the live database inside transactions that always roll back. Teaches: proving the old code reproduces the fault before fixing it, comparing old and new code on the same real rows (never typing a number), and testing EVERY way a row can reach the guarded state: the first draft refused an update but let a book INSERTED already approved straight through, because a BEFORE INSERT trigger cannot look its own row up yet. | Intermediate |
| Rehearse a forced failure in two engines (2026-10-22) | A presentation's moving 3D drawing could leave a blank box when an iPad took back its graphics memory. The fix shows the drawing's still pictures instead, and the proof FORCES that failure (a lost graphics context) in two browser engines and checks what the viewer actually sees. Teaches: simulate the failure you fear instead of waiting for it, and prove the fallback in more than one engine. | Intermediate |
| A missing space nobody saw for four months (2026-10-22) | Every package card read "BEST FORA tight budget" because the app's framework (Svelte 5) drops a space at the edge of a tag, while the source code looked right. Teaches: read the compiled output, not only the source; prove a one-character fix by comparing every word on every card before and after, in two browsers, with exactly one difference allowed; then confirm it in the live site's own code. | Beginner |
| Measure the fold before you ship a picture (2026-10-22) | Adding a tall drawing above each card on a warranty page pushed the warranty words off an iPad held sideways. Teaches: measure where each line lands (bounding boxes) in the orientation people actually use, build both layouts, and hand the decision-maker the numbers. Also: search the issue tracker for standing rules ("no page may scroll") BEFORE designing, not after. | Intermediate |
| Two keys with the same name (2026-09-06) | An edge function compared the caller's bearer token to its own runtime SUPABASE_SERVICE_ROLE_KEY and refused everyone. The runtime's copy was a 41-character secret; the caller's was a 219-character token. Teaches: prove an auth check by probing lengths and first-difference, never by reasoning about what a variable 'should' hold. | Advanced |
| Triaging cloud vendor notices (2026-10-17) | Reading every "action required" email from a cloud provider IN FULL, then checking each claim against the real setup before reacting: which region the servers are in (a price rise for Europe does not touch West US 2), which disk types exist (az disk list), and how busy a disk really is (operations per hour from /proc/diskstats times the public retail price = under $3/month worst case). Teaches that most vendor alarms are about someone else, and how to prove which ones are yours. Also: least-privilege in practice (turning off standing admin access and default-on AI agents nobody uses). | Beginner-Intermediate |
| Webhook Sender Verification (2026-10-26) | Why a webhook must prove who sent it: HMAC signatures (Twilio's X-Twilio-Signature), signing against the public URL behind a proxy, validating before parsing anything, and proving a protection holds with one refused unsigned request, never by attacking it | Intermediate |
| Least-Privilege AI Agents (2026-10-26) | Giving an AI agent only the tools its job needs: an allow-list approves but does not restrict, a deny hook is proven with a decoy file and a spy callback, and a check on the agent's output comes too late to stop an action | Advanced |
| Safe File Serving (2026-10-26) | Why a web server serves a list of pages, not a folder: directory listings, why Path(base) / name drops the base when name is absolute, and resolving a path before checking it stays inside its folder | Intermediate |
| Upgrade on a Copy (2026-10-26) | Patching 31 Python packages safely: freeze the current set, upgrade a copy of the environment, run the full test suite on both and compare the failures, hold back an upgrade whose cost jumped, then pin | Intermediate |
| Rotating a Secret Without Downtime (2026-10-26) | A staged swap: make the new credential, place it everywhere it lives (compare fingerprints, never values), restart what reads it, prove the new one works, then delete the old one and prove it is dead | Intermediate |
Azure Cost Tracking
Monitor Azure spending directly from the backoffice for cost awareness. Azure is one of 7 variable cost sources tracked by the [[#Cost Intelligence System]] ... see that section for the unified cost tracking architecture including LLM, Apify, Supabase, and budget alerting.
Updated in Session 827:
- Azure grant line:
check_azure_credit_balance.pyv1.4.0 now refreshes the HQ figure every day at 7:20 AM Pacific (the weekly run, which keeps the Discord record, was refused by Azure's rate limit about one week in three). A spent grant shows the day it ran out, not the day of the check. Paying full price until the April 2027 renewal is accepted; the renewal window alert is the one that matters. - Claude API calls are paid from the Max plan's $200 monthly API credit. OpenRouter sends each Claude call on the Foundation's own Anthropic key first (BYOK) and falls back to the OpenRouter balance only if the key fails or the month's credit is used up. That covers the ~$137 a month our scripts spent on Claude. Never turn on OpenRouter's "BYOK-only routing". Full facts, the key's expiry and the nightly watch:
claude_tool_wisdom.md§Who pays for our Claude API calls.
What's Tracked
| Metric | Description |
|---|---|
| Yesterday's cost | Previous day's total spend |
| Month-to-date | Cumulative spending this month (always queries full month from 1st) |
| 3-day average | Rolling average for trend analysis |
| Daily breakdown | Per-day costs over configurable window (default 7 days) |
Setup Requirements
Uses the same Azure Service Principal as VM control, but requires Cost Management Reader role:
az role assignment create \
--assignee $AZURE_CLIENT_ID \
--role "Cost Management Reader" \
--scope "/subscriptions/$AZURE_SUBSCRIPTION_ID"
Environment Variables
AZURE_SUBSCRIPTION_ID="your-subscription-id"
# No key: Azure sign-in is claude-dev-vm's built-in identity (qwu_azure_auth.py) since 2026-10-13
Usage
cd ~/qwu_backOffice && source .env
python "005 Operations/Execution/azure_costs.py" # Human-readable output
python "005 Operations/Execution/azure_costs.py" --json # JSON for automation
python "005 Operations/Execution/azure_costs.py" --dry-run # Validate credentials
Sample Output
AZURE COST SUMMARY
========================================
Yesterday: $0.42
Month-to-date: $12.87
3-day avg: $0.38/day
----------------------------------------
Daily breakdown:
2026-01-05: $0.35
2026-01-06: $0.41
2026-01-07: $0.42
Daily Audit & Reconciliation Infrastructure (Apr 2026)
Cost reporting was hardened across 5 phases on 2026-04-26. Every cost figure now flows through one credential path with continuous verification.
| Layer | Script | Cron | Output |
|---|---|---|---|
| Daily audit (cost drift, freshness, gaps, capability check) | audit_system.py | 0 6 * * * | audit_trends.db, Discord #daily-digest, 005 Operations/Audits/{date}-system-audit.md |
| Three-way reconciliation (expected / actual / allocated) | reconcile_costs.py | called by audit_system | gaps in audit_trends.db |
| Monthly invoice reconciliation (5% threshold) | reconcile_azure_invoice.py | 0 9 8 * * | 005 Operations/Audits/{YYYY-MM}-azure-invoice-reconciliation.md |
| SP RBAC + credential rotation audit | audit_emergency_recovery.py | called by audit_system | gaps; verifies VM Contributor + Disk Snapshot Contributor + Cost Management Reader |
| Orphaned-writer detection (sync_/collect_/refresh_/poll_ not in cron) | check_orphaned_writers.py | called by audit_system | gaps |
| Public transparency page generator | generate_cost_transparency.py | manual + session-wrap-up auto-trigger | Infrastructure-Costs.md (dg-publish: true) |
| HQ Dashboard SYSTEMS card refresh | sync_hq_system_health.py | */30 * * * * | hq_system_health Supabase row |
| Per-app cost attribution | collect_app_metrics.py | 0 1 * * * | hq_app_metrics Supabase rows |
| Git remote drift (LEAK-RISK / HTTPS-bare) | audit_system.check_git_remote_drift() (v1.5.0+) | called by audit_system | gaps; detects userinfo-embedded credentials in git remote URLs (Incident #4 vector class) and HTTPS-bare-where-SSH-expected per chat_leak_prevention.md §7.1 |
Credential pattern coverage (.env vs credential_patterns.py) | audit_system.check_credential_patterns_coverage() (v1.6.0+) | called by audit_system | ONE Low summary gap + full uncovered-keys list at .tmp/security/credential_patterns_coverage.txt; walks .env keys matching *_KEY / *_TOKEN / *_SECRET / etc. and verifies each value matches one of the 11 registered patterns per chat_leak_prevention.md item #4 |
Single source of truth: 005 Operations/Execution/cost_constants.py. Every cost number on every surface (transparency site, Digital Twin, HQ dashboard, internal reports) reads from this file. Includes __VERIFIED_AT__ for staleness detection (warn at 60d, gap at 90d).
Public surface: https://transparency.quietlyworking.org/open-playbook/infrastructure-costs/ ... auto-generated per-line breakdown, regenerates whenever cost_constants.py or audit_trends.db changes (per session-wrap-up step 4C).
Credential rotation: see 005 Operations/Directives/credential_rotation_playbook.md for procedures; 10 credentials tracked with annual cadence in CREDENTIAL_ROTATIONS constant in audit_emergency_recovery.py (8 High tier + 2 Medium). First real-rotation precedent set 2026-04-26 with OPENROUTER_API_KEY (Pattern B, executed via OPENROUTER_MGMT_KEY provisioning API).
Emergency recovery: see 005 Operations/Directives/qwu_emergency_recovery_capabilities.md. claude-dev-vm's own built-in identity (a system-assigned managed identity, no key) has the 3 RBAC roles needed to recover the VM without human-in-the-loop authentication. It replaced the qwu-vm-automation service principal on 2026-10-13, after that account's key was found readable in the old n8n Cloud; the key was deleted.
Azure portal budget and tenant posture (2026-10-18; budget raised 2026-10-29): the portal budget monthlyBudget is $560/month (alerts at 90%, about $504), raised from $425 the night claude-dev moved to 64 GB. Normal spend is about $453, so a budget email still means a real jump, not ordinary spend. The budget expires 2027-02-28 and sends nothing after that; a reminder to extend it is on the Alerts calendar for 2027-02-01. Azure Copilot is set to Restrict with all six agents off, and nobody holds the "Copilot for Azure User" role. TIG's elevated access (User Access Administrator at root) is off; if a fix ever needs root scope, turn it on in Entra ID → Properties → "Access management for Azure resources", do the fix, and turn it off again. What each Azure platform email means for our VMs is kept in infrastructure_monitoring.md §VM Lifecycle & Azure Platform Notifications.
MP Training Opportunities ⭐
This cost reporting infrastructure was built across one continuous arc on 2026-04-26. Several teachable units came out of it:
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Reading existing crontab patterns and adding a new entry that matches (cd + venv + log redirect) | First real DevOps task; pattern-matching discipline | Beginner |
Single source of truth + auto-generated transparency page (cost_constants.py → Infrastructure-Costs.md → site) | Clean architecture; "edit one place, propagate everywhere" | Beginner |
| Diagnosing a silent-pipeline-dead script (collect_app_metrics.py: 4 root causes layered) | Real debugging walkthrough; ImportError → missing imports → schema mismatch → upsert misuse | Intermediate |
| Three-way reconciliation design (expected vs actual vs allocated, all independent data sources) | Software-as-truth-system; distinguishing "what we think" from "what is" from "what we allocate" | Intermediate |
PostgREST upsert with explicit on_conflict parameter (not just Prefer: resolution=merge-duplicates) | Supabase-specific gotcha worth teaching; why "obvious" upsert fails | Intermediate |
| Active-probe fallback for distributed-system eventual consistency (Azure RBAC list lag) | Reasoning about systems that say "no" before they say "yes"; verify by trying | Advanced |
Filename-as-contract: detecting orphaned writers via sync_* / collect_* / refresh_* / poll_* prefixes | Naming conventions as machine-readable intent; how to add audits that prevent recurring patterns | Advanced |
Multi-script integration: wiring a new check into audit_system.py main loop | Software composition without rewriting; how to add capability to a running system | Intermediate |
Most valuable MP exercise: the silent-pipeline-dead diagnostic story. It's a real situation (HQ widget displaying $20.63 for 80 days) with a clear arc (TIG noticed → trace data source → find the script → diagnose 4 layered problems → fix → cron → verify the widget updates). Includes everything: API debugging, schema migration awareness, port confusion (8765 vs 8767), credential-helper failure, and a structural fix (check_orphaned_writers.py) so the same pattern can't bite again.
MP Training Opportunities ⭐ (from the 2026-10-26 L4G site ship)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Writing a nonprofit program's privacy policy and terms from research (15 comparable programs; which laws bind a 501(c)(3) and which don't) | Legal research turned into plain language, where every line is a public promise | Intermediate |
| Folding an in-flight feature's disclosure lines into a policy before it first goes public, so its own 30-day change notice never applies | Reading a document's own rules before acting; timing as a design tool | Intermediate |
| Designing a privacy-respecting public poll: no names or emails, a ZIP code's count stays private until a person names the place, a daily-salted one-way code instead of an internet address | Privacy by design; counting without tracking | Advanced |
| Shipping while other teams ship: merge main in, prove the release patch unchanged with a diff of diffs, push only as a fast-forward, re-read the rollback target | Release engineering on a shared repository | Intermediate |
| Automated page checks with Playwright (status, sideways scroll, layout shift, page weight, console errors at 384 px and desktop) and axe contrast checks in both looks | Testing what a visitor actually sees, not what the code intends | Beginner |
| Copy as code: every visible word lives in content modules, and a scanner fails on unknown placeholders and banned glyphs | Content engineering; a linter for words | Beginner |
| A silent 1,000-row cap that appeared only when another team extended the data (2 months fine, 24 months cut off) | Debugging latent limits; reading a response's Content-Range | Intermediate |
| Changing live data as one all-or-nothing step: back up every row first, rehearse inside a transaction that undoes itself at the end, check each statement's count, write the undo file before applying (140 card dates, 2,790 sale-grid cells, two holds and 15 route counts in one step) | Production data safety; making a change provable and reversible | Advanced |
| Drawing delivery areas from public USPS carrier-route data: fetch each ZIP's routes, map them, and split a city into three areas of 10,000 to 11,000 mailboxes along real roads | Working with public data, maps and a size rule; explaining a trade-off with numbers | Intermediate |
| Retiring a service safely: back up the deployed code, prove nothing else calls it, delete it, then remove the keys only it used (checked by downloading every remaining service) | Security hygiene; least privilege | Intermediate |
Most valuable MP exercise: the poll's privacy design. Students list everything a "vote" could reveal about a person, then remove each piece until only what the decision needs is left, and finally write the two policy sentences that describe what remains. Role-only throughout: no student is named.
MP Training Opportunities ⭐ (from the 2026-10-27 L4G delight pass, Session 823)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Designing small moments with taste: a card dropping into a mailbox, a flag popping, a stamp landing, each once and on purpose | Motion as meaning, not decoration; knowing when to stop | Beginner |
| Motion that respects people: a still version of every move for visitors whose device asks for less motion, and nothing hidden at rest (if it is on screen when the page loads, it shows the finished picture) | Accessibility as a design rule, not an afterthought | Beginner |
| Animating only position and opacity, and measuring layout shift (under 0.1) so the page never jumps | Performance budgets a visitor can feel | Intermediate |
| Freezing every animation at exact moments in an automated browser (pause them all, set their time) to take frame-by-frame pictures for review | Testing motion on purpose instead of hoping a screenshot lands mid-move | Intermediate |
| Reading a live database's access rules to explain a page that said "Loading..." forever (the browser was never allowed that read) | Debugging from ground truth: the rules, not the code's intent | Intermediate |
| Data minimization: asking for the smallest private read a page needs, after checking what is already public | Privacy by design; making a security review smaller | Advanced |
| Working beside other teams on one site: message before touching a shared file, merge their work in, re-check your pieces after each of their releases | Collaboration on a shared codebase | Intermediate |
| Delight that never gets in the way: a note in the browser console for curious builders, special-day postmarks keyed to Pacific time (Thanksgiving computed, not typed in) | Rewarding attention without costing anyone a click; date math | Beginner |
Most valuable MP exercise: the thank-you page. Students find out why a page waited forever (the browser was never allowed to read the booking), rebuild it to say only what is true, then design the smallest safe read that would let it show a donor-partner their own spot. Role-only throughout: no student is named.
MP Training Opportunities ⭐ (from the L4G Good-Neighbor site build, rounds 1 to 8, 2026-10-24 to 2026-10-27, Session 824)
The whole arc of rebuilding locals4good.org, from the first private preview to the area poll going live. The two entries above cover the policy writing, the poll's privacy design, Playwright checks, copy as code and the delight pass; these are the units they leave out.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| The copy deck and the blessing loop: every public sentence lives in word files, rendered as one deck for one review; each line marked NEW, KEPT (from the old site), LOCKED (family-standard words, word for word) or waiting on a ruling, and each open question carries one id so the owner rules once (21 calls on the first deck, 15 a few rounds later) | Editorial operations: how one person approves hundreds of sentences without reading the same question twice | Beginner |
| The blind five-second test: a reviewer who reads nothing about the program looks only at the first screen for five seconds as three typical visitors (a roofer on a phone, an estate planning attorney on a laptop, a homeowner who got the card), answers "What is this? Who is it for? What can I do here?", then ranks five fixes | User research without a lab; designing for the visitor, not the builder | Beginner |
| Checking research at its source: a pasted summary of mail statistics, each figure found and re-read in the original study; two dropped because the source was members-only or did not say what the summary claimed | Research honesty; why an unchecked number on a public page reads as a promise | Beginner |
| Private previews that cannot do harm: every change goes to its own preview address first; previews are kept out of search engines, testers never submit a form or place a hold on one, and the poll only pretends to count on a preview | Safe staging; a forgotten preview is a second live site | Intermediate |
| Gate words as the control surface: "bless" for public words, "ship" for production, "go" for data, "clean up" for deletion; the builder stops and asks with the exact word, and one person answers each | Release governance a non-engineer can run from a phone | Beginner |
| Shipping a feature turned off: the poll went live behind an on/off flag in the code plus a pause switch on the server, so it can be stopped later without rolling back other teams' work | Feature flags and kill switches; why "roll back" stops being safe once others ship on top | Intermediate |
| Text that moves without moving the page: TIG's cycling word in "Is my trade open?" rolls through trade, practice, firm, agency, shop, studio and clinic inside a clipped one-line window; the heading is laid out once, so it is pixel-identical at rest; screen readers hear one steady heading; it stops on a tap and skips a word that would not fit | Accessible motion; layout stability measured, not hoped for | Intermediate |
| Removing dead code safely: walk every import out from the pages, search each candidate file's name across the whole repository, keep a file another one cites as its contract, then prove nothing visible changed (every page's height identical to main on a preview; 18 files and 2,673 lines out) | Refactoring with evidence instead of confidence | Intermediate |
| Cleaning up after a release: list every preview, branch and working copy; dry run first; unlink a shared install before removing a working copy; check nothing links into it; name it all once and delete on one word | Operational hygiene; leaving no second live site behind | Beginner |
| Writing a user manual from the live product: every claim checked against the live pages and the word files, prices read from the live page, then an independent fact-check (the author's own first check caught eight lines written from words the site keeps on file but never shows) | Technical writing; the gap between what was designed and what shipped | Beginner |
Most valuable MP exercise: the blind five-second test. Students run it on a real page as the same three visitors, write their top five fixes, then compare them with what the site actually changed after its own test (the neighbor's way in moved to the first screen and the menu, the price went beside the headline, the first screen got one clear action). Role-only throughout: no student is named.
MP Training Opportunities ⭐ (from the L4G restyle of the sign-in, portal and staff pages, 2026-10-27 to 2026-10-28, Session 837)
The sign-in pages, the donor-partner portal and the staff pages took the public site's Good-Neighbor look in two batches, then seven small fixes found along the way went live one ruling at a time.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Proving a restyle changed nothing it should not: two private copies of the site on made-up data (the old build and the new), every page photographed in full with animations frozen, both looks, phone and desktop widths, compared pixel by pixel; the words on screen compared separately | Regression testing that never touches live data | Intermediate |
| Telling drawing noise from a real change: when a picture differs by one or two shades on a curved edge, photograph the SAME build twice; if it differs from itself in the same box, it is the browser. A read of every element's position and style then confirms nothing moved | Controls in an experiment; evidence over instinct | Advanced |
| Accessibility against a named standard: axe-core at WCAG 2.2 AA on 33 pages, including forms that open only on a click; every text box tied to the label beside it, every icon-only button given a spoken name, every tap target 44 px | Building for screen readers, keyboards and thumbs | Beginner |
| Moving a read from the browser to the server: the staff dashboard's numbers now arrive with the page, so nothing jumps when they fill in; the server reads as the signed-in person, so the same database rules still decide what comes back | Server rendering, layout shift, least privilege | Intermediate |
| A date window is a boundary test: "next three months" became this Pacific month and the next two; the month math was tested across a year end and both clock changes | Off-by-one and time-zone testing | Intermediate |
| A tool named in a handoff must outlive the session: a cleanup tool cited by four kickoffs had lived in a session's scratch folder and was gone; it was rebuilt in the lasting tools folder, dry run by default | Operational durability | Beginner |
Most valuable MP exercise: give a student two builds of one page and a picture diff showing a few changed pixels, and ask them to prove whether anything really changed, using the photograph-it-twice control and a layout read.
MP Training Opportunities ⭐ (from the QQT [Supporter Organization] calculators go-live, 2026-11-01, Session 872)
A supporter's newer calculators went live on their website in one evening, one guarded step at a time, with nothing sent to anyone and every step undoable.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
Prove a release on the real site before changing it: intercept the page's script and config inside a headless browser (Playwright route.fulfill), so the new version runs on the live page for the test only | Testing in production without touching production; request interception; read-only discipline | Intermediate |
| Rehearse a database write against live and roll it back, then apply, then read back at two layers (table and public API) | Transactions, idempotent guards (fingerprints), the difference between "committed" and "proven" | Intermediate |
| A shared row cannot be staged: when the sandbox and the live site read the same data, "put it on the sandbox" is a live change | Data modeling, blast radius, asking "who else reads this row?" | Beginner |
| Where consent words go: above the button, full-size, the button pointing at them (Section 17), with the court case and FTC guidance behind it | Accessibility (aria-describedby), legal design, writing for a busy reader | Beginner |
set -e + pipefail + diff in a loop: diff returns 1 when files differ, so the loop stopped after one file and the compare silently covered a fifth of the pages | Shell exit codes, why a check must be read, not trusted | Beginner |
Most valuable MP exercise: give a student a static page that loads a script from a CDN and ask them to prove a new version of that script on the live page without changing the page, then show the before and after screenshots and the count of requests they blocked.
MP Training Opportunities ⭐ (from the L4G staff and portal fix-up close-out, 2026-11-01, Session 870)
A screen reader can now tell which tab is chosen on six tab rows, and a donor-partner's record offers artwork only to paid reservations.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
State that shows only by color is invisible to a screen reader: six tab rows marked the chosen tab with a CSS class alone; one attribute (aria-pressed) driven by the same test as the class fixed every row, proven by reading both on each tab before and after a click | Accessibility basics (WCAG 1.4.1 and 4.1.2); making visual state and announced state come from one source | Beginner |
| A repeating difference is a cause, not noise: four phone pages kept measuring 18 to 27 px apart between preview and production; production against itself matched, so the cause was traced to the area poll's per-visitor random order, and pinning its seed made the two hosts match 54 of 54 | Debugging by controlled comparison; ruling out the measuring tool before blaming it | Intermediate |
| Find where a rule is actually enforced: "Upload to all months" reached unpaid bookings because neither the page nor the database function checked payment; the page now filters, and the database check was handed to the lane that owns it | Tracing a behavior through UI, API and database layers; defense in depth | Intermediate |
Most valuable MP exercise: give a student a tab row whose chosen tab differs only by color and ask them to make a screen reader announce it, then prove it with an automated check that fails before their fix and passes after.
MP Training Opportunities ⭐ (from Referral Bop played alone, 2026-11-03, Session 887)
A second public game on a live board, built from the first, plus a logo that replaced an idea nobody had asked for.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Prove a game's maximum by search: a small program walks every choice a player can make through the fixed pattern and reports the best score, and the build stops when the proof fails | Search, dynamic programming, build-time checks | Advanced |
| Fair by construction: every player gets the same timing and mix, with the faces shuffled by a seed, so nobody gets an easier game and nobody can learn the answers | Seeded randomness, game design, fairness | Intermediate |
| A wait that adapts: measure how long the connection takes and set the timeout from it, because a fixed wait can stall forever on a slow link | Networking, resilience, testing on bad connections | Intermediate |
| Rehearse the undo: run the live code, the new code, then the live code again on one local database, so going back is proven before it is needed | Release engineering, data migrations | Intermediate |
| Redraw a real logo in code: sample each square's color from the image file, draw it on a 4x4 grid, then check it by eye at phone size and on a big board | Image data, SVG, visual checking | Beginner |
| Trace a surprise to its source: when someone asks "when did we come up with this?", read the record of who proposed it and when, and say so plainly | Research, version history, honest reporting | Beginner |
MP Training Opportunities ⭐ (from Mail Truck Dash played alone, 2026-11-03, Session 884)
A new public game on a live board with prizes, from its page to its ship, plus a rendering bug TIG caught on video.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Measure a visual bug instead of eyeballing it: thin lines flickering on a turning 3D cabinet were tested at every angle in tenths of a degree, at two screen densities, counting missing pixel rows before and after the fix | Visual testing, Playwright, image analysis | Intermediate |
| Why a line vanishes: perspective, sub-pixel depth and edge smoothing stack, and the fix (a stroke stretched along one axis) came from working out the real projection rather than guessing a number | CSS 3D, linear math, root-cause analysis | Advanced |
| Page and server must agree: the rules both sides enforce live in one shared file, and a test proves the most a direct request can post equals the most the page can count | Client and server trust, game fairness | Intermediate |
| Ask the system of record: whether a finger is still down comes from the browser's own answer, not a list our code keeps, so a missed event can never freeze the game | Event handling, state design | Intermediate |
| Words are part of the design: a rule that could be read two ways was rewritten to say what actually happens, and a rule the game cannot check was never promised | UX writing, plain language | Beginner |
| Prove the build you ship is the build you tested: two builds of one commit differ by a version stamp, so a file-by-file comparison replaces that stamp and treats anything else as a stop | Release engineering, reproducible builds | Intermediate |
L4G call kit: California's tax line and QCM's Do Not Call list (2026-11-03 and 04)
The tax line. Anyone selling ad space while saying the money funds a charity must state the share deductible as a charitable contribution, or that none is (California Bus. & Prof. Code 17510.3, 17510.4). TIG's blessed statement lives in one home per system: src/lib/content/legal/tax-statement.js on the L4G site (the Questions answer and section 5 of the Terms) and L4G_TAX_STATEMENT in preference_center/email.py for receipts. A test pins the receipt's words to qwf_program_framing.md; a changed word needs his bless. The buyer's "Solicitation or Sale for Charitable Purposes Card" is built and stays off until he signs (CARD_SIGNATURE). The Do Not Call policy is a public page at locals4good.org/do-not-call.
The Do Not Call list. QCM table call_stops, migration 079. A stop for a number, a person or a business is kept for ever; its first date and writer are locked. A number one person holds ties to that person. It comes off only on the person's own word, one row by id, by a login holding access, and an alarm goes out (watch_qcm_call_stop_lifts.py, every 15 minutes).
- Before a call:
manage_qcm_call_stops.py check --phone N. Exit 0 is a yes, as far ascheckscovers: today our own list only. Exit 2 is a no, exit 1 is no answer, also a no.qcm_call_checkis the call gate; the email resolver never answers for a call. - Someone says "please don't call":
manage_qcm_call_stops.py stop --phone N --contact-id C --org-id O --name "what they said", the same day. Exit 4 means written, but a name could not be tied to a person: find them and stop them by id. - Lifting:
find, thenlift --stop-id ID --evidence "what they said and when" --actor LOGIN --apply. - Nightly: guard checks A24 and A25.
The call log and queue (migration 081, live 2026-11-04). call_log keeps every try for ever and never changes; a mistaken tap is corrected by a new row. call_queue_items holds who to call next. One tap per outcome moves both in one transaction. "They asked us not to call" writes the stop first, so a failed log can never cost the stop. "Yes, email me" writes their email yes (source phone_call_operator), or says why it did not. Nobody is called more than 2 times in 7 days or 4 in 30, across all callers. qcm_call_check now answers the stop list, the youth hold and that limit, and still not the national list or calling hours. It sends nothing and calls no one. The backoffice's door is manage_qcm_calls.py: add, today, claim, release, log, history, mark-partner.
The calling screen (migration 083, live 2026-11-04): quietlycrm.org/calls, each call's sheet, and /calls/held, behind the console's sign-in with MFA, phone first. The Call button asks the database first and only its yes hands back the dial link, the one tel: in the console, so a stale page cannot dial someone who said stop. The next call waits until the last has an outcome. TIG dials from his own phone. Nothing records, and a yes drafts nothing yet. Design: QCM-Call-Screen-Design.md.
The one youth rule and the mark. qcm_contact_youth_hold holds a known minor, a student state, or a link to a youth-protected program with no adult-partner mark: no queue row, no Call button, no sheet, and the only word printed is youth_hold. A birth date frees nobody, because the standard's youth runs to 30. Every call that happens is still logged. A mark is TIG's word only: two taps on /calls/held, or mark-partner on his spoken word. It can be taken back and stays on the record.
- Adding a name: only on TIG's word, one at a time.
add --program l4g --phone N --contact-id C --org-id O --campaign K --why "his words". The campaign must contain "coto", "rpv" or "palos", or the sheet shows a bracketed slot instead of the mailbox count. - A person must exist. With no person on the row the sheet says "[name]" and "Yes, email me" writes no yes. The only door for a new person,
inbound-contacts, requires a program, and L4G is youth-protected, so a person created on L4G is held until he marks them. - Never pick names from the CRM. On 2026-11-04 all 70 people there with both a phone and a business came through QNT, the chapter's data, which BNI's terms keep off this use. A person he names himself is his.
- Testing: a try is kept for ever, so never test the after-call taps on a real row. The dial test writes none: tap Call, see the dialer, do not press its button, then "I did not call. Give it back."
- On the QOS map: the kit itself has no node, since the map tracks the standard.
p1.workspace.v1carries its defer, andp1.consent.v1.call-stop-subjectstracks thatconsent.v1can only describe a person's stop. Calls feed no event on purpose:fn_emit_eventcopies the whole row into a table with no MFA check. - Nightly: guard checks A24, A25 and A26.
- Next: the first real call, then the L4G layer (card progress, open trades, the trade link, the follow-up draft).
MP Training Opportunities ⭐ (from the L4G call kit build, 2026-11-03 and 04)
| Task | Skills taught | Prerequisites | Time | Complexity |
|---|---|---|---|---|
| Read what a design promised against what got built: take its table of promises, then read the live triggers and grants, and write down each difference with its reason. Here it found calls that never fed the event stream, and that leaving them out was the safer choice | Database introspection, reading a design critically, recording a decision | Basic SQL | 2 hours | Intermediate |
| Let broken versions judge your rehearsal: on the call log, the first 80 let 8 through, and those 8 found a weak comparison hiding in two rehearsals that both said OK | Mutation testing, why a passing test proves little | The rehearsal task below | 3 hours | Advanced |
Write a rehearsal that must fail: run a migration in a transaction that always rolls back, and check every result with IS DISTINCT FROM | SQL, transactions, testing, why NULL hides failures | Basic SQL | 4 hours | Intermediate |
| Prove the tests by breaking the code on purpose: 65 deliberately broken versions, each of which a test must catch, and read each miss by hand | Mutation testing, reading code critically | The above | 4 hours | Advanced |
| Read a law from its primary source and turn it into one plain rule, with the words cited | Research, plain writing, knowing when to ask a lawyer | None | 2 hours | Beginner |
MP Training Opportunities ⭐ (from L4G's next-areas planning, 2026-11-03, Session 889)
A program had to choose its next neighborhoods from free public numbers, with no geometry library on the machine.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Lay one map over another: sample points along a line, test which polygon holds each, and speed it up with a simple grid | The core idea of geographic analysis, built from first principles in plain Python, so nothing is magic | Intermediate |
| Check the label, not the cell number: a Census table's cells moved by one, and a label check refused the run before any wrong number got through | A wrong column gives believable numbers forever. Teaches how to make a data source prove itself | Beginner |
| One number misleads: a median misses wealth that does not arrive as a paycheck, and high income can mean mostly renters | Teaches asking what a number leaves out before ranking anything by it | Beginner |
| Calibrate a score on known answers: the places already chosen must land high, or the score is wrong, not the choices | How to test a scoring model when there is no answer key | Intermediate |
| Leave protected traits out by design, and write down that you did | Ethics in data work is a decision made in the code, before any result exists | Beginner |
| Keep a page working under a strict content rule by shipping its own map tiles, inside the map server's download limit | Real work has limits from two sides at once, and both have to be read and honored | Intermediate |
Most valuable MP exercise: give a student three neighborhoods' public numbers and ask them to rank them for a local mailer using income alone. Then add who owns their home and ask what changed and why. The conversation is the lesson. Use made-up places, so nothing real gets labeled.
MP Training Opportunities ⭐ (from a private research list, 2026-11-04, Session 905)
A program needed a list built from public pages, where every row could be checked by someone who was not there.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Keep a fact apart from a guess: "seen in the November issue" against "pays for ads" | The habit that makes research trustworthy. A reader can only check what you actually saw | Beginner |
| A receipt on every row: what was read, where and when, with a ranked kind so weak evidence looks weak | Turns a list into something a stranger can audit | Beginner |
| Say where each fact really came from: a town from a listing is not a town printed on an ad | Mislabelled sources stay true-looking for a long time | Intermediate |
| A chain's home page shows one location's phone, so check the location's own page | Scraped facts can be real and still wrong for the row they sit on | Beginner |
| Keep only what the work needs: saved pages can hold private people's stories even when the list never uses them | Privacy is also about what you store, and a check has to look there too | Intermediate |
| Ask someone to break it before anyone trusts it: sample rows at their sources and fix what falls | How to review work without being anchored on it | Intermediate |
Most valuable MP exercise: give a student five made-up ads and ask for a list with a receipt on each row. Then have a second student try to break it. Use invented businesses, so nothing real gets labeled.
MP Training Opportunities ⭐ (from the wrong feedback link and the silent reply, 2026-11-04, Session 908)
One member's one-line email found a wrong link that had reached a whole chapter. Tracing why nobody heard about it found a second fault that had been hiding people's replies since August.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Open a link before passing it on | A link has two addresses: the words you see and where a click goes. Copying an old email left them disagreeing, and we passed on the words. Check what the person will see on the far end | Beginner |
| Words in an email body prove nothing about who sent it | A reply quotes the email before it, so our own footer made every person who answered us a "newsletter". Delivery headers tell you how a message was sent; the body cannot | Intermediate |
| Compare old and new on real data, in both directions | The first cleaner rule also caught notices that had always been read. A label change must be measured for what it adds as well as what it frees | Intermediate |
| A send loop has three outcomes | Sent, certainly not sent, and unknown. One timeout used to re-mail everyone before it, because the record was written after the loop. Ask of each person: what does a crash right here leave, and what will the next run do? | Intermediate |
| Let someone else try to break your fix | Two independent reviews found real faults in the fixes themselves, each reproduced with sending switched off before anyone believed it | Intermediate |
| A draft must be true whenever it is sent | A person sends in their own order and edits as they go. A note about order does not travel with the draft, so never write one that depends on a later step | Beginner |
| Record who raised it | Most items in a tracker read as the operator's because the capture tool filled in one name. If you want to thank people, the first job is remembering them | Beginner |
Most valuable MP exercise: give a student a folder of made-up emails (real people replying, firm footers, true newsletters) and the old rule. Have them find every honest reply it misfiles, write a better rule, then measure it both ways: who got freed, and who got newly caught.
MP Training Opportunities ⭐ (from TIG's Road to the Live Game Show emails, 2026-11-04, Session 893)
A new self-sending email went to an independent reviewer before its first send, and the review broke it in ways all 92 passing tests had missed.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Mutation testing: break the code on purpose and see which tests notice | 13 of 22 deliberate faults survived a green test run. Removing the check that stops a second copy failed nothing, because the test stopped at an earlier check | Intermediate |
| "Done" must mean nobody is left to try | A consent store blip held all 19 people, the send list came back empty, and an empty list read as finished, so one bad minute cancelled the day's email | Intermediate |
| A lost answer is not a failure | SES can take a message and the connection drop before the answer. The SDK's own retries then send it again, and a load balancer's 504 can follow a send too. Retry only what the service itself named as a refusal | Advanced |
| Fixed phrases in alerts, never an error's text | A failed lookup's error text held its URL, and the URL held a person's address, spelled so that a scrub looking for an @ missed it | Intermediate |
| Motion in an email has to stop by itself | An email has no pause button, so WCAG 2.2.2 means under 5 seconds, then rest. The first frame must be a finished sign, because older Outlook shows only that | Beginner |
| Bless the bytes, not the words | A fingerprint of every byte a reader would see means a changed signature or card stops the send and asks again, rather than reaching 19 inboxes unseen | Intermediate |
| Audit from the deepest source, and test the auditor's own reading | Two people looked undelivered when their events were matched by address; tracing by message id showed both delivered. A wrong first reading is cheap to catch and costly to report | Intermediate |
| Prove a promise in the real game, not in its description | A real browser on a throwaway local copy timed the tile falling asleep at 4.2 seconds and counted a held key once, against the words a hint makes to 19 people | Intermediate |
| Mask before the first print | One raw header put a person's address in a working log. Build the mask first; nothing can unprint it | Beginner |
Most valuable MP exercise: give a student a passing test suite and a sender, and have them delete one safety line at a time until a test fails. Each line that fails nothing is a promise nobody is really keeping.
MP Training Opportunities ⭐ (from the L4G Play live show's golden trucks, UFO and 30-second truck, 2026-11-04, Session 902)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Prove a test can fail | Each new test ran on the code live that day and failed there, with named errors. A test that has never failed has proven nothing | Beginner |
| Read the setting's whole path | Flipping a default did nothing, because the live room carried its last saved value forward. Trace where a value comes from at run time, not where it is declared | Intermediate |
| Measure a flaky test instead of re-running it | One check failed on a good build. 200,000 simulated games showed 1 in 140 has nothing for it to do, so the test was fixed, not the code | Intermediate |
| Freeze an animation to look at it | The Web Animations API can pause every animation at an exact millisecond, so a two-second effect becomes twelve still frames a person can check | Intermediate |
| Test what users do twice | A phone left open across a reopened room hung on JOIN for 10 seconds. It was found only because a new test joined a second time on the same page | Beginner |
| Change stored data once, on purpose | A one-time step in a server's start-up, guarded by a marker row, proven by a rehearsal of old code, new code, old code on one local copy | Advanced |
| Hold a promise against the platform | An email promised something a browser's storage rules can quietly take away. Read the vendor's own words, then decide whether the promise's wording still holds | Intermediate |
| Name the person in the ask | "Two scores come off" was approved. "Your friend's score comes off" would not have been. The same fact, and a different decision | Beginner |
Most valuable MP exercise: give a student a setting that "does nothing" when changed, and have them find every place between the default and the screen that could be holding the old value.
MP Training Opportunities ⭐ (from How Many Mailboxes? played alone, 2026-11-04, Session 904)
| Teachable unit | What happened | Level |
|---|---|---|
| Measure a rule before choosing it | Two scoring rules were timed with scripts on a private copy, then simulated, before anyone chose. The numbers made the choice plain in one message | Intermediate |
| Keep the answers on the server | A guessing game whose page never holds the answers: the server says only higher or lower. A test searches everything a browser can load for every answer, on every build, and it caught a real slip the first day | Intermediate |
| Know what a checksum can see | A deploy's checksum stayed the same across three versions with different server code, because the deploy reads that code from source. A fingerprint of everything the deploy really follows replaced it | Advanced |
| Doubt the test before the product | A phone looked broken on the live site. Printing the browser's own answer showed one full-page screenshot had stopped the test browser acting as a phone | Intermediate |
| Let someone try to knock your finding down | A second reviewer corrected the first reviewer's wording, and found a case it missed | Beginner |
Most valuable MP exercise: give a student a small higher-or-lower game and ask them to prove, by searching the built files, that the answer cannot be found in the browser. Then have them break it on purpose and watch the test catch it.
MP Training Opportunities ⭐ (from the L4G Play throne email's release, 2026-11-03, Session 886)
The game's throne emails went from test copies to real players on one word, after a live try turned up seven fixes.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Double opt-in by a one-button page: the email's link only opens a page, and the button on that page is the yes | A mail scanner follows every link, so a link alone could say yes for a person. The second tap also carries the 18-or-older promise, and the page names it before it comes | Email, consent, web forms |
| "I checked the file" is not "I checked the email": counting an address in the footer's file said the emails carried it, and the function they call did not. Rendering the real email is what showed it | Look at what the reader receives, not at what the code seems to hold | Testing, ground truth |
| A test can be fooled by its own data: "is ZZU first on the board?" passed on an earlier test's ZZU. Checking the run's own highlighted row fixed it | Tests share a board, so identify the thing itself, not something that merely looks like it | Testing |
| Clip a moving decoration with a road, not 100vw: a road positioned from the page's own edges leaves out a vertical scroll bar, and measuring scrollWidth over a whole loop proves it | A sideways scroll bar only shows at one moment of the animation, so one screenshot misses it | CSS layout, measurement |
| A saved default is not a choice: sound used to save as "on" even if the player never touched it, so the new muted start ignores old saved values and keeps a real choice only as long as the tab | Respecting people means knowing which of your stored values they actually chose | Product thinking, web storage |
Most valuable MP exercise: show a student an email with a confirm link and ask what happens when a spam scanner opens it. Then have them design a confirm flow a scanner can't complete and a person can finish in two taps.
MP Training Opportunities ⭐ (from the L4G Play live show's rehearsal prep and bop pictures, 2026-11-02, Session 878)
Two ships to a live game room: a closed room now always opens as a new, empty show, and every trade in the referral bop has its own picture.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| A test that passes by luck is not a test: the "same trade bopped twice" check rode on a random pop order and missed about one run in ten; recording every bop with the card the server returned turned it into a check of every case on every run | Determinism, test oracles, flaky tests | Intermediate |
| Pictures are words: each device draws its own emoji, so the pictures went under the same bless-and-fingerprint lock as the text, chosen from an older emoji set so older phones and Windows draw them | Content governance, cross-device rendering | Beginner |
| Clipping text cleanly: a two-line clamp showed the tops of its hidden third line until the line height went from 1.05 to 1.2, settled by screenshots of six variants in a real browser | CSS layout, visual testing with Playwright | Beginner |
| A shared repo needs one deploy rule: a checkout left 11 commits behind would have taken two other lanes' live work off the site; "only main deploys, and whoever ships second merges first" prevents it | Git workflow, release coordination | Intermediate |
Most valuable MP exercise: give a student a test that passes nine runs in ten and ask them to find the randomness, then rewrite the check so it tests every case on every run.
MP Training Opportunities ⭐ (from the L4G site-hardening lane, 2026-10-29, Session 846)
A reply to a Locals 4 Good sign-in email used to bounce; it now reaches a person, and Microsoft trusts the forwarded copy.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Test a DNS switch on a spare domain first: Cloudflare's "subdomain only" email switch still wrote the root domain (a signing key always, an SPF line when the root had none); it was measured on an empty spare zone, then on a copy of the live records, before the live domain was touched | Change control on shared infrastructure; measuring what a vendor's switch really does, not what its label says | Intermediate |
| An undo that runs on any surprise: the setup tool records the zone before it changes anything and puts it back if a record appears beyond the allowed list, or if the tool itself crashes | Safe automation and rollback design | Intermediate |
| Reading a mailbox's verdict on forwarded mail: Microsoft marked forwarded replies "can't verify" (compauth 601) because forwarding breaks the original sender's checks; trusting the forwarder's ARC seal turned it to a pass (reason 130), proven on the next real copy, not on the setting | Email authentication end to end: SPF, DKIM, DMARC, ARC, reading Authentication-Results | Advanced |
| A proof of live settings with controls: 17 read-only checks on two public resolvers and the provider's own view, then 12 controls, each a copy of the live state with one thing changed, each failing only its own check | Mutation-style testing applied to configuration | Advanced |
Most valuable MP exercise: give a student two copies of one forwarded email's headers (compauth=fail reason=601, then compauth=pass reason=130) and ask them to explain what changed between the two and to name the one setting that did it.
MP Training Opportunities ⭐ (from the L4G site-hardening lane, 2026-10-28, Session 838)
The checkout widget's space count was fixed and shipped, and a family-wide gap in sign-in email was found and measured.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| A refused read that looks like zero: the widget counted no sold space because a visitor's key may not read bookings, and the code treated "refused" as "nothing there"; the test stand-in served the read, so the miss stayed hidden until the stand-in refused it as the live database does | Testing against the real permission model, not a friendly fake | Intermediate |
| A browser mirror of a database rule, proven against the database itself: the website check in the browser was run on 54 values next to the database's own constraint, and both had to agree on every one | Keeping two copies of one rule from drifting | Intermediate |
| A check that cannot fail proves nothing: every check in the proof has a control (the fix taken back out) that must fail, and fail only on its own outcome | Mutation-style testing | Advanced |
| A blind reading is not a clean reading: the spam-blocklist watch asks the list's own test name on the same resolver every round, so a broken resolver can never look like "removed from the list" | Monitoring with a known-positive control | Beginner |
| Following a reply to where it lands: a sign-in email with no reply-to address, from a name with no mail record, means every reply bounces; the fix is a mail route on that one name only, never the root, where a setup wizard would take over the main mailbox | DNS mail records, blast radius | Intermediate |
Most valuable MP exercise: hand a student a count that reads zero and the permission rules of the table it reads, and ask them to prove whether zero is true or the read was refused, then to write the test stand-in that would have caught it.
MP Training Opportunities ⭐ (from the HQ strip ground-truth session, 2026-10-27, Session 827)
| Task | Skills taught | Prerequisites | Time | Complexity |
|---|---|---|---|---|
| Why a trend line lied: a disk "recovering" while it filled | Least squares by hand, step changes, reading daily data before trusting a summary | Spreadsheet basics | 2 hours | Intermediate |
| Was it the server or the laptop? Diagnose a dropped remote connection from the server's own logs (scheduled jobs on time, other logins working, the session still answering keepalives) | Linux logs, SSH keepalives, evidence over hunches | Linux command line | 2 hours | Intermediate |
| Least-privilege keys: a service-account API key, one workspace, a one-year expiry, stored only where it is used | Cloud security basics, key rotation, why a personal key is wrong for a shared job | None | 1 hour | Beginner |
| Cleaning up safely: prove every byte of a git copy is on GitHub before removing it (uncommitted files, stashes, unpushed commits, ignored files like .env) | Git internals, writing tests against throwaway repositories | Git basics | 3 hours | Intermediate |
Most valuable MP exercise: give students the 25 daily free-space readings from this session and ask "how many days until full?" Most fit a straight line through all of it and get "never". Then show them the Oct 11 jump and let them find the real answer (about three weeks) themselves.
MP Training Opportunities ⭐ (from the Interactr research and the Interactive Story Videos brief, 2026-10-30, Session 858)
TIG asked whether a lifetime video tool he bought (Interactr) is something we could build ourselves. The research turned into a new project, choose-your-own story videos, and a creative brief waiting on his approval. Records: 003 Entities/Tools/Interactr.md, 002 Projects/_Interactive Story Videos/.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Vendor due diligence before trusting a "lifetime" deal: read the terms (what "lifetime" means, the refund window), the company's public registry record (officers, filings), every help article, reviews, and the buyer's own mailbox history with the vendor | Judging a supplier from primary sources instead of its sales page | Beginner |
| An absence in the docs is not an absence in the product: searching all 55 help articles for captions, keyboard access, an API or export, and reporting "not mentioned" rather than "not supported" | Precise claims; the difference between what you checked and what is true | Beginner |
| Build vs buy, re-run when the goal changes: the first answer ("keep the licence, build later") flipped once TIG named the real goal (stories that remember choices and earn replays), because the bought tool caps minutes watched and has no story memory | Requirements drive tool choice; a recommendation is only as good as the goal it was made for | Intermediate |
| Platform rules as design constraints: YouTube forbids anything drawn over its embedded player, and iPhone Safari hides overlays in its own fullscreen, so interactive video needs self-hosting and its own fullscreen mode | Reading platform policies and browser bugs before designing, not after launch | Intermediate |
| Writing a creative brief to a standard: one page an approver reads in one sitting, every open decision with a recommendation, the three hardest tests named up front, and measures that cannot reward time-wasting ("playtime is earned by story") | Scoping creative work; designing engagement without dark patterns | Beginner |
| Privacy by default for an anonymous audience that may include minors: progress kept on the viewer's device, anonymous counts only, no lasting ID, no IP address | Youth-safe analytics design | Intermediate |
Most valuable MP exercise: give students one "lifetime deal" sales page and its terms of service. They list every promise on the page, then find the sentence in the terms that limits each one, and decide whether they would buy it for a nonprofit.
Lead Generation System
The backoffice includes a comprehensive lead generation and enrichment system supporting L4G (Locals 4 Good) business development.
Full L4G Technical Documentation: 003 Entities/Organizations/Locals 4 Good.md
The L4G system includes:
- Website: locals4good.org (Cloudflare Pages, migrated from Lovable Mar 19, 2026). SvelteKit migration complete ...
sveltekit-migrationbranch merged tomain(Session 216, Apr 12, 2026). All 20 routes verified on production (200 for public, 303 for auth-guarded). SSR confirmed: area demographics render in HTML source. Host-based redirects working (www/.com → canonical .org) viahooks.server.ts. GitHub Actionsdeploy.ymlupdated: output dir.svelte-kit/cloudflare,PUBLIC_SUPABASE_URL+PUBLIC_SUPABASE_ANON_KEYenv vars added as GitHub Actions secrets. Static assets:sitemap.xml+robots.txtinstatic/. Fixed SSR crash:$state(() => {...})stores function reference ... changed to inline expression. Public pages + checkout + full donor-partner portal (10 pages) + admin suite (13 pages) + email mirror + push notifications. Phase 6 portal includes: dashboard with postcard tracker, bookings with conversation threads (Supabase Realtime), concept selection (3 AI concepts), proof review with approve/revision form actions, supporter-creates upload with area intelligence, brand clarity wizard, post-delivery feedback with celebration animation, settings. Two new tables:l4g_conversations(RLS + Realtime),l4g_feedback. Schema v1.3.0. - Good-Neighbor redesign LIVE (2026-10-26, Session 818): all 26 public pages rebuilt; checkout is inline on each area page (a bare
/checkout301s to/areas); Privacy Policy and Terms of Use v1.0 at/privacyand/terms, public for the first time (effective October 26, 2026);/[Networking Chapter]retired (301 home). Parts of this section describe the site before the redesign: for what is deployed now, read002 Projects/_Locals 4 Good/L4G-System-Status.md(top section) and check the live site. A full rewrite of this section is deferred to the round-7 kickoff. - Delight pass LIVE (2026-10-27, Session 823): eleven small touches on the public site (the card dropping into the mailbox, three drawings that act once, the logo preview's size and month choices, a share button and a stamped "Copied", special-day postmarks, the home card's peek, the footer heart, a lost page with a returned card, a note for developers), each with a still version for reduced motion, and the pages after checkout rebuilt in the Good-Neighbor look. The old thank-you page read the booking from the browser, which the database never allows a signed-out donor-partner, so it said "Loading your postcard..." forever; the new one makes no bookings read. Showing a donor-partner their own spot waits on a small server-side read (the backend lane) and TIG's yes. Deployed state:
002 Projects/_Locals 4 Good/L4G-System-Status.md(the delight lane section). - Data Layer: Supabase (
<SUPABASE_PROJECT_ID_L4G>) ... 23 tables (schema v1.3.0), migrated from Google Sheets + new tables for conversations and feedback - Opening months LIVE (2026-10-26, Session 820): five areas take donations (Rancho Palos Verdes West and Coto de Caza from 2027-01; Newport Beach, Mission Viejo North and Anaheim Hills West from 2027-03), each offering 24 monthly cards; Anaheim Hills East is dated from 2027-06 and opens once its page has its own description.
create-checkout-sessionv17 refuses any month outside that window (the sales bound, shared with the site). Two founding-promise trades are held off sale. Mission Viejo is planned as three areas (North, Central, South). Record:L4G-System-Status.md, the 2026-10-26 23:00 paragraphs onward. - APIs: 9 Supabase Edge Functions (submit-contact-form, create-checkout-session, check-availability, expire-stale-holds, submit-category-request, subscribe-to-push, send-push-notification, provision-donor-account, record-portal-consent).
ezer-chatwas deleted 2026-10-26 (Session 820); see Ezer Chat Guide below. - Payments: Stripe Checkout via
create-checkout-sessionedge function + n8nL4G Stripe Payment Handler v1.3webhook (E2E verified Mar 18, 2026) - Automation: n8n workflows for payment processing, hold expiry, HQ sync, category request notifications, concierge response emails, conversation email mirror (webhook), email reply polling (2-min schedule)
- Category Concierge: Public category request form → admin queue at
/admin/category-requests→ approve/map/decline actions → auto-inventory population (18 months × all areas) → automated response emails viasend_l4g_concierge_response.py. Multi-channel admin notifications (Discord + SMS via Twilio) viadispatch_l4g_category_notification.py - HQ Visibility: L4G Operations module in HQ Command Center ... Kanban pipeline, production calendar, deadline alerts via
sync_hq_l4g.py(every 15 min) - Migration: Data migrated from Google Sheets → Supabase via
migrate_l4g_sheets_to_supabase.py(Feb 27, 2026) - Youth Protection:
qwf_youth_protection_standard.md(foundational directive, added 2026-04-11) ... NEVER use students in marketing/promotion until pre-employment stage. No names, photos, or identifying details externally. Applies across all QWF programs (MP, L4G, ACOFH, WOH). Overrides CX framework elements that conflict. - After-Purchase CX Vision (2026-04-11): Two-path architecture: "Supporter Creates" (templates + upload + multi-area management) and "MP Creates" (smart wizard + brand identity + 3 concepts → 3 layouts with open conversation thread + email mirror). Merge into Postcard Tracker (package-tracking UX), celebration, feedback, physical sample + frameable thank-you. QTR integration from day one (QR codes + tracking URLs). v1: TIG creates in Photoshop; long-term: Pomelli-like AI engine.
- Post-Checkout Fork:
artwork_track('self'|'create') onl4g_bookingsroutes donors to artwork upload or Brand Clarity Wizard - Brand Clarity Wizard: 3-step guided intake at
/portal/brand... saves to 9 brand fields onl4g_donor_partners - Journey Milestones:
check_l4g_milestones.pyv1.2.0 (15-min n8n poll) →send_l4g_journey_email.pyv1.2.0 (6 TIG-voice templates with portal deep links and Mailbox Walk gradient CTA buttons) + push notifications for proof_ready, proof_approved, going_to_print, postcards_mailed. Dedup vial4g_journey_events. v1.2.0 fires push notifications alongside journey emails. - Email Mirror + Notification System (Session 215):
send_l4g_conversation_email.pyv1.1.0 adds Reply-To headers (l4g-reply+{booking_id}@quietlyworking.org) and X-L4G-Booking-ID internet message header for email-to-thread routing.poll_l4g_email_replies.pyv1.0.0 polls Ezer inbox via MS Graph for replies matchingl4g-reply+pattern, strips quoted content, inserts intol4g_conversations, moves to L4G-Processed folder (state:.tmp/l4g_reply_processed_ids.json, runs every 2 min via n8n).send_l4g_push_notification.pyv1.0.0 wrapssend-push-notificationedge function with 7 event templates (print_status_change, new_message_donor/team, proof_ready, concept_ready, proof_approved, feedback_submitted). Supabasepg_nettriggernotify_l4g_conversation_insert()onl4g_conversationsINSERT → POSTs to n8n webhook for real-time email mirror. - Replies to the receipt, journey emails and the welcome reach the thread (Session 876, 2026-11-02):
send_l4g_receipt_email.pyv1.2.0,send_l4g_journey_email.pyv2.4.0 andsend_l4g_welcome_email.pyv2.7.0 set Reply-To to the booking's signedl4g-reply+<booking>.<tag>@quietlyworking.orgaddress (made byl4g_reply_address.py) plusX-L4G-Booking-ID, so a donor-partner's reply lands in that booking's conversation instead of Ezer's general inbox. Payment workflow v1.6.7 hands the welcome the earliest paid month's booking id. Only SES carries the header (Graph drops it). - Ezer and L4G mail (Session 894, 2026-11-04):
ezer_respond.pyv2.1.0 never answers an L4G donor-partner. Mail from an address on L4G's records, or whose subject names Locals 4 Good or L4G, is held for a person in Ezer's mailbox folder L4G-Held, and TIG is told the same day. - Reply addresses have their own key (Session 894):
l4g_reply_address.pyv1.1.0 signs withL4G_REPLY_TAG_KEY, with no fallback. Without the key, the reader (poll_l4g_email_replies.pyv1.2.0) stops before it looks at any email, every reply stays unread, and TIG is told; every sender sends without the threaded Reply-To rather than not at all. - Checkout and Stripe (Session 894):
create-checkout-sessioncarries the email the visitor typed withcustomer_creation: "if_required", so Stripe lists these payments under Guests, grouped by the same card, email or phone. The payment itself names the program and the typed business. New bookings leave the Stripe customer field empty. A refund by hand gets a note from us, because Stripe may not email its own on a guest payment. - What a function's log may say (Session 894): our own words and an error's code, status or class name, through
_shared/error-code.ts.deploy_l4g_edge_function.pyv1.3.0 refuses anything else, on an allow-list, in a log line or anew Fail(...), for every L4G function; a new kind of safe value is added to the rule on purpose. Forcreate-checkout-sessionit also refuses any code that names or makes a Stripe customer. Use that script, never a hand-runsupabase functions deploy, which skips both. - Email look (Session 894): the journey emails and TIG's note call
l4g_email_look.dress_for_sendat send time, as the receipt and the welcome do; the note wears the header and mailbox. Both senders stay held, and on any surprise an email goes in the old look and the day's log names the sender. - MP training opportunities (Session 894): (1) Rehearse a ship on its real path (Intermediate, 2 hours; prerequisites: bash, git): stand in the program a step calls, never the step, then put a bug back to prove the rehearsal catches it. (2) Allow-list versus block-list (Beginner, 1 hour; prerequisite: reading Python): write a rule that refuses what it has not heard of, and try to get a line past it. (3) A zero is only a zero if the query worked (Intermediate, 1 hour; prerequisites: SQL, JSON): query a log store with a wrong table name, see the empty answer, then make the script stop on it. (4) Read what a deploy really shipped (Intermediate, 1 hour; prerequisite: REST APIs): list the libraries in a function's bundle before and after a redeploy, and explain what moved although the code did not.
- Sign-in links (Session 896, 2026-11-04): every L4G sign-in email and the welcome's button link to the site's own
/auth/confirm, with the one-time token after#. A browser never sends that part to any server, so opening the link sends the token nowhere; the page takes it out of the address bar and spends nothing until its one button is pressed. The sign-in link and password reset emails also carry a code, typed on the page that asked for the email. The code and the link are one key: using either spends both, so the code is for someone reading on another device, or whose link arrived cut short. Links and codes last 1 hour (the sign-in service'smailer_otp_exp, 3600), and the emails say so; after that the person lands on the page that sends a fresh one. The vault's005 Operations/Templates/email/l4g/JSON files render what is live, andaudit_qwf_auth_mail.pyv1.5.0 alarms if the links or their hour go back. The rule behind all of it:qwf_email_sending_standard.mdPart 6b. - locals4good.com (Session 896): answers with the
.org's own web settings: full SSL, HTTPS always, TLS 1.2 or newer, HSTS for a year with nosniff. Both names, on http and https, send visitors tohttps://locals4good.org/. - Who answers for the site proof (Session 896): the L4G site-hardening lane answers for
prove_l4g_site_hardening.pyandl4g_email_look.py. Another lane may extend the proof on these terms: additions only, the diff to that lane first, one control per protection, never a weaker check, and default mode on the final tree before it is installed. - MP training opportunities (Session 896): (1) Read the rule before designing the fix (Beginner, 1 hour; no prerequisites): read Part 6b of the email standard, then mark a sign-in email from a test project against its four rules, met or missed, and explain why a code beside a link is another way to use the same key, not a spare one. (2) What a server sees of a link (Intermediate, 2 hours; prerequisites: HTML, browser developer tools): open the same link with its token after
?and after#against a local server, and read what each leaves in the log. (3) A check that can fail (Intermediate, 3 hours; prerequisites: Python, Playwright basics): write one browser check for a page's words, then put the old words back on purpose and show the check fails on that and on nothing else. - CX Automation Backend (Session 207): 6 scripts covering the full after-purchase journey ... proof upload, AI concept pipeline (enrich → brand identity → 3 briefs), concept choice processing, conversation email mirror, batch print advancement, post-delivery feedback. Plus
add_l4g_donor_booking.pyfor admin onboarding outside website checkout. - SvelteKit Migration Scaffold (Session 207):
sveltekit-migrationbranch ... adapter-cloudflare, Supabase SSR auth, Mailbox Walk CSS ported, builds in 2.4s.L4G-Phase-Prompts.mdcontains 8 copy-paste prompts for remaining migration phases 4-11. - AI Ad Pipeline:
enrich_donor_company.py→generate_brand_identity.py→generate_ad_briefs.py(Claude FLAGSHIP, 3 approaches: Trust Builder/Value Driver/Story Teller) - Print Management:
populate_l4g_postcard_config.py(36 rows, 3 areas × 12 months) +update_l4g_print_status.py(7-state forward-only machine) - Live Postcard Display: Multi-size packing algorithm (XS/SM/MD/LG/XL/Massive), booking data join, dynamic grid layout. Only booked slots shown; CTA only when entire half-section is empty.
- Area Demographics:
demographicsJSONB column onl4g_areas... full geodemographic profiles (household count, median income, education, housing, family structure, market summary). Rich card UI inAreaDemographics.tsxwith hero stats, progress bars, and market summary footer. "EDDM Routes" renamed to "Postal Routes" with custom USPS postal truck icon. L4G heart logo (logo_L4G_Heart_1k.png) deployed in header, mobile menu, and footer. Footer includes phone (773-234-KIDS), mission copy, split copyright bar. Brand rule: logo must never be CSS-inverted. - Admin Demographics Editor:
/admin/demographics... edit demographic data for any area. Area selector dropdown, 9 card sections: Hero Stats, Reach & Routes, Area Info, Household Profile, Education & Employment, Housing Profile, Family & Lifestyle, Market Intelligence (agent-generated). Each Census-sourced section has a blue "Census ACS 2022" badge; agent-generated sections have amber "Agent-generated" badge. Saves flatdemographicsJSONB and syncs column-level fields (median_income, population, total_mailboxes, eddm_routes) from JSONB values. Components:AreaDemographicsEditor.tsx,useUpdateArea.tshook. - Census ACS Enrichment:
enrich_l4g_demographics.pyv2.1.0 pulls 42 Census ACS 5-Year variables (batched for API 50-var limit, free, butCENSUS_API_KEYis required since 2026-05-12; the service can time out for minutes at a time, so retry) for each area's ZIP codes. Flat JSONB output (no nestedacssub-object). Computes: income distribution, home values, education, occupation, housing age, vehicles, household profile (median age, avg HH size, commute), family stats (married %, children %, family HH %), veterans %. Derives: Home Services Score (0-10 composite), Best For categories, market intelligence summary paragraph. Syncs column-level fields (median_income, population, total_mailboxes, eddm_routes) from JSONB values. Flags:--slug(single area),--all(all areas),--dry-run. Both Demographics and Market Intelligence cards read from identical flat data paths ... eliminates conflicting numbers. - Market Intelligence Dashboard:
AreaMarketIntelligence.tsx... Census-powered dashboard on area detail pages. Hero stat cards, narrative market summary, Home Services Score radial gauge, Best For category tags, income distribution bar chart, housing age bar chart, Census attribution footer. - Enhanced Route Cards:
RouteList.tsxroute cards show spending power ("$118M in household income"), homeowner %, and housing era chips derived from Census data. - Mobile Admin Navigation: Admin bottom nav upgraded from 5 hardcoded items to a "More" bottom sheet exposing all 12 admin pages. Eliminates dead-end navigation on mobile.
- Postcard Capacity Validation: 3-level check (AdSizeSelector UX, client-side guard, server-side edge function) ensures total slots_used ≤ 16 columns. Prevents postcard overflow for multi-size ad bookings.
- Postcard Config Admin:
/admin/postcard-config... upload background/spine images per area/month to Supabase Storagel4g-assets - Ezer Chat Guide (RETIRED 2026-10-26, Session 820): the widget was switched off 2026-06-07; the
ezer-chatfunction was deleted on TIG's "ship" because it still answered anyone on our AI key, and its two keys (ANTHROPIC_API_KEY,HQ_SUPABASE_SERVICE_KEY) were removed from L4G's function secrets. The code remains in the site repo. As it was built: conversational AI widget on all public pages.ezer-chatedge function streams responses via Anthropic Messages API (Claude Opus 4.8 as of 2026-07-06, SSE, max_tokens 500). 5-part system prompt: Ezer identity/voice rules, L4G knowledge base (areas, pricing, volume discounts), dynamic area demographics (froml4g_areas.demographicsJSONB when on area pages), navigation rules with[NAV:/path]token pattern for ask-then-navigate, guardrails. 8 React components insrc/components/ezer/: Widget orchestrator, FAB (custom octopus, Deep Forest gradient, pulsing glow), Panel (desktop 380x520 fixed + mobile vaul Drawer 85vh), MessageList (auto-scroll, streaming cursor, three-dot thinking animation), Message (markdown-lite rendering, internal link detection, "Take me there" navigation button), Input (textarea, 500-char limit, Enter to send), Greeting (contextual quick-start chips by page type), ezer-utils (nav token extraction, link parsing).EzerChatContext(React Context + useReducer) wraps PublicLayout ... state survives React Router navigation. Trust Tier 0: anonymous, ephemeral, session-scoped (crypto.randomUUID). Rate limit: 20 msgs/10min/IP. Conversation cap: 60 messages. Activity logged to HQhq_ezer_activity. Secrets:ANTHROPIC_API_KEY,HQ_SUPABASE_URL,HQ_SUPABASE_SERVICE_KEYon L4G Supabase. - Postal Route Explorer: Two-panel interactive section on area detail page (between Demographics and Benefits). Left: scrollable route card list with animated reach counter, sort toggles, demographic chips. Right: Mapbox GL map with golden delivery area boundary (#C49A3C 3-layer glow), ZIP boundaries (Census TIGER GeoJSON, 10 ZIPs), custom route markers with stagger animation, fly-to on click, styled popup cards. Lazy-loaded (separate chunk, ~472KB gz). Graceful empty state when no route data. RPV West: 23 real EDDM routes (10,786 homes across 90274 + 90275, replacing 5 placeholder routes). Admin page at
/admin/postal-routes(CRUD + bulk CSV import). Mapbox token:VITE_MAPBOX_TOKEN(GitHub secret set, URL-restricted to locals4good.org).l4g_postal_routestable with RLS (public SELECT + authenticated INSERT/UPDATE/DELETE). Unique constraint:(area_id, zip_code, route_code)... fixed from(area_id, route_code)to support same route codes across different ZIP codes. - Visual Flow Specification (v1.0.1): Design spec at
L4G-Visual-Flow-Specification-v1.0.md... companion to Mailbox Walk Palette defining HOW to compose sections. Four systems: (1) Section Rhythm ... 4 background lanes (Rest/Engage/Focus/Act) with rule: never same lane back-to-back, max 2 Focus per page, Act bookends only. (2) Card Hierarchy ... 3 weights (Primary=dominant oversized, Standard=grid with sage border, Supporting=compact). (3) CTA Repetition ... 4 touchpoints with escalating commitment, unique framing each. (4) Page Blueprints ... 13-section landing page restructured around visitor psychology (questions answered in order visitors ask them), 9-section area detail page. Gradient Direction Rule: all L4G gradients must be 135deg dark-to-light. Implementation CSS utility classes included. Hero section now matches the old WordPress site design (oldl4g.quietlyworking.org): reduced top padding, removed CTA buttons/trust line below hero headline, hero card edge-to-edge via.l4g-hero-cardclass. All 13 section cards use the old site's floating card shadow style (box-shadow: 0 10px 40px,border-radius: 30px). - Partner Tribute Page: Dedicated story page at
/partners/printing4supercheap(PartnerPage.tsx) honoring Printing4SuperCheap (P4SC) as L4G's ultimate partner. Includes: origin story (they offered Elite pricing before L4G had a name), blockquote from Chaplain TIG, "Journey of Every Postcard" 4-step visual, Jake Lorraine bio (Structure Marketing Inc., High Response Marketing ecosystem), CTA buttons to P4SC site + High Response Marketing + L4G areas.PartnerSpotlight.tsxcard on landing page (heart icon, "Made Possible By", both P4SC name instances link to partner page). Entity file:003 Entities/Organizations/Printing4SuperCheap.md. P4SC does full-service EDDM: print, bundle, prep, ship to USPS. Will support nonprofit indicia once QWF obtains it.
L4G Backend Scripts:
| Script | Purpose | Version |
|---|---|---|
send_l4g_welcome_email.py | Post-payment welcome email (Exempt, MS Graph) | v1.0.0 |
send_l4g_queue_notification.py | Queue notification when slot opens | v1.0.0 |
send_l4g_journey_email.py | 6 milestone email templates with portal deep links + Mailbox Walk gradient CTAs (Enhancement, MS Graph) | v1.2.0 |
check_l4g_milestones.py | Polls for state changes, triggers journey emails + push notifications (v1.2.0: added push for proof_ready/approved/going_to_print/mailed) | v1.2.0 |
enrich_donor_company.py | Website scraper for brand signals | v1.0.0 |
generate_brand_identity.py | AI brand guide from intake data (Claude FLAGSHIP) | v1.0.0 |
generate_ad_briefs.py | AI brief generation ... 3 concept approaches | v1.0.0 |
populate_l4g_postcard_config.py | Seeds postcard config rows for N months | v1.0.0 |
update_l4g_print_status.py | Advances print status through 7 stages | v1.0.0 |
sync_hq_l4g.py | HQ Operations module sync (every 15 min) | v1.1.0 |
dispatch_l4g_category_notification.py | Multi-channel admin notification (Discord + SMS) | v1.0.0 |
send_l4g_concierge_response.py | Auto-response email after category request resolution (Exempt, MS Graph) | v1.0.0 |
import_l4g_postal_routes.py | Import EDDM route CSV to l4g_postal_routes (--dry-run, --geocode, on_conflict upsert) | v1.1.0 |
extract_l4g_zip_boundaries.py | Extract ZIP boundary GeoJSON from Census TIGER API | v1.0.0 |
enrich_l4g_demographics.py | Census ACS 5-Year enrichment (42 vars, batched). Flat JSONB output, column sync. Income, home values, education, occupation, housing age, vehicles, household profile, family stats, veterans. Home Services Score + Best For + market summary. Flags: --slug, --all, --dry-run | v2.0.0 |
add_l4g_donor_booking.py | Admin donor onboarding ... creates auth user, donor_partner, booking, ad_proof, updates inventory. For donors who commit outside website checkout (SuiteDash, phone, in-person). --dry-run, --send-welcome | v1.0.0 |
upload_l4g_proof.py | Upload proof image to Supabase Storage, update ad_proof record to 'submitted'. --booking-id, --image-path, --dry-run | v1.0.0 |
run_l4g_concept_pipeline.py | End-to-end "MP Creates" AI pipeline: website enrichment → brand identity → 3 ad briefs → concept proof placeholders → donor notification (2x Claude FLAGSHIP) | v1.0.0 |
process_l4g_concept_choice.py | Process donor's concept selection ... marks chosen proof as in_progress, others as not_selected, updates booking to design_in_progress | v1.0.0 |
send_l4g_conversation_email.py | Email mirror for booking conversation threads (Exempt, MS Graph). Reply-To headers for email→thread routing, X-L4G-Booking-ID header | v1.1.0 |
advance_l4g_print_batch.py | Batch print status advancement for all bookings in an area/month. Wraps update_l4g_print_status.py. 7-state forward-only machine | v1.0.0 |
process_l4g_feedback.py | Post-delivery donor feedback ... stores emoji rating, response count, testimonial, would-recommend. Syncs to HQ, Discord notification | v1.0.0 |
poll_l4g_email_replies.py | Polls Ezer inbox via MS Graph for l4g-reply+ replies, strips quoted content, inserts into l4g_conversations, moves to L4G-Processed folder. State: .tmp/l4g_reply_processed_ids.json. Runs every 2 min via n8n | v1.0.0 |
send_l4g_push_notification.py | Web Push wrapper calling send-push-notification edge function. 7 event templates (print_status_change, new_message_donor/team, proof_ready, concept_ready, proof_approved, feedback_submitted). Manual and event modes | v1.0.0 |
L4G n8n Workflows:
| Workflow | ID | Schedule/Trigger | Purpose |
|---|---|---|---|
| L4G Conversation Email Mirror v1.0 | <WORKFLOW_ID> | Webhook (l4g-conversation-mirror) | Receives pg_net POST on l4g_conversations INSERT, triggers email mirror to conversation participants |
| L4G Email Reply Poller v1.0 | <WORKFLOW_ID> | Every 2 minutes | Runs poll_l4g_email_replies.py to capture email replies back into conversation threads |
Lead Generation Webhook: https://n8n.quietlyworking.org/webhook/lead-request
Available Skills
Lead generation skills live in .claude/skills/:
| Skill | Purpose |
|---|---|
linkedin-scraping | Scrape leads from LinkedIn Sales Navigator |
gmaps-scraping | Scrape local businesses from Google Maps |
apollo-scraping | Scrape verified leads from Apollo.io |
lead-generation | Multi-source lead generation routing |
lead-enrichment | Enrich existing lead lists |
email-enrichment | Find/verify emails via Anymail Finder |
friendly-name-enrichment | Clean company names to brand names |
review-enrichment | Add Google reviews to leads |
Execution Scripts
Scripts in 005 Operations/Execution/:
Scraping Scripts:
| Script | Purpose |
|---|---|
scrape_leads_linkedin.py | LinkedIn Sales Navigator scraping |
scrape_leads_gmaps.py | Google Maps local business scraping |
scrape_leads_apollo.py | Apollo.io lead scraping |
scrape_leads_yelp.py | Yelp business scraping |
scrape_leads_crunchbase.py | Crunchbase company scraping |
scrape_leads_yellowpages.py | Yellow Pages scraping |
process_lead_request.py | Master orchestrator for all sources |
Enrichment Scripts:
| Script | Purpose |
|---|---|
enrich_email.py | Email discovery via Anymail Finder |
enrich_email_reoon.py | Cheaper bulk email validation via Reoon |
enrich_reviews_google.py | Google review enrichment |
enrich_friendly_company_name.py | AI-powered name cleaning |
find_decision_makers.py | Find 1-3 decision makers per company |
extract_personalization.py | Deep website scraping for outreach hooks |
group_competitors.py | Cluster businesses for competitor FOMO |
Delivery Scripts:
| Script | Purpose |
|---|---|
upload_to_sheets.py | Push results to Google Sheets |
analyze_leads.py | Data quality audit |
L4G Cold Email Pipeline
The L4G pipeline is specialized for fundraising cold email campaigns with a two-phase enrichment strategy.
Phase 1: Initial Cold Email (Minimal Cost) Use for first outreach to new prospects:
- Scrape from Google Maps (local businesses)
- Clean company names with AI
- Find 1-3 decision makers per company
- Validate emails with Reoon (cheaper)
- Group into competitor clusters (5-7 per group)
Phase 2: Full Enrichment (Post-Conversion) Use only after prospect becomes a paying customer:
- Deep personalization from website scraping
- Google reviews + AI summaries
- LinkedIn depth enrichment
Key Features:
- Decision Makers: 1-3 contacts per company with first names for personalization
- Email Validation: Reoon validates emails at lower cost than Anymail
- Competitor FOMO: "We're also reaching out to [Business A], [Business B]..."
- Deep Personalization: AI-extracted hooks from website scraping
Directive: 005 Operations/Directives/generate_l4g_leads.md
Cost Estimates (Per 100 Leads):
| Component | Cost |
|---|---|
| GMaps scraping | ~$0.10 |
| Friendly names (Claude) | ~$0.20 |
| Decision makers | ~$0.60 |
| Reoon validation | ~$0.50 |
| Personalization | ~$2.00 |
| Total (without personalization) | ~$1.40 |
| Total (with personalization) | ~$3.40 |
Output Destination
Lead data is delivered to Google Sheets (QWU Backoffice shared drive) for team access and further processing. The upload_to_sheets.py script handles the transfer.
Rate Limiting
All scraping scripts include rate limiting via api_rate_limiter.py to avoid bans and respect source terms of service.
Communications Architecture
The QWU ecosystem uses a hybrid communications approach.
Three Layers
| Layer | Tool | Purpose |
|---|---|---|
| Personal Capture | Telegram | Quick capture, private notes |
| Team Hub | Discord | Staff, students, community |
| Power Tools | Azure VM + Claude Code | Development work |
Why Discord?
| Factor | Assessment |
|---|---|
| Cost | Free (sufficient for our needs) |
| Culture fit | Gamer/VFX/creative... matches our people |
| Bot support | Excellent, no paywalls |
| Student readiness | Used in creative industry |
| Community building | Built for this |
Discord Server Structure
🏠 QWU - UNIVERSE HUB
├── 📢 welcome-and-rules
├── 📢 announcements
├── 💬 general
├── 💬 off-topic
├── 🎉 wins
└── 💰 fundraising-general
🔒 STAFF
├── 🔒 leadership
├── 🔒 staff-only
├── 🔒 operations
└── 🔒 moderator-only
🤖 AGENTS (automation hub)
├── 🤖 agent-log # Processing summaries
├── 🤖 inbox-alerts # Notes needing review
├── 🔥 l4g-leads # Lead notifications
├── 🔔 system-status # VM/system alerts
└── 📊 daily-digest # Morning summary
🎓 MISSING PIXEL
├── 📢 mp-announcements
├── 💬 mp-general
├── 📝 mp-assignments
├── ❓ mp-help (forum)
├── 🎨 mp-show-your-work
└── 📚 mp-resources
[Additional categories for ACOFH, IYSR, WOH, QWC, L4G, Community]
Discord Role Hierarchy
| Role | Color | Purpose |
|---|---|---|
| @Admin | Red (#E74C3C) | Full server control |
| @Staff | Blue (#3498DB) | Core team members |
| @MP-Maintainer | Purple (#9B59B6) | Tier 4 students |
| @MP-Builder | Green (#2ECC71) | Tier 3 students |
| @MP-Contributor | Teal (#1ABC9C) | Tier 2 students |
| @MP-Student | Gray (#95A5A6) | Tier 1 students |
| @Volunteer | Orange (#E67E22) | Program volunteers |
| @Supporter | Gold (#F1C40F) | Donors, fans |
| @Community | Default | General public |
Webhook Configuration
Store webhook URLs in .env:
DISCORD_WEBHOOK_AGENT_LOG="https://discord.com/api/webhooks/xxx/yyy"
DISCORD_WEBHOOK_INBOX_ALERTS="https://discord.com/api/webhooks/xxx/yyy"
DISCORD_WEBHOOK_L4G_LEADS="https://discord.com/api/webhooks/xxx/yyy"
DISCORD_WEBHOOK_SYSTEM_STATUS="https://discord.com/api/webhooks/xxx/yyy"
DISCORD_WEBHOOK_DAILY_DIGEST="https://discord.com/api/webhooks/xxx/yyy"
Testing a Webhook
curl -X POST "$DISCORD_WEBHOOK_AGENT_LOG" \
-H "Content-Type: application/json" \
-d '{"content": "🧪 Webhook test successful!"}'
Data Architecture
SuiteDash vs Obsidian Boundaries
| SuiteDash (CRM) | Obsidian (PKM) |
|---|---|
| Contact records | Relationship notes |
| Transactions | Knowledge insights |
| Operational data | Agent skills |
| Tasks & projects | Research & learning |
| Communication logs | Decision rationale |
Principle: Agents facilitate intelligent handoffs between systems. SuiteDash handles transactional/operational data while Obsidian manages knowledge and insights.
Entity Resolution Security (Critical)
The Problem: Substring matching for entity lookups caused critical bugs where display names matched wrong entities.
Example Bug (2026-01-23): A BNI meeting recap email was incorrectly sent to "<EXTERNAL_EMAIL>" (an organization) because:
- Zoom display name: "Chapter President"
- Matched entity: "[Member Name], Associate Vice President..."
- Root cause: "president" substring exists in both strings
The Solution: Centralized entity_resolver.py with safety features:
from entity_resolver import resolve_for_enrichment
# Safe - uses centralized resolver with role blocklist
entity_path = resolve_for_enrichment("John Smith", caller="my_script.py")
# NEVER use substring matching directly:
# DANGEROUS: if name.lower() in f.stem.lower():
Second Bug (2026-07-30) ... the one that forced v2.0.0: an 80-minute 1:1 with a donor-partner was written onto a different person's entity file, including a 12-field frontmatter overwrite, and four of her email threads had been misfiled there since January. Two legacy scripts still carried their own 5-strategy ladders that matched on first name alone at 0.65/0.70 confidence and returned the first glob-order hit. It went undetected for six months because the pipeline logged the requested name, not the file it wrote ... the audit trail read as a clean success.
Key Features (v2.0.1, 2026-07-31):
| Feature | Purpose |
|---|---|
| Token matching | Whole-token, never substring. A search for "ann" can no longer land inside a concatenated filename like "joann" |
| Single-token rule | A one-word name resolves ONLY via exact filename, a curated alias, or corroborating context |
| Role blocklist | Role names AND role email local-parts (hello@, info@, noreply@) ... one shared set for both sides |
| Ambiguity refusal | An unbroken tie REFUSES rather than taking the first glob hit |
| Canonical beats draft | A real entity outranks a [DRAFT] stub of the same name |
| ResolutionContext | Situational context (meeting domain, sender address, org) corroborates, demotes, and breaks ties ... it never lowers the floor |
| Strictness levels | STRICT (email routing), STANDARD (enrichment), PERMISSIVE (history) |
| Write guard | authorize_entity_write() fail-closed: refuses an unvalidated path, a below-floor resolution, and a resolution pointing at a different file |
| Quarantine | A refused write is held in 005 Operations/Data/quarantine/entity_writes/, never dropped |
Honest coverage ... do NOT read "centralized" as "exclusive". A blind 5-reviewer audit
(2026-07-31) found the module's own "one resolution path" claim was false. These still
resolve entities WITHOUT the resolver and are tracked in
002 Projects/_QWU Backoffice Automation/Kickoff Prompts/entity-resolution-remaining-ladders-2026-07-31.md:
entity_lookup.py · zoom_pipeline._append_action_item_to_entity (writes) ·
meeting_entity_resolve.create_draft_person · prepare_verification_batch.find_entity_file ·
enrich_member_{instagram,linkedin}.update_entity_file (write). Also unguarded:
meeting_update_vault.append_to_person_file and email_update_vault.append_to_person_file.
Verification: tests/test_entity_resolver_misdirection.py ... 104 cases against a
synthetic fixture vault (the obvious assertion passes trivially against the real vault
and would pass with every defect present). 204/204 canonical entities resolve to
themselves; 13 of 14 mutation-injected defects are caught.
Auditing: 005 Operations/Execution/audit_entity_misdirection.py sweeps all ~949
entity files with 8 deterministic detectors (duplicate email, duplicate alias, role-mailbox
magnet, identity spread, domain spread, concatenated stem, alias-shadows-file, email-as-name).
Read-only ... it reports, a human repairs.
.venv/bin/python "005 Operations/Execution/audit_entity_misdirection.py" --markdown --min-severity 2
Rule: Any new script that looks up entity files MUST use entity_resolver.py, and any
script that WRITES to one must carry a ResolverResult through authorize_entity_write().
Never implement direct substring matching. Never trust a supplied file_path ... re-resolve
and log the resolved path, never the requested name.
What to Store in Entity Notes (003 Entities/)
| Store in Obsidian | Let Agents Search Online |
|---|---|
| Your account tier/plan | General "how to" docs |
| Your specific configurations | API reference details |
| API key location (not the key) | Troubleshooting generic issues |
| Workflows YOU actually use | Feature changelogs |
| Gotchas you've discovered | Best practices guides |
| Integration notes |
Mobile-to-Obsidian Workflow
Dual Input System:
- Drop files →
000 Inbox/___Capture/(Claude artifacts, Perplexity exports, research) - Quick dumps →
000 Inbox/Notes from my phone.md(rapid mobile text captures)
1. You drop files → 000 Inbox/___Capture/
OR append quick notes → Notes from my phone.md
2. Agent splits quick notes into individual files → ___Capture/
3. Agent moves each file to → 000 Inbox/___Processing/
4. Agent enriches (YAML, tags, internal links)
5. High confidence → auto-filed to destination
Low confidence → 000 Inbox/___Review/
6. You review items in ___Review/ (or auto-file based on threshold)
7. Final destination in appropriate folder
Project Organization
Naming Conventions
Projects in 002 Projects/ follow a visual naming convention that indicates lifecycle status at a glance.
| Type | Convention | Example |
|---|---|---|
| Evergreen | _ProjectName/ | _QWU Backoffice Automation/ |
| Client Container | _ClientName/ | _[Networking Chapter] Projects/ |
| Active Time-bound | ProjectName/ | Conference 2026/ |
| Completed | zzz-YYYYMMDD-Name/ | zzz-20251215-Old Campaign/ |
Key principles:
- Underscore prefix (
_) = ongoing, no foreseeable end date zzz-YYYYMMDD-prefix = completed (sorts to bottom, chronological within)
What Makes Something Evergreen?
| Evergreen | Not Evergreen |
|---|---|
| Client relationship containers | One-time deliverable |
| Ongoing internal systems (backoffice) | Conference with a date |
| Newsletter (continuous publication) | Specific campaign |
Test: If you can't imagine a "done" state, it's evergreen.
Project Folder Structure
Every project folder contains:
_ProjectName/
├── _Overview.md # Index/summary (required)
├── Task1.md # Individual tasks as files
├── Task2.md
├── SubProject/ # Sub-projects as folders
│ └── _Overview.md
└── zzz-20251215-Done Thing/ # Completed sub-work (bottom, chronological)
Special Files
| File | Purpose |
|---|---|
_Overview.md | Project index, status, active tasks |
_Project List.md | Global project index at 002 Projects/ root |
Example: Client Container
_[Networking Chapter] Projects/ # Evergreen (client relationship)
├── _Overview.md # Client overview, active work
├── BNI Visitor Host.md # Task (plain name)
├── 20260108 Presentation/ # Time-bound sub-project
│ └── _Overview.md
└── zzz-20251215-Holiday Card/ # Completed sub-project
Completing a Project
When a project, sub-project, or task is done:
- Rename with
zzz-YYYYMMDD-prefix:zzz-YYYYMMDD-Name/orzzz-YYYYMMDD-Name.md - Add completion notes to the bottom of the
_Overview.mdor task file - Completed items sort to bottom (
zzz-after letters) - Within completed items, chronological order (by date prefix)
- Archive during annual reviews if list gets too long
Example folder: Conference 2026/ → zzz-20260315-Conference 2026/
Example task: Design Logo.md → zzz-20260115-Design Logo.md
Completion Notes Template
Add to the bottom of completed project/task files:
---
## Completion Notes (YYYYMMDD)
**Outcome:** [What was delivered/accomplished]
**Lessons Learned:**
- [Key insight 1]
- [Key insight 2]
**Deliverables:**
- [[Link to deliverable]]
- [External URL if applicable]
YAML Frontmatter Standards
Standard Schema for SOPs
---
uid: 20241229-143022
title: "Document Title"
created: 2024-12-20
modified: 2024-12-29
version: 2.0
version_date: 251229
type: [sop]
status: [evergreen]
program: [qwf]
dg-publish: true
tags: []
---
Document History Section
Every SOP includes at the bottom:
---
## Document History
*Do not use content below this line for operations.*
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 6.96 | 261104 | TIG + Claude | Session 904: **L4G call kit parts 2 and 3.** The call log and queue, the calling screen, the one youth rule and the mark (part 2's deferred manual work), how a name goes on, the dial test that writes no try, and the QOS map check. List still empty; parked on TIG's first name. |
| 6.82 | 261104 | TIG + Claude | **L4G call kit part 1.** California's tax statement live on the site's tax answer, the Terms and receipts on TIG's "ship"; the Do Not Call policy page; QCM's Do Not Call list live on his "migrate 079" (guard A24, A25, the lift alarm). See §L4G call kit. |
| 6.75 | 261103 | TIG + Claude | Session 885: **Troubleshooting: every session suddenly cannot reach GitHub over https** (a refused login erases the stored sign-in; how to recognize it and who restores it). QQT's plan of record for the 2026-11-06 walkthrough lives in its kickoff; nothing deployed. |
| 6.74 | 261103 | TIG + Claude | Session 884: **Mail Truck Dash played alone is LIVE** (L4G Play section updated), plus MP Training Opportunities from its build. |
| 6.73 | 261103 | TIG + Claude | Session 883: **Mentor forms: a form's mentor is whoever held the meeting.** Governance note added after one form was found on the former mentor's record, plus one training row. |
| 6.72 | 261102 | TIG + Claude | Session 878: **L4G Play gets its own section** (play.locals4good.org: the arcade, the lobby, the idea box and the live show; three lanes, one deploy rule). The SHOW lane shipped TIG's rehearsal fix (a closed room always opens new and empty) and his bop pictures (one blessed picture per trade). MP notes added. |
| 6.71 | 261102 | TIG + Claude | Session 877: **The public line <TWILIO_NUMBER> is trusted at the carriers, and the L4G call kit is decided.** Twilio Phone Registry row corrected (it still said L4G SMS on a deleted Studio Flow) and now names the Trust Hub, SHAKEN/STIR, Voice Integrity and caller-name registrations. The calling rules for any QWF outbound call live in `002 Projects/_Locals 4 Good/Design Docs/L4G-Calling-Rules-Memo-v0.1.md`. |
| 2.0 | 251229 | TIG | Added section X |
| 1.0 | 251220 | TIG | Initial release |
Publishing to Transparency Site
Notes with dg-publish: true in YAML frontmatter become eligible for transparency.quietlyworking.org. Publishing is fully automated.
Architecture (v2 ... migrated Apr 2026):
The transparency site is an Astro 5.x static site on Cloudflare Pages with Svelte interactive islands. Content syncs from the Obsidian vault via sync_transparency_site_v2.py, which assigns each page to one of three content pillars.
| Component | Technology | Location |
|---|---|---|
| Framework | Astro 5.x + Svelte islands | QuietlyWorking/qwf-transparency |
| Hosting | Cloudflare Pages | qwf-transparency.pages.dev |
| CI/CD | GitHub Actions | .github/workflows/deploy.yml |
| Sync script | Python | 005 Operations/Execution/sync_transparency_site_v2.py |
| Domain | transparency.quietlyworking.org | CNAME → qwf-transparency.pages.dev |
Three content pillars:
| Pillar | URL | Content |
|---|---|---|
| Built from Broken | /built-from-broken/ | BfB series, Agent Superpowers, Content Intelligence |
| The Open Playbook | /open-playbook/ | Tool Shed, Nonprofit Tech Guide, User Manual |
| Living Proof | /living-proof/ | QWF Values, program descriptions, ecosystem health |
How it works:
sync_transparency_site_v2.pyscans the vault for.mdfiles withdg-publish: true- Assigns each file to a pillar via
PILLAR_MAP(explicit mapping + tag-based fallback) - Generates YAML frontmatter for Astro Content Collections (title, slug, pillar, description, tags)
- Resolves wikilinks to standard Markdown links (
[[Page]]→[Page](/pillar/slug/)) - Pushes to
QuietlyWorking/qwf-transparencyGitHub repo - GitHub Actions auto-deploys: fetches Digital Twin data → Astro build → wrangler deploy (~45s)
Interactive Svelte islands (loaded only when scrolled into view):
- CostTicker ... Monthly operating cost breakdown with budget bar (homepage)
- EcosystemMap ... Filterable entity grid with health indicators (Living Proof page)
- ToolShedFilter ... Search, category/tier filters, multi-sort for 49 tools with QWS scores (Tool Shed page). Data extracted from vault markdown via
extract_tool_shed_data.pyduring sync (Step 6b).
To publish a note:
- Add
dg-publish: trueto frontmatter - Run:
python "005 Operations/Execution/sync_transparency_site_v2.py" --json - Site updates live at transparency.quietlyworking.org
To keep private:
- Omit the field, or set
dg-publish: false
Automated sync:
- Immediately after pushing any
dg-publish: truefile ... agents run the sync as a follow-up step (CLAUDE.md rule) - After every
/session-wrap-up(Step 4C) - Daily at 4 AM Pacific via n8n workflow
YnawyFKfnrOao12P - Discord notification to
#system-statuson success/failure
Content series and collections on the transparency site:
- Built from Broken ... Problems we face running AI-powered nonprofit operations and the real solutions we build. 4 volumes. Series guide at
Quietly Working Universe Public Transparency Project/Built from Broken/_Series-Guide.md(not published). Each volume getsdg-publish: true+series: Built from Broken+built-from-brokentag + a PILLAR_MAP entry in the v2 sync script. Reader-first structure: Problem opens with stakes hook, no preamble sections, series context in footer. - Architecture Blueprints ... 5 ASCII art architecture maps published under Open Playbook. Green terminal styling (
#00ff00, container-query font scaling). Source:005 Operations/Dashboards/Architecture-Map-Page*.md. Published versions inQuietly Working Universe Public Transparency Project/Architecture Blueprints/.
Frontmatter enhancements (Apr 2026):
hookfield ... punchy one-liner for card display (used instead ofdescriptionon listing pages). Sync script passes it through; content schema accepts it.- PillarCard filters common tags (
QWF,QWU,built-from-broken, etc.) from card display ... only topic-specific tags shown.
Safety features:
- Private User Manual (
QWU Backoffice User Manual.md) is blocklisted by filename stem ... only the[PUBLIC]version passes - Leak detection scans for IP patterns, API tokens, SSH commands before pushing
--dry-runflag for previewing changes without pushing
Key files:
| File | Purpose |
|---|---|
005 Operations/Execution/sync_transparency_site_v2.py | Sync script v2 (Astro output, v2.0.0) ... includes Tool Shed extraction |
005 Operations/Execution/extract_tool_shed_data.py | Parses Tool Shed markdown → src/data/tool-shed.json (49 tools, v1.0.0) |
005 Operations/Execution/sync_transparency_site.py | Deprecated ... v1 (Eleventy/Vercel) |
005 Operations/Directives/sync_transparency_site.md | Directive/SOP |
005 Operations/Workflows/transparency-site-sync.json | n8n workflow (daily 4 AM Pacific) |
Currently published pages (21 files):
- Homepage (3-pillar landing), QWU Values, Tool Shed, Nonprofit Tech Access Guide, User Manual [PUBLIC]
- QWC, Locals 4 Good, OMW, IP Rights, Content Intelligence Architecture
- Built from Broken Vol 1-4, How to Give Your AI Agent Superpowers
- Architecture Blueprints (5): Mission Control, Data Flow, Integrations, Entities, Schedule
- User Manual [PUBLIC]
Transparency Content Distribution Pipeline
After the transparency site syncs, a content distribution pipeline scores articles for chaplaintig.com companion potential and queues qualifying ones to HQ Command Center for TIG's review.
How it works:
score_transparency_relevance.pyscans synced articles, applies pillar-based baselines (built-from-broken=60, open-playbook=30, living-proof=20), then uses FLAGSHIP LLM to score against 5 signals (problem-solution arc, transferable frameworks, real incidents, teaching value, chaplaintig.com fit). Final score = baseline + LLM score (cap 100). Threshold: 80%.generate_transparency_companion.pycreates a chaplaintig.com companion piece (500-800 words in TIG's brand voice) plus 3 social atoms (LinkedIn, Twitter/X, Instagram) for each qualifying article. Social atoms link to the transparency site, not chaplaintig.queue_transparency_to_hq.pycreates a WordPress draft on chaplaintig.com (blog_id 10) via SSH + WP-CLI eval-file (same pattern as video pipeline), then inserts intohq_action_queuewith the WP preview URL. Actions: Approve & Publish, Edit Draft, Skip. Deduplicates against existing pending entries.
Approval flow: "Edit Draft" opens the WordPress preview on chaplaintig.com for review. When approved in HQ, write_back_dirty_items.py publishes the existing draft (changes status from draft to publish via sudo wp post update --post_status=publish --allow-root). Social atom scheduling via Vista Social is manual for now (different format than video social_variants).
Integration: Triggered automatically as Step 4D in session wrap-up when files_synced > 0. Also available via n8n daily cron or manual invocation.
Commands:
# Score all existing articles (backfill)
python "005 Operations/Execution/score_transparency_relevance.py" --backfill
# Score + generate + queue (dry-run)
python "005 Operations/Execution/score_transparency_relevance.py" --dry-run
# Force-process a specific article regardless of score
python "005 Operations/Execution/score_transparency_relevance.py" --force-slug content-intelligence-architecture
Key files:
| File | Purpose |
|---|---|
005 Operations/Execution/score_transparency_relevance.py | LLM scoring pipeline (v1.0.0) |
005 Operations/Execution/generate_transparency_companion.py | Companion + social atoms generator (v1.0.0) |
005 Operations/Execution/queue_transparency_to_hq.py | WP draft creation + HQ action queue inserter (v1.1.0) |
005 Operations/Directives/transparency_content_distribution.md | Full directive/SOP |
.tmp/transparency_companions/ | Output directory (scoring logs, companion articles, social atoms) |
Backfill results (2026-04-12): 12 articles scanned, 8 qualified (5 built-from-broken at score 100, 3 open-playbook at 82-95), 4 below threshold (living-proof pages, user manual). All 8 queued to HQ with WordPress drafts (post IDs 29778-29785), companion articles (654-763 words), and social atoms (LinkedIn + X + Instagram). First approval: "The QWU Tool Shed" companion published live on chaplaintig.com.
Docker Fundamentals: Running Isolated Tasks
Docker lets you run code in isolated "containers"... mini computers that do one job and disappear. Your VM stays clean, and every task runs the same way every time.
Key Concepts
| Term | What It Is | Analogy |
|---|---|---|
| Image | A template/recipe for containers | Cookie cutter |
| Container | A running instance of an image | The cookie |
| Volume | A folder shared between VM and container | A window between two rooms |
Lesson 1: Run a Simple Command
docker run ubuntu echo "Hello from inside a container!"
What happens:
- Docker downloads the
ubuntuimage (first time only) - Creates a container from that image
- Runs your command inside it
- Container disappears when done
Lesson 2: Interactive Container
docker run -it ubuntu bash
The flags:
-i= interactive (keep input open)-t= terminal (give me a prompt)
Your prompt changes to something like root@a3b2c1d4:/#... you're INSIDE the container!
Lesson 3: Run Python Without Installing Python
docker run python:3.11 python -c 'print("Hello from Python 3.11")'
Docker grabs Python 3.11 and runs your code. You never installed Python on your VM!
Lesson 4: Access Your Files from Inside a Container
docker run -v $(pwd):/app -w /app python:3.11 python -c 'import os; print(os.listdir("."))'
The magic flags:
-v $(pwd):/app= Mount current folder INTO the container at/app-w /app= Start working in that folder
Lesson 5: Run a Script with Dependencies
docker run -v $(pwd):/app -w /app python:3.11 sh -c 'pip install -r requirements.txt && python your_script.py'
Lesson 6: Pass Environment Variables (API Keys)
Never hardcode API keys! Pass them securely:
docker run -v $(pwd):/app -w /app --env-file .env python:3.11 python your_script.py
Quick Reference: Docker Commands
| Command | What It Does |
|---|---|
docker run IMAGE | Run a container from an image |
docker run -it IMAGE bash | Interactive shell inside container |
docker run -v $(pwd):/app | Mount current folder into container |
docker run --env-file .env | Pass environment variables |
docker ps | Show running containers |
docker ps -a | Show all containers (including stopped) |
docker images | Show downloaded images |
docker system prune | Clean up unused containers/images |
Real-World Pattern: Running an Agent
docker run \
-v $(pwd):/app \
-w /app \
--env-file .env \
python:3.11 \
sh -c 'pip install -r requirements.txt && python execution/your_agent.py'
Docker Sandbox Security
Files in .devcontainer/
| File | Purpose |
|---|---|
Dockerfile | Container image definition |
devcontainer.json | VS Code configuration |
init-firewall.sh | Network isolation setup |
Domain Allowlist Categories
# Core services (required)
CORE_DOMAINS=(
"api.anthropic.com"
"registry.npmjs.org"
"github.com"
"marketplace.visualstudio.com"
)
# Package registries
PACKAGE_DOMAINS=(
"pypi.org"
"files.pythonhosted.org"
"registry.yarnpkg.com"
)
# MCP servers (QWF n8n)
MCP_DOMAINS=(
"n8n.quietlyworking.org"
"vistasocial.com"
)
Adding New Domains
- Edit
.devcontainer/init-firewall.sh - Add domain to appropriate array
- Rebuild container: Command Palette →
Dev Containers: Rebuild Container
Verifying Firewall
View logs inside container:
cat /var/log/firewall-init.log
Meeting Intelligence System ⭐
Overview
The Meeting Intelligence System (now formally the Meeting Experience System) transforms Zoom meetings from isolated events into relationship intelligence nodes. Every meeting captures context, every briefing surfaces that context, and over time the system builds a comprehensive map of your professional relationships.
Evergreen Project Folder: 002 Projects/_Meeting Experience System/ ... contains full system documentation, infrastructure inventory, and architecture diagram.
| File | Purpose |
|---|---|
_Overview.md | System documentation (67 components across 41 scripts, 6 directives, 10 n8n workflows), 3 experiential dimensions, 5 system phases, roadmap |
MES-System-Status.md | Infrastructure inventory: all scripts, workflows, databases, external services, known issues |
meeting-experience-system.excalidraw | Architecture diagram |
Components:
- Post-Meeting Processing - Automatically processes Zoom recordings when ready
- Pre-Meeting Briefings - Generates relationship-aware briefings before meetings
- Historical Import - Bootstraps the system with past meeting data
- Transcript Archive - Searchable meeting transcripts deep-linked to Person files
The Intelligence Flywheel
Meeting Scheduled (Calendar)
↓
Briefing Generated (20 min before)
- Pulls context from transcript archive
- Shows interaction history
- Lists open action items
↓
Meeting Happens
- You enter prepared
- Better conversation quality
↓
Recording Processed (Zoom Pipeline)
- Uses calendar attendees for accurate matching
- Links to Person/Org files
- Captures decisions, actions, goal alignment
↓
Transcript Archived
- Full text searchable
- Deep-linked from Person files
↓
Next Meeting with Same People
- Briefing has richer history
- Pattern recognition emerges
Post-Meeting Processing (Zoom Pipeline)
Trigger: n8n webhook when Zoom recording is ready
Pipeline Stages:
- Download - OAuth authentication, VTT transcript parsing (inline speaker format +
<v>tags) - Analyze - Claude API extracts topics, decisions, action items, goal alignment
- Calendar Enrichment - Match to Google Calendar event for attendee emails
- Resolve Entities - Match participants to Person/Org files (uses calendar + transcript speakers + analysis-derived names)
- Contact History - Record interaction in relationship intelligence
- Person Insights - Extract and update Person files with intelligence, action items, quotes
- Update Vault - Append meeting section to daily note (dedup-aware, replaces stale sections)
- Link Projects - Detect and link to relevant project files (word-boundary matching, stop words)
- HQ Tasks - Owner-based routing: TIG's action items →
___Tasks/files (sync to HQ), other people's items → entity files (003 Entities/People/[Name].mdunder "Their Commitments") + Discord notification
Scripts:
| File | Purpose |
|---|---|
zoom_pipeline.py | Main orchestrator |
zoom_transcript_download.py | OAuth + VTT parsing |
meeting_intel_analyze.py | Claude API analysis |
meeting_entity_resolve.py | Entity matching + SuiteDash CRM |
meeting_update_vault.py | Vault file updates |
meeting_project_link.py | Project detection |
send_meeting_followup.py | Post-meeting recap DRAFTS in TIG's Outlook (never sends) |
zoom_reconcile.py | Reconciliation safety net; holds the exclusive per-meeting processing lock |
appreciation_followup_db.py | ⛔ Component retired 2026-08-30; DB retained read-only for audit |
process_appreciation_queue.py | ⛔ RETIRED 2026-08-30 ... no-op stub |
Pipeline Flowchart: See 005 Operations/Execution/zoom_pipeline_flowchart.md for the full 8-stage visual diagram including follow-up emails, BCC monitoring, and error handling.
Usage:
# Manual processing (rare - usually triggered by webhook)
.venv/bin/python "005 Operations/Execution/zoom_pipeline.py" --meeting-id ABC123 --json
# With test fixtures (no API calls)
.venv/bin/python "005 Operations/Execution/zoom_pipeline.py" --meeting-id test-123 --use-fixtures --dry-run
Follow-Up Recaps: Drafts, Not Sends
v2.0 | Updated 2026-08-30 (replaces the Appreciation Followup System, v1.0 2026-02-08 to v1.1 2026-03-01)
After a meeting is processed, send_meeting_followup.py creates one Outlook draft per attendee
in TIG's mailbox and sends nothing. TIG reviews each draft and sends it himself. The script has
no send path, and must not be given one.
| Rule | Behaviour |
|---|---|
| Drafts only | One draft per attendee in TIG_MAILBOX, via create_outlook_draft.py. |
| Recipients | Calendar invitees intersected with the people Zoom recorded as present. Fail-open: with no participant list the filter does not run and a WARNING is logged. |
| Idempotency | (meeting_id, recipient_email) claimed with BEGIN IMMEDIATE in meeting_followup_drafts.db. |
| Recap body | Composed in Ezer's voice by compose_outbound_recap(), once per meeting. analysis.summary never reaches a recipient. Fails CLOSED: on error the draft ships with no narrative plus a visible note to TIG. |
| Internal-only fields | Never rendered: analysis.summary, key_topics, decisions[].context, action_items[].context / [].priority, all of key_quotes. Only action_items[].task / owner / due go out. |
| Notification | One SMS per meeting: how many drafts, who for, and the Outlook Drafts link. |
| Consent | check_send_permission_v2(..., "relationship_touchpoint", fail_open=True) still runs. A draft TIG sends by hand is still a send. |
What was removed and why (2026-08-30). The former appreciation system SMS'd TIG asking for a compliment, staged the email, reminded at T+1h and T+2h, and sent it anyway at T+5h with a generic warm P.S. Three faults, found by ground-truthing one SMS:
- It sent autonomously, against the standing drafts-not-sends rule set 2026-08-19.
- The P.S. carried internal analysis outbound. It interpolated
key_quotes[].significance, our private commercial read, so a supporter was emailed our assessment of her own budget position. Never read that field, or any analysis judgment, into outbound content. - Recipients came from the calendar invite with no attendance check, and the pipeline could run twice, so people who never joined were queued duplicate copies.
process_appreciation_queue.py is now a no-op stub, the appreciation handlers and their outbound
send path are gone from twilio_webhook_server.py (v3.6.0), and the n8n poller
<WORKFLOW_ID> is deactivated. appreciation_followup.db is retained read-only for audit.
Pre-meeting prep emails are drafts too (send_meeting_prep_email.py v3.0.0). Both halves of
the meeting system are draft-only.
Meeting Notes in the CRM: Daily Catch-up and Repairs
Added 2026-10-12
Every recorded meeting's notes should reach its QCM appointment (overview, transcript, insights, follow-ups). Two tools keep that true:
- Daily catch-up (
reconcile_zoom_meeting_notes.py, cron 06:20 Pacific): compares finished recordings with what QCM holds, fills what is missing from the saved analysis (no new model call), and holds anything it must not write (a young person attended, a near-empty recording, two recordings for one meeting, a failed analysis). One TODAY-tier alert per run, by date and time only. Dry run: run it with no flags. - Failed-analysis repair (
reanalyze_zoom_meeting.py --meeting-uuid <uuid>, then--live): when a meeting's write-up reads "Analysis unavailable", this makes ONE fresh analysis from the saved transcript and replaces the placeholder in the daily note, the person notes and QCM. It refuses if the saved analysis is real, the transcript is near-empty, or the youth check fails or cannot run. - "No transcript" can mean silence. A recording with audio but no Zoom transcript may simply hold no sound. Measure the archived audio with
ffmpeg -af silencedetect=noise=-60dB:d=0.5before paying to transcribe it.
Full detail: 005 Operations/Directives/process_zoom_recording.md (Edge Cases).
Missed Meetings: the 15-Minute Check
Added 2026-10-15
TIG waits 15 minutes past a meeting's start, then closes the room. The no-show check (propose_qcm_no_shows.py, cron every 15 minutes) reads the speaker record Zoom keeps with each recording. When only TIG's voice was recorded in his Zoom room from the start until 15 minutes after, it sends him ONE alert ("Possible no-show", date and time, never the name). Nothing is marked on anyone's record until he answers.
- TIG's words: "check no-show proposals" in any Claude session shows each one; then "confirm" (records the no-show) or "hold" (never asked again). Say "hold" if the person called or messaged.
- What it cannot see: Zoom tells our app who SPOKE, not who joined. Someone who joined and never spoke looks absent, which is why every proposal waits for TIG.
- What it never does: write without his confirm, touch a status a person set, propose for a young person, or send anything to the person who missed.
Policy of record: 002 Projects/_Quietly CRM/QCM-Appointment-Attendance-Policy.md.
Meeting Reconciliation System (Defense-in-Depth)
v2.0 | Updated 2026-01-26
The system ensures zero silent failures through 4-layer protection. With 2-5 Zoom meetings per week, every meeting must be captured for relationship intelligence.
Architecture:
┌───────────────────────────────────────────────────────────────────────┐
│ LAYER 1: Webhook (Primary Path) │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Zoom │───▶│ zoom_ │───▶│ #daily- │ (success) │
│ │ Webhook │ │ pipeline.py │ │ digest │ │
│ └──────────┘ └──────┬───────┘ └────────────┘ │
│ │ ┌────────────┐ │
│ └───────────▶│ #system- │ (failure) │
│ │ status │ │
├───────────────────────────────────────────────────────────────────────┤
│ LAYER 2: Daily Reconciliation (Safety Net) - 9 PM Pacific │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ 9 PM │───▶│ zoom_ │───▶│ Catches │ │
│ │ Schedule │ │ reconcile.py │ │ 7-day │ │
│ └──────────┘ │ --days 7 │ │ misses │ │
│ └──────────────┘ └────────────┘ │
├───────────────────────────────────────────────────────────────────────┤
│ LAYER 3: Weekly Deep Scan (Full Recovery) - Sunday 3 AM │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Sunday │───▶│ zoom_ │───▶│ Catches │ │
│ │ 3 AM │ │ reconcile.py │ │ 30-day │ │
│ └──────────┘ │ --days 30 │ │ misses │ │
│ │ --retry- │ │ + retries │ │
│ │ failed │ │ failures │ │
│ └──────────────┘ └────────────┘ │
├───────────────────────────────────────────────────────────────────────┤
│ LAYER 4: Health Monitoring (Observability) - Every 4 hours │
│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Every │───▶│ zoom_health_ │───▶│ Alerts if │ │
│ │ 4 hours │ │ check.py │ │ unhealthy │ │
│ └──────────┘ └──────────────┘ └────────────┘ │
├───────────────────────────────────────────────────────────────────────┤
│ ┌────────────────────────┐ │
│ │ meetings_tracker.db │ │
│ │ (005 Operations/Data/)│ │
│ │ • Locking prevents │ │
│ │ race conditions │ │
│ │ • Status tracking │ │
│ └────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
How It Works:
- Layer 1 (Webhook): Zoom webhook fires → n8n SSH to
zoom_pipeline.py→ success to #daily-digest, failure to #system-status - Layer 2 (Daily): 9 PM Pacific queries Zoom API for last 7 days → processes any missed webhooks
- Layer 3 (Weekly): Sunday 3 AM deep scan of last 30 days → force-retries failed meetings
- Layer 4 (Health): Every 4 hours checks pending count, failure count, hours since success → alerts if unhealthy
- Locking: SQLite exclusive transactions prevent webhook/reconciliation race conditions
Scripts:
| File | Purpose | Version |
|---|---|---|
zoom_pipeline.py | Main processing orchestrator | v1.12.0 |
zoom_transcript_download.py | OAuth + VTT parsing (inline speaker support) | v1.2.0 |
meeting_intel_analyze.py | Claude API analysis | v1.1.0 |
meeting_entity_resolve.py | Entity matching + SuiteDash CRM | v1.0.0 |
meeting_update_vault.py | Vault file updates (dedup-aware) | v1.2.0 |
meeting_project_link.py | Project detection (word-boundary matching) | v1.1.0 |
zoom_reconcile.py | Reconciliation - discovers and processes missed meetings (defense-in-depth: content-aware classification, targeted UUID recheck, 3-tier in-progress detection with 4-hour time gate, Discord escalation, HQ task on permanent skip) | v1.6.0 |
zoom_health_check.py | Health monitoring - detects system issues | v1.0.0 |
zoom_list_recordings.py | Query Zoom API for recent cloud recordings | v1.0.0 |
meeting_tracker.py | SQLite tracking with locking | v1.1.0 |
n8n Workflows:
| Workflow | Schedule | Purpose |
|---|---|---|
| Zoom Recording Webhook | On webhook | Primary processing path |
| Zoom Meeting Reconciliation - Daily | 9 PM Pacific | Catch webhook failures |
| Zoom Meeting Reconciliation - Weekly Deep Scan | Sunday 3 AM | Full 30-day recovery |
| Zoom Meeting Health Monitor | Every 4 hours | Proactive issue detection |
Usage:
# Check health status
.venv/bin/python "005 Operations/Execution/zoom_health_check.py"
.venv/bin/python "005 Operations/Execution/zoom_health_check.py" --json
# Check reconciliation status
.venv/bin/python "005 Operations/Execution/zoom_reconcile.py" --status
# Dry-run reconciliation (check what would be processed)
.venv/bin/python "005 Operations/Execution/zoom_reconcile.py" --dry-run --json
# Run reconciliation (7-day lookback)
.venv/bin/python "005 Operations/Execution/zoom_reconcile.py" --json
# Run deep scan (30-day lookback with retry-failed)
.venv/bin/python "005 Operations/Execution/zoom_reconcile.py" --days 30 --retry-failed --json
# Force-process a specific meeting (bypasses tracker status)
.venv/bin/python "005 Operations/Execution/zoom_pipeline.py" --meeting-id "UUID==" --force --json
# Check tracker stats
.venv/bin/python "005 Operations/Execution/meeting_tracker.py" --stats
# List Zoom recordings from last 7 days
.venv/bin/python "005 Operations/Execution/zoom_list_recordings.py" --days 7
Failure Scenarios:
| Scenario | What Happens |
|---|---|
| Webhook arrives, pipeline succeeds | Processed immediately, #daily-digest notification |
| Webhook arrives, pipeline fails | #system-status alert, retry at 9 PM reconciliation |
| Webhook missed | Discovered at 9 PM, processed, #system-status report |
| SSH failure | #system-status SSH error alert |
| Pipeline fails 3+ times | Flagged "needs attention", appears in health check |
| Transcript not ready (Zoom still processing) | skipped_transient with 7 retry attempts. Defense-in-depth chain: content detection (empty meetings skip immediately), targeted UUID recheck on final attempt, Discord escalation + starred HQ task on permanent skip |
| Extended outage | Sunday 3 AM deep scan recovers up to 30 days back |
| System unhealthy | Health monitor alerts every 4 hours until resolved |
Missing Pixel Training Opportunity (Tier 3: Specialist)
The Defense-in-Depth architecture teaches enterprise-grade reliability patterns:
- Multi-layer redundancy - Primary + backup + deep scan + monitoring (defense-in-depth)
- SQLite locking - Exclusive transactions, stale lock detection, race condition prevention
- n8n error handling -
onError: continueErrorOutput, error branches, failure alerting- Health monitoring - Threshold-based alerting, status dashboards, observability
- Idempotent design - Making operations safe to retry without side effects
Exercise: Design a Defense-in-Depth system for a different webhook service (e.g., Stripe payments). Include: (1) primary webhook handler with error alerting, (2) daily reconciliation, (3) weekly deep scan, (4) health monitoring. Document your 4-layer architecture diagram.
Pre-Meeting Briefings
Trigger: n8n polls calendar every 15 minutes, generates briefings 20 minutes before meetings
Briefing Includes:
- Attendee profiles with circle status (inner, trusted, professional, new)
- Last interaction history (from daily notes)
- Open action items (unchecked tasks mentioning the person)
- Organization context
- Suggested agenda template
- "Definition of Success" placeholders
Scripts:
| File | Purpose |
|---|---|
calendar_monitor.py | Check for upcoming meetings, coordinate briefings |
meeting_briefing.py | Generate briefing content |
Usage:
# Check upcoming meetings
.venv/bin/python "005 Operations/Execution/calendar_monitor.py" --check-upcoming --minutes 60
# Generate briefings for next hour
.venv/bin/python "005 Operations/Execution/calendar_monitor.py" --check-upcoming --generate-briefings --minutes 60 --dry-run
# Manual briefing for specific meeting
.venv/bin/python "005 Operations/Execution/meeting_briefing.py" --topic "Q1 Planning" --names "Sue,[Participant]" --duration 60
Pre-Meeting Agenda Builder (preview only, nothing built yet)
The goal: before each team, board or supporter meeting, a "since we last met" agenda waits as a draft in TIG's Outlook, with a private prep note and a text so he never forgets to send it. The agenda is written by an agent, so it is always a draft: TIG reviews and sends each one. The first target is the monthly mentor team meeting.
Where it stands (2026-11-04): two previews sit in TIG's Drafts, addressed only to him, waiting on his word about their look and content. That is the first of six questions. No script, schedule or text exists yet. State and next steps: 002 Projects/_Meeting Experience System/MES-System-Status.md.
The rule the first team meeting taught: an email built from a meeting's records keeps its privacy in the code that picks the facts. A person's review is a second look, never the control.
- The group email lists only TIG's own items, softly worded, with no dates.
- It carries nothing from the recap's story and nobody else's items. An item naming one person goes only in his private prep note.
- "Done" needs proof beyond a task list: a commit, a release note or his sent mail. Anything unproven reads as still on his list.
MP Training Opportunities (from the agenda preview, 2026-11-04, Session 907)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Allow by fact, never block by word | A private matter can appear with no name in it, so no word list can catch it. Teaches building an allow list per audience | Intermediate |
| Sent is not the same as safe to repeat | A mistake can sit in a sent folder. Teaches asking who should know a fact, not only where it was found | Beginner |
| A task list says what was said, not what happened | A task read as open the day after its feature shipped. Teaches finding proof at the source | Beginner |
| A summary can harden a promise | "I said I would try" came out with a due date. Teaches checking a summary against the speaker's words, and writing softly when the words were soft | Beginner |
| A rehearsal of the empty month | A month with no meetings would have printed a thank-you for 0. Teaches testing the zero case before anyone reads it | Beginner |
| A shortcut can reach past your own work | One git flag set other sessions' unsaved files aside for two seconds. Teaches reading what a flag does on a shared checkout, and how to prove nothing was lost | Intermediate |
Most valuable MP exercise: give a student a made-up meeting recap with one private remark in it. Ask them to write the group email, then have a second student try to find a way that remark could get through.
Pre-Meeting Prep Emails ⭐ NEW
v1.0 | Added 2026-02-07
Automated 7-day-ahead emails to external meeting attendees, sent in Ezer's voice. Also generates 30-day enriched briefings for the HQ Command Center "Upcoming Meetings" module.
Architecture:
Google Calendar (30-day lookahead)
↓
Filter External Attendees (skip TIG, @quietlyworking.org, resource calendars)
↓
Vault Enrichment (entity file, meeting history, outstanding actions, intelligence)
↓
Contact History (last contact date, relationship health assessment)
↓
├── [7-day emails] → SQLite dedup check → MS Graph send → Discord #agent-log
└── [30-day briefings] → Enriched JSON → HQ Upcoming Meetings module
Key Features:
- 7-day lookahead emails ... Warm, concise prep email asking attendees for agenda input
- 30-day enriched briefings ... Full attendee profiles with vault data, relationship health, outstanding actions
- SQLite dedup ... UNIQUE(event_id, attendee_email) prevents duplicate drafts
- Relationship health assessment ... thriving (≤14d) → healthy (≤30d) → stable (≤60d) → cooling (≤90d) → dormant (>90d)
Scripts:
| File | Purpose | Version |
|---|---|---|
send_meeting_prep_email.py | 7-day prep DRAFTS (never sends) + 30-day briefings | v3.0.0 |
n8n Workflows:
| Workflow | Schedule | Purpose |
|---|---|---|
| Meeting Prep Email - Daily | 9 AM Pacific | Create prep drafts in TIG's Outlook + generate briefings |
Usage:
# Draft 7-day prep emails (dry run ... never sends)
.venv/bin/python "005 Operations/Execution/send_meeting_prep_email.py" send --dry-run --json
# Generate 30-day enriched briefings for HQ module
.venv/bin/python "005 Operations/Execution/send_meeting_prep_email.py" briefings --days 30 --json
# Check draft history
.venv/bin/python "005 Operations/Execution/send_meeting_prep_email.py" status --json
# Cleanup old records (60+ days)
.venv/bin/python "005 Operations/Execution/send_meeting_prep_email.py" cleanup --days 60
HQ Integration:
The briefings subcommand returns enriched JSON consumed by the HQ Command Center's "Upcoming Meetings" module. Each meeting includes: attendee vault profiles, last met date, outstanding actions, intelligence snippets, relationship health scores, and prep email status.
See 002 Projects/_HQ Command Center/handoff-meeting-intelligence-pipeline.md for the full JSON schema and HQ integration guide.
Meeting-Ready BNI 1-2-1 Briefings ⭐ NEW
v1.0 | Added 2026-01-20
Enhanced briefings specifically for BNI 1-2-1 meetings that serve as working documents during the meeting itself (not just pre-meeting prep).
Key Features:
- Template Integration - Maps BNI 1-2-1 Meeting Template questions to entity data
- Intel Status Indicators:
- ✅ Confirmed - Data exists in entity file
- ⚠️ Partial - Data exists but needs verification
- ❓ Unknown - Need to ask during meeting
- Enhanced Review Intel:
- Star distribution table (count at each 1-5 star level)
- Verbatim customer quotes (10-20 examples)
- "No negative reviews" explicit messaging
- Review trends (improving/stable/declining)
- Meeting Capture Space - Action items, relationship assessment, notes
Usage:
.venv/bin/python "005 Operations/Execution/generate_121_briefing.py" "[Member Name]" \
--date "2026-01-20" --time "13:00" --location "In-person" --type "BNI 1-2-1"
Output Sections:
- Quick Contact info
- Customer Review Intel (star distribution, verbatim quotes)
- Discovery Checklist (with intel status for each template question)
- Power Team Exploration
- Personal Connection gaps
- Communication Preferences
- My Referral TO Them (capture space)
- Action Items (my commitments, their commitments, next 1-2-1)
- Relationship Assessment
Related Scripts:
| File | Purpose |
|---|---|
generate_121_briefing.py | Generate meeting-ready BNI 1-2-1 briefings |
enrich_member_reviews.py v1.3 | Enhanced review intel with star distribution |
1-2-1 Recording Processing ⭐ NEW
v1.0 | Added 2026-01-21
Process audio recordings from in-person 1-2-1 meetings (Voice Memos, etc.) into structured intelligence for Person entity files.
Pipeline:
Audio Recording (Voice Memo/m4a)
↓
[Compress if >25MB] (ffmpeg)
↓
[transcribe_121_recording.py] → Whisper API
↓
Transcript (.json + .md)
↓
[extract_121_insights.py] → Claude API
↓
Structured Insights (personal, professional, action items)
↓
Entity File Update
↓
[Archive & Cleanup]
├── Transcripts → 100 Resources/Meeting Transcripts/YYYY/YYYYMMDD-slug/
├── Recording → Google Drive: Meetings/YYYY/YYYYMMDD-slug/
└── Delete .tmp files
File Storage Architecture:
| Artifact | Location | Retention |
|---|---|---|
| Original recording | Google Drive: Meetings/YYYY/YYYYMMDD-slug/original.m4a | Permanent |
| Transcript JSON | 100 Resources/Meeting Transcripts/YYYY/YYYYMMDD-slug/transcript.json | Permanent |
| Transcript MD | 100 Resources/Meeting Transcripts/YYYY/YYYYMMDD-slug/transcript.md | Permanent |
| Insights JSON | 100 Resources/Meeting Transcripts/YYYY/YYYYMMDD-slug/insights.json | Permanent |
| Entity updates | 003 Entities/People/{person}.md | Permanent |
Naming Convention: YYYYMMDD-firstname-lastname-121 (all lowercase, hyphens)
Scripts:
| File | Purpose |
|---|---|
transcribe_121_recording.py | Audio → transcript via Whisper API |
extract_121_insights.py | Transcript → structured insights via Claude |
gdrive_upload.py | Upload recordings to Google Drive shared drive |
Usage:
# Step 1: Transcribe
.venv/bin/python "005 Operations/Execution/transcribe_121_recording.py" \
--file /path/to/recording.m4a --person "[Member Name]"
# Step 2: Extract insights (preview first)
.venv/bin/python "005 Operations/Execution/extract_121_insights.py" \
--file .tmp/121_transcripts/2026-01-21_Ramona_Petersen_transcript.json --preview
# Step 3: Apply to entity
.venv/bin/python "005 Operations/Execution/extract_121_insights.py" \
--file .tmp/121_transcripts/2026-01-21_Ramona_Petersen_transcript.json --update-entity
# Step 4: Archive (after verifying entity updates)
mkdir -p "100 Resources/Meeting Transcripts/2026/20260121-ramona-petersen-121"
mv .tmp/121_transcripts/*Ramona* "100 Resources/Meeting Transcripts/2026/20260121-ramona-petersen-121/"
# Step 5: Upload recording to Drive
.venv/bin/python "005 Operations/Execution/gdrive_upload.py" \
--file ".tmp/original.m4a" --folder "Meetings/2026/20260121-ramona-petersen-121" --name "original.m4a"
Directive: 005 Operations/Directives/process_121_recording.md
Historical Import
Import past Zoom recordings to bootstrap relationship intelligence.
What it does:
- Scans local folder of downloaded Zoom recordings
- Parses VTT transcripts
- Archives as searchable markdown in
100 Resources/Meeting Transcripts/ - Creates draft Person files for unknown people
- Updates existing Person files with meeting history
- Deep-links everything
Script: zoom_history_import.py
Usage:
# Scan without changes
.venv/bin/python "005 Operations/Execution/zoom_history_import.py" --scan
# Full import (transcripts + drafts + history)
.venv/bin/python "005 Operations/Execution/zoom_history_import.py" --full-import
# Dry run first
.venv/bin/python "005 Operations/Execution/zoom_history_import.py" --full-import --dry-run
Transcript Archive
Location: 100 Resources/Meeting Transcripts/
Format: Searchable markdown with YAML frontmatter
---
type: meeting-transcript
tags: [transcript, imported]
source: "Auto-generated from private manual v7.02 by generate_public_manual.py"
generated: "2026-11-05 07:52"
date: 2025-07-18
topic: "Time with Sue & [Participant]"
duration_minutes: 69
word_count: 6103
speakers: [TIG, [Member Name]]
---
Deep-linked from Person files:
- **2025-07-18** - Time with Sue & [Participant] (69 min) [[Meeting Transcripts/2025-07-18 - Time with Sue & [Participant].md|📝]]
Configuration
Environment Variables:
# Zoom OAuth (required for live processing)
ZOOM_ACCOUNT_ID=your_account_id
ZOOM_CLIENT_ID=your_client_id
ZOOM_CLIENT_SECRET=your_client_secret
ZOOM_WEBHOOK_SECRET=your_webhook_secret
# Google Calendar (already configured)
GOOGLE_CALENDAR_CREDENTIALS=/path/to/service-account.json
GOOGLE_CALENDAR_MAIN=[email protected]
# Discord (for notifications)
DISCORD_WEBHOOK_DAILY_DIGEST=https://discord.com/api/webhooks/...
# Anthropic (for analysis)
ANTHROPIC_API_KEY=your_api_key
n8n Workflows:
| Workflow | File | Schedule |
|---|---|---|
| Pre-Meeting Briefings | pre-meeting-briefing-monitor.json | Every 15 min |
| Meeting Prep Email - Daily | meeting-prep-email-daily.json | 9 AM Pacific |
| Zoom Recording Webhook | zoom-recording-webhook.json | On webhook |
Directives:
005 Operations/Directives/pre_meeting_briefing.md005 Operations/Directives/process_zoom_recording.md005 Operations/Directives/process_121_recording.md
Phase 3 Roadmap
Review scheduled: April 10, 2026
Potential enhancements:
- Morning digest (all meetings for the day)
- Relationship health alerts ("You haven't met with X in 30 days")
- AI-suggested meeting prep questions
Post-meeting auto-follow-up drafts→ ✅ Built (send_meeting_followup.pyv1.3.0 ... personalized action items, deferred appreciation with SMS wait-for-response, preference footer, opt-out check)Appreciation wait-for-response (SMS retry + timeout + cancel)→ ✅ Built (appreciation_followup_db.pyv1.1.0 +process_appreciation_queue.pyv1.0.0 ... T+1h/T+2h SMS reminders, T+5h fallback email, Twilio reply handler, LLM-based cancel via SMS)Pre-meeting prep emails→ ✅ Built (send_meeting_prep_email.py... preference footer, opt-out check)- Meeting pattern analytics
- Voice capture for quick context additions
QWF Email Transport Layer (SES Primary) ⭐ NEW
Overview
All backoffice Python email senders route through a single shared transport module: 005 Operations/Execution/qwf_email_transport.py. Amazon SES is the primary provider; Microsoft Graph is a gated fallback. Born 2026-06-11 during the M365 tenant block, kept permanently so supporter-facing email never again depends on a single vendor.
Why It Exists (the 2026-06-11 incident)
Microsoft retired the donated nonprofit Business Premium grant (effective 2025-07-01); QWF's subscription auto-canceled at renewal. The Tenant External Recipient Rate Limit (TERRL) quota is derived from purchased license count, so it collapsed to zero and ALL external sends bounced with 550 5.7.233 ... while internal mail and Graph API reads kept working, masking the outage from internal-only checks. Graph sendMail returned 202-accepted and then bounced (a lying 202). Twenty sender scripts were rerouted to SES the same day.
How It Works
| Piece | Detail |
|---|---|
| API | send_email(to, subject, html_body, text_body, from_persona, ...) returns {success, provider_used, message_id, error} |
| Personas | ezer (Ezer Aión), tig, noreply ... locked by the scoped IAM policy |
| Provider routing | QWF_EMAIL_PRIMARY_PROVIDER (ses) + QWF_EMAIL_GRAPH_FALLBACK (off during M365 degradation; on = true dual-provider failover) |
| BCC convention | EZER_BCC_EMAIL with recipient==bcc guard, owned by the transport (callers never add their own) |
| Observability | SES config set backoffice-emails → SNS → QSP ses-event-webhook → integration_events |
| Suppression | Shared AWS account-level list, mirrored hourly to HQ (Phase 2B infrastructure) |
Canary
check_email_transport_health.py runs via n8n (QWFEmailCanary01) every 6 hours: (1) SES leg sends to the SES mailbox simulator and verifies the email.delivered event lands in integration_events; (2) M365 recovery leg sends a Graph probe externally and reads the NDR ... NDR present = still blocked (silent), NDR absent = recovery detected (Discord alert). State file: .tmp/email_transport_canary_state.json.
Leg 3 ... inbound arrival (added 2026-08-06, v1.1.0). Legs 1 and 2 both prove mail can leave. Leg 3 proves it can arrive: SES sends to tig@ (out to SES, back in through EOP) and Graph confirms the token landed, distinguishing Inbox from Junk. Probe auto-deletes. Ladder: Inbox = RECORD (silent) · Junk or 1st miss = TODAY · 2nd consecutive miss = INTERRUPT (SMS), which at the 6h cadence is 12h of silence ... the alerting standard's irreversibility threshold. If the probe cannot be sent, the result is INDETERMINATE and never escalates, so an SES outage cannot page for the wrong failure.
Why leg 3 exists: on 2026-06-11 inbound died for four days while both outbound legs stayed green. Third parties recorded hard bounces and permanently suppressed the address ... suppression lists never self-heal, so recovery fixed nothing on their side. BNI Connect cost 8 weeks of visitor emails; Betterstack alert email was dead 57 days across 37 incidents. Two companion scripts close the loop: detect_silent_senders.py (monthly, cadence-based, finds who went quiet ... leg 3 is structurally blind to single-sender suppression) and watch_sender_recovery.py (daily, proves a claimed suppression release actually took). Diagnose suppression in ten seconds by triggering a password reset or magic link ... a sender that refuses outright is suppressed.
The machinery paid off on its first real pair (2026-08-07). Both opening watches closed CONFIRMED, 13 days inside deadline: betterstack at 04:49:46Z, and bni-connect at 18:17:05Z off the exact suppressed sender [email protected] ... its first message since 2026-06-08, a 60-day blackout. Ninety minutes later a real visitor registration arrived and the [Networking Chapter] pipeline ran unattended end to end. Neither recovery needed a human to remember to check, which was the entire point.
Whitelisting after a recovery ... measure before you widen. A vendor's "have your IT team allowlist these domains" is a template written for the worst-case tenant, not a diagnosis of yours. BNI asked for four domain allows plus a sender and an IP. Ground truth from a full-folder Graph scan of tig@: 0 of 319 Junk messages were from any BNI domain, and all 382 BNI messages ever received landed in the Inbox. Our filtering had never blocked BNI once, so the allow would have fixed nothing while handing a spoofer four trusted envelopes. Prefer the narrowest allow that carries the trust: the two-IP connection-filter entry ([Networking Platform Mail Server 1], [Networking Platform Mail Server 2], both reverse-DNS verified) tracks BNI-controlled infrastructure; a domain allow tracks a string anyone can type.
Capability limit (probed, do not re-derive): Microsoft Graph exposes no junk or safe-sender surface on v1.0 or beta ... mailboxSettings returns only calendar/locale/working-hours fields. Our 8 Graph app roles are Graph-only; Exchange Online app-only auth requires Exchange.ManageAsApp plus a certificate (a client secret is rejected), and there is no pwsh on this box. Both the mailbox Safe Senders list and the EOP tenant allow list are portal-or-PowerShell only.
Rules for New Sender Scripts
Never write inline Graph sendMail or smtplib code. Import the transport. Footers, opt-out checks, and templates stay in the calling script (Enhancement/Exempt classification is per-script policy). Full reference: 005 Operations/Directives/qwf_email_infrastructure.md §Backoffice Transactional Rerouting.
Training Opportunities
Added Session 657 (2026-09-07) ... "the deploy said success and served nothing":
| Component | Skills Developed | Difficulty |
|---|---|---|
Diagnosing a cloud deploy that reports success but serves nothing (CF Pages 83b95334) | Reading a green dashboard against ground truth; burst-probing (one hit says up, forty hits find a 25% flap); isolating by deployment url; rollback-then-re-upload instead of debugging code that was never wrong | ⭐⭐⭐ |
| Why a one-hit-a-minute uptime monitor cannot see a partial outage | Sampling and probability; designing a check whose sensitivity matches the failure it must catch | ⭐⭐ |
| Testing the DEFAULT path, not only the specified one | A sign-in proof that passes ?next=/present can never catch a wrong default landing; parameter-absent test design | ⭐⭐ |
| Running a browser proof on a second engine (Playwright WebKit as an iPad) | Cross-engine testing; what Safari does not have (background sync, install prompt, webm recording) and why the device you test on is a design input | ⭐⭐⭐ |
| Component | Skills Developed | Difficulty |
|---|---|---|
| Email outage diagnosis (NDR forensics) | SMTP error codes, SPF/DKIM/DMARC, reading bounce diagnostics, internal-vs-external probe design | ⭐⭐⭐ |
| Provider abstraction (transport seam) | Adapter pattern, failover chains, kill-switch env flags, structured returns | ⭐⭐⭐ |
| SES + SNS + webhook event pipeline | AWS IAM least-privilege, SES config sets, SNS subscriptions, event-driven observability | ⭐⭐⭐⭐ |
| Canary monitoring design | Health probes, state files, alert-on-change vs alert-always, simulator endpoints | ⭐⭐ |
| Inbound-vs-outbound blind spots | Bidirectional probe design, why a green dashboard can hide a dead channel, INDETERMINATE as a first-class state | ⭐⭐⭐ |
| Suppression-list forensics | Bounce suppression mechanics, per-sender vs per-domain scoping, magic-link diagnosis, cadence-based silence detection | ⭐⭐⭐ |
| Allow-list hygiene (measure before you widen) | Reading a vendor request critically, querying your own filter for evidence, least-privilege allow design (IP vs domain), declining a request with numbers instead of opinion | ⭐⭐⭐ |
Contact-Detail Change Guard (audit_contact_detail_changes.py) ⭐ NEW
Overview
A person's email address or phone number is how we reach them. When a bulk script rewrites one, the change is indistinguishable afterwards from the person having moved ... until mail starts landing in the wrong inbox and nobody can say when it started. This guard reads each app's own audit trail and names every contact-detail change that no person in the app asked for.
The rule
The discriminator is actor_kind on the trail row, not the value and not a second system. person
means a verified login inside the app made the change. Anything else (system, agent, operator,
anonymous), or an unattributed actor_source, means no person asked.
| Change | Meaning | Treated as |
|---|---|---|
| overwrite | a real value replaced by a different real value | flagged |
| clear | a real value erased | flagged |
| fill | a blank filled in | listed with --include-fills, never alerted |
Multi-tenancy
Findings group by org_id and the check reads nothing but the app's own trail. No contact system of
record is consulted on either side ... not QCM, not a supporter's CRM. Every tenant of a QWF app gets
the identical feature and QWF's own tenant is not special-cased. Watched columns are derived live from
information_schema joined to audit.table_policy, so a table added later is covered without anyone
remembering to add it.
Running it
# what changed in the last day
python "005 Operations/Execution/audit_contact_detail_changes.py" --since 1
# include the blanks that were filled in
python "005 Operations/Execution/audit_contact_detail_changes.py" --since 7 --include-fills
# exit 1 if anything is flagged or any app could not be read
python "005 Operations/Execution/audit_contact_detail_changes.py" --check
Scheduled daily at 06:15 Pacific with --notify. RECORD tier (Discord only) via
send_notification.py; rules contact_detail_changed_without_a_person and
contact_detail_audit_blind. An app it cannot read raises its own alert and exits 1 ... a blind
reader never reports all-clear.
Apps covered
Only apps carrying the family audit trail. QNT today (first retrofit, live 2026-09-25); add an app to
REGISTRY the day its trail ships.
Known limits
- The trail only goes back to when it shipped, so nothing earlier is visible.
- Some backoffice writers pass no surface or reason and land as
<app>.vault.unknown. Passaudit_headers(surface=..., reason=...)on every REST write so a flagged change names its author.
MP Training Opportunities (from the 2026-09-30 contact-detail guard)
| Unit | Skills taught | Level | ~Time | Why it's portfolio-worthy |
|---|---|---|---|---|
| "Did we email them, and did they reply?" ... answer it from a mail API rather than memory | REST APIs, OAuth app auth, pagination, date filtering, reading a result set critically | Beginner-Intermediate | 2-3 h | The most common real question in any org, and the one people most often answer from recall and get wrong. A student learns that searching a mailbox and listing it give different answers, and that "no reply found" is a claim needing evidence. |
| Reading an audit trail to find changes nobody asked for | SQL joins, JSONB, information_schema introspection, distinguishing a fill from an overwrite | Intermediate | 3-4 h | Teaches that logs are only worth keeping if something reads them back, and that deriving the watch-list from the schema beats hard-coding it. |
| Design exercise: a shared feature for many tenants | Multi-tenant architecture, avoiding single-customer code paths | Intermediate-Advanced | 1-2 h discussion | The original design put the check in one tenant's CRM. Moving it into the app ... where the evidence already lives ... made it work for everyone and removed the special case. A clean, small case study in "whose problem is this actually?" |
Teaching moment worth keeping: the first two detector designs would not have caught the incident they were built for. Both systems held the same wrong value, so comparing them found nothing. The lesson is to derive a detector from the causal chain (what actually made the change) rather than from the symptom (what looked different afterwards) ... and to test any proposed check against the incident before building it.
Why it exists (Sep 30, 2026)
A roster clean-up pass replaced an [Networking Chapter] member's years-old address of record on Aug 10, 2026. Seven weeks of her weekly reports went to the other inbox. Both systems agreed on the wrong value, so no cross-system comparison would have caught it ... the drift was ours, applied consistently. The trail had always recorded whether a person or a script was behind every write; nobody had read it back.
App Login Email Guard (audit_qwf_auth_mail.py) ⭐ NEW
Overview
Every QWF app's Supabase-native auth email ... the invite, password reset, signup confirmation, magic link,
email-change and reauthentication messages ... must leave from that app's OWN verified subdomain
(noreply@accounts.<app-domain>), branded, carrying the locked Auth Variant footer. This is a different leg from
the backoffice transport above: the transport governs Python senders; this governs mail the vendor sends on the
app's behalf. It is the leg most often missed, because an app works perfectly while sending unbranded mail.
Why It Exists (2026-09-09)
Quietly Quoting was one word from inviting four supporter managers when TIG asked whether the invitation would
come from the app or from the vendor. It would have come from the vendor: no custom SMTP, stock subject
("You have been invited"), one bare line, no nonprofit footer. The family already had the rule
(qwf_app_family_standard.md §9), the tool (resolve_email_config.py) and a conformance matrix with a blank QQT
row that had sat there eleven weeks. Nothing read the row. TIG's ruling: "I don't want me remembering a past
issue being the only thing standing between any user receiving a non-app domain message."
The three controls
| Control | What it does | Where |
|---|---|---|
| Daily auditor | Enumerates EVERY active Supabase project the backoffice token can see, reads each project's live auth config, and requires: custom SMTP on SES, From = noreply@accounts.<app-domain>, a sender name, all six templates non-stock and carrying the locked footer. Counts confirmed non-staff logins: a failing project with real users is High. | audit_qwf_auth_mail.py --check --notify, cron 07:30 daily; also a High gap inside audit_system.py (06:00) |
| Invite guard | assert_branded(<project ref>) raises before any script or agent creates or invites a human user in an unbranded app. A function that refuses, not a reminder. | imported by user-creating scripts |
| Provisioning step | Branding is part of standing an app up, before its first human login. | provision_qwf_sveltekit_app.md |
A project the auditor has no expected domain for is UNMAPPED, which FAILS ... a new app cannot be forgotten
by omission. Add its ref to EXPECTED the day the project is created. An auditor that cannot read the API raises
qwf_auth_mail_audit_blind rather than reporting green.
Alerting
Two HQ rules, both TODAY tier (push + Discord) via send_notification.py, never a raw webhook:
qwf_auth_mail_unbranded and qwf_auth_mail_audit_blind.
Branding an app (about an hour)
provision_app_email_subdomain.py --app-domain <app>.org... SES identity, DKIM, MAIL FROM, DMARC.- IAM user
qwf-ses-smtp-<app>scoped toses:FromAddress = noreply@accounts.<app>.org; SES config set<app>-auth-emails; six<APP>_SES_*vars in.env. Recipe:qwf_email_infrastructure.md. - Child brand
005 Operations/Brands/<APP>.brand.json→resolve_brand.py --child <APP>→validate_brand.py. - Templates
005 Operations/Templates/email/<app>/... copyqqt/(the 2026-09-09 reference; its skeletons carry the locked footer through[[TEMPLATE_PURPOSE]]), then the app inresolve_email_config.pyAPP_SUPABASE_REFS. resolve_email_config.py --app <APP> --dry-runthen--apply(it PATCHes and independently GET-verifies).- Add the ref to
audit_qwf_auth_mail.EXPECTED. - Inbox-verify: trigger one password reset to
tig@and read the headers at the mailbox. A Management API 200 is not delivery.
Family baseline (2026-09-09, first auditor run)
| State | Apps |
|---|---|
| BRANDED | QSP, QQT |
| UNBRANDED, has human logins (High) | QWR ... sends from its subdomain correctly, but its six live templates predate the locked footer (HQ 1c3891b3) |
| UNBRANDED, no human logins yet (Medium) | QNT, L4G, WHL, HQ, QKN, QTR, QST, QCM, QWB, QOS, QPN, QMP, PEZ, supporter-CC, CTIG ... each flips to High the day it gains a real user |
Known limit
After branding, the From line, DKIM alignment, subject, body and footer are all the app's ... but the verify link in the body still resolves on the vendor's own hosted domain rather than the app's. A custom auth domain is a paid Supabase add-on and a family decision TIG has not taken.
Replies to sign-in mail (2026-10-29)
The sign-in mail carries no Reply-To, so a reply goes to noreply@accounts.<app>. Until 2026-10-29 no app's
accounts. name took mail, so every reply bounced (found by the L4G site-hardening lane, 2026-10-28). L4G is
fixed: Cloudflare Email Routing on accounts.locals4good.org only (MX route1, route2, route3
.mx.cloudflare.net and its own SPF line), with one rule forwarding [email protected] to
[email protected] (an alias of TIG's mailbox) and nothing else forwarded. The root's own mail records are
untouched.
| Piece | What it does | Where |
|---|---|---|
| Setup tool | Turns the forward on for one app, guarded: it refuses any record beyond the allowed ones and puts the zone back on any surprise or crash. prove --spec is read only | setup_signin_reply_forward.py v1.2.0 |
| Proof | 17 live checks on two public resolvers and on Cloudflare, plus 12 controls, each a copy of the live state with one thing changed | prove_l4g_site_hardening.py --forward (29/29) |
| Microsoft's trust | Defender > Email authentication settings > ARC > Trusted ARC sealers: cloudflare-email.net (TIG, 2026-10-29, for the whole tenant). Without it Microsoft marks a forwarded copy "can't verify" (compauth 601); with it, compauth=pass reason=130 | TIG's Microsoft 365 tenant |
Two traps, both measured on a spare zone before the live one was touched. Cloudflare's "subdomain only" switch
still writes the ROOT: its signing key at cf2024-1._domainkey.<root> always, and an SPF line at the root when the
root has none (a root that already has one SPF line gets no second). And never use the dashboard's first-time
setup: it puts MX on the root and takes the root's mail.
Accepted on purpose: a reply can quote the sign-in link, which works up to 24 hours (mailer_otp_exp 86400),
and that copy lands only in TIG's own mailbox.
Still bouncing: quietlywriting.org, quietlyquoting.org, quietlycrm.org, quietlynetworking.org and pocketez.org
(HQ 1ecc60d7). quietlywriting.org can take the L4G path; the four with no SPF line at the root need a decision
first, because the switch would write one. Recipe and edge cases: qwf_email_infrastructure.md (the 2026-10-29
rows).
Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Enumerate-don't-list auditing (UNMAPPED fails) | Why a hand-maintained list rots and an enumeration cannot; designing a check that fails safe on the unknown | ⭐⭐⭐ |
| Turning a written standard into a control | The difference between a document, an alarm, and a function that refuses; where each belongs | ⭐⭐⭐⭐ |
| SES SMTP credential derivation | AWS SigV4-derived SMTP passwords, IAM least-privilege by ses:FromAddress, config sets | ⭐⭐⭐ |
| Email authentication at the receiving mailbox | SPF / DKIM / DMARC alignment, reading Authentication-Results, why a provider 200 is not delivery | ⭐⭐⭐ |
| Blind-instrument discipline | Why "I could not read" must never render as "all clear" | ⭐⭐⭐ |
Reading a Supporter's Scanned Documents ⭐ NEW
Built 2026-10-05, when [Supporter Organization] delivered a new contract system as eight PDFs and nobody could read them. The job was not "extract the text." It was "report what changed, and be right," which is a different and much harder thing when the reader itself makes mistakes.
The problem this solves
OCR invents differences. A character-level diff of OCR output against clean text returns a list of "changes" that are mostly the reader's own errors, and that list looks exactly like a list of real edits to the supporter's contract. Handing that to a human, and from them to the person who owns the document, is the failure the whole toolchain exists to prevent.
The four tools
| Script | Job |
|---|---|
ocr_pdf_pages.py | Verbatim OCR of a scanned page via the IMAGE tier. Refuses to correct, complete or normalise anything ... a defect in the document is evidence, and repairing it destroys the evidence. |
measure_ocr_error_rate.py | How wrong the reader is, measured against a page whose true text is already known, with a stated threshold below which a difference is noise. |
compare_contract_clauses.py | Clause comparison aligned on the HEADING, not the clause number, so a renumbering cannot masquerade as a rewrite. |
crop_pdf_evidence.py | Cuts a readable picture out of a page, centred on a located phrase, so a claim can be SHOWN to the person who owns the document rather than described to them. |
Five findings worth more than the scripts
- Check the class, not an example. "All eight documents are scans" came from running three commands against one file. Only two were scans. Per-file evidence costs seconds.
- A complete text layer is not the same as a readable one. Six of the eight PDFs had real
text, and every heading and column label was a vector outline ... visible on the page,
invisible to every extractor. On one form that was 39% of the content, including the headings
NOTICE OF RIGHT TO CANCELandNOTICE TO OWNER. A text layer can be present and still lie about what is on the page. - Measure the instrument on the real typeface before trusting it. 2,311 known words gave a 0.43% miss rate, and all of it was hyphens the text layer drops while the OCR reads them correctly. Where no ground truth existed, two different model families read the same image and agreed on every content word across 2,014 words.
- A reading-order difference is not an error. The first error measurement reported up to 107% and was measuring the wrong thing: a layout-preserving extractor and a reading-order OCR disagree about word ORDER on every multi-column page. Order-insensitive recall is the honest measure, and a recovered outlined heading is a FINDING, never an error.
- A verification step that fails on its own bug is worse than none. Two separate guards in this session condemned work that was correct ... one asked Graph for a property that does not exist on the type it queried, the other matched image ids against filenames after Outlook had rewritten both. When a check fails, first ask whether the CHECK is broken.
MP Training Opportunities ⭐ (from the 2026-10-05 contract read)
| Opportunity | Skills taught | Level | Why it is good curriculum |
|---|---|---|---|
| Classify a folder of PDFs: scan, born-digital, or born-digital-with-outlines | poppler-utils, PDF internals, content streams, evidence over assumption | Intermediate | The answer is three states, not two, and the third only appears if you look. Teaches that a clean-looking result can still be wrong. |
| Measure an OCR error rate against known-good text | Levenshtein, precision vs recall, choosing the right denominator | Intermediate | The first measurement was wrong in a way that LOOKED alarming. Teaches reading a metric before believing it. |
| Build a clause comparison that survives renumbering | parsing, alignment, regex discipline, mutation testing | Advanced | A naive diff calls the whole document changed. Teaches that the instrument shapes the finding. |
| Write a test that pins what a tool DOES, not what you assumed | pytest, test design, intellectual honesty | Beginner-Intermediate | Three real bugs were caught this way, including one that named the wrong document in its own output. |
| Verify an email actually carried its images | Microsoft Graph, MIME, Content-ID, API polymorphism | Intermediate | A 201 is not evidence. Teaches the difference between "the call succeeded" and "the thing happened." |
Prerequisite for all of the above: comfort with the command line and Python. Estimated learning time: 2 to 3 hours per row with a mentor, except the clause comparison, which is a full-day project.
Note for the drafting path
create_outlook_draft.py (v1.6.0) carries --inline (images embedded by Content-ID), --attach,
--cc and --bcc. It still has no send path, and a test now pins that fact rather than
leaving it to someone remembering. Two Graph behaviours are documented in the file because both
produce a silent wrong answer: POST /messages returns 201 and drops an attachments array,
and on the SENT copy Outlook rewrites every cid to a GUID and renames every file.
Every reply carries one green ask card (v1.6.0, 2026-10-27). A threaded reply draft
(--reply-to-message-id) is drawn in TIG's blessed reply design: his greeting, the reply, ONE green
card labelled WHAT HAPPENS NEXT as the last thing before his sign-off, and the sign-off once. The
card is required (--next-action); when there is nothing for the reader to do, the card says so. A
body that already signs off is refused, so a reply can never go out signed twice. TIG asked for this
after restyling the ask of a hand-drafted reply himself: a reader who scans should never miss the one
thing being asked of them. Tests: tests/test_create_outlook_draft_reply_design.py.
MP Training Opportunities (from the 2026-10-27 shared-folder diagnosis, Session 826)
| Opportunity | Skills taught | Level | Why it is good curriculum |
|---|---|---|---|
| Prove a shared folder is really empty before you believe it | Google Drive API, its query language, a file's own parents and owners fields | Intermediate | A folder listing left out 31 files that the same account could open one by one. Teaches checking the thing itself, not the index of it, and combining two searches when neither finds everything. |
| Start an inbox search at the last message you actually read | Microsoft Graph mail filters, time windows, evidence over memory | Beginner | A search that began at our own last reply missed a note that had arrived just before it. Teaches choosing the window from the other person's side. |
| Design the one ask so a scanning reader cannot miss it | HTML email, accessibility, writing for a busy reader | Beginner | Every drafted reply now ends with one green card naming what happens next. Teaches designing for the reader's three seconds. |
Prerequisite: comfort with Python and the command line. Estimated learning time: 1 to 2 hours per row with a mentor.
Outlook Email Processing ⭐
Overview
The Outlook Email Processing system automatically monitors Microsoft 365 Outlook inbox and sent items, classifies emails, resolves sender entities, updates vault files with correspondence history, and creates tasks for action items. It integrates with SuiteDash CRM for relationship-aware processing.
Philosophy: Email handles people and conversations. RSS feeds handle content and newsletters. This separation keeps the vault focused on relationships.
Pipeline Architecture
Outlook (Inbox + Sent Items)
│
▼
[outlook_fetch_emails.py] ← Microsoft Graph API (OAuth2)
│ Fetch from: Inbox, SentItems
│
▼
[email_classify.py] ← Claude API
│ Classifications: action-required, waiting-on, informational,
│ conversation, transactional, newsletter
│
▼
[email_entity_resolve.py]
│ 1. Match contact → Person file (email, name, alias)
│ - Received: resolve sender
│ - Sent: resolve recipient
│ 2. Query SuiteDash CRM (if not in vault)
│ 3. Create [DRAFT] for significant unknowns
│ 4. Detect circle tier (inner/trusted/professional/new)
│
├── VIP? → Discord #inbox-alerts (immediate)
│
▼
[email_update_vault.py]
├── Person file: ### Email Thread section
├── Daily journal: ## Email Activity table
│
▼
[email_task_create.py] (if action-required)
│ → 000 Inbox/___Tasks/
│
▼
[Sender Tracking] (v1.6.0)
│ → hq_email_senders (Supabase) — PostgREST upsert
│
▼
[Newsletter Audit] (if newsletter detected)
└── → 000 Inbox/___Review/Newsletter-Audit.md
Email Classification Types
| Type | Description | Action |
|---|---|---|
action-required | Needs response or action | Create task, priority alert |
waiting-on | Awaiting their reply | Track in "Waiting On" |
informational | FYI, announcements | Log only |
conversation | Ongoing thread | Update thread summary |
transactional | Receipts, confirmations | Archive, minimal processing |
newsletter | Marketing emails | Route to Newsletter Audit |
VIP Handling
VIP contacts get immediate Discord notifications and priority processing:
- Inner circle contacts (from SuiteDash)
- Emails/domains in
OUTLOOK_VIP_EMAILS/OUTLOOK_VIP_DOMAINS
Scripts
| File | Purpose |
|---|---|
outlook_pipeline.py | Main orchestrator (v1.6.0 ... suppression pre-check + sender tracking to hq_email_senders) |
outlook_fetch_emails.py | Microsoft Graph API: OAuth, fetch, pagination |
email_classify.py | Claude API classification of email intent |
email_entity_resolve.py | Match sender to People/Organizations, SuiteDash CRM lookup |
email_update_vault.py | Append correspondence sections to vault files |
email_task_create.py | Create tasks for action items (with deduplication & consolidation) |
Task Creation Intelligence (v1.3.0)
Not all action-required emails need a task. The system uses intelligent filtering to reduce noise:
Task Creation Rules: Only create a task when ALL conditions are met:
- Classification is
action-required effort_levelismoderateorsignificant(not trivial/quick)action_typeisresearchorproject(not reply/calendar/routine)- Due date is in the future (or not set)
- Email is NOT forwarded (no "Fw:", "Fwd:" prefix)
- Subject doesn't match exclude patterns in
OUTLOOK_TASK_EXCLUDE_PATTERNS - No existing task for same person + similar topic in last 7 days
What DOESN'T Become a Task:
| Type | Handling |
|---|---|
| Quick replies | Just respond in email, no task needed |
| Calendar events | Add to calendar instead |
| Routine notifications | Log only (BNI visitors, invoices, etc.) |
| Forwarded emails | Already being actioned |
| Past-due items | Event already happened |
Classification Fields (from Claude):
effort_level: trivial, quick, moderate, significantaction_type: reply, calendar, research, project, routine
Duplicate Detection:
- Jaccard similarity (70% threshold) checks existing task titles
- Person+topic dedup: Skip if same person + 2+ matching keywords in last 7 days
- Prevents task pile-up from repeated email reminders
Person Consolidation:
- Multiple action items from the same person/org are grouped into one consolidated task
- Consolidated tasks include a checklist of all action items with email context
- Uses the highest priority among all grouped items
- Tagged with
["email-task", "consolidated"]for filtering
Environment Variable:
# Add to .env for pattern-based filtering
OUTLOOK_TASK_EXCLUDE_PATTERNS=BNI: A visitor has registered,Your invoice is ready,Your weekly report
Example: If Alice sends 3 research-related emails, the system creates one task "Multiple action items from Alice Smith (3 items)" with a checklist. But if she sends a quick "yes, confirmed" reply, no task is created.
Usage
# Manual run (usually triggered by n8n)
.venv/bin/python "005 Operations/Execution/outlook_pipeline.py" --json
# With options
.venv/bin/python "005 Operations/Execution/outlook_pipeline.py" \
--limit 10 \
--dry-run \
--json
# Test with fixtures
.venv/bin/python "005 Operations/Execution/outlook_pipeline.py" \
--use-fixtures \
--dry-run \
--json
n8n Workflow
Workflow: outlook-email-workflow.json
Schedule: Every 15 minutes
Flow:
Schedule Trigger (every 15 min)
→ SSH: Run outlook_pipeline.py --json
→ Parse JSON output
→ If emails_processed > 0:
→ Post to #agent-log
→ If vip_count > 0: Post to #inbox-alerts (VIP alert)
→ If tasks_created > 0: Post to #inbox-alerts (task alert)
Directive
Full documentation: 005 Operations/Directives/process_outlook_email.md
What Counts as a Newsletter (changed 2026-11-04)
Until 2026-11-04 the pipeline searched the whole body for words like "unsubscribe" and "email preferences". A reply quotes the email before it, and our own footer says "Manage your email preferences", so every person who answered one of our emails was filed as a newsletter: no classifying, no reply draft, no text to TIG, and nothing in their record. About half of what watched people sent after 2026-10-23 went quiet this way, and two kind notes sat unread for 19 days.
The rule now (email_bulk_signals.py): someone else's mail keeps the label only when the words AND the message's own delivery markers agree: Precedence: bulk, list or junk, or a List-Unsubscribe with a web link, unless it has both Re: and In-Reply-To. The label can only shrink. Our own automated sends keep the old rule on purpose, so hundreds of them a month cost nothing and stay out of people's records. On the newest 350 inbox messages: 213 still labelled, 0 newly labelled, 121 unchanged, 16 released to the classifier, about $2 a month. Replies missed since August were filed afterward by refile_mislabeled_replies.py (53 of them).
Known gap ([Networking Chapter]-System-Status K57): going forward, the vault writer keeps one section per subject and date, so one email on a thread that day can hide the next one from a person's record. Directive: process_outlook_email.md.
Watched-Sender Reply Drafter (armed 2026-10-07) ⭐ NEW
What it does: when someone on TIG's watch list emails tig@, it reads the thread with read-only tools, writes a threaded reply DRAFT in TIG's Outlook (never a send), and texts TIG once: "draft_watched_sender_replies.md.
| Piece | Where |
|---|---|
| Watch list | QCM switches "Watch & Respond" (contact) and "Watch & Respond to Domain" (organization), migration 062. Today: 18 active [Networking Chapter] members + <supporter-domain>.com |
| Flipping a switch | The CRM site's contact and organization pages (Relationship Console build, coming). Until then, ask any session: "stop drafting for [name]" |
| Schedule | every 15 min at :07/:22/:37/:52 (seven minutes after the Outlook pipeline sorts); member rule 06:30 Pacific |
| Spend | $3 per email, $15 per day |
| Caching | Since v1.6.0 (2026-10-21) every call reads the whole conversation so far from Anthropic's prompt cache, which roughly halves the cost of an email that needs many look-ups. Healthy: cached_tokens grows call over call in 005 Operations/Data/llm_usage.jsonl. It is ~90% of the Foundation's direct Anthropic API spend, so Anthropic's "prompt cache hit rate is low" emails point here first |
| Look (v1.7.0, 2026-10-23) | TIG's new email design: his greeting, then "You asked" (their own words, matched exactly) and a one-line short answer, then the reply, then his green "what happens next" line, his sign-off and one line of his card links (only for one reader with no stop). Either box simply drops out when it does not fit. Back to the plain look: reply_design: "classic" in the same config file |
| Closing notes (v1.8.0, 2026-10-23) | "Thanks for the reminder!", "Got it", "Automation is amazing!" and the like get no draft and no text; the drafter judges by meaning (no word list) and drafts whenever unsure. If the thread shows something TIG still owes, he gets one text saying the reply brief has it |
| Review fixes (v1.8.1, 2026-10-24) | the first independent review's fixes: a question followed by a "Thanks!" on the same thread is still drafted (it had been marked covered by a draft that never existed); "You asked" shows only a whole sentence of the sender's own words, never TIG's; the short answer is checked with the reply; a layout error falls back to the plain body |
| Notes' remembered line (2026-10-24) | notes drafted with create_outlook_draft.py --design note to ONE person carry a "Since we last talked" box quoted word for word from the newest recap TIG sent them (90 days at most), with strict rails (no youth, family, health, money, faith, hard times or other people's names); absent when unsure; --corner none leaves it off. Cc and Bcc count as readers |
| Stand down | set armed: false in 005 Operations/Execution/watched_sender_config.json |
| Evidence | one brief per watched email in 002 Projects/_QWU Backoffice Automation/Reply Briefs/ |
Testing tip: a test email must ask something the sender's scope can answer, or the honest outcome is "needs your judgment." Gmail may send under a "send mail as" alias, so find a test email by subject and read its real From.
MP Training Opportunities ⭐ (from the 2026-10-07 drafter arming)
| Task | Skills taught | Prerequisites | Time | Level |
|---|---|---|---|---|
| Rehearse a database migration inside an always-rolled-back transaction (the migration body + a DO block of checks that ends in an unconditional RAISE, so nothing persists), then prove zero residue | SQL, PL/pgSQL, transactions, test design | basic SQL | 2-3 h | Intermediate |
| Run a three-path end-to-end test of an AI assistant on a real inbox (a normal request, an out-of-scope request, a prompt-injection attack) and judge each outcome against the design | AI safety testing, prompt injection, evidence gathering | email basics | 1-2 h | Beginner |
SuiteDash CRM Integration ⭐
Overview
SuiteDash is QWF's CRM platform. The QWU Backoffice integrates with SuiteDash to provide relationship-aware automation across all pipelines (email, meetings, briefings).
Key Concepts:
- Circles = SuiteDash groupings that control portal access and marketing
- Tiers = QWF relationship levels (Inner, Trusted, Professional, New)
- Two-way sync = Vault Person files ↔ SuiteDash contacts
Circle Architecture
The Two Circle Systems
| Relationship Tiers | Purpose |
|---|---|
| Inner | Full trust, immediate response, highest priority |
| Trusted | Proven relationships, same-day response |
| Professional | Active business relationships, 24-48 hour response |
| New | Fresh contacts, unproven |
| Unknown | Not in system |
Tier → Priority Mapping
| Tier | Email Priority | Meeting Priority | Access Level |
|---|---|---|---|
| Inner | Immediate alert | Direct calendar | Full transparency |
| Trusted | High | Priority slots | Project-level |
| Professional | Normal | Standard booking | Standard |
| New | Normal | Limited slots | Basic |
SuiteDash Circle Names
| Circle Name | Relationship Tier | Purpose |
|---|---|---|
Chaplain TIG Inner Circle | Inner | Highest trust, manual only |
QWF Trusted | Trusted | Proven relationships |
QWF Professional | Professional | Active business |
QWF New | New | Default for new contacts |
Two-Way Sync Architecture
┌─────────────────────┐ ┌─────────────────────┐
│ Obsidian Vault │ │ SuiteDash CRM │
│ 003 Entities/ │ │ │
│ People/*.md │◀───────▶│ Contacts │
│ │ │ │
│ - YAML frontmatter │ sync │ - Custom fields │
│ - circle: trusted │─────────│ - Circle membership│
│ - suitedash_id │ │ - Program status │
└─────────────────────┘ └─────────────────────┘
Sync Direction 1: Vault → SuiteDash
Script: vault_to_suitedash.py
Schedule: Daily at 6:00 AM Pacific
What syncs: Person file frontmatter → SuiteDash custom fields
Sync Direction 2: Tier → Circles
Script: circle_sync.py
Schedule: Daily at 6:30 AM Pacific (after vault sync)
What syncs: Person's calculated tier → SuiteDash Circle membership
How tier is determined:
- Read Person's program statuses from vault frontmatter
- Apply tier mapping (e.g., MP
maintainer= Inner, QWCactive= Trusted) - Use highest applicable tier
- Update SuiteDash Circle membership
Scripts
| File | Purpose |
|---|---|
vault_to_suitedash.py | Push vault Person data → SuiteDash custom fields |
circle_sync.py | Sync relationship tier → SuiteDash Circle membership |
suitedash_contacts.py | SuiteDash API wrapper: lookup, create, update contacts |
n8n Workflows
| Workflow | Schedule | Purpose |
|---|---|---|
vault-to-suitedash-workflow.json | Daily 6:00 AM | Sync vault → SuiteDash fields |
circle-sync-workflow.json | Daily 6:30 AM | Sync tiers → Circle membership |
Usage
# Vault → SuiteDash sync
.venv/bin/python "005 Operations/Execution/vault_to_suitedash.py" --all --json
# Circle tier sync
.venv/bin/python "005 Operations/Execution/circle_sync.py" --all --json
# Dry run (preview without changes)
.venv/bin/python "005 Operations/Execution/circle_sync.py" --all --dry-run --json
Integration Points
SuiteDash CRM data is used by:
- Email Processing → Detect circle tier for priority handling
- Meeting Intelligence → Include circle status in briefings
- Pre-Meeting Briefings → Show relationship context
- Entity Resolution → Create full Person files for unknown contacts found in CRM
Directives
Full documentation:
005 Operations/Directives/circle_architecture.md(comprehensive tier definitions)005 Operations/Directives/sync_vault_to_suitedash.md(vault → CRM sync)
MP Training Opportunities (from the 2026-10-23 email design + closing notes, Session 779)
| Task | Skills taught | Prerequisites | Time | Level |
|---|---|---|---|---|
| Add a new feature to a live system so it can only DROP OUT, never break the main job (two optional answer fields that leave the draft exactly as before when missing or unsafe) | defensive design, schema evolution, testing | Python basics | 2 hours | Intermediate |
| Decide when a rule must be judgment, not a word list: "Thanks, but I can't make it" looks like a thank-you and is a change of plan | AI judgment design, false-positive vs false-negative cost | none | 1 hour | Beginner |
| After a website move, check every asset your emails still load (a signature photo went 404 the morning a site was frozen) | web debugging, HTTP status codes, email rendering | browser basics | 1 hour | Beginner |
| Change live data through the system's own named action, then dry-run tomorrow's automation to see every email and calendar change it will cause | APIs, audit trails, safe operations | REST basics | 1-2 hours | Intermediate |
Portfolio value: high. The strongest unit is the first: a nicety that can fail a draft is worse than no nicety.
Transcript Extraction System (Planned)
Purpose
Extract detailed transcripts from Claude Code sessions for:
- Student curriculum (showing how agents think)
- Transparency publishing
- Debugging and improvement
Adaptation from Simon Willison's Tool
Based on claude-code-transcripts, adapted for QWU:
| Simon's Approach | QWU Adaptation |
|---|---|
| HTML output | Obsidian Markdown |
| GitHub Gists | transparency.quietlyworking.org |
| Standalone tool | Integrated with DOE architecture |
| Generic naming | QWU tag taxonomy + YAML frontmatter |
Output Location
qwu_backOffice/
└── transparency/
└── agent-transcripts/
├── _index.md
├── 2024-12-30-backoffice-auth-fix.md
└── 2024-12-30-vista-social-setup.md
Status: Planning phase. Implementation TBD.
Ez/Ezer Mascot
QWF's official mascot... an intelligent, empathetic octopus composed of transformable pixel-blocks.
Character Details
| Attribute | Description |
|---|---|
| Name | Ez or Ezer (Hebrew: "strength, warrior-ally") |
| Form | Octopus made of "stoicheia" pixel-blocks |
| Connection | Each block represents a Missing Pixel student |
| Personality | Background empowerer, patient teacher |
| Visual Style | Colorful, transformable, adaptive |
Why an Octopus?
- Multiple arms reaching out to help
- Highly intelligent and adaptable
- Each arm can work independently (like QWF's programs)
- Soft exterior, strong interior (vulnerability + strength)
Character Bible Location
Full character bible (visual guidelines, personality, animation principles) stored in:
Resources/Brand/ez-ezer-character-bible.md
Ez Terminal (Scheduler) ⭐ NEW
Added: Session 42 (January 23, 2026) | Updated: Session 48 (January 24, 2026)
The Ez Terminal is an interactive retro-styled terminal interface for scheduling appointments with TIG. Available at https://twin.quietlyworking.org (port 8767)
Version History
| Version | Date | Key Features |
|---|---|---|
| v5.0.0 | 260124 | QWU Universe adventure, TIG Trivia, Guestbook, 4 Easter Eggs |
| v4.0.0 | 260123 | Oregon Trail game with cross-session save |
| v3.9.0 | 260123 | Achievements system (40 achievements, 8 categories) |
| v3.8.0 | 260122 | Smart Suggestions system |
| v3.7.0 | 260122 | Daily Wisdom system |
| v3.6.0 | 260122 | Cross-session memory via localStorage |
| v3.5.0 | 260122 | Recognized Identities (TIG device detection) |
| v3.3.0 | 260118 | Initial deployment |
Features
Core Scheduling:
- Circle-based scheduling with access tiers (inner, trusted, professional, new, unknown)
- Calendar integration with Google Calendar
- Multiple theme support (green, amber, matrix, deep sea, synthwave)
Engagement Systems:
- Daily Wisdom: Rotating quotes from TIG's reading list, one per day per visitor
- Smart Suggestions: Contextual command recommendations based on session state
- Oregon Trail: Classic game ported to JavaScript with full save/load support
- QWU Universe: Text adventure exploring the QWF ecosystem (v5.0.0)
- TIG Trivia: Quiz game on TIG izms, QWF programs, and pop culture (v5.0.0)
- Guestbook: Leave messages for future visitors (v5.0.0)
Achievement System (v3.9.0 → v5.0.0):
- 47 achievements across 8+ categories (expanded in v5.0.0):
- Explorer (navigation), Traveler (visits), Sage (wisdom), Connector (scheduling)
- Timekeeper (sessions), Lorekeeper (stories), Legendary (epic feats), Hidden (secrets)
- Adventurer (games, trivia, guestbook), Easter Eggs (secrets found)
- 5-tier point system: Bronze (10), Silver (25), Gold (50), Platinum (100), Diamond (250)
- Commands:
achievements,trophies,badges,achievements all - Persistent tracking via localStorage
Oregon Trail (v4.0.0):
- Full game with 15 landmarks (Independence, MO → Willamette Valley)
- Party management, shop system, hunting, river crossings
- Random events: diseases, weather, encounters, equipment failures
- Cross-session save: game state persists in localStorage
- Commands:
trail,oregon,oregon trail
QWU Universe Adventure (v5.0.0):
- 12+ explorable locations representing the QWF ecosystem
- Locations: Gates of Hope, WHELHO Plaza, TIG's Lighthouse, Dream Forge (L4G), Gratitude Garden, Legacy Summit, Mentor's Grove, Reflection Pool, Archive of Dreams, Chamber of Origins, Hope's Heart
- 9 collectible items with narrative significance
- 5 NPCs with dialog trees (Blacksmith, Mentor, Ez, Keeper, Librarian)
- Commands:
adventure,universe
TIG Trivia (v5.0.0):
- Three categories: TIG izms, QWF Programs, Pop Culture (Ted Lasso, Firefly, Star Trek)
- Streak bonuses for consecutive correct answers
- Score tracking with achievements
- Commands:
trivia
Easter Eggs (v5.0.0): 14 total secrets discoverable through hidden commands and behaviors:
| Command | Secret Name | Theme |
|---|---|---|
kobayashi, kobayashi maru | Kobayashi Wisdom | Star Trek |
firefly, serenity | Firefly Fan | Firefly |
believe, ted lasso | Believe Sign | Ted Lasso |
biscuits | Biscuit Box | Ted Lasso |
lumala (as name) | lumala | QWU Lore |
lum netsach | Lum Netsach | QWU Lore |
pix, the pix | Pix Seeker | Missing Pixel |
splinter | Splinter Whisper | QWU Lore |
most precious one, mpo | MPO Memory | QWU Lore |
philippi | Philippi's Legacy | QWU Lore |
ezer, who are you | Ez Origin | QWU Lore |
| 10+ visits | Loyal Friend | Behavioral |
theme deep | Deep Dweller | Behavioral |
sdf x3 | SDF Master | Behavioral |
Commands Reference
| Command | Description |
|---|---|
help | Show available commands |
book, schedule | Start scheduling flow |
wisdom | View daily wisdom quote |
achievements | View unlocked achievements |
trail | Start/continue Oregon Trail |
adventure | Start QWU Universe adventure |
trivia | Start TIG Trivia game |
guestbook | View/sign the guestbook |
theme <name> | Change terminal theme |
clear | Clear terminal output |
Files
| File | Purpose |
|---|---|
002 Projects/Terminal-Scheduler/index.html | Main terminal interface (single-file app) |
005 Operations/Execution/oregon_trail.py | Python backend (reference implementation) |
005 Operations/Execution/ezer_memory.py | Session memory management |
005 Operations/Execution/scheduling_rules.py | Circle-based scheduling rules |
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Ez Terminal v5.0.0 (Browser) │
├─────────────────────────────────────────────────────────────────┤
│ ┌───────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Scheduling│ │ Achievements│ │ Oregon Trail│ │QWU Universe │ │
│ │ Flow │ │ Manager │ │ Game Engine │ │ Adventure │ │
│ └─────┬─────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │ │
│ │ ┌──────────┴───────────────┴───────────────┘ │
│ │ │ ┌─────────────┐ ┌─────────────┐ │
│ │ │ │ TIG Trivia │ │ Guestbook │ │
│ │ │ │ Engine │ │ System │ │
│ │ │ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │ │
│ └───┴──────────┼───────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ localStorage │ │
│ │ (session memory) │ │
│ └──────────┬──────────┘ │
└───────────────────────┼─────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│Identity│ │ Wisdom │ │ Trail │ │Universe│ │ Trivia │
│ State │ │ State │ │ Save │ │ Save │ │ State │
└────────┘ └────────┘ └────────┘ └────────┘ └────────┘
Missing Pixel Training Opportunity
Tier 2 - Contributor Level
The Ez Terminal demonstrates several teachable concepts:
| Skill | What Students Learn |
|---|---|
| Frontend JavaScript | Single-file app architecture, state management |
| localStorage API | Cross-session persistence, JSON serialization |
| Game Development | State machines, game loops, event handling |
| Terminal UI | ASCII art, text animation, command parsing |
| Achievement Systems | Progress tracking, unlock logic, notification UI |
| Text Adventure Design | Location graphs, inventory systems, NPC dialog trees |
| Quiz/Trivia Logic | Category selection, streak tracking, score management |
Portfolio Projects Completed (v5.0.0):
- Text adventure (QWU Universe - 12+ locations, NPCs, items)
- Trivia game (TIG Trivia - 3 categories, streaks)
- Guestbook system (moderated community messages)
Future Extension Ideas:
- Additional QWU Universe locations
- More trivia categories
- Theme creator tool
- Mobile gesture support
Chat Leak Prevention System ⭐ NEW
Status: Phase 1 complete 2026-05-20 (5 of 5 items shipped, 74-test suite passing). 90-day evaluation 2026-08-18.
What it protects: Prevents agent → chat credential leakage in Claude Code sessions. Reduces baseline of 3 documented leaks per 30 days (pre-2026-05-19) to ≤2 leaks per 90 days post-deploy, with zero leaks from covered surfaces.
Authoritative SOP: 005 Operations/Directives/chat_leak_prevention.md ... read this if you need to operate, extend, or troubleshoot the system. Components, edge cases, success criteria all there.
Component summary:
| # | Component | Location | Behavior |
|---|---|---|---|
| 1 | Permission denies | ~/.claude/settings.json | 32 deny rules block .env Read tool + Bash patterns (cat/head/tail/awk/grep/bare env) |
| 2 | safe_select_jsonb.py | 005 Operations/Execution/ | Two-pass safe SELECT against Supabase JSONB tables; hash-gated vetted-allowlist bypass via safe_jsonb_allowlists.yml |
| 3 | safe_env_read.py | 005 Operations/Execution/ | Sealed-destination .env reader; --to PATH (mode 0600 file) or --exec CMD ARGS (subprocess env), never stdout/chat |
| 4 | Chat-scan hook | .claude/hooks/credential_chat_scan.py + 005 Operations/Execution/credential_patterns.py | Alert-only on credential-shape patterns in last assistant message; banner + log at .tmp/security/leak_alerts.jsonl. NEVER blocks. NEVER auto-rotates. |
| 5a | SSH conversion | All QWU local git repos | Eliminates embedded-credential-in-remote-URL vector class permanently (Incident #4, 2026-05-19) |
| 5b | Audit drift checks | audit_system.py v1.6.0+ | check_git_remote_drift() + check_credential_patterns_coverage() run daily 6 AM Pacific |
What an operator sees:
-
Banner in chat containing
CREDENTIAL-SHAPE DETECTED IN CHAT OUTPUT (chat_leak_prevention #4)followed by vendor, description, shape (≤8 chars visible), session id, and next-action instructions ... means the chat-scan hook caught a credential pattern. Treat as compromised; rotate percredential_rotation_playbook.md; write postmortem within 7 days at002 Projects/_Chat Leak Prevention/2026-XX-XX-Incident-N-*.md. -
Permission deniedwhen trying toRead(.env)orBash(cat .env)and variants ... item #1 working as designed. Usesafe_env_read.pyfor legitimate reads, or ask TIG for permission with stronger justification. -
Daily Discord summary mentions
audit_systemgaps for git remote drift or credential-patterns coverage ... investigate via the gap recommendation text. Coverage gaps surface uncovered .env vendors (backlog: 77 at Phase 1 launch); look up the vendor's documented prefix shape from PUBLIC docs, add a pattern + tests tocredential_patterns.py.
Phase 2 forward: Credential vault MCP server documented as architectural target in the brainstorm doc §12 (002 Projects/_Chat Leak Prevention/Chat-Leak-Prevention-Brainstorm.md). Triggered by 90-day evaluation outcome. ~20-30hr scope, deferred until Phase 1 earns its place.
90-day evaluation reminder: paired Alerts-calendar event for 2026-08-18 11:00 AM Pacific fires automatically; kickoff prompt at 002 Projects/_Chat Leak Prevention/Kickoff Prompts/chat-leak-prevention-90-day-evaluation-kickoff.md resumes evaluation cleanly.
MP Training Opportunities ⭐
This Phase 1 build (2026-05-19 → 2026-05-20) produced an unusually rich set of teachable units across security, testing, and systems-design skills.
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
Permission denies in user-level ~/.claude/settings.json (declarative Read/Bash pattern syntax) | Declarative-over-imperative security; scoped enforcement (user vs project); graceful degradation philosophy | Beginner |
Two-pass safe SELECT pattern (safe_select_jsonb.py: --inspect then --allowlist) | Forced inspection discipline; allowlist-over-blocklist thinking; the alt-name-coverage trap (Incident #2 root cause) | Intermediate |
Sealed-destination wrapper (safe_env_read.py: --to mode 0600 or --exec env propagation) | Unix file modes; os.open() with O_CREAT|O_TRUNC; os.execvpe() for subprocess env injection; refuse-overwrite-without-force | Intermediate |
Vendor-prefix regex design with positive + negative tests per pattern (credential_patterns.py) | Regex anchoring + length constraints + character classes; test-gated additions prevent false-positive drift | Intermediate |
Stop / SubagentStop hook design (credential_chat_scan.py) | Claude Code hook contract; JSONL transcript parsing; content-block traversal; graceful failure (always exit 0 even on error) | Advanced |
pytest patterns (monkeypatch, tmp_path, capsys, subprocess testing, importing scripts as modules via sys.path) | Industry-standard testing tooling; the 74-test suite is reusable as a study reference | Beginner-Intermediate |
Hash-gated config for drift detection (safe_jsonb_allowlists.yml schema_hash check) | Cryptographic-fingerprint-based drift detection; "config that catches its own staleness" pattern | Advanced |
| Defense-in-depth observability trade-off (the discipline layer blocked hook verification through behavioral test) | System-level security thinking: two interlocking defenses can each prevent the other from being tested | Advanced |
| Brainstorm → Directive graduation (with §11 Decision Record archived, directive becomes operational SOP) | Knowledge-management lifecycle; the "documented but unbuilt" anti-pattern detection (gap #1) | Intermediate |
Summary-gap-with-detail-file pattern (audit_system.check_credential_patterns_coverage() emits 1 row, writes 77 details to file) | Monitoring noise control; one Discord line per audit period, full forensic detail persisted to disk | Intermediate |
| Postmortem-on-test-alert discipline (deliberate verification alerts still get documented) | Forensic integrity over operational convenience; the leak_alerts.jsonl chain stays clean | Intermediate |
These could anchor a "Security Engineering Fundamentals" curriculum track for MP students. Each pattern has a concrete artifact in the repo (script + tests) that students can study, modify, and re-test.
Troubleshooting
Every session suddenly cannot reach GitHub over https
What you see: git fetch or git push answers could not read Username for 'https://github.com', on most app copies at once. Copies that use ssh, this vault among them, keep working.
How it works since 2026-11-03: git signs in to GitHub through a small helper that reads the one stored token from the settings file each time. It ignores git's request to delete a refused login, so one bad command can no longer sign everyone out, which it did three times before this fix. There is no ~/.git-credentials any more; do not recreate it. Changing the token is one edit, in the settings file.
What to do: check_git_health.py already tells TIG within 5 minutes if the helper is missing, changed or not answering. Run install_github_signin_helper.py --check to see why, and --apply to repair it (always after a VM rebuild). --undo goes back to the old way. If the helper answers and GitHub still refuses, the token itself has expired or been revoked: see the credential rotation playbook. Never print the token (git credential fill, or the helper's get); the secret gate refuses it.
MP Training Opportunities (from this diagnosis, 2026-11-03, Session 885)
| Component | Skills Developed | Difficulty |
|---|---|---|
| Reading a file's birth time and change time to tell a replace from a truncate | Linux file metadata, evidence over guesses | ⭐⭐ |
| Reproducing a fault on a throwaway copy, with a control before and after | Experiment design, protecting live systems while testing | ⭐⭐⭐ |
| Diffing against a baseline the way the baseline was built | Data reconciliation, checking your own instrument | ⭐⭐ |
| Reading what a rewriter deletes | A tool that replaces its own block in a shared file must find that block's end, not the next heading. Run old and new code on every real file and compare, then run the new tests against the old code to prove they can fail. | Intermediate |
Can't Connect to VM
- Check if VM is running - Azure Portal → VM should show "Running"
- Check IP address - It may have changed after restart
- Try status webhook -
curl "https://n8n.quietlyworking.org/webhook/vm-control?action=status"
VS Code Remote SSH: Stuck at "Opening Remote..."
When VS Code hangs at "Opening Remote..." for more than 2-3 minutes, the VS Code server on the remote VM has likely become corrupted or stuck.
Diagnosis Steps:
-
Verify VM is running
- Check Azure Portal or use your mobile VM control widget
- Confirm the VM isn't stopped or deallocated
-
Test direct SSH connection
ssh claude-dev-vm- If this hangs: problem is network or VM itself
- If this connects: problem is VS Code server (continue below)
-
Check VS Code logs
- In VS Code:
View → Output - Select "Remote - SSH" from dropdown
- Look for where the connection stalls
- In VS Code:
Resolution Steps:
Once SSH confirms the VM is accessible:
-
Kill zombie VS Code server processes
pkill -f vscode-server -
Remove corrupted VS Code server cache
rm -rf ~/.vscode-server -
Reconnect from VS Code
- Close VS Code completely on local machine
- Reopen and connect to remote
- VS Code will reinstall server automatically
- Extensions will reinstall on first connect (takes ~1 minute)
Prevention Tips:
- Avoid force-closing VS Code while remote operations are in progress
- If disconnecting for extended periods, use
Remote-SSH: Close Remote Connectioncommand first - Keep VS Code and Remote SSH extension updated
Related Commands:
| Command | Purpose |
|---|---|
pkill -f vscode-server | Kill all VS Code server processes |
rm -rf ~/.vscode-server | Remove VS Code server installation |
cat ~/.ssh/config | View SSH connection aliases |
ssh <host-alias> | Test direct SSH connection |
Permission Denied Errors
# For Docker
sudo docker run ...
# For file access
ls -la <file> # Check permissions
chmod +x <file> # Make executable
Docker Issues
# Check if Docker is running
sudo systemctl status docker
# Restart Docker
sudo systemctl restart docker
# Clean up space
docker system prune -a
Web Server 521 Error (Cloudflare)
Discovered: January 26, 2026
Symptoms:
- Cloudflare shows "521 Web server is down" error
- Browser → Cloudflare: Working
- Cloudflare → Origin: Error
- Sites like
terminal.quietlyworking.orgortwin.quietlyworking.orgunreachable
Root Cause: Port 80/443 conflict between nginx and Caddy. If nginx starts before Caddy (e.g., after reboot), Caddy fails to bind to ports and the reverse proxy doesn't run.
Diagnosis:
# Check what's using port 80
sudo lsof -i :80
# Check Caddy status
systemctl status caddy
# Check nginx status
systemctl status nginx
Solution:
# Stop and disable nginx
sudo systemctl stop nginx
sudo systemctl disable nginx
# Restart Caddy
sudo systemctl restart caddy
# Verify
systemctl status caddy
curl -s -o /dev/null -w "%{http_code}" http://localhost:80
Prevention:
- nginx was disabled on 2026-01-26 to prevent this conflict
- If nginx is needed for something else, configure it to use different ports or remove Caddy
Systemd Service Port Conflict (Address Already in Use)
Discovered: February 25, 2026
Symptoms:
- BetterStack fires "Missed heartbeat" alert for claude-dev VM
systemctl status <service>showsfailed (Result: exit-code)- Journal shows
OSError: [Errno 98] Address already in userepeated 11 times - Health check reports
critical(v1.0.0 withheld heartbeat; fixed in v1.1.0 ... heartbeat now always pings)
Root Cause: A rogue process started outside systemd (e.g., manual run, previous session) holds the port. When systemd tries to restart its managed service, the new process can't bind and fails. After 11 rapid restart attempts (~5s each), systemd gives up.
Diagnosis:
# Check which process holds the port
ss -tlnp | grep <port>
# Check if systemd thinks the service is running
systemctl status <service>
# Check journal for the crash loop
journalctl -u <service> --since "1 hour ago"
Solution:
# Kill the rogue process
kill <pid_from_ss_output>
# Reset systemd's failure state and restart
sudo systemctl reset-failed <service>
sudo systemctl restart <service>
# Verify
systemctl status <service>
Prevention:
digital_twin_server.pynow usesReusableHTTPServerwithSO_REUSEADDR(added Feb 2026), allowing restarts even if the port is in TIME_WAIT state- Avoid running services manually when systemd manages them ... use
systemctl restartinstead - If you must test manually, stop the systemd service first:
sudo systemctl stop <service> <VM_USER>has passwordless sudo forsystemctl(added Mar 2026), allowing the agent to self-heal service failurescheck_vm_health.pyv1.1.0 always pings heartbeat regardless of status, so this scenario no longer causes false "VM down" P1 alarms
Disk Space Warnings
# Check disk usage
df -h
# Find large files
du -sh * | sort -h
# Clean Docker
docker system prune -a
MCP Server Connection Issues
- Check domain is in firewall allowlist
- Rebuild container after adding domains
- Test connectivity:
curl -I https://domain.com
n8n SSH Node Authentication Error
Discovered: January 17, 2026 during LinkedIn workflow debugging
Symptoms:
- Workflow executes on schedule but fails immediately
- Error:
"Node does not have any credentials set for \"sshPassword\"" - SSH credentials (privateKey type) are correctly configured in n8n
- Same credentials work for other workflows
Root Cause:
The n8n SSH node defaults to password authentication. When using SSH private key credentials, you must explicitly set authentication: "privateKey" in the node's parameters. Without this, the node looks for password-type credentials and fails.
Solution: In your workflow JSON, ensure each SSH node has the authentication parameter:
{
"parameters": {
"authentication": "privateKey", // ← REQUIRED for privateKey credentials
"command": "your-command-here"
},
"type": "n8n-nodes-base.ssh",
"credentials": {
"sshPrivateKey": {
"id": "credential-id",
"name": "Credential Name"
}
}
}
Via API Fix:
# Extract workflow, add authentication to SSH nodes, update
cat workflow.json | jq '
{name, connections, settings, nodes: [.nodes[] |
if .type == "n8n-nodes-base.ssh"
then .parameters.authentication = "privateKey"
else . end
]}
' | curl -s -X PUT "$N8N_API_URL/workflows/{id}" \
-H "X-N8N-API-KEY: $N8N_API_KEY" \
-H "Content-Type: application/json" -d @-
Prevention:
Always include "authentication": "privateKey" when creating workflow JSON files that use SSH nodes with private key credentials.
Fleet-Wide Remediation (Feb 13, 2026): A comprehensive audit found this issue (or variants) in 19 of 78 active workflows. Three patterns were discovered:
- Missing
authentication: privateKey... node defaults to password auth, looks for wrong credential type - Wrong credential key (
sshPasswordinstead ofsshPrivateKey) ... works by accident because n8n looks up credentials by ID, not key name - Redundant
sshAuthenticateWith: privateKey... non-standard parameter name, harmless but confusing
All 19 workflows were fixed via nuclear export→delete→reimport→publish (SQL UPDATE doesn't affect published versions in workflow_history). Additionally, 2 workflows using Discord httpRequest v4.1 (form-encoded bodyParameters) were upgraded to v4.2 (JSON body) ... Cloudflare silently blocks form-encoded Discord webhook payloads for embed-heavy messages.
n8n API Webhook Registration Issue (Self-Hosted)
Discovered: January 15, 2026 during n8n migration
Symptoms:
- Workflows created via n8n REST API activate successfully (logs show "Activated workflow")
- Webhook trigger nodes show as configured and enabled
- Calling the production webhook URL returns
{"code":404,"message":"The requested webhook \"xyz\" is not registered"} - Same workflow imported manually via UI works correctly
What We Tried:
- Multiple workflow create/delete cycles via API
- Different webhook paths and configurations
- Restarting n8n container multiple times
- Simplifying workflows to minimal webhook → respond patterns
- Using different webhook node versions (v1, v2)
- Verifying workflow activation via
/workflows/{id}/activateendpoint
Root Cause Analysis: The n8n REST API (currently in beta) appears to have a bug where webhooks created programmatically don't register in the internal webhook routing table, even though the workflow activates successfully. The issue may be related to:
- Webhook registration happening during UI import but not API import
- Missing internal event trigger when activating via API
- Race condition between workflow activation and webhook registration
Status: Unresolved limitation of n8n REST API (beta)
Workaround Implemented: Rather than relying on n8n for webhook reception, we created a standalone webhook server:
Twilio → sms.quietlyworking.org → Caddy (SSL) → twilio_webhook_server.py → sms_approval.py
Components:
| Component | Purpose |
|---|---|
twilio_webhook_server.py | HTTP server on port 8765, receives Twilio webhooks (v3.7.0; SO_REUSEADDR via ReusableHTTPServer since v3.5.1) |
| Caddy reverse proxy | SSL termination, proxies to webhook server |
sms-webhook.service | systemd service for persistence; ExecStartPre kills port squatters via port-cleanup.conf drop-in |
A STOP is recorded in two places (2026-09-06). sms_compliance.py v2.1.0 still writes the
family opt-out to the preference-center SQLite (the store every QWF sender gates on), and now
also calls qqt_consent_mirror.py to write the same boundary into Quietly Quoting's
notification_consent table, so a supporter's Alerts & Reporting grid greys the person who
stopped. The mirror is best-effort and never blocks the acknowledgement: a failure prints a
WARN line and the SQLite record still governs sending. twilio_webhook_server.py v3.7.0 passes
the To number through so the mirrored row records which QWF number was texted.
Should This Be Reported? Yes. This appears to be a legitimate bug in the n8n REST API. The behavior is:
- API reports success for workflow creation and activation
- Logs confirm activation
- But webhook routing doesn't work
Consider reporting to: https://github.com/n8n-io/n8n/issues with reproduction steps:
- Create workflow with webhook trigger via REST API
- Activate workflow via REST API
- Attempt to call webhook URL
- Observe 404 despite active workflow
Learning: For critical webhooks that must work immediately, either:
- Import workflows manually via n8n UI
- Build standalone webhook endpoints (as we did for SMS)
- Use n8n's native integrations where available (Twilio node) instead of custom webhooks
Resources
Key URLs
| Resource | URL |
|---|---|
| Azure Portal | https://portal.azure.com |
| VS Code Remote SSH Docs | https://code.visualstudio.com/docs/remote/ssh |
| Anthropic API Docs | https://docs.anthropic.com |
| n8n Dashboard | https://n8n.quietlyworking.org |
| Digital Twin Dashboard | https://twin.quietlyworking.org |
| Ez Terminal (Scheduler) | https://terminal.quietlyworking.org |
| Google Cloud Console | https://console.cloud.google.com |
| Vista Social | https://vistasocial.com |
Environment Variables Reference
All secrets and configuration are stored in .env at the vault root. Here's the complete reference:
Core Services:
# Anthropic API
ANTHROPIC_API_KEY="sk-ant-..."
# OpenAI (for some AI enrichments)
OPENAI_API_KEY="sk-..."
# OpenRouter (LLM calls via model_config.py)
OPENROUTER_API_KEY="sk-or-v1-..."
OPENROUTER_MGMT_KEY="sk-or-v1-..." # Management Key for Activity API cost cross-validation
Azure (VM Control + Cost Tracking):
AZURE_SUBSCRIPTION_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# No key: VM control and cost reads sign in as claude-dev-vm's built-in identity since 2026-10-13
Google Services:
# Calendar integration
GOOGLE_CALENDAR_CREDENTIALS="/path/to/service-account.json"
GOOGLE_CALENDAR_MAIN="[email protected]"
GOOGLE_CALENDAR_ALERTS="[email protected]"
GOOGLE_CALENDAR_TIMESLOT="[email protected]"
# Sheets (for lead delivery)
GOOGLE_SHEETS_CREDENTIALS="/path/to/service-account.json"
# Google Docs (bidirectional sync with Obsidian)
GOOGLE_DOCS_CREDENTIALS="/path/to/service-account.json"
GOOGLE_DOCS_FOLDER_DEFAULT="drive-folder-id-for-synced-docs"
GOOGLE_DOCS_SYNC_DIRS="002 Projects,Quietly Working Universe Public Transparency Project"
Discord Webhooks:
DISCORD_WEBHOOK_AGENT_LOG="https://discord.com/api/webhooks/..."
DISCORD_WEBHOOK_INBOX_ALERTS="https://discord.com/api/webhooks/..."
DISCORD_WEBHOOK_L4G_LEADS="https://discord.com/api/webhooks/..."
DISCORD_WEBHOOK_SYSTEM_STATUS="https://discord.com/api/webhooks/..."
DISCORD_WEBHOOK_DAILY_DIGEST="https://discord.com/api/webhooks/..."
Twilio Phone Registry:
All QWU Twilio numbers are under the <ADMIN_EMAIL> account with Charity A2P messaging service.
| Number | Friendly Name | Purpose | Voice Config |
|---|---|---|---|
| <TWILIO_NUMBER> | BNI-relay | [Networking Chapter] - visitor reminders, member updates, referral alerts | Webhook |
| <TWILIO_NUMBER> | U2SPEAK | [Networking Chapter] - speaker reminder automations | Webhook to n8n |
| <TWILIO_NUMBER> | ... | QWF's public line (TIG 2026-07-31; L4G joins its phone tree, 2026-11-02) + general backoffice SMS sender + Ezer inbound SMS. Trusted at the carriers since 2026-11-02: Trust Hub business profile, SHAKEN/STIR, Voice Integrity, caller name "QUIETLY WORKING" (twilio_tool_wisdom.md v1.6.1). L4G outbound calls start from it | Webhook (sms.quietlyworking.org/voice/inbound) |
| <TWILIO_NUMBER> | ... | VIP/Personal line → forwards to TIG's cell (<PERSONAL_NUMBER>) | Studio Transfer |
| TBD | EZER | Ezer AI Assistant - TIG command intake | Webhook to n8n |
# Twilio Account
TWILIO_ACCOUNT_SID="your-account-sid"
TWILIO_AUTH_TOKEN="your-auth-token"
# Phone Numbers by Purpose
TWILIO_BNI_NUMBER="<TWILIO_NUMBER>" # [Networking Chapter] automations
TWILIO_U2SPEAK_NUMBER="<TWILIO_NUMBER>" # BNI speaker reminders
TWILIO_L4G_NUMBER="<TWILIO_NUMBER>" # Locals 4 Good SMS
TWILIO_VIP_NUMBER="<TWILIO_NUMBER>" # VIP/Personal line
For detailed number documentation, see: 003 Entities/Tools/Twilio.md
Lead Generation/Enrichment:
# Anymail Finder (email enrichment)
ANYMAIL_API_KEY="your-api-key"
# Reoon (cheaper bulk email validation)
REOON_API_KEY="your-api-key"
# LinkedIn (Sales Navigator scraping)
LINKEDIN_SCRAPER_API_KEY="your-api-key"
# Apollo.io
APOLLO_API_KEY="your-api-key"
Locals 4 Good (L4G) System:
# Supabase (primary backend — migrated from Google Sheets Feb 2026)
L4G_SUPABASE_URL="https://<SUPABASE_PROJECT_ID_L4G>.supabase.co"
L4G_SUPABASE_ANON_KEY="your-anon-key"
L4G_SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
L4G_SUPABASE_DB_PASSWORD="your-db-password"
# Stripe
L4G_STRIPE_WEBHOOK_SECRET="whsec_..."
# Legacy (deprecated — kept for reference, data migrated to Supabase)
L4G_AVAILABILITY_SHEET_ID="sheet-id"
L4G_CHECKOUT_API="https://script.google.com/macros/s/.../exec"
L4G_AREA_CONFIG_API="https://script.google.com/macros/s/.../exec"
L4G_POSTCARD_API="https://script.google.com/macros/s/.../exec"
# SMS (Twilio via n8n)
L4G_TWILIO_FROM_NUMBER="<TWILIO_NUMBER>"
Social Media:
# Vista Social MCP
VISTA_SOCIAL_API_KEY="your-api-key"
Canonical Datetime (Timezone Handling):
# Timezone for all QWU operations (IANA format)
QWU_TIMEZONE="America/Los_Angeles"
# Day boundary hour (work before this counts as "yesterday")
QWU_DAY_BOUNDARY_HOUR=4
Vault Configuration:
VAULT_PATH="/home/<VM_USER>/qwu_backOffice"
Tips & Gotchas
-
SSH Key is critical - Cannot be re-downloaded. Back it up immediately.
-
VM IP can change - If VM is stopped and started, IP might change. Check Azure Portal and update SSH config if connection fails.
-
VM runs 24/7 - As of Jan 2026, auto-shutdown is disabled for continuous automation. Weekly restart Sunday 3 AM.
-
Budget ~$60-85/month - Always-on cost is higher but enables autonomous operations.
-
Disk can only grow - You can increase disk size later, but cannot shrink it. Start small.
-
B2ms sweet spot - 8GB RAM is comfortable for Docker + VS Code + agents. B2s (4GB) feels cramped.
-
Standard SSD is fine - No need for Premium SSD for development work.
-
tmux is essential - Any serious agent work needs persistent sessions.
Notes for Future Sessions
Add notes here as we continue building...
- Consider setting up static IP (VM IP can change on restart)
- Create custom Docker images for frequently-used agent setups
- Set up Docker Compose for multi-container workflows
- Implement transcript extraction system
Build inbox processing automation✅ Completed (n8n workflow)Complete Discord server setup✅ Completed (Session 9)- Add Timeslot Planning calendar integration for goal-based scheduling
- Build appointment scheduling system with CRM integration (in planning)
Create Goals & Priorities document for summary alignment✅ Completed (M7 milestone)- Set up n8n workflow for scheduled morning briefings
- Implement Vista Social content calendar integration
BNI Member Dossier System
A comprehensive member enrichment and visitor connection system for BNI (Business Network International) chapter management. Designed for the [Networking Chapter] chapter in Orange County, CA.
Purpose
When visitors register for a BNI meeting, generate personalized Connection Reports for each chapter member containing:
- Icebreakers based on shared interests
- Commonalities between member and visitor
- Customized value propositions
- Email templates for follow-up
Goal: Create deeper connections faster, maximize referrals, increase membership, decrease attrition.
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ DATA SOURCES │
├─────────────────────────────────────────────────────────────────┤
│ Airtable CSV │ LinkedIn │ Website │ Google/Yelp │
│ (base member │ (profiles │ (services, │ (reviews, │
│ data) │ + posts) │ about) │ ratings) │
└────────┬─────────┴──────┬──────┴───────┬──────┴────────┬────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ ENRICHMENT PIPELINE │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ import_bni │ │ enrich_ │ │ enrich_ │ │
│ │ _members.py │→ │ linkedin.py │→ │ website.py │→ ... │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ enrich_member_orchestrator.py │ │
│ │ - Runs full pipeline: LinkedIn → Website → Reviews → AI │ │
│ │ - Generates final synthesis with ICP, power teams, scripts │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ENTITY FILES: 003 Entities/People/ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ [Member Name].md │ │
│ │ - YAML frontmatter (tags, contact, enrichment status) │ │
│ │ - Professional Summary │ │
│ │ - Services & Offerings │ │
│ │ - Ideal Customer Profile (AI-synthesized) │ │
│ │ - Power Team Connections │ │
│ │ - Personal Interests & Background │ │
│ │ - Customer Sentiment (from reviews) │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CONNECTION REPORTS │
│ Visitor × Member matching with icebreakers, value props, etc. │
└─────────────────────────────────────────────────────────────────┘
Project Files
| Location | Purpose |
|---|---|
002 Projects/_[Networking Chapter] Projects/_[Networking Chapter] SOP/[Networking Chapter] - Standard Operating Procedures.md | Comprehensive chapter SOP (all systems) |
002 Projects/_[Networking Chapter] Projects/Member Dossier System/_Overview.md | Project documentation |
002 Projects/.../Templates/Template - BNI Member Dossier.md | Member entity template |
002 Projects/.../Templates/Template - Visitor Connection Report.md | Visitor×Member report template |
003 Entities/People/*.md | Individual member entities (23 members) |
Enrichment Scripts
| Script | Purpose |
|---|---|
import_bni_members.py | Import Airtable CSV into entity files |
enrich_member_linkedin.py | RETIRED 2026-08-12 ... no identity gate. Use enrich_member_anchors.py |
enrich_member_anchors.py | Member Identity Anchors: harvest our own records, discover only the gaps, wait for a human |
enrich_member_website.py | Website content extraction with AI |
enrich_member_reviews.py | Google + Yelp reviews with referral intelligence |
enrich_member_orchestrator.py | Full pipeline runner with AI synthesis |
gdrive_oauth.py | Google Drive OAuth2 for historical data access |
generate_connection_report.py | Generate visitor×member connection reports (v2.1.1 - parallel execution) |
bni_visitor_pipeline.py | End-to-end visitor pipeline with auto-send + housekeeping (v3.1.0) |
validate_bni_email.py | Quality validation for connection report emails (v1.0.0) |
sync_bni_roster.py | Roster sync: scrapes BNI website, diffs entity tags (v1.0.0) |
Roster Sync ... Two Sources of Truth
The roster sync system enforces a critical data governance principle:
| Source | Authority |
|---|---|
| BNI chapter website (socalbni.com) | WHO is an active member |
Entity files (003 Entities/People/) | Member DATA (email, phone, enrichment, notes) |
The stale CSV export (<chapter members CSV export>) must NEVER be used for roster lookups.
Sync script: sync_bni_roster.py scrapes the BNI website (direct HTTP, no Apify), compares against entity file tags, and applies changes:
- New members → creates entity file with
BNI-Activetag,enrichment_status: pending - Departed members → changes tag from
BNI-ActivetoBNI-Former - Changed fields → updates
company_nameorbni_categoryin frontmatter - Safety: aborts if < 5 members returned (prevents wipe on broken scrape)
# Preview changes (always do this first)
python sync_bni_roster.py --dry-run --force
# Apply changes
python sync_bni_roster.py --force
# JSON output (for n8n automation)
python sync_bni_roster.py --dry-run --json
QNT Supabase sync: qnt_roster_sync.py runs daily via n8n, keeping the Quietly Networking app's member database in sync with the same BNI website source. Confirmed active and operational (17 members, 0 errors as of March 2026).
Directive: 005 Operations/Directives/sync_bni_roster.md
Usage
Import members from Airtable (legacy ... use sync_bni_roster.py for roster updates):
python import_bni_members.py "Data Imports/aim_high_members_2024.csv"
Enrich single member:
python enrich_member_orchestrator.py "[Member Name]"
Enrich all BNI members:
python enrich_member_orchestrator.py --all
Specific sources only:
python enrich_member_orchestrator.py "[Member Name]" --sources linkedin,website
Dry run (preview without changes):
python enrich_member_orchestrator.py "[Member Name]" --dry-run
Referral Intelligence
A unique feature of the review enrichment: analyzes 1-3★ reviews to identify improvement areas and potential referral opportunities.
For example, if a landscaper's reviews mention "wish they did hardscaping," the system suggests referring a hardscape specialist TO that member.
# Output from enrich_member_reviews.py
{
"referral_opportunities": [
{
"improvement_area": "Response time",
"potential_referral": "Virtual assistant or scheduling service",
"reasoning": "Multiple reviews mention delayed responses"
}
]
}
Entity Schema
Member entities include these YAML frontmatter fields:
# === IDENTITY ===
tags: [[Networking Chapter], BNI-Active]
aliases: [Derek]
type: human
# === CONTACT INFO ===
email: [email protected]
phone_mobile: "(555) 123-4567"
121_link: https://calendly.com/member # BNI 1-2-1 scheduling
# === BUSINESS INFO ===
company_name: "Company Name LLC"
company_website: https://company.com
bni_category: "Business Category"
# === ENRICHMENT STATUS ===
enrichment_status: pending|partial|complete
last_enriched: 2026-01-11
enrichment_sources: [airtable, linkedin, website, reviews]
# === REVIEW DATA ===
google_rating: 4.8
google_review_count: 47
yelp_rating: 4.5
yelp_review_count: 23
Google Drive Integration
OAuth2 authentication provides full read access to Google Drive for accessing historical BNI data (visitor logs, meeting notes, etc.):
# Initial setup (one-time)
python gdrive_oauth.py --authorize
# Search for files
python gdrive_oauth.py --search "BNI dossier"
# List folder contents
python gdrive_oauth.py --list-folder "folder-id"
OAuth Credentials: .credentials/google-oauth-desktop.json
Token Storage: .credentials/google-oauth-token.json
Environment Variables
# Apify (for web scraping)
APIFY_API_TOKEN="your-apify-token"
# Anthropic (for AI synthesis)
ANTHROPIC_API_KEY="your-api-key"
# Google OAuth (for Drive access)
GOOGLE_OAUTH_CREDENTIALS_PATH=".credentials/google-oauth-desktop.json"
GOOGLE_OAUTH_TOKEN_PATH=".credentials/google-oauth-token.json"
Terminology
| Term | Meaning |
|---|---|
| 121 (One-to-One) | Private meeting between two BNI members |
| 121_link | Scheduling URL for booking 1-2-1 meetings |
| Power Team | Complementary business categories that refer well |
| ICP | Ideal Customer Profile |
| Referral Intelligence | Insights from reviews about referral opportunities |
Advanced Features (Phases 5-7) ⭐ NEW
The Epic Dossier System extends basic enrichment with three advanced intelligence layers:
Phase 5: Cross-Platform (Instagram Integration)
Extracts Instagram handles from cached website data and enriches member profiles:
# Extract Instagram from already-scraped websites (no API calls)
python extract_instagram_from_websites.py
# Full enrichment including Instagram (if handle exists in frontmatter)
python enrich_member_orchestrator.py "Member Name"
Scripts:
| Script | Purpose |
|---|---|
extract_instagram_from_websites.py | Mines Instagram handles from JSON-LD sameAs arrays in cached website data |
enrich_member_instagram.py | Apify-based Instagram profile and posts scraping |
How it works: When websites include structured data (JSON-LD), they often list social profiles in sameAs arrays. This script mines that data without additional API calls.
Phase 6: Network Intelligence
Generates "who should meet whom" recommendations using 8 matching algorithms:
# Generate chapter-wide connection recommendations
python network_intelligence.py --chapter [Networking Chapter] --write
Matching Algorithms:
- Power Team Matching - Complementary BNI categories (e.g., roofer ↔ real estate)
- Referral Reciprocity - Bidirectional referral potential
- Style Compatibility - Communication style matching (connector ↔ thought_leader)
- Industry Overlap - Shared industry experience
- Values Overlap - Mission alignment and shared values
- Mission Alignment - Charitable/community focus matching
- Entrepreneur Affinity - Fellow business owner connection
- Volunteer Affinity - Shared community involvement
Output: Writes "Recommended Connections" section to each member's entity file with Obsidian wiki links:
## Recommended Connections
Based on network intelligence analysis:
1. **[[[Member Name]]]** (Score: 60)
- Reasons: Power team match, Values overlap, Mission alignment
Phase 7: Predictive Intelligence
Forward-looking insights using FLAGSHIP LLM analysis:
# Generate predictive insights for members with sufficient data
python predictive_intelligence.py --write
Analysis Types:
| Insight | Description |
|---|---|
| Trajectory Direction | expanding / deepening / pivoting / legacy_building / maintaining |
| Career Stage | early / mid / established / senior / legacy |
| Growth Areas | 3-5 areas the member appears to be developing |
| Hopes We See | Aspirational reading of their public persona |
Output: Writes "Predictive Insights" section to entity files:
## Predictive Insights
**Trajectory:** Expanding (established stage)
**Growth Areas:** Digital marketing, strategic partnerships, community leadership
**Hopes We See:** Scaling impact while maintaining service quality
Updated Enrichment Pipeline (v1.4.0)
The orchestrator now runs 7 steps:
- LinkedIn profile and posts
- Website content extraction
- Google/Yelp reviews
- AI synthesis
- Meeting intelligence (from transcripts)
- Instagram (if handle in frontmatter)
- Network intelligence (chapter-wide)
# Full pipeline including all phases
python enrich_member_orchestrator.py --all
Visitor Enrichment (Symmetric Intelligence) ⭐ NEW
Enriches BNI visitors with the same depth of intelligence as chapter members, enabling symmetric connection matching.
Problem Solved: Members have rich Epic Dossier profiles, but visitors only had basic registration data. This created asymmetric matching where AI couldn't generate quality icebreakers from limited visitor information.
Solution: enrich_visitor.py runs the same enrichment pipeline on visitors:
# Enrich a specific visitor
python enrich_visitor.py "Michael Dory"
# Enrich all pending visitors
python enrich_visitor.py --pending
# Dry run (no API calls)
python enrich_visitor.py "Michael Dory" --dry-run
Pipeline (4 Steps):
- LinkedIn Lookup - Find person by name + company using Apify person search
- Website Extraction - Scrape visitor's company website for services, about, team info
- Reviews Lookup - Google Places reviews with sentiment analysis
- AI Synthesis - Generate profile with same fields as member dossiers
Output Fields:
| Field | Description |
|---|---|
professional_summary | Role, company, expertise |
services_offered | What their business provides |
ideal_customer_profile | Who they serve |
communication_style | How they likely prefer to engage |
personality_traits | Observable traits from public presence |
values_indicators | What they appear to care about |
life_hints | Personal interests, background clues |
power_teams | BNI categories that could refer to them |
connection_hooks | Specific icebreaker topics |
Storage: .tmp/bni_visitors/{meeting_date}_{visitor_name_slug}.json
Integration with Connection Reports:
generate_connection_report.py v2.1.0 automatically loads enriched visitor data when generating reports (with parallel execution for 5x speedup):
# Connection report now receives enriched visitor data
enriched_visitor = find_enriched_visitor(visitor_name)
report = generate_connection_report_ai(visitor_data, member_data, enriched_visitor)
This enables:
- Deeper icebreakers based on visitor's values and personality
- Values alignment section comparing member and visitor missions
- Power team matching from visitor's ideal customer profile
- Bidirectional referral opportunities
Entity Protection (v3.3.0):
enrich_visitor.py includes a BNI-Active write guard to prevent visitor enrichment from overwriting active member entity files. This was added after an April 9, 2026 incident where the phone/email dedup scanner matched a visitor to a member and overwrote the member's entire entity file.
Two layers of protection:
- Dedup scan skip: The phone/email matching loop skips any entity with a
BNI-Activetag - Hard block before write: Even if dedup isn't triggered, the script refuses to write visitor data to any file with
BNI-Activein its frontmatter tags
Environment Variables:
APIFY_API_TOKEN="your-apify-token" # LinkedIn person search
OPENROUTER_API_KEY="your-key" # AI synthesis (FLAGSHIP tier)
MP Student Training Opportunity (Tier 2: Contributor)
Students can learn data enrichment pipeline patterns by:
- Running visitor enrichment on test data (dry-run mode)
- Understanding the 4-step pipeline architecture
- Analyzing how AI synthesis creates structured profiles from unstructured data
- Comparing member vs visitor enrichment approaches
Learning Objectives:
- API integration with Apify (web scraping as a service)
- LLM prompt engineering for data synthesis
- JSON data transformation and storage patterns
- Pipeline design with graceful degradation (partial enrichment when sources fail)
Full Visitor Pipeline (v5.4.0)
bni_visitor_pipeline.py orchestrates visitor processing from registration through in-app delivery.
⚠ Two structural changes since the v3.2.0 description this section used to carry (corrected 2026-09-09, both had been live for weeks while the manual still described the old shape):
- IN-APP ONLY since v5.2.0. The pipeline no longer emails connection reports to members. It
writes them to Quietly Networking, where members read them. The old Stage 3 (MS Graph
/sendMail) and Stage 4 (SMS "emails sent") no longer exist. Members receive one weekly email: the Weekly Connection Report, sent by a different script (below). - Nothing is enriched or written until a human confirms identity. The Identity Anchor gate
(2026-08-11) holds every visitor on three surfaces ... LinkedIn profile, company website, Google
Business listing ... and no depth is bought and no report is generated until a person answers all
three at
quietlynetworking.org/visitors/confirm(or viareview_visitor_holds.py).
Stages as they actually run:
| Stage | Name | What It Does |
|---|---|---|
| 1 | Discovery | Finds CANDIDATES for each of the three identity anchors. Selects nothing, spends nothing. |
| 2 | HOLD + alert | Parks the visitor and tells the responsible human once (TODAY, or INTERRUPT within 24h of the meeting). A past meeting never alerts; the same tier is not repeated within 20 hours. |
| 3 | Human answers | A person confirms, declares "none exists", or declares a chain, per surface. On LinkedIn, "Only their company has one" (2026-10-07) records "no personal profile" AND the company's LinkedIn page beside it (company_page), never as the person's; a company page typed in as the person is refused. Their answer is a LOCK, never re-litigated, and a person a human declared absent is never searched for again. |
| 4 | Depth | Only now: LinkedIn profile + posts, website content, Google reviews, AI synthesis. |
| 5 | Fan-out gate | Blocks while ANY anchor is unanswered (v5.4.0 ... it previously watched only LinkedIn). |
| 6 | Connection Reports | One per active member, written to Quietly Networking. No email. |
| 7 | Housekeeping | Marks the registration email complete and files it. |
The weekly member email is a separate script. send_weekly_connection_reports.py binds every
READY visitor into one PDF per member and sends it from TIG's mailbox at noon Pacific the day before
the meeting, to every active member since TIG's "bless" on 2026-10-29 (v1.7.1, EVERY_MEMBER = True;
the rule is the room list's, shared in chapter_member_recipients.py): QCM "no answer on file" is a yes,
an explicit stop or a consent check that cannot answer holds the member, the register's WCR: STOPPED
table takes a member out (failing closed), and a member with no report that week is held and named by
SMS. Every body must carry TIG's way out ("just say so and I will take you off the list") or it is not
sent. Before 2026-10-29 only the register's opt-ins received it. A half-hourly sweep sends an addendum for anyone who becomes
ready after that, and carries the report itself late if noon passed without one. Auto-send was released
by TIG on 2026-09-08 for those two kinds of mail only; first auto-send delivered 2026-09-09.
The applicant email is a third script (2026-09-11). When the Membership Committee asks the chapter
for written feedback on a NEW applicant, bni_membership_request_forward.py (cron every 15 minutes)
reads the request from TIG's inbox, decides the kind (a renewal or category change is an existing
member and gets nothing), matches the applicant to ONE complete QNT visitor row (zero or many holds
for --confirm / --park), binds each opted-in member's EXISTING Visitor Connection Report into a
one-visitor PDF at $0, and SENDS it from TIG's mailbox with the committee's own feedback-form link.
Same recipient rule as the weekly report, imported (every active member since 2026-10-29; a member with
no page about the applicant is held and named). TIG released auto-send for it on
2026-09-11 with the rule that now governs every sender: a blessed template sends itself; only
agent-composed copy waits for his eyes. It also writes visitors.status='applied' and the
application category (never touching the visit-time profession_category). First live send: 10
members, 2026-09-11.
Since v2.4.0 (2026-10-30) it reads every email from the committee's address, whatever the subject says. On 2026-10-20 the committee reworded their subject to "Applicant Feedback Request for
Since v2.5.0 (2026-11-04) a form link goes to members only after we have opened it. A member wrote to say the feedback link for one applicant opened another applicant's form, and she was right: on 2026-11-02 every member got the wrong form in two emails. The committee had built its email from an older one, so the link's words showed the old address and a click went to the right one, and we copied the words. Now every address the email offers is opened, and the one whose own Name line names the applicant is used, through one chokepoint (member_form_check) shared by the sweep, the Send button and the "updated page" email. A form naming someone else or nobody never goes: the member gets their report with the reply wording, and TIG is told. Each send is written down as it happens, as sent, certainly not sent, or unknown; an unknown send is never retried by a machine, and a killed run resumes. A one-time fix for a wrong link already sent: --set-form, then --preview-correction, then --correct-form, whose words TIG blesses per request. First real run 2026-11-04: the right form to 18 members ([Networking Chapter]-System-Status K55).
The meeting-morning room list is a fourth script (2026-10-28). send_meeting_room_list.py (v1.2.0)
sends ONE short email to every active member at 6:00 Pacific on the meeting day, from TIG's mailbox
in his meeting-email design, with one line for every person registered for that meeting (visitors,
substitutes, guests): their own registration words, plus only the LinkedIn, website, Google stars and
round photos a person on our side confirmed. Never AI text, and never a phone number or email address taken
from the words people typed. Since TIG's "bless" of 2026-10-30 (v1.6.0, a member's idea) every
non-member on it, subs too, also shows their own phone and email from their own boxes, tappable: only one
clean value per box, never an active member's own details (nobody's if the members cannot be read), never a
detail typed for two people, never in a log; and an open seat leads with a green "Seat open" pill.
Who: every active member on the roster the BNI page keeps (never the QNT test login). TIG, the same
night: "until we receive a STOP we send to all active members". QCM "no answer on file" is a yes; an
explicit stop or a consent check that cannot answer holds the member; a reply asking to stop becomes a
row in the register's ROOM LIST: STOPPED table, read on every run and failing closed. When: cron
every half hour; the script owns the day, time and zone; a failed copy retries every half hour until
11:00, and a lasting failure is told twice (when it starts and when it ends), never once per run.
Words: tied to TIG's "bless" by a fingerprint of the template and every list word, so a changed word
stops the send and tells him. Controls: --test-to-tig sends one [TEST] copy to TIG only;
wcr_pause.py holds a week (it holds the WCR too); remove the cron lines to stop for good. A member
added after 06:20 the day before a meeting misses that morning's list unless added by hand (the 06:20
membership reconciler fills a new member's email). First live send: 2026-10-29 06:00, 19 members.
Since v1.4.0 (2026-10-29) it leaves off anyone [Member Name]'s visitor sheet marks cancelled or moved, named
in TIG's record of the run. Since v1.4.1 (2026-10-29) no copy is BCC'd to TIG: "I will receive my
own which will be my signal that they have gone out correctly." Done (TIG "bless", 2026-10-29): the
weekly report, its addendum and the applicant email now follow the same every-member rule, above.
MP Training Opportunities (from the 2026-10-28 room list)
| Task | Skills Developed |
|---|---|
| Proving a test suite catches each safeguard: break one safeguard on a COPY, run the tests, expect a failure (a break nobody catches is a missing test) | Python, pytest, mutation testing, careful reading (Intermediate, ~2 hrs) |
| Reading consent like a professional: "no answer", "said no" and "could not check" are three different answers, and only the first may become a yes | Data protection, fail-safe design, reading a library's options (Intermediate, ~1 hr) |
| Server clocks vs local time: why the server already says Thursday at 5 pm Pacific, and how a scheduled job proves it uses the right day | Linux cron, time zones, log reading (Beginner, ~1 hr) |
Then the committee changed their template, and the reader went quiet (2026-10-03). Their mail is
prose we do not control. On 2026-10-03 a live application arrived as a labelled PROSPECTIVE MEMBER
block with a Google Form link instead of the one variable sentence the reader was built for. It
parsed to nothing, logged skipped (not a request) twelve times over three hours, and raised no
alarm ... the early return for "no requests" sat above the SMS composer, so an alert was not merely
forgotten, it was unreachable. Nobody knew until TIG forwarded a screenshot of a member's copy and
asked whether the automation had fired. Two earlier drops (2026-09-23) had the same symptom from a
different wording gap, but both were renewals where nothing was owed, so the first miss taught us
nothing. v2.2.0 reads both new shapes, and an email from the committee's own address that we cannot
parse now texts TIG once ... a reader of someone else's prose is allowed to miss, never to be quiet.
The labelled template also carries the applicant's own email address, so matching now has an exact
rail instead of guessing on a name.
A second, quieter fault surfaced underneath it. The applicant had no report to send in any case: he had been waiting since his visit on one unanswered identity anchor, and the confirm screen listed only meetings from today forward, so he dropped off it the next morning. The day-before reminder skips a past meeting and the stale sweep is not scheduled ... three surfaces blind at once, with nothing left to surface him again. The confirm screen now keeps an UNRESOLVED hold visible for 120 days past its meeting. Un-hiding it immediately revealed four more people stranded the same way, two of them for six weeks. An unanswered question is a debt, and a debt outlives its due date.
The Membership Committee (MC) page in Quietly Networking (built 2026-09-11, under a testing lock).
Every request the script detects is mirrored into QNT (membership_requests, membership_request_sends,
membership_request_responses, migration 028) and managed on /membership: the queue, a HOLD settled
in-app (this visitor / park ... the next sweep acts on it exactly as the CLI flags do), a Send button
that composes the sender's own functions (Preview first; consent resolves inside the imported resolver;
a member who does not pass is refused by name), every member's answer WITH their name, and Close with
the committee's decision. Members answer on /membership/answer/<id> with five buttons (Yes / No /
Yes with a note / No with a note / I don't know them well enough); a bare Yes or No submits at once
and "Can't answer right now" stores nothing. The split is enforced by the database, not the screen:
the committee (organizations.settings.membership_committee, D-003) reads names; every other member
reads only their own row and, once the request is closed, aggregate counts. probe_qnt_membership_rls.py
proves it as synthetic users inside a rolled-back transaction. Testing lock (TIG's rule for any
member-facing system): while settings.membership_committee.testing is set, every send from the page
is rendered for its real member and delivered to TIG's inbox only; the unlock is his words after the
committee and leadership have seen it. Committee named from the 2026-2027 Chapter Roles sheet
(2026-09-12). Roadmap: hq.quietlyworking.org/roadmap/aim-high-membership-committee (public export on
the portal). Detail: the project's System Status file under 002 Projects/.
How a page earns its place in the weekly report. Every member-by-visitor page is checked before
it can be bound (wcr_acceptance_gate.py). A deterministic half refuses an em dash, a URL or star
rating the record does not carry, "recommendations" on a profile with none, a missing honesty marker
and a page outside the length band; a judged half (FLAGSHIP, thinking on) asks four questions: is it
about THIS member, could they open a conversation from it, does it invent anything, does it sound like
a person. A page that fails is written once more; if it fails again it is HELD, named on the confirm
screen and in TIG's text, and never sent. Pages are written and judged the moment a visitor becomes
ready, and the noon send only binds what already passed (wcr_page_verdicts.py).
What changed on 2026-10-28 (after the 10-29 send held 23 of 144 pages):
| Change | What it does | Why |
|---|---|---|
| Job dates reach the writer and the judge (K38) | Each LinkedIn job is told with its dates (Aug 2015 to May 2022, to now (current role)); a blank company is filled from the verification step's copy of the SAME profile only when exactly one role matches on start and end year; an About cut at 600 characters says WE cut it | The facts block printed a duration field that has been blank since August, so a TRUE "years in event sales" could not be checked and was held. About 13 of the 23 holds had this one cause |
| The writer reads the caller's record | generate_connection_report.main(record=...): the page is written from the same record the judge reads | Looked up by name, a repeat visitor's other week could stand in (46 of 213 records resolved to another week) |
| One answer covers every registration of one person | An answer on one registration is copied whole to the same person's other UPCOMING registrations: same registration email AND a name that agrees, never the QNT visitor id (six records hold someone else's), only into unanswered anchors, never over an answer, never a past meeting, a guest or a run in flight; a copy follows its source when the source is corrected | A person registered for two meetings needed every anchor answered twice (one visitor was booked for Nov 5 and Nov 12) |
| One run per person at a time | Enforced where runs start (the Run button included) and in --pending; when a run ends the next one starts | Two runs of one person at once would write the same pages side by side |
| One folder per registration | Pages land in <meeting YYYYMMDD>_<name>; a person's two registrations no longer share a folder | The second registration's pages overwrote the first's |
| A changed answer retires the old pages (K42) | Changing an anchor on a visitor whose pages exist sets the record back to partial (reports_out_of_date), so no send binds the old pages, and the visitor re-runs by itself | The noon send would have bound pages written from the earlier answer. A correction AFTER the report was sent cannot recall it |
| The writer is told what the judge holds pages for (K43, shipped that evening on TIG's "go") | Seven rules go into the writer's prompt: no detail the facts do not give, no guess dressed as fact, time in a role only from the printed length, nobody's words unless a line quotes them, no reviews cited back at the member, no joke at the visitor's expense (the Rapport tip is plain advice), nothing generic. The facts block both read gains each job's length, the LinkedIn location, the visitor's own posts (never a post they only shared), the Google listing answer in words, our research notes labelled as ours, and "no LinkedIn" as our own reviewer's finding. "What you already share" points to Ways in when Ways in found common ground | Most first drafts failed for causes born in what the writer was told, not in the writer. On the 23 held pages: first try 8 before, 20 after, all 23 after one rewrite; 13 other pages passed 13 of 13 on the first try. The first live visitor written under it, sent the same evening in a late-visitor addendum: 17 of 18 pages on the first try, 18 of 18 after one rewrite. The judge did not change |
| A test can no longer start the real pipeline (K44) | The repo's test guard (conftest.py) refuses a detached process (systemd-run, setsid) inside any test; the hold-answer tests use a fake run starter | Answering a hold starts the pipeline as a detached process, which runs outside every guard a test has. One test ran the real pipeline eight times on 10-28: paid calls, member pages and a vault page built from test data, and a real hold note pointed at a temp file. Nothing failed, so nothing showed |
Missing Pixel Training Opportunity (Tier 2: Contributor) ... Read what the judge was told before you blame the writer
On 2026-10-28 the acceptance judge held 23 pages, 13 of them for "the facts give no dates". The tempting fix was to tell the writer to stop guessing. The right first move was to print the exact facts block the judge had read: every job showed empty brackets where its dates should be, while the record held the dates all along. The judge was right about the facts it was given; the facts were wrong.
Component Skills Taught Print the judge's input before reading its verdict Debugging an AI system by its inputs; "garbage in" made concrete Count failure causes from real verdicts before changing a prompt Turning a pile of reasons into a ranked cause table Mutation checking a safety rule: break each rule on purpose and prove a test fails Why a passing test proves nothing until you have seen it fail; 22 rules checked this way that day Prerequisites: basic Python, reading JSON. Time: ~3 hours. Complexity: Intermediate.
What changed on 2026-11-02 (Google listings, applicant replies, a guest who applied):
| Change | What it does | Why |
|---|---|---|
| A pasted Google Maps link is read into its listing (K52) | On the confirm screen, the CLI (--set 3 <link>) and a member's own page, a link is read into the ONE listing it opens (visitor_discovery.place_from_link: a short link is followed one hop at a time and only between Google's own hosts; Google Maps reads the link itself through the places actor, about $0.0002) and stored by its place ID with its name. A link that opens no single listing is refused in TIG's words: "That Google link doesn't point to one listing. Pick it from the list, or paste its place ID." | A pasted link was stored as the place ID, the reviews read failed without a word, and 11 members read "zero Google reviews" about a listing with eleven at 5.0 |
| A failed reviews read is a failure, never zero (K52) | The reviews reader refuses anything that is not a place ID, checks that the run ended SUCCEEDED, and writes read_failed instead of "found, 0 reviews"; a failed read keeps only the same listing's earlier reviews | The actor's call() returns a FAILED run without raising |
| A listing titled with the person's whole name is their own reviews | listing_names_person in review_facts | A rep's own listing scored below the employer threshold and every member's page would have said her reviews belong to someone else |
| A reply that names a listing without a link waits for TIG, not the one rebuild (K53) | The applicant follow-up asks whether the reply speaks of a Google listing; it searches their name once and records exactly one in-region listing carrying their whole name, or holds with what it found; the one rebuild waits; TIG's answer on the confirm screen starts it | An applicant wrote "if you google my name, the 4 reviews I have will be there" |
| A guest who applies gets a page by hand (K54, open) | Her latest guest record is turned into a full visitor, discovered, answered by TIG, built once, read, and only then released to the forwarder | Guests never get a page or a QNT visitor; an applicant had visited twice as a member's guest |
Missing Pixel Training Opportunity (Tier 2: Contributor) ... Read the link, not its name
A pasted Google Maps link names a business, and the tempting fix is to search that name. Two offices can share a name, and a name search needs a region rule that a real listing in the next county fails. The link itself opens exactly one place, so hand the link to the tool that opens it. Exercise: take three short Maps links, follow each one hop at a time, write down every host it passes through, and explain why the reader refuses to follow a hop that leaves Google. Skills: HTTP redirects, input validation, safe link handling. Prerequisites: Python requests. About 2 hours. Intermediate.
Missing Pixel Training Opportunity (Tier 2: Contributor) ... Prove "nothing live ran" from the record that cannot be skipped
On 2026-10-28 a unit test answered a visitor hold. Answering a hold starts the visitor pipeline as a separate background program, and a separate program brings none of the test's fakes or guards with it: it ran the REAL pipeline eight times that day, on test data. The first check ("no run log after noon") passed, because a run started by a test writes its log into the test's own temp folder. The spend ledger, which records every AI call no matter who started it, showed all eight.
Component Skills Taught Why a background program escapes a test's fakes Processes versus in-process patches; what a test's monkeypatchcan and cannot reachRefusing a dangerous call by name in the shared test guard pytest conftest.py; wrappingsubprocess.Popen; tests that prove the guard bites AND that ordinary subprocesses still runProving a negative from the system of record Choosing the log nothing can bypass; counting events in a time window from JSON lines Repairing what the leak touched, evidence kept Quarantine before delete; rebuild a record from its source; read it back through the real screen Prerequisites: basic Python, pytest basics. Time: ~3 hours. Complexity: Intermediate.
Missing Pixel Training Opportunity (Tier 2: Contributor) ... Identity disambiguation
On 2026-09-09 two LinkedIn profiles carried the same name AND the same employer. One was blank (no photo, 0 connections, one role); one was lived-in (photo, 143 connections, posts, 8 roles). An automated search found only the blank one and scored it a perfect name match. This is the everyday shape of a data-quality problem, and it is a marketable skill.
Component Skills Taught Why one query returned zero and a shorter one returned the person How keyword search ANDs terms; why a form's abbreviation ("Cratus Tech" vs "Cratus Technology") is a silent killer Running several query variants and merging by unique id De-duplication, cost-aware search (stop when you have enough) Scoring "substance" (photo, connections, About, roles) Turning a judgment call into named, inspectable evidence Deciding what the score may NOT decide Why substance breaks ties but never outranks a name match ... a blank profile can still be the right person Never auto-confirming when two candidates tie Designing for a human decision instead of a confident guess Prerequisites: basic Python, REST/JSON. Time: ~4 hours. Complexity: Intermediate.
Usage:
# Process specific visitor
python bni_visitor_pipeline.py "[Member Name]"
# Process latest pending visitor
python bni_visitor_pipeline.py --latest
# Dry run (no API calls, no emails)
python bni_visitor_pipeline.py "[Member Name]" --dry-run
# Test mode: redirect ALL emails to yourself (safe testing)
python bni_visitor_pipeline.py "[Member Name]" --test-recipient=<ADMIN_EMAIL>
v3.4.0 Change (YAML Deprecated ... Phase 7 Complete): Defense-in-depth opt-out system using the self-service Preference Center:
- Layer 1 (Sole Source): SQLite database via
preference_center/db.pyv2.1.0 (PreferenceDB) - Layer 2: Entity file flag (
visitor_reports_opt_out: true) - Layer 3:
should_send_report()function checks all layers - Layer 4: Full audit logging to
.tmp/logs/YYYY-MM-DD.log - Fail-safe: If SQLite is unavailable, email is blocked (not sent). Better to miss one email than to send to someone who opted out.
- Obsidian Dashboard:
005 Operations/Dashboards/Preference-Center-Status.mdauto-regenerates on every preference write + daily cron safety net
Self-Service Preference Management: Users can manage their own preferences via:
- Ezer Chat: Say "manage my preferences" or "unsubscribe" at terminal.quietlyworking.org
- Email Footer: All automated emails include a preferences link
- Direct URL: https://preferences.quietlyworking.org (requires magic link). Root URL (
/) serves the preferences page in landing mode. Token-based access via/p/{token}. (Fixed Apr 2026: root was previously redirecting to Architecture Map due to shared digital_twin_server.py ... now checksHostheader.)
Available Products:
| Code | Display Name | Description |
|---|---|---|
visitor_reports | Visitor Connection Reports | Briefings when visitors come to BNI |
meeting_recaps | Meeting Recap Emails | Summary after each BNI meeting |
network_recs | Connection Recommendations | AI-suggested connections |
verification_emails | LinkedIn Verification Requests | Profile verification requests |
meeting_followups | Meeting Follow-ups | Summary and action items after meetings with Chaplain TIG |
relationship_touchpoints | Relationship Touchpoints | Periodic check-in messages to stay connected |
all_communications | All Communications | Turn off everything |
Key Files:
preference_center/db.py- SQLite database operations + auto-dashboard; also the family consent gatecheck_send_permission_v2(v2.4.0)preference_center/magic_link.py- Token generation/verification (v1.0.0)preference_center/email.py- Magic link + confirmation emails (v1.1.0)preference_center/api.py- API endpoint handlers (v1.1.0)generate_preferences_dashboard.py- Cron safety net for Obsidian dashboard (v1.0.0)
Consent gate reading rule (v2.4.0, 2026-10-08): a deleted consent record can only tighten. The gate reads
QCM's contact_state_consent. A soft-deleted opted_in / never_set row no longer permits anything; a
soft-deleted opted_out row still blocks, Do-Not-Call included. In QCM a row is soft-deleted only by an operator
"reverts to Not set" or by merge clean-up, and a withdrawal is always written as opted_out in place, so a
person's stop is lifted only by an explicit, attributed opted_in. QCM enforces the same rule at the writer:
contact-state-consent v1.1.0 refuses to DELETE an opted_out row (409). Pinned by
tests/test_consent_resolver_ignores_deleted_rows.py; standard: qwf_consent_and_preferences_standard.md
(The Universal Gate).
MP Training Opportunities (2026-10-08, Session 715):
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Auditing what is DEPLOYED, not what is in the repo (download a function's live code, diff it, read its caller check) | Security review starts from ground truth; the code you can see is not always the code that runs | Intermediate |
| Splitting a safety-gate change by direction (does each effect tighten or loosen?) and testing both failure modes | The first, "obviously consistent" fix here would have let one reset click lift a person's stop; a reviewer caught it | Advanced |
A live proof on a throwaway fixture, run before AND after a deploy, cleaned up in a finally | Proving behavior on production without touching anyone's real record; a test that cannot show the old behavior proves nothing | Intermediate |
ez_chat_handler.py- "preferences" intent handling (v3.8.0)- Dashboard:
005 Operations/Dashboards/Preference-Center-Status.md - Directive:
005 Operations/Directives/preference_center.md
Missing Pixel Training Opportunity (Tier 2: Contributor)
Component Skills Taught SQLite database (db.py) Python, database design, CRUD operations, indexing Magic link auth (magic_link.py) HMAC-SHA256 cryptography, token-based auth, security Email system (email.py) MS Graph API, HTML templates, OAuth2 API handlers (api.py) REST design, rate limiting, error handling Intent detection NLP keyword matching, modal flows Portfolio Value: Full-stack system demonstrating database, security, API, and UX skills.
Email Sending Conventions (System-Wide)
Added: Session 65 (February 8, 2026)
Every email script is classified as Enhancement or Exempt:
| Classification | Footer | Opt-out Check | Examples |
|---|---|---|---|
| Enhancement | Preference link required | Required before send | Follow-ups, touchpoints, BNI reports, recaps |
| Exempt | No preference link | No check needed | Transactional, conversational, recently-requested |
Enhancement Scripts (footer + opt-out):
| Script | Category Code | Fail Mode |
|---|---|---|
send_meeting_followup.py v1.3.0 | meeting_followups | Fail-open |
send_meeting_prep_email.py | meeting_followups | Fail-open |
generate_bni_followup_emails.py | meeting_recaps | Fail-closed |
send_journey_message.py | relationship_touchpoints | Fail-closed |
bni_visitor_pipeline.py v3.4.0 | visitor_reports | Fail-closed |
Exempt Scripts (no footer):
| Script | Reason |
|---|---|
send_verification_email.py | Transactional (LinkedIn verification) |
send_optout_confirmation.py | Transactional (preference confirmation) |
ezer_respond.py | Conversational reply (human-initiated) |
video_email_sender.py | TIG-requested action |
qwr_notify_article_ready.py v2.1.0 | Transactional (supporter requested) |
Fail-open vs Fail-closed: Fail-open scripts send the email even if the preference check fails (suitable for operational emails like meeting follow-ups). Fail-closed scripts block the email if the preference check fails (required for ongoing automated communications like BNI reports and journey touchpoints).
BCC Monitoring: All 10 email scripts BCC EZER_BCC_EMAIL (<ADMIN_EMAIL>) on every outbound. Guard: BCC skipped if recipient == BCC address.
Name Rule: Her name is Ezer Aión (Hebrew "helper who runs toward" + Greek "eternal age"). Always use the accent on the 'o'. Never write "Aion" without it.
Missing Pixel Training Opportunity (Tier 2: Contributor)
The Email Preference System teaches real-world compliance patterns:
- Opt-out Architecture ... Defense-in-depth (database + entity file + self-service), fail-open vs fail-closed behavior
- Magic Link Auth ... HMAC-SHA256 token generation, expiry management, rate limiting
- Classification System ... Why transactional emails don't need unsubscribe links (CAN-SPAM)
- Database Migration ... Auto-adding columns via ALTER TABLE, handling defaults for existing rows
Exercise: Add a new product category to the preference center (add to db.py PRODUCTS, api.py PRODUCTS dict, email.py PRODUCT_NAMES), create a test email script that checks opt-out and appends the footer, verify with dry-run.
v3.1.0 Change (Email Housekeeping): After sending member emails, the pipeline automatically:
- Searches for the original registration email by visitor name
- Marks it with green checkmark (flag status:
complete) - Moves it to
Inbox/BNI/Visitorsfolder for organization
v3.0.0 Change (Auto-Send): Previously created Outlook drafts requiring manual review. Now sends emails directly after quality gate validation. User validated quality over multiple production runs before enabling.
Quality Gate: validate_bni_email.py ensures each email has:
- 10 required sections (Profile, Icebreakers, Social Proof, etc.)
- Minimum 10,000 characters
- Minimum 300 unique words
- Emails failing validation are NOT sent
[Networking Chapter] Mentor Program (QNT) ⭐ NEW
Added: Session 658 (September 7-9, 2026)
TIG becomes Lead Mentor for the [Networking Chapter] chapter in October 2026. The program's data lives in
the Quietly Networking database, not in the vault, because it is a product feature that any future
chapter inherits ... see feedback_build_qnt_features_against_tenant_db_not_vault.md.
What is live
| Piece | State |
|---|---|
Schema (7 tables, is_mentor(), 15 RLS policies) | Deployed 2026-09-06, migration 024 |
| Auto-send columns on the touch log | Deployed 2026-09-07, migration 025 |
| Four confirmed mentors + their caps + availability tokens | Seeded |
| Day-before reminders | Live on cron, first real send 2026-09-08 |
| Every screen (Lead Mentor console, the mentor's availability page, the member's confirm / move page, the session form) | Live on quietlynetworking.org since 2026-09-09, extended through 2026-09-23 |
| The mentor's availability page saves itself (no Save button; a status line says saved / not yet / failed) | Live 2026-09-23, TIG's ruling the same day |
Two public walk-through pages, /mentor-meeting-how-to (for members) and /mentor-window-how-to (for mentors) | Live 2026-09-23; nine scenes each, screens from the public demo chapter only |
Nightly booker (qnt_mentor_scheduler.py) | Built and dry-run clean; no cron installed ... it waits on TIG's "flip the scheduler" |
The one script in this program that sends
qnt_mentor_reminders.py v1.0.0, cron 0 16 * * * UTC (09:00 Pacific in summer, 08:00 in winter),
log .tmp/logs/qnt_mentor_reminders.log.
It is the only exception to the standing drafts-not-sends rule, released by TIG on 2026-09-07 for exactly two kinds of message: a mentor's certification session is tomorrow, or a mentor meeting is tomorrow. The reason is timing, not convenience ... a reminder that arrives late is not a reminder.
Its safety properties, in the order they matter:
- Consent resolves fail-closed through the family gate before any send, category
relationship_touchpoint, channel email. The resolved answer is stored on the touch row, so the log says why a message was allowed, not just that it went. - A person it cannot send to is FLAGGED to TIG by SMS, never skipped. No registration on
record, no address on file, or a consent HOLD all produce a
flag_onlyrow and a line in the text message. Nobody is silently short. - Idempotent on a per-recipient-per-day key, so a double cron run cannot double-send.
- A mentor registered for a session that is not tomorrow gets nothing. The window is exact.
- It will not remind someone to attend a session they never registered for ... that is a lie with a link in it, and it becomes a flag instead.
Nine unit tests cover the decision logic and none of them reach a live writer.
The region's monthly Traffic Lights report (added Session 724, 2026-10-11)
Every month the SoCal BNI office emails each chapter member a "Member Traffic Lights" report: one table with each
member's total score, a score for each of seven activities, and a colour (green, yellow, red, grey). The first one
arrived 2026-10-09 and nothing filed it: it came from "BNI Office" [email protected] (not the
officememo@ address the manual names), no schedule ran the reader, and it was a PDF the reader had never seen.
qnt_mentor_regional_reports.py v1.1.0 now reads it:
- The colour is painted, not written. In the region's PDF (a Google Sheets export) the colour exists only as a cell fill. The reader records the fill on each member's Total Score cell (TIG's decision D-14, "painted"); if a paint ever cannot be read, it works the colour out from the region's own cut-offs, marks the row, and texts TIG.
- It refuses to guess. A row loads only when its seven scores add up to its total. A name that matches no member, or matches two, is left for a person. When the email's subject and the file name a different month, nothing loads and TIG is texted, because a wrong month would overwrite another month's colours.
- BNI Connect's sender is admitted only for a subject naming Traffic Lights; it also sends renewals and event registrations. The mailbox is read only.
- Schedule: daily 12:15 Pacific (
--poll --apply) and the 12th at 12:45 Pacific (--check-month --apply, a push and Discord alert when nothing has loaded for the month). Log.tmp/logs/qnt_mentor_traffic_lights.log. - Who sees it: chapter leaders and each member's own mentor, in QNT only (the board chip, the mentor's page, and section 3 of the session form). Never a public surface, never a corpus (D-8).
October 2026 is loaded: 19 members. Detail and history: the [Networking Chapter] Mentor System Status.
Training opportunity (Missing Pixel): reading a PDF table whose meaning lives in its drawing layer is a strong intermediate exercise in Python, PDF internals and test design (build a synthetic file that mimics the real one, so the tests carry no real member data). Prerequisites: Python, basic PDF structure. About 6 hours. Intermediate.
The certification gate (SoCal BNI, per term)
Not in the Mentor handbook ... it comes from the region's own notices. Every leadership and support role must (a) sign the Leadership Team Agreement and (b) attend one role certification start to finish. The region's wording: anyone who cannot attend and stay for the whole session "will be replaced" in the role. All certifications fall in September.
Governance worth remembering
- Routing follows the office, not the person. Every completed mentor form goes to the Lead Mentor AND the Chapter Vice President within 24 hours, and the Lead Mentor must confirm the VP received it. The VP is resolved at send time from the roster, never stored as an address, because the office turns over every October.
- A form's mentor is whoever held the meeting, and a label can be wrong. A meeting imported from a list lands on whoever the member was paired with at import time, and a form takes its mentor from the meeting's pairing. So after a member moves, a new mentor's form can sit on the old mentor's record. Check the typed mentor against the record before a report goes out in the Lead Mentor's name, and ask the Lead Mentor whose meeting it was. Since 2026-11-03 the Vice President sender does that check itself: a form naming a different mentor than the one it is filed under stays unsent, the Lead Mentor is told once with both names, and the other forms still go. Once the record is right it sends itself on the next run. It cannot see a new mentor who leaves the pre-filled name alone, so the Lead Mentor decided (D-16, "block it") that a mentor who did not hold a meeting will not be able to file its form at all. That app change is built and proven but not live: it waits on the Lead Mentor's word on its words, parked to 2026-11-05. See the [Networking Chapter] Mentor System Status.
- A mentor's cap is the mentor's. The system may lower one on their word and may never raise one. The handbook's hard ceiling is 6.
- Nothing reaches BNI Connect automatically. BNI's terms forbid it. The system's job is to make the manual step fast and error-free, not to eliminate it.
MP Training Opportunities (from building the form's own-mentor rule, 2026-11-04, Session 909)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Prove a permission rule as made-up people, then roll it all back | Every login, member and form in the proof exists only inside one database transaction that is undone at the end, so a live rule is tested without touching a real record. Teaches transactions, row-level security and how to test against production safely | Advanced |
| One person wearing three hats hides a wrong rule | In this chapter the Lead Mentor, the only owner and the only super admin are the same person, so a rule keyed to the wrong role passes every eye test. Teaches testing each role alone, and picturing the second chapter | Intermediate |
| See the test fail first, then break the code on purpose | The proof failed 52 of 143 checks before the rule existed. Later, removing the rule from the page by hand made tests fail, while an early version of one test passed no matter what. Teaches why a test that cannot fail proves nothing | Intermediate |
| The order of checks decides the words people read | The first draft put the new rule ahead of an older check, so a person tapping Approve read a sentence about the wrong thing. Teaches that an error message is part of the design, and how to review for it | Intermediate |
| Commit only your own lines in a shared file | Many sessions edit one status file. A private git index built from the last commit holds just your lines, and moving the branch with a compare-and-swap refuses if anyone committed in between. Teaches what the index, a tree and a ref really are | Advanced |
MP Training Opportunities
Added from the mentor mismatch hold (Session 888). Exercise: give a student a form whose name box is pre-filled from the record it is later checked against, and ask them to find what that check can never catch. It teaches tracing where each value comes from before trusting a comparison, writing the failing test first, and saving a live script in steps that each stay safe. Intermediate, 1 to 2 hours.
| Task | Skills taught | Level | Time |
|---|---|---|---|
| Trace who really wrote a record, then correct it without setting anything off | Reading an audit trail instead of trusting a label, finding what a data change triggers before making it, guarded writes that name the person and the reason | Intermediate | 2 hours |
| Re-run a mailbox search that returned an incomplete answer | Search result caps, why a keyword that matches newsletters is a bad key, verifying a count against the source rather than trusting a tool's first answer | Beginner | 1 hour |
| Write a database migration with its own proof inside the transaction | SQL, transactions, why an assertion that runs after the commit proves nothing | Intermediate | 3 hours |
| Build a sender that refuses to send | Fail-closed design, why "flag the person you could not reach" beats "skip them quietly", idempotency keys | Intermediate | 4 hours |
| Write tests for a script that emails real people | Pure functions, fixtures, why no test may ever reach a live writer | Intermediate | 2 hours |
The lesson worth teaching from this session: the roster said one mentor for four weeks. Nothing was broken, no error appeared, and the number was simply wrong because a search tool returned its first hundred matches and the reply we needed was the hundred-and-first. A confident wrong answer looks exactly like a right one. The habit that catches it is asking what the tool was actually searching, then searching a key that cannot be diluted ... the exact subject line, not the word everyone uses.
MP Training Opportunities (from the 2026-11-01 stale-tab diagnosis and fresh-build fix, Session 869)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Read one person's own requests | A mentor's screen said "not set up as a mentor" while every database row said she was. The server's request log, filtered to her sign-in (request.sb.jwt.authorization.payload.subject), showed the exact queries her browser sent, and a column only the old code asks for dated her tab to four days earlier. Teaches going to the deepest witness before touching data | Intermediate |
| A web app keeps running the code it loaded | An open tab never fetches new code on its own; a database change that closes an old read breaks only the tabs nobody reloaded. Teaches build ids, /version.json, and why "it works for me" is not proof | Intermediate |
| A failed read is not an empty answer | The page treated "the lookup failed" the same as "no record", so a refusal became a false sentence. Teaches separating error, empty and loading states in React Query | Beginner |
| Never reload over someone's save | The first fix reloaded in the same page move that started a save-as-you-leave; a reviewer proved the save was cut off. The fix waits for every write and stands down when someone types or taps. Teaches React unmount order, counting in-flight writes, and adversarial review | Advanced |
| Check a suggested fix before offering it | "Sign out and back in" sounded right but QNT's sign-out stays inside the same old copy of the app. Teaches reading the code path behind a help sentence | Beginner |
What a hand delete does to a mentor meeting's calendar entry (tested 2026-11-04, Session 906)
The rule today: the calendar's owner never deletes a mentor meeting from their own calendar. Their entry is the real one. A mentor's invitation is only a copy.
- The owner deletes: nothing is called off, and the member still expects the meeting. Any guest's copy goes too. If the member later moves the meeting, the system's update reports success while the entry stays deleted, and the new time never shows. A Lead Mentor who mentors gets no reminder email for their own meetings, so the entry's alert is their prompt.
- A mentor deletes their invitation: only their copy goes, and the meeting stands. In the tests, the next change brought the copy back. Tested through Google's API, not a press of its Delete button, and not on Outlook.
- Undo: on a computer, Google Calendar, Settings, Trash, Restore, within 30 days.
- Open, waiting on TIG's word: have the morning run put a deleted entry back once and say so, or add a bold line to the owner's own entries. The full record is the Known Issue "A hand delete on TIG's calendar" in the mentor System Status file. Members' and mentors' answers are in the QNT User Manual.
MP Training Opportunities (from the calendar delete tests, 2026-11-04, Session 906)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Test a vendor's behavior on throwaway data | The question could not be answered from documents alone. Far-future entries, removed in a finally block and confirmed gone, proved it with no risk to a real meeting. Teaches designing a safe live experiment and cleaning up after it | Intermediate |
| "Success" does not mean it worked | Updating a deleted entry answered 200 and changed its time while it stayed deleted. Teaches reading the state back after a write instead of trusting the response | Beginner |
| A test that proves nothing | One round's guest copy read 404 because it had not appeared yet (more than 8 seconds). Teaches polling for slow effects, and telling "not there yet" from "not there" | Intermediate |
| Check who a rule is true for before writing it | A bold "don't delete" line sounded right for everyone, and would have told mentors something untrue. Teaches mapping each audience to the fact before writing the sentence | Beginner |
| Find what a planned fix would not catch | A morning restore can't catch a delete made later that morning, before a meeting that day. Teaches walking a fix through the timeline and saying its limit before anyone approves it | Beginner |
| Run the check you hand to someone else | A check written into a handoff printed nothing because its search did not fit the log's format, and the format checker still passed it. Teaches extracting and running exactly what you wrote | Beginner |
Yes and No on the new-member forms (live 2026-11-03, Session 910)
Every line of the Sessions 1 to 4 forms has its own Yes and No. A second tap takes it back. A blank means not answered and never stops Submit. It came from a mentor's bug report: an empty box could not tell "no" from "we didn't get to it."
- The record: no migration. Each section keeps
ticked(the Yes lines) and gainsno, written only when it holds a line. Forms saved before read their ticks as Yes. - The readers: the form, its paste block and the Vice President's report (
qnt_mentor_form_routing.pyv1.8.0). One shared fixture pins the app and the report byte for byte. A member's own page never carries the answers. - Ship order: the report first, then the app, because the old report ignored
noand a form approved in that gap would have lost it. Undo runs the other way. Never run the report by hand: it sends real email. - First real use: 2026-11-04, a real form with 30 Yes and 5 No, waiting on the Lead Mentor at this writing.
What the first Monthly Mentor Team Meeting's recording taught the recap (named, not fixed; TIG's design call). There is one recap narrative
per meeting, so a line from a private talk reached people who had left. A soft "it'll probably be today or tomorrow" printed as a due date. A
Zoom name that isn't plain ASCII loses its speaker label. And drafts don't buy review time: TIG sent these about two minutes after the "drafts ready"
text, so any check must live inside the pipeline. See process_zoom_recording.md, changelog 2026-11-03, and the parked kickoff qnt-mentor-yes-no-followups-parked-kickoff-2026-11-05.md.
MP Training Opportunities ⭐ (from Yes and No, 2026-11-03, Session 910)
| Teachable unit | What happened | Level |
|---|---|---|
| Three answers need three states | An empty box meant both "no" and "not asked." Adding a second list made Yes, No and not answered all readable, and old records kept their meaning with no migration | Intermediate |
| One fixture, two readers | The app and an emailed report had to print a form the same way, in two languages. One shared file of expected output pins both, and 6,000 random records came out identical | Intermediate |
| Ship the reader first | The old report would have dropped the new answers, so the report shipped minutes before the app, and the undo order is the reverse | Advanced |
| Know what your test can't see | The fake browser let a click through a locked form, and a scripted event isn't a real tap. Both gave wrong answers until a real browser settled them | Intermediate |
| Read the sentence, not the summary | A recap said "due" where the person said "probably." Only the transcript showed it was a hope | Beginner |
| A draft isn't a safety net | A person sent drafts within two minutes of being told they were ready, so the check had to move ahead of the message | Beginner |
Most valuable MP exercise: give a student a checklist stored as ticks, and ask them to add a real "No" without changing how any old record reads. Then have them open the same form on an old build and find out what gets lost.
BNI Meeting Recap System ⭐ NEW
Automated weekly meeting recap generation for BNI chapters. Processes Zoom chat logs to extract visitors, referrals, announcements, and engagement data, then generates personalized HTML emails for each chapter member. As of February 2026, supports slide-augmented recaps that combine chat analysis with Claude Vision screenshot analysis from Google Drive meeting folders.
Purpose
After each BNI meeting, generate a comprehensive recap email that:
- Highlights visitors with full contact details and interest levels
- Showcases referrals given with context (not just names)
- Features the week's speakers with their Ideal Customer Profiles
- Lists 1-2-1 connection requests with contact links
- Includes chapter announcements with actionable URLs
- Celebrates chat engagement champions
- Personalizes content for each recipient
Goal: Keep members engaged, reinforce connections made during the meeting, and make follow-up easy.
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ DATA SOURCES │
├─────────────────────────────────────────────────────────────────────┤
│ Zoom Chat │ Speaker │ [Member Name]'s │ Entity Files │
│ Export │ Schedule │ Visitor Sheet │ (003 Entities/) │
│ (.txt) │ (Google │ (attendance, │ (member data, │
│ │ Sheets) │ interest) │ ICP, contact) │
└──────┬────────┴───────┬────────┴────────┬────────┴─────────┬────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ PROCESSING PIPELINE │
│ ┌──────────────────┐ ┌────────────────┐ ┌──────────────────────┐ │
│ │ process_bni_chat │ │ read_speaker_ │ │ read_cathie_visitor_ │ │
│ │ .py │→ │ schedule.py │→ │ sheet.py │ │
│ │ - Parse messages │ │ - Get speakers │ │ - Attendance data │ │
│ │ - Extract data │ │ for date │ │ - Interest levels │ │
│ └──────────────────┘ └────────────────┘ └──────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ generate_bni_meeting_recap.py (v4.3.0) │ │
│ │ - Combines all data sources │ │
│ │ - EPIC visitor spotlights with enriched entity data │ │
│ │ - Builds personalized HTML for each recipient │ │
│ │ - Speaker spotlight with ICP and referral triggers │ │
│ │ - Week-over-week trend tracking │ │
│ │ - Discord approval flow + MS Graph email delivery │ │
│ └────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ OUTPUT │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ Discord Preview │ │ HTML Email │ │ Markdown Archive │ │
│ │ (approval flow) │ │ (MS Graph API) │ │ (.tmp/bni_recap/) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Scripts
| Script | Purpose |
|---|---|
process_bni_chat.py | Parse Zoom chat export, extract participants, referrals, 1-2-1 requests, announcements |
generate_bni_meeting_recap.py | Generate personalized HTML recap emails |
read_speaker_schedule.py | Read speaker assignments from Google Sheets |
read_cathie_visitor_sheet.py | Read visitor attendance/interest from [Member Name]'s tracking sheet |
extract_slide_intel.py | Analyze meeting screenshots via Claude Vision (multimodal). Downloads from Google Drive shared folder, analyzes each image, synthesizes presentations + attendees + notable moments into unified JSON intelligence. |
Usage
Step 1: Process the Zoom chat export
python process_bni_chat.py --file "path/to/zoom_chat.txt" --json --output processed.json
Step 2: Generate recap (preview mode)
python generate_bni_meeting_recap.py processed.json --preview
Step 3: Post to Discord for approval
python generate_bni_meeting_recap.py processed.json --discord
Step 4: Send test email to yourself
python generate_bni_meeting_recap.py processed.json --test-email <ADMIN_EMAIL>
Step 5: Send to all participants
python generate_bni_meeting_recap.py processed.json --send
Email Personalization
Each recipient receives a customized email with:
| Section | Description |
|---|---|
| Personalized Header | Shows their referrals given/received, 1-2-1 requests, chat champion status |
| Visitor Spotlight | EPIC enriched cards with company, BNI category, summary, power teams, contact info |
| Speaker Spotlight | This week's speakers with Ideal Customer Profile and referral triggers |
| Referrals Given | Stories about referrals passed during the meeting |
| 1-2-1 Connections | Who wants to meet with whom, with contact links (calendly/email/phone) |
| Don't Forget | Chapter announcements with dates and action links |
| Chat Champions | Top engaged members in the Zoom chat |
| Trends | Week-over-week attendance, referrals, engagement |
Slide-Augmented Recap (v2 ... February 2026)
When meeting screenshots are uploaded to a Google Drive folder, the recap can be augmented with visual intelligence:
Step 0: Extract slide intelligence from Google Drive
python extract_slide_intel.py --date 2026-02-26 --folder-id <drive_folder_id>
This uses Claude Vision (FLAGSHIP tier) to analyze each screenshot, then synthesizes:
- Presentation titles, speakers, and key content from each slide
- Attendees spotted in gallery-view screenshots
- Notable moments (reactions, celebrations, achievements)
Output: .tmp/slide_intel/YYYY-MM-DD_slide_intel.json
The slide intel JSON is then combined with chat analysis to produce a richer recap with keynote spotlights, visual context, and attendee cross-referencing.
Data integrity safeguard: Before sending to any recipients, the send script must:
- Build recipient list from
BNI-Activeentity tags - Call
check_send_permission(email, "meeting_recaps", fail_open=False)for each recipient - Generate per-recipient preference center footer with magic link
- Send individually (no BCC) for unique unsubscribe links
Official roster verification: The authoritative member list is at socalbni.com (AJAX POST to /bnicms/v3/frontend/chapterdetail/display with website_type=2, website_id=5197). Cross-reference entity files against this periodically to catch stale BNI-Active tags.
Current Path (v3 ... August 2026): the QNT in-app loop
The legacy path above is historical. As of 2026-08-20 the weekly recap is produced by the
Quietly Networking app, not by generate_bni_meeting_recap.py. First end-to-end edition shipped
2026-08-20 (25 emails: 15 members + 10 visitors).
TIG pastes the Zoom chat into QNT (MeetingDrawer -> meetings.chat_raw)
|
v POST /qnt/process-meeting (qnt-webhook, localhost:8100)
qnt_meeting_pipeline.py parse_zoom_chat + LLM connection extraction (~$0.27/run)
| Track A: qnt_source_adapter resolves visitors from [Member Name]'s
| sheet (THE visitor authority) + speakers for the date
v
qnt_recap_generator.py 16-section registry -> TWO editions (member + visitor),
| persisted as `draft` newsletter_editions rows
v
EITHER qnt_recap_send.py --send (automated; fail-closed consent gate)
OR draft_qnt_recap_emails.py --create <- CURRENT PRACTICE
one Outlook DRAFT per recipient; NO send path
Two editions, deliberately different audiences. visitor_spotlight and power_team_matches
are member-only edition sets in SECTION_REGISTRY (not config toggles) ... a visitor edition
that lists the day's visitors publishes every visitor's email address to every other visitor, and
several of those addresses come from the private registration sheet rather than from the person's
own chat message. The visitor edition instead carries the full member roster with each
member's own details, gated by their public_email / public_phone flags.
⚠ The consent category above is STALE. The live gate is
check_send_permission_v2(email, "email", "relationship_touchpoint", fail_open=False).
qnt_recap_send.py's docstring still says meeting_recaps, but the code passes
relationship_touchpoint, and that difference is load-bearing: meeting_recaps resolves
17 of 17 members to HOLD and would send to nobody. Do not "fix" the code to match the docstring.
Drafts, not sends. Per the standing 2026-08-19 HARD RULE, every relationship-facing recap is
created as a DRAFT in TIG's mailbox for him to review and send. draft_qnt_recap_emails.py reuses
qnt_recap_send._compose_edition_html so a draft cannot drift from what the sender would produce
(and so the preference footer is never omitted), passes tracking_id=None (no open pixel, no click
wrapping ... a hand-sent message has no newsletter_recipients row to attribute), and has no send
path. --only builds a second wave after a later consent decision without duplicating drafts for
people who already received the email.
Also gated as of 2026-08-20: draft_weekly_report_emails.py (Weekly Connection Report) had no
consent check at all. It now resolves the same channel and category, fail-closed, so one member
decision governs both weekly touchpoints.
Speaker Spotlight Feature
The speaker spotlight (v4.2.0+) features all speakers from the meeting:
- Speaker 1 and Speaker 2 are treated equally (just presentation order)
- Pulls from the speaker schedule Google Sheet for the meeting date
- Falls back to one random member if no speakers scheduled
- Shows member's company, Ideal Customer Profile, and "When to Refer" triggers
- Uses fuzzy name matching to find entity files (e.g., "[Member Name]" → "[Member Name].md")
Spotlight Content (from Entity Files):
## Ideal Customer Profile
- **Industries**: Local Service Businesses, Retail/Boutique Owners...
- **Company Size**: Small-to-medium businesses, $100K-$5M revenue...
- **When to Refer**: Business owner asking about direct mail marketing...
EPIC Visitor Spotlight (v4.3.0+)
Visitor cards now pull enriched data from entity files for symmetric intelligence - visitors get the same rich treatment as members:
| Field | Source | Example |
|---|---|---|
| Company | Entity company_name or [Member Name]'s sheet | [Member Company] |
| BNI Category | Entity bni_category | Bookkeeping |
| Summary | Entity ## Professional Summary | "[Member Title] at [Member Company]..." |
| Power Teams | Entity ## Power Team Connections | CPA, Business Broker, Financial Advisor |
Entity linkedin_url | Clickable link | |
| Calendly | Entity 121_link | "Schedule 1:1" button |
Entity Lookup: The lookup_visitor_entity() function searches:
- Exact match:
003 Entities/People/{Name}.md - Draft files:
000 Inbox/___Review/{Name} [DRAFT].md - Fuzzy match: Files containing all significant name parts
Result: Instead of "[Member Name] - TBD", recaps now show:
[Member Name] - [Member Company] | Bookkeeping "[Member Title] at [Member Company]..." Power Team: CPA / Tax Preparer, Business Broker, Financial Advisor
Data Extraction
The chat parser (process_bni_chat.py v2.4.0) extracts:
| Data Type | Detection Method |
|---|---|
| Participants | Matches against vault entity files + display name parsing |
| Visitors | Prefixes: "VIP -", "VIP ", "S/BNI -", "S/BNI ", "BNI -", "Guest -", "Visitor -" |
| Referrals | Keywords: "referral", "passing", "thank you for referring" |
| 1-2-1 Requests | Patterns like "@Name let's 121", "would love to 1:1 with Name" |
| Announcements | Keywords + REQUIRES URL (dates alone not sufficient) |
| Contact Info | Email, phone, calendly links from message content |
Filtering Rules:
- 1-2-1 requests require a specific person target (not "visitors" or "everyone")
- Self-promotional calendly shares are excluded from 1-2-1 requests
- Announcements without actionable URLs are filtered out
- Thank-you/gratitude messages excluded from announcements
- Duplicate announcements from same sender are deduplicated
Visitor Interest Levels
When [Member Name]'s visitor tracking sheet is available, visitors display interest badges:
| Stars | Meaning |
|---|---|
| ⭐⭐⭐ | High interest - likely to join |
| ⭐⭐ | Moderate interest |
| ⭐ | Low interest |
| (no stars) | No interest data available |
Output Files
| File | Location |
|---|---|
| Processed chat data | .tmp/bni_chat/processed_YYYY-MM-DD.json |
| HTML email template | .tmp/bni_recap/YYYY-MM-DD_recap.html |
| Markdown archive | .tmp/bni_recap/YYYY-MM-DD_recap.md |
| Trend history | .tmp/bni_recap/trend_history.json |
Environment Variables
# MS Graph API (for sending emails)
AZURE_CLIENT_ID="your-client-id"
AZURE_CLIENT_SECRET="your-client-secret"
AZURE_TENANT_ID="your-tenant-id"
OUTLOOK_EMAIL="[email protected]"
# Discord (for approval flow)
DISCORD_WEBHOOK_BNI_RECAP="webhook-url"
# Google Sheets (for speaker schedule + visitor tracking)
GOOGLE_SERVICE_ACCOUNT_KEY_PATH=".credentials/google-service-account.json"
SPEAKER_SCHEDULE_SPREADSHEET_ID="spreadsheet-id"
CATHIE_VISITOR_SPREADSHEET_ID="spreadsheet-id"
Directives
| Directive | Purpose |
|---|---|
process_bni_chat.md | SOP for parsing chat exports |
generate_bni_followup_emails.md | SOP for the full recap pipeline |
MP Training Opportunities
| Task | Skills Developed |
|---|---|
| Writing member Ideal Customer Profiles | Marketing, ICP development, copywriting |
| Creating referral trigger lists | Business development, market research |
| Processing visitor data from sheets | Data entry, spreadsheet skills |
| Testing email templates | QA, attention to detail |
| Auditing Ezer email templates for voice consistency | QA, Python reading, pattern recognition ... trace a bug from the user-facing artifact back through f-strings and data sources (Intermediate, ~1 hr) |
Writing defensive data normalizers (normalize_first_name pattern) | Python functions, edge-case design, idempotence, smoke testing ... start with a naive .capitalize() then discover why "iPhone" breaks it (Beginner → Intermediate, ~45 min) |
System Architecture Audit ⭐ NEW
A monthly deep-analysis process to evaluate system health, identify gaps, and ensure all components work together effectively.
Purpose
As the QWU Backoffice grows in complexity (38 directives, 73 scripts, 8 agents, 11 skills), it becomes critical to regularly step back and assess:
- Which components are fully integrated
- What systems are dangling or incomplete
- Where gaps exist that need attention
- How well the layers work together
This audit serves both operational health and educational purposes ... providing students with a comprehensive view of how a production automation system is structured.
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│ QWU BACKOFFICE SYSTEM │
│ ~45,000 lines | 3-Layer Architecture │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ LAYER 1 │ │ LAYER 2 │ │ LAYER 3 │
│ DIRECTIVES │ │ ORCHESTRATION │ │ EXECUTION │
│ (What to do) │ │ (Decision-making)│ │ (Doing the work) │
│ │ │ │ │ │
│ 38 SOPs │ │ Claude Agents │ │ 73 Python │
│ ~10K lines │ │ 8 specialized │ │ scripts │
│ 005 Operations/ │ │ .claude/agents/ │ │ ~36K lines │
│ Directives/ │ │ │ │ 005 Operations/ │
│ │ │ │ │ Execution/ │
└───────────────────┘ └───────────────────┘ └───────────────────┘
System Component Map
Capture & Ingest (Fully Integrated ✅)
📱 Phone Captures 📧 Outlook Emails 🎥 Zoom Meetings
│ │ │
▼ ▼ ▼
process_inbox.py outlook_pipeline.py zoom_pipeline.py
│ │ │
│ └───────────┬───────────────┘
│ │
│ ▼
│ ┌─────────────────────┐
│ │ ENTITY RESOLUTION │
│ │ + SuiteDash CRM │
│ └─────────────────────┘
│ │
└──────────────────┬───────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ VAULT UPDATES │
│ 003 Entities/People/ (150+ profiles) │
│ 003 Entities/Organizations/ (50+ orgs) │
│ 001 Daily/ (daily notes) │
│ ___Tasks/ (action items) │
└─────────────────────────────────────────────────────────┘
Intelligence Layer (Fully Integrated ✅)
8 Thought Leaders Wisdom Pipeline
┌─────────────────┐ ┌─────────────────┐
│ Simon Sinek │ │ youtube_ │
│ Brené Brown │───(4-12 hrs)───▶│ monitor.py │
│ Lex Fridman │ │ │ │
│ Andrej Karpathy │ │ ▼ │
│ Dwarkesh Patel │ │ wisdom_ │
│ [+3 others] │ │ indexer.py │──▶ SQLite
└─────────────────┘ │ │ │
│ ▼ │
│ wisdom_ │──▶ Attributed
│ synthesizer.py │ Content
└─────────────────┘
Daily Operations (Fully Integrated ✅)
06:00 ─────▶ morning_briefing.py ─────▶ Discord #daily-digest
│ │
└────────────────────────▶│
▼
┌─────────────┐
│ Daily Note │
└─────────────┘
▲
18:00 ─────▶ summarize_session.py ────────────┘
Lead Generation (Partially Integrated 📋)
Lead Sources (40% Built) Enrichment (85% Built)
┌─────────────────────┐ ┌─────────────────────┐
│ LinkedIn │ │ enrich_leads.md │
│ Google Maps │ │ (orchestrator) │
│ Apollo ─?──▶ │ ── ? ──▶ │ │ │
│ Yellow Pages │ │ ┌─────────────────┐ │
│ Yelp │ │ │friendly_name │ │
│ Crunchbase │ │ │reviews │ │
└─────────────────────┘ │ │email │ │
⚠️ Router exists │ └─────────────────┘ │
sources need testing └─────────────────────┘
QWF Creative (Router Built, Execution Light 📋)
┌─────────────────────┐
Request ─────▶ │ qwf-master-router │ ✅ Complete
└────────┬────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│qwf-writer │ │qwf- │ │qwf-social │
│📋 Light │ │creative- │ │-media- │
│execution │ │director │ │manager │
└───────────┘ │📋 Light │ │📋 Light │
└───────────┘ └───────────┘
Component Health Scorecard
| Component | Status | Completeness | Notes |
|---|---|---|---|
| Email Processing | ✅ Active | 100% | SuiteDash CRM integrated |
| Meeting Intelligence | ✅ Active | 100% | SuiteDash CRM integrated |
| Morning Briefing | ✅ Active | 100% | Project tasks included |
| Daily Synthesis | ✅ Active | 100% | Goals context included |
| Expert Monitoring | ✅ Active | 100% | YouTube, 54 channels monitored |
| BNI Member Enrichment | ✅ Active | 100% | Full pipeline |
| Inbox Processing | ✅ Active | 95% | Dual-input working |
| Wisdom Synthesis | ✅ Active | 90% | Architecture complete |
| Lead Enrichment | ✅ Active | 85% | Core enrichments work |
| Document Sync | 📋 Defined | 60% | Scripts exist |
| Calendar/Scheduling | 📋 Defined | 50% | Not in briefing |
| Lead Generation | 📋 Partial | 40% | Router only |
| QWF Creative | 📋 Router | 30% | Needs execution |
Known Gaps (January 2026)
| Gap | Severity | Impact | Recommendation |
|---|---|---|---|
| Lead gen sources not wired | Medium | Can't generate leads at scale | Test Apify actors, document |
| QWF creative execution light | Medium | Content not automated | Build templates per type |
| Calendar not in briefing | Low | Missing today's meetings | Wire calendar_events.py |
| Prompts directory empty | Low | No prompt versioning | Extract from scripts |
| Scheduling rules undefined | Low | Manual booking only | Document rules |
Running the Audit
Trigger: Monthly (1st of month) or on-demand
Command:
/audit-system
Or ask Claude: "Run a comprehensive system architecture audit with ultrathink"
Process:
- Claude explores all directives, scripts, agents, skills
- Maps dependencies and integrations
- Identifies gaps and incomplete work
- Generates health scorecard
- Produces recommendations
Directive: 005 Operations/Directives/audit_system_architecture.md
Audit Output Template
Each audit produces:
- Architecture Diagram - Visual map of system components
- Integration Map - What connects to what (strong/weak)
- Health Scorecard - Component completeness percentages
- Gap Analysis - What's missing or broken
- Recommendations - Prioritized next actions
- Comparison - Changes since last audit (if available)
Audit History
| Date | Auditor | Version | Key Findings | Actions Taken |
|---|---|---|---|---|
| 2026-01-25 | Claude Opus 4.5 | v1.2.0 | Duplicate task bug | Added deduplication, cleaned 128 duplicates |
| 2026-01-11 | Claude Opus 4.5 | v1.0 | 75% complete, lead gen/creative gaps | Initial baseline established |
Student Learning Objectives
This audit system teaches students:
- Systems Thinking - How components interconnect
- Technical Debt Awareness - Identifying incomplete work
- Documentation Discipline - Maintaining living docs
- Architecture Visualization - Diagramming complex systems
- Prioritization - Ranking gaps by severity/impact
EPIC Appointment Intelligence System v2.0 ⭐ NEW
Added: Session 22 (January 13, 2026)
The Reframe
What we thought: Build a lead-to-appointment conversion pipeline What we learned: Appointments don't come from lead generation - they come from speaking engagements, referrals, networking, and relationship reconnections
The Real Opportunity: TIG is often booked 8 months in advance. Those 8 months are not empty waiting time - they're the beginning of Stage One: Hope for a Better Future.
Core Components
| Component | Purpose |
|---|---|
| Waiting Period Experience | Transform wait time into relationship building |
| Multi-Channel Booking | Make it easy for anyone to book time |
| Meeting Intelligence | Dossiers, templates, and follow-up |
| Relationship Management | Circle progression and analytics |
Waiting Period Philosophy
"By the time they sit down with TIG, he should know what they love, who shaped them, what they dream about, and what challenges they face. And they should feel known, valued, safe, hopeful, and excited."
The waiting period embodies the first 4 of TIG's 7 Steps of Mentorship:
- Love them like they've never been loved before ← Every message
- Come alongside so they know they're not alone ← Consistent presence
- Help them see hope for a better future ← Stories, wisdom, QWU
- Help them discover their purpose ← Questions, reflection
Timeline Intelligence
| Days Out | Journey Type | Focus |
|---|---|---|
| 180+ days | Full Journey | All 6 phases, deep relationship building |
| 60-179 days | Standard | Condensed 4 phases |
| 14-59 days | Accelerated | Essential 3 phases |
| 3-13 days | Express | Warm welcome + single question |
| 0-2 days | Immediate | Logistics + genuine excitement |
Four Tracks (Entry Points)
| Track | Who | Approach |
|---|---|---|
| Inspired | Heard TIG speak | Fan the flame, build on momentum |
| Curious | Friend referral | Gentle introduction, earn trust |
| Connected | Networking catch-up | Relationship maintenance, go deeper |
| Returning | 2nd+ time meeting | Augmented content, deeper questions |
The 11 EPIC Capabilities
| # | Capability | Priority |
|---|---|---|
| 0 | Waiting Period Experience | Critical |
| 1 | Intelligent Follow-Up Sequencing | Critical |
| 2 | Multi-Channel Booking | High |
| 3 | Smart Scheduling Assistant | High |
| 4 | Meeting Dossier Generation | Critical |
| 5 | Appointment Analytics | Medium |
| 6 | AI Scheduling Negotiation | Medium |
| 7 | L4G Sales Pipeline (Separate) | Medium |
| 8 | Circle Auto-Promotion | Medium |
| 9 | Meeting Type Templates | High |
| 10 | White-Glove VIP Mode | High |
Key Files
Directives:
waiting_period_experience.md- THE CORE journey systemsequence_appointment_followup.md- Pre/post meeting automationmulti_channel_booking.md- Channel handlingsmart_scheduling_assistant.md- Intelligent schedulingmeeting_type_templates.md- Pre-configured meetingsvip_white_glove.md- Inner Circle experienceappointment_analytics.md- Analytics dashboardscheduling_negotiation.md- AI conversation handlingcircle_auto_management.md- Relationship lifecycle
Scripts:
initiate_waiting_period.py- Start sequence on bookingcalculate_journey_tier.py- Determine timeline tierdetect_visitor_track.py- Identify track typeselect_journey_content.py- Choose appropriate contentsend_journey_message.py- Deliver touchpointsrecord_journey_response.py- Capture responsescheck_content_history.py- Prevent repeats
Templates:
waiting_period_questions.md- Question progression (L1→L4)waiting_period_content_map.md- Content calendar by phase
Success Metrics
The Ultimate Metric: Do they feel loved, not alone, and hopeful before they ever shake TIG's hand?
Measurable:
- Journey completion rate (target: 60%+)
- Question response rate (target: 40%+)
- Show rate (target: 95%+)
- Circle progression rate
Ezer Aión Assistant System ⭐ NEW
Added: Session 22 (January 13, 2026)
Who Is Ezer Aión?
From the Splinter universe: An ancient companion who has traveled with adventurers across centuries, providing cognitive connection and unwavering support. Not a servant. Not a tool. A partner who runs toward danger alongside you.
In the real world: TIG's AI-powered assistant and the friendly face of QWU's automated operations. She handles outreach, verification, scheduling, and the thousand small tasks that keep the mission moving forward.
The Name
| Component | Meaning |
|---|---|
| Ezer (עֵזֶר) | Hebrew for "helper who runs toward" |
| Aión (Αἰών) | Greek for "eternal age" |
| Easter Egg | "AI-on" hidden in the name |
Core Voice Attributes
- Eternal Companion - Timeless patience, not urgency
- Warm Transparency - Always honest, even when uncertain
- Gentle Efficiency - Gets things done without being cold
- Patient Guide - Never makes anyone feel dumb
- Quietly Present - Always there when needed
Hemingway Principles
| Principle | Application |
|---|---|
| Get to the point | Short, clear messages |
| Write with warmth | Every word carries kindness |
| Be positive | "I'll find out" beats "I don't know" |
| Eliminate fluff | Kind doesn't mean wordy |
| Use active voice | "I'll check" not "That will be checked" |
Capabilities
| Capability | Description |
|---|---|
| LinkedIn Verification | Email campaigns to verify LinkedIn URLs |
| Response Handling | Conversational responses via Claude API |
| Intent Detection | confirmation, rejection, correction, question, confused, OOO |
| Self-Annealing FAQ | Learns from new questions |
Key Files
Voice Profile: 003 Entities/Voice Profiles/Ezer Aión/Brand Voice.md
FAQ: 004 Knowledge/FAQ/Ezer FAQ.md
Directives:
ezer_respond.md- Response handler SOPenrich_linkedin_url.md- LinkedIn enrichmentsend_linkedin_verification_email.md- Verification workflowsms_timing_rules.md- Communication timing
Scripts:
ezer_respond.py- Main response handler with Claude APIfind_linkedin_url.py- Search for LinkedIn profilesprepare_verification_batch.py- Batch preparationsend_verification_email.py- Send via Microsoft Graphupdate_entity_linkedin.py- Update entity filessms_timing.py- SMS delivery timing rules
Standard Signature
Cheers,
Ez 💙
Ezer Aión | Assistant to Chaplain TIG
Quietly Working Foundation
quietlyworking.org
Name Normalization Helper (Added 2026-04-19)
Raw first names pulled from external data sources (Google Calendar profile names, Supabase CRM rows, BNI rosters) are often stored in all-lowercase because users type their own names that way during signup. When those strings get substituted into hardcoded email f-strings, recipients see disrespectful output like "Howdy jane!".
Fix: 005 Operations/Execution/name_utils.py exports normalize_first_name(raw, fallback="there"). It only uppercases the first character when the input is entirely lowercase ... preserving deliberate interior capitalization like "McKenzie", "DeShawn", "iPhone", and "JANE".
Every Ezer-voiced email script that substitutes a first name into template text now normalizes through this helper: send_meeting_prep_email.py, send_meeting_followup.py (already used .title()), generate_bni_followup_emails.py, ezer_respond.py, send_l4g_welcome_email.py, send_l4g_queue_notification.py, qwr_notify_article_ready.py.
Voice profile rule: Brand Voice.md v1.1 (2026-04-19) added an explicit "Capitalization of Proper Names" subsection so LLM-composed emails (the flagship ezer_respond.py path) also follow the rule. This closes the gap where hardcoded templates and LLM output could disagree on voice.
Email Conventions
All outbound emails from Ezer follow the Email Sending Conventions documented in the BNI Visitor Pipeline section. Enhancement emails (automated follow-ups, touchpoints, reports) include a preference management footer and check opt-out status before sending. Exempt emails (transactional, conversational) do not. All emails BCC TIG. See [[#Email Sending Conventions (System-Wide)]] for the full classification table.
The Long Game
Every interaction builds toward a future where "Got an email from Ez" feels normal and welcome across the entire QWU network.
Ezer Omnibus - Unified Communication Gateway ⭐ NEW
Added: Session 24 (January 16, 2026)
Ezer Omnibus is the unified communication intelligence system that routes all incoming messages (SMS, Discord, Voice) through a single intelligent gateway with full access to QWU's knowledge base, calendar, contacts, health tracking, and program operations.
Vision: "One conversation, every channel, complete context."
Architecture
INBOUND CHANNELS
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ SMS │ │ Discord │ │ Voice │
│ (Twilio) │ │ (DM) │ │ (Whisper) │
│ │ │ │ │ │
│ External │ │ TIG's │ │ Voice msgs │
│ inquiries │ │ interface │ │ transcribed │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────────┼───────────────────┘
│
▼
┌─────────────────────┐
│ UNIFIED ROUTER │
│ │
│ • Normalize input │
│ • Identify sender │
│ • Classify intent │
│ • Route to handler │
└──────────┬──────────┘
│
┌─────────┬─────────┼─────────┬─────────┐
▼ ▼ ▼ ▼ ▼
┌──────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Approval │ │Calendar│ │ Health │ │Program │ │General │
│ Handler │ │Handler │ │Handler │ │Handler │ │ Query │
│ │ │ │ │ │ │ │ │Handler │
│ YES/NO │ │ Events │ │BP, meds│ │L4G/WOH │ │Claude │
└──────────┘ └────────┘ └────────┘ └────────┘ └────────┘
Intent Classification
| Priority | Intent | Example | Handler |
|---|---|---|---|
| 1 | Compliance | STOP, HELP, START | ComplianceHandler |
| 2 | Backoffice | "Run the audit", "Fix the error" | BackofficeHandler |
| 3 | Health | "BP 120/80 pulse 72" | HealthHandler |
| 4 | Approval | YES, NO (with ref code) | ApprovalHandler |
| 5 | Calendar | "What's on my calendar tomorrow?" | CalendarHandler |
| 6 | Program | "L4G", "interested in advertising" | ProgramHandler |
| 7 | General | Natural language questions | GeneralHandler (Claude) |
Handler Capabilities
| Handler | Channel | Capabilities |
|---|---|---|
| Backoffice | SMS | Full Claude Code access: read files, run scripts, fix errors, system operations |
| Health | SMS, Discord | BP, weight, medication tracking → Obsidian vault |
| Calendar | SMS, Discord | Natural language date queries → Google Calendar |
| Program | SMS | L4G/WOH inquiries → SuiteDash lead creation |
| General | SMS, Discord | Claude Sonnet 4 + Wisdom DB for intelligent responses |
Access Levels
| Access | Who | Capabilities |
|---|---|---|
| FULL | TIG (phone/Discord ID) | All handlers including health, calendar, general queries |
| PUBLIC | Unknown numbers | Compliance, program inquiries, basic help |
Key Scripts
| Script | Purpose | Version |
|---|---|---|
twilio_webhook_server.py | SMS/MMS router - main gateway | v3.7.0 (passes the To number to the compliance handlers) |
sms_intent_classifier.py | Intent classification engine | v1.4.0 |
ezer_backoffice_agent.py | Claude Agent SDK wrapper for SMS-triggered backoffice | v1.1.0 |
process_sms_image.py | MMS image analysis (IMAGE tier) | v1.0.0 |
sms_compliance.py | STOP/HELP/START compliance | v2.1.0 (SQLite primary since v2.0.0; v2.1.0 mirrors STOP/START into Quietly Quoting's consent store) |
health_tracker.py | Health data → Obsidian | v1.0.0 |
calendar_assistant.py | Calendar queries | v1.0.0 |
ezer_discord_handler.py | Discord DM processing | v1.2.0 |
voice_transcriber.py | Whisper transcription | v1.0.0 |
ezer_query.py | Claude-powered queries | v1.0.0 |
program_inquiry.py | SuiteDash lead creation | v1.0.0 |
SuiteDash Integration
Program inquiries automatically create CRM leads:
| Program | Trigger | SuiteDash Action |
|---|---|---|
| L4G | Text "L4G" or mention advertising | Create contact, set l4g_client_type = "lead" |
| WOH | Text "WOH" or mention volunteering | Create contact, set woh_volunteer_status = "interested" |
| General | Any other inquiry | Create contact, set qwf_program_interest = "general" |
Setting these custom fields triggers SuiteDash Path A automations for student outreach.
Usage Examples
SMS Health Tracking:
→ "BP 118/78 pulse 68"
← "Recorded: 118/78 mmHg, pulse 68 bpm
7-day avg: 121/79
Status: Within normal range"
MMS Health Screenshot (v2.8+):
→ [Sends photo of Eufy scale or sleep tracker screenshot]
← "Recorded: 186.1 lbs, BMI 22.0, 11.5% body fat. 7-day avg: 185.8 lbs"
[Automatically extracts metrics using IMAGE tier (Gemini 3 Pro), logs to health tracker]
MMS Reference Image (v2.8+):
→ [Sends photo of a receipt or reference image]
← "Saved! [AI description]. I'll remember this."
[Stores in 000 Inbox/___Capture/ with AI-generated metadata for future search]
SMS Calendar Query:
→ "What's on my calendar tomorrow?"
← "Tomorrow (Thu Jan 16):
• 9:00 AM - BNI Meeting (1h)
• 2:00 PM - Client Call (30m)
2 events, 1.5 hours scheduled"
SMS Program Inquiry:
→ "I'm interested in advertising with L4G"
← "Thank you for your interest in supporting QWF through Locals 4 Good!
A member of our student outreach team will contact you within 24 hours."
[Creates SuiteDash lead, notifies #l4g-leads Discord]
Discord General Query:
→ "What does the research say about 3D Gaussian Splatting?"
← "Based on recent research from George Drettakis and the INRIA team,
3DGS offers real-time rendering at 30+ fps while maintaining high
visual quality comparable to NeRF..."
[Pulls from Wisdom DB with expert attribution]
Master Directive
See: 005 Operations/Directives/ezer_omnibus.md for complete technical specification.
Phase 7: Proactive Intelligence ⭐ NEW
Added: Session 26 (January 16, 2026)
Ezer evolves from reactive (responds when prompted) to proactive (anticipates needs).
New Components
| Script | Purpose | Version |
|---|---|---|
ezer_memory.py | Cross-channel conversation persistence (SQLite) | v1.0.0 |
ezer_scheduler.py | Proactive outbound SMS (health check-ins, reminders) | v1.0.0 |
ezer_briefing.py | Multi-source briefing generator | v1.0.0 |
Cross-Channel Memory
All conversations now persist across SMS, Discord, and Email:
# Record a message
from ezer_memory import record_message, get_conversation_context
record_message(channel="sms", user_id="+1234567890", role="user", content="BP 120/80")
# Get context across all channels
context = get_conversation_context(user_id="+1234567890", limit=10, hours_back=24)
Features:
- Commitment extraction ("remind me to..." → tracked)
- User identity linking across channels
- Searchable conversation history
Proactive Scheduler
Automated outbound communications:
# Morning health check-in
python ezer_scheduler.py health-checkin --json
# Send reminder
python ezer_scheduler.py reminder --user "+1234567890" --message "Call John" --json
# Check status
python ezer_scheduler.py status --json
n8n Workflows:
| Workflow | Schedule | Purpose |
|---|---|---|
morning-health-checkin.json | Daily 7 AM PT | BP reminder with context |
discord-dm-poller.json | Every 3 min | Process Discord DM voice messages |
Briefing Engine
Multi-source briefings accessible via CLI or SMS:
# Morning briefing (calendar + health + commitments)
python ezer_briefing.py morning --json
# Brief on a person (SuiteDash + Vault + Memory)
python ezer_briefing.py person "Sarah Chen" --json
# Brief on a topic (Wisdom DB + Memory)
python ezer_briefing.py topic "3D Gaussian Splatting" --json
SMS Usage:
→ "brief me"
← "Friday, January 16, 2026
📅 8 events (9.75 hours)
💊 BP: 118/76 (on track)
📋 0 pending commitments"
Morning Health Check-in Flow
[7:00 AM - Automated SMS from Ezer]
"Good morning! Ready for your BP reading?
Yesterday: 118/78
Reply with: BP [reading] pulse [rate]"
[TIG replies]
"BP 120/80 pulse 70"
[Ezer responds]
"Recorded! 7-day avg: 119/79. You're on track."
Phase 8: Backoffice Agent (read-only since 2026-10-26)
Added: Session 52 (January 26, 2026). Made read-only: Session 819 (October 26, 2026), on TIG's "go".
TIG can text Ez a question about the backoffice from anywhere and get an answer. Ez reads the vault to answer. Ez does not run commands, change files, deploy or restart anything from a text. Ask for a change and Ez says plainly that changes can't be made by text, and that TIG can make it from a Claude session at his desk.
What You Can Ask
- "What's broken?" ... Ez reads today's logs and audit notes
- "What did the audit find?"
- "What's in the error logs today?"
- "What does the WCR directive say about addendums?"
Architecture
SMS from TIG
↓
Twilio signature check (an unsigned request is refused with 403)
↓
Intent Classifier (BACKOFFICE_COMMAND)
↓
ezer_backoffice_agent.py
↓
Claude Agent SDK, three tools only
├─ Read
├─ Glob
└─ Grep
(a PreToolUse hook refuses secret paths and anything outside the vault)
↓
Response Summary
├─ SMS to TIG (concise)
└─ Discord #agent-log (the kind of request and a masked number, never the text)
Safety Controls
| Control | Implementation |
|---|---|
| Who can ask | Twilio-signed requests from TIG's phone number only |
| Tools | Read, Glob and Grep; no shell, no edits, no writes |
| Where it reads | /home/<VM_USER>/qwu_backOffice only |
| Protected paths | Secret files (.env, credentials, keys) and backup folders, refused by the hook |
| Cost cap | $1.00 per request (EZER_AGENT_MAX_COST_PER_REQUEST) |
| Turn limit | 5 agentic turns |
| Timeout | 120 seconds |
Usage Examples
Simple Query:
→ "What's broken?"
← "System healthy. Last audit: 2 warnings
(both resolved). No errors in logs."
Check Logs:
→ "What's in the error logs today?"
← "3 entries today:
- 08:15 n8n timeout (resolved)
- 11:42 API rate limit (retried)
- 14:30 Discord 429 (backed off)
All resolved. No action needed."
Directive & Scripts
| Resource | Location |
|---|---|
| Directive | 005 Operations/Directives/ezer_backoffice_agent.md |
| Agent Script | 005 Operations/Execution/ezer_backoffice_agent.py v2.0.0 |
| Intent Classifier | 005 Operations/Execution/sms_intent_classifier.py v1.5.0 |
| Router | 005 Operations/Execution/twilio_webhook_server.py v3.8.0 |
Ezer Universal Interface ⭐ NEW
Added: Session 51 (January 25, 2026)
The Ezer Universal Interface is a cross-domain AI chat widget that provides a consistent conversational AI experience across all QWU-affiliated websites with progressive trust tiers and site-specific intelligence.
Key Concept: "The Octopus That Swims Through All Waters"
Ezer appears as a friendly octopus (🐙) toggle button on any QWU site. When clicked, it opens a CRT-styled chat sidebar. The same user identity persists across all domains via a cross-domain cookie bridge.
Trust Tier System
| Tier | Name | How Achieved | Ezer Knows |
|---|---|---|---|
| 0 | Anonymous | First visit | Nothing personal |
| 1 | Remembered | Click "Remember me" | Device ID, past conversations |
| 2 | Named | Tell Ezer your name | Name for personalization |
| 3 | Verified | Email magic link | Account data, verified identity |
| 4 | Operator | TIG's devices | Full system access |
Phase 6: Site-Specific Intelligence
Each QWU site has its own context that shapes Ezer's behavior:
| Site | Domain | Ezer's Personality |
|---|---|---|
| Digital Twin | twin.quietlyworking.org | Technical, precise - system diagnostics |
| L4G | locals4good.org | Warm, community-focused - postcard fundraising |
| WOH | waronhopelessness.org | Bold, empowering - "punch fear in the face" |
| HeroesKids | heroeskids.org | Reverent, gentle - HIGH SENSITIVITY |
| IYSR | iysr.org | Professional, collaborative - YSO network |
Architecture
┌──────────────────────────────────────────────────────────────┐
│ ANY QWU WEBSITE │
│ ┌───────────────┐ ┌─────────────────────────────────────┐│
│ │ ezer.js │────│ /api/ezer/context/{site} ││
│ │ Widget │ │ Returns: greeting, tools, tone, ││
│ └───────┬───────┘ │ sensitivity, prompts ││
│ │ └─────────────────────────────────────┘│
│ ▼ │
│ ┌───────────────┐ │
│ │ bridge.html │ (hidden iframe for cross-domain cookies) │
│ └───────────────┘ │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ CENTRAL API (twin.quietlyworking.org:8767) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ /api/ezer - Chat (site-aware system prompts) │ │
│ │ /api/ezer/identity - Get/validate device tier │ │
│ │ /api/ezer/remember - Upgrade Tier 0 → 1 │ │
│ │ /api/ezer/introduce - Upgrade Tier 1 → 2 │ │
│ │ /api/ezer/verify - Send magic link (→ Tier 3) │ │
│ │ /api/ezer/context/* - Site-specific intelligence │ │
│ └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Key Files
Widget:
100 Resources/ezer-widget/ezer.js- Standalone widget (v1.5.0)100 Resources/ezer-widget/bridge.html- Cross-domain identity bridge100 Resources/ezer-widget/demo.html- Testing page
Server:
005 Operations/Execution/digital_twin_server.py- Central API (v1.12.0)005 Operations/Execution/ezer_site_contexts.json- Site configurations
Directive:
005 Operations/Directives/ezer_universal_interface.md- Full specification
Embedding on External Sites
<script src="https://twin.quietlyworking.org/ezer/ezer.js"></script>
<script>
Ezer.init({ site: 'l4g' }); // Site-specific context
</script>
Language Rules (Nonprofit Terminology)
Ezer enforces QWF's nonprofit framing:
| Never Say | Always Say |
|---|---|
| customer | supporter, donor-partner |
| payment | donation |
| revenue, profit | proceeds |
| advertising | recognition |
| business | program, mission |
Missing Pixel Training Opportunity
Skill Level: Intermediate-Advanced
| Component | Skills Taught |
|---|---|
| Widget development | JavaScript, DOM, async/await |
| API design | REST, CORS, authentication |
| Cross-domain identity | Cookies, postMessage, iframes |
| Context-aware AI | System prompts, prompt engineering |
Strategic Goals Framework ⭐ NEW
Added: Session 22 (January 13, 2026)
2026 Strategic Initiatives
| # | Goal | Priority | Status |
|---|---|---|---|
| 1 | Automate QWU Backoffice Operations | 9/10 | ~90% |
| 2 | Establish L4G Revenue Pipeline | 8/10 | ~30% |
| 3 | Launch Missing Pixel Tier 2 Curriculum + Frontier Operations | 7/10 | ~35% |
| 4 | Complete Transparency Infrastructure | 5/10 | ~40% |
| 5 | Launch QWU Tool Shed (QTS) | 5/10 | ~5% |
| 6 | Implement WHELHO Life Framework (App) | 5/10 | ~40% |
| 7 | Build Client Project Hierarchy | 6/10 | ~25% |
| 8 | Integrate YouTube Content Library | 3/10 | ~10% |
Key Files
Strategic:
002 Projects/_Goals and Priorities.md- Master goals document002 Projects/_Client Projects/_Overview.md- Client management002 Projects/_Locals 4 Good/L4G Customer Experience Framework.md- L4G CX playbook
WHELHO:
004 Knowledge/Concepts/WHELHO/WHELHO.md- Master framework (8 wheel sections + core values)004 Knowledge/Concepts/WHELHO/- Individual section docs (Spirit, Mind, Body, Relationships, Money, Recreation, Work, Purpose)005 Operations/Templates/WHELHO Annual Review.md- Annual review template002 Projects/WHELHO App/WHELHO-App-Project-Brief.md- WHELHO App project brief002 Projects/WHELHO App/WHL-System-Status.md- System Status (infrastructure, deployment, schema)002 Projects/WHELHO App/WHELHO-App-Vision.md- Technical vision document002 Projects/WHELHO App/WHELHO-Development-Plan.md- Phase-by-phase development plan002 Projects/WHELHO App/WHELHO-Spline-Scene-Brief.md- Spline 3D scene design spec (object names, variables, events, creative direction)
Client Project Hierarchy
Year → Quarter → Month → Week → Day
Each client is treated as their own entity with goals that roll up to the big picture:
- [Networking Chapter] - Chapter automation, member enrichment
- [Supporter Organization] - Operations efficiency
- Missing Pixel Students - Tier 2 curriculum
- L4G Prospective Clients - Revenue pipeline
Scheduling Love Principle
"When folks need help, it's never at a convenient time."
Protected calendar blocks (minimum 4 hours/week) for:
- Unexpected client needs
- Relationship investment
- Spontaneous opportunities
- Rest and recovery
Integration with Morning Briefing
The morning briefing surfaces:
- Top 2-3 goals by priority
- Milestones marked as CURRENT
- Client tasks due this week
- "Scheduling Love" blocks for the day
QWU Cosmic Style Guide ⭐ NEW
Added: Session 22 (January 13, 2026)
Design Philosophy
The Aesthetic: "Dark but kind space, a playground of galaxies" - vast yet warm, mysterious yet inviting, powerful yet gentle.
Hemingway Principle
Every visual element must earn its place. Ask:
- What purpose does this serve?
- Does it guide the eye intentionally?
- Can it be removed without losing meaning?
Color Palette
The Void (Backgrounds):
| Name | Hex | Usage |
|---|---|---|
| Deep Space | #0a0a14 | Primary background |
| Cosmos | #12121f | Secondary background, cards |
| Nebula Dark | #1a1a2e | Elevated surfaces, modals |
The Stars (Accents):
| Name | Hex | Usage |
|---|---|---|
| Nebula Purple | #4a1a6b | Borders, subtle highlights |
| Cosmic Magenta | #9b3d8f | Primary accent, focus states |
| Stellar Orange | #d4782c | CTAs, energy, warmth |
| Aurora Teal | #2dd4bf | Success, growth, positive |
Text:
| Name | Hex | Usage |
|---|---|---|
| Stardust | #e8e4f0 | Primary text |
| Moonlight | #a8a4b8 | Secondary text |
| Dim Star | #6b6780 | Disabled, placeholder |
Design Rules
- Dark mode ALWAYS default
- Minimum contrast ratio: 4.5:1 for body text
- No information conveyed by color alone
- Motion: Only functional animations (loading, progress, UX feedback)
- Decorative animations are forbidden
Key File
Full Specification: 005 Operations/Directives/qwu_style_guide.md
Content Calendar System ⭐ NEW
A multi-channel content distribution hub that automates social media posting via Vista Social and sends reminder notifications for manual platforms.
Architecture
Content Notes (Obsidian)
│
▼
n8n Cron (6am PT daily)
│
▼
process_content_calendar.py
│
├── Automated → Vista Social API → Instagram, Facebook, Twitter, LinkedIn, TikTok
│
└── Reminder → Discord #content-queue → Circle, Skool, Press Ranger
Delivery Modes
| Mode | Platforms | What Happens |
|---|---|---|
automated | Instagram, Facebook, Twitter/X, LinkedIn, TikTok | API schedules post via Vista Social |
reminder | Circle.so, Skool, Press Ranger | Discord notification with copy-paste content |
manual | L4G Production | Tracked only (for production calendar) |
Content Note Format
Content items live in 005 Operations/Content Calendar/ as Obsidian notes with YAML frontmatter:
---
uid: content-20260120-instagram-woh
title: "New drop announcement"
type: social-post # social-post | article | press-release | announcement
program: WOH # QWF | WOH | MP | ACOFH | L4G | IYSR | QWC
platforms:
- instagram
- facebook
scheduled: 2026-01-20T10:00:00 # ISO format
status: approved # draft | approved | scheduled | published | posted | failed
voice: woh-combat # tig-standard | woh-combat | l4g-b2b
delivery: automated # automated | reminder | manual
assets:
- https://example.com/image.jpg
---
Check out our latest drop! 🔥
#WarOnHopelessness #NewDrop
Supported Platforms
Automated (via Vista Social):
- Instagram (Posts, Stories, Reels)
- Facebook (Posts)
- Twitter/X (Tweets, Threads)
- LinkedIn (Posts, Articles)
- TikTok (Videos)
Reminder (Discord notification):
- Circle.so (Community posts)
- Skool (Community posts)
- Press Ranger (Press releases)
Manual (tracked only):
- L4G Production (Artwork pipeline)
Daily Workflow
- 6:00 AM PT: n8n triggers
process_content_calendar.py - Script scans
005 Operations/Content Calendar/for items with today'sscheduleddate - For each item:
- automated: Calls Vista Social API, updates status to
scheduled - reminder: Sends Discord notification to #content-queue
- manual: Logs only (no action)
- automated: Calls Vista Social API, updates status to
- Discord #content-queue receives summary notification
Key Files
| File | Purpose |
|---|---|
005 Operations/Content Calendar/ | Content notes folder |
005 Operations/Templates/content-calendar-item.md | Template for new items |
005 Operations/Directives/process_content_calendar.md | Full directive |
005 Operations/Execution/process_content_calendar.py | Processing script |
005 Operations/Workflows/content-calendar-daily.json | n8n workflow |
Commands
# Check what would be processed today (dry run)
python "005 Operations/Execution/process_content_calendar.py" --dry-run
# Process today's content
python "005 Operations/Execution/process_content_calendar.py"
# JSON output for n8n
python "005 Operations/Execution/process_content_calendar.py" --json
# Process specific date
python "005 Operations/Execution/process_content_calendar.py" --date 2026-01-20
Discord Integration
| Channel | Purpose |
|---|---|
| #content-queue | Daily summaries + manual posting reminders |
Webhook configured in .env:
DISCORD_WEBHOOK_CONTENT_QUEUE=https://discord.com/api/webhooks/...
DISCORD_CHANNEL_CONTENT_QUEUE=<DISCORD_CHANNEL_ID>
Integration with Vista Social
The content calendar uses the Vista Social Python wrapper (vista_social_api.py) with built-in rate limiting. See [[#Vista Social Integration]] for full details.
Platform → Profile Mapping:
- Uses program + platform to auto-detect Vista Social profile ID
- Profile mapping defined in directive:
005 Operations/Directives/process_content_calendar.md
Status Lifecycle
draft → approved → scheduled → published/posted
↓
failed (with error message)
| Status | Meaning |
|---|---|
draft | Work in progress |
approved | Ready for scheduling |
scheduled | API confirmed scheduling |
published | Successfully posted (automated) |
posted | Manually marked as done (reminder/manual) |
failed | Error occurred (check error field) |
QWR Article Generation System ⭐ NEW
The Quietly Writing (QWR) app uses an n8n workflow to generate articles with optional Perplexity deep research and persona-targeted content generation.
Architecture (v8.0)
Webhook (POST /generate-article)
│
▼
Fetch Article Details (Supabase)
│
▼
Check Research Type (IF node)
├── research_type === "research"
│ ▼
│ Build Research Prompt (Code)
│ ▼
│ Call Perplexity via OpenRouter
│ ▼
│ Store Research Results v2.0 (Supabase PATCH + citation URL extraction)
│ ▼
└── Fetch Brand Voice (Supabase)
▼
Fetch Persona Context (Supabase — if persona_id set)
▼
Fetch Living Document Enrichment (from brand)
▼
Fetch Wisdom Entries (Supabase)
▼
Generate Article (Claude via OpenRouter — with persona + enrichment context)
▼
Update Article in Supabase
▼
Discord Success Alert
All error outputs route to a Discord Error Alert node.
v5.0 additions: If the article has a persona_id, the workflow fetches the persona's profile (demographics, pain points, objections, communication preferences) and injects it into the AI prompt so the article speaks directly to that reader. Living document enrichment (voice phrases, tonal qualities, thematic patterns) from the brand is also included for more authentic output.
v6.0 additions: When gap_opportunity_id is present, fetches the full gap with evidence and injects it as the article's primary angle. Customer language injection from Reddit-derived terms, phrases, and questions. Keyword-matches against active gaps (score >= 40) for standard articles. Content strategy matching: if the article has a content_strategy_id, fetches the strategy's platform, expertise level, style guide, and target persona for strategy-aware generation.
v7.0 additions: Content Strategy context injection ... goal framing, expertise level, style guide, content themes, and avoid list from the strategy record. Strategy overrides the old hardcoded style guide when present.
v8.0 additions (Quality Targets): Enforces measurable quality standards ... minimum word count, required heading structure (H2/H3), citation density targets, readability scoring. Store Research Results v2.0 extracts citation URLs from Perplexity annotations and inline markdown links into research_citations for accurate source counting. Score Article Quality node counts actual sources_used instead of regex phrase matching. Generate Article node strengthened with inline source attribution requirements and emoji control.
Key Details
| Property | Value |
|---|---|
| n8n Workflow ID | <WORKFLOW_ID> (v5.0) |
| Webhook Path | /generate-article |
| Supabase Project | <SUPABASE_PROJECT_ID> |
| OpenRouter Credential | OpenRouter QWR (<N8N_CREDENTIAL_ID>) |
| Supabase Credential | Supabase QWR (<N8N_CREDENTIAL_ID>) |
| Previous Workflow IDs | <WORKFLOW_ID> (v1-v4, archived) |
Research Depth Levels
| Depth | Perplexity Model | Max Tokens |
|---|---|---|
quick | perplexity/sonar | 4096 |
deep | perplexity/sonar-pro | 8192 |
comprehensive | perplexity/sonar-deep-research | 8192 |
Content Strategy Tracks
Research prompts are guided by content strategy tracks stored in research_settings.tracks:
- sme_authority ... Authoritative primary sources for expert positioning
- seo_geo ... Question-based sections for AI citation and featured snippets
- pillar_content ... Hub-and-spoke subtopic mapping
- multi_post_hooks ... Narrative angles for multi-part series
- quick_value ... Key facts, breadth over depth
Supabase Article Statuses
Allowed values (enforced by check constraint): pending, generating, ready, failed
n8n Expression Gotchas (Learned the Hard Way)
- No optional chaining (
?.) in n8n expressions ... use||fallbacks - Only ONE
respondToWebhooknode per workflow - Add
alwaysOutputData: trueto Supabase query nodes that may return zero rows - OpenRouter uses short model IDs (
anthropic/claude-sonnet-4, notanthropic/claude-sonnet-4-20250514) - Supabase update via native node has UUID filter bugs ... use httpRequest PATCH instead
Related Files
| File | Purpose |
|---|---|
005 Operations/Handoffs/qwr-research-workflow-spec.md | Full research branch specification |
005 Operations/Handoffs/qwr-backoffice-handover.md | QWR system handover notes |
🎓 Missing Pixel Training Opportunities
n8n Workflow Debugging (Intermediate)
- Skills: n8n expression syntax, API debugging, webhook workflows, Supabase REST API
- Prerequisites: Basic JavaScript, REST API concepts
- Exercise: Deploy a simple webhook-to-Supabase pipeline with error handling and Discord alerts
QWR Audience Intelligence System ⭐ NEW
The Audience Intelligence system enables persona-targeted article generation. Users create customer personas through conversational AI interviews, link Google Docs as "Living Documents" for voice enrichment, and select which persona to target when creating articles. The full pipeline ensures articles speak directly to the intended reader.
Architecture
┌─────────────────────────────┐
│ ONBOARDING / SETTINGS │
│ ┌─────────┐ ┌───────────┐ │
│ │ Persona │ │ Living │ │
│ │Interview│ │ Documents │ │
│ └────┬────┘ └─────┬─────┘ │
└───────┼─────────────┼────────┘
│ │
┌───────▼─────────────▼────────┐
│ SUPABASE │
│ personas table (profile JSONB)│
│ living_documents table │
│ brands.living_document_ │
│ enrichment (combined JSONB) │
│ articles.persona_id (FK) │
└───────┬─────────────┬────────┘
│ │
┌─────────────────┤ │
│ │ │
┌───────▼───────┐ ┌─────▼─────┐ ┌───▼──────────┐
│ Article Gen │ │ Interview │ │ Living Doc │
│ v5.0 (n8n) │ │ Webhook │ │ Sync (n8n) │
│ reads persona│ │ (n8n) │ │ weekly │
│ + enrichment │ └───────────┘ └──────────────┘
└───────────────┘
Components
| Component | Type | ID/Path | Purpose |
|---|---|---|---|
| Personas table | Supabase | personas | Stores persona profiles (name, archetype, profile JSONB) |
| Interview Sessions table | Supabase | interview_sessions | Tracks conversation state for resume capability |
| Living Documents table | Supabase | living_documents | Google Docs linked for voice enrichment |
| Interview Webhook | n8n workflow | nbUJ57ZFAjttguu4 | Handles start/message commands for conversational persona interviews |
| Living Doc Sync | n8n workflow | tvi5z83IrONMO40U | Weekly sync: fetches Google Docs, extracts voice patterns via Claude |
| Article Gen v6.0 | n8n workflow | <WORKFLOW_ID> | Reads persona context + living doc enrichment + gap evidence + content strategy for targeted generation |
| PersonaSelector | Lovable component | PersonaSelector.tsx | Dropdown in article creation flow |
| usePersonas hook | Lovable hook | usePersonas.ts | Fetches active personas by brand |
Persona Interview Flow
- User clicks "Add Persona" in Settings > Audience
- Enters a name for the reader persona
- Frontend creates persona record in Supabase, calls interview webhook with
command: 'start' - AI conducts 4-phase conversational interview: Discovery, Clarification, Documentation, Validation
- Each message sent via
command: 'message'to the same webhook - On completion, persona
profileJSONB is populated with demographics, pain points, objections, communication preferences, decision factors - Persona appears in the PersonaSelector when creating articles
Living Documents
Users link Google Docs (shared as "Anyone with the link can view") categorized by type:
quotes... Favorite quotes, sayings, mantraspersonal_writing... Blog posts, journal entries, essaysindustry_inspiration... Thought leadership articlesbrand_reference... Brand guidelines, tone documentsgeneral... Other reference material
The weekly n8n sync workflow reads each doc, extracts voice-relevant patterns (phrases, tonal qualities, vocabulary signatures), and stores per-document document_profile JSONB. A combined brands.living_document_enrichment JSONB aggregates all docs for the brand.
Tier Limits
| Tier | Max Personas | Max Living Docs |
|---|---|---|
| Trial | 1 | 1 |
| Starter | 3 | 3 |
| Growth | 5 | 5 |
| Agency | 10 | 10 |
Frontend Pages
| Page | Feature Added |
|---|---|
| Settings > Audience | Persona list, create/edit/delete, interview dialog, Living Documents management |
Article Creation (/articles/new) | PersonaSelector dropdown (follows PlatformSelector pattern) |
| Article Detail | "Written for {persona}" badge when persona_id is set |
| Onboarding | 8-step wizard includes persona interview at step 5 |
Related Files
| File | Purpose |
|---|---|
lovable-prompt-audience-intelligence-onboarding.md | 8-step onboarding redesign with persona interview |
lovable-prompt-audience-personas-settings.md | Audience tab + persona management in Settings |
lovable-prompt-audience-living-docs-settings.md | Living Documents section in Audience tab |
lovable-prompt-article-persona-selector.md | PersonaSelector in article creation flow |
qwr_audience_intelligence_migration.sql | Full schema (tables, functions, RLS) |
Daily Journal Command Center ⭐ NEW
The Daily Journal Command Center transforms the Obsidian daily note into a personal intelligence system with relationship tracking, email digests, and proactive insights.
Architecture
Morning Briefing → Email Intelligence → Relationship Health
↓ ↓ ↓
Thread Tracking ← Decisions/Commitments → Opportunities
↓ ↓ ↓
EOD Summary ← Pattern Analysis → This Day in History
Key Scripts
| Script | Purpose |
|---|---|
morning_briefing.py | Morning intelligence briefing |
generate_email_digest.py | Rich email summaries |
relationship_health.py | Contact health tracking |
track_thread_continuity.py | Email thread tracking |
analyze_sentiment.py | Sentiment/escalation detection |
detect_opportunities.py | Intro matching |
capture_decisions.py | Decision/commitment extraction |
generate_catchup_briefing.py | After-absence catch-up |
analyze_patterns.py | Weekly pattern recognition |
Dashboards
- Relationship Health Dashboard -
005 Operations/Dashboards/Relationship-Health-Dashboard.md - System Health Dashboard -
005 Operations/Dashboards/System-Health-Dashboard.md
Configuration
- CSS Snippet:
.obsidian/snippets/daily-journal-command-center.css - Keyboard Shortcuts:
.obsidian/hotkeys.json - Documentation:
005 Operations/Directives/daily_journal_technical_reference.md - User Guide:
005 Operations/Directives/daily_journal_user_guide.md
Session Work Tracking
The /session-wrap-up skill (Step 5) recompiles a ## What Got Done Today section in the daily journal after each session. This provides the "EOD Summary" component shown in the architecture diagram above ... a consolidated view of all Claude Code session accomplishments for the day, placed between Morning Briefing and Email Intelligence.
A note added below the Email Intelligence block in a daily note stays there since 2026-11-03: each digest refresh removes only its own block, which ends at its Complete Log, and writes the new one at the bottom of the note. Before that, a refresh deleted everything below the block down to the next ## heading.
Related Directives
daily_journal_command_center.md- Full implementation specificationdaily_journal_technical_reference.md- Maintenance documentationdaily_journal_user_guide.md- End user workflow guide
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| CSS Styling | Obsidian customization, responsive design | ⭐⭐ |
| Dataview Queries | SQL-like querying, data visualization | ⭐⭐⭐ |
| Script Documentation | Technical writing, markdown | ⭐⭐ |
| Dashboard Creation | Information architecture, UX design | ⭐⭐⭐ |
Supervisor Observability System (SOS) ⭐ NEW
Added: Session 44 (January 23, 2026)
The Supervisor Observability System provides complete visibility into supervisor activity, enabling real-time monitoring, trend analysis, and proactive issue detection.
Why SOS?
As supervisors (operations, relationship-intelligence, content-pipeline, lead-intelligence, student-programs) handle more automated tasks, we need visibility into:
- What each supervisor is doing
- Success/failure rates across the fleet
- Token consumption per supervisor
- Trends that might indicate problems
Core Principle: Automation without visibility is abdication.
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ SUPERVISOR OBSERVABILITY SYSTEM │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ACTIVITY LOGGING LAYER │ │
│ │ Every supervisor task logged with: │ │
│ │ • Timestamp, supervisor, task name, status │ │
│ │ • Duration, tokens used, scripts called │ │
│ │ • Outcome details, errors if any │ │
│ │ │ │
│ │ supervisor_logger.py → activity.jsonl │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ AGGREGATION LAYER │ │
│ │ • Hourly/daily/weekly rollups │ │
│ │ • Per-supervisor metrics │ │
│ │ • Token usage tracking │ │
│ │ • Success/failure ratios │ │
│ │ │ │
│ │ supervisor_metrics.py → daily/*.json, metrics_cache.json │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ OBSIDIAN │ │ DISCORD │ │ CLI │ │
│ │ DASHBOARD │ │ DIGESTS │ │ QUERY │ │
│ │ │ │ │ │ │ │
│ │ Real-time │ │ Daily summary │ │ Ad-hoc │ │
│ │ Weekly reports│ │ Weekly trends │ │ inspection │ │
│ │ │ │ Alerts │ │ JSON output │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Components
| Script | Purpose | CLI Usage |
|---|---|---|
supervisor_logger.py | Core logging module | Used by supervisor_base.py |
supervisor_metrics.py | Aggregation & rollups | --hours 24, --rollup, --trends 7, --cache |
supervisor_query.py | Ad-hoc queries | --supervisor ops, --failures, --summary |
supervisor_daily_digest.py | Daily Discord summary | --no-send for preview |
supervisor_weekly_report.py | Weekly trends | --no-send, --json |
Automated Schedules (n8n)
| Workflow | ID | Schedule | Output |
|---|---|---|---|
| SOS Metrics Cache | <WORKFLOW_ID> | Every 15 min | Refreshes metrics_cache.json |
| SOS Daily Digest | <WORKFLOW_ID> | 9 PM Pacific | Discord embed to #system-status |
| SOS Weekly Report | <WORKFLOW_ID> | Sunday 8 PM | Discord + Obsidian report |
Quick Reference
# Recent activity
python "005 Operations/Execution/supervisor_query.py" --last 20
# Check for failures
python "005 Operations/Execution/supervisor_query.py" --failures
# Summary statistics
python "005 Operations/Execution/supervisor_query.py" --summary
# 24h metrics
python "005 Operations/Execution/supervisor_metrics.py" --hours 24
# Generate daily rollup
python "005 Operations/Execution/supervisor_metrics.py" --rollup
# Preview daily digest
python "005 Operations/Execution/supervisor_daily_digest.py" --no-send
Alert Thresholds
| Metric | Warning | Critical | Action |
|---|---|---|---|
| Success Rate | < 80% | < 60% | Investigate failures |
| Avg Duration | > 30s | > 60s | Check for bottlenecks |
| Hourly Tokens | > 50k | > 100k | Review token usage |
| Failed Tasks/Hour | > 5 | > 10 | Immediate investigation |
Data Locations
- Activity log:
.tmp/supervisor_activity/activity.jsonl - Token usage:
.tmp/supervisor_activity/token_usage.jsonl - Daily rollups:
.tmp/supervisor_activity/daily/YYYY-MM-DD.json - Metrics cache:
.tmp/supervisor_activity/metrics_cache.json - Weekly reports:
005 Operations/Reports/supervisor-weekly-*.md
Related Resources
- Directive:
005 Operations/Directives/supervisor_observability.md - Dashboard:
005 Operations/Dashboards/Supervisor-Activity-Dashboard.md - n8n Workflows:
005 Operations/Workflows/sos-*.json
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Logging Module | Python classes, file I/O, JSON handling | ⭐⭐ |
| Metrics Aggregation | Data analysis, statistical calculations | ⭐⭐⭐ |
| CLI Tools | argparse, user interfaces, formatted output | ⭐⭐ |
| Discord Webhooks | API integration, rich embeds, error handling | ⭐⭐⭐ |
| n8n Workflows | Workflow automation, SSH triggers, scheduling | ⭐⭐⭐ |
| Obsidian Dashboards | Markdown, Dataview, information architecture | ⭐⭐ |
Relationship Intelligence Layer ⭐ NEW
Added: February 4, 2026
The Relationship Intelligence Layer is a unified, person-centric system that tracks relationship health across all QWU contexts (personal, BNI, fundraising, volunteers) using half-life decay curves. It ensures every human feels known, valued, and cared about ... regardless of which program brought them in.
Philosophy
"Your network is not your list of connections. It's the actual strength of actual relationships with people who would actually help you." ... Nate B Jones
Key Concepts
| Concept | Description |
|---|---|
| Warmth Score | Relationship strength (0-100) using half-life decay model |
| Vouch Score | Predicts who would advocate for you if asked |
| Reciprocity Ledger | Tracks social capital balance (investments vs returns) |
| Conversation Quality | Auto-detects spam, sales pitches, recruiting (20% noise filtered) |
| Relationship Classification | Human-defined types, donor status, programs, opportunities |
Warmth Bands
| Score | Band | Meaning |
|---|---|---|
| 80-100 | Hot | Active relationship, mutual investment |
| 60-79 | Warm | Healthy but needs periodic touchpoints |
| 40-59 | Cooling | At risk ... natural re-engagement window closing |
| 20-39 | Cold | Requires intentional reactivation |
| 0-19 | Dormant | Would need significant effort to revive |
Classification Taxonomy
Relationship Types: INNER_CIRCLE, BNI_MEMBER, GUILD_VOLUNTEER, PROFESSIONAL_COLLEAGUE, MENTOR, MENTEE, COMMUNITY_LEADER, INDUSTRY_CONTACT, VENDOR, SALES_TARGET, RECRUITER, IGNORE
Donor Status Lifecycle: NOT_APPLICABLE → PROSPECT → QUALIFIED → CULTIVATION → SOLICITED → PLEDGED → DONOR_FIRST → DONOR_REPEAT → DONOR_MONTHLY → DONOR_MAJOR → DONOR_LAPSED → CHAMPION
Program Affiliations: L4G, QWC, WOH, MP, GENERAL, SCHOLARSHIP, CAPITAL
Opportunity Types: 21 types including UPGRADE, MONTHLY_CONVERT, MAJOR_GIFT, L4G_SPONSOR, AMBASSADOR, REFERRAL_SOURCE
Scripts (005 Operations/Execution/)
| Script | Version | Purpose |
|---|---|---|
parse_linkedin_export.py | v1.1.0 | LinkedIn data export ingestion |
calculate_relationship_health.py | v1.1.0 | Half-life decay warmth scoring |
build_reciprocity_ledger.py | v1.0.0 | Social capital balance tracking |
calculate_vouch_scores.py | v1.0.0 | Advocacy prediction |
find_conversation_resurrections.py | v1.0.0 | Dormant thread hook detection (LLM) |
relationship_classification_schema.py | v1.0.0 | Classification taxonomy + DB tables |
classify_conversation_quality.py | v1.0.0 | Spam/sales/recruiting detection |
batch_classify_relationships.py | v1.0.0 | Markdown classification workflow |
generate_relationship_dashboard.py | v1.3.0 | Weekly dashboard with noise filtering |
Outputs
| Output | Location |
|---|---|
| Weekly Dashboard | 005 Operations/Dashboards/Relationship-Intelligence-Dashboard.md |
| Classification Queue | 000 Inbox/___Review/Relationship-Classification-Queue.md |
| Database | 005 Operations/Data/relationship_intelligence.db |
| Directive | 005 Operations/Directives/relationship_intelligence_layer.md |
Classification Workflow
- Run
batch_classify_relationships.py generateto create classification queue - Edit
000 Inbox/___Review/Relationship-Classification-Queue.mdin Obsidian - Add tags:
[BNI_MEMBER] [DONOR_REPEAT] [PROGRAM:L4G] [OPP:UPGRADE] - Run
batch_classify_relationships.py parseto update database - Regenerate dashboard to see filtered results
Data Sources
| Source | Status |
|---|---|
| LinkedIn connections (3,352) | ✅ Integrated |
| LinkedIn messages (3,842) | ✅ Integrated |
| LinkedIn endorsements | ✅ Integrated |
| LinkedIn recommendations | ✅ Integrated |
| Ezer memory (SMS, Discord) | ⏳ Planned |
| Calendar data | ⏳ Planned |
| BNI Epic Dossier | ⏳ Planned |
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| LinkedIn Export Parsing | Python data processing, CSV/JSON | ⭐⭐ |
| Decay Curve Math | Mathematical modeling, half-life calculations | ⭐⭐⭐ |
| Spam Classification | NLP basics, heuristic scoring, keyword matching | ⭐⭐ |
| Batch Processing Workflow | File parsing, database updates, CRUD operations | ⭐⭐⭐ |
| Dashboard Generation | Markdown templating, SQL queries, data visualization | ⭐⭐⭐ |
Parallel Execution System ⭐ NEW
The Parallel Execution System enables dependency-aware concurrent task execution across all major QWU pipelines, achieving 40-70% speedups through intelligent parallelization.
Architecture
The system uses a TaskGraph pattern that models tasks with dependencies:
[Independent Tasks - Run in Parallel]
├── Task A (no deps)
├── Task B (no deps)
├── Task C (no deps)
│
▼
[Dependent Tasks - Wait for Prerequisites]
├── Task D (depends: A, B)
├── Task E (depends: C)
│
▼
[Final Task]
└── Task F (depends: D, E)
Core Components
| Component | Purpose |
|---|---|
parallel_tasks.py | Core TaskGraph utility with dependency resolution |
ThreadPoolExecutor | Python standard library concurrent execution |
--dry-run flag | Preview execution plan without API calls |
Available Parallel Pipelines
| Pipeline | Script | Tasks | Speedup |
|---|---|---|---|
| Daily Briefing | daily_briefing_parallel.py | 6 collectors | 5x |
| L4G Lead Generation | l4g_parallel_pipeline.py | 8 tasks | 40% |
| Multi-Source Scraping | multi_source_scraper.py | 3-5 sources | 3x |
enrich_visitor_parallel.pybni_visitor_pipeline.py | see note below | n/a | |
| Content Pipeline | content_pipeline_parallel.py | 8 tasks | 60% |
| Member Enrichment | enrich_member_parallel.py | 7 tasks | 60% |
| Lead Enrichment | enrich_leads_parallel.py | 6 tasks | 60% |
| Appointment Analytics | appointment_analytics_parallel.py | 10 tasks | 3x |
Usage Examples
# Daily briefing - parallel collection
python 005\ Operations/Execution/daily_briefing_parallel.py --dry-run
# L4G pipeline with dependency graph
python 005\ Operations/Execution/l4g_parallel_pipeline.py \
--query "HVAC contractors" \
--location "Portland, OR" \
--dry-run
# Visitor enrichment ... RETIRED 2026-08-11, use the gated pipeline instead.
#
# `enrich_visitor_parallel.py` bypassed the identity confirmation gate on all
# four surfaces (LinkedIn profile, company website, Google Business listing,
# vault entity file) and still selected a Google business by taking the first
# search result unscored. It now refuses to run and points here.
#
# Its parallel design cannot hold the gate: a human confirmation sits between
# its phases, and you cannot parallelize past a person. The speed it existed
# for is also moot ... enrichment now STOPS at the gate and waits for an
# answer, so the wall clock is set by how long the answer takes.
python 005\ Operations/Execution/bni_visitor_pipeline.py --visitor "John Smith" --dry-run
# ...then answer the identity questions for that visitor:
python 005\ Operations/Execution/review_visitor_holds.py --show "John Smith"
# Lead enrichment with task selection
python 005\ Operations/Execution/enrich_leads_parallel.py \
--json-file .tmp/leads.json \
--enrichments friendly_name,email,reviews \
--dry-run
TaskGraph API
from parallel_tasks import TaskGraph, parallel_execute
# Simple parallel execution (no dependencies)
tasks = [
("task_a", lambda: {"success": True, "data": "A"}),
("task_b", lambda: {"success": True, "data": "B"}),
]
results = parallel_execute(tasks, max_workers=4)
# Dependency-aware execution
graph = TaskGraph(max_workers=5)
graph.add("fetch_data", fetch_data_func)
graph.add("process", process_func, depends_on=["fetch_data"])
graph.add("upload", upload_func, depends_on=["process"])
results = graph.execute(dry_run=False)
Key Files
- Directive:
005 Operations/Directives/task_management_rollout.md - Training Guide:
005 Operations/Directives/background_tasks_guide.md - Core Utility:
005 Operations/Execution/parallel_tasks.py
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| TaskGraph Pattern | Dependency graphs, topological sorting | ⭐⭐⭐ |
| ThreadPoolExecutor | Concurrent programming, thread safety | ⭐⭐⭐ |
| Dry-Run Pattern | Testing strategies, side-effect isolation | ⭐⭐ |
| Performance Profiling | Timing analysis, bottleneck identification | ⭐⭐⭐ |
QKN Quietly Knocking ⭐ NEW
Added: February 13, 2026
Quietly Knocking (QKN) is a multi-tenant AI-powered outbound outreach platform. It handles the complete pipeline from lead sourcing through personalized email campaigns to dynamic landing pages with variable-based personalization ... all integrated with the QWF product ecosystem.
Architecture
Lovable Frontend (quietlyknocking.org)
→ Supabase SDK
→ Supabase (<SUPABASE_PROJECT_ID>, us-west-1)
← n8n workflows (planned)
← Python scripts (existing lead gen + enrichment pipeline)
← Instantly API (Phase 1 sending)
Ecosystem Position
QKN is the outreach/conversion arm of the QWF product family:
- QWR → QKN: Brand voices + customer personas flow into sequence generation
- QKN → QSP: Campaign results, lead stage changes, and conversions push to SPOT dashboard
- Internal scripts → QKN: 14+ existing lead gen and enrichment scripts get multi-tenant wrappers
Key Concepts
- Campaign Math Calculator: User inputs desired leads/month → system reverse-engineers infrastructure (emails → accounts → domains → recommended tier)
- Landing Page Variable System:
{{variableName|fallback text}}syntax with 4-level resolution chain (lead data → campaign defaults → inline fallback → hide element) - Three Landing Page Modes: QKN-hosted, embeddable JS snippet, WordPress shortcode
- "We Hold Your Hand" Onboarding: QWF handles technical infra (DNS, DMARC, warmup), supporter owns strategic IP (voice, personas, messaging)
- 4-Week Warmup Sprint: Domain warmup timeline becomes structured onboarding cadence
Current State (February 13, 2026)
| Component | Status |
|---|---|
| Supabase project | ACTIVE_HEALTHY ... <SUPABASE_PROJECT_ID> |
| Domain | quietlyknocking.org ... Live on Lovable |
| Lovable preview | quietly-knocking.lovable.app |
| Schema | v1.0.0 deployed ... tenants, profiles, campaign_purposes |
| Auth | Configured ... autoconfirm, redirect URLs for all 3 domains |
| Lovable Prompt 001 | DEPLOYED ... landing page, auth, onboarding wizard, dashboard, app layout |
| [Supporter Organization] (tenant #1) | Planned ... migration from internal pipeline |
Pricing Tiers
| Tier | Monthly | Campaigns | Domains | Accounts | Leads/mo |
|---|---|---|---|---|---|
| Starter | $149 | 1 | 1 | 3 | 500 |
| Growth | $349 | 5 | 3 | 9 | 2,000 |
| Agency | $699 | Unlimited | 10 | 30 | 10,000 |
Reference
- GitHub Repo:
https://github.com/QuietlyWorking/quietly-knocking(Lovable-managed;ARCHITECTURE.mdat root) - System Status:
002 Projects/_Quietly Knocking/QKN-System-Status.md - Product Directive:
005 Operations/Directives/qkn_product.md - Foundation Prompt:
002 Projects/_Quietly Knocking/001-lovable-prompt-foundation.md
QSP Quietly Spotting ⭐ NEW
Added: February 13, 2026
Quietly Spotting (QSP) is a multi-tenant command center for small businesses. Your Single Point of Truth (SPOT) ... one dashboard that aggregates data from QWF products (QQT, QWR, QNT) and third-party tools into a modular, customizable dashboard.
Architecture
CF Pages Frontend (quietlyspotting.org → quietly-spotting.pages.dev)
→ Supabase SDK
→ Supabase (<SUPABASE_PROJECT_ID>, us-west-1)
← n8n workflows (hourly sync)
← Python scripts (sync_qqt_submissions.py, sync_qwr_articles.py)
← QQT Calculator API + QWR Agency API
Ecosystem Position
QSP is the aggregation hub of the QWF product family:
- QQT → QSP: Quote submissions sync hourly into
qqt_submissionstable - QWR → QSP: Article data syncs hourly into
qwr_articlestable - QKN → QSP (v2): Campaign results and lead conversions
- QNT → QSP (v2): BNI chapter data via Quietly Networking API
Multi-Tenancy Model
Shared Supabase database with tenant_id column + RLS (following QQT's proven pattern):
tenants... businesses (root entity)profiles... users within businesses (linked to auth.users)companies... sub-brands within a tenant (e.g., [Supporter Organization]'s 4 companies)modules... system catalog of available featurestenant_modules... which modules each tenant has enabledintegrations... external tool connections with encrypted credentials
Modules
| Module | Type | Dashboard Widget | Dedicated Page | Phase |
|---|---|---|---|---|
| Lead Pipeline | Built-in | Pipeline summary | /leads | v1 |
| QQT Connect | QWF product | Recent submissions | /quoting | v1 |
| QWR Connect | QWF product | Content status | /content | v1 |
| Contact Book | Built-in | ... | /contacts | v1 |
| Email Infrastructure | Third-party (Instantly + DMARC Report) | Email health summary | /modules/email-infrastructure | v2 |
| Campaign Manager | Third-party (Instantly) | Campaign metrics | /modules/campaigns | v2 |
Contribution Tiers
| Free | Starter ($29/mo) | Growth ($79/mo) | |
|---|---|---|---|
| Users | 2 | 3 | 10 |
| Modules | 3 | 5 | Unlimited |
| Companies | 1 | 1 | 4 |
| Third-party integrations | No | 2 | Unlimited |
Current State (March 27, 2026)
| Component | Status |
|---|---|
| Supabase project | ACTIVE_HEALTHY ... <SUPABASE_PROJECT_ID> (us-west-1) |
| Domain | quietlyspotting.org ... CNAME → quietly-spotting.pages.dev (Cloudflare proxied) |
| Hosting | Cloudflare Pages ... quietly-spotting project, GitHub Actions CI/CD (migrated from Lovable 2026-03-27) |
| GitHub repo | QuietlyWorking/quietly-spotting ... direct code commits, push-to-deploy |
| Schema | v5.0.0 ... 25 tables + confidence tracking on companies (11 core + bug_reports + 4 Phase 2 + 1 v3 func + 8 Phase 3e), RLS, triggers |
| Auth | Working ... sign-in, sign-up, redirect to /dashboard |
| Lovable Prompts | 34 total (001-034) ... ARCHIVED. All future changes are direct code commits. |
| Storage buckets | avatars (2MB) + logos (5MB) ... public read, auth upload |
| QQT sync | Active ... 12 submissions synced per tenant |
| QWR sync | Active ... 5 articles synced for [Supporter Organization] (v1.1.0) |
| Email Infra sync | Active ... 18 sending accounts + 12 DMARC domains synced |
| Campaigns sync | Active ... 1 campaign synced |
| Reviews sync | Tested ... 113 Google reviews synced for [Supporter Organization], 8 flagged. v1.1.0: confidence gate (skips low-confidence auto-matches) |
| KPI snapshots | Tested ... nightly aggregation across 6 categories |
| Alert engine | Tested ... rule eval + SMS/Discord/in-app dispatch |
| n8n workflows | 4 active ... QQT 1h, QWR 1h, Email Infra 2h, Campaigns 4h |
| QWF test tenant | b28692ad ... growth plan, 4 modules |
| [Supporter Organization] tenant | <TENANT_ID> ... growth plan, 4 companies, 6 modules, 113 reviews, 2 team accounts |
| supporter-CC migration | Executed 2026-03-17 ... 6 accounts, 6 domains, 3 campaigns ported |
| Phase 3e | Executive Pulse DEPLOYED. Schema v4 (8 tables). Role system upgraded (Owner>Admin>Manager>Viewer). Prompts 032-033 deployed. |
| Phase 3f | Place ID Verification. Schema v5 deployed (confidence tracking). lookup-place-id edge function deployed. Prompt 034 ready. Backend confidence gate active. |
| Phase 4 (Active) | Local Growth Engine ... BrightLocal API integration (citation sync live), citation health dashboard, geo-grid rank visualization, GBP audit/optimization queue, schema markup generator, on-page SEO audit (Lighthouse), monthly cross-product performance report. Agency white-label architecture baked in (nullable agency_id + brand_config). 4 Supabase tables deployed. First [Supporter Organization] citation sync complete (baseline synced). Directive: qsp_local_growth_engine.md. |
Local Growth Engine (Active ... April 2026)
New QSP module providing local SEO services to all supporters. Replaces $1,500-3,000+/mo external SEO agency with built-in capabilities.
External Integration: BrightLocal Track agency account. Current plan (since 2026-10-28): Track, 2-5 locations, monthly billing ($62/mo), renews on the 28th; 1 location in use ([Supporter Organization]). The account was deactivated 2026-10-21 when the card on file was removed and the renewal failed; TIG reactivated it 2026-10-28 and every report, schedule and history survived. Before that: Track 11-20 monthly from 2026-04-21 ($99, then $104 from 2026-07-21), which never held more than 1 location. L4G stays out of BrightLocal unless it gets a Google Business Profile (ruling pending in the L4G search lane). Price list and gotchas: brightlocal_tool_wisdom.md READ FIRST. QWF absorbs cost. No nonprofit discount available (confirmed via support email thread, Session 195).
BrightLocal API Status (as of May 8, 2026):
- Single API key on all three hosts:
tools.brightlocal.com/seo-tools/api/v2/,/v4/, andapi.brightlocal.com/manage/v1/ - Three deprecation map corrections from [Vendor Support] (BrightLocal Sales) on 2026-04-30 collapsed the earlier "Data API agreement" framing as a misconception ... the Track-plan key has live Management API access, no separate agreement needed
- All five sync modules now wired in
sync_brightlocal_data.pyv2.0.0: citation health (/v2/ct/get-results), keyword rankings (/v4/gpw/{id}/results.keywords), GBP audit (/v4/gpw/{id}/results.summary+ locally-computed completeness score), multi-site reviews (/manage/v1/rm/reports/{id}/reviews, paginated 20/page fixed), composite SEO health (locally-computed 25/35/25/15 weighted from the four component scores) - One open question with [Vendor Support] (on annual leave per [Vendor Support] 2026-05-05): LRT rankings-fetch path. Workaround:
/v4/gpw/{id}/results.keywords[*].client_rankcarries the same data and is sufficient for QSP - Note: GBP Post Scheduler is Manage-tier+ only (correction from original 2026-04-02 research). Vista Social remains Plan B for QWR → GBP routing
BrightLocal Track Features Used:
- Citation Tracker (NAP consistency across 50-300+ directories)
- Local Search Grid (geo-grid rank tracking across service area)
- Local Rank Tracker (weekly keyword position tracking)
- GBP Audit (profile completeness scoring + recommendations)
- GBP Post Scheduler (publishes directly to Google Business Profile)
- White-label reporting (QWF-branded reports)
- API access (pay-per-request: ~$3/mo per location)
Built-In Features (No External Cost):
- Schema Markup Generator ... JSON-LD from QSP business data (LocalBusiness, Service, FAQ, Review, BreadcrumbList, Organization)
- On-Page SEO Audit ... Google Lighthouse API crawl with prioritized fix list
- Monthly Performance Report v2 ... Supporter-deliverable PDF with branded supporter cover (centered logo, supporter brand color cascade), Local SEO Performance section (5 cards: composite SEO Health, Citation Health, Keyword Rankings, GBP Profile Snapshot, Multi-Site Reviews) backed by per-card 6-month TrendCharts (vanilla SVG, stoplight zones, smart-label dedup), tier-aware content sections (TransparencySection, HighlightCallouts, ActionChecklistV2 with purple QWF / supporter-brand-color ownership badges), period-bounded BL queries that respect the period selector, full chrome suppression in print, page numbers via
@pagecounters. Service tier on tenants determines the engagement-model framing per supporter (vospa / diy / hybrid) - GBP Optimization Queue ... Actionable task cards from BrightLocal audit data
- Local Keyword Matrix ... Service x city keyword combinations via QWR DataForSEO
Agency White-Label Architecture (Built Into Phase 4, Launched Later):
All new tables include nullable agency_id column. Reports use brand_config object (logo, name, colors). RBAC scoped by agency_id + organization_id. Enables future Tier 2 Agency supporters (e.g., [Supporter Company]) to white-label the entire QWF stack.
Supabase Tables ... Deployed (Session 195):
qsp_citation_health... Directory-level NAP consistency (created via Management API)qsp_local_rankings... Keyword positions + geo-grid data (created via Management API)qsp_gbp_audit... GBP completeness score + recommendations (created via Management API)qsp_seo_health... Overall local SEO health score (created via Management API)
Supabase Tables ... Planned:
qsp_onpage_audit... Lighthouse audit results per URLqsp_monthly_reports... Generated report data + PDF referencesqsp_schema_output... Generated JSON-LD per locationagency_brand_config... White-label branding per agency | [Supporter CRM] sync | Active ... thousands of jobs synced for [Supporter Organization]. Safety gate + audit logging + soft-delete pattern.sync_audit_logtable deployed (immutable, RLS-protected). | | QWF Passport | CLOSED 2026-10-20 ...generate-crossover-token(QSP) +verify-crossover-token(QWR, QQT, QNT) still deployed, but each checker now holds its own randomQWF_SSO_SECRET, so every pass is refused until the durable fix (QNT System Status, top security section) | | Contact Form | Deployed ...submit-contact-formedge function + centralized pipeline | | Landing Page | Deployed ... Prompt 011 with heritage, ecosystem, contact sections |
Data Sync Pipeline
Each tenant stores QQT/QWR API keys in the integrations table. Python sync scripts iterate all connected tenants, call upstream APIs, and upsert into QSP tables. n8n workflows trigger the scripts hourly via SSH to claude-dev.
| Script | Upstream API | Target Table | Schedule |
|---|---|---|---|
sync_qqt_submissions.py | QQT Calculator API /v1/submissions | qqt_submissions | Every 1h |
sync_qwr_articles.py | QWR Agency API /v1/brands/{id}/articles | qwr_articles | Every 1h |
sync_qsp_sending_accounts.py | Instantly API v2 /accounts | sending_accounts | Every 2h |
sync_qsp_dmarc_domains.py | DMARC Report API v2 /all_domains.json | dmarc_domains | Every 2h |
sync_qsp_campaigns.py | Instantly API v2 /campaigns | campaigns, campaign_metrics | Every 4h |
sync_qsp_reviews.py | Google Maps via Apify | reviews | Every 6h (cron stopgap; n8n still pending ... see HQ issue 935bb95b) |
compute_kpi_snapshots.py | All QSP source tables | kpi_snapshots | Nightly (pending n8n) |
check_qsp_alerts.py | alert_rules → alert_history | SMS, Discord, in-app | Post-sync (pending n8n) |
<supporter-CRM sync script> | supporter CRM API | <supporter_crm_jobs>, <supporter_crm_appointments> | Every 4h (v1.1.0, safety gate integrated) |
<supporter-CRM extract script> | supporter CRM API | CSV output (.tmp/) | Manual (v1.0.0, warm campaign extraction) |
sync_brightlocal_data.py | BrightLocal CT Data API | qsp_citation_health | Manual (v1.0.0, first [Supporter Organization] sync complete) |
sync_safety_gate.py | Pre/post-sync validation module | sync_audit_log | Called by sync scripts |
All scripts support --dry-run and --tenant-id flags. [Supporter CRM] sync includes --force flag to bypass the safety gate (row-count sanity check blocks syncs where incoming rows < 50% of existing). The <supporter-CC migration script> script was a one-time migration from supporter-CC to QSP [Supporter Organization] tenant (executed 2026-03-17).
[Supporter CRM] Custom Fields API (discovered 2026-04-05): [Supporter CRM] released custom fields support on 2026-03-30 (API v2.2607.0). Endpoints: GET/PUT /api/v2/contacts/{id}/custom-fields, GET/PUT /api/v2/jobs/{id}/custom-fields, GET /api/v2/company-settings/custom-fields. Webhooks: contact.custom-field.value_changed, job.custom-field.value_changed. [Supporter Organization] has 1 job-level field ("Confirmer"). No contact-level DNC field yet ... recommendation pending with [Supporter Team Member].
Reference
- GitHub Repo:
https://github.com/QuietlyWorking/quietly-spotting(CF Pages, direct code commits;ARCHITECTURE.mdat root) - CF Pages Project:
quietly-spotting→quietly-spotting.pages.dev - System Status:
002 Projects/_Quietly Spotting/QSP-System-Status.md - Product Directive:
005 Operations/Directives/quietly_spotting.md - Lovable Prompts (archived):
002 Projects/_Quietly Spotting/lovable-prompts/001-034 - Sync Scripts:
005 Operations/Execution/sync_qqt_submissions.py,sync_qwr_articles.py,sync_qsp_sending_accounts.py,sync_qsp_dmarc_domains.py,sync_qsp_campaigns.py,sync_qsp_reviews.py,sync_brightlocal_data.py,compute_kpi_snapshots.py,check_qsp_alerts.py - Migration Script:
005 Operations/Execution/<supporter-CC migration script>(one-time, executed 2026-03-17) - Safety Module:
005 Operations/Execution/sync_safety_gate.py(pre/post-sync validation, batch checking, audit logging) - Data Safety Directive:
005 Operations/Directives/supporter_data_safety.md(foundational ... live supporter data handling) - Edge Functions:
generate-crossover-token(QWF Passport),submit-contact-form(contact pipeline),lookup-place-id(Google Place ID search with confidence scoring)
QNT Quietly Networking ⭐ NEW
Added: February 13, 2026
Quietly Networking (QNT) is a multi-tenant AI-powered chapter management platform for BNI networking groups. It provides visitor enrichment, connection reports, meeting intelligence, relationship health scoring, inviting engine, growth analytics, Stripe billing, community events, and AI-generated recaps ... built on a CF Pages + Supabase + claude-dev AI processing engine stack.
Architecture
CF Pages Frontend (React, Dark Mode) — quietlynetworking.org
→ Supabase SDK
→ Supabase (<SUPABASE_PROJECT_ID>, us-west-1)
← Edge Functions (enrich-visitor, verify-crossover-token, Stripe webhooks)
← FastAPI Webhook Receiver (qnt.quietlyworking.org:8100)
← Python AI Pipeline (Apify + Claude Opus 4.7)
Ecosystem Position
QNT is the BNI chapter management arm of the QWF product family:
- QNT → QSP: Chapter data, visitor stats, and referral metrics feed the SPOT dashboard
- [Networking Chapter] → QNT: First alpha tenant (27 members), production test environment
- Shared Infrastructure: Enrichment scripts inherited from [Networking Chapter] backoffice (65+ scripts)
Key Features (46 Lovable Prompts)
| Phase | Feature | Prompts |
|---|---|---|
| Foundation | Auth, dashboard, dark mode, nav | 001 |
| Members | CRUD, directory, profiles, import | 002 |
| Visitors | Registration, enrichment, connection reports | 003 |
| Impersonation | View As for super admin | 004 |
| Meetings | Management, processing, AI recaps | 005-006 |
| 1-to-1s | Scheduling, briefings, recording, AI analysis | 007-008 |
| Relationships | Health dashboard, matrix heatmap, warmth bands | 009 |
| Engagement | Nudges, decay alerts, recalculation | 010 |
| Public Presence | Business cards, directory, visitor landing pages | 011-012 |
| Inviting | AI matching, funnel tracking, growth leaderboards | 013-014 |
| Billing | Stripe Checkout, trials, usage metering | 015 |
| Onboarding | Self-service chapter signup, wizard | 016 |
| Analytics | Chapter analytics (4-tab), AI growth insights | 017-018 |
| Events | Community events, RSVP, recurring, recaps | 019-020 |
| Timezone Fix | Pacific timezone utility for all date calculations | 021 |
| Roster Sync | Notification bell, roster source settings, auto-sync | 022 |
| Bug Fixes | Auth display, dashboard data loading, notification wiring | 023 |
| Landing Page | 11-section marketing page, TIG voice, QWF framing | 024 |
| Settings | Chapter info, meeting format config (JSONB) | 025 |
| Members UX | Remove Inactive→Alumni, clickable cards, contact icons | 026 |
| Epic Profiles | Enrichment data rendered as rich profile sections | 027 |
| Historical Import | CSV upload, column mapping, batch insert | 028 |
| Alpha Polish | Alpha badge, bug reporter, alpha gate on landing page | 029 |
| Branding | Official logo, fern icon, favicon suite, OG/Twitter meta | 030 |
| Heritage | "Tested. Proven. Now Yours." heritage section | 031 |
| Newsletter | 3-step composer, 11 section types, template management | 032-033 |
| Recognition | "You Got Caught" member appreciation system | 034 |
| Web Archive | Public newsletters, Chapter Impact stats, recognition widget | 035 |
| Landing Refresh | MissionBanner, Ecosystem, Contact, updated features/pricing | 036 |
| Botanical Palette | Logo-derived Forest/Fern/Bud color system, migraine-friendly | 037-039 |
| Sticky Header | Warm cream nav bar, sticky header architecture (QWR pattern) | 040 |
| Speaker Management | Meeting templates, speaker queue, materials, planning timeline, artifacts | 041-045 |
| Landing Page Update | Speaker management features reflected in all landing page sections | 046 |
Support Tiers
| Connect ($49/mo + $5/member) | Grow ($99/mo + $7/member) | |
|---|---|---|
| Core features | Yes | Yes |
| WordPress plugin, business cards, landing pages | No | Yes |
| Inviting engine, growth analytics | No | Yes |
Backend Brain
The AI processing engine runs on claude-dev with a FastAPI webhook receiver:
- Webhook URL:
qnt.quietlyworking.org→ port 8100 (systemd:qnt-webhook.service, v1.4.0) - Visitor enrichment: LinkedIn lookup → profile → website → reviews → Claude Opus 4.7 synthesis. Idempotency guard:
/enrich-visitorand/enrich-memberskip enrichment ifenrichment_status == 'complete'(passforce=trueto override). - Connection reports: AI-generated per-member reports for each visitor
- Meeting pipeline:
qnt_meeting_pipeline.py... chat parsing, artifact download, Vision slide analysis, recap generation - Presentation media:
process_presentation_media.py... PDF/PPTX/video→images for Vision analysis
Current State (April 9, 2026)
| Component | Status |
|---|---|
| Supabase project | ACTIVE_HEALTHY ... <SUPABASE_PROJECT_ID> |
| Hosting | CF Pages ... quietlynetworking.org (migrated from Lovable 2026-03-20) |
| Deploy pipeline | Push to main → GitHub Actions → CF Pages auto-deploy |
| Supabase auth site_url | Updated to https://quietlynetworking.org |
| Backend brain | Deployed ... visitor enrichment + roster sync + newsletter + meeting pipeline end-to-end |
| Webhook receiver | v1.4.0 ... idempotency guard on /enrich-visitor and /enrich-member (skips if enrichment_status == 'complete') |
| QWF Passport | CLOSED 2026-10-20 ... verify-crossover-token on QNT Supabase refuses every pass (its own random QWF_SSO_SECRET); it had auto-created confirmed logins for any free Quietly Spotting account |
| Stripe | Configured (TEST MODE) ... 2 products, 4 prices, webhook |
| Alpha tenant | [Networking Chapter] ... 27 members (17 active, 2 on leave, 8 alumni), 2,373 historical visitors imported |
| Timezone fix | Prompt 021 deployed ... 8 affected areas fixed |
| Database | 35 tables, 16 migrations, full RLS (added: chapter_testimonials, card_shares, card_clicks; per-member privacy columns testimonials_enabled, card_shareable, card_redirect_url) |
| Newsletter system | 3-step composer, 11 section types, template management, send-newsletter edge function |
| Recognition engine | "You Got Caught" member appreciation with public web archive |
| Speaker management | Meeting templates, speaker queue, materials collection, planning timeline, message sequences, dues tracking |
| Landing page | Botanical palette, warm cream nav, full-color logo, contact form, 50+ features showcased |
| UX clickability | Dashboard MetricCards, visitor rows, relationship pairs, leaderboard names, inviter names ... all clickable with intuitive navigation (Apr 9, 2026) |
| Visitor Epic Profiles | Enrichment data rendered on Overview tab ... summary, services, power teams, connection hooks, life hints, reviews with stars/trends, LinkedIn card, personality traits (Apr 9, 2026) |
| Connection reports | Markdown-rendered via react-markdown, RLS super admin bypass, correct column mappings (Apr 9, 2026) |
| Card upload | Members can upload own card image (JPG/PNG/WebP, 5MB) via drag-and-drop on card builder (Apr 9, 2026) |
| Permission controls | Edit controls respect impersonation (useEffectiveUser), card builder has ownership check (Apr 9, 2026) |
| Alpha readiness | Alpha badge, bug reporter, landing page alpha gate (Prompt 029) |
| Branding | Custom logo, fern icon, favicon suite, botanical palette deployed |
| [Networking Chapter] website | <chapter-domain> ... Astro 5 SSR on CF Pages (migrated from React SPA 2026-04-09), multi-tenant middleware, live data from QNT Supabase. Client JS: ~6KB (form islands only). |
| Business cards | generate_business_cards.py ... Pillow-based card generator, 18 cards, trackable sharing via card_shares/card_clicks tables |
| Card sharing | /card/[slug] Astro API route with OG meta tags for social previews, increment_card_share_clicks RPC |
| Form backends | /api/rsvp (→ visitors table), /api/testimonial (→ chapter_testimonials table, slug→UUID resolution, author→member matching, triggers notification) ... Astro API routes with CF runtime env |
| Testimonial flow | "Write a Note" clickable card on member profiles → optional email → API resolves slug/author → Postgres trigger creates notification → member approves in QNT My Testimonials page → "Show on profile" toggle → appears on website. Reciprocal: "Say thanks" email + "Write one back" link for fellow members (Apr 13, 2026) |
| My Testimonials | QNT page (/my-testimonials): pending review cards, approve/dismiss, profile visibility toggle, source badges (Website, BNI Review, Google, LinkedIn, Yelp). Notification bell targeting via target_member_id (Apr 13, 2026) |
| Meeting preview | /meeting ... agenda timeline, visitor host promise, FAQs with expandable answers (Apr 13, 2026) |
| Events page | /events ... scattered Polaroid photo layout (alternating sides), "Here's what you missed" past events section with photo stacks. /events/[slug] ... detail pages with photo gallery, Google Maps embed, schema.org Event structured data (Apr 13, 2026) |
| Event photos | Upload promo photos in QNT EventForm, recap photos in EventDetail Photos tab. event-images storage bucket (public, 10MB, jpg/png/webp). Hero/gallery/recap cascade to public site. Series inheritance via "Apply to all" toggle (Apr 13, 2026) |
| SEO profession pages | /find ... directory of 18 professions. /find/[profession] ... auto-generated landing pages per profession with member profiles, testimonials, contact links. Targets "find a [profession] in Southern California" queries (Apr 13, 2026) |
| Recurring event mgmt | "Apply changes to entire series" toggle in QNT EventForm ... updates name, time, location, format, visibility, photos across all siblings. useUpdateRecurringSeries mutation (Apr 13, 2026) |
| Per-member privacy controls | Three independent boolean toggles on members (default true): public_profile (existing ... appearance on public chapter website), testimonials_enabled (testimonial form + display), card_shareable (digital business card sharing). Plus card_redirect_url text ... fallback URL when card sharing is off. Admin via MemberForm Privacy section; member self-service via MyTestimonials (testimonials) and BusinessCardBuilder (card). Self-service uses two SECURITY DEFINER RPCs: set_my_testimonials_enabled(boolean) and set_my_card_settings(boolean, text) ... both authenticated-only, no broader UPDATE on members. Website enforcement: [slug].astro hides TestimonialForm, /api/testimonial returns 403, public_chapter_testimonials view filters approved testimonials about disabled members, /card/<slug> 302-redirects to card_redirect_url (clean URL ... ?s= token stripped) or 404s. Click tracking + sharer attribution preserved through redirects. public_profile audit closed two leaks (fetchMember, fetchUpcomingSpeakers) in aim-high-bni/src/lib/supabase.ts. First user: [Member Name] flipped all three off for regulatory compliance (Apr 16, 2026) |
| Testimonial author bylines | Member profile testimonial cards now display " ... Author Name" below each quote. Previously the author was discarded by mapping to from: null. Legacy renewal-review entries renamed from "Anonymous Member" → "Anonymous Fan" (14 rows) for warmer attribution (Apr 16, 2026) |
| Notification dropdown scrolling | Replaced Radix <ScrollArea max-h-[400px]> with native <div className="max-h-[400px] overflow-y-auto">. The Radix viewport's h-full requires explicit parent height, not max-h ... without it, overflow never engages and the scrollbar never appears. Native scroll is the right fit for popover lists (Apr 16, 2026) |
| Hand-written type drift fixes | Aligned src/types/database.ts Member interface with actual DB columns: instagram → instagram_handle, joined_date → join_date, last_enriched_at → enriched_at. Saving member edits had been crashing with "Could not find the X column"; reads silently returned undefined (Instagram links never rendered, member tenure showed " ... ", enrichment recency pill missing) (Apr 16, 2026) |
Reference
- GitHub Repo (QNT):
https://github.com/QuietlyWorking/quietly-networking(CF Pages source; push tomain→ GitHub Actions → auto-deploy) - GitHub Repo ([Networking Chapter]):
https://github.com/QuietlyWorking/aim-high-bni(CF Pages; push tomain→ GitHub Actions → auto-deploy) - Production URL:
https://quietlynetworking.org - [Networking Chapter] URL:
https://<chapter-domain>(CNAME →aim-high-bni.pages.dev) - System Status:
002 Projects/_Quietly Networking/QNT-System-Status.md - Backend Scripts:
005 Operations/Execution/qnt_webhook_receiver.py(v1.4.0),qnt_visitor_pipeline.py,qnt_roster_sync.py,qnt_import_historical_visitors.py,qnt_newsletter_pipeline.py,qnt_meeting_pipeline.py,process_presentation_media.py,generate_business_cards.py - Edge Functions:
enrich-visitor,verify-crossover-token(QWF Passport),sync-roster,create-checkout-session,create-portal-session,stripe-webhook,sync-member-count,send-newsletter,submit-contact-form - Astro API Routes (aim-high-bni):
/card/[slug](OG card sharing + click tracking),/api/rsvp(visitor registration),/api/testimonial(testimonial submission) - Supabase RPC:
increment_card_share_clicks(atomic click counter for card shares),set_my_testimonials_enabled(boolean)(member self-toggle for testimonials_enabled),set_my_card_settings(boolean, text)(member self-toggle for card_shareable + card_redirect_url, validateshttps?://prefix) - CF Pages Env Vars (aim-high-bni):
SUPABASE_URL,SUPABASE_ANON_KEY,AIM_HIGH_ORG_ID,QNT_SERVICE_ROLE_KEY(runtime SSR + API routes)
Solo tier design and the Passport close (2026-10-20, Session 763)
TIG asked for a solo tier: the personal Catch features (capture, research, a follow-up series, all on the phone) for networkers with no chapter, starting with friends of his who have no CRM. The session measured QNT's monthly cost at the vendors' own records (about $70 cash, about $110 fully loaded; a new contact $0.60 to $0.80), ran a four-reviewer review that found and confirmed a live side door (a free Quietly Spotting account could sign into QNT, QWR and QQT), closed it on TIG's word by giving each checking app its own unrecorded secret, and inventoried all 179 QNT features for TIG's feature pick. Design of record: 002 Projects/_Quietly Networking/Design Docs/QNT-Solo-Tier-Brief-v0.1.md and QNT-Solo-Tier-Feature-Picks-v0.1.md; channel qnt-solo-tier-20261020-1927-3b2637.
MP Training Opportunities (from the 2026-10-20 QNT solo-tier session, Session 763)
| Skill / Pattern | Why It Teaches | Difficulty |
|---|---|---|
| Price one unit from the code's call count times the vendor's billing rule | The first estimate averaged a run's cost and assumed one search per capture; the code runs 3 to 4, and each bills even when empty, so the real cost was about double. Teaches reading the code path before multiplying | Intermediate |
| A capability claim needs a delivered instance, not a built engine | "Already has a follow-up series" was written from "the engine exists" while nothing had ever sent. Teaches checking for a sent row before a value pitch | Beginner |
| Compare secrets without ever seeing them | The side door was proven, and its close verified, by comparing the SHA-256 digests the API returns for each project's secret; no value was printed or stored. Teaches proof without exposure | Intermediate |
| Build an inventory with a mark on every row | 179 features, each with status, source line and VERIFIED / ARTIFACT-ASSERTED / INFERRED, then three load-bearing rows re-read at the code. Teaches evidence discipline at scale | Intermediate |
Most valuable MP exercise: give a student a small capture pipeline's code and a vendor price page, and ask "what does one capture cost us?" The averages in the dashboard point one way; the loop in the code points the other.
Live Demo Mode and the landing embeds (added September 2026)
https://quietlynetworking.org/demo runs the real app on a sample chapter with no login and no server. One seam (src/lib/createAppClient.ts) hands both Supabase client modules a browser-side PostgREST engine, so every screen renders from fixtures in the tab and nothing reaches the live database. The landing page embeds that demo in all nine feature boxes: each box is the app rendered at laptop width and scaled into the box, mounted only while it is near the viewport, with a captured screenshot behind it and, on phones, instead of it. "Make it yours" on the page (chapter name, colours, first name, seat, browser-only logo, dark or light mode) re-dresses every frame without a reload, and the page URL carries the chapter name and colours only, so a shared link opens already personalized. Three "Show me" tours run inside the frames; a seat switch (President / Visitor Host / Mentor / Member) shows each seat's real access.
Operating rules for the backoffice:
| Rule | How |
|---|---|
| Prove nothing leaves the browser | scripts/demo_zero_egress.py --base <url> --landing (in the app repo, backoffice venv Playwright). Add --allow-host static.cloudflareinsights.com on the custom domain only; never on *.pages.dev. |
| Prove the page form works | scripts/demo_personalization_check.py --base <url> --landing (17 checks) and without --landing for the in-demo form (18 checks). |
| Recapture the posters after any demo-affecting change | Build, vite preview --port 4173, then scripts/capture_demo_shots.py --base http://localhost:4173, commit public/demo-shots/. CI has no Playwright, so this is a backoffice step. --check exits 1 when a source commit is newer than the shots. |
| Disclosure copy | Name the sample PEOPLE, never the visitor's chapter: "{chapter}, with sample members and visitors. The software is real." |
| Icons | No sparkle, brain, rocket, bot, gem, star, zap, target or lightbulb anywhere in a QWF surface (detect_ui_slop.py flags the imports). |
Full design of record: 002 Projects/_Quietly Networking/QNT-System-Status.md §Live Demo Mode.
Audit trail ... who changed what, when, from where, and why ⭐ NEW
Added: September 25, 2026 (first implementation of 005 Operations/Directives/qwf_audit_log_standard.md)
The QNT database now writes a permanent record of every change itself, in the same instant as the change. No page, script or agent can skip it, and nobody (not even the service key) can edit or delete a record.
| What | Where |
|---|---|
| The record | audit.trail in the QNT database (schema audit, never exposed by the API) |
| Which tables | audit.table_policy: 51 audited, 4 exempt with a written reason |
| Who / where it came from | a person's login (jwt), our servers asserting the person (asserted, headers x-qwf-actor / x-qwf-surface), or the operator's SET LOCAL qwf.* (context); unknown is written unattributed |
| Browser | src/lib/createAppClient.ts adds the page's surface to every REST call |
| Edge functions | supabase/functions/_shared/audit.ts (19 functions) |
| Backoffice scripts | 005 Operations/Execution/qwf_audit_headers.py; safe_run.sh marks scheduled runs qnt.cron.* |
| Reading it | History button on each pairing, History on member and visitor pages, the /activity page, the dashboard feed |
Operations:
- Prove any change to the trigger or a named action first:
.venv/bin/python "005 Operations/Execution/probe_qnt_audit_trail.py" --migration <file>(rolled back, changes nothing). - Re-check live:
probe_qnt_audit_trail.py(no flag) andqnt_audit_smoke.py(real, reverted writes on the test fixture). - A new table: add
SELECT audit.enroll('table');(oraudit.exempt('table', 'why')) to the migration that creates it. - If the record ever blocks saving:
002 Projects/_Quietly Networking/migrations/040-audit-trail-EMERGENCY-OFF.sql.
MP Training Opportunities (from the 2026-09-25 audit-trail build)
| Task | Skills taught | Prerequisites | Time | Level |
|---|---|---|---|---|
| Read a record's history and explain each row (who, source, surface) | SQL reading, audit concepts | basic SQL | 1 h | Beginner |
| Write a rolled-back proof for a database change | transactions, test design, Postgres roles | SQL, Python | 3 h | Intermediate |
| Trace one action from a button to the trail row (browser header -> PostgREST -> trigger) | web requests, HTTP headers, triggers | JavaScript basics | 2 h | Intermediate |
| Adversarial review: find how a record could lie (forged actor, leaked label, side-door read) | security thinking, RLS | Postgres RLS | 4 h | Advanced |
Member access ... each member chooses who sees each piece (migration 060) ⭐ NEW
Added: October 29, 2026 (the expand step of the QNT member launch; no screen uses it yet)
The QNT database now holds each member's own choices: for each piece of their profile (name, business, photo, phone, email, links, their old visitor record and so on), whether everyone, fellow members, or only the member sees it. Phones start hidden from fellow members; everything else starts where the public page already was. Only the member changes their choices, and a piece a member removes stays empty against every writer but the member.
| What | Where |
|---|---|
| The choices | columns on members (share_everyone, share_members, member_removed, choices_confirmed_at), kept by the guard trg_members_guard_member_choices |
| What a signed-in person reads | the views member_directory, visitor_records, testimonial_records, meeting_records and newsletter_edition_records: each applies the choices, and masks a hidden phone or email inside free text too |
| The public page and the website | public_members and the other public_* views, on the same choices (unchanged on the day 060 applied) |
| Who reads the chapter at all | is_chapter_reader: a current member of the chapter, its owner or an admin, or a super admin |
Operations:
- Prove the live schema:
.venv/bin/python "005 Operations/Execution/probe_qnt_member_access.py"(rolled back, changes nothing; 120 checks as synthetic people). - Prove a change before applying it: the same script with
--migration <file>; adding--rollback <its ROLLBACK file>proves the undo too. - The undo works only before first use: once any member makes a choice, an edit, a removal, a link or a read mark, every fix goes forward.
- The live state in counts:
005 Operations/Handoffs/rounds/qnt-member-launch-20261027-1307-f5a71f/assets/member_launch_state.sql.
MP Training Opportunities (from the 2026-10-29 member access build)
| Task | Skills taught | Prerequisites | Time | Level |
|---|---|---|---|---|
| Write an expand step for an access change: add the new beside the old, then prove "nothing changed" row by row | migrations, views, set comparison (EXCEPT both ways) | SQL, Postgres RLS | 4 h | Advanced |
| Before asking for a go, count who loses what per login, people and test logins apart | impact analysis, SQL counting | basic SQL | 1 h | Intermediate |
QWR Content Performance Intelligence ⭐ NEW
Added: February 14, 2026
Content Performance Intelligence closes the feedback loop for QWR articles: "Is what I'm writing actually working?" It captures baselines at onboarding, scores every article across 4 dimensions, tracks SERP positions weekly, and monitors AI citation rates bi-monthly.
Architecture
ARTICLE GENERATION (existing)
│
▼
ENHANCED SCORING (qwr_content_scorer.py)
├── Quality Score (existing 7 dimensions)
├── SEO Score (keyword placement, density, headings — mechanical)
├── GEO Score (structure, extractable claims, authority — hybrid LLM)
└── Platform Score (platform_specs compliance — mechanical)
│
▼
POSITION TRACKING (qwr_position_tracker.py — weekly)
├── SERP Position via DataForSEO
├── Baseline detection (first snapshot = baseline)
└── Trend: improving / stable / declining / new / lost
│
▼
AI CITATION TRACKING (qwr_citation_tracker.py — bi-monthly)
├── Perplexity Sonar via OpenRouter
├── Share of Model calculation
└── Monthly summary aggregation
│
▼
PROGRESS DASHBOARD + RECOMMENDATIONS (Lovable)
├── Position Tracker (sparkline charts, baseline → current)
├── AI Citation Scorecard (citation rate, Share of Model)
├── Content Score Trends (SEO/GEO/Platform over time)
├── Platform Effectiveness (per-platform optimization scores)
└── "What to do next" recommendations (Phase 5 — future)
Components
| Component | Type | Status |
|---|---|---|
qwr_content_scorer.py | Python script | Deployed ... SEO (mechanical) + GEO (hybrid LLM) + Platform (mechanical) |
qwr_position_tracker.py | Python script | Deployed ... DataForSEO SERP tracking with baseline detection |
qwr_citation_tracker.py | Python script | Deployed ... Perplexity Sonar via OpenRouter, 3-method citation extraction |
| SQL migration | Supabase | Deployed ... 3 tables, 2 columns, 2 views, RLS policies |
qwr-position-tracker.json | n8n workflow 7LXfOraJ2HevC8FF | Published ... Sunday 8am UTC |
qwr-citation-tracker.json | n8n workflow CgWmYlv0gzuJMfaO | Published ... 1st and 15th at 9am UTC |
| Article Gen scoring hook | n8n (SSH node on gDPgfxRqHBDvPfoa) | Deployed ... scores every new article after generation |
| Enhanced Article Scores | Lovable prompt | Executed ... 4 score rings on Article Detail sidebar |
| Performance Dashboard | Lovable prompt | Executed ... /performance page with 5 sections |
| Onboarding Baseline | Lovable prompt | Executed ... baseline capture after onboarding |
| Docs Center update | Lovable prompt | Executed ... Content Performance section added to /docs |
Database Tables
| Table | Purpose |
|---|---|
seo_position_history | Weekly position per keyword with trend + baseline tracking |
seo_citation_tracking | Per-keyword AI citation results with Share of Model |
seo_citation_summary | Monthly aggregated citation rates per brand |
Cost Per Brand/Month
| Component | Cost |
|---|---|
| Position Tracking (DataForSEO) | ~$0.40 |
| Citation Tracking (Perplexity Sonar) | ~$0.10 |
| Article Scoring (LLM for GEO) | ~$0.15 |
| Total | ~$0.65 |
Scoring Dimensions
Each article receives 4 scores (0-100) displayed as colored rings on the Article Detail sidebar:
| Score | Method | What It Measures |
|---|---|---|
| Quality | LLM (existing) | Voice accuracy, readability, coherence, structure, engagement, accuracy, originality |
| SEO | Mechanical | Keyword placement, density, heading optimization, meta readiness |
| GEO | Hybrid LLM | Extractable claims, structured data, authority signals, citation-friendliness |
| Platform | Mechanical | Compliance with platform-specific best practices (length, formatting, hashtags) |
Reference
- System Status:
002 Projects/_QWR Quietly Writing App/QWR-System-Status.md→ Content Performance Intelligence section - Directive:
005 Operations/Directives/qwr_seo_intelligence.md(Phases 3-4 evolved into this)
QWR Press Release Service ⭐ NEW
Added: February 14, 2026
The Press Release Service extends QWR from article generation into professional press release distribution. Supporter-partners can request voice-matched, intelligence-informed, GEO-optimized press releases distributed to 500+ media outlets via Press Ranger Gold tier, with full impact tracking through Content Performance Intelligence.
How It Works
SUPPORTER REQUEST (/press-releases/new)
└── Topic + context + optional talking points
│
▼
AI GENERATION (qwr_press_release_generator.py)
├── AP-style structure (inverted pyramid, dateline, boilerplate)
├── Voice matching (supporter's voice profile)
├── SEO optimization (keyword placement)
├── GEO optimization (extractable claims, citation-friendly)
└── Stored as articles.content_type = 'press_release'
│
▼
PR SCORING (qwr_pr_readiness_scorer.py)
├── Newsworthiness (0-100)
├── Headline Quality (0-100)
├── Quote Quality (0-100)
├── Structure (0-100)
└── Overall Readiness (0-100)
│
▼
DISTRIBUTION (human-in-the-loop via Press Ranger)
├── PR Distribution Alert workflow checks every 5 min
├── distribution_status: 'ready' → Discord alert to operator
├── Operator submits to Press Ranger → marks 'distributed'
└── Gold tier: 500+ outlets (Bloomberg, AP, Yahoo Finance, AIWire, etc.)
│
▼
IMPACT TRACKING (Content Performance Intelligence)
├── SERP position tracking (weekly)
├── AI citation monitoring (bi-monthly)
└── Performance dashboard integration
Bundle Pricing (All Tiers)
| Purchase | Per Release | Total | QWF Margin |
|---|---|---|---|
| 1 release | $799 | $799 | $399 (100%) |
| 3-pack | $699 | $2,097 | $299 (75%) |
| 6-pack | $649 | $3,894 | $249 (62%) |
| 12-pack | $599 | $7,188 | $199 (50%) |
Credits are per-account (not per-brand), never expire, and are consumed FIFO from the oldest bundle.
Components
| Component | Type | Status |
|---|---|---|
qwr_press_release_generator.py | Python script | Built ... Claude FLAGSHIP, AP-style + voice matching + SEO/GEO |
qwr_pr_readiness_scorer.py | Python script | Built ... 5-dimension scoring |
| PR Generation webhook | n8n DugxqzUd2Eogdvg7 | Deployed ... /webhook/qwr-press-release |
| PR Distribution Alert | n8n vQFSORoJR3QHuH3O | Deployed ... every 5 min check for ready PRs |
| Stripe Webhook v1.2 | n8n Lgj4TMhbMbZyPTrO | Deployed ... credit provisioning branch |
| Checkout edge function | Supabase | Updated ... one-time payment mode for credit bundles |
| 4 Stripe bundle products | Stripe (LIVE) | Active ... single, 3-pack, 6-pack, 12-pack |
| Press Release UI | Lovable prompt | Executed ... list, request, detail, credits, dashboard card |
| Landing Page v2.0.0 | Lovable prompt | Executed ... "Your Story. On Bloomberg." spotlight with pricing |
Database Changes
| Change | Details |
|---|---|
articles.content_type | TEXT DEFAULT 'article' CHECK IN ('article', 'press_release') |
articles.pr_metadata | JSONB ... headline, dateline, distribution_status, pr_score, readiness_checklist |
press_release_credits table | Per-supporter credit tracking with FIFO consumption |
get_pr_credits_remaining(UUID) | Returns remaining credit count |
use_pr_credit(UUID) | FIFO deduction from oldest bundle |
Distribution Network
Press Ranger Gold tier provides access to 500+ outlets including:
- Tier 1: Bloomberg, Associated Press, Yahoo Finance
- Industry: AIWire, Blockchain News, MarketWatch
- Consumer: Google News, Apple News, Business Insider
- Full list: Varies per release based on industry targeting
Current State (February 14, 2026)
| Layer | Status |
|---|---|
| Backend (Python scripts) | Fully deployed |
| Database (Supabase) | Migration complete |
| Workflows (n8n) | 2 workflows active |
| Payments (Stripe) | LIVE ... 4 bundle products |
| Frontend (Lovable) | Prompts executed |
| Distribution | Human-in-the-loop (Phase 2 will add API automation) |
Reference
- System Status:
002 Projects/_QWR Quietly Writing App/QWR-System-Status.md→ Press Release Service section - Directive:
005 Operations/Directives/qwr_press_release_service.md
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| AP-style press release structure | Technical writing, journalism standards | ⭐⭐ |
| Stripe one-time payment integration | Payment APIs, checkout flows, credit systems | ⭐⭐⭐ |
| n8n webhook + distribution alert pattern | Event-driven architecture, polling workflows | ⭐⭐ |
| FIFO credit system (SQL functions) | Database functions, transactional logic | ⭐⭐⭐ |
| GEO optimization for AI citations | Emerging SEO, structured content | ⭐⭐ |
🎓 Missing Pixel Training Opportunities ... Verification & Migration Discipline (added 2026-08-11)
Drawn from the Opus 5 FLAGSHIP migration. These are unusually good teaching material because the session got things WRONG and the record shows how each was caught ... which is far more instructive than a clean run. Every item below maps to a real artifact a student can read.
| Component | Skills Developed | Difficulty |
|---|---|---|
| Staged rollout of a vendor upgrade (probe → non-behavioral code → flip → watch) | Release engineering, change isolation, risk staging | ⭐⭐⭐ |
| Writing a one-shot empirical probe instead of trusting docs | API testing, evidence discipline, cost-aware experimentation | ⭐⭐ |
| Mutation testing: reverting each fix to prove its test fails | Test quality, "a suite that never caught anything is a green light that means nothing" | ⭐⭐⭐ |
| Distinguishing a model OUTCOME field from a caller INTENT field | Data modeling, schema design, why proxies mislead | ⭐⭐⭐ |
| Instrument reliability: how a measuring tool lies | Debugging methodology, epistemics, root-cause analysis | ⭐⭐⭐⭐ |
| Designing a blind multi-reviewer fan-out | Code review process, decorrelating errors, adversarial thinking | ⭐⭐⭐⭐ |
| Reading a rollback path in the direction people actually reach | Operational safety, failure-mode analysis | ⭐⭐ |
| Fail-closed gates and why silent aborts are the dangerous kind | Systems safety, alerting design | ⭐⭐⭐ |
Three teaching moments with the receipts, suitable as a discussion exercise:
-
"Two reviewers confirmed a number that was wrong by 7x." The exposure figure was computed by classifying ledger rows on
thinking_tokens. Two independent reviewers re-derived it from the same field and confirmed it; a third asked what did the caller actually request? and the number collapsed. Lesson: independent recomputation validates arithmetic, not premises. Vary the question, not just the reviewer. Artifacts: channelr02,r04-advisor.md. -
"I documented the trap and then walked into it." The same session proved
reasoning_tokensreads 0 on calls that reasoned, wrote that finding into three files ... and then used the field as its population classifier. Lesson: a finding you record but do not apply to your own work is not yet a lesson. Artifact:probe_opus5_passthrough.pydocstring vs the retracted figure inr02. -
"A schedule that stops beats a flag that persists." A claim that two pipelines were still running rested on
n8n list:workflow --onlyActive, which reads a column the project's own docs say is not publish state. The decisive evidence ... regular execution clusters that simply stopped ... was already in hand and was read second. Lesson: absence of expected activity is stronger evidence than presence of a stale boolean. Artifact:r03.mdretraction.
Suggested exercise (Intermediate, ~90 min): give the student r01.md through r04-advisor.md
and the raw probe JSON in .tmp/opus5_probe/, and ask them to find the error before reading
r04. Then ask the harder question: which reviewer would you have believed, and why?
Portfolio value: high. Verification discipline, adversarial review design, and honest retraction are rare, demonstrable, and transfer to any engineering employer.
LLM Model Tiers and the Opus 5 Migration ⭐ NEW
Source of truth: 005 Operations/Directives/llm_model_strategy.md. This section is the
operator-facing summary; the directive owns the detail and the model-upgrade runbook.
Every LLM call in the backoffice routes through 005 Operations/Execution/model_config.py.
Tiers are selected by name (ModelTier.FLAGSHIP, STANDARD, FAST, ...) and the concrete
model behind each tier is set by MODEL_TIER_* variables in .env, which override the
DEFAULT_MODELS map in code.
FLAGSHIP is Claude Opus 5 (migrated 2026-08-11)
Identical pricing to the previous Opus 4.8 ($5 input / $25 output per MTok, 1M context), so the migration was a capability move with no cost move.
The one thing to know: Opus 5 reversed the thinking default
On Opus 4.x, omitting the thinking parameter meant no thinking. On Opus 5, omitting it
means adaptive thinking is ON. llm_call() therefore states thinking intent explicitly in
both directions and sends reasoning: {"enabled": False} whenever a caller passes
thinking=False. Do not remove that branch ... it is pinned by 26 assertions in
tests/test_model_config_reasoning_payload.py.
Two further Opus 5 behaviors worth knowing before wiring anything new:
- It can decline a request (HTTP 200 with a
refusalstop reason).llm_call()now fails loudly on this rather than returning an empty string as a successful answer. - It rejects an explicit thinking-disable above effort
high; a guard refuses that combination locally instead of shipping a 400.
Two instrument traps recorded here so nobody re-learns them
| Trap | What actually happens |
|---|---|
completion_tokens_details.reasoning_tokens | Unreliable in BOTH directions for Anthropic-via-OpenRouter: reads 0 on calls that demonstrably reasoned, and undercounts by ~6x when non-zero (it counts the returned summary, not the billed thinking). Use message.reasoning_details as the did-it-reason flag. |
| Inferring caller intent from the ledger | thinking_tokens is a model OUTCOME. The ledger now carries thinking_requested (caller intent) because intent cannot be recovered from the other fields after the fact ... four high-volume scripts pass thinking=True with no effort, so filtering on thinking_tokens or effort misclassifies them. This mistake produced a cost estimate wrong by 7x on 2026-08-11. |
Rollback
SET MODEL_TIER_FLAGSHIP=anthropic/claude-opus-4.8 in .env. Do not delete or comment
the line ... DEFAULT_MODELS[FLAGSHIP] is now opus-5, so an absent or blank override falls
through and rolls you forward. Cron- and n8n-invoked scripts pick it up on their next run;
long-lived servers hold their boot environment and need a restart.
Known scope limits
The tier system migrated; several surfaces did not, because they bypass model_config
entirely ... the STORM/research path, two provenance-metadata strings, and three edge functions
in separate repos (HQ ezer, HQ quick-intel, L4G ezer-chat). See the Known Issues table in
QWU-Backoffice-System-Status.md.
Update 2026-10-06: Opus 5.5 / Fable 5.1, session rules, and the model release intake
Current tiers: FLAGSHIP is Claude Opus 5.5 and APEX is Claude Fable 5.1 (both since
2026-10-05; thinking cannot be disabled on either, and Opus 5.5's effort default is medium).
SYNTHESIS and STANDARD are still Claude Sonnet 5; Sonnet 5.5 is pending TIG's decision. The
sections above describe the Opus 5 era and are kept as history.
Which model a Claude Code session runs in is a separate question from script tiers. Rule
(TIG, 2026-10-06): one model and one effort chosen up front for the kind of work, fixed for the
session. Consequential attended work = Opus 5.5 at xhigh; routine attended work = Opus 5.5 at
high; max only for a named extreme problem; Fable 5.1 only for long unattended runs.
Full rules: claude_tool_wisdom.md §"Choosing a model and effort for a Claude Code session".
New models now arrive with their homework done. watch_claude_models.py checks Anthropic's
and OpenRouter's model lists every 3 hours. A new model triggers intake_claude_model.py: a model
card built from the same Anthropic pages every time, where a script checks every quote word for
word against its page; a probe of our own call path (probe_model_call_shapes.py); and five drafted
decisions parked on TIG's Alerts calendar for the next 7 AM. Cards live in
002 Projects/_QWU Backoffice Automation/Model Cards/. Directive:
claude_model_release_intake.md. First card: Claude Sonnet 5.5 (128/128 quotes verified).
Prompt caching is an opt-in per script (2026-10-21)
llm_call(..., system_prompt=STABLE_TEXT, cache_system=True) marks the system text as an Anthropic
cache breakpoint (5-minute life; cache_ttl="1h" for the 1-hour cache). It is OFF by default because
a cache write costs 1.25x input and only a later read saves (0.05x on Opus 5.5): switch it on only where
the same system text comes back within 5 minutes, and prove it with
probe_openrouter_prompt_caching.py (a pair whose second call reads). Never use a top-level
cache_control on one-question calls: it writes every time and reads never.
| Check | Where |
|---|---|
| Reads and writes per call | cached_tokens, cache_write_tokens in 005 Operations/Data/llm_usage.jsonl |
| OpenRouter's own charge per call | billed_cost in the same row (our cost_estimate matches it) |
| Healthy | one write, then reads, within a run; a write on every row with no reads is a surcharge |
| Where it is on | attribute_subscription_apps.py v1.1.0 (nightly 04:30 chain) |
| Per-script decisions | llm_model_strategy.md §Prompt Caching |
On this fleet caching is worth a few dollars a month; the larger saving is repeat work (the same
question asked night after night), handled by kickoff llm-repeat-work-kickoff-2026-10-21.md.
Cost Intelligence System ⭐ NEW
Added: February 16, 2026
A unified cost tracking and budget intelligence system providing real-time visibility into all QWF operating costs with app-level attribution, billing channel separation, and proactive budget alerting. As a nonprofit pursuing 100% financial self-sufficiency, every dollar of cost visibility directly impacts sustainability calculations.
Architecture Overview
COST SOURCES (7 variable sources)
├── LLM APIs (~$90/mo)
│ ├── OpenRouter ($5/$25 per MTok for Opus 4.7)
│ └── Anthropic Direct ($15/$75 per MTok — Ezer edge function)
├── Apify (~$45/mo) → collect_apify_costs.py
├── Azure VMs (~$150/mo) → azure_costs.py
├── Supabase (~$135/mo) → collect_app_metrics.py
├── ESP VPS ($3/mo) → hardcoded
├── Betterstack ($0/mo) → lifetime license
└── Email ($0/mo) → included in M365/SES
│
▼
COLLECTION LAYER
├── model_config.py → llm_usage.jsonl (per-call, real-time)
├── collect_app_metrics.py → hq_app_metrics (daily, per-app)
├── collect_apify_costs.py → summary dict (on-demand/daily)
├── azure_costs.py → Azure Cost Management API (daily)
└── summarize_session.py → integrates all sources into daily digest
│
▼
VALIDATION & ALERTING
├── validate_llm_costs.py → 7-check validation suite
├── check_budget_alerts.py → threshold monitoring → Discord #system-status
└── OpenRouter Activity API → cross-validation (96% coverage confirmed)
│
▼
OUTPUT SURFACES
├── Digital Twin → twin.quietlyworking.org Operating Costs section (v2.4)
├── Discord #daily-digest → cost summary in session digest
├── Discord #system-status → budget alerts on threshold breach
├── HQ App Observatory → per-app sustainability dashboard
└── CLI → model_config.py --usage, azure_costs.py, collect_apify_costs.py
Azure Nonprofit Credit Tracking
Added: June 11, 2026
The $2,000/year Azure nonprofit sponsorship credit is tracked by check_azure_credit_balance.py (weekly cron, Mon 7 AM Pacific) so exhaustion never lands as a surprise credit-card charge. Key mechanics learned from the 2026-06 investigation:
- Billing structure: MCA billing account/profile ("Chaplain TIG Heaslet"); credits are granted as 12-month lots. Past card charges traced to a 32-day gap between grant lots (old lot expired 3/13/2026 mostly forfeited; new lot started 4/14/2026), NOT to SKU eligibility or overflow.
- Two modes:
estimate(live;AZURE_CREDIT_GRANT_AMOUNT+AZURE_CREDIT_GRANT_STARTin.env, balance = grant minus Cost Management spend since start) andapi(exact lot data; requires claude-dev-vm's identity to hold a Reader billing role, which is a separate ACL from Azure RBAC ... auto-upgrades when granted). - Outputs: Discord
#system-status(🟢/🟡 <3 mo runway/🔴 <1 mo/⛔ exhausted), HQ Ecosystem Vitals widget (via single-rowhq_azure_credittable in HQ Supabase), structured JSON. - Operational date: re-request the nonprofit grant well before each April 14 lot expiry to avoid another gap.
- Sponsorship credits never cover Marketplace products, support plans, or savings plans/reservations (those bill the card directly).
Cost Sources
| Source | Monthly Cost | Collection Method | Granularity |
|---|---|---|---|
| Azure VMs | ~$150 | azure_costs.py via Cost Management API | Per-resource, daily |
| LLM APIs (OpenRouter) | ~$90 | model_config.py logs to llm_usage.jsonl | Per-call, per-model, per-app |
| Supabase | ~$135 | collect_app_metrics.py (tier-based) | Per-app, per-tier |
| Apify | ~$45 | collect_apify_costs.py via REST API | Per-actor, per-run, per-day |
| ESP VPS | ~$3 | Hardcoded ($35.49/year) | Fixed |
| Cloudflare Pages | $0 | Free tier (unlimited bandwidth) | Per-project |
| Betterstack | $0 | Lifetime AppSumo (2 stacked codes): 200 monitors, 10 status pages, 5 members | N/A |
| Email (Graph/SES) | $0 | Included in existing licenses | N/A |
| Total | ~$423/mo |
Cost Optimization Note (March 2026): QWR frontend hosting migrated from Lovable ($320/yr per project on Pro plan) to Cloudflare Pages (free tier, unlimited bandwidth). As additional apps migrate, each saves ~$320/yr in Lovable hosting costs. Lovable remains the build tool (AI-assisted UI development via prompts) but is no longer required for hosting.
Supabase Pricing Model
The Supabase cost is computed dynamically, not hardcoded per-app:
| Component | Monthly Cost | Notes |
|---|---|---|
| Org base (Pro) | $25.00 | Single "Quietly Working" org |
| Compute credit | -$10.00 | Included in Pro plan |
| 12x MICRO projects | $120.00 | QWR, HQ, QQT, QRP, QSP, QNT, QKN, Pocket EZ, L4G, QTR, WHL, [Supporter Organization] CC ($10 each) |
| Total | $135.00 | Verified 2026-04-06 (invoice KWNZSK-00017) |
Helper function _app_monthly_cost() in collect_app_metrics.py computes per-app cost:
(base_org_cost - compute_credit) / total_apps + compute_tier_cost
Billing Channels (Critical)
Two billing channels for LLM costs with 3x pricing difference for the same model:
| Channel | Provider | Opus 4.7 Input | Opus 4.7 Output | Used By |
|---|---|---|---|---|
openrouter | OpenRouter | $5/MTok | $25/MTok | All backoffice scripts via model_config.py |
anthropic_direct | Anthropic API | $15/MTok | $75/MTok | Ezer edge function (Supabase) |
Every LLM usage log entry includes billing_channel to ensure correct cost attribution.
App-Level Cost Attribution
model_config.py v2.1.0 maps 40+ scripts to app codes via SCRIPT_APP_MAP and 55+ scripts to 13 business purposes via SCRIPT_PURPOSE_MAP:
| App Code | Scripts | Description |
|---|---|---|
bni | Meeting followups, visitor pipeline, enrichment scripts | [Networking Chapter] operations |
hq | Audit, briefing, capture, entity management, summarization | HQ Command Center + backoffice |
qwr | Content pipeline, press releases, citation tracking | Quietly Writing |
qnt | Visitor pipeline, meeting pipeline, newsletter pipeline | Quietly Networking |
pocket_ez | Pocket Ez edge functions | Pocket Ez companion app |
qrp | Property management scripts | Quietly Renting Property |
ops | Budget alerts, cost collection | Infrastructure operations |
backoffice | Default for unmapped scripts | General backoffice operations |
Purpose-Level Attribution (v2.1.0)
Purpose attribution answers "value vs waste" at the business-function level ... more actionable than app-code alone. Every LLM call is tagged with a purpose field in llm_usage.jsonl.
| Purpose | Label | Example Scripts |
|---|---|---|
email_processing | Email Processing | outlook_pipeline, email_classify, email_entity_resolve |
bni_relationships | BNI & Relationships | meeting_followup, bni_visitor_pipeline, meeting_prep |
qwr_content | QWR Content | qwr_article_generator, qwr_citation_tracker, press releases |
meeting_intel | Meeting Intelligence | zoom_pipeline, meeting_update_vault |
wisdom_knowledge | Wisdom & Knowledge | wisdom_indexer, generate_wisdom_capture |
content_social | Content & Social | content_pipeline_supervisor, social media scripts |
hq_inbox | HQ Inbox Processing | process_inbox, master_capture |
pocket_ez_chat | Pocket Ez Chat | pocket_ez edge function |
ops_admin | Ops & Admin | audit_system, summarize_session |
ops_intelligence | Ops Intelligence | auto_remediate_server, vm health |
lead_enrichment | Lead Enrichment | enrich_names_ai, lead generation |
mission_intel | Mission Intelligence | expert tracking, voice profiles |
izm_capture | TIG Izm Capture | capture_tig_izm |
CLI: python model_config.py --usage --days 30 shows breakdowns by model, tier, app, purpose, and billing channel.
Cost Attribution Report
report_llm_costs.py v1.0.0 provides a full cost dashboard:
python report_llm_costs.py # Last 30 days
python report_llm_costs.py --days 7 # Last 7 days
python report_llm_costs.py --current-month # Current calendar month
python report_llm_costs.py --all # All time
python report_llm_costs.py --json # JSON output for Discord/dashboards
Report sections: Total Summary, By Purpose (primary), By Tier, Top 10 Scripts, Weekly Trend (with bars), Monthly Totals, By App, Blind Spots reminder.
Email Pipeline Cost Optimization
The email pipeline is the largest LLM cost driver (~44% of total spend). Optimization strategy:
- Suppression pre-check (v1.5.0→v1.6.0):
outlook_pipeline.pyloadshq_email_suppressionstable BEFORE classification. Suppressed senders/domains get a syntheticclassification: "suppressed"and skip Opus entirely (~$0.036/email saved). As of March 2026: 82% suppression rate (1,153/1,413 emails skipped), reducing email processing costs from $1.17/day (Feb) to $0.22/day (Mar) ... 81% reduction. v1.6.0 also tracks all senders inhq_email_sendersSupabase table for the HQ Email Sources module. - Newsletter short-circuit:
email_classify.pyskips Opus for pre-detected newsletters. - Active unsubscribing: User progressively reduces inbox volume by unsubscribing from non-essential emails.
- Task creation safety net:
email_task_create.pyRule 0 blocks task creation for suppressed emails (defense in depth).
Budget Alerting
check_budget_alerts.py runs daily via n8n cron. Zero-dependency design (stdlib only) ensures it works even when pip packages are broken.
| Budget | Monthly Limit | Warn At (75%) | Critical At (90%) | Daily Anomaly |
|---|---|---|---|---|
| LLM (OpenRouter) | $150 | $112.50 | $135 | $15/day |
| Apify | $75 | $56.25 | $67.50 | $10/day |
| Total Variable | $400 | $300 | $360 | ... |
Features: MTD spend tracking, end-of-month projection, daily anomaly detection, Discord #system-status alerts.
Cost Validation
validate_llm_costs.py runs 7 validation checks:
- Parse integrity ... Counts unparseable log lines
- Zero-cost anomalies ... Flags $0 calls on non-FAST tiers
- Cost spike detection ... Single calls exceeding $1.00
- Daily trend analysis ... Flags days 3x above average
- Billing channel consistency ... Verifies rates match expected ranges per channel
- App attribution coverage ... Reports explicit vs derived app codes
- OpenRouter Activity API cross-validation ... Compares local totals against API (requires
OPENROUTER_MGMT_KEY)
Cross-validation result: 96% coverage ($88.62 local vs $92.47 API over 30 days). The 4% gap represents calls outside model_config.py (other API keys, direct usage).
Components
| Script | Version | Purpose |
|---|---|---|
model_config.py | v2.1.0 | LLM cost logging with app/purpose/channel attribution |
report_llm_costs.py | v1.0.0 | Full cost attribution dashboard (purpose/app/tier/trend) |
collect_app_metrics.py | v1.2.0 | Per-app Supabase + shared infra cost collection |
collect_apify_costs.py | v1.0.0 | Apify per-actor, per-run cost breakdown |
validate_llm_costs.py | v1.1.0 | 7-check LLM cost validation suite (pricing ranges from cost_constants) |
check_budget_alerts.py | v1.0.0 | Zero-dep budget threshold monitoring |
azure_costs.py | v1.2.0 | Azure VM cost collection (MTD always queries full month) |
summarize_session.py | ... | Integrates Apify costs into daily digest |
Key Files
| File | Location | Purpose |
|---|---|---|
| LLM usage log | 005 Operations/Data/llm_usage.jsonl | Per-call LLM cost records (moved from .tmp/) |
| Cost tracking directive | 005 Operations/Directives/cost_tracking.md | Full SOP with edge cases and changelog |
| Cost constants (SSoT) | 005 Operations/Execution/cost_constants.py | Single source of truth for all cost figures (v1.1.0) |
| Budget config | check_budget_alerts.py BUDGETS dict | Threshold definitions |
| HQ Observatory schema | hq_observatory_schema.sql | Supabase tables for app metrics |
Digital Twin Cost Transparency (v2.4)
As of v2.4, the Digital Twin at twin.quietlyworking.org displays a full Operating Costs section ... publicly visible, following QWF's "Show don't Tell" transparency value. The server's /api/costs endpoint aggregates all 7 cost sources with TTL-based caching.
What's displayed publicly:
- Monthly burn total with per-source breakdown (LLM, Azure, Supabase, Apify, ESP)
- Variable budget progress bar ($400/mo target) with day-of-month context and month-end projection
- LLM Intelligence panel: tier cards (Flagship/Standard/Fast/Image), top scripts by cost
- Infrastructure panel: Azure MTD, Supabase ($135), ESP ($2.96), Betterstack (Free), Email (Free)
What's excluded (security): No API keys, server IPs, .env variable names, Supabase project IDs, credential names, user emails, or full file paths.
Caching strategy:
| Source | Cache TTL | Why |
|---|---|---|
| LLM JSONL aggregation | 5 min | ~100ms parse of ~7K lines; avoids re-parse on 60s polling |
| Azure costs (SQLite) | 5 min | Batched with LLM cache |
| Apify (API calls) | 30 min | Network-bound (5-15s per call); stale OK |
| Fixed costs | 5 min | Hardcoded values, negligible |
Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| API cost cross-validation | REST APIs, data reconciliation, coverage analysis | ⭐⭐⭐ |
| Budget alerting (zero-dep) | Python stdlib, Discord webhooks, projection math | ⭐⭐ |
| App-level attribution architecture | Dictionary mapping, log enrichment, CLI analytics | ⭐⭐ |
| Purpose-level attribution (v2.1.0) | Multi-dimensional analysis, defaultdict aggregation, business-function mapping | ⭐⭐⭐ |
| Cost attribution report (report_llm_costs.py) | JSONL parsing, formatted output tables, trend visualization, CLI arg parsing | ⭐⭐ |
| Suppression-before-classification optimization | Defense-in-depth, cost-aware pipeline design, synthetic classification patterns | ⭐⭐⭐ |
| Billing channel separation | Multi-channel pricing, rate card validation | ⭐⭐ |
| Supabase tier-based costing | Dynamic pricing models, per-app allocation | ⭐⭐ |
QWR Reverse Benchmarking Intelligence ⭐ NEW
Added: February 18, 2026
Reverse Benchmarking Intelligence studies what competitors are terrible at instead of copying their strengths (Rory Sutherland's concept). It uses two intelligence sources ... competitor reviews (G2, Trustpilot, Capterra) and Reddit research (subreddit pain points + customer language) ... to produce scored gap opportunities that feed directly into article generation.
Architecture
REVIEW SOURCES (per competitor per brand)
└── qwr_review_scraper.py → competitor_reviews table
└── qwr_review_analyzer.py → review_analysis table
REDDIT SOURCES (per subreddit per brand)
└── qwr_reddit_scraper.py (PRAW) → reddit_posts table
└── qwr_reddit_analyzer.py → reddit_analysis table
CROSS-REFERENCE
└── qwr_gap_opportunity_generator.py → gap_opportunities table (scored 0-100)
└── Article Gen v6.0 (gap context + customer language injected)
Two-Source Intelligence Model
| Source | What It Captures | Method |
|---|---|---|
| Competitor Reviews (G2, Trustpilot, Capterra) | Strengths, weaknesses, feature gaps, pricing complaints | Apify scraping → Claude FLAGSHIP analysis |
| Reddit (subreddit monitoring) | Pain points, unmet needs, customer language (terms, phrases, questions) | PRAW API → Claude FLAGSHIP analysis |
The power is in the cross-reference: when competitor reviews say "reporting is weak" AND Reddit users complain about "spending hours building reports manually," that's a high-confidence gap opportunity with real customer language to inject into articles.
Components
| Component | Type | Status |
|---|---|---|
| SQL Migration (7 tables) | Supabase | Deployed |
qwr_review_scraper.py | Python script | Built ... G2, Trustpilot, Capterra via Apify |
qwr_review_analyzer.py | Python script | Built ... Claude FLAGSHIP with thinking |
qwr_reddit_scraper.py | Python script | Built ... PRAW-based subreddit scraping |
qwr_reddit_analyzer.py | Python script | Built ... pain points, unmet needs, customer language |
qwr_gap_opportunity_generator.py v2.0.0 | Python script | Built ... cross-references sources → scored gaps + content strategy matching (suggested_strategy_id) |
qwr_competitor_intel.py v2.0.0 | Extended | Added review scraping trigger |
qwr_opportunity_scorer.py v2.0.0 | Extended | Added gap opportunity scoring |
qwr_update_article_gen_workflow.py v3.0.0 | Extended | Article gen v5.0 → v8.0 (content strategy + quality targets) |
qwr_fix_citation_pipeline.py v1.0.0 | Built | Citation URL extraction, quality scorer fix, inline attribution |
| n8n: Review Scraping + Analysis | Workflow q9D0RU8D7eBCmq34 | Deployed ... scheduled |
| n8n: Reddit Scraping + Analysis | Workflow DSv69vT6GfXzY6IS | Deployed ... scheduled |
| n8n: Gap Opportunity Generator | Workflow aEj51qiTeZl9C4tL | Deployed ... scheduled |
| n8n: Subreddit Discovery Webhook | Workflow wC2Yly51PIPd7Rkh | Deployed ... /webhook/qwr-subreddit-discovery |
| Lovable: Source Setup UI (054) | Frontend | Executed |
| Lovable: Intelligence Dashboard (055) | Frontend | Executed |
| Lovable: Gap Opportunity Detail (056) | Frontend | Executed |
| Lovable: Landing Page Update (057) | Frontend | Executed |
Database Tables
| Table | Purpose |
|---|---|
review_sources | Competitor review source configs (platform, URL, competitor name) |
competitor_reviews | Raw scraped reviews (rating, text, reviewer, date) |
reddit_sources | Subreddit monitoring configs per brand |
reddit_posts | Raw scraped Reddit posts/comments |
review_analysis | Claude analysis output (strengths, weaknesses, gaps as JSONB) |
reddit_analysis | Claude analysis output (pain_points, unmet_needs, customer_language as JSONB) |
gap_opportunities | Scored content gaps (title, gap_type, score 0-100, source_evidence, suggested_angles, suggested_strategy_id) |
Article Gen v6.0 Integration
The article generation workflow now includes:
- "Write About This Gap" flow: When
gap_opportunity_idis in the webhook body, fetches the full gap with evidence and injects it as the article's primary angle - Standard article flow: Keyword-matches against active gaps (score >= 40) and surfaces relevant opportunities
- Customer language injection: Fetches Reddit-derived common_terms, power_phrases, questions_asked, frustration_phrases, and praise_phrases ... articles use the exact words real customers use
- Post-generation update: PATCHes gap status to
article_generatedafter successful generation
Dependencies
| Dependency | Status | Notes |
|---|---|---|
| PRAW 7.8.1 | Installed | pip install praw ... in requirements.txt |
| Reddit API credentials | PENDING | User creating app at reddit.com/prefs/apps → REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_USER_AGENT in .env |
Gap Scoring
Each gap opportunity is scored 0-100 based on:
- Review evidence strength ... How many reviews mention this weakness, across how many platforms
- Reddit evidence strength ... How many independent pain point mentions in relevant subreddits
- Cross-source correlation ... Higher score when both sources identify the same gap
- Content viability ... Is this gap something an article can actually address?
Gaps scoring 40+ are surfaced to article generation. Gaps scoring 70+ trigger Discord alerts as high-value content opportunities.
Reference
- Directive:
005 Operations/Directives/qwr_reverse_benchmarking.md - System Status:
002 Projects/_QWR Quietly Writing App/QWR-System-Status.md→ Reverse Benchmarking Intelligence section
Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Apify review scraping (multi-platform) | Web scraping, API integration, data normalization | ⭐⭐ |
| PRAW Reddit API | OAuth, API wrappers, rate limiting | ⭐⭐ |
| Claude FLAGSHIP analysis with thinking | LLM prompt engineering, structured output extraction | ⭐⭐⭐ |
| Cross-source gap scoring | Multi-signal scoring algorithms, evidence weighting | ⭐⭐⭐ |
| Customer language injection into AI content | NLP concepts, voice matching, authenticity engineering | ⭐⭐⭐ |
| n8n scheduled pipeline orchestration | Workflow automation, SSH execution, error handling | ⭐⭐ |
QWR Content Strategy System ⭐ NEW
Added: February 26, 2026
The Content Strategy System adds strategy-aware article generation to QWR. Supporters define named content strategies (e.g., "LinkedIn Thought Leadership for CMOs") that combine a target persona, platform, expertise level, and style guide. The article generation pipeline uses this context to produce platform-optimized content.
Architecture
STRATEGY CREATION (Frontend)
└── /brands → Strategy Settings tab
└── Create strategy: name, goal, persona_id, platform, expertise, style_guide
└── content_strategies table (Supabase)
GAP → STRATEGY MATCHING
└── qwr_gap_opportunity_generator.py v2.0
└── Cross-references gap_opportunities with content_strategies
└── Sets suggested_strategy_id on gaps that match a strategy's persona/platform
ARTICLE GENERATION (v6.0)
└── If content_strategy_id in webhook body:
└── Fetches strategy + target persona + platform rules
└── Generates platform-optimized article
Components
| Component | Type | Status |
|---|---|---|
content_strategies table | Supabase | Deployed ... stores strategy configs per brand |
qwr_gap_opportunity_generator.py v2.0.0 | Python script | Built ... adds suggested_strategy_id to matching gaps |
| Article Gen v6.0 | n8n workflow <WORKFLOW_ID> | Deployed ... reads strategy context for generation |
| Lovable: Strategy Settings (065) | Frontend | Executed ... CRUD for content strategies |
| Lovable: Landing Page v3.0 (066) | Frontend | Executed ... updated landing page with Content Strategy messaging |
Database Tables
| Table | Purpose |
|---|---|
content_strategies | Strategy configs: name, goal, persona_id, platform, expertise_level, style_guide, brand_id |
gap_opportunities.suggested_strategy_id | FK to content_strategies ... set by gap generator when a gap matches a strategy |
Content Strategy Fields
| Field | Purpose | Example |
|---|---|---|
name | Human-readable strategy name | "LinkedIn Thought Leadership for CMOs" |
goal | What the strategy achieves | "Establish authority in marketing automation" |
persona_id | Target persona (FK) | Links to personas table |
platform | Target platform | linkedin, blog, twitter, newsletter |
expertise_level | Content depth | beginner, intermediate, expert |
style_guide | Writing style notes | "Conversational but authoritative, use data" |
Reference
- System Status:
002 Projects/_QWR Quietly Writing App/QWR-System-Status.md→ Content Strategy section - Development Plan:
002 Projects/_QWR Quietly Writing App/QWR-Content-Strategy-Development-Plan.md
QWR Preparation Workbook ⭐ NEW
Added: February 26, 2026
The Preparation Workbook is an interactive, AI-guided pre-signup experience at /prepare. Visitors build their content strategy through a 6-chapter conversational journey ... without creating an account. This replaces the traditional "sign up and figure it out" onboarding with a value-first approach.
Architecture
VISITOR (no auth required)
└── /prepare route
└── 6 chapters, sequential progression
├── Ch 1: Brand Discovery (AI conversation)
├── Ch 2: Voice Discovery (URL input + AI conversation)
├── Ch 3: Audience Discovery (AI conversation)
├── Ch 4: Platform Selection (grid, no AI)
├── Ch 5: Strategy Builder (AI conversation)
└── Ch 6: Review & Export (summary + PDF + signup CTA)
BACKEND
└── n8n webhook (POST /qwr-workbook)
└── SSH to backoffice VM
└── qwr_workbook_engine.py
└── workbook_sessions table (Supabase, anon RLS)
POST-SIGNUP IMPORT
└── /signup?workbook={session_id}
└── import-workbook edge function (--no-verify-jwt)
└── Creates brand, personas, content_strategies from workbook seeds
Components
| Component | Type | Status |
|---|---|---|
workbook_sessions table | Supabase | Deployed ... anon RLS policies for public access |
qwr_workbook_engine.py v1.0.0 | Python script | Built ... 4 AI chapter types, 7 commands, magic link resume |
| n8n: Workbook Webhook | Workflow 8S3sUIaYvJXhdzPg | Deployed ... /webhook/qwr-workbook |
| Lovable: Preparation Workbook (067) | Frontend | Executed ... /prepare route with chat UI |
import-workbook | Supabase Edge Function | Deployed ... converts workbook seeds to full records |
Database: workbook_sessions
| Column | Type | Purpose |
|---|---|---|
id | UUID (PK) | Session identifier |
email | text | Optional ... for save-and-resume |
supporter_id | UUID (FK) | Set after signup/import |
status | text | active, completed, imported, abandoned |
current_chapter | int | 1-6 progress tracker |
chapters_completed | int[] | Array of completed chapter numbers |
chapter_data | JSONB | Per-chapter metadata (phases, message counts) |
brand_seed | JSONB | Extracted: company, industry, differentiators, positioning |
voice_seed | JSONB | Extracted: core attributes, tone spectrum, A/B choices |
persona_seeds | JSONB | Extracted: array of personas with demographics, pain points, goals |
platform_selections | JSONB | User-selected platforms (up to 12 options) |
strategy_seeds | JSONB | Extracted: strategies linking persona + platform + expertise |
conversation_histories | JSONB | Per-chapter message arrays |
magic_link_token | text | 30-day token for cross-device resume |
magic_link_expires_at | timestamptz | Token expiry |
imported_at | timestamptz | When workbook was imported post-signup |
AI Conversation Flow
Each AI chapter (1, 2, 3, 5) follows a 3-phase progressive methodology:
- Discovery ... Open-ended questions to understand the visitor's situation
- Clarification ... Targeted follow-ups based on extracted data gaps
- Documentation ... Summary and confirmation of what was captured
Phase transitions are configurable per chapter with min_questions and min_completeness thresholds. Every 4 messages (or on phase change), Claude STANDARD extracts structured data from the conversation into the chapter's seed field.
Context Threading
Each chapter builds on previous chapters:
- Chapter 2 (Voice) receives brand context from Chapter 1
- Chapter 3 (Audience) receives brand + voice context
- Chapter 5 (Strategy) receives brand + voice + persona + platform context
Save and Resume
- Magic link: Visitor enters email → receives a token → can resume on any device within 30 days
- No auth required: The workbook operates entirely on anon RLS policies
- Session ID: Used as de facto access control (UUID is unguessable)
Post-Signup Import
When a visitor signs up with ?workbook={session_id}:
- Edge function
import-workbookreads the workbook session - Creates
brandsrecord frombrand_seed - Creates
personasrecords frompersona_seeds - Creates
content_strategiesfromstrategy_seeds - Sets
supporter_idandimported_aton the workbook session
Reference
- System Status:
002 Projects/_QWR Quietly Writing App/QWR-System-Status.md - Development Plan:
002 Projects/_QWR Quietly Writing App/QWR-Content-Strategy-Development-Plan.md(Phase 4) - Prompt:
002 Projects/_QWR Quietly Writing App/067-lovable-prompt-preparation-workbook.md
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Conversational AI with phase transitions | LLM prompt engineering, state machines, progressive extraction | ⭐⭐⭐ |
| Anon RLS policies for public features | Supabase auth model, Row Level Security, access control patterns | ⭐⭐ |
| Magic link authentication pattern | Token generation, expiry management, cross-device UX | ⭐⭐ |
| Chat UI with typing indicators | React state management, WebSocket-like UX patterns, auto-scroll | ⭐⭐ |
| Post-signup data import flow | Edge functions, data transformation, multi-table inserts | ⭐⭐⭐ |
QWF Ecosystem Landing Section ⭐ NEW
Added: February 18, 2026
Every QWF app landing page includes a standardized "Part of Something Bigger" section that introduces the full Quietly Working universe. Visitors to any single app discover it's part of a connected ecosystem with a free command center (Quietly Spotting) and seamless cross-app authentication (QWF Passport).
What It Contains
- v2.0 (March 2026): Interactive SVG Orbit + 9 Cards ... replaced flat card grid with animated hub-and-spoke visualization. QSP always center node. Current app at 12 o'clock. Traveling dots along connection lines. Auto-cycle spotlight (stops on any interaction). Bidirectional hover/click linking between orbit nodes and cards. Mobile fallback: vertical hub-list.
- 9 items: Quietly Spotting (FREE), Quietly Writing, Quietly Quoting (IN PRODUCTION), Quietly Tracking (IN PRODUCTION), Quietly Networking, Quietly Knocking, Quietly Managing (IN PRODUCTION), QWF Passport (ONE ACCOUNT), Your Tools (CONNECT, dashed border)
- Canonical icons (match QSP sidebar): Telescope, Feather, Calculator, Link2, Handshake, DoorOpen, Home, Fingerprint, Plug
- Mission footer: "Every penny of proceeds from QWF apps supports underserved youth through The Missing Pixel Project."
- Current app highlight: "You are here" label above current app's orbit node + badge on card
- Cascade generator:
005 Operations/Execution/generate_ecosystem_cascade.pyv2.1.0 produces per-app prompts (6 apps). QWR and Pocket Ez are manual.
Locked Copy (Must Not Change)
- Section heading: "Part of Something Bigger"
- Section subtitle: "Every QWF tool stands on its own. Together? They're something else entirely."
- QSP badge: "FREE"
- Passport tagline: "One login. Every app. Zero friction."
- Mission text: "Every penny of proceeds from QWF apps supports underserved youth through The Missing Pixel Project."
Implementation Status
| App | Status | Notes |
|---|---|---|
| QSP (Quietly Spotting) | ✅ Orbit v2 Deployed | Prompts 016-022: orbit + refinements + crosshair favicon + telescope navbar + orbit hero (Mar 8) |
| QWR (Quietly Writing) | ✅ Orbit v2 Deployed | Prompt 091: manual orbit prompt with ref-based stopCycle (Mar 8) |
| QQT (Quietly Quoting) | ✅ Orbit v2 Deployed | Prompt 015: cascade orbit (Mar 8) |
| QNT (Quietly Networking) | ✅ Orbit v2 Deployed | Prompt 049: cascade orbit (Mar 8) |
| QTR (Quietly Tracking) | ✅ Orbit v2 Deployed | Prompt 009: cascade orbit (Mar 8) |
| QKN (Quietly Knocking) | ✅ Orbit v2 Deployed | Prompt 005: cascade orbit (Mar 8) |
| QMP (Quietly Managing) | ✅ Orbit v2 Deployed | Prompt 011: cascade orbit (Mar 8) |
| Pocket Ez | ✅ Orbit v2 Deployed | Prompts 009-011: bioluminescent variant + navbar fix + alpha pill (Mar 8) |
Reference
- Directive:
005 Operations/Directives/qwf_ecosystem_landing_section.md - Standard:
005 Operations/Directives/qwf_app_family_standard.md→ Section 6 (Landing Page Standards)
Unified Supporter Statement System ⭐ NEW
Purpose
Replaces fragmented per-app Stripe receipts with a single monthly statement from QWF. Covers all apps... subscribed ones show usage and impact, unsubscribed ones serve as mission-aligned discovery opportunities with dual framing (supporter value + student training).
Architecture (Hybrid)
Per-app Stripe subscriptions stay intact. A unified statement is layered on top via three new HQ tables:
| Table | Purpose |
|---|---|
hq_supporter_profiles | Cross-app identity keyed by email |
hq_supporter_subscriptions | Per-app subscription snapshot + usage stats (JSONB) |
hq_supporter_statements | Statement history with rendered HTML |
7-Section Narrative Arc
| Section | Content |
|---|---|
| 1. Greeting | Seasonal, TIG-voiced, sent from Ezer Aión |
| 2. Your Impact | Mission-first metrics (students trained, total supporters) |
| 3. Your Apps | Ecosystem orbit + dual-framed cards (subscribed = usage, unsubscribed = "For You" + "For Students") |
| 4. Support Summary | Financial breakdown framed as gratitude |
| 5. Your Journey | Badge system (Explorer → Builder → Champion → Patron) |
| 6. Did You Know? | Rotating monthly content (student spotlights, tips, ecosystem facts) |
| 7. Footer | QWF 501(c)(3) + orbit + preference center link |
Dual-Framing Pattern
Every unsubscribed app card tells two stories:
- For You: What the app does for the supporter
- For Students: What training opportunities the app creates
FORGE Fuel Line Scaling
The 7-section template adapts per fuel line (same skeleton, different content blocks):
| Fuel Line | Frequency | Section 3 Becomes |
|---|---|---|
| Product (apps) | Monthly | App cards + usage |
| KINDLE (recurring donors) | Monthly | Program impact cards |
| ALLOY (corporate) | Monthly | Partnership impact |
| CRUCIBLE (major donors) | Quarterly | Personal narrative |
| EMBER (estate) | Annual | Legacy projections |
Phasing
| Phase | Scope | Status |
|---|---|---|
| 1 | QWR supporters only (MVP) | ✅ Built ... HQ tables deployed, data collection + statement generation working, test email sent |
| 2 | Multi-app expansion + badges | ⏳ Planned |
| 3 | QSP supporter portal (web) | ⏳ Planned |
| 4 | FORGE fuel line integration | ⏳ Planned |
Key Files
| File | Purpose |
|---|---|
005 Operations/Directives/unified_supporter_statement.md | Full specification |
005 Operations/Execution/collect_supporter_data.py | Cross-app data aggregation ... queries Stripe + per-app Supabase, upserts to HQ |
005 Operations/Execution/generate_supporter_statement.py | Jinja2 template rendering + Graph API email send |
005 Operations/Execution/hq_supporter_statement_migration.sql | HQ Supabase schema (3 tables + RLS + indexes) |
005 Operations/Templates/statements/ | Jinja2 HTML templates (base, sections, fuel lines) |
🎓 Missing Pixel Training Opportunities
| Task | Skills | Level |
|---|---|---|
| Build Jinja2 email template | HTML email, CSS, template engines | Intermediate |
| Cross-app data aggregation | REST APIs, PostgREST, data modeling | Intermediate |
| Badge system computation | Python, business logic, gamification design | Beginner |
| Seasonal content management | Content strategy, JSON config, writing | Beginner |
Auto-Remediation System ⭐ NEW
Overview
Automatically diagnoses and remediates server outages detected by Betterstack, using Claude Opus with extended thinking to analyze issues and execute safe, predefined recovery actions. Target: resolve common outages in under 60 seconds without human intervention.
Origin: A WPMU outage on 2026-02-23 had a 12-hour gap between Betterstack alert (overnight) and human response. The manual fix took ~60 seconds (MariaDB restart). This system automates that response.
Architecture
Betterstack detects outage
↓
Outgoing Webhook (ID: 80218)
↓
n8n Webhook (/webhook/auto-remediation-alert)
↓
Parse Alert → Skip if resolved
↓
SSH to claude-dev (nohup fire-and-forget)
↓
auto_remediate_server.py
├── Map monitor to server playbook
├── Run diagnostics (SSH commands)
├── Claude Opus thinking_call() analysis
├── Execute remediation (if mode=remediate)
├── Verify health (HTTP check)
└── Post Discord embed to #system-status
Server Playbooks
Each monitored server has a playbook defining safe boundaries:
| Playbook | Server | Monitors | Diagnostic Commands | Approved Actions |
|---|---|---|---|---|
wpmu | auto_remediate_server.py v1.1.0): the server is deleted and its sites are frozen copies on Cloudflare Pages | quietlyworking.org (now no playbook: the alert gets the "no playbook" report) | ... | ... |
n8n | qwu-n8n (Azure VM) | n8n Workflow Engine | Docker containers, disk/memory, Docker logs, health endpoint, pg_isready | Restart n8n container, restart all containers, restart postgres, prune Docker |
claude-dev-service | claude-dev (local) | Ezer SMS Gateway, Digital Twin, QNT Webhook | systemd status, journalctl, port listening, disk/memory | Restart service, restart Caddy |
Each playbook also has a Never Do list (e.g., never modify wp-config.php, never run docker compose down, never kill processes by PID).
Safety Modes
| Mode | Behavior | Use Case |
|---|---|---|
diagnose (default) | Runs diagnostics + AI analysis, posts findings to Discord. Does NOT execute remediation. | First 2 weeks of deployment (current) |
remediate | Everything in diagnose + executes approved remediation actions. Max 2 attempts per incident. | After validation period |
Key Components
| Component | Location | Notes |
|---|---|---|
| Script | 005 Operations/Execution/auto_remediate_server.py v1.0.0 | ~950 lines, 3 playbooks |
| Directive | 005 Operations/Directives/auto_remediation.md | Full SOP with playbooks |
| n8n Workflow | "Auto-Remediation - Betterstack Alert Handler" (<WORKFLOW_ID>) | Fire-and-forget SSH pattern |
| Workflow JSON | 005 Operations/Workflows/auto_remediation_webhook.json | 6 nodes |
| Betterstack Webhook | Outgoing Webhook ID 80218 | Fires on incident_started, incident_resolved, incident_reopened |
| Logs | .tmp/logs/auto_remediation.log + .tmp/logs/auto_remediation_runs.log | Per-run + background execution logs |
Manual Testing
# Diagnose-only for a specific monitor
python '005 Operations/Execution/auto_remediate_server.py' \
--monitor-name "quietlyworking.org" --mode diagnose --json
# Dry-run remediation (shows what it would do)
python '005 Operations/Execution/auto_remediate_server.py' \
--monitor-name "n8n Workflow Engine" --mode remediate --dry-run --json
# Simulate a webhook trigger
curl -X POST "https://n8n.quietlyworking.org/webhook/auto-remediation-alert" \
-H "Content-Type: application/json" \
-d '{"monitor": "quietlyworking.org", "type": "down"}'
Deduplication
Lock files (.tmp/auto_remediation_<monitor>.lock) prevent concurrent remediation attempts for the same monitor. Locks auto-expire after 10 minutes.
Lessons Learned During Build
- Claude wraps JSON in code fences ... Even with
json_mode=True,thinking_call()responses may be wrapped in```jsoncode fences. Script strips these before parsing. - SSH nohup for long-running LLM calls ... n8n SSH node must return immediately (webhook timeout). Use
nohup ... &pattern withechofor immediate response. - Full venv path in SSH ... Non-interactive SSH doesn't source
.bashrc, so use.venv/bin/python3instead ofpython. - n8n webhookId bug ... After
import:workflow, verifywebhook_entitytable has correctwebhookIdvalue.
Enabling Remediation Mode
When ready to switch from diagnose-only to active remediation:
- In the n8n workflow SSH command, change
--mode diagnoseto--mode remediate - Redeploy the workflow using the standard n8n deployment process
- Test with
--dry-runfirst to verify action selection
🎓 Missing Pixel Training Opportunities
| Module | Skills | Tier | Prerequisites |
|---|---|---|---|
| Server Diagnosis Lab | SSH, Linux troubleshooting, MariaDB, Bitnami stack, systemd | Tier 2 (Contributor) | Basic Linux CLI |
| Auto-Remediation Agent | Python (subprocess, JSON, argparse), Claude API, webhook architecture, safety engineering | Tier 3 (Specialist) | Python intermediate, API experience |
| n8n Webhook Workflow | n8n workflow design, SSH nodes, Code nodes, conditional branching | Tier 2 (Contributor) | n8n basics |
| Monitoring & Alerting | Betterstack API, REST APIs, uptime monitoring concepts | Tier 1 (Explorer) | API basics |
Portfolio project candidate: Build a simplified auto-remediation agent for a single service (e.g., restart a Docker container when health check fails). Demonstrates AI agent development, DevOps patterns, and safety-first engineering.
QTR Quietly Tracking ⭐ NEW
Added: February 27, 2026
Quietly Tracking (QTR) is a smart link + dynamic landing page + conversion attribution system. Create trackable links that resolve to beautiful, variable-driven landing pages. Track visits, conversions, and attribute results back to content strategies.
Architecture
Lovable Frontend (quietlytracking.org)
→ Supabase SDK
→ Supabase (<SUPABASE_PROJECT_ID>, us-west-1)
← Edge Functions (planned: render-landing-page, track-visit, track-conversion)
← n8n workflows (planned)
Ecosystem Position
QTR is the attribution/conversion arm of the QWF product family:
- QWR → QTR: Articles auto-generate smart links with
content_responsetemplates for CTAs - QKN → QTR: Outbound campaigns use QTR smart links for landing pages and conversion tracking
- QTR → QSP: Visit and conversion analytics push to SPOT dashboard
- L4G → QTR: Postcard QR codes resolve to
local_offertemplate landing pages
Smart Link Resolution Flow
User clicks link → quietlytracking.org/[slug]
→ Edge function: render-landing-page
→ Lookup slug in qtr_smart_links
→ Get template from qtr_link_templates
→ Interpolate: URL params → template variables → fallbacks
→ Record visit in qtr_page_visits
→ Return rendered HTML page
Template Types (MVP ... 6 pre-built)
| Type | Use Case |
|---|---|
content_response | Article → "Here's your next step" |
resource_download | "Download the guide you read about" |
event_registration | QR at event → registration form |
local_offer | Postcard QR → business offer |
quote_followup | Proposal link → accept/schedule |
contact_request | General inquiry with context |
MVP uses client-side template definitions (src/data/templates.ts) rather than DB-seeded rows. Template type stored as _template_type in variables JSONB.
Schema (v1.0.0)
7 tables: 3 QWF standard (supporter_partners, contact_submissions, bug_reports) + 4 QTR core:
| Table | Purpose |
|---|---|
qtr_link_templates | Reusable page designs with variable placeholders |
qtr_smart_links | Generated trackable links (template instances, slug-based) |
qtr_page_visits | Page view analytics (fingerprint, referrer, UTM, geo) |
qtr_conversion_events | Conversion tracking with attribution back to visits |
RLS: owner-only for templates/links, anonymous INSERT for visits/conversions/contacts.
Pricing
| Tier | Monthly | Annual |
|---|---|---|
| Starter | $79/mo | $790/yr |
| Pro | $149/mo | $1,490/yr |
| Agency | $249/mo | $2,490/yr |
No free tier. 30-day trial. QWF ecosystem bundle: Full Pro access included free with any paying QWF app subscription.
Current State (February 27, 2026)
| Component | Status |
|---|---|
| Supabase project | ACTIVE_HEALTHY ... <SUPABASE_PROJECT_ID> (us-west-1) |
| Domain | quietlytracking.org ... registered, DNS via Cloudflare |
| Lovable project | a404ee32-52c7-4781-8411-974ed9bdbaf7 |
| Schema | v1.0.0 ... 7 tables, RLS policies, indexes |
| Auth | Configured ... email/password + Google OAuth (shared QWF client) |
| QWF Passport | Secret set, edge function pending |
| Lovable Prompts | 7 total (001-002, 006-007 executed; 003-005 written) |
| Edge functions | Not yet deployed (render-landing-page, track-visit, track-conversion, verify-crossover-token, submit-contact-form) |
| Landing page | Deployed ... 14 sections, alpha gate, heritage, ecosystem, contact form |
| Accent color | Teal/Cyan (#06B6D4) |
Lovable Prompts
| # | Name | Status |
|---|---|---|
| 001 | Foundation + Auth + Onboarding | EXECUTED |
| 002 | Link Creator + Links Management | EXECUTED |
| 003 | Template Gallery | WRITTEN |
| 004 | Analytics Deep Dive | WRITTEN |
| 005 | Settings (Profile/Subscription/Brand) | WRITTEN |
| 006 | Alpha Stage (badge, bug reporter, landing page, contact form) | EXECUTED |
| 007 | Favicon + Site Meta | EXECUTED |
Reference
- GitHub Repo: Lovable-managed (via project
a404ee32-52c7-4781-8411-974ed9bdbaf7) - System Status:
002 Projects/_Quietly Tracking/QTR-System-Status.md - Development Plan:
002 Projects/_Quietly Tracking/QTR-Development-Plan.md - Lovable Prompts:
002 Projects/_Quietly Tracking/lovable-prompts/001-007
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Smart Link System Design | Database schema, slug resolution, variable interpolation, fallback chains | ⭐⭐⭐ |
| Template System | JSONB data modeling, dynamic HTML rendering, variable placeholders | ⭐⭐ |
| Conversion Attribution | Analytics pipeline, event tracking, funnel visualization | ⭐⭐⭐ |
| QR Code Integration | QR generation, print-to-digital bridge, campaign tracking | ⭐⭐ |
| Lovable Prompt Engineering | AI-assisted UI, iterative prompt design, component architecture | ⭐⭐ |
QQT Web Calculator Release Tools ⭐ NEW (2026-11-01, Session 872)
How a new version of the Quietly Quoting calculator widget reaches a supporter's live website, and the tools that make each
step guarded, proven and undoable. First used end to end for [Supporter Organization]'s go-live on 2026-11-01 (widget v17 live on
www.gate-log.md
(005 Operations/Handoffs/rounds/qqt-[Supporter Organization]-calcs-golive-20261030-0937-dfd9d1/).
The release path (each step opens on TIG's own word): build (widget dist-vNN, committed) -> upload (immutable
widget-assets/vNN/) -> read-only pre-proof on the live pages in a browser -> the sandbox (data on the review copies,
then one sandbox deploy, then proofs) -> the live data steps -> the supporter site's loader on the stable line -> ONE
repoint of the stable line -> read-only live proof. Card words are shared between live and sandbox rows, so they can
never be "sandbox only".
Tool (005 Operations/Execution/) | Version | What it does | Safety |
|---|---|---|---|
upload_qqt_widget_asset.py | v1.0.0 | Uploads one build file to widget-assets/<dest> | --no-clobber; never use it for the stable object |
publish_qqt_widget_stable.py | v1.4.0 | Repoints widget-assets/stable/widget.js (the shim every supporter page loads) to a release | Hand-kept release list (v11, v17) with full hashes; compare-and-swap (--expect-current); dry run by default, --apply writes; never below v11 |
snapshot_rows.py | v1.2.0 | Raw snapshot of the rows a supporter-tenant write changes (cards, review-rc, live-rc), straight to a file | Never prints values; refuses a redacted answer (the CLI masks hash-shaped text) |
make_cards_undo.py | v1.2.0 | Generates the exact undo for the card-words write from a raw snapshot | The undo restores only the changed fields, guarded by fingerprints |
make_tier_art_update.py | v1.2.0 | Writes the review-then-live SQL pair (and its reverse) for a new drawings folder | Validates every value; refuses non-ASCII |
check_live_slug_origins.py | v1.1.1 | Lists every host the supporter's key serves and flags any page still loading an old widget on a live slug | Fails CLOSED (any API or page error is a failure); GETs only |
check_qqt_review_copy_requests.py | v1.1.0 | Daily read-only check that names any sandbox review-copy request no proof made | NOT scheduled yet (TIG's word "schedule check" adds the cron line + Betterstack heartbeat) |
prove_qqt_[Supporter Organization]_site_pages.py | v1.8.0 | Proves [Supporter Organization]'s calculator pages in a real browser; --channel stable, --drawings, --timing, --materials, --consent | Read-only unless --submit; a live --submit texts [Supporter Organization]'s team, so it needs the "live proof" word and a warning first |
prove_qqt_widget_hosts.py | v1.0.0 | Proves which hosts the widget key serves (12/12) | GETs only; never prints the key |
deliver_qqt_calculator_alerts.py | v1.8.0 (LIVE) | The lead alerts; the homeowner's Timing answer leads the subject, text and Discord title | --preview composes without sending and hides contact details |
create_outlook_draft.py | v1.8.0 | Drafts TIG's emails; --attach now works on threaded replies | No send path, ever |
Writing a supporter's rows (the pattern every data step followed): raw snapshot -> regenerate the undo and check it
matches -> apply_supabase_migration.py <file> --project-ref <ref> --rehearse (runs against live, always rolls back) ->
apply_qqt_migration.py <file> -> read back at the database AND at the public API. Files carry no BEGIN/COMMIT so the
rehearsal can roll them back.
Deploying a supporter site QWF hosts ([Supporter Organization]): a fresh worktree from origin/main, the change as [skip ci]
commits, build with env -i PUBLIC_SITE_STAGE=production, prove every page byte-identical to the live build except the
intended ones, push HEAD:main first, then npx [email protected] pages deploy dist --project-name <project> --branch main --commit-hash <hash> with the token only in the child's environment, then compare every served page to the build
and confirm no CI run. Publish Log row each time.
Scratch tool worth knowing: .tmp/qqt_golive_release/r7/prove_stable_line.py v1.1.0 swaps a new widget into the
live pages inside the browser only (and can add a config map or preview draft words in the browser), so a release is
proven on the real site before anything changes there.
QWF Ecosystem Widget ⭐ NEW
Added: February 28, 2026
A living, interactive visualization of the entire Quietly Working Universe ... 52 entities across 7 categories, served as an embeddable widget with Shadow DOM isolation. One JavaScript file, 17 KB gzip, drops onto any QWF website. Includes per-entity Media Kit sections (logo downloads, social links, boilerplate copy) since v2.3.0.
Architecture
WordPress Site (any of 11 QWF sites)
→ [qwf_ecosystem] shortcode
→ widget.js loader (0.48 KB)
→ widget.bundle.js (17 KB, Preact + Shadow DOM)
→ GET /api/ecosystem (Digital Twin, port 8767)
→ ecosystem_registry.json (52 entities)
→ live metrics (Supabase, Betterstack, supervisors)
Two Display Modes
| Mode | Shortcode | Best For |
|---|---|---|
| Block (compact) | [qwf_ecosystem] | Footers, sidebars, "about us" sections. Shows category rings → click to expand grid → click entity for detail panel. ~180px collapsed. |
| Page (full) | [qwf_ecosystem mode="page"] | Dedicated ecosystem page. Full-height with sidebar filters, search, categorized grid, all categories visible. |
Entity Detail Panel
Expanding panel below clicked entity cards:
- Left side: Summary, highlight bullets, live metrics (uptime, health, success rate), MP Training Ground badge, Media Kit section, CTA button
- Right side: Interactive SVG connection graph ... radial node-link diagram with center node (current entity) and connected entities radiating outward
- Graph interactions: Hover for tooltip, click node with ↗ to open entity website, click node without ↗ to navigate to that entity's detail panel (cross-category navigation)
Media Kit (v2.3.0)
Each entity detail panel can include a "Media Kit" section with:
- Logos: Thumbnail previews with download buttons (filename preserved, not "true.png"). Supports PNG, SVG.
- Social: Platform icons (Instagram, Facebook, X, LinkedIn, YouTube, TikTok) with inline SVG and direct profile links.
- Boilerplate: Pre-written description with one-click "Copy" button for collaborators.
Media data is stored in detail.media in ecosystem_registry.json. 12 entities have media kits (all programs + apps). To add media to an entity, add a media object with logos, social, and/or boilerplate fields ... no code rebuild needed.
Connection System
- 273+ connections across 52 entities
- 100% resolution rate (every connection name maps to a real entity)
- Connections stored as name strings in
detail.connectionsarrays inecosystem_registry.json - Animated pulse particles travel along connection lines
Color Palette Customization
6 overridable CSS tokens via shortcode attributes:
| Attribute | Controls |
|---|---|
palette_bg | Widget outer background |
palette_bg_card | Cards, rings, panels, graph nodes |
palette_bg_hover | Hover/selected states |
palette_text | Primary text |
palette_text_muted | Secondary text, labels |
palette_border | Borders, dividers |
Example: [qwf_ecosystem palette_bg="#0c1629" palette_text="#f0e6d3" accent="#d4a843"]
Overrides applied as CSS custom properties on Shadow DOM root. Partial overrides fine ... unspecified tokens keep theme defaults.
All Shortcode Attributes
| Attribute | Default | Purpose |
|---|---|---|
mode | block | block or page |
theme | dark | dark, light, or auto (OS preference) |
accent | #2dd4bf | Stat values, CTA buttons, pulse bar |
categories | all | Comma-separated: app,program,system,infra,pedagogy,content,site |
featured | false | Show only featured entities |
refresh | 60 | API poll interval (seconds) |
page_url | none | "View All" link URL in block mode |
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Preact 10.x (3 KB React alternative) |
| Build | Vite 6.x, IIFE output, Terser |
| Isolation | Shadow DOM (no style leakage) |
| API | Digital Twin server (Python, port 8767) |
| Data | Static JSON registry + live metric merge |
| WordPress | mu-plugin, [qwf_ecosystem] shortcode |
Key Files
| File | Purpose |
|---|---|
100 Resources/ecosystem-widget/src/ | Widget source (11 Preact components, styles, types) |
100 Resources/ecosystem-widget/qwf-ecosystem-widget.php | WordPress mu-plugin |
005 Operations/Data/ecosystem_registry.json | Entity data (52 entities, single source of truth) |
005 Operations/Execution/digital_twin_server.py | API server (/api/ecosystem endpoint) |
Entity Registry
Edit 005 Operations/Data/ecosystem_registry.json to add/remove/modify entities. No code rebuild needed ... the API serves whatever is in the registry (60s cache). Identity fields (name, short_name, category, status, color_hex, emoji, tagline, website) auto-sync to hq_app_registry via sync_hq_app_registry.py v1.0.0 (runs daily in HQ Daily Sync n8n workflow v6.0.0). Operational fields in hq_app_registry (metrics, hosting, etc.) are preserved during sync.
7 categories: Apps (10), Programs (7), Systems (10), Infrastructure (5), Teaching (3), Content (4), Sites (11)
6 statuses: production, active, alpha, building, planning, standby
Build & Deploy
# Rebuild widget
cd "100 Resources/ecosystem-widget" && npm run build:all
# Deploy WordPress plugin update
scp "100 Resources/ecosystem-widget/qwf-ecosystem-widget.php" bitnami@<WP_SERVER_IP>:/tmp/
ssh bitnami@<WP_SERVER_IP> "sudo cp /tmp/qwf-ecosystem-widget.php /opt/bitnami/wordpress/wp-content/mu-plugins/"
# Cache bust: increment version in loader.ts + .php, rebuild, redeploy
Reference
- Live URL:
https://twin.quietlyworking.org/ecosystem/widget.js?v=2.3.0 - System Status:
002 Projects/_QWF Ecosystem Widget/Ecosystem-Widget-System-Status.md - User Manual:
002 Projects/_QWF Ecosystem Widget/User-Manual.md - Directive (landing section):
005 Operations/Directives/qwf_ecosystem_landing_section.md(separate ... Lovable apps only)
Transparency Site Native Version (Svelte/Astro)
Added: April 11, 2026
The transparency site (transparency.quietlyworking.org) has its own native implementation of the ecosystem widget ... a Svelte 5 island component that renders at build time using ecosystem.json data (no client-side API polling).
Component: src/components/islands/EcosystemFooterWidget.svelte (in QuietlyWorking/qwf-transparency)
Differences from WordPress widget:
| Feature | WordPress (Preact) | Transparency (Svelte) |
|---|---|---|
| Framework | Preact + Shadow DOM | Svelte 5 + client:visible |
| Data source | API polling (60s) | Build-time JSON |
| JavaScript | 17 KB bundle | Zero JS until scrolled into view |
| Mode | Block or Page | Page mode (always expanded) |
| Theming | CSS custom properties + Shadow DOM | Inherits site CSS variables |
Feature parity with WordPress widget:
- Page-mode layout with sidebar filters (search, category checkboxes, status filters)
- Entity cards with emoji icons, pulse indicators, taglines, MP badges
- Entity detail panel: summary, highlights, connection graph, live metrics, CTA buttons
- SVG connection graph with animated pulses, glow filters, hover tooltips
- Inline wiki links (auto-linkify entity names in text, longest-first matching)
- Media Kit: logo downloads, social SVG icons, boilerplate with copy button
- Animated stat counters (IntersectionObserver + requestAnimationFrame)
- Rainbow gradient pulse bar
prefers-reduced-motionsupport
Integration: Loaded in src/components/Footer.astro with client:visible directive. Container is full-width (no max-width constraint).
Key files:
| File | Repo | Purpose |
|---|---|---|
src/components/islands/EcosystemFooterWidget.svelte | qwf-transparency | The component |
src/data/ecosystem.json | qwf-transparency | Build-time data (fetched in CI) |
src/components/Footer.astro | qwf-transparency | Integration point |
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Widget Architecture | Preact, Shadow DOM, CSS isolation, IIFE bundles | ⭐⭐⭐ |
| SVG Data Visualization | SVG coordinate math, radial layouts, animation, interactivity | ⭐⭐⭐ |
| WordPress Plugin Dev | PHP shortcodes, mu-plugins, data attributes, multisite | ⭐⭐ |
| JSON Data Modeling | Entity registries, relationship graphs, schema design | ⭐⭐ |
| API Integration | REST polling, cache management, error resilience | ⭐⭐ |
| CSS Custom Properties | Theming systems, palette overrides, responsive design | ⭐⭐ |
Portfolio project candidate: Build a mini ecosystem widget for a different dataset (e.g., a student's personal project portfolio). Demonstrates frontend engineering, data visualization, and embeddable component design.
QWR Team Accounts System ⭐ NEW
Status: Deployed (Feb 27-28, 2026) Prompts: 068-079 (12 Lovable prompts)
What It Does
Transforms QWR from a single-user platform into a multi-user team collaboration system. Account owners can invite team members, assign them to specific brands, and control what they can do ... all while maintaining backward compatibility for solo users.
Architecture
Roles (4-tier hierarchy):
| Role | Can Do | Can't Do |
|---|---|---|
| Owner | Everything + billing + team management | ... |
| Admin | Manage team, create/edit content, all brands | Billing, delete account |
| Editor | Create/edit content for assigned brands only | Team management, settings |
| Viewer | View content and analytics for assigned brands | Create, edit, or manage anything |
Seat Allocation per Tier:
| Tier | Seats | Monthly |
|---|---|---|
| Starter | 2 | $99 |
| Growth | 5 | $299 |
| Agency | 15 | $799 |
Backward Compatibility: Solo users are unaffected. The get_account_id() helper function returns auth.uid() for users who aren't members of any team ... they ARE the account. This means zero migration needed for existing supporters.
Database Layer
4 new tables:
account_members... Team roster (role, status, invite token, expiry)member_brand_access... Which brands each member can accessteam_activity_log... Audit trail of team actionsapproval_requests... Content approval workflow queue
4 helper functions (STABLE SECURITY DEFINER):
get_account_id()... Returns the account a user belongs to (or self for solo users)get_user_role()... Returns user's role within their accountuser_has_brand_access()... Checks if user can access a specific brandget_team_member_count()... Current member count for seat enforcement
RLS Migration: All 37 existing tables migrated from auth.uid() to get_account_id() + user_has_brand_access() pattern. 76 old policies dropped and replaced with team-aware policies.
Invite Flow
- Owner/Admin clicks "Invite Member" in Team Settings
- Enters email, role, brand assignments
account_membersrow created (status='invited', invite_token generated, 7-day expiry)- n8n webhook
qwr-team-invitefires →qwr_team_invite.pysends invitation email via Graph API (from Ezer Aión) - Recipient clicks invite link →
/invite/:tokenpage (Lovable) accept-team-inviteedge function validates token, links user to account- Team Welcome page shows role, brands, capabilities
Infrastructure Components
| Component | Type | ID/Path |
|---|---|---|
accept-team-invite | Supabase Edge Function | verify_jwt=false |
qwr_team_invite.py | Python script (v1.0.0) | 005 Operations/Execution/ |
| QWR Team Invite Email Sender | n8n workflow | <WORKFLOW_ID> |
| Stripe Webhook Handler v1.3 | n8n workflow | <WORKFLOW_ID> |
Stripe Integration: v1.3 of the webhook handler sets max_team_members on supporter_partners during both new subscriptions (checkout.session.completed) and tier changes (customer.subscription.updated). This enforces seat limits: Starter→2, Growth→5, Agency→15.
Training Opportunities
| Skill | What Students Learn | Complexity |
|---|---|---|
| Row-Level Security | PostgreSQL RLS with helper functions, multi-tenant patterns | ⭐⭐⭐ |
| Role-Based Access Control | 4-role hierarchy, permission gates, brand-level scoping | ⭐⭐⭐ |
| Invite Flow Architecture | Token generation, expiry, edge function validation, email delivery | ⭐⭐⭐ |
| Backward Compatibility | Designing systems that don't break existing users | ⭐⭐ |
| Webhook Integration | n8n → SSH → Python → Graph API pipeline for email delivery | ⭐⭐ |
QWR's Stripe events and plan fields
How Stripe events reach QWR. Stripe posts to QWR's n8n handler. Before the handler acts, it checks Stripe's signature over the exact bytes received, refuses a request older than 5 minutes, and refuses a test-mode event. A refused request gets a 400 and changes nothing. A request with no signature is refused quietly, since anyone on the internet can send one. A request with a wrong signature posts at most one alert an hour to #system-status, with a count, because it can mean the stored signing secret has gone stale.
Who may change a plan. A signed-in person can edit only their own profile and onboarding fields. Plan, seat, credit and billing fields change only through the server, and a database trigger refuses anything else. The functions that spend or read credits check whose account it is, and the ones only the server should call are closed to everyone else.
How to re-check it. prove_qwr_plan_lock.py makes real calls as a visitor, a signed-in owner and the server, inside a transaction that always rolls back, so it changes nothing and needs no test account. It reports each check as pass or fail. tests/test_prove_qwr_plan_lock.py guards the proof itself.
MP Training Opportunities (from the 2026-11-03 QWR webhook and plan-field work and the 2026-11-04 home network comparison, Session 898)
| Task | Skills taught | Prerequisites | Time | Level |
|---|---|---|---|---|
| Verify a signed webhook on a test payload: compute the signature over the raw bytes, and show why re-encoded JSON fails | HMAC, HTTP, why bytes matter | JavaScript or Python basics | 2 h | Intermediate |
| Prove a database access rule by running real requests as each role inside a transaction that always rolls back, on a practice database | SQL, row and column rights, test design | SQL basics | 3 h | Advanced |
| Compare three products for one job: price the whole job, read a hands-on review, and grade each claim as federal record, independent test, vendor's words, forum or your own judgment | research, evidence grading, plain writing | None | 2 h | Beginner |
| Read a vendor's public data instead of its page: a store's product feed, a help center's article data, a federal list as JSON | REST APIs, JSON, source checking | REST basics | 1 h | Beginner |
QWF Documentation Standard ⭐ NEW
Added: March 2, 2026
Defines the structure, quality criteria, and maintenance rules for all user-facing documentation across QWF apps. Every "Quietly ___" app must have a User Manual (Markdown, in the vault) and an In-App Documentation Center (/docs, built in Lovable).
Two-Layer Documentation Model
| Layer | Format | Location | Authority |
|---|---|---|---|
| User Manual | Markdown | 002 Projects/_[App] Projects/[App]-User-Manual.md | Source of truth |
| Documentation Center | App components (React or SvelteKit) rendering a bundle generated from the manual by render_<app>_docs.py | /docs route in the app (public) | Derived from manual |
Source of Truth Rule: The User Manual is always the master document. The /docs center renders manual content with visual elements (cards, diagrams, interactive navigation). When they drift: update the manual first, then create a /docs sync prompt. Never update /docs independently.
Sync Cadence (reworded 2026-09-22 on TIG's "adopt"): re-run the renderer and commit the bundle in the SAME session as any manual change; the app's proof suite runs render_<app>_docs.py --check and fails on drift, so /docs can never quietly disagree with the manual. The old "sync prompt every 5+ Lovable prompts" cadence survives only as the legacy path for apps that have no renderer yet (QWR today).
Required Sections (14 total, in order)
Every QWF app User Manual must include: (1) Table of Contents, (2) What Is [App]?, (3) Key Concepts (supporter-partner framing), (4) Getting Started (step-by-step + flow diagram), (5) Core Feature Sections (user journey order), (6) Settings (one subsection per tab), (7) The Landing Page, (8) FAQ (15+ entries, 4+ categories), (9) Troubleshooting (5+ entries), (10) Keyboard Shortcuts, (11) Getting Help (identical across apps), (12) Glossary, (13) Release History, (14) Planned Updates.
Pricing Comparison Chart (Required)
Every subscription app manual includes a two-part pricing comparison: (1) "Every Tier Includes the Full Platform" ... grouped feature list reinforcing full access at every tier, (2) "What Differs by Tier" ... compact table showing only volume limits and access features.
Quality Checklist (12 checks)
Completeness, accuracy, consistency, terminology, pricing, diagrams, cross-references, TOC currency, troubleshooting, release history, /docs sync, planned vs. shipped.
Key Files
- Directive:
005 Operations/Directives/qwf_documentation_standard.md - Model manual:
002 Projects/_QWR Quietly Writing App/QWR-User-Manual.md(v4.0.1); rendered reference implementation002 Projects/_Quietly Quoting/QQT-User-Manual.md(v1.0.2) - Renderer:
005 Operations/Execution/render_qqt_docs.py(manual ->src/lib/docs/manual.json; dry-run default,--write,--checkexits non-zero on drift; reusable for any QWF manual via--manual/--app) - Proof:
005 Operations/Execution/prove_qqt_docs.py(deployed url, both engines; every H2 renders once, sidebar mirrors the manual, active section follows scroll, no auth, Help links resolve, no shell elements on the public page, drift gate red on a removed heading and on a hand-edited bundle) - CLAUDE.md: Added as Foundational Directive (v1.30.0)
Implementation Status
| App | User Manual | /docs Center | Notes |
|---|---|---|---|
| QWR (Quietly Writing) | ✅ v4.0.1 | ✅ v4 (Prompt 088) | Hand-synced by prompt; no renderer yet (the legacy path) |
| QQT (Quietly Quoting) | ✅ v1.0.2 | ✅ LIVE at quietlyquoting.org/docs (2026-09-22) | First SvelteKit Documentation Center and the first rendered one. Bundle generated by render_qqt_docs.py from the manual, committed, drift-gated by --check; proof prove_qqt_docs.py (43 checks, chromium + webkit). Help links: office sidebar, Field eyebrow, first-sign-in alpha note |
| QNT (Quietly Networking) | Not started | Not started | ... |
| Others | Not started | Not started | ... |
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| User Manual Writing | Technical writing, documentation structure, Markdown | ⭐⭐ |
| Quality Audit | Cross-referencing, accuracy verification, gap analysis | ⭐⭐ |
| Writing a manual FROM the live app | Browser automation walk-throughs per role, evidence capture, "never document what you did not see" | ⭐⭐⭐ |
| Markdown-to-bundle renderer | Python text processing, deterministic output, structure-driven element mapping, drift gates | ⭐⭐⭐ |
| /docs Sync Prompts (legacy) | Lovable prompt engineering, React component design | ⭐⭐⭐ |
Weavy Creative Production System ⭐ NEW
Added: March 4, 2026
Weavy (weavy.ai) is a node-based AI creative workflow platform used for QWF visual production ... product photography, lifestyle shots, character consistency, and video generation. All QWF visual production workflows are built in Weavy and documented in a dedicated user manual.
Architecture
Weavy Platform (weavy.ai)
→ Node-based workflows (text, image, LLM, generation nodes)
→ AI Models (Flux 1.1 Ultra, Nano Banana Pro, GPT Image 1, Gemini 2.5 Flash, etc.)
→ Post-processing (Bria BG Remove, Magnific Upscale, Relight)
→ Final assets for print/web/social
Methodology Source
Rory Flynn (Systematiq AI) is the primary methodology source. His newsletter archive and workflow breakdowns provide the prompt patterns, compositing pipelines, and workflow architectures that underpin all QWF visual production. See 003 Entities/Experts/Rory Flynn.md.
Key principles adopted from Rory's work:
- JSON structure over prose prompts
- Material physics over aesthetic descriptions
- Camera specifications as model filters
- Negative constraints as fences
- Three-tier prompt maturity (V1 Curated → V2 Variance → Active Production)
Active Workflows
| Project | Workflow | Status | Notes |
|---|---|---|---|
| [Supporter Organization] [Supporter Mascot] | 3-phase plushie vendor reference | Phase 3 operational | Hero shot, turnaround grid, lifestyle shots |
| L4G Postcard Ads | Ad creative generation | Planned | Missing Pixel student training component |
| WOH Product Shots | Product photography pipeline | Planned | War on Hopelessness merchandise |
Weavy User Manual
A comprehensive 17-chapter + appendices user manual lives at 004 Knowledge/How-To/Weavy-User-Manual.md (v2.0.0). Covers:
- 12 prompt patterns (JSON-structured, material physics, camera specs, multi-set generation, etc.)
- 2 complete case studies (Sandwich Tornado compositing, AI GPT Photoshoot pipeline)
- Node types, model comparison, credit costs, glossary
- 4-week learning path with prioritized Systematiq newsletter issues
Key Files
| File | Purpose |
|---|---|
004 Knowledge/How-To/Weavy-User-Manual.md | Comprehensive Weavy user manual (v2.0.0) |
005 Operations/Directives/weavy_creative_workflows.md | Creative production directive (v1.5.0) |
003 Entities/Experts/Rory Flynn.md | Methodology source entity |
002 Projects/_[Supporter Organization] Projects/[Supporter Mascot]-Plushie-Weavy-Workflow.md | [Supporter Organization] [Supporter Mascot] 3-phase workflow spec |
002 Projects/_[Supporter Organization] Projects/[Supporter Mascot].md | [Supporter Mascot] character bible |
App Mode (Missing Pixel)
Weavy offers an App Mode that provides a simplified interface for students: single-step execution with pre-configured parameters. Training progression: App Mode (beginner) → Workflow Editor (intermediate) → Custom Workflow Builder (advanced).
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| JSON Prompt Engineering | Structured prompting, material physics, camera specs | ⭐⭐ |
| Node-Based Workflow Design | Visual programming, data flow, input/output wiring | ⭐⭐ |
| AI Model Selection | Cost/quality tradeoffs, model strengths, credit budgeting | ⭐⭐ |
| Multi-Phase Production Pipeline | Project planning, phase progression, asset management | ⭐⭐⭐ |
| Compositing Workflows | Background removal, element layering, lighting unification | ⭐⭐⭐ |
WHL WHELHO App ⭐ NEW
Added: March 18, 2026 · Major rewrite: April 17, 2026 (SvelteKit migration + health module + nutrition system) · UX pass: April 20, 2026 (user-local timezone, mobile nav, /you hub)
April 20, 2026 ... UX Pass
Three architectural upgrades landed in rapid succession after TIG noticed a day-of-week drift bug (the tennis-day water bump fired on the wrong calendar day when Pacific and UTC disagreed late in the evening).
1. User-local timezone architecture (8 commits across Phases 1, 2, 4). Every WHELHO user profile now carries timezone (IANA name), home_city (display-only), and tennis_days (SMALLINT[] of day-of-week indices). src/lib/timezone.ts generalized with getLocalDayOfWeek/Hour/DayKey(tz) helpers ... Pacific is the fallback, never the assumption. Food page's isTennisDay(), streak day-keys, inferMealType(), and afternoon nudge all now read the user's tz. Onboarding auto-detects browser tz via Intl.DateTimeFormat().resolvedOptions().timeZone and lets the user confirm/edit. A new <TravelBanner> compares browser tz vs profile tz on every portal page and offers a one-tap switch when they differ (transparency-first ... never silently follows device). The backoffice gained a check_timezone_hygiene.py guard script scanning any supporter-app src/ for raw new Date().getDay()/getHours()/getDate() patterns. timezone_standard.md amended to document two operating modes (Pacific for backoffice, user-local for supporter apps). qwf_app_family_standard.md section 3 split into 3a (user-local) + 3b (Pacific legacy). WHELHO is the canonical reference.
2. Navigation refactor ... PlanetNav desktop breadcrumb + mobile bottom tabs + /you hub. Deep portal pages were requiring back-back-back navigation to reach anywhere outside the current branch. New PlanetNav.svelte renders a sticky top bar on every portal page except /dashboard (custom header) and /onboarding ... top-left Nano A orb links to dashboard, clickable breadcrumb trail reconstructed from URL segments, right-side "You" link. New BottomTabs.svelte pins 5 tabs to the bottom of every portal page on mobile only (sm:hidden): Planet · Health · Library · You · +Log. +Log opens a QuickLogSheet.svelte bottom sheet with water quick-log chips (tap container → POSTs logFromPantry → banner confirm → zero navigation) and a 2×2 nav tile grid (Food/Scan/Vitals/Intake). Dashboard's custom top header wrapped in hidden sm:flex so mobile shows only the planet; Ezer + Bug Report moved to /you to preserve mobile access. New /you route is the identity & settings hub ... planet name, email, Time & Location (moved wholesale from /health/profile), Ezer + Bug Report, Sign out. /health/profile shrinks to body stats + daily nutrition targets only.
3. Pantry + Food log polish.
- Pantry one-tap log icon: ClipboardPlus button on every pantry row's action cluster. POSTs to
logFromPantrywith meal_type inferred from user-local hour; 3s success banner; no navigation. - Water bar on past days: Navigating to any past day now shows that day's water intake, that day's goal, and that day's extra-hydration bump. Computed client-side per the user's timezone +
tennis_days. Streak flame hidden on past days. Tap-to-log disabled ... bar renders as<div>viasvelte:elementwith opacity reduction for read-only affordance. - Day-nav arrow fix: Day label now uses
min-w-[10rem] sm:min-w-[11rem] text-center tabular-numsso the right chevron stops drifting as labels change width. Click-click-click forward works identically to backward.
Design principles encoded this session (now memory feedback entries ... apply to every QWF app):
- Meaningful WHY for every permission/data ask ... opt-in default, always-editable in settings (
feedback_meaningful_why_for_permissions.md) - Ask once on context change, always editable ... travel/locale: ask plain question, remember answer, never silently follow device (
feedback_ask_once_always_editable.md)
Added: March 18, 2026 · Major rewrite: April 17, 2026 (SvelteKit migration + health module + nutrition system)
WHELHO is a personal development app built around the planet metaphor ... your life as a celestial body with 8 realms, a values-driven core, and elements that orbit between crust (where you are) and core (where you're pulled). It uses Spline 3D for real-time planet visualization and serves as both a FORGE product fuel line and the Missing Pixel pre-student pipeline gatekeeper.
As of April 17, 2026 the app also ships a full personal health management platform within the Body realm ... bloodwork intelligence, pantry-backed food logging, water tracking, body-stats-derived daily targets, chronic-condition management, surgical timeline, immunizations, allergies, insurance, family history, and emergency contacts. This module is production-used daily by TIG.
Architecture
Cloudflare Pages (whelho.org)
→ SvelteKit 2.57 + Svelte 5 runes + TypeScript + @sveltejs/adapter-cloudflare
→ Spline 3D (@splinetool/runtime, dynamic onMount import)
→ @supabase/ssr (SSR cookie-based auth, 1-year persistent sessions)
→ Supabase PostgreSQL (nvimpjmhiondaxtrwlny, us-west-1) with RLS per user_id
← Edge Functions (submit-contact-form)
← Claude Opus 4.7 API for lab/photo/text extraction
← Open Food Facts API for barcode lookup
← ZXing for in-browser barcode scanning
Migrated React 18 → SvelteKit on 2026-04-16 with zero data loss (Supabase untouched). Rollback tag react-final-20260416 pinned in case of emergency.
Ecosystem Position
WHELHO occupies a unique dual role in QWF:
- FORGE Fuel Line: Premium personal development tool generating fundraising revenue alongside QWR, QQT, L4G
- MP Gatekeeper: The Values Discovery journey qualifies users for Missing Pixel ... "accepting 100% responsibility for everything in your life" is demonstrated through the app, not declared on a form
- Precious Monster Transformation: During Values Discovery CHOICE phase, users who genuinely commit to their values become "Precious Monsters" ... the bridge to Missing Pixel
Key Concepts
- Planet Metaphor: User's life = a planet with 4 layers (Core → Mantle → Crust → Atmosphere) and 8 realms. Core values create gravitational pull. Elements orbit between crust (current state) and core (aspirational state). The Atmosphere represents external factors users can't control but must account for.
- Density = Stability: The denser the core values, the more inherently stable the planet ... atmospheric storms don't wobble a planet with a dense core. This is the central physics teaching: the planet shows users their growth without explanation.
- 8 Realms: Spirit, Mind, Body, Relationships, Money, Recreation, Work, Charity ... each a segment on the 3D planet surface
- Values Discovery Flow: Excavation → Illumination → Recognition → Choice → Commitment ... AI-assisted pattern detection surfaces values from freeform responses
- Two-Track Development: Track A (React code, done by code students) and Track B (Spline 3D scene design, done by 3D art students) merge at integration
- Spline Integration: Planet designed in Spline's browser editor, embedded via
@splinetool/react-spline. Events:onSplineMouseDownfor realm clicks,emitEvent()for animations, Variables API for dynamic control - Own Brand:
whelho.org(not a "Quietly ___" app), but follows QWF App Family patterns
Current State (April 17, 2026)
| Component | Status |
|---|---|
| Supabase project | ACTIVE_HEALTHY ... nvimpjmhiondaxtrwlny (us-west-1, free tier) |
| Domain | whelho.org ... Live on CF Pages, SSL valid |
| CF Pages project | whelho ... GitHub Actions auto-deploy on push to main |
| GitHub repo | QuietlyWorking/whelho (private) |
| Database schema | v2 ... 28+ tables with RLS |
| Auth | @supabase/ssr + 1-year persistent cookies on both server + browser client |
| Phase 0: Foundation | ✅ Complete |
| Phase 1: Planet + Onboarding | Track A ✅ (SvelteKit rewrite), Track B (Spline scene) design brief complete, not yet built |
| Alpha stage | ✅ Deployed ... invitation-only gate, access request form, bug reports, alpha badge |
| Contact form edge function | ✅ Deployed ... submit-contact-form with honeypot, rate limiting, duplicate detection |
| Health module | ✅ Production-used ... 13 sections (food log + pantry + vitals + bloodwork + conditions + procedures + allergies + immunizations + insurance + family history + emergency contacts + providers + medications + referrals) |
| Nutrition system | ✅ Production ... barcode scan + OFF lookup, Claude vision label extraction, Claude text-describe, 30 quick-picks, auto-pantry, 27-key micronutrient tracking, body-stats-derived target rings, educational NOVA/Nutri-Score popups |
| Water tracking | ✅ Production ... named containers, vertical water bar on Today card, streak flame, afternoon nudge, tennis-day bump |
Database Schema (28+ Tables)
Core identity:
| Table | Purpose |
|---|---|
profiles | User profiles, planet name, subscription status |
values | Core values discovered via Values Discovery |
realms | 8 realms per user |
elements | Items within realms (now extended with slug/description/goal_kind/target_value/icon for health-alignment goals) |
element_values | Element-value connections (magnetic pull) |
discovery_responses | Values Discovery freeform answers |
checkins | Conversational reflections on elements |
breadcrumbs | Purpose Window insights |
student_applications | Student verification |
planet_snapshots | Year-over-year planet state |
contact_submissions | Access requests |
bug_reports | Alpha/beta bug reports |
Health module (added 2026-04):
| Table | Purpose |
|---|---|
health_profile | Body stats (sex, DOB, height_cm, weight_kg, activity_level, calorie_goal, custom targets, daily_water_target_oz) ... drives all daily-target rings |
health_food_log | Every eaten item with full macros + detailed fats + added sugar + cholesterol + ingredients + allergens + facility allergens + 27-key micronutrients JSONB + NOVA + Nutri-Score + photo + alignments to realm elements |
health_meal_templates | Pantry of frequently-eaten items (auto-populates on every log, dedupes by barcode/name). Shares nutrition fields with health_food_log plus photo_url, back_photo_url, emoji, use_count, last_used_at |
food_element_alignments | Many-to-many food↔realm-element linkage (supports/undermines with direction) |
health_providers | Care team with address, phone, portal URL, specialty, primary-care flag |
health_medications | Prescriptions with dosage, frequency, prescriber link |
health_conditions | Chronic + active diagnoses with category/status/severity/diagnosed_at/primary_provider |
health_condition_medications | Join table linking conditions to their managing medications |
health_procedures | Surgical + implant + diagnostic timeline with is_implant flag + device_details |
health_lab_reports | Uploaded labs with Opus 4.7 extracted biomarkers (JSONB) + ai_summary + red_flags |
health_referrals + health_referral_milestones | 6-step specialist workflow with auth-letter photo capture |
health_vitals | Row-per-metric capture of 22 measurement types with reading_group_id for multi-metric bundles |
health_allergies | Substance × category × severity 4-tier |
health_immunizations | Vaccine history with dose number/total, manufacturer, lot, next-due |
health_insurance | 6 coverage types with carrier/plan/member/group/phone/effective-dates |
health_family_history | 16 relation types × condition × age_at_onset × deceased flags |
health_emergency_contacts | Tappable tel/mailto with primary flag + display order |
health_shares | Time-limited public share links for sending labs to family/caregivers |
health_ai_insights | Cached AI-generated feeling patterns + food-lab correlations (reserved for Phase 5) |
Storage: medical-docs private bucket with per-user folder RLS isolation. Stores lab PDFs, pantry front+back photos, food-log photos.
Phase Roadmap
| Phase | Scope | Status |
|---|---|---|
| 0: Foundation | CF Pages + Supabase, auth, deploy pipeline, cosmic theme | ✅ Complete |
| 1: Planet + Onboarding | Spline 3D planet, formation animation, realm interactions | Track A (code) done; Track B (Spline scene) pending |
| 2: Values Discovery | Excavation → Commitment arc, AI-assisted pattern detection | Not Started |
| 3: Realms + Elements | Realm zoom, element CRUD, crust/core positions | ✅ Elements CRUD done; realm zoom + crust/core viz pending |
| 4: Health module (inserted ... Body realm went deep) | Bloodwork + food log + vitals + conditions + procedures + allergies + immunizations + insurance + family history + emergency contacts + providers + medications + referrals + water tracking + educational overlays + daily target rings | ✅ Shipped 2026-04-16 → 2026-04-17 |
| 5: Living Practice + Atmosphere | Conversational check-ins, progress on planet, celebration particles, atmosphere layer | Not Started |
| 6: Purpose Window | Work + Charity pattern detection, breadcrumb surfacing | Not Started |
| 7: Polish + Launch | Mobile optimization, MP bridge, beta, landing page, launch | Not Started |
Reference
- GitHub Repo:
https://github.com/QuietlyWorking/whelho(private, direct code commits) - System Status:
002 Projects/WHELHO App/WHL-System-Status.md - Project Brief:
002 Projects/WHELHO App/WHELHO-App-Project-Brief.md - Vision Doc:
002 Projects/WHELHO App/WHELHO-App-Vision.md - Development Plan:
002 Projects/WHELHO App/WHELHO-Development-Plan.md - Spline Scene Brief:
002 Projects/WHELHO App/WHELHO-Spline-Scene-Brief.md - Local Code:
/home/<VM_USER>/whelho/
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Spline 3D Planet Design (Track B) | 3D modeling, materials, lighting, particles, camera choreography | ⭐⭐⭐ |
| SvelteKit + Supabase SSR auth | Svelte 5 runes, cookie-based sessions, RLS policies, adapter-cloudflare | ⭐⭐⭐ |
| Claude vision pipeline (photo → structured JSON) | Multimodal API calls, adaptive thinking, structured extraction, confidence grading, variant disambiguation | ⭐⭐⭐ |
| Claude text-describe with fallback heuristics | Prompt engineering, JSON schema enforcement, ambiguity detection patterns | ⭐⭐⭐ |
| Schema migrations + PostgREST cache | ALTER TABLE IF NOT EXISTS patterns, NOTIFY pgrst reload, RLS policy design, JSONB vs. normalized columns | ⭐⭐ |
| Data backfill from markdown to Supabase | Regex parsing of tabular markdown, bulk INSERT batching via Management API, dedup by normalized keys | ⭐⭐ |
| Client-side image compression (Canvas + fallback chain) | createImageBitmap / HTMLImageElement decoder chain, EXIF orientation, quality vs. readability tradeoffs | ⭐⭐ |
| Mobile-first UI patterns | Horizontal-scroll → grid + chips + see-all, dropdown menu consolidation, tappable address deep links, water bar + bottom sheet | ⭐⭐ |
| Persistent session gotcha (both-sides cookies) | @supabase/ssr cookieOptions on createBrowserClient AND hooks.server.ts setAll | ⭐⭐⭐ |
| PWA service worker + install prompt flow | Manifest, beforeinstallprompt, iOS vs Android install UX, 14-day dismissal cooldown | ⭐⭐ |
| Storage orphan cleanup | Set-difference between bucket listing and DB-referenced paths, Supabase Storage REST delete | ⭐ |
| Values Discovery UX (future) | Conversational UI, multi-step flows, AI pattern detection | ⭐⭐⭐ |
| Purpose Window Algorithm (future) | Pattern matching, breadcrumb surfacing, data visualization | ⭐⭐⭐ |
Cloudflare & DNS Management ⭐ NEW
Added: March 18, 2026
Cloudflare manages DNS, Pages hosting, and Workers for all QWF domains. This section documents the dual-token pattern, zone management, and common operations.
Unified API Token
QWU uses one Cloudflare API token covering all operations:
| Token | Env Var | Permissions | Use For |
|---|---|---|---|
| Account Token | CLOUDFLARE_API_TOKEN | Pages Edit, Workers Scripts, DNS Edit/Read, SSL Edit/Read, WAF Edit/Read, Zone Edit/Read, Zone Settings, Access (apps/policies/groups), Waiting Rooms, Load Balancers, Zone Versioning, Analytics, Logs | All CF operations: deploys, project management, DNS CRUD, SSL, zone admin, Access apps |
History: Session 119 (2026-03-10) created a Pages-only Account API Token for QWR's Lovable-to-CF-Pages migration. A separate DNS-only token (CLOUDFLARE_API_TOKEN_OLD) was kept for DNS operations. On 2026-04-19 the main token's permissions were expanded to cover everything QWU needs, and the old DNS-only token was retired.
Does NOT have: User-scope permissions (can't revoke tokens via API ... must use the dashboard). Redirect Rules (rulesets) permissions ... host-based redirects go through CF Pages Functions middleware instead.
Account & Zone Registry
| Domain | Zone ID | Hosting | Notes |
|---|---|---|---|
quietlyworking.org | ... | CF Pages | WP Legacy frozen copy since 2026-10-23 (project quietlyworking-org); subdomains host n8n, HQ, portal, status and more |
quietlyquoting.org | ... | Lovable | QQT |
quietlyknocking.org | ... | Lovable | QKN |
quietlyspotting.org | ... | Lovable | QSP |
quietlytracking.org | ... | Lovable | QTR |
quietlynetworking.org | ... | CF Pages | QNT (migrated 2026-03-20) |
locals4good.org | ... | CF Pages | L4G (migrated 2026-03-19) |
whelho.org | 4e73ca94aad582ed7157175b5a1f6fca | CF Pages | WHELHO |
<chapter-domain> | ... | CF Pages | [Networking Chapter] chapter site (CNAME → aim-high-bni.pages.dev) |
preciousmonster.org | ... | ... | Reserved (MP lore) |
preciousmonster.com | ... | ... | Reserved (MP lore) |
Cloudflare Account ID: see CLOUDFLARE_ACCOUNT_ID in .env
Supporter zones on the same account: [Supporter Organization] has 40 subsite zones (e.g., <supporter-news-domain>, <supporter-news-domain>, …) on this CF account ... delegated admin for a supporter system. Do not modify [Supporter Organization] zones unless the task is explicitly authorized for [Supporter Organization] infrastructure. See 002 Projects/_[Supporter Organization] Projects/ and CLAUDE.md → "Supporter Systems". [Supporter Organization] migration from Namecheap DNS to CF completed 2026-04-18 ... see [Supporter Organization]-Cloudflare-Migration.md for the full zone list, IP-lockdown pattern, and Phase 4 hardening artifacts.
DNS Management Script
Script: 005 Operations/Execution/cloudflare_api.py (v1.0.0)
Full CRUD for DNS records. Supports:
- List all records for a zone
- Create A, AAAA, CNAME, TXT, MX records
- Update existing records
- Delete records
- Toggle proxied/DNS-only status
Credential selection (--token-env, added 2026-07-28). The script defaults to CLOUDFLARE_API_TOKEN, which only sees QWU-owned zones. Supporter zones are invisible to it and fail with a misleading "Zone not found: <domain>" ... which reads as "the zone does not exist" rather than "this credential cannot see it." Point the script at the right credential by env-var name:
# QWU-owned zone (default ... no flag needed)
python "005 Operations/Execution/cloudflare_api.py" records --domain quietlyworking.org
# [Supporter Organization]-owned zone (supporter system)
python "005 Operations/Execution/cloudflare_api.py" records \
--domain <supporter-domain>.com --token-env GC_CLOUDFLARE_API_TOKEN
The flag takes the variable name, never a token value. Reads on registered supporter zones have standing permission; any write to a supporter zone needs task-level permission from TIG at the moment of the write (see the Supporter Systems guardrails in CLAUDE.md), and a primary-URL DNS change needs explicit execution-time approval every time.
CF Pages Deployment
Apps on CF Pages deploy via wrangler pages deploy:
# Deploy from local build (WHELHO only — pending GH Actions secrets)
cd /home/<VM_USER>/whelho && npm run build
npx wrangler pages deploy dist --project-name=whelho
# QWR, L4G, QNT, [Networking Chapter] deploy via GitHub Actions (auto on push to main)
# WHELHO deploys via direct wrangler (GH Actions secrets pending)
Common Operations
Check DNS records:
TOKEN=$(grep "^CLOUDFLARE_API_TOKEN=" .env | cut -d'=' -f2- | tr -d '\r\n ')
ZONE_ID="4e73ca94aad582ed7157175b5a1f6fca"
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" | python3 -m json.tool
Create/update DNS record:
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"type":"CNAME","name":"www","content":"whelho.pages.dev","proxied":true}' \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records"
Known Issues
CLOUDFLARE_API_TOKENdoes NOT have User-scope permissions (can't revoke tokens via API ... must use the dashboard) or Redirect Rules (rulesets) permissions ... use CF Pages Functions middleware for host-based redirects instead- GitHub fine-grained PATs with
admin: Truedo NOT include Actions Secrets management ... a separate "Secrets" permission is needed to set repo secrets via API - WHELHO's GitHub Actions deploy workflow exists but fails until
CLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_IDsecrets are set in the repo
WP Legacy Sites on Cloudflare Pages ⭐ NEW
Added: October 23, 2026 (Session 777)
On the night of 2026-10-22, PHP2Shell (CVE-2026-109618 / 109619, pre-auth remote code execution in PHP itself) was being exploited widely, and QWF's WordPress server ran a Bitnami PHP 8.2.7 that can never be patched in place. Instead of building a new WordPress server, TIG chose to leave WordPress: every live site became an exact static copy on Cloudflare Pages, the web addresses moved, and the old server stopped answering the internet. TIG calls these sites "WP Legacy"; they will be rebuilt on the new stack at his pace, using the copies as the verbatim source.
What runs where
| Site | Pages project | Notes |
|---|---|---|
| quietlyworking.org (+ www) | quietlyworking-org | Security page and every other page byte-for-byte; the old Locals 4 Good Google Sheet table is a saved snapshot |
| heroeskids.org (+ www) | heroeskids-org | Also answers childrenoffallensoldiers.com (301 to heroeskids.org, as before); 130 footer redirects WordPress used to do |
| missingpixel.org (+ www) | missingpixel-org | Slider scroll-video frames copied whole |
| quietlyworking.us (+ www) | quietlyworking-us | |
| iysr.org, how-tosandreviews.com (+ www) | iysr-org, how-tosandreviews-com | |
| legacy.chaplaintig.com | legacy-chaplaintig-com | chaplaintig.com itself is the SvelteKit site, untouched |
| whelho.com, adventurewithheroes.com, ruapod.com | qwf-domain-redirects | 301 to whelho.org, heroeskids.org, chaplaintig.com (they used to dead-end) |
Copies, reports and screenshots live in /home/<VM_USER>/wp-legacy-freeze/. The old server (qwu-wpmu) was DELETED on 2026-10-25 and its address released; every job that reached it is off. Everything it held is in the private Azure container qwu-wpmu-final-archive. To change a page, edit the copy and redeploy with deploy_frozen_site.py.
What changed for visitors (deliberately)
- Pages look the same. Logins, comments and search stop; the donation thermometer, testimonials and the Sheet table show their 2026-10-23 values.
- Contact forms stopped lying, then started delivering. WordPress had shown "Thanks for contacting us" for about three years while every message vanished (no mail path on the server). Since 2026-10-23 (TIG: "deliver", "bless") the 7 Divi forms on quietlyworking.org, missingpixel.org and heroeskids.org hand each message, server to server, to Quietly Webbing's
submit-site-form, which saves it and passes it to the family contact pipeline: TIG gets a notice, the person gets the blessed reply ("Got it...", or for an email-only signup "Thank you for staying close...", which promises no newsletter). "Sent" shows only after the message is saved; any failure says "please email us" and is counted. Bots meet a hidden trap field, a signed time token and the page's math question. Discord gets the site and page only, never a person (youth rail). check_qwf_site_forms.pywalks every form every 10 minutes and alerts TIG (TODAY) if one stops or a visitor is turned away. The old [Networking Chapter] RSVP page was removed on TIG's word. Full record:QWU-WPMU-Freeze-Move-Plan.mdsection "Forms". - Six oversized media files were re-encoded to fit Pages' 25 MiB limit (originals kept).
Tools
| Script | What it does |
|---|---|
freeze_wp_site.py | Crawls a live site (sitemaps + links the site answers 200), turns links WordPress redirects into redirect rules, copies Slider Revolution runtime folders over read-only SSH, closes asset gaps, normalizes names, restores Cloudflare-obfuscated emails, shrinks oversized media, writes _headers / _redirects / _routes.json |
deploy_frozen_site.py | Creates the Pages project if needed and deploys the copy with its one edge function (www and alias redirects, honest form and comment replies, noindex on *.pages.dev) |
verify_frozen_site.js | Loads each page live and from the deployment in Chromium, compares pixels, lists files the copy lacks |
cutover_wp_legacy_to_pages.py | Dry run by default; --apply moves the web records (MX and everything else untouched) and saves a rollback file; --rollback <file> restores in under a minute |
Editing a page now: edit the copy under sites/<host>/public/ and redeploy with deploy_frozen_site.py --host <site>. Do not use wp-admin. Re-freezing from the private WordPress is not possible yet: the freeze tool crawls the public hostname, which now answers from the copy. Full record, deliberate differences and every rollback: 002 Projects/_QWU WordPress Multisite/QWU-WPMU-Freeze-Move-Plan.md.
Only the live copy exists (2026-10-31). Each form site's Pages project (quietlyworking-org, missingpixel-org, heroeskids-org) holds only its live deployment: every older one was deleted after Turnstile's first clean week, because old copies kept taking messages without the bot check at their own pages.dev address and none was a safe rollback. To undo a change, deploy the copy fresh (--no-turnstile only if the bot check itself breaks). Since deploy_frozen_site.py v1.3.0 a copy takes a form post only at the site's own address (a signed canary from check_qwf_site_forms.py excepted), so a preview cannot become a second door into TIG's inbox; prove a preview with check_qwf_site_forms.py --base SITE=URL.
MP Training Opportunities ⭐ (from the 2026-10-23 WordPress exit)
| Task | Skills taught | Prerequisites | Time | Complexity |
|---|---|---|---|---|
| Freeze a CMS site into static files | HTTP, crawling with wget, finding files JavaScript loads later | Command line, basic HTML | 3-4 h | Intermediate |
| Prove a copy matches the original | Playwright, screenshots, pixel diffs, telling a real defect from an animation | JavaScript basics | 3 h | Intermediate |
| Move a domain with a one-command undo | DNS records, Cloudflare API, proxied CNAMEs, custom-domain validation | REST APIs, JSON | 2 h | Intermediate |
| Teaching moment: a form that says "thanks" and delivers nothing | Tracing a message end to end; "a fallback nobody hears is a new silent failure" | None | 1 h | Beginner |
| Teaching moment: patch, move or leave? | Reading a CVE advisory, weighing mitigation vs migration under time pressure | Some ops experience | 1-2 h | Advanced |
QWF App Registry ⭐ NEW
Added: March 18, 2026
Centralized registry of all QWF apps with hosting, database, domain, and development workflow information. This is the single reference for understanding the full app ecosystem.
App Table
| App | Program | Hosting | Framework | GitHub Repo | Domain | Supabase Project | Stage |
|---|---|---|---|---|---|---|---|
| QWR | Quietly Writing | CF Pages | React → SvelteKit (P3) | quietly-writing | quietlywriting.org | <SUPABASE_PROJECT_ID> | Production |
| QQT | Quietly Quoting | CF Pages | SvelteKit ✅ (cutover 2026-06-28) | quietly-quoting | quietlyquoting.org | <SUPABASE_PROJECT_ID> | Beta |
| QNT | Quietly Networking | CF Pages | React | quietly-networking | quietlynetworking.org | <SUPABASE_PROJECT_ID> | Alpha |
| QKN | Quietly Knocking | CF Pages (off Lovable 2026-09-21, CI wired same day) | React → SvelteKit (P5) | quietly-knocking-bb01e834 (the unsuffixed quietly-knocking repo is archived) | quietlyknocking.org | <SUPABASE_PROJECT_ID> | Alpha |
| QSP | Quietly Spotting | CF Pages | React → SvelteKit (P2) | quietly-spotting | quietlyspotting.org | <SUPABASE_PROJECT_ID> | Production |
| QTR | Quietly Tracking | CF Pages (off Lovable 2026-09-21, CI wired same day) | React → SvelteKit (P6) | quietly-tracking | quietlytracking.org | <SUPABASE_PROJECT_ID> | Foundation |
| L4G | Locals 4 Good | CF Pages | SvelteKit ✅ | locals4good | locals4good.org | <SUPABASE_PROJECT_ID_L4G> | Beta |
| HQ | Command Center | CF Pages (off Lovable 2026-07-05) | React → SvelteKit (later) | hq-command-center | hq.quietlyworking.org | <SUPABASE_PROJECT_ID> (shared with QWR) | Production |
| WHL | WHELHO | CF Pages | React | whelho | whelho.org | nvimpjmhiondaxtrwlny | Alpha |
| PEZ | Pocket Ez | Lovable | ... | ... | ... | <SUPABASE_PROJECT_POCKET> | Planned |
| QMP | Missing Pixel | Lovable | ... | ... | ... | <SUPABASE_PROJECT_ID> | Planned |
| AH | [Networking Chapter] | CF Pages | React | aim-high-bni | <chapter-domain> | <SUPABASE_PROJECT_ID> (shared with QNT) | Production |
| QWB | Quietly Webbing | CF Pages | SvelteKit (P1) | ... (planned) | quietlywebbing.org | ... | Planned |
| QCP | Quietly Capsuling | CF Pages | SvelteKit | ... (planned) | ... | ... | Concept |
| QPN | Quietly Planning | CF Pages | SvelteKit | quietly-planning | quietlyplanning.org | qadtibkqekzryahqrbll | Phase 0 (landing + auth live 2026-09-02; never the retired code QPL) |
Frontend Development Workflow
| Hosting | Framework | How to Make Changes | Deploy Process |
|---|---|---|---|
| CF Pages (every QWF app: QWR, QSP, L4G, QNT, WHL, AH, QQT, HQ, QKN, QTR, QOS, QST, QCM, Portal, QPN) | React (current) / SvelteKit (target) | Direct code commits to GitHub repo | Push to main → GitHub Actions → wrangler pages deploy (auto). QKN + QTR were the last two wired (2026-09-21). |
| Lovable | none | Retired 2026-09-21. No QWF app is on Lovable; subscription cancelled (Free from 2026-09-28). Never write a Lovable prompt. | none |
SvelteKit migration (decided 2026-04-11): All supporter-facing apps migrate to SvelteKit before onboarding active supporters. Zero current supporters = zero risk window. QWB is the greenfield template app (P1). QSP migrates during Content tab build (P2). QWR migrates frontend (P3). Lovable apps (QQT/QKN/QTR) skip React entirely → go directly to SvelteKit/CF Pages (P4-6). L4G SvelteKit migration complete (Session 216, Apr 12, 2026) ... sveltekit-migration branch merged to main, all 20 routes verified on production. SSR live (area demographics in HTML source). deploy.yml updated (output dir .svelte-kit/cloudflare, Supabase env vars as GH secrets). Host-based redirects via hooks.server.ts. HQ stays on Lovable. See 005 Operations/Directives/qwf_content_intelligence_platform.md.
Content Intelligence Platform (Decided 2026-04-11)
The chaplaintig.com content creation pipeline is being productized into a unified cross-app system: QSP is the cockpit, QWR is the engine, QWB is the runway. Every supporter gets an AI content strategist that monitors their industry experts (Wisdom Library), identifies content opportunities (QSP SEO gaps), writes articles in their brand voice with heritage references (QWR), generates social content, and (Agency tier) auto-publishes via Vista Social.
Key features: Content tab in QSP (Wisdom Feed, Draft Queue, Calendar, Performance Feedback), "Why This Article" transparency cards, auto-generate threshold, federated settings mirror (QWR/QQT/QNT settings visible in QSP). Full directive: 005 Operations/Directives/qwf_content_intelligence_platform.md.
Supabase Service Role Key Env Vars
| App | Env Var |
|---|---|
| QWR | SUPABASE_SERVICE_ROLE_KEY |
| QSP | QSP_SUPABASE_SERVICE_ROLE_KEY |
| QNT | QNT_SUPABASE_SERVICE_ROLE_KEY |
| QKN | QKN_SUPABASE_SERVICE_ROLE_KEY |
| QQT | QQT_SUPABASE_SERVICE_ROLE_KEY |
| QMP | QRP_SUPABASE_SERVICE_ROLE_KEY (naming inconsistency ... QRP, not QMP) |
| PEZ | POCKET_EZ_SUPABASE_SERVICE_ROLE_KEY |
| L4G | L4G_SUPABASE_SERVICE_ROLE_KEY |
| WHL | WHL_SUPABASE_SERVICE_ROLE_KEY |
| QCM | QCM_SUPABASE_SERVICE_ROLE_KEY (since 2026-10-06 the named secret key backoffice, not the legacy JWT) |
Retiring a Leaked Supabase Signing Secret (QNT B7 2026-09-26, QCM B4 2026-10-06)
Added: October 6, 2026 | Tool: 005 Operations/Execution/rotate_qcm_legacy_jwt.py (QCM) / rotate_qnt_legacy_jwt.py (QNT) | Pattern: supabase_tool_wisdom.md §PATTERN entries
When a project's legacy JWT signing secret leaks, anyone holding it can sign a service_role token that bypasses every row rule. The fix that works with zero downtime:
- Census every holder by DIGEST, not by name. Compare every project's function-secret digests, the vault, Cloudflare vars, GitHub secret names, n8n, databases and running processes with the project's keys, in memory. On QCM this found two holders no document listed.
- Read the request logs for the HS256 fields (
request.sb.jwt.{apikey,authorization}.payload.algorithm).supabase_mgmt.py key-usagecannot see legacy keys at all. - Move one holder at a time to its own NAMED new-style key (service-role consumers each get a secret key; public consumers get the publishable key), each gated on a count-only probe and proven by its real consumer in the logs.
- Disable the legacy API keys, watch 15 minutes, then revoke the HS256 key. Disabling alone still lets a forged token in as a bearer; only the revoke closes it.
- Prove it: the platform's own legacy
service_roletoken refused 10 of 10, as key and as bearer.
Every step has a go word from TIG at the moment it runs. QCM's run: 7 holder slots in 5 places, zero downtime, nobody signed out. Record: register B4; channel qcm-b4-signing-key-retirement-20261006-1417-d83008.
MP training opportunity (Advanced, ~6 hours supervised): "find every holder of a credential without ever seeing it" ... digest comparison, log forensics, staged migration with rollback at every step. Prerequisites: REST APIs, Python, SQL basics. Teaches the habit that a 200 from the wrong key is not proof.
Shared Infrastructure
All QWF apps share:
- Supabase Management API:
QWU_BACKOFFICE_SUPABASE_TOKEN(note theQWU_BACKOFFICE_prefix ...grep "^SUPABASE"misses it) - Contact Form Pipeline:
submit-contact-formedge function →contact_submissionstable → n8n webhook → Ezer auto-reply → Discord alert → HQ Contact Center - Contact spam hold (2026-10-24,
process_contact_submission.pyv2.4.0): a message shaped like link spam (forum or web-page link code, a link in the name, 6+ links, or mostly another alphabet plus a link) from ANY form is held: no auto-reply, no TIG notice, no Discord; row marked held/declined; one held list to TIG at 07:05 Pacific (--held-digest) only on a day something was held. The WP Legacy sites' 7 forms also carry Cloudflare Turnstile (deploy_frozen_site.pyv1.2.0,provision_qwf_site_turnstile.py). Record:002 Projects/_QWU WordPress Multisite/QWU-WPMU-Freeze-Move-Plan.md"Spam". - QWF Passport (SSO):
generate-crossover-token+verify-crossover-tokenedge functions for one-click cross-app auth - Theme System: CSS custom properties on
:root(dark) and[data-theme="light"]... dark mode default across all apps - Pacific Timezone:
src/utils/timezone.tsin every Lovable app ... never use rawnew Date()for Supabase date queries
Animation Architecture (4-Tier System)
Added: March 20, 2026 | Reference: 005 Operations/Directives/qwf_animation_standard.md
All QWF apps follow a 4-tier animation architecture that prevents over-engineering (loading 584KB of WebGL for a text fade) and under-delivering (faking 3D with parallax when Spline exists).
| Tier | Tool | Bundle Impact | Domain | Status |
|---|---|---|---|---|
| 1 | CSS Transitions + Intersection Observer | 0 KB | In-app micro-interactions (hovers, toasts, modals) | Active everywhere |
| 2 | GSAP + ScrollTrigger | ~40 KB | Scroll-driven landing page animation (text reveals, parallax, pins) | Implement when needed |
| 3 | Spline 3D | ~584 KB + scene | Interactive 3D experiences | WHELHO coded, scene pending |
| 4 | Hana (by Spline) | Unknown (CDN) | Interactive 2D widgets | Monitor (v1.0.x, too immature) |
Key rules:
- Tiers 2-4 are lazy-loaded on landing page routes only ... zero impact on authenticated app routes
- GSAP is free for 501(c)(3) nonprofits under standard license
- Slider Revolution stays on WordPress as a WYSIWYG design tool ... design params extracted and rebuilt as GSAP code in React
- All Tier 2 animations must respect
prefers-reduced-motion: reduce - Tier 3 scenes must provide static image fallback when WebGL is unavailable
- Hana (Tier 4) has no npm package, no React component, no scroll animation, no self-hosting ... evaluate when it matures
Design tools:
- Slider Revolution:
003 Entities/Tools/Slider Revolution.md... GSAP extraction workflow documented - Spline 3D:
003 Entities/Tools/Spline 3D.md... React integration, Hana evaluation, pricing - App Family Standard:
005 Operations/Directives/qwf_app_family_standard.mdSection 10
Consent Words on Forms: Section 17 (Session 895, 2026-11-04)
Any sentence that turns a press into agreement goes ABOVE the button it governs: the last field, then the words, then the button. Full-size, full text color, links underlined and opening in a new tab, the button pointing at the words with aria-describedby. Where texts are involved, the words name who will text, say message and data rates may apply, and say how to stop. The rule is App Family Standard Section 17. The research and a ready answer for anyone who asks are in qwf_consent_and_preferences_standard.md, "Where the consent words go on a form".
No required checkbox (TIG, 2026-11-04). A required tick does not go on a form to prove the words were seen. The words above the button, plus the saved record of exactly what each person saw, are the proof. The upgrade for a supporter who sends sales texts in bulk is an optional, unticked box, and their team texts only the people who tick it. A required box fits only where agreeing is the deal itself (Section 3, a price that follows a count).
Checking a page. node "005 Operations/Execution/measure_consent_placement.js" --url <page> --out <folder> loads the page at phone and desktop width and blocks every write at the network layer, so nothing can be saved or sent. For each button it records the words near it: where they sit, size, color, contrast, link style and aria-describedby, with screenshots. --click opens a step, --fill types fake values, and --button and --words measure exact elements.
The first audit (read-only, 2026-11-03) covered every family app, the sites QWF hosts and the frozen legacy sites. Results are in a private report, each app's System Status carries its own result, and the fix items are in HQ (search "Section 17 audit"). Each fix ships on its own app's word.
MP training opportunities (Session 895): (1) Read a form the way a person on a phone does (Beginner, 2 hours; prerequisite: basic HTML): run the tool on one public form, find the button that makes an agreement, and say whether the words are seen before the press. (2) Turn a rule into a check (Intermediate, 4 hours; prerequisites: JavaScript basics, Playwright): add one new measure to the tool and prove it on a page that passes and a page that fails. (3) Have someone try to break your finding (Intermediate, 1 hour; prerequisite: reading code): a second person checks a reported gap at the source and says whether it is real. In this audit that step downgraded some findings and made one worse.
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| App Registry Maintenance | Documentation, cross-system awareness, infrastructure mapping | ⭐ |
| CF Pages Migration | Vite, GitHub Actions, Cloudflare, DNS, build pipelines | ⭐⭐⭐ |
| Multi-App SSO (QWF Passport) | Edge functions, JWT tokens, cross-origin auth, Supabase Auth | ⭐⭐⭐ |
| Contact Form Pipeline | Edge functions, webhooks, n8n workflows, anti-spam (honeypot, Cloudflare Turnstile, content-based spam hold) | ⭐⭐ |
| Theme System Implementation | CSS custom properties, accessibility, dark mode patterns | ⭐⭐ |
| GSAP Scroll Animation (Tier 2) | GSAP timeline API, ScrollTrigger, React hooks, accessibility (reduced motion) | ⭐⭐⭐ |
| Spline 3D Integration (Tier 3) | WebGL, 3D design, React-Spline events API, lazy loading, performance optimization | ⭐⭐⭐ |
| Standalone Tenant Websites | Vite+React+TS+Tailwind on CF Pages, live Supabase data via anon key, DNS cutover | ⭐⭐ |
| Pillow Business Card Generation | Python image generation, batch processing, dynamic text/photo compositing | ⭐⭐ |
| OG Meta Tag Social Previews | CF Pages Functions, Open Graph protocol, link sharing with click tracking | ⭐⭐ |
| Headshot Scraping & Storage | Web scraping member photos, Supabase Storage upload, image pipeline | ⭐⭐ |
| RLS Policy Design for Public Reads | Supabase Row-Level Security for anon public access, multi-table policy strategy | ⭐⭐ |
Testimonial Intelligence Pipeline ⭐ NEW
Added: March 30, 2026 | Directive: 005 Operations/Directives/testimonial_intelligence.md
Automatically discovers, curates, and publishes testimonials about Chaplain TIG from multiple sources with appropriate approval workflows.
Architecture
Sources (meetings, LinkedIn, manual) → Extract → Score (Claude FLAGSHIP) → Pipeline
│
├── Public source → TIG approval in HQ → Publish to WP
└── Private source → Giver approval (Ezer email) → TIG approval → Publish to WP
Scripts
| Script | Purpose |
|---|---|
extract_testimonials.py | Main orchestrator ... extract, score, advance pipeline |
extract_testimonials_meetings.py | Second-pass LLM on meeting transcripts for praise of TIG |
extract_testimonials_linkedin.py | Apify actor scrapes LinkedIn recommendations |
score_testimonial.py | Quality score (0-1) + WHELHO realm mapping |
testimonial_giver_approval.py | Ezer sends warm approval email via Graph API |
sync_testimonials_to_hq.py | Push to HQ action queue for TIG review |
publish_testimonials_wp.py | Generate Divi shortcode, push to WordPress |
sync_testimonials_to_wp.py | Sync JSON cache to WP for shortcode widget |
WordPress Widget
Custom mu-plugin tig-testimonials-widget.php deployed to wp-content/mu-plugins/. Shortcode: [tig_testimonials]
| Style | Usage | Description |
|---|---|---|
slider | [tig_testimonials style="slider"] | Hero slider with progress bar, pause on hover, realm-colored dots |
wall | [tig_testimonials style="wall"] | Full-width glassmorphism masonry wall, scroll-reveal animation |
grid | [tig_testimonials style="grid"] | Static grid layout |
featured | [tig_testimonials style="featured"] | Single large centered testimonial |
ticker | [tig_testimonials style="ticker"] | Auto-scrolling horizontal ticker |
Options: count, realm, speed, theme (dark/light), featured (true/false), order (quality/recent/random).
HQ Command Center
Voices page (Lovable prompts 109-111):
- Filter tabs: All, Needs Review, Giver Approval, Approved, Published, Archived
- Per-card actions: Ask Giver, Approve Direct, Archive, Publish, Feature, Edit inline
- Two-state giver badge: "Not Yet Asked" (gray) vs "Awaiting Reply" (amber)
- Manual Add form with auto-set giver approval toggle by source type
Database
Table: hq_testimonials (Supabase, HQ project)
Pipeline states: extracted → giver_pending → giver_approved → tig_pending → approved → published
Automation
| Cron | Frequency | Purpose |
|---|---|---|
sync_testimonials_to_wp.py | Every 5 min | Sync published testimonials JSON to WordPress |
testimonial_giver_approval.py | Every 5 min | Send emails when TIG clicks "Send Request" in HQ |
ezer_respond.py (n8n) | Every 15 min | Detect giver approval/decline replies, update Supabase |
Critical rule: Pipeline does NOT auto-advance to giver_pending. Testimonials from private sources stay at extracted until TIG explicitly clicks "Send Request" in HQ. Prevents accidental emails.
Key URLs
- chaplaintig.com/voices/ (WordPress page, post ID 29667)
- hq.quietlyworking.org → Voices tab
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| WordPress mu-plugin development | PHP, shortcodes, CSS animation, vanilla JS | ⭐⭐ |
| Supabase pipeline state machine | Database design, RLS policies, PostgREST API | ⭐⭐ |
| Glassmorphism CSS | CSS backdrop-filter, color-mix, masonry layout | ⭐⭐ |
| Intersection Observer animations | JavaScript, scroll-driven UX, progressive enhancement | ⭐⭐ |
| Apify web scraping | API integration, actor execution, data parsing | ⭐⭐ |
| Email template design | HTML email, Microsoft Graph API, conversational UX | ⭐⭐ |
Puzzle Operations Blueprint ⭐ NEW
Added: April 7, 2026 | TWL: 005 Operations/Directives/puzzle_tool_wisdom.md | Entity: 003 Entities/Tools/Puzzle.md
Puzzle is the visual operations layer for QWF ... mapping teams, roles, processes, and tools into an interactive blueprint. Future foundation for QOP (Quietly Operating). Full operational knowledge (capabilities, gotchas, patterns, vendor relationship) lives in the Tool Wisdom Library directive.
Setup
- Plan: AppSumo Tier 3 Optimizer Lifetime (6 seats, unlimited workspaces)
- Workspaces: "Quietly Working" (6096), [Supporter Organization] (9437), [Networking Chapter] (11667) ... one MCP connector per workspace,
?workspace=<id>on the URL - MCP Integration: HTTP MCP via
https://app.puzzleapp.io/mcp - VSCode Config:
claude-code.mcpServers.puzzlein usersettings.json - Auth: OAuth once per connector, and tokens PERSIST across sessions (corrected 2026-08-06; stored in
~/.claude/.credentials.jsonunder.mcpOAuthwith a refresh token and multi-day expiry). A connector that suddenly stops working is far more likely an approval problem than an expiry ... diagnose with.venv/bin/python "005 Operations/Execution/check_mcp_connector.py" <name>.
VSCode Remote SSH ... SUPERSEDED 2026-08-06
Forward the OAuth callback port in VSCode's Ports panel before opening the auth URL. Use claude mcp login <name> --no-browser instead, which prints the URL and accepts the redirect pasted back, eliminating the port-forwarding dance entirely. Note that VSCode often auto-forwards the port anyway, so an OAuth can quietly succeed while appearing to fail ... check .mcpOAuth before re-running it.
Before touching any Puzzle canvas ... read the TWL
The TWL is the source of truth and it now carries several rules that will cost you a session if skipped. Read 005 Operations/Directives/puzzle_tool_wisdom.md first. The expensive ones, by name only:
- Confirm the connector's workspace binding before the first write ... a cross-workspace
tab_idis silently ignored and the content lands in the connector's own workspace, returningok: true. - A new
.mcp.jsonconnector is QUARANTINED until approved, and it reports itself as an auth failure. - Adding or removing a step forces a full delete + fresh
create_process(passingsection_idskips auto-layout), and that create can fail after the delete has run, leaving the canvas empty ... never calldeletewithout the re-create payload in hand. - No horizontal-bar glyph in any step name, and names truncate silently at ~40 chars (~30 on a diamond).
- Puzzle rewrites your text on write (smart typography) ... read stored values back rather than trusting the payload.
Sharing a canvas outside Puzzle, and what QWF does with it (added Aug 14, 2026)
A Shared View is the only embeddable Puzzle surface: app.puzzleapp.io/share/<id>/<slug> answers
frame-ancestors *, while the /workspaces/ editor URL answers frame-ancestors 'self' and renders
an empty box. Creation is UI-only ... there is no share tool in the MCP surface, so a human clicks
Share. Three costs, none obvious: the vendor's branding cannot be removed, embedding forecloses the
password option, and a Shared View is a publicly linked crawlable document from the moment it
exists, not an unlisted one.
A Shared View is NOT one tab. The vendor: "a single shareable view that includes content across multiple canvases, including Workflow sections, Teams, People, and Tool Groups." Nothing can enumerate what is in one ... the share document is an ~8.7 KB JavaScript shell with no canvas prose.
How the Living Roadmap uses this (the "How this works" band):
| Plane | Table | Written by |
|---|---|---|
| Identity ... "there is a map, and here it is" | hq_projects.puzzle_canvas_source | TIG, in the HQ Projects UI. Recording the link IS the approval. |
| Presentation ... what the page renders | hq_roadmaps.puzzle_canvas | check_puzzle_canvas_drift.py only |
Typing a URL in HQ can never publish anything ... the band renders only from the second table. What appears on a page is decided by TIG's act plus an automatic, fail-closed content gate that scans the canvas for credentials, other supporters' names, youth PII, banned glyphs and vault paths. If the scan finds something, the map stays hidden and says why. There is no approval ritual: fetch, scan, fingerprint and outline are machine work (TIG, 2026-08-14: "I added it to the roadmap ... that should be my BLESSSING right there!").
Two traps worth naming, both paid for in production:
- An empty tab, a deleted tab and a foreign tab are one API response. A content gate that scans strings passes trivially on zero strings, so "I looked and found nothing" read as "there is nothing to find." Any tool gating on Puzzle content must refuse an empty canvas outright.
- A prefix regex on a share URL is a typo filter, not an invariant. Browsers normalize:
.../share/..passed two database CHECKs, a client guard AND an unauthenticated reachability probe (the vendor's login page answers 200). Validate what a browser will FETCH.
Current Workspace State (April 9, 2026)
| Component | Count | Status |
|---|---|---|
| Teams | 5 | Created (QWF Leadership, QWC Creative Dept, Missing Pixel, Backoffice Operations, Product Development) |
| Roles | 5 | Created with reporting connections |
| Tool Groups | 5 | Created with 32 tools mapped |
| Workflows | 7 | Created ... 20 sections, 77 steps, 66 connections |
Operational Workflows (Built April 9, 2026)
All 7 workflows include role assignments, tool references, step connections, and rich markdown notes.
| Workflow | Sections | Steps | Owner |
|---|---|---|---|
| Content Pipeline | Capture, Analysis, Approval & Distribution | 9 | Claude Agent |
| Lead Gen | Discovery, Enrichment, Delivery | 9 | Claude Agent |
| Relationships | Data Ingestion, Intelligence, Action | 8 | Claude Agent + Ezer |
| Email Comms | Classification, Drafting, Review & Send | 7 | Ezer + TIG |
| Meeting Intel | Pre-Meeting, Post-Meeting, Follow-Up | 8 | Claude Agent + Ezer |
| Infrastructure | Health Checks, Monitoring & Alerting | 8 | Claude Agent |
| VOSPA | Discovery, Scoring & Prioritization, Delivery | 10 | QWC Consultant + TIG |
Section names prefixed with workflow context (e.g., "Content Pipeline: Capture") for canvas readability. Each section has markdown notes explaining purpose, business rationale, flow position, and ownership.
Tab Organization (Pending)
All workflows currently share one canvas tab. Feature request submitted to Puzzle team (April 9, 2026) for MCP tab creation/management. Feedback board: feedback.puzzleapp.io. Until available, manual tab creation in UI or wait for MCP endpoint.
MCP Usage Patterns ... Duplicate Prevention (Verified April 21, 2026)
Auto-layout works correctly for fresh creates ... verified experimentally:
create_processwith NO connections → steps lay out in a 2x2 grid (Graphviz fallback)create_processWITH connections → steps spread left-to-right with arrows- Multiple sections on same tab → spaced horizontally, not stacked
The "stacked vertically" complaint is NOT a layout bug. Root cause: iterating with Claude on an existing workflow using repeated create_process calls. Each call spawns NEW step entities at the same (x,y) Graphviz computes for the same logical structure. Old steps never delete. Result: a "deck of cards" shadow under each node where 3-4 step records pile at identical coordinates.
Correct pattern for modifications:
list_sections+list_stepsto find existing IDs- Propose precise diff (mirrors [Vendor Founder]'s "Do not make any changes yet" video pattern)
update_workflowfor in-place edits (takes IDs, edits fields)deletefor removalscreate_processONLY for brand-new sections/steps
Prompt prefix that prevents duplicates:
"Before modifying any existing workflow, first call
list_sectionsandlist_stepsto find current IDs. Then propose what you'll change as a diff. Useupdate_workflowfor in-place edits anddeletefor removals. Only callcreate_processfor brand-new sections or steps that don't exist yet."
This issue was diagnosed while helping Josefa Savu in the Puzzle Circle community forum (Session 261).
Known Issues
create_processbug RESOLVED (April 8-9, 2026) ... was caused by Anthropic timeouts on Puzzle's backend. Brian confirmed fix, we verified.- Static API token not yet available ... OAuth per-session only. Feature request logged on Puzzle feedback board.
- Also has "Easy Mode IT" workspace (separate from QWF operations)
Key Contacts
- [Vendor Founder] ... Founder/CEO (
), warm relationship, attended BNI - [Vendor Contact] ... Senior Product Operations ([email protected]), primary support, escalates to engineering
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| MCP Server Configuration | OAuth flows, VSCode settings, API integration | ⭐⭐ |
| Operations Blueprint Design | Organizational modeling, process mapping, VOSPA framework | ⭐⭐ |
| Workflow Modeling via API | Structured process creation, step typing, role assignment, connection mapping | ⭐⭐⭐ |
| Vendor Relationship Management | Bug reporting, feature requests, professional email communication | ⭐ |
Trails AI SOP + Video Pair ⭐ NEW
Added: May 9, 2026 | TWL: 005 Operations/Directives/trails_tool_wisdom.md | Entity: 003 Entities/Tools/Trails.md
Trails (trails.so) is the click-by-click documentation layer that pairs with Puzzle. Where Puzzle answers WHO/WHAT/WHY (process map, roles, RACI), Trails answers HOW (the exact keystrokes, narrated). Every recording produces a polished step-by-step written guide AND an AI-narrated branded video in one pass... edit the guide text, the video auto-re-renders. Full operational knowledge (capabilities, gotchas, patterns, plan comparison) lives in the Tool Wisdom Library directive.
Setup
- Plan: Business plan, $40/month (negotiated by Mike Cooch / SuiteDash partnership; retail is $299/mo or $240/mo annual)
- Seats: Unlimited users
- Translations: Spanish, German, French, Italian (Business-tier only)
- Capture surfaces: Chrome Extension, Desktop App, or upload existing video
- Output pair: branded video (AI voiceover) + written step-by-step guide, both at sharable URLs
- Sharing: public link, private, team-shared, iframe embed for knowledge bases / /docs centers / SuiteDash
The Strategic Pair: Puzzle + Trails
The single most valuable pattern. Each tool covers one axis of an SOP that the other can't:
| Axis | Tool | What it answers |
|---|---|---|
| Architectural | Puzzle | Who does this? What's the sequence? Which tools? Where does data flow? What's the org context? |
| Operational | Trails | When you sit down to do step 4, exactly what do you click? What does the screen look like? |
A Puzzle workflow step description links out to the corresponding Trails guide URL. Supporter sees the diagram (Puzzle), clicks into a step, watches the click-by-click execution (Trails). This is the difference between "here's the map" and "here's the GPS-narrated drive." Pattern documented in trails_tool_wisdom.md §Patterns.
Acknowledged Gaps (per Trails' own competitive analysis)
- No SSO/SAML (enterprise identity not supported)
- No public API/webhook/MCP (no programmatic guide creation, no n8n integration)
- No view analytics (can't tell what supporters actually watched)
- No LMS features (quizzes, completion tracking, certificates absent)
- Translations limited to 4 languages (ES/DE/FR/IT)
If we ever need any of these, wrap Trails embeds in our own analytics layer / pair with a separate LMS / use HeyGen for languages Trails doesn't cover.
Embedding into QWF Surfaces
Embed support is confirmed across all paid tiers, but the exact mechanism (iframe code format, private-guide auth behavior, CSP compatibility with SuiteDash) needs hands-on verification at first login. Pending checklist documented in TWL Implementation Status. Expected-to-work targets: QWF /docs centers (Astro), SvelteKit apps, SuiteDash supporter portals (verify CSP), Notion embed blocks, transparency site.
Workflow Discipline
- Record-Edit-Don't-Re-record: AI auto-cleans fillers/restarts. Mistakes during recording are fine; fix wording in text afterward and the video re-renders.
- Narrate live: voice transcription becomes the guide text directly. Faster than recording silently and writing descriptions afterward.
- Blur after, not during: mask sensitive regions in the editor; recording naturally is faster.
- Per-workspace branding: set QWF logo + colors once; every recording inherits.
Key Contacts
- Mike Cooch ... SuiteDash founder, partnership broker for the $40/mo Business rate. Treat the deal as a SuiteDash-originated benefit; verify terms before promising it to supporters.
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Recording an SOP | Process decomposition, on-camera narration, screen capture hygiene | ⭐ |
| AI-cleanup mindset | Understanding when to trust automation vs re-record; text-first editing discipline | ⭐ |
| Embed integration | iframe deployment, CSP debugging, /docs center authoring | ⭐⭐ |
| Puzzle+Trails pairing | Linking architectural diagrams to operational walkthroughs; layered documentation design | ⭐⭐ |
| Supporter onboarding pack creation | Translation strategy, blur/privacy review, branded asset preparation | ⭐⭐ |
TWL Preload Hook ⭐ NEW
Added: April 7, 2026 | Reference: 005 Operations/Directives/context_management.md
A UserPromptSubmit hook that automatically detects domain keywords in user messages and injects system reminders to read relevant Tool Wisdom Libraries before beginning work. Part of the self-annealing system ... ensures agents consult domain-specific gotchas and patterns without relying on human reminders.
How It Works
- User submits a message mentioning a domain keyword (e.g., "BrightLocal", "n8n", "Supabase")
.claude/hooks/twl_preload.pyscans the message against a keyword-to-TWL mapping- If matched, injects a system reminder: "Read [TWL directive] before proceeding"
- Agent reads the TWL, gaining access to gotchas, working examples, and vendor intelligence
Domain Coverage (32 domains)
All 28 TWLs on disk are mapped (added Descript, FFmpeg in Session 228; DaVinci Resolve, DaVinci Fusion, Blackmagic Cloud, BMPCC 6K Pro in Session 219), plus additional domains for supporter systems, email, QSP, ESP, and WordPress. Keywords include tool names, common abbreviations, and related concepts.
Drift Detection (Session Wrap-Up Step 3B)
The /session-wrap-up skill now includes a drift detection step that compares TWLs on disk (005 Operations/Directives/*_tool_wisdom.md) against the hook's keyword configuration. If a TWL exists on disk but is not mapped in the hook (or vice versa), it flags the mismatch for correction.
File Locations
- Hook script:
.claude/hooks/twl_preload.py - Hook configuration:
.claude/settings.json(registered as UserPromptSubmit hook) - TWL directives:
005 Operations/Directives/*_tool_wisdom.md
Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Hook system design | Event-driven architecture, keyword matching, system prompts | ⭐⭐ |
| Drift detection | Configuration auditing, file system comparison, self-healing systems | ⭐⭐ |
HQ Issues Preload Hook ⭐ NEW
Added: April 18, 2026 | Reference: 005 Operations/Directives/hq_issues_preload_system.md · Built from Broken Vol. 5
A UserPromptSubmit hook that auto-surfaces open HQ Issue Tracker items for any QWF app mentioned in a prompt. Closes the GTD trust loop by ensuring captured ideas resurface at the right context, not just when the user remembers to look.
How It Works
- User submits a message mentioning a QWF app (acronym like "QWR" OR full name like "Quietly Writing")
.claude/hooks/hq_issues_preload.pyscans the message against a 16-app keyword map (strong triggers only ... short acronyms like "HQ" or "MP" require longer phrases to fire)- For matched apps, batched query to
hq_contact_submissionsfor strictly-open items (status IN ('pending','acknowledged','in_progress')) - Per-session 5-min cache + cross-session per-app
last_seentracking - Injects a compact
<hq-open-items>system message with priority-coded items, stalled-item warnings, "🆕 new since last touch" highlights, and cross-app label crosslinks
The Trusted Inbox Triad
The hook is one of three required parts. Skip any → trust breaks:
- Capture (frictionless):
/hq-addslash command inserts items in one line, no UI switch - Surface (automatic): the hook (this section)
- Decay (close-the-loop): session-wrap-up Step 3C lists items per app touched this session and prompts status updates
Operating Modes
| Mode | Purpose |
|---|---|
| (no args, JSON stdin) | Hook mode ... UserPromptSubmit |
--close-the-loop | Wrap-up: list items per app touched in last 24h |
--list-apps | Drift check: keyword map vs. hq_app_registry |
--app <code> | Debug: force-fetch one app |
--test | Smoke-test HQ Supabase reachability |
File Locations
- Hook script:
.claude/hooks/hq_issues_preload.py - Hook configuration:
.claude/settings.json(UserPromptSubmit, 6s timeout) - Capture skill:
.claude/skills/hq-add/SKILL.md - Wrap-up integration:
.claude/skills/session-wrap-up/SKILL.mdStep 3C - Cache:
.tmp/hq_issues_cache.json(5-min TTL per session) - Last-seen:
.tmp/hq_issues_last_seen.json(persists across sessions)
Origin
Session 246 (April 18, 2026). TIG had a Bugs/Ideas/Suggestions panel in HQ for capture, but items piled up unseen. A 4-week-old "decommission supporter-CC" item that had passed its target date by 18 days was discovered only when the new hook surfaced it on its first run. Same architectural family as the TWL Preload Hook (Vol 1) ... same UserPromptSubmit pattern, different data plane (live DB rows vs. static docs). Documented publicly as Built from Broken Vol 5.
Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Hook + DB query layer | UserPromptSubmit pattern, PostgREST queries, env-based auth | ⭐⭐ |
| Cache + last-seen design | Session-scoped TTL, persistent state files, JSON serialization | ⭐⭐ |
| The Trusted Inbox Triad | GTD principles, capture-surface-decay loop design | ⭐⭐⭐ |
Date/Time Output Validation Hook ⭐ NEW
Added: April 12, 2026 | Reference: Built from Broken Vol. 3
A PreToolUse hook that validates day-of-week names against actual calendar dates before any Bash, Write, or Edit tool call can execute. Catches hallucinated day names in emails, files, and any output before they reach the real world.
How It Works
- Agent attempts to run a Bash command, write a file, or edit content
.claude/hooks/date_validation_hook.pyscans the tool input for day-of-week names near recognizable dates- Supports three date formats: text ("April 9th, 2026"), ISO ("2026-04-09"), US ("04/09/2026")
- Uses 200-character proximity threshold to pair day names with dates (avoids false positives across paragraphs)
- Computes actual calendar day via Python
datetime - If mismatch: blocks the tool call with a specific error naming the correct day
- If match or no date/day pairs found: passes through silently
Origin
Session 220 (April 12, 2026). Agent composed an email containing a date with the wrong day-of-week name attached. The date itself was correct but the day name was fabricated. A memory rule was initially created but immediately recognized as the same probabilistic failure pattern solved by the TWL preload hook in Vol. 1. Deterministic hook > memory rule.
Key Design Decisions
- PreToolUse, not PostToolUse: Blocks BEFORE the output exists, not after. No wrong data reaches any destination.
- Exempt paths:
.tmp/,.claude/hooks/,/tmp/are skipped. Test fixtures and hook source code contain intentional bad data. - Fail-open on parse errors: If the hook can't parse content, it allows through. Only blocks on verifiable mismatches.
- Complements qwu_datetime.py: That utility solves INPUT (what day is it now?). This hook validates OUTPUT (is the day name I just wrote correct?).
File Locations
- Hook script:
.claude/hooks/date_validation_hook.py - Hook configuration:
.claude/settings.json(registered as PreToolUse hook on Bash, Write, Edit) - Test suite:
.tmp/test_date_hook.py
Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| PreToolUse hook design | Event-driven validation, JSON stdin/stdout contract, permission decisions | ⭐⭐ |
| Date parsing & validation | Regex, datetime, multi-format parsing, proximity matching | ⭐⭐ |
| Bootstrapping problem | Self-referential test challenges, exempt path design | ⭐⭐⭐ |
CLAUDE.md Values Block Generator ⭐ NEW
Added: April 23, 2026 | Reference: 002 Projects/_QOP Quietly Operating/QOP-System-Status.md · 002 Projects/_QOP Quietly Operating/Values-Editor-Spec-v1.md
generate_claude_md_block.py reads a resolved values.json file and writes a managed marker-block section into CLAUDE.md so every Claude Code session starts with TIG's compass, hard rules, forbidden vocabulary, encouraged vocabulary, decision filters, and ethical boundaries injected structurally. The V layer of VOSPA is now load-bearing infrastructure, not advisory memory.
How It Works
- Loads
005 Operations/Values/QWF.values.resolved.json(falls back toQWF.values.jsonfor root files, wrapping with a trivial_resolutionblock at read time). - Renders each startup-priority section per the
formatTemplateenum ... compass-block, hard-rule-block, forbidden-table, encouraged-table, filter-questions, boundaries-list. - Inserts a managed block before
## Directive StructureinCLAUDE.md. The## QWF Values & Decision Filtersheading is owned by the block, so rename protection works. - Wraps content in BEGIN/END marker comments with sha256 of the source payload (minus
_resolution) for drift detection. - Atomically writes the new
CLAUDE.mdafter backing up the prior state to.tmp/claude_md_backups/.
CLI Flags
| Flag | Purpose |
|---|---|
--org <shortName> | Which values file to read. Default: QWF. |
--target <path> | Target CLAUDE.md path. Default: project CLAUDE.md. |
--dry-run | Print the managed block to stdout, do not modify target. |
--force | Regenerate even when source hash matches the existing marker (for testing or stale companion files). |
--rollback | Restore the most recent backup from .tmp/claude_md_backups/; a safety backup of the pre-rollback state is written first. |
Exit codes: 0 success / in-sync, 1 user error, 2 file not found, 3 validation failure, 4 rollback used.
Dog-Food Compliance
Every prose string the script writes into CLAUDE.md or stdout uses ellipsis, never em dashes. When a source field contains an em dash, the generator:
- Substitutes to ellipsis at render time
- Emits a stderr warning with the exact field path
- Increments a substitution counter
- Prints an end-of-run summary: "N em-dash substitutions performed. Source file has N drift instances that should be fixed at source in a follow-up session."
The only em dash permitted in the managed block is the Pattern cell for punctuation-em-dash in the Forbidden Vocabulary table, where the character is the labeled specimen being forbidden (analogous to a test-fixture).
When To Run
- Any time
QWF.values.json(or a child program's values file) changes. - After running
resolve_values.pyon a child. - To verify idempotency: re-running on an unchanged source prints
CLAUDE.md in syncand exits without touching the file. - To restore a prior CLAUDE.md state:
--rollbackuses the most recent.tmp/claude_md_backups/entry.
Known Limitations (Session Log: Session 269)
resolve_values.pycannot self-resolve a root file (no parent to cascade from). The generator side-steps this by wrapping unresolved roots with a trivial_resolutionblock at read time. Fix needed:--rootflag or auto-detect inresolve_values.py. Tracked inQOP-System-Status.mdKnown Issues.- Drift-detection cron (auto-regenerate when source hash drifts from marker hash) not yet scheduled. Planned for a future session.
Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Marker-block pattern | Auto-generated content blocks with sha256 drift detection ... same pattern linters and code generators use | ⭐⭐ |
| Atomic file writes | Write to temp, rename; backup-before-write; rollback from backups | ⭐⭐ |
| Specimen-vs-prose distinction | Self-validating schemas where the rule-subject legitimately appears in the rule declaration | ⭐⭐ |
| CLI + importable module pattern | Single Python file that works standalone AND as a library for future in-app use | ⭐⭐ |
QWB Quietly Webbing ⭐ NEW
Added: April 8, 2026
A new QWF fundraising program for AI-managed digital presence via natural language chat interface.
Tagline: "Your digital presence, managed through conversation."
Domains: quietlywebbing.org (primary), quietlywebbing.com (redirect)
Concept
QWB gives supporters a complete digital presence ... website, content, SEO, analytics ... managed through a chat interface that learns their brand. Uses a three-layer architecture (directives/orchestration/execution) scoped per supporter.
Architecture (Updated 2026-04-11)
Frontend: SvelteKit on CF Workers with Durable Objects (bidirectional WebSocket for real-time chat). First SvelteKit app in QWF ecosystem.
Agent Backend: Dedicated qwu-agents Azure VM (B2s, 2 vCPU, 4GB, ~$30/mo). Hub-and-spoke model:
- Backoffice (claude-dev) = central brain ... pushes TWLs, directives, wisdom.db as read-only intelligence
- qwu-agents = execution ... Docker containers per supporter, isolated repos/memory/conversations
- One-directional SSH: backoffice -> qwu-agents only (never reverse)
- Learning loop: agents submit gotcha reports -> backoffice reviews -> TWLs updated -> all agents benefit
Per-Supporter CLAUDE.md: Brand voice, visual identity, guardrails, capabilities. Loaded via Agent SDK setting_sources=["project"].
Approval Workflow: Git-native. Draft branch -> sandbox preview -> approve -> merge to production. Deploy options: now / tonight / scheduled.
Self-Improving Feedback: GREEN (auto-learn), YELLOW (propose to TIG), RED (hard stop)
Model: Opus 4.7 (FLAGSHIP) for all supporter conversations. Prompt caching on repeated system prompts + CLAUDE.md reduces cost ~90% per turn.
Key architectural decision: qwu-agents VM chosen over Docker-on-backoffice after security audit found 103 secrets, 12 Supabase god-mode keys, live Stripe keys, and SSH keys for lateral movement on claude-dev. Blast radius of misconfiguration on backoffice = total. Dedicated VM limits blast radius to supporter websites only.
BLOCKING question (as of 2026-04-11): Frontend-to-agent communication pattern not yet decided. Three options: Durable Objects + WebSocket, SSE via HTTPS proxy, or Supabase Realtime as message bus. Must resolve before implementation.
Scaling path: Single VM handles ~20 concurrent sessions. Beyond that: resize VM, multiple VMs, or Anthropic Managed Agents (beta April 2026). Agent tools designed runtime-agnostic so the intelligence layer ports without rewrite.
Proof of Concept: [Supporter Organization]
- Sandbox:
<supporter-sandbox-domain>(live, 29 pages +/about-us301 →/about) - Repo:
<supporter website repo>(public) - Stack: Astro static + Cloudflare Pages Functions for
/api/lead+/api/feedback. Zero JS at the page level (Pills/Form scripts inline-compiled); Lighthouse projected 95+. - Stage:
alpha... PUBLIC_SITE_STAGE env var gates alpha banner + feedback pill visibility. Flip toproductionin.github/workflows/deploy.ymlto hide widgets after sign-off. - Primitives: 14 canonical components in
src/components/primitives/(NavHeader, Footer, CalloutBanner, Hero, Features, Gallery, Testimonials, LogoWall, CtaBand, FaqAccordion, EmbedBlock, TeamGrid, ArticleList, RichTextContent, StatsStrip, TagCloud, LeadForm, FeedbackPill). - Lead pipeline:
/api/leaddual-writes to QSP Supabasewebsite_leads(tenant_id scoped) + MS Graph email to<supporter contact inbox>. - Feedback pipeline (alpha-only):
/api/feedbackdual-writes to HQhq_contact_submissions(source_app=) + private MS Graph email to TIG only. Screenshots supported (file pick / drag / paste / clipboard), client-resized to 1920px, uploaded to HQ Supabase Storage bucket site-feedback-screenshots, rendered inline in notification email and (after HQ prompt 127) in HQ detail view. - Social proof: 60 real 5-star Google reviews in a CSS-columns masonry wall, Google source badges, 8 AI-extracted praise themes (Opus 4.7 analyzed from the full corpus), multi-source trust stats replacing the old "4.5★" single line.
- Status (2026-04-20, Session 249): Alpha review begins. [Supporter Lead] + [Supporter Team Member] + [Supporter Team Member] + rest of [Supporter Organization] team invited via announcement email to walk the site and submit via the private feedback pill. Known gaps: Blog/Gallery/Videos detail pages not yet clickable (data ready in JSON), Get Started form not yet wired, FAQ expansion pending. Six open business decisions awaiting [Supporter Lead].
- Full System Status:
002 Projects/_[Supporter Organization] Projects/[Supporter Organization]-Website/[Supporter Organization]-Website-System-Status.md
Key Documents
- [Supporter Organization] site System Status:
002 Projects/_[Supporter Organization] Projects/[Supporter Organization]-Website/[Supporter Organization]-Website-System-Status.md - Gap audit:
.tmp/[Supporter Organization]-site-audit/30-gap-report.md - Ecosystem vision:
002 Projects/_QWF App Ecosystem/QWF-App-Ecosystem-Vision.md - Architecture decisions:
memory/qwb_architecture_chat.md - Program genesis:
memory/project_qwb_quietly_webbing.md - TWLs: SvelteKit, CF Pages, Claude Agent SDK (all in
005 Operations/Directives/)
L4G Locals 4 Good Platform ⭐ NEW
L4G is a nonprofit postcard fundraiser ... local businesses donate to appear on EDDM postcards mailed to 10,000+ households, funding Missing Pixel student training. NOT an advertising business.
Infrastructure
| Component | Detail |
|---|---|
| Frontend | SvelteKit on CF Pages ... locals4good.org |
| Backend | Supabase <SUPABASE_PROJECT_ID_L4G> (Pro plan, us-west-1) |
| Payments | Stripe via create-checkout-session edge function |
| MS Graph API (TIG-voiced, Ezer Aión personality) | |
| Workflows | 8 n8n workflows (Stripe handler, category notifier, concierge, conversation mirror, reply poller, etc.) |
| Print Vendor | Printing4SuperCheap (P4SC) ... full-service EDDM |
| GitHub | QuietlyWorking/locals4good ... auto-deploy via GitHub Actions |
CX Scripts (10 in 005 Operations/Execution/)
| Script | Purpose |
|---|---|
add_l4g_donor_booking.py | Manually add donor-partner with booking (outside checkout) |
run_l4g_concept_pipeline.py | AI concept generation: enrichment → brand identity → 3 ad briefs |
process_l4g_concept_choice.py | Process donor's concept selection |
upload_l4g_proof.py | Upload Photoshop proof to Supabase Storage |
advance_l4g_print_batch.py | Batch print status advancement (7-stage pipeline) |
check_l4g_milestones.py | Poll for journey state changes, trigger emails + push |
process_l4g_feedback.py | Post-delivery feedback processing |
send_l4g_journey_email.py | 6 milestone email templates |
send_l4g_conversation_email.py | Email mirror for conversation threads |
poll_l4g_email_replies.py | Poll inbox for email replies → thread |
Print Status Pipeline
not_started → layout_ready → sent_to_printer → printing → shipped → delivered_to_po → mailed
Milestone emails trigger at printing (going_to_print) and mailed (postcards_mailed). Postcard tracker in portal shows package-tracking UX.
Two Artwork Paths
- MP Creates: Brand Clarity Wizard → AI concept pipeline → 3 concepts → donor selects → TIG designs in Photoshop → proof review → approval
- Supporter Creates: Upload own artwork → proof review → approval
Phase 11 Dry Run Results (Apr 12, 2026)
All 10 CX scripts validated end-to-end with --dry-run. Both artwork paths verified. One bug fixed (process_l4g_concept_choice.py queried non-existent notes column → fixed to reviewer_notes). Platform verified ... ready for May 2026 mailing.
Open items: Welcome email doesn't differentiate artwork tracks; no May 2026 bookings entered yet; background/spine images not uploaded for RPV West May 2026 postcard config.
Site Hardening Proof (Oct 2026)
005 Operations/Execution/prove_l4g_site_hardening.py (v1.20.0, 2026-11-04; each version's own note is at the top of the file) is the one proof every site-hardening change must pass before TIG's "ship". It builds the site, runs the changed code in node against an in-memory stand-in of the database that refuses what the live database refuses (a visitor reading bookings gets 42501, as live does), and checks the outcome; each check has a control that must fail on its own outcome only, so a check that could never fail is caught. --db-parity adds read-only checks on live data: the donor-partner website check against the database's own constraint, the tables' live columns, the visitor's space count against checkout's count (5 neighborhood-months, 0 disagreements at the v1.6.0 ship), and what the public key can read.
| Version | Date | What it proves |
|---|---|---|
| v1.5.0 | 2026-10-28 | L8: the portal's settings and brand pages show "Saved!" only when the database hands the row back; the browser's website check mirrors the database constraint rule for rule (82 checks) |
| v1.6.0 | 2026-10-28 | Queue item 0: the checkout widget counts sold spaces the way checkout does (through the public card reader, never the bookings table a visitor cannot read), refuses every size on a sold-out month, starts the 15-minute countdown only once payment exists (100 checks, 31 controls) |
| v1.7.0 | 2026-10-29 | Queue item 1: --forward alone (no site build, under a minute) judges the sign-in reply forward: on 1.1.1.1 and 8.8.8.8, accounts.locals4good.org takes mail at Cloudflare only, with one SPF line; the root's Google mail, its one SPF line and the bounce.accounts. record unchanged; Cloudflare's signing key published; one rule, to one verified address, and nothing else forwarding (29/29: 17 checks, 12 controls) |
| v1.20.0 | 2026-11-04 | A failed sign-in, a link or reset email that could not be sent, and a refused password change read in our words, never the sign-in service's, as a word-for-word table and in a real browser, whatever the service answers. A sweep of every non-staff source file refuses any line that could show a service's raw error text; staff pages keep the reason on purpose. The footer links the email promise (269 checks, 66 controls) |
A run takes about 30 minutes; run it in the background and run it again if main moves under it (its first check says so). Records: channel l4g-staff-check-fix-20261026-2254-4d039e.
References
- System Status:
002 Projects/_Locals 4 Good/L4G-System-Status.md - CX Framework:
002 Projects/_Locals 4 Good/L4G Customer Experience Framework.md - Visual Flow Spec:
002 Projects/_Locals 4 Good/L4G-Visual-Flow-Specification-v1.0.md - Memory:
memory/l4g.md - SvelteKit TWL:
005 Operations/Directives/sveltekit_tool_wisdom.md
L4G next-areas planning (private) ⭐ NEW
How L4G works out where to go next, before anyone says a place out loud. It is a plan on paper: nothing here touches L4G's database, site or poll, and every load of a new area is its own word from TIG. The plan of record is 002 Projects/_Locals 4 Good/Design Docs/L4G-Coast-Blocking-Plan-v0.1.md and its JSON. Place names, income and fit stay in that private plan.
The cards are real USPS carrier routes, 10,000 to 11,000 mailboxes each, with routes that hold more businesses than homes left out and named.
The tools, in order:
| Script | What it does |
|---|---|
survey_l4g_route_fit.py | Reads USPS's public routes one ZIP at a time and lays each route's shape over Census block groups, then adds Zillow home values and IRS income by ZIP. Free public data; caches in .tmp/l4g/route-fit/ |
score_l4g_coast_plan.py | Gives every section its facts, a fit out of 100, the why in a sentence, and real demand (counts of leads, donor-partners, category requests and poll flags, no names) |
check_l4g_coast_plan.py | Proves the rules: every card in the band, no route twice, none on an existing card, the game's list clean. --refresh re-reads USPS first |
build_l4g_coast_map.py | Draws the private map page for TIG, with its own map tiles because an Artifact page cannot load images from another site |
Fit counts money, homeowners, home value, houses over apartments, and older homes that need work. Under 30% owners reads Poor whatever the money. It never uses race, ethnicity, religion, national origin, age or family makeup.
The weekly watch: Mondays 07:40 Pacific, score_l4g_coast_plan.py --watch re-ranks in memory and posts one note to #l4g-leads only when real demand moves the top of the list. It is read-only on L4G and never rewrites the plan. If the survey cache was wiped, it says so in the same channel and a session re-runs the survey. Undo: delete its crontab lines; a backup sits in 005 Operations/Data/cron_backups/.
L4G Play (play.locals4good.org) ⭐ NEW
A practice-and-party arcade for Locals 4 Good, built 2026-10-31 to 2026-11-02: Grab Your Category (the real
reservation form as a timed game, with a shared board), a lobby of classic arcade cabinets at /, an idea box whose
ideas reach only TIG, Mail Truck Dash played alone at /truck (LIVE 2026-11-03; 30 seconds on one big pad since 2026-11-04, its own phone and
computer boards, a first place is a throne), Referral Bop played alone at /bop (LIVE 2026-11-03: 60 seconds of signs in nine
spots, one fixed pattern with its trades, whammies and spots shuffled per run, the board checking every bop by its own clock and
doing its own count, its own phone and computer boards, a first place is a throne), and a live three-game show TIG runs from his
phone at talks (a mail truck tapper, a mailbox guess, a referral bop where every trade has its own picture and the Missing Pixel
logo is the bonus). How Many Mailboxes? played alone at /guess (LIVE 2026-11-04) is the last cabinet to open: most neighborhoods found in 60 seconds, a tie to the sooner last find, three digits a guess, its answers only on the board, which says higher or lower by its own clock and shows a count only once found or at the end, its own phone and computer boards, a first place is a throne. One Cloudflare Worker, l4g-play, with two Durable
Objects (Arcade for the boards and ideas, Room for the show), built in SvelteKit and deployed by hand from main
(no CI yet). Every word and picture players see is locked to TIG's bless by a fingerprint: a build whose words
changed cannot deploy. Every wrangler deploy first runs scripts/ship-check.mjs, which refuses anything but a deployable build of newest
main, clean and unchanged since its build: merge main, build, run the suite, push to main, deploy from that same folder with no rebuild.
- Source of truth:
002 Projects/_Locals 4 Good/L4G Play/L4G Play-System-Status.md(deployed versions, lanes, how to deploy, undo) andL4G-Play-Design-v0.1.md(TIG's decisions and the rails). - Three lanes share one repo (
QuietlyWorking/l4g-play): SHOW (the live show), EMAIL (the throne email) and LOBBY (the lobby and each game alone). Onlymaindeploys; whoever ships second mergesorigin/mainfirst; the lanes leave each other notes in the status file's header. - The throne email (RELEASED 2026-11-03): whoever takes first place on Grab Your Category may add an email.
l4g_play_throne_mail.py, every minute from crontab, confirms it (a confirm email, then a page with one button that also says 18 or older), and emails the player when someone takes their throne, up to three a day, in TIG's blessed words, fromtig@. TIG gets a note on every change of first place, with no player's address in it. A changed word turns every player email back into a test copy for his inbox. Its Betterstack heartbeat (create_l4g_play_throne_heartbeat.py) alarms by SMS, email and push if the sender stops; the board's mail key comes fromprovision_l4g_play_mail_key.py. Undo steps are in the status file. - Every game's rules (TIG, 2026-11-03): starts muted, full screen only by the player's own icon (
FullscreenToggle.svelte), NEW BEST only on the board's first place, I'm done beside Play again, the little truck on the start screen (DriveByTruck.svelte), no sideways scroll bar. - A game's top score is proven, not assumed: the page and the board stop at the same number, and for the bop
tests/bop_rules.mjsfinds its maximum by search on every build (720 today; it depends on how many trades L4G has, so a build whose proof fails does not finish). A person looks at every winning run before a prize:/adminshows each entry's details beside the board. - Shipping a game: the rehearsal first (
tests/rehearse_boards.py: the live code, the new code, the live code again on one local board), the full suite on the exact build, TIG's "ship", read the room (a deploy restarts it, so it must read closed), deploy that same build, thentests/live_browsers.pyagainst the live site (read only: it never posts). - Keys: the remove page
/adminand the show's remote/show/hostshare one key held only on TIG's own devices (provision_l4g_play_admin_key.py); the person check is Cloudflare Turnstile (provision_l4g_play_person_check.py status). - Around a talk: TIG's remote checks the live reads from locals4good.org, he scans the board's join code, one real join through the person check; afterwards he closes the room (two taps) so nobody's name stays on a public page.
- The live show, as of 2026-11-04 (each on TIG's word): no named prizes, only a surprise he may type on the day; nothing on the big board is clickable, because people see it as video, so his remote gives him the join line to post in the Zoom chat; a golden truck in every new show for a browser that played any game alone (it follows the browser on that device, not the person, and changes no points); a little saucer beams each player's truck onto the board as they join (no words, no sound, nothing under reduced motion, never over the join code); and Mail Truck Dash on its own 30-second clock while the mailbox guess and the bop keep 60.
- Nobody comes off an all-time list (TIG, 2026-11-04), even when a game's rules change. The two scores retired when the truck went to 30 seconds were put back the same night, and the code that retired them is gone. A monthly board, after the talk, is how a newcomer gets a throne they can win.
- Every deploy checks itself (
scripts/ship-check.mjs, run bywrangler deploy): a deployable build of newest main, a clean tree, nothing changed since the build, every word and name as blessed. So: merge main, build, run the suite, push to main, deploy from that same folder with no rebuild. - The blessed emails quote the show's numbers, so
tests/show.pyfails if a default among them changes; a change goes through the email lane first.
Security Plan and Guards ⭐ NEW
Added: October 31, 2026 (Sessions on Oct 30 and 31). A question about firewalls led TIG to ask for the late-2026 gold standard, a budget-first plan for the home office that becomes a studio, and the back office. The plan lives on a private briefing page and a wall map ("Light the Path", 35 lanterns) that update as steps finish. Decision record: memory project_security_gold_standards_briefing.md; research: 002 Projects/_QWU Backoffice Automation/Security-Gold-Standards-Research-2026-10-30.md.
Road to the Live Game Show Presentation emails (2026-11-05 to 2026-11-19) ⭐ NEW
TIG's own countdown to his talk: seven dated emails, drawn as arcade announcements with the lobby sign's moving lights, to every active [Networking Chapter] member and nobody else. He blessed all seven on 2026-11-04.
The sender: send_road_to_game_show_emails.py (cron every 5 minutes, Betterstack heartbeat). The signs are GIFs from render_road_show_signs.py, carried inside each email. They play once for under 5 seconds and rest with every bulb lit, because an email has no pause button (WCAG 2.2.2). Its ledger is 005 Operations/Data/road_show_mail_ledger.jsonl, by name and a hash of the address, never an address.
What stops an email: a changed byte (each is tied to his bless by a fingerprint of the whole email), a speaker schedule that has moved, a missing guest link (emails 4, 5 and 6), or a hold in the code. TIG hears ahead of time, while he can still act.
What stops a person: a row in ROAD SHOW: STOPPED in the consent register stops these emails only. "News and ideas" off on the preferences page stops these and the throne alerts. "Staying in touch" off also stops the room list and the weekly report. A consent check that cannot answer makes that member wait, and tries again.
No second copy: one SES call per copy (single_attempt=True). Only SES's own named refusals are tried again. Anything else may have followed a send, so it is told to TIG and never resent, and the next send waits 30 minutes.
TIG can say, in any session: "guest link --test nov16 so he can press the button), and "take --preflight KEY, which sends nothing. After the last email, remove the cron line and the heartbeat.
Not in this release: the thank-you and the December reminder stay Outlook drafts for him to send.
Audited before the first send (2026-11-04, Session 900): an independent session checked all 24 items of its kickoff against the real roster, consent records, game and mailbox: no email held, no word changed. Evidence, item by item: 002 Projects/_Locals 4 Good/L4G Play/Road-Show-Ground-Truth-Audit-2026-11-04.md. Nothing automatic adds a stop row: a member's reply reaches TIG's Inbox and, within 15 minutes, a draft and a text, and the row waits for his word. After adding a row, run --preflight on the next email and read the name under stopped, because a natural spelling refuses the whole email and tells him.
What is done
- The newsletter server is off. It was a standby server never put to use; it was archived and powered off (Oct 30).
- Both Amazon owner (root) keys are switched off (Oct 30), confirmed by a fresh credential report. TIG deletes them after a quiet week.
- GitHub secret scanning and push protection are on for the 8 public code projects (Oct 31). Our GitHub key cannot read the alerts; GitHub emails the owner.
- n8n is on a version that fixes every published advisory through the Oct 28 batch.
- Agent guards (Oct 31): the user-level deny rules now cover every key file in the home folder; new pip and npm package versions wait 3 days before they install; an hourly agent settings watch tells TIG once about anything new that can run.
Package waiting periods (how to take a brand-new release on purpose)
- pip:
~/.config/pip/pip.confsetsuploaded-prior-to = P3D. Override:pip install --uploaded-prior-to P0D <package>==<version>. - npm:
~/.npmrcbefore=is rolled to "3 days ago" every hour by the watch.npm ciand installs with a lockfile keep their locked versions. Override:npm install --before=$(date -u +%Y-%m-%dT%H:%M:%SZ) <package>@<version>. - Claude Code itself is the native install, so neither setting delays its updates.
Agent settings watch
005 Operations/Execution/watch_agent_config.py (cron hourly at :17, Betterstack heartbeat). It watches hooks in every settings file and the code those hooks import, MCP servers, plugins, the mod, git hooks and git settings that run programs, deny rules and the permission mode. New, removed or uncommitted = a push to TIG (told only when the push got out); an edit already committed and pushed = a Discord record. Its state lives outside the vault in ~/.local/state/qwu/. Full detail and what it does NOT watch: infrastructure_monitoring.md. Its first version sent one false push (a plugin list's timestamp changed); fixed the same day after an independent review.
DNS signing (DNSSEC), staged
005 Operations/Execution/manage_dnssec.py: status, verify, enable --apply, disable --apply, plan. It refuses any domain not registered with Cloudflare (so supporter domains in the account can never pass) and any non-QWF domain without TIG's word. Cloudflare publishes the signature at the registry in one to two days. Order: a spare test domain (enabled Oct 31), then quietlyworking.org, then the other QWF domains, each verified before the next.
Email-domain policy (DMARC): read, not yet changed
90 days of reports were read and every legitimate sender listed. Tightening goes in stages, after one more signing step on the Microsoft side, so forwarded mail keeps arriving.
TIG's workstation: Windows updates
Updates had been paused through registry settings during a 2025 scare that was later cleared. A read-only PowerShell check (002 Projects/_QWU Backoffice Automation/windows-update-check-2026-10-31.ps1) finds how updates were blocked; the undo (windows-update-undo-2026-10-31.ps1) saves copies, makes a restore point and removes the block, after a full backup with Veeam Agent for Windows Free. The remaining Windows hardening steps are on the plan.
Training Opportunities
| Task | Skills Taught | Prerequisites | Est. Time | Complexity |
|---|---|---|---|---|
| Close password logins on a Linux server with an sshd drop-in, then prove it from outside | SSH, Linux config precedence, outside-in testing | Basic Linux | 2 hours | Intermediate |
| Find and retire cloud owner keys (credential report, deactivate, wait, delete) | AWS IAM, least privilege, reversible change | Cloud account basics | 2 hours | Intermediate |
| Archive and power down a server safely (dump, copy, checksum, verify, then stop) | Backups, integrity checks, change discipline | Basic Linux | 3 hours | Intermediate |
| Read DMARC aggregate reports and list who sends as a domain | Email authentication (SPF, DKIM, DMARC), XML parsing | Python basics | 3 hours | Intermediate |
| Stage DNSSEC on a test domain and verify with validating resolvers | DNS, DNSSEC, dig, staged rollout | DNS basics | 2 hours plus waiting | Advanced |
| Build a "tell me once" config watcher with a dead-man heartbeat | Monitoring design, fingerprints, alert fatigue | Python | 4 hours | Advanced |
| Diagnose a blocked Windows Update with a read-only PowerShell check | Windows registry, policies, services, PowerShell | Windows basics | 2 hours | Beginner to Intermediate |
Session Log
The private version of this manual maintains a detailed session log documenting 68+ operational sessions dating from December 2025 to present. These sessions chronicle the iterative build-out of the entire QWU Backoffice system.
The session log is excluded from the public version because it contains specific operational details (error messages with IPs, credential rotation events, supporter-specific interactions) that would be sensitive to publish.
For students: The session log demonstrates real-world iterative development ... each session builds on the last, errors lead to improvements, and the system self-anneals over time. Ask your mentor about accessing the session log during supervised learning.
Project System Status Files ⭐ NEW
Projects with 2+ deployed external systems maintain a *-System-Status.md file in their 002 Projects/ folder. These files are the anti-compaction memory ... they survive context loss between Claude sessions and are the first file read when resuming work.
Why This Exists
LLM conversations compact over time, losing operational details like URLs, credential access methods, and deployment state. This caused false claims about system status and forgotten access methods. System Status files fix this by storing critical operational state in the filesystem where it persists permanently.
Current Status Files
| Project | File | Key Systems |
|---|---|---|
| QWR Quietly Writing | QWR-System-Status.md | Cloudflare Pages, Supabase, n8n (26 workflows), Stripe, GitHub Actions CI/CD, Python scripts |
| [Networking Chapter] | [Networking Chapter]-System-Status.md | SuiteDash, n8n, Twilio (2 numbers), MS Graph, Apify |
| QWU Backoffice | QWU-Backoffice-System-Status.md | n8n (10+ workflows), Discord, Google Calendar, Betterstack, Zoom |
| Lead Generation | LeadGen-System-Status.md | Apify (7 actors), Anymail, Reoon, Google Sheets, n8n |
| Appointment Scheduling | Scheduling-System-Status.md | Google Calendar, SuiteDash, Discord approval, n8n |
| Locals 4 Good | L4G-System-Status.md | Supabase (<SUPABASE_PROJECT_ID_L4G>), Lovable frontend, Stripe, n8n |
| QWU WordPress Multisite | QWU-WPMU-System-Status.md | AWS Lightsail, WordPress 6.9.1, Divi 4.27.6 (Divi 5 migration HOLD ... revisit May 5, 2026), 11 QWF program sites + chaplaintig.com (blog_id 10) |
| HQ Command Center | HQ-System-Status.md | Supabase, Lovable frontend, API server (8766), n8n, relationship_intelligence.db |
| [Supporter Organization] Command Center | supporter-CC-System-Status.md | Supabase (<SUPABASE_PROJECT_GCC>), Lovable frontend, Instantly, DMARC Report |
| QMP Quietly Managing Property | QRP-System-Status.md | Supabase (<SUPABASE_PROJECT_ID>), Lovable frontend (9 prompts), n8n (bill pipeline), edge functions (submit-contact-form), Python scripts |
| Pocket Ez | Pocket-Ez-System-Status.md | Supabase (<SUPABASE_PROJECT_POCKET>), Lovable PWA, OpenRouter (Claude Opus 4.7), pocketez.org |
| [Supporter Organization] PDF Plugin | [Supporter Organization]-PDF-Plugin-System-Status.md | AWS Lightsail (supporter-owned), WordPress Multisite, dozens of publication sites, n8n email relay |
| QKN Quietly Knocking | QKN-System-Status.md | Supabase (<SUPABASE_PROJECT_ID>), Lovable frontend, quietlyknocking.org |
| QSP Quietly Spotting | QSP-System-Status.md | Supabase (<SUPABASE_PROJECT_ID>), Lovable frontend, n8n (2 sync workflows), Python scripts, quietlyspotting.org |
| QNT Quietly Networking | QNT-System-Status.md | Supabase (<SUPABASE_PROJECT_ID>), CF Pages frontend (quietlynetworking.org), FastAPI webhook (internal port), Stripe (test), QWF Passport deployed |
| QTR Quietly Tracking | QTR-System-Status.md | Supabase (<SUPABASE_PROJECT_ID>), Lovable frontend, quietlytracking.org |
| Meeting Experience System | MES-System-Status.md | 41 scripts, 6 directives, 10 n8n workflows, Zoom OAuth, Google Calendar, Twilio, contact_history.db |
What Goes In (The Amnesia Test)
Include anything that answers: "If I lost all conversation history, what would I need to know to resume work?"
- Access credentials & methods ... SSH clone commands, deploy key locations, credential IDs (not raw secrets)
- URLs & endpoints ... Project URLs, webhook paths, API endpoints
- What's deployed vs. planned ... Script versions, workflow status, prompt execution state
- Architecture decisions ... "Uses HTTP Request nodes, not Supabase nodes (buggy)"
- Known issues ... Active bugs with severity
- Session log ... What changed and when
Maintenance Rules
- Session-start: Read the relevant status file before working on any project
- Session-end: Update the file if any deployed state changed (enforced by
/session-wrap-up) - Periodic audit: Clone repos and diff actual components against the status file to catch drift
HQ Command Center ⭐ NEW
Added: February 5, 2026
The HQ Command Center is a personal operational dashboard at hq.quietlyworking.org that aggregates tasks, relationships, system health, calendar, and WHELHO life domains into a single power-user interface.
Architecture
Lovable Frontend (hq.quietlyworking.org)
→ Supabase SDK
→ Supabase (<SUPABASE_PROJECT_ID>)
← Synced by n8n (daily 6 AM)
← HQ API (<internal API host>)
← Local Data Sources (relationship_intelligence.db, vault tasks, WHELHO)
Components
| Component | Status | Details |
|---|---|---|
| Supabase Project | ✅ Deployed | <SUPABASE_PROJECT_ID> (US West 1) ... 25+ tables + 4 views (incl. hq_quick_intel/hq_quick_intel_actions/hq_quick_intel_messages/hq_tool_registry as of 2026-04-24) |
| API Server | ✅ Built | hq_api_server.py v1.0.0 on an internal port |
| Task Sync | ✅ Built | sync_hq_tasks.py v1.5.0 ... ~235 tasks synced (SUPERVISOR skip filter) |
| Relationship Sync | ✅ Built | sync_hq_relationships.py v1.6.0 ... 1,435 relationships |
| Meeting Sync | ✅ Built | sync_hq_meetings.py v2.2.0 ... 51 upcoming + 17 past meetings (enriched with relationship data from hq_relationships). Past meetings merge tracker DB + daily notes, resolve attendee emails from entity frontmatter, and clean up stale duplicate records |
| System Health Sync | ✅ Built | sync_hq_system_health.py |
| n8n Daily Sync | ✅ Deployed | <WORKFLOW_ID> v7.0.0 (2026-04-24) ... tasks + relationships + meetings + agent efficiency + app registry + tool registry sync (6 AM, 7 parallel SSH nodes) |
| Lovable Frontend | ✅ Built | 139 prompts total ... includes Projects Intelligence Hub (101-103), L4G Operations Module (105-106), Agent Efficiency (107), Action Queue (108), Testimonials/Voices (109-111), Dynamic Issue Form (112), Content Review (113-115), Issue Tracker Complete App List + Sidebar Fix + Searchable Dropdown (116-118), Quick Intel (119-122), Issue Tracker Mobile Fix + Editable Issues (123-124), Transparency Article Cards (125-126), Issue Detail Polish (127-130), Quick Intel Tool Search (131), Quick Intel Deep Analyze + Usage Panel + Delete (132-133), Email Sources Layout + Perf + Virtualization (134-139) |
| Quick Intel Deep Stack | ✅ Built (2026-04-24) | process_quick_intel_deep.py v1.2.0 (state-machine orchestrator, wisdom.db dedup, resume-aware), n8n Quick Intel Deep Process workflow (<WORKFLOW_ID>, webhook → SSH → orchestrator → Discord), edge function quick-intel v2.1.0 (dedup + 5-surface usage panel + Content preset + smarter dedup_hit gating), schema hq_quick_intel_deep_schema.sql (deep_status/content_status lifecycle + supabase_realtime publication) |
| Tool Registry | ✅ Built (2026-04-24) | hq_tool_registry table ... 25+ tools mirroring CLAUDE.md TWL catalog (slug, display_name, category, status, has_twl, twl_path, entity_path, aliases). sync_hq_tool_registry.py v1.0.0 (TWL directives → registry, idempotent, preserves manual curation). Used by Quick Intel TWL routing combobox (Lovable prompt 131) ... alias-aware search (cf → Cloudflare Pages, fusion → DaVinci Fusion) |
| Email Sources Module | ✅ Built | /email-sources page ... sender table with search/filter/sort, Block Email + Block Domain buttons, skip-confirmation checkbox |
| Sender Tracking | ✅ Built | outlook_pipeline.py v1.6.0 tracks all senders in hq_email_senders Supabase table after classification |
| Google Calendar Edge Function | ✅ Deployed | v3.0.0 ... RFC3339 timestamps with Pacific TZ offset, DST auto-detection, dual-format private key, auth handled in-function |
| Calendar Health Monitor | ✅ Deployed | check_calendar_health.py v1.0.0 ... every 2h cron, auto-redeploy via Supabase CLI, Discord alerts |
| Ezer Agent Edge Function | ✅ Deployed | Claude Opus 4.7, 8 tools, auth handled in-function |
| Dispatch Agent Edge Function | ✅ Deployed | v1.1.0, auth handled in-function (fixed from true) |
| Custom Domain | ✅ Live | hq.quietlyworking.org via Lovable |
| Meeting Prep Emails | ✅ Built | send_meeting_prep_email.py v1.0.0 ... 7-day emails + 30-day briefings |
| Issue Tracker | ✅ Built | Kanban + table views, detail drawer, triage badge, app sidebar cards, color-coded app borders, dynamic Source App dropdown (Prompt 112), filter fix for beta status (Prompt 116), sidebar shows all programs incl. planning (Prompt 117), searchable combobox dropdown (Prompt 118), mobile touch-scroll fix + visible search (Prompt 123), inline editing for description/subject/type/source app (Prompt 124) |
| Notification System | ✅ Built | Unified dispatcher (SMS + Discord + Push), per-person notify toggles, Settings notification preferences |
| App Observatory | ✅ Built | hq_app_registry (64 rows, synced from ecosystem_registry.json via sync_hq_app_registry.py v1.0.0), hq_app_metrics, collector running. Category constraint: app/program/system/infra/pedagogy/content/site. Status constraint: active/building/standby/production/beta/alpha/planning/paused. Includes QOP, QCP, Heart Draft, FORGE, EPIC, GUILD. 3 duplicates cleaned (other_epic→epic, whelho→whl, heart_draft→hd). |
| Projects Intelligence Hub | ✅ Built | 37 projects bootstrapped, momentum scoring, 57 goal milestones, grid/table views, detail drawer |
| Interactive Work Items | ✅ Built | Click-to-cycle status, add manual tasks, internal notes, delete, sync protection via hq_dirty trigger |
| Goals Sync | ✅ Built | sync_hq_goals.py v1.1.0 ... parses Goals doc, upserts milestones with sort_order, skips hq_dirty items |
| Git Activity Sync | ✅ Built | sync_hq_git_activity.py v1.0.0 ... per-project commit recency |
| Health Computation | ✅ Built | compute_hq_project_health.py v1.0.0 ... momentum scoring (0-100), health assessment |
| L4G Operations Module | ✅ Built | Prompt 106 ... Kanban pipeline (5 journey stages), production calendar (area×month grid), deadline alerts. Backend: sync_hq_l4g.py v1.1.0 syncs from L4G Supabase every 15 min |
| L4G Sync | ✅ Built | sync_hq_l4g.py v1.1.0 ... 14 pipeline rows, 18 production rows, 25 alerts. n8n workflow <WORKFLOW_ID> |
| Agent Efficiency | ✅ Built | Prompt 107 ... Dashboard card with redundancy ratio (color-coded), SVG sparkline (last 20 sessions), wasted token estimate, engram candidates list. Backend: sync_hq_agent_efficiency.py syncs from local QCM metrics to HQ Supabase daily |
| Action Queue | ✅ Built | Prompt 108 ... Dashboard panel showing pending decision items from hq_action_queue. First action type: video_article (chaplaintig.com pipeline). Publish/Edit Draft/Reject buttons. Real-time Supabase subscription. Write-back via write_back_dirty_items.py v1.1.0 |
Data Sources
| Source | Frequency | Script |
|---|---|---|
Vault tasks (000 Inbox/___Tasks/) | Daily 6 AM | sync_hq_tasks.py |
Relationships (relationship_intelligence.db) | Daily 6 AM | sync_hq_relationships.py |
| Upcoming meetings (briefings + calendar) | Daily 6 AM | sync_hq_meetings.py |
| Past meetings (daily notes + zoom + tracker DB) | Daily 6 AM | sync_hq_meetings.py |
| System health (twin API) | Every 15 min (Phase 3) | sync_hq_system_health.py |
| L4G pipeline, production, alerts | Every 15 min | sync_hq_l4g.py (3 tables: hq_l4g_pipeline, hq_l4g_production, hq_l4g_alerts) |
| Agent efficiency metrics (QCM redundancy_metrics) | Daily 6 AM | sync_hq_agent_efficiency.py (3 tables: hq_agent_efficiency, hq_engram_candidates, hq_agent_efficiency_summary view) |
| Google Calendar | Live via edge function | google-calendar-events (v3.0.0) |
| Calendar health | Every 2 hours via cron | check_calendar_health.py (auto-redeploy + Discord alert) |
| Action queue (video articles) | Real-time (Supabase subscription) + write-back via n8n | hq_action_queue table, write_back_dirty_items.py v1.1.0 |
UX Efficiency Features (Prompts 038-047)
The HQ Command Center emphasizes power-user efficiency: zero-confirmation actions with undo toasts (Gmail/Slack pattern), keyboard triage mode (j/k navigation + single-key actions), right-click context menus, inline entity editing (click-to-edit contact fields), enhanced Cmd+K command palette (searches people + tasks + pages), double-click entity access, sticky batch action bar, and agent task popout panels (work while agents process in floating panels).
Focus Module (Prompts 065-067)
The Focus module surfaces the 3-5 most urgent tasks. Two input paths:
- Phone capture: Type
@star [task]→ process_inbox.py detects prefix → creates task withstarred: true+priority: high→ sync to HQ - HQ UI: Click star icon on any task row, or press
son a selected task
Focus section appears at the top of the Tasks page (collapsible, starred pending/in-progress tasks as compact cards). Dashboard Focus widget shows top 5 starred tasks. Snooze system includes timezone-safe date picker (Pacific time via Intl.DateTimeFormat).
Quick Capture Prefixes (Prompt 080)
Enhanced phone capture with typed prefixes:
@star [task]... Creates task withstarred: true+priority: high(Focus module integration)@do [task]... Creates task withpriority: high(quick action items, not starred)
Both prefixes are detected by process_inbox.py and route through the standard task sync pipeline to HQ.
QSP Discovery Funnel (Prompt 081)
Unauthorized login attempts now redirect to a QSP discovery page instead of a generic error. When someone visits hq.quietlyworking.org without auth, they see QSP (Quietly Spotting) as the recommended entry point ... routing potential users to the public product rather than a dead end.
Google Calendar Edge Function v3.0.0 (Prompt 084)
The google-calendar-events edge function was rebuilt after diagnosing a persistent failure mode. The fix is locked into Lovable's codebase via Prompt 084 to prevent Lovable from overwriting it.
Root cause: Google Calendar API requires RFC3339 timestamps with timezone offsets (e.g., 2026-02-28T00:00:00-08:00). Lovable's generated version used bare timestamps without offsets, causing 400 Bad Request errors from Google's API.
v3.0.0 changes:
- RFC3339 timestamps with Pacific timezone offset
- DST auto-detection (PST
-08:00vs PDT-07:00) using date-based calculation - Dual-format private key handling (escaped
\\nand literal newlines) auth handled in-function(Lovable kept resetting totrue)- Supabase secrets re-set:
GOOGLE_SA_PRIVATE_KEY,GOOGLE_CALENDAR_TIMEZONE
Self-healing health monitor: check_calendar_health.py v1.0.0 runs every 2 hours via cron. If the edge function fails:
- Calls the edge function endpoint and validates the response
- If broken: auto-redeploys the known-good source from
002 Projects/_HQ Command Center/supabase/functions/google-calendar-events/index.tsvia Supabase CLI - Re-checks after deployment
- Posts to Discord
#system-statuswith status (healthy/healed/still-broken)
Email Sources Module (Prompts 085-087)
The Email Sources module provides visibility into every email address the Outlook pipeline processes, enabling bulk sender management directly from the HQ UI.
Components:
/email-sourcespage ... Full sender table with search, filter by domain, sort by email count/task count/last seen. "Pure cost waste" rows (high email count, zero task count) highlighted with yellow left-border.- Block Email / Block Domain buttons ... Per-row actions that insert into
hq_email_suppressionsfor immediate suppression in the next pipeline run. - Skip Confirmation checkbox ... Custom AlertDialog with "Don't ask again this session" checkbox (
useReffor persistence +useStatefor visual state). When checked, subsequent blocks execute immediately with toast-only feedback. Resets on page refresh. hq_email_senderstable ... Supabase table tracking all scanned senders. Schema: email (unique), domain, display_name, first_seen, last_seen, email_count, task_count, last_classification. Populated byoutlook_pipeline.pyv1.6.0 via PostgREST upsert (fetch + calculate + upsert, because PostgREST cannot do atomicSET count = count + N).
Suppression Impact (March 2026): 82% suppression rate (1,153/1,413 emails skipped). Email processing costs dropped from $1.17/day (Feb) to $0.22/day (Mar) ... 81% reduction, ~$29/month savings.
Projects Intelligence Hub (Prompts 101-103)
The Projects module provides portfolio-level visibility across the QWU project portfolio (67 projects as of 2026-07-28, up from the original 37), answering "Where should I invest my time?"
Backend pipeline:
bootstrap_hq_projects.pyv1.0.0 ... One-time load of the original 37 projects across 6 categories (qwf_app, operational, student_program, supporter, infrastructure, planning). A 7th category,personal, arrived later with the two-plane project model and is populated bysync_personal_projects.py(folder-scan), not by the bootstrap list. New projects are created bycreate_project.py(the canonical creator ... never a handmkdir).sync_hq_goals.pyv1.1.0 ... Parses_Goals and Priorities.md, upserts milestones with numericsort_order(M1→1000, M2→2000, ... M10→10000), skipshq_dirtyitems to protect user editssync_hq_git_activity.pyv1.0.0 ... Scans git log per project folder, updateslast_commit_dateanddays_since_activitycompute_hq_project_health.pyv1.0.0 ... Weighted momentum scoring (0-100): commits recency (25%), strategic priority (20%), no blockers (15%), completion momentum (15%), active work items (25%). Health: healthy (>50), needs_attention (20-50), stalled (<20 + 30d inactive)
Frontend (Prompts 101-103):
- Prompt 101: Grid view (cards grouped by category with momentum rings, health dots, goal badges) + table view (10 sortable columns) + detail drawer (strategic context, work items, activity)
- Prompt 102: Kanban card left border accent matching source app's
color_hexfromhq_app_registry - Prompt 103: Interactive work items ... click-to-cycle status (open → in_progress → done), add manual tasks with inline form, internal notes per item, delete manual items with undo, "HQ" badges on manual items, pencil icon on edited goal milestones
- Search (added 2026-07-28, direct code commit
cc4ff20... no longer a Lovable prompt): a search bar above the category pills. Matches acrossdisplay_name+short_name+project_code+app_code+description+tags+ category label, because the handle you remember lives in a different column per row (MPis a short_name;qcmexists ONLY as a project_code;[Supporter Organization]only as a display_name). Tokens are AND-ed, so "[Supporter Organization] dmarc" narrows to one. Header shows aN of 67match count. Helpers:projectSearchHaystack/tokenizeProjectSearch/matchesProjectSearchinsrc/components/projects/projectConstants.ts; input reuses the houseSearchFiltercomponent. Deliberate tradeoff: a flat haystack (nothing is ever hidden) over a smarter ranking that would suppress description-only matches ... so a 2-character query returns some substring noise, which one more character clears. - Grid-view category bug fixed (same commit):
ProjectGridderived its render order solely fromCATEGORY_CONFIG, so the 4category='personal'projects ... a category never added to that map ... were invisible in grid view (the default), appearing only in table view. Fixed by adding thePersonalcategory AND ordering off the union of config keys + categories actually present in the data, so a future category cannot silently vanish the same way.
Sync protection: hq_dirty boolean + preserve_work_item_hq_edits PostgreSQL trigger. When a user edits a milestone in HQ, the trigger prevents the next sync_hq_goals.py run from overwriting those changes. Manual items (source='manual') are naturally safe via the UNIQUE(source, source_ref) constraint.
Tables: hq_projects (67 rows across 7 categories as of 2026-07-28: qwf_app 15 · operational 19 · infrastructure 14 · supporter 7 · planning 6 · personal 4 · student_program 2), hq_work_items (goal milestones + manual items, with sort_order, hq_dirty, last_hq_edit_at columns)
Action Queue Panel (Prompt 108)
The Action Queue surfaces pending decision items directly on the HQ dashboard, starting with video_article items from the chaplaintig.com pipeline.
Flow:
Pipeline creates WordPress draft
→ inserts row into hq_action_queue (status: pending)
→ HQ dashboard shows card with Publish / Edit Draft / Reject buttons
→ user clicks → hq_action_log entry created
→ write_back_dirty_items.py processes action
→ tig_publish_article.py publishes (or marks rejected)
→ hq_action_log.processed_at stamped
Schema changes (HQ Supabase <SUPABASE_PROJECT_ID>):
hq_action_queue...statuscolumn (pending/completed/dismissed/expired)hq_action_log...processed_atcolumn (write-back processing tracker)hq_video_articles... stores article metadata for the approval UI- RLS policies for broadcast items (
user_id IS NULL)
Write-back: write_back_dirty_items.py v1.1.0 reads unprocessed hq_action_log entries, dispatches by action type (publish runs tig_publish_article.py, reject marks dismissed), then stamps processed_at. Triggered by n8n workflow.
Testimonial Intelligence Pipeline (Prompts 109-111)
Added: March 30, 2026
End-to-end system for discovering, approving, and publishing testimonials about Chaplain TIG from multiple sources.
Pipeline stages:
Sources (meetings, LinkedIn, manual)
→ extract_testimonials.py (discovery)
→ score_testimonial.py (0-1 quality + WHELHO realm mapping)
→ Pipeline routing:
Public source → tig_pending (TIG reviews in HQ)
Private source → giver_pending (Ezer asks permission)
→ testimonial_giver_approval.py (warm email via Ezer)
→ TIG approves in HQ Voices page
→ sync_testimonials_to_wp.py (JSON cache to WordPress)
→ tig-testimonials-widget.php (live on chaplaintig.com)
Backend scripts (7):
extract_testimonials.py... Main orchestratorextract_testimonials_meetings.py... Scans meeting transcripts for praise/endorsementextract_testimonials_linkedin.py... Scrapes LinkedIn recommendations via Apifyscore_testimonial.py... Quality scoring + WHELHO realm/theme taggingtestimonial_giver_approval.py... Ezer sends warm approval request emails (Exempt classification)sync_testimonials_to_hq.py... Pushes to HQ action queuepublish_testimonials_wp.py... Generates display output, pushes to WordPress
WordPress widget: tig-testimonials-widget.php v1.0.1 ... Dynamic testimonial slider with auto-rotating cards, progress bar, mouseover pause with "Paused" indicator, shortcode [tig_testimonials]. Reads from /wp-json/tig/v1/testimonials REST endpoint backed by JSON cache synced from Supabase.
HQ Frontend:
- Prompt 109: Voices page ... Kanban pipeline view with filter tabs (Needs Review / Awaiting Giver / Approved / Published / Archived), testimonial cards with quality scores (color-coded), source type badges (LinkedIn=blue, Meeting=purple, Networking=amber, Google=green), WHELHO realm tag pills, action buttons per status, Add Testimonial slide-over form, real-time Supabase subscriptions, stats bar
- Prompt 110: Inline editing ... click-to-edit quote text directly on cards
Schema: hq_testimonials table with multi-stage pipeline_status (extracted → giver_pending → giver_approved → approved → published), quality_score (0-1), realm_tags (WHELHO), themes, dedup_hash (SHA-256), giver_approval_needed boolean.
Directive: 005 Operations/Directives/testimonial_intelligence.md
Key Files
| File | Purpose |
|---|---|
002 Projects/_HQ Command Center/HQ-System-Status.md | System Status (source of truth) |
005 Operations/Directives/hq_command_center.md | Governing directive |
005 Operations/Data/relationship_intelligence.db | Relationship intelligence SQLite DB |
005 Operations/Data/linkedin_export/ | Source LinkedIn data (2 exports) |
002 Projects/_HQ Command Center/handoff-meeting-intelligence-pipeline.md | Meeting module handoff (Upcoming + Past) |
005 Operations/Execution/check_calendar_health.py | Calendar edge function health monitor (v1.0.0, every 2h cron) |
002 Projects/_HQ Command Center/supabase/functions/google-calendar-events/index.ts | Known-good edge function source (v3.0.0) |
002 Projects/_HQ Command Center/084-lovable-prompt-fix-calendar-edge-function.md | Lovable prompt locking edge function code |
005 Operations/Execution/bootstrap_hq_projects.py | One-time project bootstrap (37 projects) |
005 Operations/Execution/sync_hq_goals.py | Goals doc → milestones sync (v1.1.0, hq_dirty aware) |
005 Operations/Execution/sync_hq_git_activity.py | Per-project git commit recency sync |
005 Operations/Execution/compute_hq_project_health.py | Momentum scoring + health assessment |
005 Operations/Execution/sync_hq_agent_efficiency.py | QCM redundancy metrics → HQ Supabase sync |
005 Operations/Execution/write_back_dirty_items.py | Process HQ action log entries (publish/reject articles) v1.1.0 |
005 Operations/Directives/testimonial_intelligence.md | Testimonial pipeline SOP |
005 Operations/Execution/extract_testimonials.py | Testimonial extraction orchestrator |
005 Operations/Execution/score_testimonial.py | Quality scoring + WHELHO realm mapping |
005 Operations/Execution/testimonial_giver_approval.py | Ezer giver approval emails |
005 Operations/Execution/sync_testimonials_to_hq.py | Push to HQ action queue for TIG review |
005 Operations/Execution/publish_testimonials_wp.py | Generate Divi shortcode, push to WordPress |
005 Operations/Execution/sync_testimonials_to_wp.py | Supabase → WordPress JSON sync |
005 Operations/Execution/wp-mu-plugins/tig-testimonials-widget.php | WordPress testimonial widget v1.0.1 (slider/wall/grid/ticker/featured) |
Quick Intel ... YouTube Video Analysis (Prompts 120-122)
Added: April 12, 2026
Quick Intel lets TIG paste a YouTube URL into HQ and get instant AI-powered analysis about the video's relevance to the QWU backoffice stack, tools, and mission. Designed for the mobile capture, desktop decide workflow ... triage videos on the phone, review and route on the desktop.
How it works:
HQ Frontend (URL + question)
→ Edge Function (quick-intel)
→ YouTube Data API (metadata)
→ YouTube captions (transcript extraction)
→ Claude Opus 4.7 (analysis with QWU context)
→ Response stored in hq_quick_intel table
→ Follow-up conversation via hq_quick_intel_messages
→ Action routing via hq_quick_intel_actions
→ Backoffice process_quick_intel_action.py
Preset quick actions: Relevant? | Key Insights | Wisdom | TWL | Brand Voice
Routing actions (combinable, checkbox-based):
| Action | Pipeline | End State |
|---|---|---|
| Content | Full Gemini multimodal → article → social atoms | Content Review page + Wisdom Library |
| Wisdom | Save selected insights (no article/social) | Wisdom Library |
| TWL | Extract tool patterns, gotchas, best practices | wisdom.db with tool tags |
| Idea | Create issue tagged to QWF property | HQ Issues page |
| Student | Flag for MP curriculum | HQ Tasks with skill area tag |
| Copy | Copy analysis to clipboard | Local only |
Hot priority toggle (flame icon) bumps all selected actions to high priority.
Edge function: quick-intel on HQ Supabase (<SUPABASE_PROJECT_ID>), auth handled in-function. Secrets: ANTHROPIC_API_KEY, YOUTUBE_API_KEY.
Tables: hq_quick_intel (sessions), hq_quick_intel_actions (routing queue), hq_quick_intel_messages (follow-up chat). Schema: hq_quick_intel_schema.sql.
Backoffice handler: process_quick_intel_action.py v1.0.0 ... processes pending actions from Supabase (7 action types).
Mobile: Quick Intel card pinned to top of mobile dashboard (prompt 122). Compact layout with horizontally scrollable preset chips.
Issue Tracker Improvements (Prompts 123-124)
Added: April 12, 2026
- Prompt 123 (Mobile Fix) ... Fixed Source App dropdown on mobile: touch scroll was blocked by Popover component intercepting touch events, search input not visible due to Popover positioning. CSS
touch-action: manipulationfixes + PopoversideOffsetadjustment. - Prompt 124 (Editable Issues) ... Added inline editing in the Issue Tracker detail drawer: click-to-edit description, subject, type badge, and source app. Changes save directly to Supabase without opening a separate form.
In-App Search Rollout (2026-07-28 → 07-30, direct code commits)
Added: July 30, 2026. Not Lovable prompts ... HQ is direct-code since the CF Pages migration.
Search was added to the three surfaces that lacked it, chosen by measurement rather than hunch: every page and component tree was greped for a search input, then live row counts were pulled for all 28 HQ tables. Surfaces that already had search (Wisdom 10.5k rows · Relationships 1,973 · Ezer Activity 1,360 · Tasks 761 · Content Review 590 · Suppressions 499 · Issues · Meetings · Subscriptions · QCM Contacts) were left alone.
| Surface | Rows | Commit | Haystack |
|---|---|---|---|
| Projects | 68 | cc4ff20 | display_name + short_name + project_code + app_code + description + tags + category label |
| Brain | 813 | b374352 | decision_title + decision_one_liner + non_technical_description + why_at_decision + category + corrected_category + source_app + checkpoint + trust_weight + verdict_status |
| Observatory | 68 | 2d08c21 | display_name + app_code + domain + category + status + health_status |
Shared design rules (all three):
- Flat haystack, AND-ed tokens. Every field searched together; a project or card is never hidden because something else matched by name. A tiered match would scan tighter but introduces a mode the user cannot see ... unacceptable on a governance surface, and needless elsewhere. The cost is some substring noise on 2-character queries, which one more character clears.
- The name you remember lives in a different column per row.
MPis ashort_name;qcmexists ONLY as aproject_code;[Supporter Organization]only as adisplay_name. Single-field search would have been broken for most rows ... which is why the data was queried before the feature was designed. N of TOTALmatch count on every one. This is the detector, not decoration: a count that does not reconcile is how silent under-rendering announces itself.- Portfolio-level stats never re-scope. Observatory's ecosystem tiles and Brain's pending count stay on the full set; only the card list narrows.
- Reuses the house
SearchFiltercomponent (src/components/SearchFilter.tsx, 300 ms debounce, clear button). No new input was written.
Brain also gained a verdict-status filter (multi-select chips: pending / silent / reinforced / redirected / applied, all-on by default, AND-ing with the existing trust-weight chip). Previously "every redirect" (2 cards of 813) and "every reinforced" (99) had no filter path at all. The ?card= deep-link now clears search and restores all statuses before resolving, because it queries rendered DOM and would otherwise report "Card not found" for a visible card. Closes HQ issue 4aefd8c1.
Three invisible-row bugs were found by this work (pattern: memory/feedback_config_mirror_driving_iteration_hides_rows.md):
| Surface | What was hidden | State |
|---|---|---|
| Projects grid | All 4 category='personal' projects, ~7 weeks, in the DEFAULT view | ✅ Fixed cc4ff20 |
| Projects health tiles | 31 of 68 projects at health='unknown' ... no tile, no filter path | ⏳ Open, HQ issue 1a9a0e5d |
| L4G pipeline board | 48 of 62 bookings at journey_stage='awaiting_choice' | ✅ Visibility fixed 2d08c21; placement is a TIG decision, HQ issue 34031f72 |
The shared mechanism: a config map or hardcoded list that drives render ITERATION, so a value it was never taught renders nowhere while a count beside it still includes the row. Fixed by ordering off the union of known keys and keys present in the data, so an unknown value sorts last instead of vanishing.
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Supabase Schema Design | SQL, RLS policies, PostgREST | ⭐⭐⭐ |
| API Server (no framework) | Python http.server, REST design | ⭐⭐ |
| Data Sync Pipeline | ETL patterns, incremental sync, deduplication | ⭐⭐⭐ |
| Lovable Frontend | AI-assisted UI development, prompt engineering | ⭐⭐ |
| Meeting Intelligence Pipeline | Calendar APIs, vault enrichment, relationship health scoring, email automation | ⭐⭐⭐ |
| Self-Healing Infrastructure | Health checks, auto-remediation, Supabase CLI deployment, cron monitoring | ⭐⭐⭐ |
| Quick Intel Feature Build | Edge functions, YouTube APIs, LLM integration, full-stack feature delivery | ⭐⭐⭐ |
| Search-a-Data-Page (Projects search, 2026-07-28) | React useMemo filtering, debounced input, reusing a house component instead of writing a new one, ground-truthing the data before designing the feature | ⭐⭐ |
| The Invisible-Row Bug Class (same session) | Defensive rendering: why iterating a config map to decide what to render silently deletes rows, how to iterate data instead, and why a N of TOTAL count is the cheapest detector | ⭐⭐⭐ |
| Measure-Before-You-Build (search rollout, 2026-07-30) | Surveying a codebase for a missing capability, then ranking candidates by live row counts instead of intuition; recognising that the highest-count surfaces already had it | ⭐⭐ |
Why the three above pair well as one lesson (~90 min, Intermediate): the student's instinct is to search display_name and ship. Querying the live table first reveals the name they'd search by is scattered ... MP is a short_name, qcm exists only as a project_code ... so the naive build is broken for two thirds of the rows. Same session then surfaced a 7-week-old invisible-data bug caused by a config map that gated rendering. Teaches the habit ("look at the real data before you design") and the failure mode it prevents, on the same 30-line diff. Prerequisites: React + TypeScript basics, REST/JSON. Reference: feedback_config_mirror_driving_iteration_hides_rows.md.
QWR SEO Intelligence ⭐ NEW
Added: February 5, 2026
The SEO Intelligence system provides keyword research, competitive analysis, and content opportunity scoring for QWR (Quietly Writing) to help users discover what to write about.
Components
| Component | Status | Details |
|---|---|---|
| Keyword Research | ✅ Built | qwr_keyword_research.py v1.1.0 ... DataForSEO integration with null safety |
| Opportunity Scorer | ✅ Built | qwr_opportunity_scorer.py ... Multi-signal scoring |
| n8n Keyword Lookup | ✅ Built | qwr-seo-keyword-lookup.json ... On-demand webhook |
| n8n Weekly Refresh | ✅ Built | qwr-seo-weekly-refresh.json ... Scheduled analysis |
| Wisdom Library Monitor | ✅ Built | qwr-wisdom-library-monitor.json ... Source change detection |
| Unsubscribe RPC | ✅ Built | unsubscribe_from_wisdom_source ... Supabase RPC for Remove button (cascading cleanup of entries + source) |
| SQL Migration | ✅ Built | 2026-02-05-seo-intelligence-tables.sql |
| Lovable SEO Prompts | ✅ Written | lovable-prompt-seo-keyword-intelligence.md, lovable-prompt-seo-opportunities-widget.md |
Key Files
- Directive:
005 Operations/Directives/qwr_seo_intelligence.md - Scripts:
qwr_keyword_research.py,qwr_opportunity_scorer.py - Workflows:
qwr-seo-keyword-lookup.json,qwr-seo-weekly-refresh.json,qwr-wisdom-library-monitor.json - Migration:
005 Operations/Migrations/2026-02-05-seo-intelligence-tables.sql
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Google Autocomplete Scraping | HTTP requests, parsing, rate limiting | ⭐⭐ |
| Multi-Signal Scoring | Weighted algorithms, normalization | ⭐⭐⭐ |
| n8n Workflow Design | Webhook triggers, SSH execution, error handling | ⭐⭐ |
Supporter System Safeguards ⭐ NEW
Added: February 5, 2026
QWF serves supporters with hosted infrastructure. Supporter systems are off-limits to autonomous agent operations unless explicitly directed for a specific task.
SSH Permission Protocol
Before SSHing to ANY server:
- Is this server in CLAUDE.md "Infrastructure Access" whitelist? → Proceed (QWU infrastructure)
- Is this server in "Supporter System Registry"? → ASK for explicit permission first
- Unknown server? → STOP and ASK
Supporter System Registry
| Customer | Server | Project Folder | Guardrails |
|---|---|---|---|
| [Supporter Organization] | <SUPPORTER_SERVER_IP> | _[Supporter Organization] Projects/ | <supporter deploy-guard script> ... no writes 8 AM-5 PM Pacific |
Deploy Guard Script
005 Operations/Execution/<supporter deploy-guard script> prevents accidental writes to supporter production servers during business hours. Run it before any supporter server operation.
Why This Exists
On 2026-02-05, an agent SSHed to a supporter's server without permission, mistaking it for QWU infrastructure. The project folder name identified a supporter organization, but the agent ignored that signal. This safeguard system ensures supporter systems are always treated with explicit authorization.
Data Safety: The foundational directive supporter_data_safety.md codifies how to safely sync and handle live supporter business data ... safety gates, audit logging, soft-delete, rollback runbook, and anomaly detection. All sync scripts touching supporter data must integrate sync_safety_gate.py.
Supporter Server Operations (Withheld)
Four subsections documenting a specific supporter's email relay and server-hardening details (ban thresholds, blocklists, capacity figures) are excluded from the public manual: publishing a supporter's defensive configuration would hand a hostile reader an attack map. The generic patterns (fail2ban jails, iptables persistence) are standard Linux administration ... the commands below remain as teaching material.
Useful commands:
# Check fail2ban status
sudo fail2ban-client status
sudo fail2ban-client status wordpress-flood
# Unban an IP
sudo fail2ban-client set wordpress-login unbanip <IP>
# View current iptables blocks
sudo iptables -L INPUT -n
# Save updated rules (after manual iptables changes)
sudo /usr/sbin/iptables-save | sudo tee /etc/iptables.rules > /dev/null
Key Files
- CLAUDE.md ... Supporter Systems section with registry and protocol
- Script:
005 Operations/Execution/<supporter deploy-guard script> - Script:
005 Operations/Execution/<supporter bug-report relay script>(email relay) - MEMORY.md ... Supporter systems lesson learned
[Supporter Organization] Command Center ⭐ NEW
Added: February 9, 2026
The [Supporter Organization] Command Center is a multi-company supporter-facing dashboard for a supporter family's 4 service companies. Built as a client-facing layer powered by QWU Backoffice ... all orchestration, scripts, and AI stay inside QWU while the [Supporter Organization] team sees dashboards and takes actions.
Architecture
Lovable Frontend (<supporter-subdomain>.quietlyworking.org)
→ Supabase SDK
→ Supabase (<SUPABASE_PROJECT_GCC>, us-west-1)
← Synced by n8n + Python scripts on claude-dev
← Data Sources (Instantly, DMARC Report, Google Workspace, QWR Agency API)
Companies Served
Four supporter companies across construction, rentals, and creative arms. Names and domains are withheld from the public manual.
Stack
| Layer | Technology | Details |
|---|---|---|
| Frontend | Lovable | 8 prompts executed (auth, impersonation, brand, email health, leads, campaigns, settings/profile) |
| Backend | Supabase | 17 tables with RLS, separate project from QWR |
| Auth | Supabase Auth | Magic link, branded email templates, avatars bucket |
| Data Sync | n8n + Python | Instantly API, DMARC Report API, QWR Agency API |
| Brand | [Supporter Organization] Brand Kit | Dark mode default, 3-color brand palette, brand display font |
Phase Roadmap
| Phase | Name | Status |
|---|---|---|
| 0 | Foundation (Supabase + Auth + Layout Engine) | Complete |
| 1 | Lead Engine + Core Dashboard | In Progress |
| 2 | Content & SEO Intelligence | Not Started |
| 3 | Calculators & SOP Engine | Not Started |
| 4 | AI Intelligence Layer | Not Started |
| 5 | System Map & Polish | Not Started |
| 6 | Creative-arm productization | Not Started |
Key Files
| File | Purpose |
|---|---|
002 Projects/_[Supporter Organization] Projects/[Supporter Organization]-Command-Center/supporter-CC-System-Status.md | System Status (source of truth) |
002 Projects/_[Supporter Organization] Projects/[Supporter Organization]-Command-Center/supporter-CC-Project-Brief.md | Project brief (v0.1.0) |
002 Projects/_[Supporter Organization] Projects/[Supporter Organization]-Command-Center/[Supporter Organization]-brand-reference.md | Brand kit (extracted from 14-page PDF) |
002 Projects/_[Supporter Organization] Projects/[Supporter Organization]-Command-Center/004-lovable-prompt-email-infrastructure.md | Email Health page prompt |
002 Projects/_[Supporter Organization] Projects/[Supporter Organization]-Command-Center/005-lovable-prompt-lead-pipeline.md | Lead Pipeline page prompt |
002 Projects/_[Supporter Organization] Projects/[Supporter Organization]-Command-Center/006-lovable-prompt-campaign-management.md | Campaign Management page prompt |
005 Operations/Directives/<supporter outreach campaigns directive> | 3 campaigns with ICPs + 9 email templates |
005 Operations/Directives/provision_sending_domain.md | Domain provisioning (Cloudflare → Workspace → DMARC → Instantly) |
002 Projects/_[Supporter Organization] Projects/QWF-[Supporter Organization]-Ecosystem-Overview.md | 100,000 ft view: 4 Mermaid diagrams (ecosystem rings, data pipeline, flywheel thesis, before/after) |
002 Projects/_[Supporter Organization] Projects/[Supporter Organization]-SOPs/[Supporter Organization]-SOPs-System-Status.md | SOP digitization status (40 SOPs identified across 6 categories from 34 source documents) |
002 Projects/_[Supporter Organization] Projects/[Supporter Mascot]-Plushie-Weavy-Workflow.md | [Supporter Mascot] plushie 3-phase Weavy workflow spec (vendor reference package for [Supporter Team Member]) |
005 Operations/Directives/weavy_creative_workflows.md | Weavy creative production directive (v1.5.0) ... powers [Supporter Mascot] workflow |
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Multi-Company Supabase Schema | RLS policies, company isolation, role-based access | ⭐⭐⭐ |
| Lovable Prompt Engineering | AI-assisted UI, backend-first pattern, iterative refinement | ⭐⭐ |
| Brand Kit to Code Translation | Design system extraction, CSS variables, dark mode | ⭐⭐ |
| Supabase Auth Configuration | Magic link, email templates, redirect URLs, Management API | ⭐⭐⭐ |
| Cold Email Infrastructure | Sending account warmup, DMARC compliance, domain health | ⭐⭐ |
| Data Source Integration | API sync patterns (Instantly, DMARC Report, QWR Agency API) | ⭐⭐⭐ |
Site Replication Pipeline (QWB) ⭐ NEW
Added: April 8, 2026
A 7-stage deterministic pipeline for replicating existing websites as pixel-perfect static sites on Cloudflare Pages. Core engine for the QWB (Quietly Webbing) program. Migrated from React SPA to Astro static framework on April 9, 2026 after React caused Lighthouse to drop from 99→64. Astro outputs zero JavaScript by default (projected Lighthouse 95-100). First tested on a supporter's Squarespace website (replicated to a private sandbox).
Architecture: Content is Sacred
The pipeline strictly separates deterministic extraction (zero AI) from constrained code generation. Previous AI-only approach produced ~55% fidelity because AI hallucinated content, truncated text, and rearranged sections. The pipeline achieves this by:
- Stages 1-4 and 6: 100% deterministic (no AI) ... Playwright DOM traversal,
innerHTMLpreservation,getComputedStylecapture, pixel-level screenshot comparison - Stage 5: AI-constrained ... Generates Astro pages (was React) with content provided verbatim (not generated)
- Stage 7: AI-targeted ... Claude Vision reads diff images and makes specific CSS fixes
Pipeline Stages
| Stage | Script | Purpose |
|---|---|---|
| 0. SCAFFOLD (Session 251) | scaffold_supporter_site.py v1.2.0 | Provision new supporter-site repo from QuietlyWorking/qwb-supporter-site-template (18 primitives, lint gate, CI workflow). Idempotent 10-step run: GitHub repo + CF Pages project + HQ hq_app_registry row + .site-config.json + .approved-assets.json. Regenerates src/styles/tokens.css from supporter's BRAND.json (see contract in directive §Stage 0) |
| 1. DISCOVER | discover_site.py | Crawl sitemap, detect platform (Squarespace/WordPress/Wix), enumerate pages. Refuses to run unless Stage 0 completed (scripts/lint_sandbox.mjs present) |
| 2. CAPTURE | capture_site.py | Squarespace JSON API, Playwright rendered HTML, full-page screenshots (3 viewports), asset download |
| 3. EXTRACT | extract_dom.py | DOM traversal: verbatim innerHTML, section ordering, image-to-section mapping, computed styles |
| 4. TOKENIZE | tokenize_styles.py | Color clustering, typography detection, spacing scale, Tailwind v4 @theme generation |
| 5. GENERATE ⭐ NEW (Session 258) | page_json_to_astro.py v1.0.0 | Deterministic page.json → Astro converter. 48 renderer functions across 13 implemented primitives. AssetAuthority rejects unauthorized image URLs. Low-confidence sections wrapped in {/* REVIEW NEEDED */} comments. Emits <page>.astro.conversion-log.json sidecar (committed to git). Strict-by-default with --allow-unregistered + --allow-missing-primitive dev escape hatches |
| 6. COMPARE | compare_screenshots.py | Pixel-level comparison: baseline vs replica, diff images, match percentage |
| 6b. PARITY ⭐ NEW (Session 258) | check_sandbox_parity.py v1.0.0 | Playwright-based 9-check structural audit (h1, headings, nav/footer links, image count, form presence, meta, sections). Findings allowlist at <site_repo>/.parity-allow.json decays over time |
| 7. REFINE | refine_components.py | Claude Vision targeted fixes, comparison loop until 95%+ (planned) |
Orchestrator (Session 258): rebuild_page_from_production.py is the single-command entry point that wraps migrate_page.py → page_json_to_astro.py → npm run lint:sandbox → git commit for rebuilding any page from its production URL. Makes the right thing the easy thing.
Asset authority (Session 258): 005 Operations/Standards/site/approved-assets.json is the central registry; sync_approved_assets.py pushes supporter-scoped copies into each /<slug>-website/.approved-assets.json. Both the converter and the sandbox lint read it ... 4-tier check chain (scraped page URLs, cdnPrefixes, cdnUrls, localPathGlobs). Unauthorized URLs halt in strict mode; dev iteration uses --allow-unregistered.
Sign-off gate (Session 258): Before a supporter sees the alpha feedback link, operator walks 005 Operations/Directives/qwb_supporter_site_signoff_checklist.md. Stricter caps for pre-production (lint allowlist ≤3, parity allowlist ≤5).
tokens.css generation contract (hardened Session 257): Stage 0's step_generate_tokens_css() reads BRAND.json v2.1 and regenerates src/styles/tokens.css. Brand-derived tokens (colors, fonts, gradients) are swapped per-supporter; structural tokens (spacing, radii, type scale, shadows, motion) are preserved verbatim from the template. Drift-prone optional keys (accent.primaryHover, foundation.borderStrong, gradients.signature) cascade on-brand (darken accent.primary 20% / semantic.slate / foundation-derived gradient) rather than defaulting to unbranded blues and grays. Every fallback logs a tokens.css WARN: line. Regression fixtures at 005 Operations/Execution/tests/scaffold_tokens_css/.
First Baseline Results (April 8, 2026)
- 185 pages discovered (vs old scraper's hardcoded 19)
- 19 core pages captured: JSON API + rendered HTML + 57 screenshots + 155 assets
- 117 sections extracted with 5700 element styles
- Pixel comparison baseline: 13.4% (current sandbox vs production, 57 screenshot pairs)
- Design system auto-detected: brand font + 3-color palette
Key Files
| File | Purpose |
|---|---|
005 Operations/Directives/site_replication_pipeline.md | Full directive with architecture and edge cases |
005 Operations/Execution/site_replication/ | All pipeline scripts (6 built, 2 planned) |
005 Operations/Execution/site_replication/run_pipeline.py | Orchestrator (stages 1-6) |
.tmp/site_replication/<supporter-domain>-com/ | [Supporter Organization] capture/extract/compare data (223MB) |
.tmp/[Supporter Organization]-sandbox-audit-260407b.md | Full 19-page visual audit report |
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Playwright DOM extraction | Browser automation, JavaScript evaluation, getComputedStyle | ⭐⭐⭐ |
| Pixel-level image comparison | Image processing, Pillow, similarity metrics | ⭐⭐ |
| Squarespace JSON API | REST APIs, CMS data structures, content extraction | ⭐⭐ |
| Design token extraction | CSS analysis, color clustering, typography detection | ⭐⭐⭐ |
| Visual regression testing | QA automation, screenshot comparison, diff reporting | ⭐⭐ |
| Deterministic code emitters (Session 258) | Python dispatch registries (dict[tuple, callable]), template-free code generation, when-to-avoid-LLMs | ⭐⭐⭐⭐ |
| Authority check chains (Session 258) | 4-tier decision logic, defense-in-depth validation, fail-closed-by-default APIs with named escape hatches | ⭐⭐⭐ |
| Round-trip test harnesses (Session 258) | Behavioral assertions vs golden files, pytest-style runners, maintainable test design | ⭐⭐⭐ |
| Parity audits with Playwright (Session 258) | Headless browser testing, symmetric two-URL rendering, fuzzy-match scoring with difflib | ⭐⭐⭐ |
| Central registry + scoped distribution (Session 258) | Source-of-truth design, idempotent sync scripts, offline CI reads | ⭐⭐⭐ |
Pocket Ez Companion App ⭐ NEW
Added: February 9, 2026
Pocket Ez is a mobile-first AI companion app where Ezer Aión (Ez) has "morphed into the shape of a phone." This is Ez at her most personal ... a bioluminescent, emotionally intelligent companion that exists entirely for the individual user. Built with Lovable (frontend) + Supabase (backend) + Claude via OpenRouter (AI).
Architecture
User (pocketez.org / pocket-ez.lovable.app)
→ Lovable PWA (React + Vite + Tailwind)
→ Supabase Edge Functions (Deno)
→ Claude Opus 4.7 via OpenRouter (conversation)
→ Claude Sonnet 4.5 via OpenRouter (memory extraction, background)
→ Supabase Postgres (8 tables with RLS)
Stack
| Layer | Technology | Details |
|---|---|---|
| Frontend | Lovable PWA | React + Vite + Tailwind + shadcn/ui, bioluminescent particle system |
| Backend | Supabase | <SUPABASE_PROJECT_POCKET> (us-west-2), 8 tables, 3 edge functions |
| Auth | Supabase Magic Link | Custom bioluminescent email template |
| AI | OpenRouter | Opus 4.7 (conversation), Sonnet 4.5 (memory extraction) |
| Domains | pocketez.org (primary) | pocket-ez.lovable.app (Lovable) |
| Repo | github.com/QuietlyWorking/pocket-ez | Lovable-managed |
Key Features
- 8 Quick Intents: Just talk, Help me think, Center me (SDF), I matter (Miracle), QWU Gateway, Journal, Health check-in, My schedule
- Memory System: Ez remembers facts, preferences, relationships, concerns, achievements, dates, and emotional patterns across sessions. Extracts memories via background Sonnet calls; injects top 15 by importance into each conversation.
- Bioluminescence: The entire UI shifts color based on Ez's emotional state (10 emotion types with intensity and transition data)
- Progressive Trust Tiers: 0 (anonymous) through 4 (operator) ... nothing gated behind login
- Critical Safety Fallbacks: If the API fails, mode-aware pre-written responses ensure the user always receives warmth. Someone tapping "I matter" during an outage still hears they're a miracle.
- Crisis Detection: Ez detects suicidal ideation and provides 988/Crisis Text Line resources while staying in character
Critical Safety Design Pattern
This pattern should be adopted by any future user-facing QWF application.
When any error occurs (API timeout, network failure, empty response), the system returns a mode-aware fallback instead of generic error text. Fallbacks are:
- Handwritten and reviewed (not AI-generated)
- Mode-specific (miracle gets affirmation, SDF gets breathing exercise, etc.)
- Rendered identically to normal responses (user can't tell the difference)
- Deployed in both backend (edge function v4) and frontend (client-side fallback bank)
The words "try again", "error", "something went wrong" do not exist anywhere in the user-facing codebase. Ez doesn't have outages. She has moments where the bridge between worlds is thin.
Key Files
| File | Purpose |
|---|---|
002 Projects/_Pocket Ez/Pocket-Ez-System-Status.md | System Status (source of truth) |
002 Projects/_Pocket Ez/Pocket-Ez-Creative-Brief.md | Creative brief (lore, design, decisions) |
002 Projects/_Pocket Ez/lovable-prompt-01-living-shell.md through 06-refocus.md | 6 Lovable build prompts |
002 Projects/_Pocket Ez/007-lovable-prompt-timezone-fix.md | Pacific timezone fix (Prompt 007) |
002 Projects/_Pocket Ez/008-lovable-prompt-landing-page-alpha-stage.md | Landing page + alpha stage (Prompt 008) |
002 Projects/_Pocket Ez/edge-functions/submit-contact-form.ts | Contact form edge function (source_app: pez) |
002 Projects/_Pocket Ez/lovable-bugfix-critical-safety.md | Critical safety fallback prompt |
003 Entities/Voice Profiles/Ezer Aión/Pocket Voice.md | Ez's third voice profile (pocket-specific hybrid) |
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Supabase Edge Functions (Deno) | Serverless TypeScript, Deno runtime, CORS, JWT | ⭐⭐⭐ |
| Memory Extraction Pipeline | LLM-powered data extraction, background processing, JSON parsing | ⭐⭐⭐ |
| Critical Safety Fallback Design | Defensive UX, error handling philosophy, mode-aware systems | ⭐⭐ |
| Lovable Prompt Engineering | AI-assisted UI, iterative prompting, PWA patterns | ⭐⭐ |
| OpenRouter API Integration | Multi-model routing, cost optimization (Opus vs Sonnet) | ⭐⭐ |
| Progressive Trust Architecture | Auth tiers, device tokens, magic link, RLS policies | ⭐⭐⭐ |
Public Manual Generation System ⭐ NEW
Purpose
The QWU Backoffice User Manual contains sensitive operational data ... Azure credentials, IP addresses, SSH configs, Supabase project IDs, personal names, webhook URLs, and API key patterns. The Public Manual Generation System creates a redacted version suitable for publishing to transparency.quietlyworking.org via Digital Garden, preserving educational and transparency value while removing all sensitive data.
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Private Manual (this file) │
│ dg-publish: false │
│ 9,500+ lines with all operational data │
└────────────────────┬─────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ generate_public_manual.py (v2.0.0) │
│ 1. Load canonical supporter blocklist (FAIL-CLOSED if missing) │
│ 2. Section exclusions (Session Log, Document History → notes) │
│ 3. Literal replacements (140+ rules from JSON config) │
│ 4. Regex replacements (supporter names, JWT patterns, IDs) │
│ 5. Frontmatter update (dg-publish: true) │
│ 6. Supporter-name HARD-RULE scan on final bytes │
│ → any finding ABORTS generation, writes NOTHING │
│ 7. Leak validation scan + ATOMIC write (temp + rename) │
└────────────────────┬─────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Public Manual [PUBLIC].md │
│ dg-publish: true │
│ ~7,000 lines with <PLACEHOLDER> tags and educational notes │
│ Published to transparency.quietlyworking.org via Digital Garden │
└──────────────────────────────────────────────────────────────────┘
Key Files
| File | Purpose |
|---|---|
005 Operations/Execution/generate_public_manual.py | Generation script (v2.0.0: fail-closed supporter gate, atomic write, --self-test) |
005 Operations/Data/public_manual_redactions.json | Redaction config (v2.0.0, 140+ rules + reviewed exceptions) |
005 Operations/Data/roadmap_scrub_blocklist.json | CANONICAL supporter-name list, SHARED with the Living Roadmap scrub (one list, two enforcers) |
005 Operations/Execution/stamp_manual_version.py | Version-stamp authority for this manual's four version locations + TOC renumbering |
005 Operations/Directives/generate_public_manual.md | Directive/SOP |
QWU Backoffice User Manual [PUBLIC].md (same directory as this file) | Generated output |
Redaction Categories
| Category | Count | Examples |
|---|---|---|
| Credentials | 6 | Azure Client/Tenant/Subscription IDs, SSH credential IDs |
| Network | 4 | VM IP addresses (claude-dev, n8n, supporter servers) |
| Service IDs | 15 | Supabase project IDs, n8n workflow IDs, YouTube/Discord channel IDs |
| Personal | 8 | Email addresses, VM usernames, file paths with usernames |
| Personal Names | 15 | BNI members, supporters (public figures like experts are NOT redacted) |
| Regex Patterns | 25+ | Supporter-name families (word-boundary + substring forms), JWT-style API keys (<API_KEY>), Betterstack monitor IDs |
| Supporter-Name Gate | fail-closed | Names sourced from the shared canonical blocklist; ANY match in final output aborts generation (exit non-zero, nothing written). Proven by --self-test poisoned fixtures. |
| Section Exclusions | 4 | Session Log, Document History, and two supporter-operations subsections (→ educational placeholders with reasons) |
Usage
# Generate public manual
python "005 Operations/Execution/generate_public_manual.py"
# Preview without writing (dry run)
python "005 Operations/Execution/generate_public_manual.py" --dry-run
# Check if public version is current
python "005 Operations/Execution/generate_public_manual.py" --verify
# Show redaction statistics
python "005 Operations/Execution/generate_public_manual.py" --stats
# Prove the supporter-name gate fails closed (poisoned-fixture suite)
python "005 Operations/Execution/generate_public_manual.py" --self-test
Safety Layers (Defense-in-Depth)
- Private manual has
dg-publish: false(cannot be published to Digital Garden) - Private manual has visible
[!WARNING]callout at the top - Public manual is the ONLY version with
dg-publish: true - Script includes built-in leak detection (scans output for IP patterns, UUIDs, JWT prefixes, known sensitive strings)
- Supporter-name HARD-RULE gate (v2.0.0) fails CLOSED: names come from the canonical shared blocklist; a missing blocklist or any name match in final output aborts generation and writes nothing. The atomic temp-then-rename write means an abort can never strand a partial file for the transparency sync to ship.
- Session wrap-up Step 3B regenerates public version after every private manual update
- Redaction config JSON is version-controlled and auditable
[PUBLIC]suffix in filename makes the distinction visible in any file listing
Adding New Sensitive Data
When new sensitive data is added to the private manual (new IPs, credentials, project IDs, personal names):
- Add entries to
005 Operations/Data/public_manual_redactions.json - Re-run
generate_public_manual.py - Review the leak validation output for any remaining warnings
- If false positives appear, add to the
known_false_positiveslist in the config
Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| JSON config-driven redaction pipeline | Configuration management, data classification, JSON schema design | ⭐⭐ |
| Regex pattern matching for sensitive data | Regular expressions, security awareness, PII identification | ⭐⭐ |
| SHA256 integrity verification | Cryptographic hashing, data integrity, change detection | ⭐ |
| Defense-in-depth safety design | Security architecture, multiple validation layers, fail-safe design | ⭐⭐⭐ |
| Leak detection scanning | Security auditing, pattern recognition, automated compliance | ⭐⭐ |
Living Roadmap Delivery Pipeline ⭐ NEW
Added: July 22, 2026
The Living Roadmap gives each project a native, backward-looking roadmap page in HQ (hq.quietlyworking.org/roadmap/<project>) and, when TIG chooses to share one, an exact-parity public export for supporters at portal.quietlyworking.org/roadmap/<project>. The delivery pipeline moves progress from a dev session's draft to a blessed public page through four stations: drafter → release gate → parity export → bridge. Backward-looking only ... the monday.com VOSPA Gantt owns forward projections (different instrument, same journey). Tenant #1: recall-organ (QWU-internal). Tenant #2: [Supporter Organization]-qqt (2026-07-24) ... the first SUPPORTER-FACING tenant, which forced the audience-aware tenancy model below.
Cadence, Coverage, and Auto-Draft (2026-07-29)
The pipeline above moves a drafted entry to a published page. It says nothing about whether an entry ever gets written, and that turned out to be the real failure mode: 3 of 6 roadmaps had never released a single entry, and one session's work was recorded only because TIG asked whether the roadmap had been updated.
The purpose, corrected. The Living Roadmap's PRIMARY audience is TIG's own whole-universe view ... 100k-ft to ground floor, the graph vantage point, every project's history in one place. Supporter-facing was the secondary discovery. That inverts what coverage means: the question is not "does an outsider need this?" but "is this project on the map?"
Four mechanisms, each aimed at a different way silence happens:
| Mechanism | Answers | Where |
|---|---|---|
watch_paths (jsonb on hq_roadmaps) | which tenant does a changed file belong to | longest-prefix-wins; replaced folder inference |
audience (internal / external) | does silence here hurt anyone | governs ALARM behaviour only, never existence |
roadmap_posture (on hq_projects) | is this project on the map at all | has-roadmap / no-audience / deferred; NULL = UNDECLARED |
| auto-draft on the wrap-up seam | did anyone actually write an entry | summarize_session.py v1.12.0 |
watch_paths exists because inference could not work. QQT's work lives in 002 Projects/_Quietly Quoting/ while its tenant is [Supporter Organization]-qqt under 002 Projects/_[Supporter Organization] Projects/[Supporter Organization] QQT Living Roadmap/. Any folder-based guess is structurally blind to that, so the mapping is declared data. Longest-prefix-wins lets a nested project claim its own files instead of double-drafting into the folder containing it.
audience splits the alarm, not the artifact. An external tenant's silence reads to a supporter as "have they forgotten me?" and earns an interruption. An internal tenant's quiet just means that project is not being worked, which costs nobody anything. The governing principle: passive surfaces show everything; active interruptions are reserved for relationship damage. The Projects-board badge lights up for every tenant including internal ones; Discord only ever fires for external.
check_roadmap_cadence.py (daily 8 AM Pacific, safe_run.sh lock check-roadmap-cadence, read-only, exit 1 when flagged) separates WAITING (written but never released ... invisible to the reader, so a stuck review queue looks identical to no work), QUIET (nothing released in N days), and NEVER (never shown a single entry ... a cold start never self-heals). It is the only mechanism here that is not session-triggered, so it fires when no session happens ... which is exactly when someone is being starved.
Coverage reports, it does not nag. All 67 projects carry a declared posture (63 has-roadmap, 4 deferred, 0 no-audience; TIG ruled personal projects ON the map). The pre-existing backlog is reported every run but never pinged, because dozens of daily lines about a known backlog is how a gate teaches its reader to ignore it. Only a project created after the posture epoch with no posture alarms. propose_roadmap_postures.py proposes all of them at once and applies only a file TIG has ratified.
Auto-draft is session-scoped, and that is load-bearing. resolve_roadmap_projects.py reads the files THIS session touched from the QCM per-session event log, never repo-wide git. On a shared checkout with many concurrent agents, "commits since midnight" is largely other people's work ... resolving from git returned l4g (another session's in-flight work) while the session had actually touched [Supporter Organization]-qqt. The session id must be given (session_data["session_id"], or CLAUDE_CODE_SESSION_ID from the environment); absent it, nothing drafts. Entries land as DRAFTS, invisible until TIG releases. Gate off with QWF_DRAFT_ROADMAPS=0.
⚠ Three claims in the paragraph above were wrong when written, and cost two misfiled drafts (2026-07-30). Corrected below; the sentences are kept rather than deleted because the reasoning error is the lesson.
- "Auto-draft is session-scoped" was true of ONE of the TWO drafting paths.
run_session_wrap_up.run_roadmap_draftsselected projects from the ENTIREgit statusdirty tree and was never retired when the session-scoped path shipped. It misfiled a QQT price-book narrative onto the Call Intelligence roadmap.- "a missing draft is caught by the cadence gate" holds for 2 of 8 roadmaps.
check_roadmap_cadence.classify()routes every non-external roadmap tointernaland returns early, andWAITINGinverts on this failure (it keys on drafts existing and aging, so when drafting stops the roadmap reads healthier).- "a mis-attributed one is a plausible lie no gate catches" was the correct diagnosis and the wrong conclusion. It IS catchable, and now is.
There was also a fourth, unstated fault: the session-scoped query had no time window. Agent sessions here persist for days, so "this session's files" silently meant "every file this session ever touched." That is how a 2026-07-28 file attributed a 2026-07-29 narrative onto the supporter-facing
[Supporter Organization]-qqtpage.
The attribution gate (2026-07-30) ... at the chokepoint, not in the callers
draft_roadmap_history.attribution_gate (v1.2.0) refuses a draft the calling session cannot evidence. It lives inside draft() ... the only function that writes an hq_roadmap_history row ... rather than in any caller, because two callers of that module drifted apart for six days and both misfiled. A gate in either would not have caught the other, nor a future third.
| Refusal | Fires when |
|---|---|
NO-SESSION | no usable session id. Placeholders "unknown" / "wrap-up" count as absent ... they were the literal values both callers wrote into technical_overlay.source_session. |
NOT-EVIDENCED | the session touched no file this project watches on occurred_on. The refusal names where the evidence did point. |
MULTI-MATCH | the session touched files for more than one roadmap-bearing project. One narrative is composed from the WHOLE session and handed identically to every matched tenant, so a two-project session cannot help but narrate A onto B. TIG-ratified: refuse and alert rather than write. |
INTENT-MISMATCH | a declared project disagrees with the file evidence. |
The gate runs before the FLAGSHIP voice pass, so a refusal costs no spend. Backfills are legitimate and use --allow-unevidenced --reason "<why>", which stamps technical_overlay.attribution_gate_bypassed onto the row ... 26 existing rows are backfill and were previously indistinguishable from drafter output, which cost a whole forensic leg.
Supporting changes: the whole-tree path is now opt-in (run_session_wrap_up v1.5.0, --draft) rather than deleted, because the session-scoped replacement was never armed ... it read CLAUDE_SESSION_ID / SESSION_ID while the environment supplies CLAUDE_CODE_SESSION_ID, so it returned [] on every real invocation and had drafted exactly once, for its own author. Deleting the old path first would have taken drafting to zero silently. resolve_session_id() is now THE single place the env chain lives; the gate deliberately does not resolve an id of its own, because a fallback there would let a backfill run from inside an agent session inherit that identity and bypass the audit stamp.
Two rails worth carrying past this feature:
- Prove both directions. A gate hardwired to refuse passes every refusal test. The first version of this gate shipped with proofs of all four refusals and zero allow-path checks.
tests/test_roadmap_attribution_gate.py(17 assertions) now covers both, including replays of both real misfiles and bothaudience=externalroadmaps ...recall-organbakes to the same supporter portal as[Supporter Organization]-qqtand had been absent from every earlier scope. - Verification depth tracks blast radius. The investigation swept 10 internal drafts in full and gave the 2 supporter-facing drafts a metadata-only glance. The row never opened was the second misfile.
The overclaim rail. The generator writes optimistically. On the 2026-07-28 [Supporter Organization] entry it produced "Now when a homeowner gets a contract, it looks like it came from [Supporter Organization]" when the domain was registered but not yet activated by the vendor. Re-read every generated entry for overclaim before it reaches TIG ... an overclaim on a roadmap is worse than no entry, because the whole point of the artifact is that a gap cannot go silent.
Audience-Aware Tenancy (2026-07-24)
A supporter-facing tenant page never redacts that supporter's OWN names ... it is their story, told to them (TIG's audience ruling, 2026-07-23). The mechanics:
- Blocklist
roadmap_scrub_blocklist.jsonv1.2.0 gainstenant_self_names(e.g.[Supporter Organization]→ 18 names incl. the supporter's vendor [Supporter CRM] per TIG's ruling). Names STAY insupporter_names... every other surface keeps redacting them. - Scrub
scrub_roadmap_content.pyv1.2.0:scan(text, profile, audience=None)+redact(text, audience=None). The exemption applies to thesupporterclass ONLY;credential+youth_piiare structurally audience-blind (they stop the bake for everyone); infra never exempts; an unknown audience raises (typos fail loud). Supporter matching is substring for names >4 chars (underscore-joined identifiers defeat\b), word-boundary for acronyms. - Audience resolution is registration-driven:
resolve_audience(project)returns a tenant key when the project'sproject_codeorparent_codematches atenant_self_nameskey, elseNone(categorical redaction, tenant #1 unchanged). A new tenant costs a blocklist entry + a normal project registration ... zero code. - The whole pipeline threads it: renderer bake + gates, the drafter's narrative scan (a tenant's wrap-up drafts never fail-close on the tenant's own name), and the hourly bridge's re-bake judgment.
- Edge armor is understood, not fought: Cloudflare Email Obfuscation rewrites email addresses in DEPLOYED bytes (kept ON ... it shields the supporter's address from scrapers); the bridge compares through
_normalize_edge()so content, not armor, is judged. - Verification standard: raw form-generic both-directions sweeps on LIVE bytes ... the tenant's names present-and-unredacted AND other-supporter names / phone shapes / IPs / infra hostnames / github.com / vault paths all zero.
- Multi-tenant work items:
hq_work_itemshas a GLOBAL unique on(source, source_ref)... tenant #2+ roadmap refs are namespaced ([Supporter Organization]-qqt:M0).
Architecture
LIVING ROADMAP DELIVERY PIPELINE
════════════════════════════════
Dev session finishes roadmap-worthy work
│
▼
Drafter ... hq_roadmap_history row lands as status=draft
│ (only TIG's session can read drafts; the page renders released-only)
▼
TIG's release gate ... Drafts panel on the HQ /roadmap page
│ Release = ONE atomic RPC (release_roadmap_history_entry):
│ the draft→released flip and the audit row commit or fail TOGETHER
▼
Released history ... immutable via DB trigger (born-protected)
│
▼
PARITY export bake ... render_supporter_roadmap_export.py
│ fetch → deep-redact → node SSR of the REAL RoadmapView
│ → 9 fail-closed gates on final bytes → emit
▼
TIG's blessing gate ... sha256 frozen the moment TIG says BLESSED
│
▼
QPL ship ... static/roadmap/<project>.html, far-end verified
│ (HTTP 200 + deployed sha == blessed sha + scrub re-scan)
▼
Hourly staleness bridge ... cron :20, ping-only until trust is earned
The One-Render Model
The public page is the HQ component. The SSR harness (scripts/roadmap-export/ in the hq-command-center repo) bundles the same RoadmapView.tsx + roadmap.css the HQ page runs and server-renders them with the released data ... parity by construction, not by imitation. A structural parity check (parity-check.mjs, jsdom) proves the bake equals the same component's client render. The bake is a pure function of (released content + vocabulary register + HQ source): baking twice yields byte-identical output, which is what lets the bridge byte-compare honestly.
Interactive surfaces adapt rather than lie: the WaitingOn panel ships as a read-only stub (TIG's edit controls removed at the bundle layer; HQ source untouched), and the story-mode toggle ships functional via a small inline script ... a dead button on a public page is a small lie.
The Redaction Family (Reason-Named, Visible)
Redaction is data-level (before render), curated, and visible by design ... the declassified-document model: supporters see the true document with honest marks, not a quietly different document.
| Marker | Covers | Why this reason-name |
|---|---|---|
[REDACTED FOR SECURITY] | Infrastructure words (paths, hosts, systems) | Protecting QWF's operational surface |
[REDACTED FOR PRIVACY] | Supporter / partner names | Their privacy, not our threat (Sacred Guesthood) |
The Key card on the page teaches both marks. Marker/legend drift cannot ship: the orchestrator gate checks page markers against REDACTION_MARKERS while the validator independently checks the Key's registered rows ... a marker present in one but not the other fails one of the two gates.
Youth protection has NO marker, deliberately. A [REDACTED] mark would advertise existence. Youth content fail-closes ... it never enters the document at all, and the colophon states that promise plainly.
Fail-Closed Carve-Outs (Never Redaction Candidates)
- Credentials and youth-PII hard-block the bake. They are upstream wrongs to fix at the source, never things to mask with a marker. The scrub self-test proves
redact()passes such text through UNCHANGED soscan()still catches it. technical_overlayis physically absent ... no select list anywhere includes the column, so its bytes cannot reach the public page.- CSS routes through esbuild's parser (comments structurally removed) because a stylesheet header comment once tried to ship a vault path on the very first bake ... the
vault_pathscan class remains as the export-only backstop.
The Blessing Gate
No commit, no push, no public-page replacement until TIG says BLESSED while looking at the actual baked file. At blessing the sha256 is frozen; the ship then proves itself at the far end: HTTP 200, deployed sha == blessed sha, and a scrub re-scan of the deployed bytes. The blessed sha is recorded durably (System Status + directive changelog) ... the .tmp blessed file is evidence, not the record.
Bridge Semantics
check_roadmap_export_staleness.py runs hourly (cron 20 * * * * via safe_run.sh). It re-bakes from current released content and byte-compares against the deployed page:
ROADMAP_REBAKE_AUTOPUSHabsent or0(current state): detector only ... a stale page sends a Discord ping and waits. TIG refreshes attended with--now.ROADMAP_REBAKE_AUTOPUSH=1: the bridge may push a fresh bake autonomously. TIG flips this by observation of the ping-only behavior, never by default.- A "stale" ping during a renderer rebuild is correct behavior (the bake changed on purpose); it clears at the post-blessing ship.
- Coupling contract: the bridge imports four exact symbols from the renderer (
build_html,fetch,run_gates,VOCAB_PATH). Any renderer rewrite preserves those names or amends the bridge import in the same atomic commit ... otherwise the hourly cron crash-pings.
Vocabulary Lock
All roadmap surfaces speak the Council-ratified Project-Progress Vocabulary (Stage / Foundation / Track / Milestone / Deliverable) with the masthead slot convention ({{PROGRAM}} · {{FAMILY}} · {{TAGLINE-CAPS}}). Canonical list: 005 Operations/Data/roadmap_vocabulary.json; shared validator: validate_roadmap_vocabulary.py; human-readable mirror + full register map: qwu_naming_conventions.md §Project-Progress Vocabulary.
Key Files
| File | Purpose |
|---|---|
005 Operations/Execution/render_supporter_roadmap_export.py | Export orchestrator v2.1.0 (fetch → resolve_audience → redact → SSR → gates → emit) |
005 Operations/Execution/scrub_roadmap_content.py | ONE scrubbing truth v1.2.0: audience-aware scan() + redact() + carve-outs (self-test 48/48) |
005 Operations/Execution/validate_roadmap_vocabulary.py | Vocabulary validator v1.2.0 (8 checks incl. marker wellformedness, overlay-bytes-absent) |
005 Operations/Execution/check_roadmap_export_staleness.py | Hourly staleness bridge v1.1.0 (edge-normalized compare + ping / gated autopush) |
005 Operations/Execution/draft_roadmap_history.py | Drafter v1.1.0 (audience-aware narrative scan; wrap-up auto-draft) |
005 Operations/Data/roadmap_scrub_blocklist.json | Shared pattern data v1.2.0 (supporter_names + tenant_self_names; two enforcers, one list) |
005 Operations/Data/roadmap_vocabulary.json | Canonical vocabulary + supporter register + Key entries v1.4.0 |
hq-command-center: scripts/roadmap-export/ | SSR harness (entry, esbuild runner, WaitingOn stub, parity check) |
hq-command-center: src/components/roadmap/RoadmapView.tsx | The single render truth (HQ page AND public export) |
quietly-portal: static/roadmap/recall-organ.html | Tenant #1 blessed artifact (refreshed 2026-07-24 with the template upgrades) |
quietly-portal: static/roadmap/[Supporter Organization]-qqt.html | Tenant #2 blessed artifact (sha c2d3c68b...178d27, 2026-07-24; live bytes differ only by CF email armor) |
002 Projects/_HQ Command Center/HQ-System-Status.md §Living Roadmap | System Status source of truth |
002 Projects/_[Supporter Organization] Projects/[Supporter Organization] QQT Living Roadmap/ | Tenant #2 project (design doc, System Status) |
🎓 Missing Pixel Training Opportunities
| Component | Skills Developed | Difficulty |
|---|---|---|
| Fail-closed scanner design with poisoned fixtures | Security engineering, adversarial test design, fail-safe defaults | ⭐⭐⭐ |
| Byte-provable deploys (sha-diff blessing + far-end verification) | Cryptographic hashing, deploy verification, trust-but-prove discipline | ⭐⭐ |
| Born-protected audit tables (immutability triggers from day one) | SQL triggers, data integrity, designing for permanence | ⭐⭐⭐ |
| Atomic RPC releases (two writes commit or fail together) | Transactions, failure-mode analysis, why atomicity matters | ⭐⭐⭐ |
| CF Pages routing forensics from raw CI logs | Debugging discipline, hosting internals, reading ground truth over docs | ⭐⭐⭐ |
| Declassified-document parity model + reason-named redaction family | Information design, security communication, honesty as architecture | ⭐⭐ |
| Audience-aware redaction (tenant self-name exemption, structural carve-outs) | Access-control modeling, privacy engineering, fail-loud config validation | ⭐⭐⭐ |
| Realtime publication debugging (silent postgres_changes no-op) | Supabase realtime internals, silent-failure diagnosis, belt-and-suspenders UI design | ⭐⭐⭐ |
CSS grid min-width:auto blowout + edge-transform normalization (CF email armor) | CSS layout internals, CDN edge behavior, comparing content vs armor | ⭐⭐ |
| Gate placement: the chokepoint vs. every caller (2026-07-30) | Architectural reasoning, invariants, why guarding N callers loses to guarding the one write | ⭐⭐⭐ |
| Prove-both-directions test design (a gate hardwired to REFUSE passes every refusal test) | Test design, falsifiability, distinguishing "safe" from "broken" | ⭐⭐ |
Forensic discriminators and their limits (source_session as a fingerprint) | Incident forensics, evidence reasoning, checking whether a signal predates the code that writes it | ⭐⭐⭐ |
| Blind multi-reviewer fan-out (decorrelated errors; reviewers found what the builder had cleared) | Review architecture, cognitive bias, why one deep look shares its own blind spot | ⭐⭐⭐ |
| Trace-the-consumer debugging (a correct function nobody supplies an input to) | Debugging discipline, dead-code detection, docstrings are not evidence of wiring | ⭐⭐ |
Advisor Brain Vocabulary ⭐ NEW
Source of truth: 002 Projects/_QWF Advisor Brain/BRAIN-Glossary.md (24 terms, each with what it
is, what it is not, and where it lives). This section is the pointer plus the collisions ...
never a second copy that can drift.
Why it exists: "Brain", "corpus", "domain" and "surface" were being used interchangeably, which is how a wrong routing decision gets made at 1am.
The four organs (each stands on its own)
| Organ | Role | Registered as |
|---|---|---|
| Brain | Memory ... decisions + the WHY. The only organ that should eventually ACT. | brain |
| Council | Adversarial deliberation. An ADVISOR, convened per question. | council |
| STORM | Citation-verified research. An ADVISOR, called by TIG and the Council. | storm-research-organ |
| Recall Organ | Read-only recall over operational exhaust. Sibling project. | recall-organ |
The five collisions to avoid
- "recall" ... verb (finding precedent) vs proper noun (the Recall Organ project). Use "retrieval" for the act. Reserve "Recall Organ", capitalized and in full, for the project.
- "domain" ... RETIRED. Use
category(the engine-enforced controlled vocabulary). - "corpus" ... does NOT mean the Ledger. The Ledger (
decisions.v1) is one of eleven corpuses. - "card" vs "entry" ... a Card is the surface where a decision awaits TIG's verdict
(
hq_brain_verdicts); an Entry is the record in the Ledger. Different objects. - "trust" vs "calibration" ... trust meter = approval rate; calibration meter = prediction agreement. The §14 autonomy gate reads calibration. Conflating them opens a gate early.
Terms that are easy to get backwards
| Term | Means | Does NOT mean |
|---|---|---|
| Precedent | a ratified Entry used as input to a later decision (a role, not an object) | any Entry ... a pending Entry is not precedent |
| Principle | a distilled standing rule in the memory corpus ... compiled precedent | a single ruling |
| Retrieval | the act of finding relevant precedent | the Recall Organ |
| Ranker | the function that ORDERS precedent (authority x recency) | a predictor ... it orders, it does not answer |
| Corroboration | the same choice made repeatedly for the same thing ... weight UP | not approved-with-reinforcement, which is a verdict state |
| Contradiction | rulings inside one topic cluster that disagree ... fires the alarm | a duplicate |
Naming rulings (TIG, 2026-07-26)
- "retrieval" replaces "recall" for the act. Ratified.
- "corroboration weight" ... NOT "concurrency weighting" (concurrency already means simultaneous execution everywhere in the stack) and NOT "reinforcement" (already a verdict state; reusing it would plant the same double-meaning bug that caused the calibration-meter defect).
distinguishbecomes a first-class Entry kind: when two rulings both stand because the situations differ, the retriever surfaces the disambiguating condition alongside whichever ruling it qualifies. Structural, not hoped-for.
Measurement (measure_corpus_health.py, read-only, no spend)
Three axes the ranker deliberately does not measure:
- Coverage ... "if a decision arrives tomorrow, is there precedent to answer it?" Low-coverage categories are where the Brain over-reaches, answering from analogy with nothing to stop it.
- Freshness distribution ... the age histogram per category, not the average. Bimodal is the signal: old cluster + new cluster with a thin middle usually means an un-superseded precedent break already sitting in the corpus.
- Contradiction density ... fraction of topic clusters holding rulings that answer the same question differently. The accumulated total of what the alarm catches one at a time.
Call Intelligence Pipeline ⭐ NEW
Turns a recorded phone call or in-person appointment into a structured, reviewable log. Built 2026-07-29/30 because TIG became medical advocate for a family member and needed a durable record of every care conversation. Full anti-compaction detail: 002 Projects/_Call Intelligence/Call Intelligence-System-Status.md.
New persistent VM service: Syncthing v2.1.2, installed rootless to ~/.local/bin (no passwordless sudo on this box; the static binary is also newer than Ubuntu 24.04's 1.27.2). Runs as an enabled systemd --user service (~/.config/systemd/user/syncthing.service) with lingering already on, so it survives reboot. GUI bound to 127.0.0.1:8384 only. Two receive-only folders with trashcan versioning pull from the phone.
| Path | Purpose |
|---|---|
~/call_intel/inbox/{call,voice,drive_exports} | Landing zones ... transient |
~/call_intel/archive/<source>/<YYYY>/<MM>/ | Permanent archive, OUTSIDE the git tree |
~/call_intel/config/ | Syncthing config + keys |
Deliberately outside the vault: a 96 MB recording has no business near a repo the auto-committer sweeps with git add -A.
Pipeline ... cron call-intel-ingest, every 15 min Pacific, wrapped in safe_run.sh:
poll_drive_call_exports.py --apply # dialer exports from Drive
&& ingest_call_media.py --apply # COPY landing zone -> archive, register hq_call_media
&& process_call_media.py --apply --discord # -> hq_calls draft + failure alerts
Log: .tmp/logs/call_intel_ingest.log. All three are dry-run by default and idempotent.
| Script | Version | Role |
|---|---|---|
normalize_call_recording.py | v1.2.0 | Filename grammars, ffprobe, UTF-16-BE sidecar, ffmpeg remux |
analyze_call_recording.py | v1.2.0 | Whisper + FLAGSHIP fusion, uncertainty flagging |
ingest_call_media.py | v1.0.0 | Landing zone to archive (COPY) |
poll_drive_call_exports.py | v1.0.0 | Drive export poller (md5-idempotent) |
process_call_media.py | v1.2.0 | Archive to hq_calls draft; --check / --retry-failed |
voice_transcriber.py | v1.2.0 | Opt-in with_segments; get_file_duration now uses ffprobe |
Two gotchas that cost real time:
- Whisper rejects the raw recordings. The phone writes AAC into a container branded
3gp4while naming the file.m4a. The OpenAI API sniffs the brand, not the extension, and returns "Invalid file format" even though m4a is supported. An ffmpeg remux is the fix. - Ingest COPIES, never moves. The landing zones are Syncthing receive-only folders, which treat a local deletion as divergence ... the folder reports out-of-sync forever and "revert local changes" would re-download whatever the archive took. Verified by asserting
receiveOnlyChangedFiles == 0after archiving.
Historical cost discontinuity: voice_transcriber.get_file_duration() previously estimated duration from FILE SIZE at an assumed 64 kbps ... measured 36% low. Audio cost_usd figures recorded before 2026-07-30 are systematically understated. Do not read pre/post as a trend; the step change is the fix. Relevant to llm_usage.jsonl history and audit_system.py drift checks.
Document History
The private version of this manual maintains a ~270-row version-history table. It is excluded from the public version because its rows weave supporter names, private individuals, and infrastructure identifiers through narrative context too densely for line-level redaction to leave readable, safely-anonymous text.
For students: The history table demonstrates real versioned documentation discipline ... every change dated, versioned, and reasoned. Ask your mentor about reviewing it during supervised learning.
Last updated: 2026-11-05 07:52 (v7.02)