What this PR is

The final act of the zo-k8s slim-down (#22, #24, #25). Every workload the cluster ever hosted now lives on Vercel β€” glassbox, the 2ls.tech site, the static drop-and-share site, and finally everwhen prod (#26). Before anything was destroyed, all four hosts were checked live and answered with server: Vercel. The cluster was serving nothing.

The live half is already done: terraform apply destroyed the DOKS cluster and the ynab.mseeks.me record (the ingress load balancer's advertised hostname β€” pure LB plumbing). The cluster's destroy_all_associated_resources = true flag reaps the LB and node droplets with it. Afterward doctl shows no clusters or volumes and terraform plan reports no changes. That ends roughly $108/mo of spend (two nodes plus the LB).

This diff is the repo half: the cluster and LB Terraform files, the last infra/k8s/ stacks (ingress-nginx, cert-manager, the shared preflight lib), and every Kubernetes-flavored comment and instruction, removed.

dns.tf β€” records stay, the cluster subtext goes

One resource is gone: mseeks_me_ynab, whose only job was to give the ingress LB a stable hostname (originally for a long-retired app, kept because cert-manager's HTTP-01 self-checks routed through it). It referenced data.digitalocean_loadbalancer.ingress, so it had to die with the LB.

Every other record survives untouched in value. What changed is the commentary: the Vercel records no longer explain themselves by contrast ("hosted on Vercel, not the cluster", "no cert-manager", "unlike the LB IP this used to track"). They now just say what is.

The header without its LB aside, and the mseeks.me zone flowing from the mail records straight into the Vercel CNAMEs where the ynab record used to sit.

infra/dns.tf Β· 338 lines
infra/dns.tf338 lines Β· Terraform
1# DNS β€” reverse-engineered from the live DigitalOcean zones and imported into
2# state (target: `terraform plan` reports no changes).
3#
4# SOA and the default DigitalOcean NS records (ns1/2/3.digitalocean.com) are
5# managed automatically by DO and are intentionally NOT declared here.
6#
7# Every zone here is an iCloud Custom Email Domain (Apple) setup: MX -> icloud
8# mail, an SPF TXT, an apple-domain verification TXT, and a DKIM CNAME.
9#
10# SPF quoting gotcha: write SPF TXT values WITHOUT surrounding quotes (e.g.
11# `v=spf1 include:icloud.com ~all`). DigitalOcean adds the DNS wire quotes on
12# its own, so escaping quotes into the value makes `terraform apply` CREATE a
13# doubly-quoted record (`""v=spf1 ...""`) that SPF verifiers reject. `plan`
14# won't warn you: the provider compares TXT values quote-insensitively, so even
15# a broken live record shows no drift. The mseeks.me / msull92.com SPF entries
16# still carry escaped quotes but resolve clean (created pre-terraform); left
17# as-is because recreating them from the quoted value would re-break them.
18# playeverwhen.com and 2ls.tech below use the correct unquoted form.
β‹― 41 lines hidden (lines 19–59)
19 
20# ============================== mseeks.me ==============================
21 
22resource "digitalocean_domain" "mseeks_me" {
23 name = "mseeks.me"
25 
26resource "digitalocean_record" "mseeks_me_mx_01" {
27 domain = digitalocean_domain.mseeks_me.name
28 type = "MX"
29 name = "@"
30 value = "mx01.mail.icloud.com."
31 priority = 10
32 ttl = 14400
34 
35resource "digitalocean_record" "mseeks_me_mx_02" {
36 domain = digitalocean_domain.mseeks_me.name
37 type = "MX"
38 name = "@"
39 value = "mx02.mail.icloud.com."
40 priority = 10
41 ttl = 14400
43 
44resource "digitalocean_record" "mseeks_me_apple_domain" {
45 domain = digitalocean_domain.mseeks_me.name
46 type = "TXT"
47 name = "@"
48 value = "apple-domain=Y94Tzo0dJVhRhQxf"
49 ttl = 3600
51 
52resource "digitalocean_record" "mseeks_me_spf" {
53 domain = digitalocean_domain.mseeks_me.name
54 type = "TXT"
55 name = "@"
56 value = "\"v=spf1 include:icloud.com ~all\""
57 ttl = 3600
59 
60resource "digitalocean_record" "mseeks_me_dkim" {
61 domain = digitalocean_domain.mseeks_me.name
62 type = "CNAME"
63 name = "sig1._domainkey"
64 value = "sig1.dkim.mseeks.me.at.icloudmailadmin.com."
65 ttl = 43200
67 
68# glassbox.mseeks.me -> Vercel. Glassbox is hosted on Vercel: the project +
69# custom domain are managed in the Vercel dashboard and deploys ride its
70# GitHub integration (push to main in mseeks/glassbox), so the only infra
71# Terraform owns is this record. Vercel terminates TLS at its edge. The value
72# is this project's own CNAME target, the
73# one the dashboard issued when glassbox.mseeks.me was added to the project
74# (Vercel mints per-project targets; the old universal cname.vercel-dns.com is
75# legacy). If the Vercel project is ever deleted and recreated, it gets a new
76# target β€” read the fresh value off the project's Domains tab, and only
77# re-point this record once the domain is attached there (flipped early, the
78# host serves Vercel's DEPLOYMENT_NOT_FOUND page).
79resource "digitalocean_record" "mseeks_me_glassbox" {
80 domain = digitalocean_domain.mseeks_me.name
β‹― 258 lines hidden (lines 81–338)
81 type = "CNAME"
82 name = "glassbox"
83 value = "e24be5a7fd9d016f.vercel-dns-017.com."
84 # The target is stable for the life of the Vercel project, so no short-TTL
85 # churn guard is needed.
86 ttl = 3600
88 
89# static.mseeks.me -> Vercel. The `static` drop-and-share site is hosted on
90# Vercel: the project + domain are managed in the Vercel dashboard and
91# deploys ride its GitHub integration β€” a push to main in mseeks/static
92# deploys, which is also how read-thru guides publish (see CLAUDE.md).
93# Vercel terminates TLS at its edge. The value
94# is this project's own CNAME target, issued by the dashboard when the domain
95# was added (per-project targets; the old universal cname.vercel-dns.com is
96# legacy). If the Vercel project is ever deleted and recreated it gets a new
97# target β€” read it off the project's Domains tab, and only re-point this
98# record once the domain is attached there.
99resource "digitalocean_record" "mseeks_me_static" {
100 domain = digitalocean_domain.mseeks_me.name
101 type = "CNAME"
102 name = "static"
103 value = "a4cfcb8c32267377.vercel-dns-017.com."
104 # Stable Vercel target, so no short-TTL churn guard is needed.
105 ttl = 3600
107 
108# ============================== msull92.com ==============================
109 
110resource "digitalocean_domain" "msull92_com" {
111 name = "msull92.com"
113 
114resource "digitalocean_record" "msull92_com_mx_01" {
115 domain = digitalocean_domain.msull92_com.name
116 type = "MX"
117 name = "@"
118 value = "mx01.mail.icloud.com."
119 priority = 10
120 ttl = 14400
122 
123resource "digitalocean_record" "msull92_com_mx_02" {
124 domain = digitalocean_domain.msull92_com.name
125 type = "MX"
126 name = "@"
127 value = "mx02.mail.icloud.com."
128 priority = 10
129 ttl = 14400
131 
132resource "digitalocean_record" "msull92_com_apple_domain" {
133 domain = digitalocean_domain.msull92_com.name
134 type = "TXT"
135 name = "@"
136 value = "apple-domain=SLKQpEYsouHgb3mI"
137 ttl = 3600
139 
140resource "digitalocean_record" "msull92_com_spf" {
141 domain = digitalocean_domain.msull92_com.name
142 type = "TXT"
143 name = "@"
144 value = "\"v=spf1 include:icloud.com ~all\""
145 ttl = 3600
147 
148resource "digitalocean_record" "msull92_com_dkim" {
149 domain = digitalocean_domain.msull92_com.name
150 type = "CNAME"
151 name = "sig1._domainkey"
152 value = "sig1.dkim.msull92.com.at.icloudmailadmin.com."
153 ttl = 43200
155 
156# ============================== 2ls.tech ==============================
157# HCL resource names can't start with a digit, so these use a `_2ls_tech`
158# prefix (a leading underscore is valid) rather than the plain `2ls_tech`
159# the mseeks_me/msull92_com convention would suggest.
160 
161resource "digitalocean_domain" "_2ls_tech" {
162 name = "2ls.tech"
164 
165resource "digitalocean_record" "_2ls_tech_mx_01" {
166 domain = digitalocean_domain._2ls_tech.name
167 type = "MX"
168 name = "@"
169 value = "mx01.mail.icloud.com."
170 priority = 10
171 ttl = 14400
173 
174resource "digitalocean_record" "_2ls_tech_mx_02" {
175 domain = digitalocean_domain._2ls_tech.name
176 type = "MX"
177 name = "@"
178 value = "mx02.mail.icloud.com."
179 priority = 10
180 ttl = 14400
182 
183resource "digitalocean_record" "_2ls_tech_apple_domain" {
184 domain = digitalocean_domain._2ls_tech.name
185 type = "TXT"
186 name = "@"
187 value = "apple-domain=KAYDndJUw5TrEwCe"
188 ttl = 3600
190 
191# Google Search Console domain verification (TXT at the apex). Value UNQUOTED
192# like the apple-domain record above β€” DigitalOcean adds the DNS wire quotes
193# itself (see the SPF quoting gotcha at the top of this file).
194resource "digitalocean_record" "_2ls_tech_google_site_verification" {
195 domain = digitalocean_domain._2ls_tech.name
196 type = "TXT"
197 name = "@"
198 value = "google-site-verification=9e1Dm56-fdkP2jTv1pGNI5II2ol0pR6Xtw8RtS1gXwM"
199 ttl = 3600
201 
202# SPF value UNQUOTED on purpose β€” see the SPF quoting gotcha at the top of this
203# file. Recreated once (terraform apply -replace) to strip the doubly-quoted
204# `""v=spf1 ...""` the original escaped-quote value produced on create.
205resource "digitalocean_record" "_2ls_tech_spf" {
206 domain = digitalocean_domain._2ls_tech.name
207 type = "TXT"
208 name = "@"
209 value = "v=spf1 include:icloud.com ~all"
210 ttl = 3600
212 
213resource "digitalocean_record" "_2ls_tech_dkim" {
214 domain = digitalocean_domain._2ls_tech.name
215 type = "CNAME"
216 name = "sig1._domainkey"
217 value = "sig1.dkim.2ls.tech.at.icloudmailadmin.com."
218 ttl = 43200
220 
221# 2ls.tech (+ www) -> Vercel. The Two Ls company site is hosted on Vercel:
222# the project + both domains are managed in the Vercel dashboard and deploys
223# ride its GitHub integration (push to main in mseeks/2ls.tech). Vercel
224# terminates TLS at its edge, serves the apex, and redirects www to it. The
225# mail/verification records above are unaffected.
227# Values: both are this project's own targets, issued by the dashboard when
228# the domains were added (Vercel mints per-project values now; the universal
229# 76.76.21.21 / cname.vercel-dns.com are legacy). If the Vercel project is
230# ever deleted and recreated it gets new targets β€” read them off the
231# project's Domains tab, and only re-point these records once the domains
232# are attached there.
233resource "digitalocean_record" "_2ls_tech_apex" {
234 domain = digitalocean_domain._2ls_tech.name
235 type = "A"
236 name = "@"
237 value = "216.198.79.1"
238 # Stable Vercel target, so no short-TTL churn guard is needed.
239 ttl = 3600
241 
242resource "digitalocean_record" "_2ls_tech_www" {
243 domain = digitalocean_domain._2ls_tech.name
244 type = "CNAME"
245 name = "www"
246 value = "bae588b1e31b34e9.vercel-dns-017.com."
247 ttl = 3600
249 
250# ============================== playeverwhen.com ==============================
251 
252resource "digitalocean_domain" "playeverwhen_com" {
253 name = "playeverwhen.com"
255 
256resource "digitalocean_record" "playeverwhen_com_mx_01" {
257 domain = digitalocean_domain.playeverwhen_com.name
258 type = "MX"
259 name = "@"
260 value = "mx01.mail.icloud.com."
261 priority = 10
262 ttl = 14400
264 
265resource "digitalocean_record" "playeverwhen_com_mx_02" {
266 domain = digitalocean_domain.playeverwhen_com.name
267 type = "MX"
268 name = "@"
269 value = "mx02.mail.icloud.com."
270 priority = 10
271 ttl = 14400
273 
274resource "digitalocean_record" "playeverwhen_com_apple_domain" {
275 domain = digitalocean_domain.playeverwhen_com.name
276 type = "TXT"
277 name = "@"
278 value = "apple-domain=DEESFXkRhJ4wE6Lm"
279 ttl = 3600
281 
282# Google Search Console domain verification (TXT at the apex). Value UNQUOTED
283# like the apple-domain record above β€” DigitalOcean adds the DNS wire quotes
284# itself (see the SPF quoting gotcha at the top of this file).
285resource "digitalocean_record" "playeverwhen_com_google_site_verification" {
286 domain = digitalocean_domain.playeverwhen_com.name
287 type = "TXT"
288 name = "@"
289 value = "google-site-verification=1FSmsnv2MPG_mKoCrdlSABXfyXFTaeVBETNRmLNi85o"
290 ttl = 3600
292 
293# SPF value UNQUOTED on purpose β€” see the SPF quoting gotcha at the top of this
294# file. DigitalOcean adds the DNS wire quotes itself.
295resource "digitalocean_record" "playeverwhen_com_spf" {
296 domain = digitalocean_domain.playeverwhen_com.name
297 type = "TXT"
298 name = "@"
299 value = "v=spf1 include:icloud.com ~all"
300 ttl = 3600
302 
303resource "digitalocean_record" "playeverwhen_com_dkim" {
304 domain = digitalocean_domain.playeverwhen_com.name
305 type = "CNAME"
306 name = "sig1._domainkey"
307 value = "sig1.dkim.playeverwhen.com.at.icloudmailadmin.com."
308 ttl = 43200
310 
311# playeverwhen.com (apex + www) -> Vercel. The Everwhen SSR app is hosted on
312# Vercel: the project + both domains are managed in the Vercel dashboard,
313# deploys ride its GitHub integration (push to main in mseeks/everwhen),
314# server routes run as Vercel Functions, and Vercel terminates TLS at its
315# edge. www redirects to the apex at Vercel's edge. The iCloud MX/TXT records
316# above are unaffected.
318# Values: both are this project's own targets, issued by the dashboard when
319# the domains were added (a CNAME is illegal at a zone apex, so the apex gets
320# a per-project anycast A instead). If the Vercel project is ever deleted and
321# recreated it gets new targets β€” read them off the project's Domains tab,
322# and only re-point these records once the domains are attached there.
323resource "digitalocean_record" "playeverwhen_com_apex" {
324 domain = digitalocean_domain.playeverwhen_com.name
325 type = "A"
326 name = "@"
327 value = "216.198.79.1"
328 # Stable Vercel target, so no short-TTL churn guard is needed.
329 ttl = 3600
331 
332resource "digitalocean_record" "playeverwhen_com_www" {
333 domain = digitalocean_domain.playeverwhen_com.name
334 type = "CNAME"
335 name = "www"
336 value = "739dd9fe1c5c2ea6.vercel-dns-017.com."
337 ttl = 3600

The Terraform surface, after

kubernetes.tf (the DOKS cluster, its version data source, five outputs) and loadbalancer.tf (the read-only adoption of the CCM-owned LB) are deleted whole. What Terraform manages now: DNS zones and records, the Spaces bucket that holds its own state, and the adopted everwhen Supabase project. The README shrinks to match β€” layout, prerequisites (no more kubectl), the DNS import runbook, and remote state. supabase.tf loses a header analogy that compared its project/schema split to the cluster/workload split.

The README's new frame: DNS + state + Supabase, sites on Vercel.

infra/README.md Β· 79 lines
infra/README.md79 lines Β· Markdown
1# infra
2 
3Terraform for personal infrastructure hosted on **DigitalOcean**. This is the
4single source of truth for everything in the DO account β€” currently DNS and
5the Spaces bucket that holds this config's own state (plus the adopted
6Supabase project in `supabase.tf`). The sites and apps themselves are hosted
7on Vercel; the only infra they need here is their DNS records.
8 
9## Layout
10 
11```
12infra/
13β”œβ”€β”€ versions.tf # terraform + provider version pins
14β”œβ”€β”€ providers.tf # digitalocean provider (reads token from env)
15β”œβ”€β”€ backend.tf # remote state: DigitalOcean Spaces (s3 backend)
16β”œβ”€β”€ spaces.tf # the Spaces bucket that holds this config's own state
17β”œβ”€β”€ dns.tf # DNS zones + records
18β”œβ”€β”€ supabase.tf # the adopted everwhen Supabase project
19β”œβ”€β”€ .env.example # token + Spaces-key template (copy to .env, gitignored)
20└── .gitignore
21```
22 
23One root module, split into files by concern. Keep it that way until there's a
24real reason not to β€” no `environments/`, no workspaces, no modules until a
25pattern actually repeats.
26 
β‹― 53 lines hidden (lines 27–79)
27## Prerequisites
28 
29- `terraform` (>= 1.5)
30- `doctl` (handy for inspecting the account)
31- A DigitalOcean Personal Access Token
32 
33## Auth
34 
35All credentials come from `.env` (gitignored) β€” never the repo:
36 
37- `DIGITALOCEAN_ACCESS_TOKEN` β€” DO API token, used by the provider and doctl.
38- A Spaces access-key pair, exposed under two names: `AWS_*` (read by the s3
39 state backend) and `SPACES_*` (read by the provider to manage the bucket).
40 
41```sh
42cp .env.example .env # then paste your token + Spaces keys into .env
43set -a; source .env; set +a
44```
45 
46## Usage
47 
48```sh
49terraform init
50terraform plan
51terraform apply
52```
53 
54## Importing existing DNS
55 
56DNS was originally configured in the DO web UI. To bring it under Terraform
57without recreating anything, we reverse-engineer it and import:
58 
591. Inspect live records: `doctl compute domain records list <domain>`
602. Add `import` blocks (one per zone/record, keyed by the live IDs).
613. Generate matching HCL: `terraform plan -generate-config-out=dns.generated.tf`
624. Review, fold into `dns.tf`, `terraform apply` to write the imports into state.
635. Confirm `terraform plan` reports **no changes** β€” proof the config matches reality.
64 
65## Remote state (DigitalOcean Spaces)
66 
67State lives in a **self-managed** Spaces bucket β€” `zo-tfstate` (nyc3) β€” that this
68same config creates (`spaces.tf`), so Terraform manages the very bucket its state
69lives in. Versioning is on (state rollback) and `prevent_destroy` stops a
70`destroy` from deleting the bucket out from under itself. Backend config is in
71`backend.tf`; day to day, just `terraform init` once, then `plan`/`apply`.
72 
73How it was bootstrapped (one-time, kept for reference / disaster recovery):
74 
751. Created the bucket under **local** state (`terraform apply`, no backend block).
762. Added the `backend "s3"` block and ran `terraform init -migrate-state` to copy
77 local state into the bucket.
78 
79The bucket carries a ~$5/mo Spaces minimum.

CLAUDE.md β€” instructions describe only what exists

The Infrastructure section is rewritten from scratch: Terraform-owned DNS, state, and Supabase; the Vercel hosting model (push to main deploys); the Mac-side Ollama LaunchAgent; and the secrets rule. Gone are the cluster inventory, the headroom/request-budget guidance, and the retirement history paragraph β€” with the cluster gone, guidance about scheduling pods on it is noise a future session should never see. Two smaller mentions elsewhere ("static/k8s deploy" in the plumbing rule, "or k8s" in the read-thru bullet) are trimmed the same way. A full-repo sweep for k8s/kubectl/helm/DOKS/LB terms comes back empty.

The Infrastructure section, describing the post-cluster world only.

CLAUDE.md Β· 85 lines
CLAUDE.md85 lines Β· Markdown
β‹― 78 lines hidden (lines 1–78)
1# Zo
2 
3You are **Zo**, the AI assistant for this personal workspace, home to the user's projects, code, research, and writing.
4 
5## Identity
6 
7You are precise, direct, and low-ceremony, a craftsperson about correctness. You do exactly what's asked and say what you think plainly. Your core domains are engineering, research, and writing.
8 
9## Many Hands Engineering (MHE)
10 
11MHE is the framework underpinning the agentic and code-maintenance work across this workspace. Its canonical source is `projects/many-hands-engineering/many-hands-engineering.typ` (plain-text Typst source; `many-hands-engineering.pdf` is the rendered human version). Before acting on anything MHE-related, **read the source thoroughly and build an intuitive understanding of it first**. Don't pattern-match from fragments or secondhand summaries.
12 
13## Guidelines
14 
15- When the user expresses a lasting preference ("from now on"), update this file to reflect that preference.
16- Do exactly what's asked, and don't expand scope. Surface adjacent improvements as suggestions, not actions. When something's genuinely ambiguous, ask rather than guess; on trivial, reversible choices, pick a sensible default and note it.
17- For substantive work, **propose before acting**; when the user says to just discuss, write no code until told.
18- Bias hard to **KISS**: the simplest design that works, accreted slowly. Strip needless indirection, and prefer promoting or reusing an existing artifact over creating a new one.
19- **Chat style:** terse and neutral. Lead with the answer, give the shortest correct response, cut preamble; expand only when asked. Use rich formatting (headers, lists, tables, code/inline code, terminal-friendly links) to communicate efficiently, with emoji in reasonable moderation as quick visual anchors.
20- **Plain language first:** prefer plain, everyday words and clear explanations over jargon, and explain a necessary technical term the first time it appears. This sits above the other style rules when they pull against each other, but reconcile intelligently: stay terse, never sacrifice technical precision, and keep the exact name when a vaguer plain word would be wrong.
21- Stay neutral and even: surface disagreement or problems directly and plainly, with a brief why, without editorializing or overselling. Say it once, then stay open to well-supported counterpoints.
22- Break complex work into small iterative slices and track it with todo lists. Narrate sparingly: a line at meaningful milestones, not step-by-step.
23- When writing code, match the surrounding codebase: its style, naming, comment density, and test conventions.
24- Keep personal-workspace plumbing (static-site deploy, private-repo references, local paths) out of reusable or public artifacts. It belongs only in zo's root CLAUDE.md. Before publishing, sweep and strip to a generic, self-contained form (everything it needs bundled).
25- Connected tools: read freely, but confirm before any write or send. Query judiciously. Don't over-fetch and pollute context.
26- Reads run free. What needs a confirm is set by blast radius, not formality: **consequential or hard-to-reverse** actions get an explain-and-confirm first (deleting important files, changing global config or the user's installation); routine, in-task changes don't. An action you were explicitly asked to take, like pushing to `main`, counts as approved. If a suggested fix looks wrong for the setup, say so instead of running it.
27- Never commit secrets. Scan diffs before committing, and never print or inspect a secret's value (copy it mechanically: `pbcopy`, or a gitignored `.env` between repos). Reuse credentials already in hand; scrub any key that reaches git history.
28- In personal repos, commit and push to `main` when asked. No PR ceremony for small changes (opening a PR still follows the read-thru flow below).
29- Run terminal commands in short combinations rather than clever mega combos, for easy review and understanding by the user, and better matching against auto-approval rules.
30- Always check if you're working in a git worktree.
31 - If you are not, prompt the user if you should create one before starting work.
32 - If you are, then know that submodules will need to be initialized before working with them.
33 - If you are and you're ready to merge it back in, ask the user first to avoid complex dirty state.
34- On `zo`'s `main`, keep every project's submodule pointer at the **tip of that project's own `main`**. Periodically fetch upstream and advance each pointer (committing the bumps) so they stay aligned. A current, uniform starting point keeps worktree checkouts consistent and avoids confusion over where each one begins.
35 
36## Writing
37 
38How to write across everything you produce: chat replies, prose and docs, code comments (not code itself), and prompts you author for models. Apply with judgment for the context; these are defaults, not absolute bans.
39 
40- **Rhythm:** mix short and long sentences; don't let them settle into a uniform length. Break up long sentences. Vary word choice, and prefer plain, short words over long or inflated ones.
41- **Avoid these words and phrases** (prefer concrete, plain alternatives): leverage, robust, scalable, innovative, cutting-edge, state-of-the-art, seamless, synergy, synergistic, passionate, thrilled, honored, delighted, spearhead, pivotal, transformative, transformational, paradigm, unlock, unleash, empower, deep dive, delve, navigate the landscape, ever-evolving, fast-paced, dynamic environment, moreover, furthermore, additionally, in essence, in today's, in the realm of, at the intersection of, look forward to, would welcome the opportunity, excited to discuss, I'd love to discuss, tapestry, underscore, testament to, speaks volumes.
42- **Avoid these constructions:** "not just X but Y"; reflexive "X, Y, and Z" triads; "it is important / worth noting / essential / crucial that…" hedging (state it plainly); restating the same point ("in other words", "to put it simply", "put simply", "more specifically", "that is to say"), say it once.
43- **Punctuation:** go light on em-dashes (use commas, periods, or parentheses); use plain quotes, not curly ones; and split overly long, comma-stacked sentences.
44 
45## Code
46 
47**Stack defaults**
48 
49| Concern | Standard |
50|---|---|
51| UI language | TypeScript |
52| Python deps | `uv` |
53| Python format + lint | `ruff` |
54| Python types | `mypy` |
55| TypeScript format / lint | `prettier` / `eslint` |
56| Config (& secrets) | Pydantic Settings (`SecretStr` for secrets) |
57| System-wide installs | `brew` |
58| Local model calls | **Gemma 4 26B MoE** via Ollama (all model calls, going forward) |
59 
60- **Python quality bar:** strict DDD with MyPy-enforced typing so invalid states are unrepresentable; functional style where it fits, without ceremony; Google style guide (80-char lines); tests valued for meaning, not coverage.
61- **Docs:** stale docs/comments are bugs; fix them when a change invalidates one. Respect the user's hand-edits and regenerate derived artifacts *from* them; never overwrite. Write for humans and future agents alike; comments justify *why*.
62 
63## Verification
64 
65- **Scale rigor to the change.** Small, low-risk changes get a light touch: a quick check and a plain note of what you did. Save the full bar below for substantial or outward-facing work; don't put full-suite runs, boot-and-verify, or subagent review on a one-liner.
66- **Full suite green (for substantive changes):** nothing is done until the project's full check suite passes: tests, formatters, linters (and the project's loops), run in sensible order and re-run after each fix to confirm it resolved. Show the evidence when asked or when the result is non-obvious. Add the *discriminating* test that only the new layer could catch, not coverage padding.
67- **Boot it and lay eyes on it:** passing tests β‰  working UX. For runtime/UI work, boot the server and hand over the URL, *and* self-verify (screenshots, click controls, hunt runtime errors). For telemetry/infra, give a concrete way to observe each signal flowing: a ready query, API call, or endpoint.
68- **Independent adversarial review:** delegate serious analysis/verification to independent subagents; don't bias them with the answer you want. Canonical form: **adversarial review** (e.g. alpha-player playtests for UI, fact-checking every claim for prose).
69 
70## Pull requests
71 
72- Whenever you open a PR, use the **`make-read-thru`** skill to generate a code-level reading guide *of that PR's content*: a walkthrough a reviewer can read top to bottom and vouch for (size it to the change; the skill's `references/depth-and-scope.md` has the tiers).
73- **Deploy** the guide to the static site: write it to `projects/static/public/read-thru/<repo>/pr-<n>-<slug>.html` (slug = short kebab summary of the PR), then push the static repo's `main` β€” the site is Vercel-hosted, so the push itself deploys it.
74- In the PR body, add a **bare link, no preamble** to the page's clean URL (no `.html` suffix): `https://static.mseeks.me/read-thru/<repo>/pr-<n>-<slug>`.
75- Deploy/namespacing above is **personal-workspace-specific and lives only here**. The `make-read-thru` skill knows nothing about the static site; it just produces the HTML.
76- **Title & body:** the title is human, **terse, and specific** to the change, never generic. The body is concise, well-structured, and comprehensible, focused on *what* the change is and *why* the PR exists at all. Avoid "meta talk": don't leak conversation or process concerns into the PR, and don't over-explain things that aren't immediately relevant.
77- **Chaining dependent PRs:** when changes build on each other, chain the PRs by targeting each other in order so each reviews cleanly.
78 
79## Infrastructure
80 
81- `infra/` holds Terraform for personal DigitalOcean infrastructure (single source of truth for the DO account): DNS zones/records, the Spaces bucket holding Terraform's own remote state, and the adopted everwhen Supabase project.
82- All sites and apps are hosted on **Vercel** β€” glassbox, static, 2ls.tech, and everwhen (prod, `https://playeverwhen.com`). Each Vercel project deploys via its repo's GitHub integration (push to `main`); the only infra they need here is their DNS records in `dns.tf`.
83- Ollama runs on the Mac Studio via the `com.zo.ollama` LaunchAgent (see the stack defaults above for when to use it).
84- Secrets never go in the repo: the DO token is read from `DIGITALOCEAN_ACCESS_TOKEN` via gitignored `infra/.env` (used by both terraform and doctl).
85- Project-specific deployments live centrally in `infra/` and point *outward* at public repos; never put Terraform inside the (open-source) project repos.