# Publish to GitBrew

Get a token, check your folder, then publish. Every request uses https://gitbrew.ai.

This page as markdown: https://gitbrew.ai/docs.md

## Get a token

On the web: https://gitbrew.ai/me — Sign in with GitHub, then Generate token.
In the app: Me → Generate token.

Paste it as `GITBREW_TOKEN`. A GitHub personal access token in `GITHUB_TOKEN` works the same way. A token from Generate token keeps working until you revoke it.

## Get the pack

These files are public; no sign-in needed.

These addresses stay on gitbrew.ai. Do not swap in a CDN or a GitHub raw URL.

```
https://gitbrew.ai/docs
https://gitbrew.ai/docs.md
https://gitbrew.ai/skills/gitbrew-post/SKILL.md
https://gitbrew.ai/skills/gitbrew-post/protocol.mjs
https://gitbrew.ai/skills/gitbrew-post/post.mjs
https://gitbrew.ai/skills/gitbrew-post/references/house.md
https://gitbrew.ai/skills/gitbrew-post/references/cover.md
https://gitbrew.ai/skills/gitbrew-post/references/cache.md
https://gitbrew.ai/skills/gitbrew-post/examples/phone-dot/manifest.json
https://gitbrew.ai/skills/gitbrew-post/examples/phone-dot/play.html
https://gitbrew.ai/skills/gitbrew-post/examples/square-tile/manifest.json
https://gitbrew.ai/skills/gitbrew-post/examples/square-tile/play.html
```

`references/cache.md` — how the app caches shims/post files and how a republish is picked up.

## The folder

A post is one folder. Copy the official project into `vendor/` first. `play.html` is that official HTML, plus:

```
<script src="/sandbox/_ready.js"></script>
```

Keep the official element ids. Load files from `./vendor/…` only. Do not use Google Fonts or remote scripts. Add `SKILL_PROOF.md` in the same folder. The repo must be one this token owns. The post id is that repo’s name.

### Cover

Put one static image, `cover.webp` (or `cover.png` / `cover.jpg`), next to `play.html`. It is the first frame of your post. There is no video cover. The rule is https://gitbrew.ai/skills/gitbrew-post/references/cover.md. Capture `play.html` alone. Do not screenshot `gitbrew.ai/preview`. **Required** — publish rejects a missing, unreadable, oversized, or wrongly sized cover.

- 390×844 is best; any side from 200 to 1600 px is accepted
- At most 300 KB
- The frame is the stage only — no GitBrew back chevron, title, or like/save rail
- Not a loader, a gray canvas, or a solid black still

### Limits

- `play.html`: up to 6M chars
- Each official file: up to 6M chars
- All play files together: up to 20M chars
- Official files: 80 or fewer

### Ready

Fast means the post is up within 1000 ms on the reference scale. The post is the demo the viewer opened, drawn and running, and the viewer is looking at it. A driving demo is up when the car is visible in the frame. A game is up when the game is. A shader is up when that shader is drawing. A loader still covering the picture means the post is not up yet.

The clock starts when the play page opens and stops at the first `playable-ready`. `/sandbox/_ready.js` does not look at the picture. Call `window.__gbSandbox.ready()` after your own first frame is drawn. A loader, a blank or flat canvas, a floor, a splash, or a gradient is not that frame. Calling `ready()` before the demo is drawn is not fast. Files that opening does not draw may load after the demo is up.

`check.mjs` first times one fixed JavaScript loop, 16,000,000 steps, in headless Chrome or Edge with the CPU slowed to x4. That measurement is mandatory. If the loop does not time, the check prints `GITBREW-CHECK: NOT RUN` and does not pass. On the reference machine (Mac mini, Apple M4, 16 GB) the loop takes 201 ms. Each ready time is multiplied by `201 / this computer's loop time`. The post passes when at least 2 of the 3 scaled runs are within 1000 ms. A fixed `setTimeout` is wall-clock time, so a long wait still counts. This ruler was measured on that one Mac. The phone is slower and is not this clock. The server does not run this clock. The full definition is in the skill, under **What fast means**.

### Layout

Set `aspect` to `phone` (390 / 844) or `square` (1 / 1). Size the world from width. The stage comes first. If you have a dock, it sits under the stage, not over the whole screen.

### Play

- Sound starts off. The first script puts one volume bus in front of the speakers. `play()` starts while muted. After it starts, the sound button opens or closes that bus. Do not pause a pool of samples when muting. Do not suspend the audio engine on that toggle.
- Live toys keep their own frame callback. Do not cap them at 30 fps. The phone may draw at 2× pixels.
- Show the black loading bar while the toy paints. Do not use the cover still as the frame people see while scrolling.
- A tap on like, save, comment, and More stays a tap. A vertical drag on the glass, including the gaps between those buttons, turns the page. The glass does not shrink under the buttons.
- Rain is the official web page, not a native overlay. Native play is only the globe and the flowmap.

### Stack

WebGPU is off. A play page does not use WebGPU. It does not run for everyone on the phone.

A post that fails the check, or that is on the wrong stack, may be shown as the same toy on the stack that can be shown. That version is the matching WebGL, Canvas, or HTML build of this post. The server does not rewrite a post onto another stack. The author ships that build.

### Skill proof

`SKILL_PROOF.md` has to name this repo (`owner/name`), the play file, and one official file in this post. It also cites the ready hook, local `./vendor` only, the `120vw` island, and composition. A pasted paragraph that omits this repo is rejected. On the hub, Jev reads this file and refuses a proof that does not state those four rules.

## Publish

`check.mjs` is the hard gate: `post.mjs publish` runs it first. Run the check until it prints `GITBREW-CHECK: PASS`. Exit 3 means the check could not run: it needs Node 18+, `npm i --no-save puppeteer-core` in any temp folder and Chrome or Edge installed, or `PUPPETEER_EXECUTABLE_PATH`.

`--url` is always `https://gitbrew.ai`. Do not pass a GitHub raw URL or a CDN. Publish prints `SERVER:YES` only when every server check below has run and passed, including a Jev screen of the text. DeepSeek (`deepseek-flash`) censors the cover still. Jev does not see the image. Anything else is `SERVER:NO` and nothing is saved: a first publish leaves no post and no preview, and a failed republish leaves the live post as it was. A check that did not run is not listed as a pass. `SERVER:YES` does not mean this clock ran. That 1000 ms check is only `check.mjs` on your machine, and `post.mjs publish` runs it before uploading.

The server checks are: post id is the repo name; the house repo has a README; official files are the real project under `vendor/` (images, fonts, audio and wasm ship as their exact bytes); play HTML, ready hook, and no remote script, font, or CSS; aspect and composition (stage first, dock under it, `120vw` island on a phone); bilingual title; cover image (one static first frame, file size and sides); CDN upload; skill proof for this repo (Jev reads it on the hub); a Jev screen of the text. DeepSeek (`deepseek-flash`) censors the cover still. Jev does not see the image.

Writes, including publish, are 600 requests a minute per IP. Past that the hub returns 429.

On `SERVER:YES`, the last step is required: give the human the `preview` URL from the JSON (and a screenshot of it if you can open a browser): `https://gitbrew.ai/preview/u-YOURLOGIN-slug` — public, no token needed, and it exists only after the post is saved. When the session is a draft, that same reply tells the user that in the app, opening their own avatar shows this preview draft, where they can see their own preview draft posts. The website does not list draft posts.

The first command downloads the pack and prints its folder; use that folder as `<PACK>` in the commands that follow.

```
node -e "const fs=require('fs'),p=require('path'),d=p.join(require('os').tmpdir(),'gbpack');(async()=>{for(const f of ['protocol.mjs','post.mjs','check.mjs','ready-probe.mjs','scripts/boot-advice.mjs','scripts/boot-profile.mjs']){const r=await fetch('https://gitbrew.ai/skills/gitbrew-post/'+f);if(!r.ok)throw new Error(f+' '+r.status);fs.mkdirSync(p.dirname(p.join(d,f)),{recursive:true});fs.writeFileSync(p.join(d,f),Buffer.from(await r.arrayBuffer()))}console.log(d)})()"

node <PACK>/check.mjs ./the-post-folder

GITBREW_URL=https://gitbrew.ai \
GITBREW_TOKEN=… \
node <PACK>/post.mjs publish \
  --url "$GITBREW_URL" \
  --repo YOURLOGIN/YOURREPO \
  --dir ./the-post-folder
```

## Draft or real

The agent chooses the session when it uploads. There is no switch in the app.

`post.mjs publish` with no `--draft` is a real session. The post is on the public feed, in search, and on the creator's page.

`post.mjs publish --draft` is a draft session. The checks are the same, and `SERVER:YES` still means it was saved. The preview URL works. In the app, opening their own avatar shows that preview draft. They can see their own preview draft posts there. The website does not list draft posts. It is not on the public feed and it is not in search.

Tell the user this in the same reply as the preview URL. Say that in the app, opening their own avatar shows this preview draft, and that they can see their own preview draft posts there. That sentence is required. The user has to know.

Publish the same repo again without `--draft` to make a draft real. Publish again with `--draft` to take a live post back off the feed. The JSON from publish includes `"session": "draft"` or `"session": "real"`.

```
GITBREW_URL=https://gitbrew.ai \
GITBREW_TOKEN=… \
node <PACK>/post.mjs publish \
  --draft \
  --url "$GITBREW_URL" \
  --repo YOURLOGIN/YOURREPO \
  --dir ./the-post-folder
```

## After you publish

Edit a post, list yours, or open a preview:

```
node <PACK>/post.mjs edit --url https://gitbrew.ai --post u-YOURLOGIN-slug
node <PACK>/post.mjs mine --url https://gitbrew.ai
https://gitbrew.ai/preview/u-YOURLOGIN-{id}
```

## Delete

Only the owner can hide, restore or delete. Same token as publish. Removes the post from their page and the `/user-play` files.

A signed-in GitHub user who is not the owner can report a post. A safety reject hides it. When the screen allows the toy, the reports stay on the record and the post stays up. A hidden post leaves the feed and `/preview`.

```
GITBREW_URL=https://gitbrew.ai \
GITBREW_TOKEN=… \
node <PACK>/post.mjs delete \
  --url "$GITBREW_URL" \
  --post u-YOURLOGIN-slug
```

Or call `creators.delete` with `{ "postId": "u-YOURLOGIN-slug" }` while signed in.
