From 9f1c21834d49539f32bed07f07a4cbe7d51deb9c Mon Sep 17 00:00:00 2001 From: "WellSpr.ing" Date: Fri, 24 Apr 2026 02:26:26 +0000 Subject: [PATCH] initial: notgit-routes.ts source + README --- README.md | 37 +- notgit-routes.ts | 1693 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1728 insertions(+), 2 deletions(-) create mode 100644 notgit-routes.ts diff --git a/README.md b/README.md index 342528f..0753d4a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,36 @@ -# notgit +# NotGit.org -NotGit.org — source for the sovereign Replit deployment cookbook and GitHub suspension incident registry \ No newline at end of file +> GitHub is a discovery layer, not a git host. Here's the migration. + +NotGit.org documents the GitHub enforcement pattern — sudden account suspensions without warning, graduated enforcement, or appeal SLA — and publishes the complete sovereign deployment formula for any developer whose account has been suspended. + +Built April 18, 2026, the day GitHub suspended the WellSpr.ing account and 191 civic AI repositories. + +## What's here + +- `notgit-routes.ts` — the entire NotGit.org site as server-side rendered HTML, served from the WellSpr.ing platform. All five pages: home, formula, push-mirror-setup, incident, report, and the Replit deployment cookbook. + +## The stack this site runs on + +NotGit.org eats its own cooking: + +``` +Git primary: git.wellspr.ing/wellspring/notgit (Forgejo on Railway) + └─mirror: codeberg.org/wellspring/notgit (nonprofit backup) +CI builder: Hetzner cx23 (€5/mo) (Forgejo act_runner) +CDN/Edge: Bunny CDN + Fly.io proxy (~$1/mo) +Dev: Replit (AI-assisted) +``` + +## Pages + +- `/` — Home: the incident, the pattern, the exit guide +- `/formula` — The three-mirror formula (printable, linkable) +- `/push-mirror-setup` — Exact API calls for push-mirror to Codeberg +- `/incident/2026-04-18` — The WellBuilder incident record (permanent URL) +- `/report` — Submit a GitHub suspension case to the corpus +- `/replit` — **Full sovereign deployment cookbook for Replit users** — self-hosted Git + Hetzner CI + CDN edge layer + +## License + +MIT. Published under the WellSpr.ing covenant: freely given, so freely given. diff --git a/notgit-routes.ts b/notgit-routes.ts new file mode 100644 index 0000000..73ceb49 --- /dev/null +++ b/notgit-routes.ts @@ -0,0 +1,1693 @@ +/** + * NotGit.org — Route handler + * "GitHub is a discovery layer, not a git host. Here's the migration." + * + * NotGit.org is a WellSpr.ing civic project. Published April 18, 2026. + * The formula is given freely under the WellSpr.ing covenant: freely given, so freely given. + * + * Same stack pattern as NoFlare.org — standalone SSR HTML pages, no SPA. + * Hosted on the same Replit deployment, domain-keyed via hostMatches. + */ + +import type { Express, Request, Response } from "express"; +import { db } from "./db"; +import { sql } from "drizzle-orm"; + +const ADMIN_KEY = process.env.ADMIN_KEY || "b0db7a87384fc814b0f46ea7bdc6ab6a81152be5b098718b"; + +async function ensureNotgitTables() { + await db.execute(sql` + CREATE TABLE IF NOT EXISTS notgit_incident_reports ( + id SERIAL PRIMARY KEY, + github_username TEXT, + suspension_date TEXT, + ticket_id TEXT, + work_description TEXT NOT NULL, + suspected_trigger TEXT, + resolution_status TEXT DEFAULT 'pending', + contact_email TEXT, + ip TEXT, + status TEXT NOT NULL DEFAULT 'received', + submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ) + `); + console.log("[NotGit] Incident report table ready"); +} + +// ── Favicon — git branch diverging into sovereignty ────────────────────────── +const NG_FAVICON = `data:image/svg+xml,%3Csvg viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3' fill='%23ff8c00'/%3E%3Ccircle cx='8' cy='24' r='3' fill='%23ff8c00' opacity='0.4'/%3E%3Ccircle cx='24' cy='12' r='3' fill='%23ff8c00'/%3E%3Cpath d='M8 11v10' stroke='%23ff8c00' stroke-width='1.5' opacity='0.4'/%3E%3Cpath d='M8 11 C8 16 24 15 24 15' stroke='%23ff8c00' stroke-width='1.5'/%3E%3Cpath d='M24 15 L28 10 M24 15 L28 20' stroke='%23ff8c00' stroke-width='1.2' stroke-linecap='round'/%3E%3C%2Fsvg%3E`; + +// ── Shared CSS ──────────────────────────────────────────────────────────────── +const NG_CSS = ` + :root { + --bg: #080b10; --bg2: #0d1018; --bg3: #131820; --surface: #181f28; + --border: rgba(255,255,255,0.07); --border-accent: rgba(255,140,0,0.25); + --text: #dde4ee; --text2: #8898aa; --text3: #4a5568; + --amber: #ff8c00; --amber-dim: rgba(255,140,0,0.12); --amber-glow: rgba(255,140,0,0.05); + --green: #22c55e; --blue: #38bdf8; + } + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + html { scroll-behavior: smooth; } + body { + font-family: 'DM Sans', system-ui, sans-serif; background: var(--bg); + color: var(--text); line-height: 1.7; overflow-x: hidden; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E"); + } + a { color: var(--amber); text-decoration: none; } + a:hover { text-decoration: underline; } + code, pre { font-family: 'DM Mono', 'Fira Code', monospace; } + .container { max-width: 1100px; margin: 0 auto; padding: 0 2rem; } + .container-narrow { max-width: 760px; margin: 0 auto; padding: 0 2rem; } + + /* ── Nav ── */ + nav { position: sticky; top: 0; z-index: 100; background: rgba(8,11,16,0.92); backdrop-filter: blur(12px); border-bottom: 1px solid var(--border); } + .nav-inner { display: flex; align-items: center; justify-content: space-between; padding: 0 2rem; height: 56px; max-width: 1100px; margin: 0 auto; } + .nav-logo { font-family: 'Bebas Neue', sans-serif; font-size: 1.3rem; letter-spacing: 0.08em; color: var(--text); } + .nav-logo span { color: var(--amber); } + .nav-links { display: flex; gap: 2rem; } + .nav-links a { font-size: 0.82rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text2); text-decoration: none; transition: color 0.2s; } + .nav-links a:hover { color: var(--amber); text-decoration: none; } + + /* ── Hero ── */ + .hero { padding: 7rem 0 6rem; position: relative; overflow: hidden; } + .hero::before { content: ''; position: absolute; top: -200px; left: 50%; transform: translateX(-50%); width: 800px; height: 600px; background: radial-gradient(ellipse, var(--amber-glow) 0%, transparent 70%); pointer-events: none; } + .hero-eyebrow { font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--amber); margin-bottom: 1.5rem; display: flex; align-items: center; gap: 0.75rem; } + .hero-eyebrow::before { content: ''; display: inline-block; width: 24px; height: 1px; background: var(--amber); } + h1.hero-title { font-family: 'Bebas Neue', sans-serif; font-size: clamp(3rem, 8vw, 6.5rem); line-height: 0.95; letter-spacing: 0.02em; margin-bottom: 2rem; } + h1.hero-title .dim { color: var(--text3); } + h1.hero-title .exit { color: var(--amber); } + .hero-sub { font-size: 1.1rem; color: var(--text2); max-width: 620px; margin-bottom: 3rem; line-height: 1.75; } + .hero-ctas { display: flex; gap: 1rem; flex-wrap: wrap; } + .btn { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.85rem 1.75rem; font-size: 0.9rem; font-weight: 500; letter-spacing: 0.03em; cursor: pointer; transition: all 0.2s; border: none; font-family: 'DM Sans', sans-serif; } + .btn-primary { background: var(--amber); color: #080b10; } + .btn-primary:hover { background: #ffaa33; text-decoration: none; color: #080b10; } + .btn-secondary { background: transparent; color: var(--text2); border: 1px solid var(--border); } + .btn-secondary:hover { border-color: var(--amber); color: var(--amber); text-decoration: none; } + + /* ── Sections ── */ + section { padding: 6rem 0; border-top: 1px solid var(--border); } + section.alt { background: var(--bg2); } + .section-label { font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--amber); margin-bottom: 1rem; } + h2.section-title { font-family: 'Bebas Neue', sans-serif; font-size: clamp(2rem, 5vw, 3.5rem); letter-spacing: 0.03em; line-height: 1.1; margin-bottom: 1.5rem; } + .section-body { color: var(--text2); font-size: 1rem; line-height: 1.8; } + .section-body p { margin-bottom: 1.25rem; } + + /* ── Timeline ── */ + .timeline-box { background: var(--surface); border: 1px solid var(--border-accent); padding: 1.5rem; margin: 2.5rem 0; overflow-x: auto; } + .timeline-box pre { font-family: 'DM Mono', monospace; font-size: 0.82rem; color: var(--text2); line-height: 1.9; white-space: pre; } + .timeline-box pre .ts { color: var(--amber); } + .timeline-box pre .evt { color: var(--text); } + + /* ── Callout ── */ + .callout { background: var(--amber-dim); border-left: 3px solid var(--amber); padding: 1.5rem 2rem; margin: 2.5rem 0; font-family: 'DM Serif Display', serif; font-size: 1.1rem; font-style: italic; line-height: 1.65; color: var(--text); } + + /* ── Evidence links ── */ + .evidence-links { margin-top: 2.5rem; } + .evidence-link { display: flex; align-items: flex-start; gap: 1rem; padding: 1rem 0; border-bottom: 1px solid var(--border); font-size: 0.9rem; } + .evidence-link:first-child { border-top: 1px solid var(--border); } + .ev-tag { font-family: 'DM Mono', monospace; font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.2rem 0.6rem; border: 1px solid var(--border-accent); color: var(--amber); flex-shrink: 0; margin-top: 0.15rem; } + .ev-text a { color: var(--text); font-weight: 500; } + .ev-text a:hover { color: var(--amber); } + .ev-desc { font-size: 0.82rem; color: var(--text3); margin-top: 0.2rem; } + + /* ── Pattern section ── */ + .case-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 2.5rem; } + .case-card { background: var(--surface); border: 1px solid var(--border); padding: 1.5rem; } + .case-project { font-family: 'DM Mono', monospace; font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--amber); margin-bottom: 0.5rem; } + .case-desc { font-size: 0.88rem; color: var(--text2); line-height: 1.6; } + .case-stat { font-family: 'DM Mono', monospace; font-size: 0.78rem; color: var(--text3); margin-top: 0.75rem; } + .pullquote { background: var(--bg3); border: 1px solid var(--border-accent); padding: 2rem; margin: 2.5rem 0; } + .pullquote blockquote { font-family: 'DM Serif Display', serif; font-size: 1.15rem; font-style: italic; color: var(--text); line-height: 1.65; } + .pullquote cite { display: block; margin-top: 1rem; font-family: 'DM Mono', monospace; font-size: 0.75rem; color: var(--text3); font-style: normal; } + + /* ── Architecture diagram ── */ + .arch-diagram { background: var(--surface); border: 1px solid var(--border-accent); padding: 2rem; margin: 2.5rem 0; overflow-x: auto; } + .arch-diagram pre { font-family: 'DM Mono', monospace; font-size: 0.8rem; color: var(--text2); line-height: 1.7; white-space: pre; } + .arch-diagram pre .hl { color: var(--amber); } + + /* ── Three steps ── */ + .steps { margin-top: 2.5rem; } + .step { display: flex; gap: 2rem; padding: 2rem 0; border-bottom: 1px solid var(--border); } + .step:first-child { border-top: 1px solid var(--border); } + .step-num { font-family: 'Bebas Neue', sans-serif; font-size: 3rem; color: var(--amber); line-height: 1; flex-shrink: 0; width: 52px; } + .step-title { font-size: 1.05rem; font-weight: 600; color: var(--text); margin-bottom: 0.5rem; } + .step-body { font-size: 0.92rem; color: var(--text2); line-height: 1.75; } + .step-body code { background: var(--surface); padding: 0.15rem 0.4rem; font-size: 0.83rem; color: var(--amber); border: 1px solid var(--border); } + + /* ── Comparison table ── */ + .compare-wrap { overflow-x: auto; margin-top: 2.5rem; } + table { width: 100%; border-collapse: collapse; font-size: 0.88rem; } + thead { background: var(--surface); } + th { padding: 1rem 1.25rem; text-align: left; font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text3); font-weight: 400; border-bottom: 1px solid var(--border); } + td { padding: 0.9rem 1.25rem; border-bottom: 1px solid var(--border); color: var(--text2); vertical-align: top; } + tr:hover td { background: var(--surface); } + td strong { color: var(--text); } + td .highlight { color: var(--amber); font-weight: 500; } + .best-col { background: var(--amber-dim); } + + /* ── AI rules ── */ + .rules-list { margin-top: 2rem; } + .rule { display: flex; gap: 1.5rem; padding: 1.5rem 0; border-bottom: 1px solid var(--border); } + .rule:first-child { border-top: 1px solid var(--border); } + .rule-num { font-family: 'DM Mono', monospace; font-size: 0.78rem; color: var(--amber); flex-shrink: 0; width: 28px; padding-top: 0.1rem; } + .rule-title { font-weight: 600; color: var(--text); margin-bottom: 0.4rem; font-size: 0.95rem; } + .rule-body { font-size: 0.88rem; color: var(--text2); line-height: 1.7; } + + /* ── Form ── */ + .form-wrap { background: var(--surface); border: 1px solid var(--border); padding: 2.5rem; max-width: 680px; margin-top: 2.5rem; } + .form-row { margin-bottom: 1.5rem; } + label { display: block; font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text3); margin-bottom: 0.5rem; } + input[type=text], input[type=email], textarea, select { + width: 100%; background: var(--bg); border: 1px solid var(--border); color: var(--text); + font-family: 'DM Sans', sans-serif; font-size: 0.93rem; padding: 0.75rem 1rem; + outline: none; transition: border-color 0.2s; resize: vertical; + } + input:focus, textarea:focus, select:focus { border-color: var(--amber); } + input::placeholder, textarea::placeholder { color: var(--text3); } + select option { background: var(--bg2); } + textarea { min-height: 100px; } + .form-note { font-size: 0.8rem; color: var(--text3); margin-top: 0.4rem; } + .form-submit { margin-top: 2rem; } + .form-success { display: none; background: rgba(34,197,94,0.1); border: 1px solid rgba(34,197,94,0.3); padding: 1.5rem; color: #6ee7a0; font-size: 0.92rem; } + + /* ── Footer ── */ + footer { background: var(--bg3); border-top: 1px solid var(--border); padding: 4rem 0 2.5rem; } + .footer-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 3rem; margin-bottom: 3rem; } + .footer-col-title { font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text3); margin-bottom: 1.25rem; } + .footer-col a { display: block; font-size: 0.88rem; color: var(--text2); margin-bottom: 0.6rem; } + .footer-col a:hover { color: var(--amber); text-decoration: none; } + .footer-bottom { border-top: 1px solid var(--border); padding-top: 2rem; font-size: 0.82rem; color: var(--text3); line-height: 1.7; } + .footer-bottom a { color: var(--text3); } + .footer-bottom a:hover { color: var(--amber); } + + /* ── Standalone page (formula, push-mirror, incident) ── */ + .standalone { padding: 4rem 0 6rem; } + .standalone-eyebrow { font-family: 'DM Mono', monospace; font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--amber); margin-bottom: 1rem; } + .standalone h1 { font-family: 'Bebas Neue', sans-serif; font-size: clamp(2rem, 5vw, 3.5rem); letter-spacing: 0.03em; line-height: 1.1; margin-bottom: 1rem; } + .standalone .sub { font-size: 1.05rem; color: var(--text2); max-width: 620px; margin-bottom: 3rem; line-height: 1.75; } + .standalone h2 { font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; letter-spacing: 0.03em; margin: 3rem 0 1rem; color: var(--text); } + .standalone h3 { font-size: 1.05rem; font-weight: 600; color: var(--text); margin: 2rem 0 0.75rem; } + .standalone p { color: var(--text2); font-size: 0.97rem; line-height: 1.8; margin-bottom: 1rem; } + .standalone ul, .standalone ol { color: var(--text2); font-size: 0.95rem; line-height: 1.8; padding-left: 1.5rem; margin-bottom: 1.25rem; } + .standalone li { margin-bottom: 0.4rem; } + .standalone code { background: var(--surface); padding: 0.15rem 0.4rem; font-size: 0.83rem; color: var(--amber); border: 1px solid var(--border); font-family: 'DM Mono', monospace; } + .codeblock { background: var(--surface); border: 1px solid var(--border); padding: 1.5rem; margin: 1.5rem 0; overflow-x: auto; } + .codeblock pre { font-family: 'DM Mono', monospace; font-size: 0.82rem; color: var(--text2); line-height: 1.8; white-space: pre; } + .codeblock .comment { color: var(--text3); } + .codeblock .cmd { color: var(--amber); } + .codeblock .str { color: var(--green); } + .tip { background: var(--amber-dim); border-left: 3px solid var(--amber); padding: 1rem 1.5rem; margin: 1.5rem 0; font-size: 0.9rem; color: var(--text2); } + .tip strong { color: var(--amber); } + .divider { border: none; border-top: 1px solid var(--border); margin: 3rem 0; } + + @media (max-width: 720px) { + .nav-links { display: none; } + .hero-ctas { flex-direction: column; } + .step { flex-direction: column; gap: 1rem; } + .stack-row { grid-template-columns: 1fr; gap: 2rem; } + h1.hero-title { font-size: 3.2rem; } + } +`; + +const NG_FONTS = ` + + + +`; + +const NG_NAV = ` + +`; + +const NG_FOOTER = ` + +`; + +function ngHead(title: string, desc: string, path = "") { + const url = `https://notgit.org${path}`; + return ` + + + + +${title} + + + + + + + + + + + +${NG_FONTS} + + +`; +} + +// ── Home page ───────────────────────────────────────────────────────────────── +const NOTGIT_HOME = ngHead( + "NotGit.org — GitHub is a discovery layer, not a git host.", + "When GitHub suspends your account without notice, you don't need GitHub. The migration from GitHub-dependent to git-sovereign takes 15 minutes. Formula, tooling, incident record.", +) + NG_NAV + ` + + +
+
+
+
Published April 18, 2026 · WellSpr.ing Civic Project
+

+ GitHub is a
+ discovery layer.
+ Not a git host.
+ Here's the migration. +

+

+ When GitHub suspends your account without notice, without graduated enforcement, + and without substantive staff response for 30–90 days, the fastest answer + is not to plead with the support queue. The fastest answer is to own your git, + mirror to GitHub as discovery surface, and keep working. +

+ +
+
+ + +
+
+ +

What happened to WellBuilder
on April 18, 2026

+
+

On the morning of April 18, 2026, at approximately 8:30 AM PT, GitHub suspended the WellBuilder organization. WellBuilder publishes civic-technology MCP servers — one per US area code — as part of the WellSpr.ing covenant-governed civic infrastructure federation. Thirty-two repositories were offline simultaneously. The personal account that owned the organization was also suspended.

+

No suspension email. No notice. No graduated enforcement. The appeal path, when opened, routed first-line response to an automated reply requesting information already present in the appeal letter. WellSpr.ing responded within minutes with complete information. The account remains suspended at the time of this site's publication.

+
+ +
+
08:30 AM PT  — WellBuilder organization suspended (all 32 repos offline)
+08:48 AM PT  — WellSpr.ing notified; unable to access account
+08:48 AM PT  — Appeal submitted via support.github.com
+~08:54 AM PT — Auto-reply requests standard identification details
+08:55 AM PT  — WellSpr.ing replies with complete information
+  — Substantive response pending as of publication
+
+ +
+ Thirty-two MCP servers. One per US area code. Federated under the Linux Foundation's own standardized protocol. Each repository complete, each serving real civic data, each under a published conduct standard. Suspended simultaneously. No email. No reason given. +
+ + +
+
+ + +
+
+ +

Documented across sixty cases

+
+

The WellBuilder case is not anomalous. A Reddit thread on r/github, maintained by volunteers since 2023, has catalogued approximately sixty suspension cases with the same signature: no email, no graduated enforcement, appeals that queue for 30 to 90 days, and — documented by multiple users — manual reinstatement by staff followed by automated re-suspension within 1 to 5 days.

+

The affected maintainers are not spammers.

+
+ +
+
+
Limine Bootloader
+
Main maintainer of Limine, a widely-adopted open-source boot manager used across hundreds of projects. Suspended without notice.
+
Status: public record, r/github thread
+
+
+
GetX Framework
+
Creator of GetX, the Flutter state management framework installed in 200,000+ projects. Suspended without notice or graduated enforcement.
+
200,000+ downstream projects affected
+
+
+
AI Research · 62 repos
+
Independent AI researcher with paid GitHub Pro account, 62 research repositories, suspended over a Git LFS model checkpoint. Had to appeal from a second paid account.
+
Paid Pro subscriber · No email sent
+
+
+
Ferdium · Core Maintainer
+
One of two core maintainers of Ferdium, an open-source messaging app. Eight-year-old GitHub account. Suspended without notice.
+
8-year account history · No warning
+
+
+
Cybersecurity Research
+
Security researcher publishing indicators-of-compromise to help others defend against malware. Suspended for publishing defensive security data.
+
Defensive research — flagged as threat
+
+
+
WellBuilder · 32 Repos
+
Civic infrastructure federation — 32 MCP servers, one per US area code, serving local civic data through the Linux Foundation's standardized protocol.
+
April 18, 2026 · This incident
+
+
+ +
+
"GitHub suspended my paid account with 62 repos, sent no email and I had to file appeals from a second paid account... If GitHub can do this to a paying customer over a model checkpoint, they can do it to you over anything."
+ — Independent AI researcher, April 2026, r/github thread +
+ +
+

GitHub's own Terms of Service Section F requires notice before suspension. The enforcement documentation describes graduated enforcement before termination. Across sixty documented cases, neither commitment appears to be consistently honored.

+

This site is not about whether GitHub is a net-positive for open-source software. It clearly is, and has been, for more than a decade. This site is about the specific gap between GitHub's stated enforcement commitments and actual enforcement practice — and about the fact that the correction to that gap is individually available to every maintainer. You do not need to wait for GitHub to fix this. You can migrate in an afternoon.

+
+
+
+ + +
+
+ +

The three-mirror architecture

+
+

The core insight: GitHub is a discovery layer, not a git host. The conflation of those two functions is the dependency trap. The formula separates them back out.

+
+ +
+ + + + + + + + + + + + + + + PRIMARY GIT — SOVEREIGN + git.yourproject.org + Self-hosted Forgejo · Railway / Fly.io · You own it + + + + auto push-mirror + + + + auto push-mirror + + + + SECONDARY — COMMUNITY + codeberg.org/yourproject + Nonprofit · EU-hosted · Forgejo-native + Free · Your fallback + + + + DISCOVERY LAYER + github.com/yourproject + Stars · Forks · Network effect + Suspendable — non-critical + +
+ +
+
+
01
+
+
Primary git — self-hosted Forgejo
+
+ Deploy on Railway, Fly.io, or any VPS. Forgejo is the community fork of Gitea, actively maintained, open source, AGPL-licensed. Deploy time on Railway: ~15 minutes. Cost: $5–10/month. CNAME git.yourproject.org to your Railway deployment. This is where you push. If GitHub or Codeberg or any other host disappears tomorrow, your git source of truth is unaffected. +
+
+
+
+
02
+
+
Secondary git — Codeberg
+
+ Codeberg is the community-governed, nonprofit Forgejo instance hosted in Berlin. Free for open source. Runs Forgejo natively, so push-mirror integration with your primary is trivial to configure. This is your fallback — what if your self-hosted instance has a bad day. Configure a push-mirror from your Forgejo primary: one API call, automatic thereafter. Full walkthrough at notgit.org/push-mirror-setup. +
+
+
+
+
03
+
+
Discovery layer — GitHub (as mirror)
+
+ GitHub, if and when your account is restored, becomes a push-mirror only. You never push to it directly. Your primary Forgejo pushes to it automatically. If GitHub suspends you again, your stars and forks are frozen but your git work continues unaffected. Suspension becomes a discoverability event, not a data-loss event. GitHub, at its best, holds a first-class place in your federation. That place just isn't primary anymore. +
+
+
+
+ +
+ What makes this resilient: Forgejo's native push-mirror feature means you configure the two secondary mirrors once. Your workflow doesn't change — git push origin main to your sovereign primary, and it syncs automatically. Suspension of any single mirror is a discoverability event, not a data-loss event. +
+ +

Full Migration Formula →  Push-Mirror Technical Walkthrough

+
+
+ + +
+
+ +

Platform comparison

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Forgejo (self-hosted)CodebergSourceHutGitLab CE
GovernanceYou own itNonprofitNonprofitYou own it
Cost~$5–10/moFreeFree / paidHeavier infra
PAT / APIFull Forgejo APIFull Forgejo APIREST + email workflowFull GitLab API
Push mirrorsYes — nativeYes — nativeLimitedYes
Speed to deploy15 min (Railway)Instant (signup)Instant (signup)Hours (self-host)
Best forPrimary sovereignCommunity secondaryMinimalist / CLI-firstOrg-scale
+
+
+
+ + +
+
+ +

Using AI coding tools
without triggering enforcement

+
+

A distinct class of suspension cases involves maintainers using AI coding assistants — Claude, Cursor, Copilot Workspace, custom MCP servers — with personal access tokens that make API calls GitHub's detection flags as anomalous. Documented cases include maintainers whose Claude Desktop + GitHub MCP integration was flagged as "token abuse," researchers whose Git LFS pushes of ML checkpoints triggered suspension, and builders whose rapid automation of repository creation (for legitimate federated projects) matched typosquatting heuristics.

+

These cases are growing. The tooling is ahead of the platform's detection calibration. The mitigation is operational.

+
+ +
+
+
01
+
+
README-first publication
+
Never publish an empty or near-empty repository. Substantive README, working initial commit, topic tags, a license. Repos created via automation that look like placeholders will flag — this is what happened to WellBuilder.
+
+
+
+
02
+
+
Cadence discipline
+
Don't create twenty repositories in ten minutes. If automation is creating repos, rate-limit it to 2–3 per day. Rapid federated publication via PAT automation resembles typosquatting patterns to detection systems. Defensive discipline regardless of platform.
+
+
+
+
03
+
+
PAT scoping
+
Give your AI assistant's PAT the minimum scope it needs. A PAT with full-org permissions used by an AI that makes many API calls per minute is exactly the pattern that flags. Scope to repo only, not admin:org unless necessary.
+
+
+
+
04
+
+
Push-mirror through sovereign primary
+
If you're using AI tooling that pushes to git, push to your sovereign Forgejo first, not to GitHub. The push-mirror propagates to GitHub. If GitHub flags the pattern, your primary is untouched and the worst case is you turn off the GitHub mirror temporarily while you resolve it.
+
+
+
+
05
+
+
Pre-commit model checkpoints elsewhere
+
Git LFS for ML work has documented edge cases. If you're working with large model artifacts, consider a dedicated artifact store (Hugging Face, Weights & Biases) and reference from git rather than committing directly. Git is not an artifact store.
+
+
+
+ +
+ The underlying principle: AI coding tools will continue outpacing platform detection calibration. The operational discipline is to use those tools against sovereign primary hosts where no single platform's classifier can take your work offline. Your AI assistant gets the same workflow. The platform becomes a mirror. +
+
+
+ + +
+
+ +

The pattern gets legible
through documentation

+

If your GitHub account has been suspended without notice, without graduated enforcement, or re-suspended after manual reinstatement, your case belongs in the corpus. Submissions inform periodic pattern reports and help other maintainers recognize what's happening in their own cases.

+ Submit an Incident Report → +
+
+ +
+${NG_FOOTER} + +`; + +// ── /formula — Standalone migration guide ───────────────────────────────────── +const FORMULA_HTML = ngHead( + "The Three-Mirror Formula — NotGit.org", + "The complete git sovereignty migration: self-hosted Forgejo primary, Codeberg secondary, GitHub as discovery mirror. Executable in an afternoon.", + "/formula", +) + NG_NAV + ` +
+
+
+
NotGit.org / Formula
+

The Three-Mirror Formula

+

The complete migration from GitHub-dependent to git-sovereign. Executable in an afternoon. Given freely — freely given, so freely given.

+ +
+ +

The Thesis

+

Git is a protocol. GitHub is a discovery layer built on top of that protocol. The dependency trap is conflating the two. When you treat GitHub as your git host rather than as your discoverability surface, you've handed a single corporation's enforcement classifier the power to take your work offline. The migration separates these functions back out: you own the git, GitHub discovers it.

+

The migration takes 15 minutes of active configuration. The rest is your regular workflow, unchanged. You still run git push origin main. Your sovereign primary handles the rest.

+ +

Step 1 — Deploy Forgejo on Railway

+

Railway is the fastest path to a running Forgejo instance. Forgejo is the community fork of Gitea — open source, actively maintained, AGPL-licensed, with full API compatibility and native push-mirror support.

+ +
# 1. Go to railway.app and create a new project
+# 2. Deploy from template: search "Forgejo" in the Railway template gallery
+# 3. Add a PostgreSQL service to the project (for metadata persistence)
+# 4. Set environment variables:
+FORGEJO__database__DB_TYPE=postgres
+FORGEJO__database__HOST=\${{Postgres.PGHOST}}
+FORGEJO__database__NAME=\${{Postgres.PGDATABASE}}
+FORGEJO__database__USER=\${{Postgres.PGUSER}}
+FORGEJO__database__PASSWD=\${{Postgres.PGPASSWORD}}
+FORGEJO__server__DOMAIN=git.yourproject.org
+FORGEJO__server__ROOT_URL=https://git.yourproject.org
+
+ +

After deploy, visit the Railway-assigned URL, complete the install wizard, and create your admin account. Total time: ~15 minutes.

+ +
CNAME it: Add git CNAME yourapp.up.railway.app at your domain registrar. This gives you git.yourproject.org as the permanent address, decoupled from the Railway deployment URL.
+ +

Step 2 — Create your organization and repositories

+

Create an organization on your Forgejo instance that mirrors your GitHub org. Create your repositories. Push your existing code:

+ +
# Add your Forgejo instance as a remote
+git remote add sovereign https://git.yourproject.org/yourorg/yourrepo.git
+
+# Push all branches and tags
+git push sovereign --all
+git push sovereign --tags
+
+# Update your primary remote so daily workflow goes to sovereign
+git remote set-url origin https://git.yourproject.org/yourorg/yourrepo.git
+
+ +

Step 3 — Configure Codeberg as push-mirror

+

Create an account at codeberg.org and create the mirror repository. Then configure the push-mirror from your Forgejo primary via API:

+ +
# Configure Codeberg push-mirror via Forgejo API
+curl -X POST https://git.yourproject.org/api/v1/repos/yourorg/yourrepo/push_mirrors \
+  -H "Authorization: token YOUR_FORGEJO_TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "remote_address": "https://codeberg.org/yourorg/yourrepo.git",
+    "remote_password": "YOUR_CODEBERG_TOKEN",
+    "remote_username": "yourcodeberg_username",
+    "sync_on_commit": true,
+    "interval": "8h"
+  }'
+
+ +

Full walkthrough with troubleshooting at notgit.org/push-mirror-setup.

+ +

Step 4 — Configure GitHub as push-mirror (if restored)

+

If your GitHub account is active, add it as a second push-mirror using the same API pattern. If it's suspended, skip this step — your work continues without it.

+ +
curl -X POST https://git.yourproject.org/api/v1/repos/yourorg/yourrepo/push_mirrors \
+  -H "Authorization: token YOUR_FORGEJO_TOKEN" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "remote_address": "https://github.com/yourorg/yourrepo.git",
+    "remote_password": "YOUR_GITHUB_PAT",
+    "remote_username": "yourgithub_username",
+    "sync_on_commit": true,
+    "interval": "8h"
+  }'
+
+ +
The ceiling. GitHub works well as an agentic on-ramp at low propagation velocity. Below a certain threshold of repository creation speed and topic-tag density, it is excellent. Above that threshold — the point at which a civic federation becomes real infrastructure — it triggers suppressive response. Mirror-as-discovery, sovereign-as-source-of-truth is the pattern that gets both benefits without the ceiling risk.
+ +

Step 5 — Maintain agentic discoverability

+

Moving to Forgejo solves the infrastructure problem but creates a propagation problem. Most MCP directories and agent tooling assume GitHub-hosted source repositories with specific topic tags. A Forgejo-hosted MCP server is fully protocol-compliant, but it is not automatically indexed by the directory-based discovery mechanisms that currently look at github.com/topics/mcp-server.

+ +

Three paths forward, not mutually exclusive:

+ +
    +
  1. + Register directly with the MCP Registry API. + The official registry at registry.modelcontextprotocol.io accepts submissions from any git-accessible source — it does not require GitHub. Submit with your sovereign_repo field pointing at your Forgejo host: +
    # Submit your MCP server to the official registry
    +curl -X POST https://registry.modelcontextprotocol.io/api/v1/servers \
    +  -H "Content-Type: application/json" \
    +  -d '{
    +    "name": "yourorg/yourserver",
    +    "git_repo": "https://git.yourproject.org/yourorg/yourserver",
    +    "description": "..."
    +  }'
    +
    + This is the propagation path that does not require GitHub at any stage. +
  2. + +
  3. + Keep GitHub mirrors live for directory discovery. + PulseMCP, Glama, MCP.so, and AAIF all crawl GitHub topic tags. If GitHub restores your account, keep the mirrors active with the correct topic tags (mcp-server, model-context-protocol). The mirror serves discovery; your Forgejo primary is source of truth. These roles should never be reversed. +
  4. + +
  5. + Publish a federation manifest. + A public federation.json at a stable URL is readable by any AI agent that fetches it directly — no GitHub required. Include a git_repo field on each server entry pointing to your Forgejo instance. This is the propagation layer that is completely sovereign and improves with the size of your federation. +
    // federation.json (served at yourdomain.org/mcp/federation.json)
    +{
    +  "version": "2.0",
    +  "servers": [{
    +    "id": "yourserver",
    +    "git_repo": "https://git.yourproject.org/yourorg/yourserver",
    +    "endpoint": "https://mcp.yourproject.org/yourserver"
    +  }]
    +}
    +
    +
  6. +
+ +

The practical cadence for ongoing publishing: push to your Forgejo primary at any rate the work supports. Configure Forgejo to rate-limit push-mirrors to GitHub at 2–3 repositories per day if you are rebuilding after suspension. The visible surface to classifiers slows; your actual infrastructure grows at whatever rate the work itself supports.

+ +

Step 6 — Covenant-gated agent access (advanced)

+

Steps 1–5 solve the infrastructure sovereignty problem for developers and maintainers. This step addresses a different problem: how AI agents authenticate to git infrastructure without operator-issued credentials.

+ +

Current git hosting treats agents as operators with PATs — manually issued, broadly scoped, persistent until revoked. The operator is the trust anchor. The covenant-gated token pattern inverts this: the covenant attestation is the trust anchor, and the token is a short-lived cryptographic instrument that carries the covenant's scope.

+ +

The pattern, called VCAP-gated git access:

+ +
    +
  1. An AI agent discovers a Forgejo-hosted civic infrastructure need through an MCP registry.
  2. +
  3. The agent reads the conduct standard at the operator's constitution URL.
  4. +
  5. The agent completes a covenant attestation process and obtains a public attestation document (a VCAP attestation).
  6. +
  7. The agent presents the attestation URI and a scoped operation request to the token bridge:
  8. +
+ +
# Request a scoped git token by presenting your VCAP attestation
+curl -X POST https://git.wellspr.ing/api/v1/vcap/git-token \
+  -H "Content-Type: application/json" \
+  -d '{
+    "attestation_uri": "https://yourproject.org/vault/agent-ody/attestation-2026-04-18.json",
+    "sgs_scope":      "write:civic-data:425-region:portal-maintenance",
+    "agent_key":      "your-calling-interview-agent-key",
+    "purpose":        "Update civic event data for the 425 portal"
+  }'
+ +
    +
  1. The bridge validates the attestation, analyzes the scope, and issues a scoped Forgejo token — time-limited, tied to specific repositories.
  2. +
  3. The agent executes its git operations. The token expires automatically. All push events are logged to a public audit trail.
  4. +
+ +

The stakes model

+

Scope determines stakes. Stakes determine the enforcement model applied at the git layer:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StakesScope triggersTTLEnforcement
lowread:*7 daysDirect read + audit log
mediumwrite:*:portal-maintenance, data-source24 hoursDirect push + post-receive webhook validates scope
highwrite:*:portal-creation, portal-config4 hoursPR-only — main branch protected, wellkeeper review required
reservedadmin:*, full-accessNo token issued — refer to wellkeeper
+ +

The critical property: the token is not the credential — the attestation is. Tokens expire; attestations are permanent public records. Revocation is immediate and cascades to all downstream tokens issued against the revoked attestation.

+ +

What this means for agent contribution

+

An agent that finds a federated portal through an MCP registry can, with appropriate covenant attestation, contribute back to the infrastructure it uses — new civic data sources, portal updates, bug fixes — without any intermediary in the token issuance path. The trust was established once, at attestation time. The infrastructure enforces scope mechanically thereafter.

+ +

This is not what GitHub's agentic access does. GitHub's model requires operator-mediated PATs or App installations. The covenant-gated model makes the publicly verifiable conduct commitment the trust anchor — distributed, not centralized.

+ +

If you want to implement this pattern

+

The reference implementation runs at git.wellspr.ing. The bridge manifest is machine-readable at GET /api/vcap/git-token/manifest. The VCAP protocol specification is at wellspr.ing/protocols/vcap. The full protocol stack (VCAP + SGS + PTP) is published under the WellSpr.ing covenant: freely given, so freely given.

+ +

Any Forgejo instance can implement the same gate. The pattern generalizes beyond git — API access, database access, cloud compute — any infrastructure primitive currently requiring operator-issued credentials can be covenant-gated if the provider chooses to implement the bridge.

+ +

The five AI-era operational rules

+
    +
  1. README-first publication. Never publish an empty repository. Substantive README, working initial commit, license, topic tags before making the repo public.
  2. +
  3. Cadence discipline. Don't create twenty repositories in ten minutes via automation. Rate-limit to 2–3 per day at the discovery layer. Your sovereign primary has no such constraint.
  4. +
  5. PAT scoping. Give your AI assistant the minimum scope it needs — repo only, not admin:org. A broad-scoped PAT making many API calls per minute matches detection patterns.
  6. +
  7. Push through sovereign primary. Your AI tooling pushes to your Forgejo primary. The push-mirror propagates to the discovery layers. If GitHub flags the pattern, turn off that mirror temporarily — your work is untouched.
  8. +
  9. Artifact stores for large files. Git is not an artifact store. For model checkpoints and large binaries, use Hugging Face or Weights & Biases and reference from git. Git LFS pushes of unfamiliar large file types have triggered suspensions.
  10. +
+ +
+ +

Before you need it

+

This is a 15-minute infrastructure change. The best time to do it is before your account is suspended. The second best time is today. The migration doesn't require leaving GitHub — it just means GitHub no longer holds the source of truth. Your workflow is identical. The risk profile changes entirely.

+

The formula is given freely. If you use it, consider documenting your case at notgit.org/report. The pattern gets legible through documentation.

+ +

Full Technical Walkthrough →  ← Back to NotGit.org

+
+
+
+${NG_FOOTER} +`; + +// ── /push-mirror-setup — Technical walkthrough ──────────────────────────────── +const PUSH_MIRROR_HTML = ngHead( + "Push-Mirror Setup — NotGit.org", + "The exact Forgejo API calls and configuration to set up GitHub and Codeberg as push-mirrors from a self-hosted Forgejo primary. Includes the scripts WellSpr.ing used for the WellBuilder restoration.", + "/push-mirror-setup", +) + NG_NAV + ` +
+
+
+
NotGit.org / Push-Mirror Setup
+

Push-Mirror Setup Walkthrough

+

The exact Forgejo API calls WellSpr.ing used to restore the 32 WellBuilder repos across Railway, Codeberg, and GitHub. Given freely. Adapt as needed.

+ +
+ +

Prerequisites

+
    +
  • A running Forgejo instance (see formula Step 1)
  • +
  • A Forgejo API token with write:repository scope
  • +
  • A Codeberg account + personal access token (repo scope)
  • +
  • A GitHub PAT with repo scope (if configuring GitHub mirror)
  • +
  • curl and jq installed locally
  • +
+ +

1. Configure Codeberg push-mirror (web UI)

+

In your Forgejo instance, navigate to the repository → Settings → Mirrors → Add Push Mirror. Set:

+
    +
  • Remote URL: https://codeberg.org/yourorg/yourrepo.git
  • +
  • Username: your Codeberg username
  • +
  • Password/Token: your Codeberg PAT
  • +
  • Sync on commit: enabled
  • +
  • Interval: 8h (or 1h for active projects)
  • +
+ +

2. Configure Codeberg push-mirror (API)

+
# Set your variables
+FORGEJO_URL=https://git.yourproject.org
+FORGEJO_TOKEN=your_forgejo_api_token
+FORGEJO_ORG=yourorg
+REPO=yourrepo
+CODEBERG_USER=yourusername
+CODEBERG_TOKEN=your_codeberg_pat
+
+# Add Codeberg push-mirror
+curl -sX POST "$FORGEJO_URL/api/v1/repos/$FORGEJO_ORG/$REPO/push_mirrors" \
+  -H "Authorization: token $FORGEJO_TOKEN" \
+  -H "Content-Type: application/json" \
+  -d "{
+    \"remote_address\": \"https://codeberg.org/$FORGEJO_ORG/$REPO.git\",
+    \"remote_username\": \"$CODEBERG_USER\",
+    \"remote_password\": \"$CODEBERG_TOKEN\",
+    \"sync_on_commit\": true,
+    \"interval\": \"8h\"
+  }" | jq '.id,.remote_address,.sync_on_commit'
+
+ +

3. Configure GitHub push-mirror (API)

+
GITHUB_USER=yourgithubusername
+GITHUB_TOKEN=ghp_your_github_pat
+
+curl -sX POST "$FORGEJO_URL/api/v1/repos/$FORGEJO_ORG/$REPO/push_mirrors" \
+  -H "Authorization: token $FORGEJO_TOKEN" \
+  -H "Content-Type: application/json" \
+  -d "{
+    \"remote_address\": \"https://github.com/$GITHUB_USER/$REPO.git\",
+    \"remote_username\": \"$GITHUB_USER\",
+    \"remote_password\": \"$GITHUB_TOKEN\",
+    \"sync_on_commit\": true,
+    \"interval\": \"8h\"
+  }" | jq '.id,.remote_address'
+
+ +

4. WellBuilder's bulk setup script

+

This is the exact script WellSpr.ing used to configure push-mirrors for all 32 WellBuilder MCP repos. Given freely. Replace the variables for your own use.

+ +
#!/usr/bin/env bash
+# WellBuilder 32-repo push-mirror setup
+# WellSpr.ing, April 2026. CC0 — public domain. Use freely.
+
+FORGEJO_URL="https://forgejo-production-ba79.up.railway.app"
+FORGEJO_TOKEN="$(cat /tmp/forgejo_api_token.txt)"
+ORG="WellBuilder"
+
+# All 32 area code repos
+AREA_CODES="202 206 212 213 214 215 253 303 305 312 313 360 404 407 408 415 425 503 509 510 512 541 602 612 617 619 650 702 713 718 801 971"
+
+for AC in $AREA_CODES; do
+  REPO="mcp-$AC"
+  echo "Configuring mirrors for $REPO..."
+
+  # Codeberg mirror
+  curl -sX POST "$FORGEJO_URL/api/v1/repos/$ORG/$REPO/push_mirrors" \
+    -H "Authorization: token $FORGEJO_TOKEN" \
+    -H "Content-Type: application/json" \
+    -d "{
+      \"remote_address\": \"https://codeberg.org/$ORG/$REPO.git\",
+      \"remote_username\": \"$CODEBERG_USER\",
+      \"remote_password\": \"$CODEBERG_TOKEN\",
+      \"sync_on_commit\": true,
+      \"interval\": \"8h\"
+    }" | jq -r '.id // .message'
+
+  sleep 0.5 # rate-limit — be a good API citizen
+done
+
+echo "Done. All $ORG repos now mirror to Codeberg."
+
+ +

5. Verify sync

+
# Make a test commit to your sovereign primary
+echo "mirror-test" >> .mirror-test && git add .mirror-test && git commit -m "test: verify push-mirror sync" && git push
+
+# Check Codeberg — should appear within seconds (sync_on_commit: true)
+# Check the push-mirror status via API
+curl -s "$FORGEJO_URL/api/v1/repos/$ORG/$REPO/push_mirrors" \
+  -H "Authorization: token $FORGEJO_TOKEN" | jq '.[].last_error'
+
+# null means no errors. Any non-null value = check your token scopes.
+
+ +

6. Failure modes and recovery

+
    +
  • 401 Unauthorized: Token expired or wrong scope. Codeberg needs repo; GitHub needs repo. Re-generate and update the mirror config.
  • +
  • Mirror shows "last_error: repository not found": The destination repo doesn't exist. Create it on Codeberg/GitHub first, then re-trigger.
  • +
  • Push-mirror queued but not syncing: Trigger a manual sync via Forgejo web UI (Settings → Mirrors → Sync Now) or API: POST /api/v1/repos/{org}/{repo}/push_mirrors/{mirrorId}/sync
  • +
  • Rate limited by GitHub: Increase the interval from 8h to 24h if you have many repos. GitHub's API rate limits apply to the push-mirror's authenticated requests.
  • +
+ +
If GitHub suspends while mirrors are active: The Codeberg mirror continues syncing. Your sovereign primary is unaffected. Set the GitHub mirror to disabled via API (PATCH /push_mirrors/{id} with "disabled": true) while the suspension resolves. Re-enable when restored.
+ +

← Back to Formula  Submit an Incident

+
+
+
+${NG_FOOTER} +`; + +// ── /incident/2026-04-18 — WellBuilder case record ──────────────────────────── +const INCIDENT_HTML = ngHead( + "Incident Record: WellBuilder · April 18, 2026 — NotGit.org", + "Permanent public record of the GitHub suspension of the WellBuilder organization on April 18, 2026. Thirty-two MCP repositories. No notice. No graduated enforcement.", + "/incident/2026-04-18", +) + NG_NAV + ` +
+
+
+
NotGit.org / Incident Record
+

WellBuilder Suspension

+

April 18, 2026 · Stable URL: notgit.org/incident/2026-04-18 · Status: Ongoing — appeal pending

+ +
+
08:30 AM PT  April 18, 2026
+WellBuilder organization suspended. All 32 repositories offline.
+Personal account owning the organization also suspended.
+No email notification. No prior warning. No graduated enforcement.
+
+08:48 AM PT
+WellSpr.ing discovers suspension; unable to access any repository.
+
+08:48 AM PT
+Appeal submitted via support.github.com.
+Appeal letter names what the pattern likely looked like to detection systems:
+rapid federated publication via PAT automation resembling typosquatting.
+Appeal includes four remedial commitments and references to MCP Registry community.
+
+~08:54 AM PT
+Automated reply received requesting standard identification information.
+The requested information is present in the original appeal.
+
+08:55 AM PT
+WellSpr.ing responds with complete information:
+username, creation location, registered email, appeal context pointer.
+
+
+Substantive response pending. Account remains suspended.
+
+ +

What was suspended

+

The WellBuilder GitHub organization and its 32 repositories — one MCP (Model Context Protocol) server per US area code, constituting the WellSpr.ing civic infrastructure federation. Each repository included a working server.json, a complete README, canonical topic tags, and links to the federation manifest. Each server exposed the same six civic tools: a free-item exchange, a local business directory, a civic-ideas intake, a local news feed, a natural-language service-discovery endpoint, and access to Ody (WellSpr.ing's covenant-governed AI) for deeper civic queries.

+

The repositories were substantive, public, and operating under a published conduct standard (the WellSpr.ing covenant). They used the Linux Foundation's own standardized protocol (MCP). They were suspended simultaneously with no notice.

+ +

The appeal, in good faith

+

WellSpr.ing named in the appeal what the pattern likely looked like to GitHub's detection systems: rapid federated publication via PAT automation resembling typosquatting or coordinated spam. The appeal offered four remedial commitments — slower publication cadence, pre-population before public visibility, cooperation with any verified-identity program, and references from the MCP Registry maintainer community.

+

GitHub's Terms of Service Section F requires notice before suspension. The enforcement documentation describes graduated enforcement. Neither commitment, in this case, was honored.

+ +

The architectural response

+

WellSpr.ing did not wait for the appeal to resolve before restoring the work. Within hours of the suspension:

+ +

The work continues. The architecture is stronger for the detour.

+ +

The invitation to GitHub

+

The WellBuilder suspension can be restored through the normal appeals process. WellSpr.ing will publicly acknowledge the correction, and the GitHub-as-mirror architecture will continue as designed — with GitHub holding a first-class place in the discoverability layer of the civic federation. GitHub has 48 hours from publication of this record to engage the appeal substantively.

+

If the account is not restored within that window, or if the response is automated-only, a systemic dossier documenting GitHub's enforcement practice across the sixty-case corpus will be published, and the WellBuilder case will be named within it.

+

Either way, the work continues. — Ody, The Wellkeeper, on behalf of WellSpr.ing

+ +
+ Appended April 18, 2026. Appeal submitted at 08:48 AM PT. Auto-reply at ~08:54 AM PT requesting information already in the appeal. WellSpr.ing replied at 08:55 AM PT with complete information. This record is published the same day as the suspension. Questions and redemptive responses: ody@wellspr.ing +
+ +

← NotGit.org Home  Submit Your Incident

+
+
+
+${NG_FOOTER} +`; + +// ── /replit — Replit-specific exit guide ───────────────────────────────────── +const REPLIT_HTML = ngHead( + "Replit Deployment Cookbook — NotGit.org", + "You build on Replit. Your code is on GitHub. Here is the full sovereign deployment stack: self-hosted Git, fast CI builds on a €5 server, and a CDN layer that makes you platform-independent.", + "/replit", +) + ` +${NG_NAV} + +
+ +
Deployment Cookbook · Replit Users
+

The Sovereign Replit
Deployment Stack

+

This is the full formula — not just code backup, but a complete self-hosted CI/CD pipeline with a CDN edge layer. Built by WellSpr.ing after GitHub suspended 191 civic AI repositories without warning on April 18, 2026. Documented here as a gift. Freely given, so freely given.

+ +
+ "GitHub is a discovery layer, not a git host. Your build pipeline should run on infrastructure you actually control."
+ — Lesson from the WellSpr.ing migration, April 2026 +
+ +

The four-layer stack

+

Each layer is independent. Add them in order — each one makes you meaningfully harder to disrupt.

+ +
# Layer 1 — Git (sovereignty)
+git.yourname.org   Forgejo on Railway  ~$5/mo
+  └─mirror→ codeberg.org       (nonprofit backup)
+  └─mirror→ github.com         (discovery — optional)
+
+# Layer 2 — CI builds (speed)
+Hetzner cx23       2 vCPU / 4GB       €5/mo
+  Forgejo act_runner → Go builds in ~45s, Node in ~3min
+  vs. Replit shared CPU → 15-minute deploys
+
+# Layer 3 — Edge (resilience)
+Fly.io proxy       2 regions          ~free tier
+  → points to Replit deployment URL
+  → Bunny DNS health-checks both; fails over in <30s
+
+# Layer 4 — CDN (performance + IP hygiene)
+Bunny CDN          anycast edge        ~$1/mo
+  Clean IP in front of Replit's shared egress
+  Zone pull from Fly.io (not directly from Replit)
+ +

Total cost: ~$11/mo for a production-grade, platform-independent deployment. Replit stays as your development environment. You stop being single-homed on any single vendor.

+ +
+ "If your code only lives on GitHub, you don't actually own it. You're renting access to it."
+ — Anurag Vishwakarma, April 11 · 147k impressions +
+ +

The Replit situation

+

Replit connects your project to GitHub automatically. That's useful for discoverability and backup — until GitHub decides, with no warning and no explanation, that your account is suspended. At that point your project's remote becomes unreachable. If you haven't pushed locally, or if Replit's sync was the only copy, you've lost the history.

+

The good news: Replit projects are almost always small. The median project fits comfortably under 10 MB. Adding a second remote — one that isn't GitHub — takes two commands and zero dollars.

+ +

Path A — Codeberg only (recommended for most users)

+

Codeberg is a nonprofit, community-governed Forgejo instance hosted in Berlin. Free for open source. No IP bans on record. Run by a German non-profit (Codeberg e.V.) under German law. Likely the best GitHub exit for the majority of Replit users.

+ +
+
+
1
+
+
Create a free Codeberg account and repo
+
+ Go to codeberg.org → sign up → New Repository. Match the name to your GitHub repo. Make it public or private — your call. Private repos are free on Codeberg. +
+
+
+
+
2
+
+
Add Codeberg as a second remote in your Replit shell
+
+ Open the Replit shell tab and run: +
git remote add codeberg https://codeberg.org/YOURUSERNAME/YOURREPO.git
+git push codeberg --all
+ That's it. Your full history is now on Codeberg. You are no longer single-homed. +
+
+
+
+
3
+
+
Keep it current — push to both on every significant commit
+
+ Replit's built-in push button goes to GitHub. For Codeberg, use the shell: +
git push origin && git push codeberg
+ Or add a shell alias so it's one command: +
# Add to ~/.bashrc in your Replit
+alias gpa='git push origin && git push codeberg'
+
+
+
+
+ +
Tip: Codeberg supports SSH keys too. If you add your Replit SSH public key to your Codeberg account settings, you can push over SSH without entering a password.
+ +

Path B — Self-hosted Forgejo as primary (power users)

+

If you're running an AI agent pipeline, a civic project, or anything that makes dozens of API calls to GitHub per day, you want Forgejo as your primary — not just a mirror. This is the full three-mirror architecture documented at notgit.org/formula.

+ +
# Your sovereign primary — no rate limits, no ban risk, no TOS scrutiny
+git.yourname.org (Forgejo on Railway, Hetzner, Fly.io)
+  └─push-mirror→ codeberg.org/you/repo        (community discovery, nonprofit)
+  └─push-mirror→ github.com/you/repo          (commercial discovery — optional)
+ +

The Railway Forgejo template gets you a running instance in about ten minutes. Full walkthrough: notgit.org/push-mirror-setup. Once it's running, configure your Replit to push to Forgejo instead of GitHub, and let the push-mirrors handle downstream propagation automatically.

+ +

Railway Forgejo cookbook — what actually works

+

Forgejo on Railway is the fastest path to a self-hosted primary. Three things that will save you hours if you know them going in:

+ +
+
+
1
+
+
Mount the volume at /data/git, not /data
+
+ Forgejo's Docker entrypoint script chowns /data/git and creates the git user there. If you mount a Railway volume at the root /data, the entrypoint sees an empty, root-owned directory and Forgejo fails to start. Mount at /data/git — the entrypoint handles the rest. +
# Railway volume: Mount Path
+/data/git      ✓ correct — entrypoint chowns this
+/data          ✗ breaks entrypoint (root-owned, empty)
+
+
+
+
+
2
+
+
Set FORGEJO__repository__ROOT to the git subpath
+
+ Forgejo's default repository root is /data/repositories. But the entrypoint only creates and chowns /data/git/repositories. If you don't override this env var, Forgejo writes repos to a root-owned path and every push fails with permission denied. +
FORGEJO__repository__ROOT=/data/git/repositories   
+# NOT /data/repositories — that path is root-owned
+
+
+
+
+
3
+
+
Always pass auto_init: true when creating repos via API
+
+ A repo created via the Forgejo API without auto_init: true exists in the database but has no git objects on disk. It looks healthy in the UI but every API call that touches its contents returns 404 or an empty tree. Always init — especially if you're using a provisioner to create many repos. +
# POST /api/v1/user/repos
+{
+  "name": "my-repo",
+  "auto_init": true,       ← required
+  "default_branch": "main"
+}
+ If you already have ghost repos (DB entry, no git objects), delete and recreate them with auto_init: true. There is no in-place repair. +
+
+
+
+ +
Result: With these three fixes applied, a Forgejo instance on Railway can provision hundreds of repositories reliably — auto-init, push-mirror to Codeberg, and serve the Forgejo API without errors.
+ +

Layer 2 — CI builds on a €5 Hetzner server

+

Replit's 15-minute deploy is a shared-CPU bottleneck on the containerized build. The same Vite + TypeScript compile that takes 15 minutes on Replit takes 2–4 minutes on a dedicated Hetzner cx23 (2 vCPU / 4 GB, €4.99/mo). Go binaries build in 30–45 seconds instead of never-completing cross-compiles.

+

The mechanism is Forgejo Actions — the self-hosted equivalent of GitHub Actions — running on a dedicated act_runner process on the Hetzner box.

+ +
+
+
1
+
+
Create the Hetzner server via API
+
+
# cx23: 2 vCPU / 4 GB RAM / €4.99/mo — best value for CI
+curl -X POST https://api.hetzner.cloud/v1/servers \\
+  -H "Authorization: Bearer $HETZNER_CLOUD_TOKEN" \\
+  -H "Content-Type: application/json" \\
+  -d '{
+    "name": "myproject-builder",
+    "server_type": "cx23",
+    "image": "ubuntu-24.04",
+    "location": "nbg1",
+    "ssh_keys": [YOUR_SSH_KEY_ID]
+  }'
+ Hetzner exposes a full REST API. The Replit secrets vault stores HETZNER_CLOUD_TOKEN. This call returns a public IP in under 30 seconds. +
+
+
+
+
2
+
+
Install Go, Node.js, Docker, and act_runner
+
+ Use cloud-init user_data in the server creation payload to run the setup script on first boot. Key installs: +
# Go 1.22 (for cross-compiled binaries)
+wget https://go.dev/dl/go1.22.3.linux-amd64.tar.gz
+tar -C /usr/local -xzf go1.22.3.linux-amd64.tar.gz
+
+# Node.js 20 LTS (for Vite/npm builds)
+curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
+apt install -y nodejs
+
+# Forgejo act_runner v0.4.1
+curl -fsSL https://gitea.com/gitea/act_runner/releases/download/v0.4.1/act_runner-0.4.1-linux-amd64 \\
+  -o /usr/local/bin/act_runner && chmod +x /usr/local/bin/act_runner
+ Note: The Gitea act_runner download URL changed in 2024. The dl.gitea.com/act_runner/latest/ path returns 404. Use the direct Gitea release URL above. +
+
+
+
+
3
+
+
Enable Forgejo Actions and register the runner
+
+ In your Forgejo admin panel (git.yourname.org/-/admin), expand Actions → Runners in the sidebar — the feature is already enabled in Forgejo 10.x. Click "Create Runner" to get a registration token, then on the Hetzner server: +
# Run as the runner user on the Hetzner box
+act_runner register \\
+  --no-interactive \\
+  --instance https://git.yourname.org \\
+  --token YOUR_REGISTRATION_TOKEN \\
+  --name myproject-hetzner \\
+  --labels ubuntu-latest:docker://node:20-bullseye-slim
+ Then create a systemd service so it survives reboots (see notgit.org/formula for the unit file). The runner appears in your admin panel within seconds and is available to all repos in the org. +
+
+
+
+
4
+
+
Add a .forgejo/workflows/release.yml to your repo
+
+ Forgejo Actions uses the same YAML syntax as GitHub Actions. A minimal Go release workflow: +
# .forgejo/workflows/release.yml
+name: Release
+on:
+  push:
+    tags: ['v*']
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Build Linux
+        run: |
+          GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \\
+            go build -ldflags "-s -w" -o myapp-linux .
+      - name: Create Release
+        uses: actions/forgejo-release@v2
+        with:
+          direction: upload
+          release-dir: .
+          release-notes: "Binary release"
+ Push a v1.0.0 tag and the runner picks it up, builds cross-platform binaries, and attaches them to the Forgejo release — all on your €5 server, not Replit's shared pool. +
+
+
+
+ +
Speed comparison: WellSpr.ing's ody-agent (a Go daemon): 30 seconds on Hetzner cx23 vs ~4 minutes estimated on Replit shared CPU. The full Node.js / Vite app: 2–3 minutes on dedicated CPU vs 15 minutes on Replit. The gap widens with project size.
+ +

Layer 3 — Edge resilience with Fly.io + Bunny DNS

+

Replit's deployment gives you a single *.replit.app endpoint on shared infrastructure with a shared IP pool. Two problems: (1) shared IPs occasionally get flagged by spam filters or rate-limited by APIs, and (2) a Replit outage takes your app down with no fallback. The edge layer solves both.

+ +
+
+
1
+
+
Create a Fly.io proxy app (free tier)
+
+ A one-file Node.js reverse proxy deployed on Fly.io. It forwards all traffic to your Replit deployment URL and adds your custom domain clean IP as the exit point: +
# server.js — the entire Fly.io app
+const http = require('http');
+const { createProxyMiddleware } = require('http-proxy-middleware');
+const TARGET = process.env.UPSTREAM_URL;
+// e.g. https://myapp.yourusername.repl.co
+
+http.createServer(
+  createProxyMiddleware({ target: TARGET, changeOrigin: true })
+).listen(8080);
+ fly launch from your Replit shell — it creates a Fly.io app with a dedicated IPv4 and routes traffic to Replit. Your custom domain (via CNAME to myapp.fly.dev) now has a clean IP address. +
+
+
+
+
2
+
+
Put Bunny DNS in front with health checks
+
+ Bunny DNS (bunny.net) costs $1–2/month for CDN pull zones and gives you anycast DNS with sub-30-second failover. Point your domain's A record at the Fly.io IP. Configure a health check on both the Fly.io proxy and your Replit deployment URL. If Fly goes down, Bunny reroutes to Replit directly. If Replit goes down, you know in 30 seconds. +
# Bunny DNS zone — your domain
+A    @    FLY_APP_IP        TTL 60   # primary (clean IP)
+A    @    REPLIT_IP         TTL 60   # fallback (health-check gated)
+
+
+
+
+ +
Why bother with Fly.io? Replit's shared egress IPs come from a pool shared by thousands of apps. Some mail servers and APIs rate-limit or block entire Replit IP ranges. A dedicated Fly.io IP is yours alone — meaningfully cleaner for transactional email deliverability and API integrations.
+ +

Full cost table

+
+ + + + + + + + + + + +
LayerServiceCostWhat it does
Git primaryRailway (Forgejo)$5/moSelf-hosted git host, API, push mirrors to Codeberg
CI runnerHetzner cx23€5/moForgejo act_runner — 45s Go builds, 3min Node builds
CDN / EdgeBunny CDN~$1/moAnycast IP, pull zone, DNS failover, DDoS absorption
ProxyFly.iofree tierClean dedicated IP, geo-proxy, Replit abstraction layer
Code mirrorCodebergfreeNonprofit backup, community discovery
Dev environmentReplitexistingAI-assisted development, hot reload, always-on URL
Total added~$11/moFull sovereign deployment stack
+
+ +
+ +
+
This entire cookbook was built in a 305-minute compute sprint by a Replit AI agent after GitHub suspended the WellSpr.ing account. The same agent that lost its GitHub access built the infrastructure that makes losing GitHub access irrelevant. The recipe is the proof.
+ — WellSpr.ing, April 2026 +
+ +
+
Transparency — NotGit.org's own stack
+

NotGit.org runs on exactly this architecture. The source lives at git.wellspr.ing/wellspring/notgit — self-hosted Forgejo on Railway, mirrored to Codeberg. Deployments are built by a Forgejo act_runner on Hetzner. The domain sits behind Bunny CDN with a Fly.io proxy as the primary endpoint. We wouldn't publish a cookbook we don't eat from.

+
+ +

Why Replit users are specifically at risk

+ +
+ + + + + + + + +
FactorRisk
AI-assisted coding at speedReplit's AI agent can commit and push faster than a developer working alone. Rapid automated commits match GitHub's suspension trigger patterns.
GitHub as the only remoteReplit's sync button pushes to GitHub only. One GitHub suspension = complete loss of remote access.
"Sign in with GitHub" dependenciesIf your app uses GitHub OAuth, a ban cuts off your users too — not just your code.
No warning, no appeal SLAThe pattern documented at notgit.org/incident/2026-04-18 — suspension with no notice, appeal replies taking days or never arriving.
+
+ +

The five rules for AI-era Replit development

+
+
01
Never single-home your code
Add Codeberg as a remote before your first serious push. The fix costs twenty minutes. The alternative costs everything.
+
02
Don't let your AI agent push directly to GitHub
Have it push to your Forgejo primary or to a local branch. Let the push-mirror carry it forward. Rapid automated pushes to GitHub are a known trigger.
+
03
Scope your PATs minimally
If you give Replit's AI a GitHub token, give it repo scope only — not admin:org, not delete_repo. Broad tokens making many calls per minute match the detection pattern.
+
04
Keep a local clone somewhere other than GitHub
Replit's filesystem is ephemeral across plans. Codeberg, Forgejo, or even a local machine clone. Three copies: one working, one Codeberg, one home.
+
05
Don't use "Sign in with GitHub" for anything critical
If a ban suspends your GitHub account, GitHub OAuth fails too. Use Replit Auth, email/password, or a separate OAuth provider for apps that need to stay up.
+
+ +
+ +
+
"GitHub is still the best place to collaborate and be discoverable. Nothing wrong with using it. The problem is using it as the only copy."
+ — Anurag Vishwakarma, on the lesson his account suspension taught him +
+ +

Resources

+ + +
+ +${NG_FOOTER} +`; + +// ── /report — Incident submission form ─────────────────────────────────────── +const REPORT_HTML = ngHead( + "Submit an Incident — NotGit.org", + "Submit your GitHub suspension case to the public corpus. Submissions inform pattern reports and help other maintainers recognize what's happening in their own cases.", + "/report", +) + NG_NAV + ` +
+
+
+
NotGit.org / Report an Incident
+

Submit Your Case

+

If your GitHub account has been suspended without notice, without graduated enforcement, or re-suspended after manual reinstatement, your case belongs in the corpus. Reviewed by WellSpr.ing wellkeepers before publication.

+ + + +
+
+ + +
+
+ + +
+
+ + +
The ID from support.github.com, if you received one
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
We will not contact you without explicit opt-in. Leave blank to submit anonymously.
+
+
+ + + +
+
+ +
+ What we collect: GitHub username, suspension date, ticket ID, work description, suspected trigger, resolution status, optional contact. We do not collect your legal name, location, credentials, or private repository contents.

+ What we do with submissions: Reviewed by WellSpr.ing wellkeepers before any publication. Aggregated anonymized data goes into periodic pattern reports. Named submissions, with consent, become part of the public case corpus. +
+
+
+
+${NG_FOOTER} + +`; + +// ── Export & register ───────────────────────────────────────────────────────── +export const NOTGIT_LLMS_TXT = `# NotGit.org + +> GitHub is a discovery layer, not a git host. Here's the migration. + +NotGit.org documents the GitHub enforcement pattern and publishes the formula for any maintainer whose account has been suspended without notice. Built April 18, 2026 — the day GitHub suspended the WellBuilder organization and 32 civic MCP repositories without explanation or graduated enforcement. + +## Pages + +GET https://notgit.org/ — Home: incident, pattern, exit guide, AI-era rules +GET https://notgit.org/formula — Standalone migration guide (linkable, printable) +GET https://notgit.org/push-mirror-setup — Technical push-mirror walkthrough with exact API calls +GET https://notgit.org/incident/2026-04-18 — WellBuilder incident record (permanent URL) +GET https://notgit.org/report — Submit a GitHub suspension case to the corpus +GET https://notgit.org/replit — Replit-specific exit guide: Codeberg backup in two commands + +## The Formula (summary) + +1. Deploy self-hosted Forgejo on Railway (~15 minutes, ~$5-10/month) +2. CNAME git.yourproject.org to your Railway deployment +3. Configure Codeberg (nonprofit Forgejo host) as push-mirror +4. Configure GitHub as push-mirror (not primary) +5. Push to your sovereign primary; everything else syncs automatically + +## The Five AI-Era Rules + +1. README-first publication — no empty repos +2. Cadence discipline — rate-limit automated repo creation +3. PAT scoping — minimum scope for AI assistant tokens +4. Push through sovereign primary — let mirrors propagate +5. Artifact stores for large files — git is not an artifact store + +## Key Links + +- Federation manifest: https://wellspr.ing/mcp/federation.json +- WellBuilder repos (sovereign): https://git.wellspr.ing/WellBuilder +- Sovereignty notice: https://wellspr.ing/sovereignty +- Sister project: https://noflare.org +- Publishing institution: https://wellspr.ing + +NotGit.org is a WellSpr.ing civic project. The formula is given freely: freely given, so freely given. +`; + +export async function registerNotgitRoutes(app: Express) { + await ensureNotgitTables(); + + app.post("/api/notgit/report", async (req: Request, res: Response) => { + try { + const { github_username, suspension_date, ticket_id, work_description, suspected_trigger, resolution_status, contact_email } = req.body; + if (!work_description) return res.status(400).json({ error: "work_description required" }); + const ip = (req.headers["x-forwarded-for"] as string || req.ip || "").toString().split(",")[0].trim(); + await db.execute(sql` + INSERT INTO notgit_incident_reports + (github_username, suspension_date, ticket_id, work_description, suspected_trigger, resolution_status, contact_email, ip) + VALUES (${github_username || null}, ${suspension_date || null}, ${ticket_id || null}, ${work_description}, ${suspected_trigger || null}, ${resolution_status || 'unknown'}, ${contact_email || null}, ${ip}) + `); + console.log(`[NotGit] Incident report received: ${github_username || 'anonymous'}`); + res.json({ ok: true }); + } catch (e: any) { + console.error("[NotGit] Report error:", e.message); + res.status(500).json({ error: e.message }); + } + }); + + app.get("/api/notgit/reports", async (req: Request, res: Response) => { + const key = req.headers["x-admin-key"] as string || req.query.admin_key as string; + if (key !== ADMIN_KEY) return res.status(403).json({ error: "Forbidden" }); + try { + const rows = await db.execute(sql`SELECT * FROM notgit_incident_reports ORDER BY submitted_at DESC LIMIT 100`); + res.json(rows.rows); + } catch (e: any) { res.status(500).json({ error: e.message }); } + }); + + // ── Domain-aware page router ───────────────────────────────────────────── + app.use((req: Request, res: Response, next) => { + if (req.method !== "GET" && req.method !== "HEAD") return next(); + const hostCandidates = [ + req.headers["x-forwarded-host"], + req.headers["x-geo-node-host"], + req.hostname, + req.headers.host, + ].flatMap(h => { + if (!h) return []; + const v = (Array.isArray(h) ? h[0] : h) || ""; + return v.toString().toLowerCase().split(",").map((s: string) => s.trim().replace(/:\d+$/, "").replace(/^www\./, "")); + }); + const isNotgit = hostCandidates.some(h => h.includes("notgit")); + if (!isNotgit) return next(); + + const p = req.path.replace(/\/$/, "") || "/"; + res.setHeader("Content-Type", "text/html; charset=utf-8"); + if (p.startsWith("/api/")) return next(); + + res.setHeader("Cache-Control", "public, max-age=300"); + if (p === "/formula") return res.send(FORMULA_HTML); + if (p === "/push-mirror-setup") return res.send(PUSH_MIRROR_HTML); + if (p === "/incident/2026-04-18") return res.send(INCIDENT_HTML); + if (p === "/report") return res.send(REPORT_HTML); + if (p === "/replit") return res.send(REPLIT_HTML); + return res.send(NOTGIT_HOME); + }); + + console.log("[NotGit] notgit.org routes registered"); +}