| 1 | # π€ AGENTS.md |
| 2 | |
| 3 | The canonical agent-instructions file for this repo β keep it current here, |
| 4 | not in a tool-specific file. (Claude Code reads it via the `CLAUDE.md` |
| 5 | pointer.) |
| 6 | |
| 7 | The marketing site for **Carloku** β the hosted CARLOS platform |
| 8 | (console.carloku.com). Brand boundary: Carloku is the *hosted platform* |
| 9 | brand; CARLOS is the framework/CLI/console name. Never rename CARLOS things |
| 10 | to Carloku here. |
| 11 | |
| 12 | **Voice lives in PRODUCT.md** β the one feeling is lightness ("finally, |
| 13 | simple"); the landing page appeals to the heart, /get-started carries the |
| 14 | tech. Read it before touching copy. |
| 15 | |
| 16 | Rules inherited from the `website` repo, which governs the sibling site: |
| 17 | |
| 18 | - **Small site, nothing fetched.** `/` and `/get-started/` are hand-written |
| 19 | and share one `site.css`. No frameworks, no analytics, and nothing pulled |
| 20 | from another origin β the two Geist faces in `fonts/` are self-hosted. The |
| 21 | only script is `site.js`: 2KB, hand-written, and pure enhancement (the hero |
| 22 | terminal types itself, the header grows a hairline on scroll). The pages |
| 23 | read fine with it blocked; keep it that way, and don't add a second one. |
| 24 | - **`/docs` is built; the rest of the site is not.** Docs pages come from the |
| 25 | markdown in `src/docs/` through Eleventy, the same pipeline rastrillo.org |
| 26 | uses, run here before you ship. `_site/` is gitignored and never committed; |
| 27 | Eleventy and markdown-it are devDependencies that no visitor ever meets. |
| 28 | See "Building the docs" below. |
| 29 | - **Light and dark** via `prefers-color-scheme` β keep both working. |
| 30 | - **AI authorship is always marked** with a visible π€ (cascades from a |
| 31 | heading); person-emoji (π¨/π€/π§) blocks are certified human and |
| 32 | off-limits to LLM edits. Baseline: everything here is AI-written unless |
| 33 | marked otherwise. |
| 34 | - **Don't overclaim.** The console requires Keymail sign-in; pricing does |
| 35 | not exist yet, so the site says nothing about it. |
| 36 | |
| 37 | ## Building the docs |
| 38 | |
| 39 | `/docs` is ten pages of markdown under `src/docs/`, rendered by Eleventy into |
| 40 | `_site/` before you ship. **The corpus is authored here, by hand.** That is the |
| 41 | opposite of carlosframework.com, whose `/docs` corpus is vendored out of the |
| 42 | platform repo by a sync script: there is no sync step in this repo, nothing |
| 43 | under `src/docs/` or `src/_data/docsnav.json` is generated, and nothing will |
| 44 | overwrite an edit you make. The markdown IS the source β edit it here. |
| 45 | |
| 46 | ``` |
| 47 | npm install |
| 48 | npm run check # builds, then gates the rendered output |
| 49 | npm run serve # localhost:8080, live reload |
| 50 | ``` |
| 51 | |
| 52 | `npm run check` is `eleventy` followed by `hack/check-docs.mjs`, which reads |
| 53 | `_site/` after the build: the hand-written pages and shared assets were |
| 54 | emitted at all; every nav entry has a built page and a built `.md` twin; every |
| 55 | internal `/docs` href resolves to a built file, and to a real `id="β¦"` when it |
| 56 | carries a fragment; and `slugify` in `eleventy.config.js` agrees with Go's |
| 57 | `internal/docsite.Anchor` on every case in `src/_data/docsanchors.json`. That |
| 58 | last one is why the anchor fixture is vendored rather than retyped: a heading |
| 59 | whose fragment the platform's Go gate accepts cannot 404 in a browser here |
| 60 | because the two slug rules drifted. |
| 61 | |
| 62 | **Eleventy does not clean stale output.** Delete or rename a page and its old |
| 63 | built HTML stays in `_site/` until you remove it β and the gate will *not* |
| 64 | notice, because every check it runs is satisfied by a file that should no |
| 65 | longer exist. `_site/` is gitignored, so a stale page never shows up in a diff |
| 66 | either. **Build clean whenever you are verifying anything:** |
| 67 | |
| 68 | ``` |
| 69 | rm -rf _site && npm run check |
| 70 | ``` |
| 71 | |
| 72 | The deploy sequence below gets this for free: it builds inside a fresh |
| 73 | `git archive` export, which has no `_site/` in it at all. |
| 74 | |
| 75 | `index.html` and `get-started/index.html` are hand-written and pass through the |
| 76 | build byte-identical β `templateFormats` excludes `html` on purpose, so |
| 77 | Eleventy copies them instead of rendering them as templates. If either changes |
| 78 | in `_site/` without you editing its source, something is wrong. |
| 79 | |
| 80 | ## Deploying |
| 81 | |
| 82 | Merging does **not** publish. The live site is stale until someone deploys. |
| 83 | |
| 84 | **Since 2026-10-04 the site is an ordinary console app** (platform #209): |
| 85 | |
| 86 | | | | |
| 87 | | --- | --- | |
| 88 | | console | `https://console.carloku.com` | |
| 89 | | account / app | `bes` (named `carloku`) / `website` | |
| 90 | | channel | `edge`, the app's only channel | |
| 91 | | kind | `static` | |
| 92 | | platform URL | `https://website.bes.oncarlos.com` | |
| 93 | | custom domains | `carloku.com`, `www.carloku.com` | |
| 94 | |
| 95 | carloku.com and www.carloku.com are **platform claims** on that app. They are |
| 96 | in the flagship's own zone, which `carlos domains attach` refuses to members, |
| 97 | so the operator attached them with `carlos domains attach --platform`. Only an |
| 98 | ops account owner or a `CARLOS_SYSTEM_OPERATORS` address can change or detach |
| 99 | them. A deploy never touches them: deploy the app and both hostnames follow. |
| 100 | |
| 101 | **There is a build step, so what ships is `_site/`, never the repo tree.** A |
| 102 | tree ship has no `_site/` in it: it would publish `index.html` buried under |
| 103 | `src/`, no built pages, and no `/docs` at all. |
| 104 | |
| 105 | ``` |
| 106 | SHA=$(git rev-parse --short HEAD) # the sha you are shipping |
| 107 | |
| 108 | # 1. Clean export. Never deploy a working checkout: the packer takes every |
| 109 | # regular file it sees, including .git, node_modules/ and .claude/. |
| 110 | rm -rf "$TMPDIR/carloku-ship" && mkdir -p "$TMPDIR/carloku-ship" |
| 111 | git archive "$SHA" --prefix=export/ | tar -x -C "$TMPDIR/carloku-ship" |
| 112 | cd "$TMPDIR/carloku-ship/export" |
| 113 | |
| 114 | # 2. Build, and gate the rendered output. `check` runs eleventy, then |
| 115 | # hack/check-docs.mjs over what it produced. |
| 116 | npm ci |
| 117 | npm run check |
| 118 | |
| 119 | # 3. Cache-bust BOTH stylesheets across every built page β see Caching |
| 120 | # below; this is not optional. |
| 121 | find _site -name '*.html' -exec sed -i \ |
| 122 | -e "s/\.css\"/.css?v=$SHA\"/g" \ |
| 123 | -e "s/\.css?v=[^\"]*\"/.css?v=$SHA\"/g" {} + |
| 124 | |
| 125 | grep -rho '\.css?v=[^"]*"' _site --include='*.html' | sort -u # ONE line: your sha |
| 126 | grep -rn '\.css"' _site --include='*.html' \ |
| 127 | && echo "UNBUSTED LINK β do not ship" || echo "cache-bust ok" |
| 128 | |
| 129 | # 4. Deploy the BUILT OUTPUT through the console. --version is required: |
| 130 | # the export has no .git, so the default (git rev-parse) has nothing to |
| 131 | # read. |
| 132 | carlos deploy --console https://console.carloku.com \ |
| 133 | --account bes --app website --kind static \ |
| 134 | --version "$SHA" --label "<one line about the change>" --no-prompt _site |
| 135 | ``` |
| 136 | |
| 137 | `carlos deploy` ships, promotes onto `edge` and watches the platform URL until |
| 138 | `X-Carlos-Version` reports the new sha. It runs as whoever is signed in |
| 139 | (`carlos auth login --console https://console.carloku.com`) and needs that |
| 140 | address to be a member of `bes`. Use a current `carlos` binary. An old one on |
| 141 | your `PATH` may not know this console's API. |
| 142 | |
| 143 | **Step 3 explained, because both halves are load-bearing.** The links in `src/` |
| 144 | are plain (`href="/site.css"`), so the first expression stamps a token onto a |
| 145 | bare link and the second re-stamps a link that already carries one. Both end at |
| 146 | the closing quote on purpose, and so does the verification grep. A sha |
| 147 | beginning with `0` β this app has already shipped `01b5365` β makes |
| 148 | `?v=01b5365` contain the literal substring `?v=0`, so an unanchored grep for |
| 149 | `?v=0` calls every freshly bumped file stale (13 of 13, measured), and an |
| 150 | unanchored `sed` run twice yields `?v=01b536501b5365`. Anchoring on `"` fixes |
| 151 | both and makes step 3 idempotent: run it again and again, the answer is the |
| 152 | same. Don't hard-code link or page counts anywhere; the corpus grows, which is |
| 153 | also why the `find` walks `_site` instead of naming files. |
| 154 | |
| 155 | **Caching: the edge sends no `Cache-Control` and no `ETag` on static routes, |
| 156 | only `Last-Modified`** (confirmed live on carloku.com 2026-08-27). Browsers |
| 157 | therefore apply HEURISTIC caching, roughly 10% of the age since |
| 158 | `Last-Modified`, so a returning visitor can hold a stale page for days. This |
| 159 | bit the sibling site the day its redesign shipped: one browser served the whole |
| 160 | old page, another served the NEW html against the OLD stylesheet. The real fix |
| 161 | is server-side `Cache-Control` on static routes β platform issue |
| 162 | **carlosframework/platform#234**. Until that lands, **step 3 is a required part |
| 163 | of deploying this site**, and it is required for `docs.css` exactly as much as |
| 164 | for `site.css`: the docs pages link both, and a docs page rendered against a |
| 165 | stale `docs.css` loses its whole layout. |
| 166 | |
| 167 | **Verify** by header and by content, and verify `/docs` **specifically**: the |
| 168 | landing page looks right whether or not the built docs made it into the |
| 169 | artifact. |
| 170 | |
| 171 | ``` |
| 172 | carlos channels --console https://console.carloku.com --account bes --app website |
| 173 | carlos releases --console https://console.carloku.com --account bes --app website |
| 174 | |
| 175 | curl -sI https://carloku.com/docs/ | grep -i x-carlos-version # your sha |
| 176 | curl -s https://carloku.com/ | grep -i "<something from the change>" |
| 177 | curl -s https://carloku.com/docs/ | grep -o 'docs\.css?v=[^"]*' # your sha |
| 178 | ``` |
| 179 | |
| 180 | Every edge serves the site, so to check one directly add |
| 181 | `--resolve carloku.com:443:<edge-ip>`. The four flagship edges are |
| 182 | 99.81.104.219 (Ireland), 16.60.78.128 (London), 44.210.206.26 (Virginia) and |
| 183 | 54.252.186.195 (Sydney). `-I` sends a HEAD, and HEAD is excluded from edge |
| 184 | compression, so anything about `Content-Encoding` needs a GET. |
| 185 | |
| 186 | **Rollback** is a pointer move. Every release stays in the bucket, cache-busted |
| 187 | as it was shipped, so a rollback restores a self-consistent page-plus-stylesheet |
| 188 | pair: |
| 189 | |
| 190 | ``` |
| 191 | carlos rollback --console https://console.carloku.com --account bes --app website edge |
| 192 | # or, to a named release: |
| 193 | carlos promote --console https://console.carloku.com --account bes --app website <sha> edge |
| 194 | ``` |
| 195 | |
| 196 | **DNS** is in Route53, in the carloku-infrastructure tofu (`dns-carloku.tf`), |
| 197 | since 2026-10-05. The apex carries one latency-routed A record per edge, each |
| 198 | behind a health check that asks that edge for carloku.com, so every visitor |
| 199 | gets their nearest healthy edge. `www` is a CNAME to `website.bes.oncarlos.com`, |
| 200 | which the platform steers the same way. `dashboard` and `console` are CNAMEs to |
| 201 | the console and are not this site's. A new flagship region needs a line in that |
| 202 | file, or the apex never sends anyone there. The domain is still registered at |
| 203 | DNSimple; only the delegation moved. |
| 204 | |
| 205 | **History.** Until 2026-10-04 the site was `bab/carloku`, a direct-bucket app |
| 206 | shipped with the pinfra `ship-app.sh` onto `canary/rehearsal`, with box-local |
| 207 | routes on the Ireland box. Those routes were removed when the hostnames moved |
| 208 | to `bes/website`. The old app's releases are still in the bucket, but nothing |
| 209 | routes to it: **never ship to `bab/carloku` again.** |
| 210 | |