Enginecy · Internal engineering guide
The Build Agent turns a site request into a live subdomain.
Open this repo in Claude Code on any machine and you're talking to the agent. It creates the repo, art-directs and builds the site, deploys to Cloudflare, and wires the subdomain — end to end, no dashboard clicks. The same folder is the headless runner's brain, so interactive and automated work share one source of truth.
The pipeline
// 01 · the stack
What it runs on
Deterministic Node scripts do the mechanical work; Claude does the judgment (design, copy, decisions). GitHub and Cloudflare are reached through org-owned credentials — no personal tokens.
| Layer | Technology | Notes |
|---|---|---|
| Agent runtime | Claude Code (CLI / app) | The folder is the agent; CLAUDE.md is its constitution |
| Automation | Node 20+ ESM (.mjs) | Scripts in scripts/ — no framework |
| Source control | GitHub · org Enginecy | Every site is its own private repo Enginecy/<slug> |
| GitHub auth | GitHub App | "Enginecy Site Pipeline" — org-owned, auto-rotating tokens. No PATs |
| Hosting · Lane A | Cloudflare Pages | Static / Jamstack / 3D (wrangler via npx) |
| Hosting · Lane B | Dokploy | dokploy2.enginecy.cloud — apps needing a server / DB |
| Design DB | ui-ux-pro-max | 67 styles · 161 palettes · 57 font pairings · 21 stacks |
| Pre-deploy gate | site-auditor subagent | Renders the build in a headless browser; blocks deploy on AA-contrast / a11y / slop failures |
| AI imagery | Pollinations | Real photos + scroll-world video, generated at build time (POLLINATIONS_API_KEY) |
| Registry / alerts | NocoDB · Telegram · Discord | Optional — skipped if unconfigured |
Templates
Quick · Lane A
astro-marketing, astro-content
Bespoke · Lane A
bespoke-astro, three-experience, scroll-world
Apps · Lane B
next-app, webapp-fullstack
// 02 · setup
One time, per machine
Clone, run setup, then supply the two secret pieces from the team vault. Everything else travels in the repo.
git clone https://github.com/Enginecy/build-agent && cd build-agent
./setup.sh # deps + pinned design DB; creates .env
- Fill
.envCopy values from the vault: GitHub App IDs + key path, Cloudflare tokens/account IDs (mo & client), optional NocoDB / Telegram / Discord.
- Add the App key
Drop the GitHub App
.peminto./secrets/and pointGITHUB_APP_PRIVATE_KEY_FILEat it (exact filename, incl..pem). - Set your git identity
Once — this is what puts your name on commits (see §10).
git config --global user.name "Your Name"
git config --global user.email "<id>+<user>@users.noreply.github.com"
./secrets/*.pem and
work/<slug> resolve against the current directory. .env auto-loads — no --env-file flag needed.// 03 · mental model
Three repos, one working copy
Keeping these straight removes nearly every point of confusion.
Enginecy/build‑agent
This folder — the agent's brain (scripts, skills, commands). Clone once; git pull for updates.
Enginecy/website‑template
The template catalog. Never edited directly — scaffold copies a template out of it.
Enginecy/<slug>
The actual website — the source of truth for a site. Shared by the whole team.
A site's code does not live in this folder. To work on a site you bring it local into
work/<slug> (a git-ignored scratch area). That checkout is your working copy — edit it, build it, push it back.
Enginecy/<slug> ──pull──▶ work/<slug> ──edit·build·push──▶ Enginecy/<slug> ──deploy──▶ live
remote, your local remote,
shared truth working copy updated
// 04 · scripts & commands
Your two toolbars
Slash commands (in Claude Code)
| /new-site | Full net-new build, from repo creation to a live subdomain |
| /edit-site | Pull → edit → build → push → deploy an existing site |
| /art-direct | Run only the design flow on work/<slug> |
| /deploy | Build + push + deploy an already-prepared checkout |
| /scroll-world | Build & launch an immersive 3D scroll-world site |
| /dashboard | Launch the local control panel |
Scripts (from repo root — .env auto-loads)
| create-repo | Create private Enginecy/<slug> (idempotent) |
| scaffold | Copy a template into work/<slug> + write config & brief |
| pull | Clone or fast-forward work/<slug> from the remote |
| push | Commit + push to main — stale-push guard, your attribution |
| deploy-pages | Deploy dist/, attach subdomain + DNS, poll until live |
| register | Record the deploy in the NocoDB registry + notify |
| generate-media | Generate AI imagery (Pollinations) into public/img/ |
| build-scroll-world | Scroll-world scenes: stills → dive/connector videos → config.json (--mode kenburns|aivideo) |
| audit-scroll-world | Deterministic AA check of the scroll-world engine's runtime UI (nav, CTAs, labels) |
| list-sites | List registered sites (JSON) — powers the command site-pickers |
| add-domain | Check availability / register a new domain via Cloudflare Registrar (§09) |
| dashboard | Local control panel at localhost:4545 |
| deploy-dashboard | Deploy the hosted control-panel Worker (admin) |
| publish-guide | Rebuild this guide from docs/team-guide.html and publish it to baguide.enginecy.cloud (--deploy) |
/new-site) and you get clickable menus — kind of site (incl.
scroll-world), account, a site-picker for edit/deploy. Pass full
key=value args to skip the menus (and for headless claude -p).AI imagery & scroll-world
Any site's imagery step generates real photos via Pollinations:
enginecy-image-direction writes prompts → brief/media.json →
generate-media.mjs → public/img/. The scroll-world
template is an immersive "scroll to fly through it" 3D scroll (pre-rendered video scrubbed by
scroll); /scroll-world drives build-scroll-world.mjs to generate scene
stills + dive/connector videos. Prereqs: POLLINATIONS_API_KEY + ffmpeg.
Two motion modes. --mode kenburns (default) is a deterministic ffmpeg
push-in over each still — cheap, but a moving photo. --mode aivideo
generates a real Pollinations video per scene, so the camera genuinely flies through
the space (true parallax); if a scene's video generation fails (e.g. a Pollinations
402 balance limit) that scene falls back to ken-burns automatically, and
cached scenes are reused so you can re-run just the failed one later.
Control panel & site registry
Every deploy is recorded in a NocoDB registry — register.mjs runs at
the end of /new-site and /edit-site and upserts by slug
(one row per site), auto-filling Type, Account, Lane, Template, Category, Vertical, Domain, Status and
DeployedAt from site.config.json. See status across everything two ways:
Hosted · no install
dashboard.enginecy.cloud — behind Cloudflare Access; sign in with your @enginecy.com email + one-time PIN.
Local
node scripts/dashboard.mjs (or /dashboard) → localhost:4545. Same panel + your local work/ checkouts.
Both read .env server-side — tokens never reach the browser. One shared UI in shared/dashboard-view.mjs.
// 05 · workflow a
Build a brand-new site
One command — or plain English. The agent parses the details and runs the whole pipeline, asking only for anything required and missing.
/new-site slug=pharmacy-wellness template=bespoke-astro \
lane=A account=mo subdomain=pharmacy.enginecy.cloud \
brief='Trust-first pharmacy & wellness landing'
…or just say: "Build a trust-first pharmacy landing at pharmacy.enginecy.cloud."
Under the hood: create-repo → scaffold → the design flow
→ npm run build (must pass) → the audit gate
(site-auditor — blocks on any critical finding) → push →
deploy-pages (polled to 200) → register → reports the live URL & repo URL.
bespoke-astro, content → astro-content, 3D → three-experience, app → Lane B).// 06 · workflow b
Edit an existing site
The easy way is one command; the manual equivalent is four steps.
/edit-site <slug> <mo|client> <subdomain>
node scripts/pull.mjs <slug> # get latest into work/<slug>
# …edit work/<slug>… (optionally: /art-direct <slug>)
cd work/<slug> && npm install && npm run build
# …audit gate: run the site-auditor; fix any critical finding, re-audit…
cd ../.. && node scripts/push.mjs <slug>
node scripts/deploy-pages.mjs <slug> <mo|client> <subdomain>
scaffold is create-only — it clones the template and refuses if
work/<slug> exists. For an existing site always start with pull, never scaffold.// 07 · across machines
Collaborating — different people, different machines
The shared truth is the GitHub repo. Git handles multi-machine, sequential
collaboration natively; each person's work/<slug> is just their own checkout.
The golden rule: pull before you edit
node scripts/pull.mjs <slug>
push never force-pushes. If your checkout is behind the remote, the push is refused
— clearly — instead of clobbering someone's work:
refusing to push: remote is 1 commit(s) ahead of your checkout (stale).
Pull first: node scripts/pull.mjs <slug> then rebuild & re-run.
pull also refuses if you have uncommitted changes (commit or stash first) or if
history has diverged (resolve manually). For genuinely concurrent work, use feature branches + PRs — the scripts operate on main.
// 08 · the design flow
What makes each site distinct
Never ship a template's placeholder look. Order:
direction → data → copy → execution → imagery → polish → gate. Run just this with /art-direct <slug>.
| Step | Skill | Output |
|---|---|---|
| Direction | enginecy-art-direction | Concept, color world, type, motion, per-section direction |
| Data | ui-ux-pro-max | Concrete palette / type / pattern → tokens.css |
| Copy | art-direction (copy) | Headline formulas, benefits over features — banned-phrase list |
| Execution | frontend-design + enginecy-motion | Aesthetic + scroll/motion (Lenis, GSAP) with exact easing |
| Imagery | enginecy-image-direction | Photographic prompts + anti-plastic-skin negative prompt |
| Polish | enginecy-visual-quality | Append polish.css, walk the prohibitions list |
| Gate | site-auditor + accessibility-review | Render the build, block on contrast / a11y / slop · Lighthouse ≥ 90 |
The audit gate — site-auditor
Contrast bugs kept slipping through — light text on light buttons, dark headings on dark
sections — so an automated auditor now runs after every build and before every
deploy. /new-site, /edit-site, /deploy and
/scroll-world all invoke it as a gate; a CRITICAL finding means
do not deploy until it's fixed and re-audited. It's a read-only Claude subagent
(.claude/agents/site-auditor.md) that:
- Renders the actual build in a headless browser and reads computed styles — so Tailwind utilities and runtime-injected CSS are covered, which static linters miss.
- Sweeps every text element for AA contrast (
lib/contrast-audit.js): normalises every colour through a canvas (handlesoklch/oklab/color(srgb)), composites alpha over the real background, flags anything under 4.5:1 (3:1 for large text), and hands text-over-image to a visual screenshot pass. - Runs
accessibility-review(focus, landmarks, alt, touch targets, reduced-motion) and theenginecy-visual-qualityprohibitions. For scroll-world it also runsaudit-scroll-world.mjsagainst the engine's runtime UI.
h1{color:var(--ink)} beats a layered utility like text-white, so
<h3 class="text-white"> on a dark section silently renders dark-on-dark. Fix: wrap
base element styles in @layer base { … } so utilities win — all four Tailwind
templates already have this fix.// 09 · lanes & routing
Where a site ships — and to whose account
Lanes — decide by: does it need a server runtime you control?
- No → Lane A (Cloudflare Pages):
astro-marketing,astro-content,bespoke-astro,three-experience,scroll-world. - Yes → Lane B (Dokploy):
next-app,webapp-fullstack.
Cloudflare account routing — a hard rule
Pages, R2, and DNS all follow ownership. Pass the right account flag:
| Site owner | Flag | Account · env |
|---|---|---|
| Enginecy | account=mo | mo@enginecy.com · CLOUDFLARE_* |
| Client | account=client | backend@enginecy.com · CLOUDFLARE_CLIENT_* |
deploy-pages picks the token from this flag, creates the project, deploys dist/,
attaches the subdomain + proxied CNAME, and polls https://<subdomain> until it returns
200. First-provision SSL can take a few minutes.
Custom domains — not just subdomains
The parameter is named subdomain, but the deploy step attaches whatever
hostname you pass — a subdomain, www, or an apex/root domain
(handled via CNAME flattening at @). The real requirement isn't "subdomain vs. domain" —
it's that the domain's DNS zone lives in the correct Cloudflare account. For
*.enginecy.cloud that's already true; a brand-new domain needs one-time onboarding.
Path A — register via Cloudflare Registrar (at-cost): the domain becomes a managed
zone in the same account automatically, so deploy-pages then just works. One-time per
account, in the dashboard: add a payment method + default registrant
contact (Domains → Registrations), and add "Registrar" (read + edit) to that
account's API token in .env.
# check availability + at-cost price (free, read-only)
node scripts/add-domain.mjs <mo|client> clientbrand.com
# register into the account (CHARGES it — deliberate two-step)
node scripts/add-domain.mjs <mo|client> clientbrand.com --register --yes [--auto-renew]
# then deploy onto it (apex or any host)
node scripts/deploy-pages.mjs <slug> <mo|client> clientbrand.com
add-domain never buys without both --register and
--yes; it polls to succeeded and confirms the zone is
deploy-ready. Buying elsewhere and pointing nameservers at Cloudflare (Path B) works too — Path A just
keeps everything in one account.// 10 · attribution
Getting your name on commits
Org access authorizes the push; it does not decide attribution. GitHub attributes a commit by matching the author email to a verified email on an account.
push authors each commit with your own git identity and clears any stale
per-repo override. It falls back to the Enginecy Build Agent bot only when no identity is set
(e.g. the headless runner). The push itself is authenticated by the App.
For your avatar to appear, both must be true:
- Your
git config --global user.emailis an email verified on your GitHub account — the<id>+<user>@users.noreply.github.comaddress is ideal (private + still links to you). - You're a member of the
Enginecyorg.
// 11 · troubleshooting
When something errors
| Symptom | Cause & fix |
|---|---|
Set GITHUB_APP_ID… | Run scripts from the repo root (.env auto-loads there); confirm .env is filled |
Invalid API Token | Regenerate the Cloudflare token with Pages › Edit + DNS › Edit, paste into .env |
installation token / .pem error | GITHUB_APP_PRIVATE_KEY_FILE path wrong or key missing from secrets/ (check exact filename) |
refusing to push… stale | Your checkout is behind → pull, rebuild, re-push |
not a clean fast-forward | Diverged → resolve manually in work/<slug> (git rebase origin/main) |
| Commit shows as the bot | You didn't set git config --global user.email — set it (§10) |
| Subdomain not 200 yet | First-provision SSL delay — wait a few minutes; the deploy script polls |
| Auditor flags dark-on-dark / light-on-light headings | A Tailwind text-* utility is being beaten by an unlayered base rule — wrap base heading styles in @layer base (§08) |
scroll-world aivideo scene fell back to ken-burns | The video API failed for that scene (often a Pollinations 402 balance limit) — top up the key and re-run; cached scenes are reused |
token lacks Registrar permission | Add "Registrar" (read + edit) to that account's API token in .env |
Registration: no default registrant contact | Set a default contact + payment method in the dashboard → Domains → Registrations |
// 12 · quick reference
Copy-paste
# ── Setup (once per machine) ──
git clone https://github.com/Enginecy/build-agent && cd build-agent
./setup.sh # then fill .env, add secrets/*.pem
git config --global user.email "<id>+<user>@users.noreply.github.com"
# ── New site (in Claude Code) ──
/new-site slug=<slug> template=<template> lane=A account=<mo|client> subdomain=<host> brief='<one line>'
# ── Edit an existing site ──
/edit-site <slug> <mo|client> <subdomain>
# …or manually:
node scripts/pull.mjs <slug>
# …edit work/<slug> …
cd work/<slug> && npm install && npm run build && cd ../..
node scripts/push.mjs <slug>
node scripts/deploy-pages.mjs <slug> <mo|client> <subdomain>
# ── Design flow only ──
/art-direct <slug>
# ── Register a new domain (Path A · Cloudflare Registrar) ──
node scripts/add-domain.mjs <mo|client> <domain> # check price — free
node scripts/add-domain.mjs <mo|client> <domain> --register --yes # buy it
# ── See status of everything ──
open https://dashboard.enginecy.cloud # hosted, Access-gated, no install
node scripts/dashboard.mjs # local panel → localhost:4545