Repo-specific context for work in adrianwedd.com, maintained by the repo
owner. Read it before any task here: it records constraints that aren't
derivable from the code, and it takes precedence over general instincts about
how a site "should" be built. It does not override the user's instructions, and
it does not extend to other repositories. Entries are labeled OBSERVED
(explicit in code/history) or INFERRED (probable; evidence stated).
If something here conflicts with what you observe in the code, do not follow it blindly — parts go stale (see Section 4). Drift is a repair job, not a stop sign: work out which source is stale, wrong, or differently scoped (git log the claim, read the code, check the CI run), reconcile what the evidence settles, and say what you resolved. Correcting a stale fact here is in scope for the task that exposes it; changing an invariant, a decision, or the Section 5 list needs the task to name that change. Escalate only an unsettled conflict about an invariant.
A task is delegation: it authorizes the reversible, repository-local work
needed to achieve it — reading anything local, the Section 6 checks, editing
content, components, styles, scripts, worker code, and workflows, wrangler deploy --dry-run, committing on a branch, pushing it, and opening a PR —
without a second permission check, unless the task says review-only / no
changes. The boundary is what reaches the public or a live system, not
"how big is the change": implementation is not execution. Editing
deploy.yml, a worker, or a workflow on a branch is repo-local (call the diff
out in the PR); a real wrangler deploy, a KV/R2/DO mutation, a secret, a
frontmatter change that can fire autopublish, an NLM generation run, a YouTube
upload, or a URL change landing on main is the gate (Section 5). When the
current task explicitly authorizes a class of action, do not ask again whether
you may do it; only the Section 5 boundaries that are genuinely the human's
always need a fresh, specific go-ahead.
Missing conversational history is never a blocker. Reconstruct state from
git log, gh issues/PRs and workflow runs, files on disk, and the Section 6
checks; re-derive an earlier agent's reasoning from the evidence rather than
reporting its absence. Resolve ordinary ambiguity with evidence first, and
report the assumption you made after doing the recoverable work, not instead
of it.
Content is the product; code exists only to publish it safely. This repo is
Adrian Wedd's personal site (Astro 6 static, GitHub Pages) plus its publishing
pipeline: Cloudflare Workers (worker/ social automation, worker-csp/,
worker-mta-sts/) and NotebookLM media-kit tooling (scripts/notebooklm/).
"Done" for any change = the CI-equivalent gates pass, published URLs remain
stable, and nothing is published to any external platform without explicit
instruction.
Deliberately NOT: server-rendered (fully static; the CSP worker only rewrites at the edge); a place for secrets (GitHub secrets / wrangler secrets / gitignored files only); framework-everywhere (islands only); auto-deployed workers (CI deploys the site only; workers deploy manually). OBSERVED throughout config and CI.
Forbidden first. Each names the "helpful improvement" that violates it.
- Never rename, move, or delete a published content file in
src/content/— filename = permanent URL. The "helpful" slug/typo cleanup breaks permalinks. URL changes require a redirect inastro.config.mjs+ explicit approval. OBSERVED (CLAUDE.md permalink policy, redirects in astro.config.mjs). - Never compress, re-encode, or "optimize" published audio/video. Audio ships
at NLM original quality (256k AAC in MP4, named
.m4a); videos are muxed with-c copy. The "helpful" file-size optimization destroys irreplaceable takes — deleted notebooks can't re-issue the same narration. Never delete generated media on your own judgment. OBSERVED (CLAUDE.md, repeated user directives in history). - Never set
autopublish: trueon content that was hand-posted or hasdate≥ today unless explicitly told — the social pipeline fires ondateand will re-broadcast. The "helpful" frontmatter normalization posts to Facebook/Twitter/Bluesky for real. OBSERVED (social-autopublish.yml + incident history). - Never use Tailwind's
dark:prefix; theming is CSS custom properties insrc/styles/global.css. The "helpful" idiomatic-Tailwind refactor breaks light mode. OBSERVED (CLAUDE.md). - Never emit a raw
<img>for a local path — use<Picture>fromastro:assets. CI gate fails the deploy. OBSERVED (deploy.yml gate). - Every
.webpheroImage must have a.jpgtwin at the same path inpublic/. Local build passes without it; only CI catches it — this broke main twice. OBSERVED (deploy.yml gate + commit history #420/#425). - Content descriptions ≤ 160 chars; all frontmatter must pass
node scripts/validate-content.js. OBSERVED (script + CI). - Never generate an href from a collection ID without
slug()fromsrc/lib/utils.ts— Astro 6 IDs include.md. The "helpful" directentry.idinterpolation 404s. OBSERVED (CLAUDE.md + utils.ts). - Interactive scripts must follow the View Transitions pattern (is:inline +
documentElement.datasetsentinel + event delegation + lazy lookups +astro:after-swapinside the guard).Analytics.astro's bare module<script>is a deliberate exception — do not "align" it with the pattern. OBSERVED (CLAUDE.md + component code). - No tracking before consent: keep
dns-prefetch(notpreconnect) for GA4 origins. The "helpful" preconnect perf tweak opens connections pre-consent. OBSERVED (CLAUDE.md + Analytics.astro). - No custom fonts — system stack only. Do not add font files,
@font-face, Google Fonts, Adobe Fonts, or font preconnects. OBSERVED (CLAUDE.md). - The
overridesblock inpackage.jsonis load-bearing: it pins transitive deps to pass CI'snpm audit --audit-level=high --omit=devgate. The "helpful" removal of "unused" overrides re-breaks the deploy. New audit failures are fixed by adding overrides, never by downgrading Astro. OBSERVED (commits #471, #480, #489). - The worker's idempotency +
CronLockDurable Object logic is load-bearing:forceRetrybypassesfailedrecords but neverpublished; the DO closes a KV TOCTOU window;release()requires the fencing token. The "helpful" simplification to plain KV get/put re-opens double-posting. OBSERVED (CLAUDE.md + worker code/tests). - Never commit
src/data/base-cv.json,.dev.vars,.env*, or NLM auth profiles. They are gitignored/synced for a reason. OBSERVED (.gitignore, deploy.yml CV sync). - Do not delete branches you didn't create — auto-draft branches hold unmerged content. INFERRED (content-pipeline.yml creates draft branches; prior sessions harvested them).
- Do not "fix"
.lychee.tomlexclusions — own-domain and social-domain exclusions are deliberate (pre-deploy 404s, bot-blocked sites). OBSERVED (comments in .lychee.toml). public/og/blog/text-card PNGs are committed on purpose;generate-og-images.mjsdeliberately skips posts with a heroImage. Do not "backfill" cards for heroImage posts or gitignore the directory. OBSERVED (script comments + CLAUDE.md).- Never make a monitoring check quieter to make a dashboard greener. A missing heartbeat, an unreadable repo, a source that never checked in — all count as degraded on purpose. The "helpful" change that treats absence as "unknown, so probably fine" restores the exact blind spot these checks were built to close. Equally: never let a check sit permanently red (alert on the un-notified subset of crisis flags, not the raw 90-day-TTL count), and never let one root cause emit two alerts. A check that is always red and a check that cries wolf both end up ignored, which is the same as not existing. OBSERVED (worker/src/heartbeat.ts, watchdog.ts, docs/runbooks/monitoring.md).
- Staleness thresholds for anything on GitHub's scheduler must be sized from the MEASURED gap distribution, never from the cron expression. Measured 2026-07-30: the nominally-10-minute social-cron gaps ran median 26.9 min / max 72.9, and the nominally-5-minute Upptime run gaps median 26.6 / max 60.1. The first-cut thresholds, written from the cron lines with "a few minutes" of grace, would have false-alarmed on 31% and ~50% of probes. Re-measure before tightening. OBSERVED (worker/src/heartbeat.ts, .github/workflows/monitor-watchdog.yml).
- An authenticated monitoring check needs a body assertion, not just a status
code.
/api/healthanswers200 {"ok":true}to an unauthenticated caller, so a status-code-only Upptime check would go permanently green if$SECRET_SITEwere ever unset or rotated — failing silently, in the one direction that cannot be noticed. OBSERVED (adrianwedd/status.upptimerc.yml— the repo was named adrianwedd/upptime when this was verified live 2026-07-30, renamed 2026-07-31). - The three monitoring sweeps must keep
set +e -u +o pipefail. GitHub Actions invokesrun:asbash -e {0}, so restoring-e/pipefail makes the scripts abort on the very pipelines that detect problems — verified: the integrity check aborted after 2 findings of 5 and exited 1. And their post-check steps must stayif: success(), neveralways(): on a crashed check the missingcountoutput reads as 0, which closes the open issue and (for the watchdog) sends a healthy check-in that suppresses the email escalation. OBSERVED (the three workflows + their in-line comments).
Executors never relitigate these.
- Static GitHub Pages + edge CSP worker, not SSR:
worker-cspinjects per-request nonces. Rejected: meta-tag CSP hashing — decided 2026-07-03 (#473): Astro 6'ssecurity.cspdocuments ClientRouter (View Transitions) and Shiki as unsupported, both load-bearing here. The'unsafe-inline'meta fallback inSEOHead.astrois risk-accepted for the worker-bypass path; keep its host lists in sync withworker-csp/src/csp.ts. Revisit only if Astro's CSP gains ClientRouter support or the site drops VT. OBSERVED (#497, worker-csp/, Astro config.d.ts). - Lighthouse CI is manual (
workflow_dispatch) since #488; a PR showing Lighthouse "skipping" is CORRECT, not broken. Run locally vianpm run build && npm run lighthouse. Rejected: per-PR runs (flaky/slow). OBSERVED (#488, lighthouse.yml). - Validation gates run on PRs too since #503, because green PRs previously
broke main.
npm run verifymirrors them locally. OBSERVED (#503, deploy.yml). - OG image dimensions come from a build-time file-header parser
(
src/lib/image-dimensions.ts), replacing a mis-sizing filename heuristic. Never import it into a Worker — it usesnode:fs. OBSERVED (CLAUDE.md + code). - Media (audio/video) live on Cloudflare R2 at
cdn.adrianwedd.com; infographics stay in git. Rejected: all-in-git (bloat) and all-on-R2 (heroes need build-time processing). OBSERVED (CLAUDE.md, upload scripts). Video distribution goes to YouTube, not social attachments — platform size caps make worker video delivery impossible. OBSERVED (worker platform code, upload-videos-to-youtube.py). repo:frontmatter was removed from 5 projects because those repos are private (lychee 404s). Revisit trigger: repos go public. OBSERVED (.lychee.toml exclusions + history).- Social queue seed is
social/facebook-posts.jsonbut KV is authoritative after sync. Editing the JSON alone changes nothing live. OBSERVED (worker /api/queue/sync). - Search is Pagefind (build-time index); hosted search rejected. INFERRED from the static/zero-runtime-JS philosophy applied consistently.
- CLAUDE.md's "four collections" is stale:
src/content.config.tsdefines six (blog, projects, gallery, audio, fixes, case-studies). Trust the code. OBSERVED (content.config.ts:124).
- Green local build ≠ green deploy. The jpg-twin, raw-
<img>, audit, lychee, and internal-link gates run in CI. Tell: you touched a heroImage, image markup, or dependencies and only rannpm run build. Runnpm run verify. - Editing frontmatter dates/flags on existing content. Tell: any diff to
dateorautopublishon a published file — this can re-fire social posts. - Treating untracked repo-root files as litter. Treat ANY untracked root file
as hazardous unless your current task created it — never commit, delete, or
build on one; leave it untouched and note it in your summary (it is not a
reason to stop). Known strays as of 2026-07-02:
diff.txt,modify_config.py,put_payload.json,tunnel_config.json(exposes a live Cloudflare tunnel ID and home-LAN topology),TOMORROW.md. OBSERVED (untracked, unrelated to the site). - Assuming merged worker code is deployed. Workers deploy manually. Tell:
claiming a worker fix is "live" after merge without a
wrangler deploy. - Trusting CLAUDE.md/AGENTS.md/GEMINI.md over the code or this file — parts are stale (collection count; an old 64k-audio step). If code, CLAUDE.md, and this file conflict with EACH OTHER, reconcile them (code wins on what the system does; this file wins on what is allowed) and say what you resolved; escalate only a conflict about an invariant the evidence cannot settle.
- Running two
nlmCLI processes concurrently (token-file race), or using the NLM MCP instead of the CLI. Tell: parallel nlm invocations in one step. - "Cleaning up"
dist/into git or formatting sweeps acrosssrc/content/producing giant content diffs unrelated to the task.
A trigger stops movement through that door, not the task: finish every independent repo-local part, then bring the gated decision back once with evidence and a recommendation.
- Anything touching secrets/auth:
.dev.vars,.env*, GA4 keys,PIPELINE_PAT,PUBLISH_SECRET/CLI_SECRET, NLM cookies, YouTube OAuth. - Any non-dry-run
wrangler deploy(all three workers) — the--dry-runin Verification step 7 is allowed. Also: KV/R2/DO mutations, DNS/email config, Cloudflare API calls (even reads — the account holds non-site infrastructure). - Any call to
social.adrianwedd.comendpoints,fb-post.sh, queue sync, or frontmatter changes that could trigger autopublish — these post publicly. - Deleting/renaming anything in
src/content/,public/notebook-assets/,public/og/, or any generated media; adding redirects. - NotebookLM generation runs (daily quota ~50/type; dedicated-account ToS risk) and YouTube uploads.
- Branch protection (a live repo setting), and weakening a
deploy.ymlgate or a.lychee.tomlexclusion. Editing either file on a branch for a task that names it is repo-local — call the diff out in the PR. - A conflict about an invariant that the evidence cannot settle; any task that seems to require violating Section 2. (Untracked root files and ordinary doc drift are handled per Section 4, not escalated.)
node scripts/validate-content.js→ exits 0, no errors listed.npx astro check→ 0 errors.npm run lint→ 0 errors.npm run verify→ exits 0. This mirrors CI: check + validate + build +scripts/test-site.sh+ internal-link check. Slow but required before claiming done.- If any
src/content/filename, any slug, a collection definition, or a redirect changed: list the before/after URL impact in the PR and leave the merge to the human — that is the approval Section 2 requires. No automated check exists for this. - If you touched a
.webpheroImage: confirm the.jpgtwin exists at the same path underpublic/(ls public/<path>.jpg). - If you touched
worker/:cd worker && npm test→ all pass, thennpx wrangler deploy --dry-run→ succeeds (dry-run is allowed; actual deploy is not). - If you touched
worker-csp/:cd worker-csp && npm test && npx tsc --noEmit→ all pass. npm run format:check→ clean (or format only the files you changed).
No automated check exists for social re-broadcast safety or media quality — review the diff against Section 2 and escalate on any doubt.