@homie-rocks/studio 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,8 +110,8 @@ are watched only by those they let in (NETPLAY.md section 16, site/SITE.md).
110
110
  A studio pins one version, so nothing changes until it asks. To take a newer one:
111
111
 
112
112
  ```sh
113
- npx -y @homie-rocks/studio@0.17.0 upgrade # the plan; changes nothing
114
- npx -y @homie-rocks/studio@0.17.0 upgrade --apply # after the person agrees
113
+ npx -y @homie-rocks/studio@0.18.2 upgrade # the plan; changes nothing
114
+ npx -y @homie-rocks/studio@0.18.2 upgrade --apply # after the person agrees
115
115
  npm install && npm run build
116
116
  ```
117
117
 
@@ -278,7 +278,7 @@ npx homie-studio agents pass night-rush --label Claude
278
278
  ## Setup status
279
279
 
280
280
  ```sh
281
- npx -y @homie-rocks/studio@0.17.0 setup status --connector yes # before a studio exists
281
+ npx -y @homie-rocks/studio@0.18.2 setup status --connector yes # before a studio exists
282
282
  npx homie-studio setup status # in a studio (also: homie-studio doctor)
283
283
  ```
284
284
 
@@ -288,9 +288,16 @@ with the studio's own Wrangler, and the account's email verified: a deploy that
288
288
  deploy refused for it is remembered), Chrome for the checks, ffmpeg, and the optional GitHub, ElevenLabs and
289
289
  fal (and, in Claude Code, the status line). Each row is green, missing or "do this now", says what it
290
290
  unlocks, and gives the exact fix: a command the AI runs or a page the person taps. It only reads, answers in
291
- seconds, and never prints a key, a token or an account's name. A studio the Claude app's setup card made
291
+ seconds, and never prints a key, a token or an account's name or id. A studio the Claude app's setup card made
292
292
  (`setup attach`) counts as connected to Cloudflare and GitHub there. `--json` gives the rows.
293
293
 
294
+ When a server's AI guides think with Workers AI (0.18.1), a **Workers AI** row checks that the model they use
295
+ (`HOMIE_BRAIN_MODEL` in wrangler.jsonc `vars`, else `@cf/meta/llama-3.1-8b-instruct-fp8-fast`) answers on the
296
+ studio's account: one tiny call through Cloudflare's API with the studio's own Wrangler login (a one-word prompt,
297
+ one token out: about 0.1 of the 10,000 free neurons a day; `lib/brain-probe.mjs`). A model Cloudflare moved to
298
+ Workers Paid (error 5035) or retired (5007) is named, with what to do: pick another model, or, the person's money,
299
+ Workers Paid. A spent daily allowance (3036) is said as that, not as a broken model.
300
+
294
301
  ## The Game Codex
295
302
 
296
303
  `games/<id>/CODEX.md` is a game's plan: Markdown with a small frontmatter for its look (palette, fonts, a
@@ -459,7 +466,7 @@ npx homie-studio port check my-game --url http://127.0.0.1:8787 # the owner te
459
466
  | `template/` (repository root), `scripts/template.mjs` | The public "Deploy to Cloudflare" template, generated from `new --template`. |
460
467
  | `lib/port.mjs` | `port plan` (reads a game and grades the port) and `port import`. |
461
468
  | `lib/port-check.mjs` | `port check`: held and alternating directions on keys, Android Chrome and iPhone WebKit touch, UI cover, two browsers finishing a round, a killed host, a late joiner, the big screen. |
462
- | `port/` | The port toolkit (`@homie-rocks/studio/port`, or `window.HomiePort` from `homie-port.js` in a static game): `createRoom`, the touch kit, keys, camera rules, bots, a HUD, sandbox shims, first-touch audio, `exposePort`. |
469
+ | `port/` | The port toolkit (`@homie-rocks/studio/port`, or `window.HomiePort` from `homie-port.js` in a static game): `createRoom`, the touch kit, keys, camera rules, bots, a HUD, sandbox shims, first-touch audio, `exposePort`; `fitView` and `createLabels` (`port/view.ts`): a flat world on every screen (the whole of it where it reads, else filling a phone held upright and following the player) and names that never pile up when bodies crowd (yours placed first, the rest moved or faded). |
463
470
  | `worker/index.mjs` | The site Worker and the `Table` (netplay relay, `room.mjs`) and `Lobby` Durable Objects; `/<game>/tv` is the big screen with a join QR (`qr.mjs`); `/<game>/watch` watches a live room from any player's view (NETPLAY.md section 16); `/music/<slug>/` and `/videos/<slug>/` are song and video pages (their files served with byte ranges, from the site's files or, once moved, from the studio's R2 at the same address; a loose file from `media put` at `/media/<key>`). `seats.mjs`: room sizes (up to 32). |
464
471
  | `worker/site.mjs`, `lib/site.mjs`, `lib/markdown.mjs`, `site/SITE.md` | The site: its sections, each game's landing, posts and their feeds, the look (theme tokens) and what the studio's `site/` folder overrides; the safe markdown posts are written in. `worker/pages.mjs`: the play page. |
465
472
  | `worker/stats.mjs`, `worker/stats-page.mjs`, `lib/stats.mjs` | The studio's own stats: what is counted and how, the owner-only `/api/stats` and `/_studio/stats`, and `homie-studio stats`. |
@@ -0,0 +1,154 @@
1
+ /**
2
+ * `homie-studio doctor`'s Workers AI row: when a server's AI guides think with Workers AI (brain `workers-ai`), is the
3
+ * model they use (wrangler.jsonc var HOMIE_BRAIN_MODEL, else worker/brain.mjs DEFAULT_MODEL) one this studio's
4
+ * Cloudflare account can run? Cloudflare moves models to the Workers Paid plan from time to time (three on 2026-07-28,
5
+ * error 5035) and retires others (5007); either way the guides fall back to the game's script without a word, so the
6
+ * doctor asks once.
7
+ *
8
+ * The check is ONE tiny call to the model through Cloudflare's REST API (`POST /accounts/:id/ai/run/:model`, a
9
+ * one-word prompt, max_tokens 1: about 0.1 of the 10,000 free neurons a day), with the studio's own Wrangler login
10
+ * (`wrangler auth token`, held in this process only) or CLOUDFLARE_API_TOKEN. It never prints the token, the account's
11
+ * id or its name. A call that cannot be made (not signed in, several accounts and none named) says so and spends
12
+ * nothing.
13
+ */
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { DEFAULT_MODEL } from '../worker/brain.mjs';
17
+ import { configPath, workerDir } from './studio.mjs';
18
+
19
+ const MODELS = 'https://developers.cloudflare.com/workers-ai/models/';
20
+ /** A Workers AI model id: @cf/<maker>/<model> (or @hf/…); nothing that could leave the API path. */
21
+ export const MODEL_ID = /^@[a-z0-9-]+\/[A-Za-z0-9._-]+\/[A-Za-z0-9._:-]+$/;
22
+
23
+ const parseJson = (text) => { const s = String(text ?? ''); const i = s.search(/[[{]/); if (i < 0) return null; try { return JSON.parse(s.slice(i)); } catch { return null; } };
24
+
25
+ /** The model the studio's guides think with: wrangler.jsonc "vars".HOMIE_BRAIN_MODEL, else the toolkit's default. */
26
+ export function brainModelOf(root) {
27
+ try {
28
+ const json = JSON.parse(readFileSync(configPath(root), 'utf8').replace(/^\s*\/\/.*$/gm, ''));
29
+ const v = json?.vars?.HOMIE_BRAIN_MODEL;
30
+ if (typeof v === 'string' && v.trim()) return { model: v.trim(), set: true };
31
+ } catch { /* no config: the default */ }
32
+ return { model: DEFAULT_MODEL, set: false };
33
+ }
34
+
35
+ /**
36
+ * Whether any open server of the studio's games thinks with Workers AI: a game.json seed in the built catalogue, or a
37
+ * row in the live database (one read-only `wrangler d1 execute --remote`). A database without the servers table, or
38
+ * one that cannot be read, says no.
39
+ */
40
+ export async function usesWorkersAi(root, { exec, env = process.env, accountId = null, bin } = {}) {
41
+ try {
42
+ const cat = JSON.parse(readFileSync(join(root, 'site', 'dist', 'games.json'), 'utf8'));
43
+ if ((cat.games ?? []).some((g) => (g.servers ?? []).some((s) => s.brain === 'workers-ai'))) return true;
44
+ } catch { /* not built */ }
45
+ if (!bin || !existsSync(bin)) return false;
46
+ let db = 'DB';
47
+ try { db = JSON.parse(readFileSync(configPath(root), 'utf8').replace(/^\s*\/\/.*$/gm, ''))?.d1_databases?.[0]?.binding ?? 'DB'; } catch { /* the default binding */ }
48
+ const r = await exec(bin, ['d1', 'execute', db, '--remote', '--json', '--command', "SELECT COUNT(*) AS n FROM servers WHERE brain = 'workers-ai' AND state = 'open'"], {
49
+ cwd: workerDir(root), timeout: 25_000, env: { ...env, WRANGLER_SEND_METRICS: 'false', CI: '1', ...(accountId ? { CLOUDFLARE_ACCOUNT_ID: accountId } : {}) },
50
+ });
51
+ if (r.code !== 0) return false;
52
+ const parsed = parseJson(r.stdout);
53
+ return Number((Array.isArray(parsed) ? parsed[0] : parsed)?.results?.[0]?.n) > 0;
54
+ }
55
+
56
+ /** How to call Cloudflare's API as this computer's Wrangler login: headers, or null. Never printed. */
57
+ export async function cloudflareAuth({ exec, env = process.env, bin, cwd } = {}) {
58
+ if (env.CLOUDFLARE_API_TOKEN) return { authorization: `Bearer ${env.CLOUDFLARE_API_TOKEN}` };
59
+ if (env.CLOUDFLARE_API_KEY && env.CLOUDFLARE_EMAIL) return { 'x-auth-key': env.CLOUDFLARE_API_KEY, 'x-auth-email': env.CLOUDFLARE_EMAIL };
60
+ if (!bin || !existsSync(bin)) return null;
61
+ const r = await exec(bin, ['auth', 'token', '--json'], { cwd, timeout: 15_000, env: { ...env, WRANGLER_SEND_METRICS: 'false', CI: '1' } });
62
+ if (r.code !== 0) return null;
63
+ const j = parseJson(r.stdout);
64
+ if (typeof j?.token === 'string' && j.token) return { authorization: `Bearer ${j.token}` };
65
+ if (typeof j?.key === 'string' && typeof j?.email === 'string') return { 'x-auth-key': j.key, 'x-auth-email': j.email };
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * What Cloudflare's answer to the probe means (its errors: developers.cloudflare.com/workers-ai/platform/errors/):
71
+ * `{ why, state }` with why one of ok, paid-only, missing, not-allowed, agreement, allowance, busy, auth, other.
72
+ */
73
+ export function classifyProbe(status, body) {
74
+ const errs = Array.isArray(body?.errors) ? body.errors : [];
75
+ const codes = errs.map((e) => Number(e?.code)).filter(Number.isFinite);
76
+ const msg = errs.map((e) => String(e?.message ?? '')).filter(Boolean).join('; ');
77
+ const has = (c) => codes.includes(c);
78
+ if (status >= 200 && status < 300 && body?.success !== false) return { why: 'ok', state: 'ok', msg };
79
+ if (has(5035) || (status === 403 && /workers paid|paid plan|upgrade/i.test(msg))) return { why: 'paid-only', state: 'act', msg };
80
+ if (has(5007) || /no such model/i.test(msg) || status === 404) return { why: 'missing', state: 'act', msg };
81
+ if (has(5018)) return { why: 'not-allowed', state: 'act', msg };
82
+ if (has(5016) || /agree/i.test(msg)) return { why: 'agreement', state: 'act', msg };
83
+ if (has(3036) || (status === 429 && /free allocation|neurons/i.test(msg))) return { why: 'allowance', state: 'later', msg };
84
+ if (has(3040) || status === 429 || status >= 500) return { why: 'busy', state: 'unknown', msg };
85
+ if (status === 401 || has(10000) || has(10001) || (status === 403 && /auth/i.test(msg))) return { why: 'auth', state: 'unknown', msg };
86
+ if (status === 400) return { why: 'input', state: 'act', msg };
87
+ return { why: 'other', state: 'unknown', msg };
88
+ }
89
+
90
+ /**
91
+ * The one call. `{ status, body }`, or `{ error }` when the network failed. `fetchFn` stands in for the network in
92
+ * tests; the call is a one-word prompt with max_tokens 1.
93
+ */
94
+ export async function probeModel({ model, accountId, headers, fetchFn = globalThis.fetch, base = 'https://api.cloudflare.com/client/v4', ms = 12_000 }) {
95
+ try {
96
+ const res = await fetchFn(`${String(base).replace(/\/+$/, '')}/accounts/${encodeURIComponent(accountId)}/ai/run/${model}`, {
97
+ method: 'POST',
98
+ headers: { ...headers, 'content-type': 'application/json', accept: 'application/json', 'user-agent': 'homie-studio-doctor' },
99
+ body: JSON.stringify({ messages: [{ role: 'user', content: 'Say OK.' }], max_tokens: 1 }),
100
+ signal: AbortSignal.timeout(ms),
101
+ });
102
+ return { status: res.status, body: await res.json().catch(() => null) };
103
+ } catch (error) {
104
+ return { error: String(error?.name === 'TimeoutError' ? `no answer in ${Math.round(ms / 1000)} s` : error?.message ?? error) };
105
+ }
106
+ }
107
+
108
+ /**
109
+ * The doctor's row, or null when no server thinks with Workers AI. `who`: the doctor's Wrangler answer
110
+ * (`signedIn`, `accountIds`); `bin`: the studio's own Wrangler; `remote`: the studio is live, so its database may be
111
+ * read; `exec`, `fetchFn` replaceable for tests.
112
+ */
113
+ export async function workersAiRow({ root, studio, env = process.env, exec, fetchFn = globalThis.fetch, who = {}, bin, remote = false, cli = 'npx --no-install homie-studio' }) {
114
+ if (!root) return null;
115
+ const accountId = env.CLOUDFLARE_ACCOUNT_ID || studio?.cloudflare?.accountId || (who.accountIds?.length === 1 ? who.accountIds[0] : null);
116
+ if (!(await usesWorkersAi(root, { exec, env, accountId, bin: remote ? bin : null }))) return null;
117
+ const { model, set } = brainModelOf(root);
118
+ const row = {
119
+ id: 'workers-ai', label: 'Workers AI', need: 'for AI guides', model,
120
+ unlocks: 'AI guides that think and talk on servers whose brain is workers-ai, on the studio\'s own Workers AI (free: 10,000 neurons a day for the account)',
121
+ };
122
+ const where = set ? 'HOMIE_BRAIN_MODEL in wrangler.jsonc' : 'the toolkit\'s default';
123
+ const pick = (lead) => ({
124
+ who: 'ai', open: MODELS,
125
+ say: `${lead}Pick a model the account can run: set HOMIE_BRAIN_MODEL in wrangler.jsonc "vars" to a text-generation model from ${MODELS} that is not marked Workers Paid${set && model !== DEFAULT_MODEL ? ` (or remove it for the default, ${DEFAULT_MODEL})` : ''}, run \`${cli} doctor\` again, then \`npm run deploy\`. Until then the guides answer from the game's script.`,
126
+ });
127
+ const money = ' Moving the account to Workers Paid ($5 a month) also works, but that is the person\'s money: ask them, never decide it.';
128
+ if (!MODEL_ID.test(model)) return { ...row, state: 'act', detail: `${where} is "${model.slice(0, 80)}", which is not a Workers AI model id (@cf/<maker>/<model>)`, fix: pick('') };
129
+ if (who.signedIn === false && !env.CLOUDFLARE_API_TOKEN) return { ...row, state: 'later', detail: `${model} (${where}): checked once Wrangler is signed in`, fix: null };
130
+ if (!accountId) return { ...row, state: 'unknown', detail: `${model}: this login reaches several Cloudflare accounts and studio.json does not say which is the studio's (cloudflare.accountId, written at the first deploy)`, fix: null };
131
+ const headers = await cloudflareAuth({ exec, env, bin, cwd: workerDir(root) });
132
+ if (!headers) return { ...row, state: 'unknown', detail: `${model}: could not ask Cloudflare (no sign-in to call its API with)`, fix: { who: 'ai', run: 'npx wrangler login', say: 'Sign in to Cloudflare again (you approve once in the browser), then run doctor again.' } };
133
+ const answer = await probeModel({ model, accountId, headers, fetchFn, base: env.CLOUDFLARE_API_BASE_URL || undefined });
134
+ // Cloudflare's own words, without the account's id, and short.
135
+ const plain = (s) => String(s ?? '').split(accountId).join('<account>').replace(/\s+/g, ' ').trim().slice(0, 160);
136
+ if (answer.error) return { ...row, state: 'unknown', detail: `${model}: this computer's request to Cloudflare failed (${plain(answer.error)}); run doctor again`, fix: null };
137
+ const c = classifyProbe(answer.status, answer.body);
138
+ const said = c.msg ? ` (Cloudflare: "${plain(c.msg)}")` : '';
139
+ switch (c.why) {
140
+ case 'ok': return { ...row, state: 'ok', detail: `${model} answers on this studio's Cloudflare account (${where}; one tiny call just now)`, fix: null };
141
+ case 'paid-only': {
142
+ const fix = pick(model === DEFAULT_MODEL ? 'The toolkit\'s default model is paid-only now. ' : '');
143
+ return { ...row, state: 'act', detail: `${model} needs the Workers Paid plan, and this account is on Workers Free: every guide on a workers-ai server answers from the game's script${said}`, fix: { ...fix, say: fix.say + money } };
144
+ }
145
+ case 'missing': return { ...row, state: 'act', detail: `Cloudflare has no model ${model} (renamed or retired)${said}`, fix: pick('') };
146
+ case 'not-allowed': return { ...row, state: 'act', detail: `this account may not use ${model} (a private model)${said}`, fix: pick('') };
147
+ case 'agreement': return { ...row, state: 'act', detail: `${model} asks for its maker's licence to be agreed once before it is used${said}`, fix: pick('Agreeing to a licence is the person\'s to do, never yours; or simply: ') };
148
+ case 'input': return { ...row, state: 'act', detail: `${model} did not take a chat message: the guides need a text-generation model${said}`, fix: pick('') };
149
+ case 'allowance': return { ...row, state: 'later', detail: `today's free Workers AI allowance (10,000 neurons for the account) is used up, so ${model} could not be checked; the guides answer from the script until 00:00 UTC`, fix: { who: 'person', say: `Nothing to do now: run doctor again after 00:00 UTC. The studio's daily budget (\`${cli} agents brain <game> <server> workers-ai --budget <neurons>\`) keeps the guides under the allowance.` } };
150
+ case 'busy': return { ...row, state: 'unknown', detail: `Workers AI was busy just now (status ${answer.status})${said}; run doctor again in a minute`, fix: null };
151
+ case 'auth': return { ...row, state: 'unknown', detail: `Cloudflare did not let this sign-in run Workers AI (status ${answer.status})${said}`, fix: { who: 'ai', run: 'npx wrangler login', say: 'Sign in to Cloudflare again (the login includes Workers AI), then run doctor again.' } };
152
+ default: return { ...row, state: 'unknown', detail: `${model}: Cloudflare answered ${answer.status}${said}`, fix: null };
153
+ }
154
+ }
package/lib/doctor.mjs CHANGED
@@ -8,6 +8,9 @@
8
8
  * Homie tools are there: --connector yes|no)
9
9
  * Cloudflare to go online: signed in, and the account's email verified (Cloudflare checks that at the
10
10
  * first deploy; a deploy that went through proves it)
11
+ * Workers AI for AI guides only when a server's guides think with Workers AI: the model they use
12
+ * (HOMIE_BRAIN_MODEL, else the default) answers on the studio's account, not paid-only
13
+ * or retired (lib/brain-probe.mjs: one tiny call, about 0.1 of the free daily neurons)
11
14
  * Chrome for checks the two-browser check, the look pictures and playtests
12
15
  * ffmpeg recommended sound effects and the theme, trailers and hero footage
13
16
  * GitHub optional a private backup, publishing by pull request, building from the Claude app
@@ -17,12 +20,14 @@
17
20
  *
18
21
  * It is safe at any time, inside a studio or before one exists: it only reads (local files, `--version` of a few
19
22
  * tools, `wrangler whoami`, `gh auth status`, `elevenlabs auth status`, one GET to the directory and, when a fal key
20
- * is set, fal's free pricing API), never changes anything, and never prints a key, a token or an account's name.
23
+ * is set, fal's free pricing API; when a server's AI guides use Workers AI, one read of the live database and one
24
+ * one-word call to the model), never changes anything, and never prints a key, a token or an account's name or id.
21
25
  * Every check has a time limit, so it answers in seconds even with no network.
22
26
  */
23
27
  import { spawn } from 'node:child_process';
24
28
  import { existsSync, readFileSync } from 'node:fs';
25
29
  import { join } from 'node:path';
30
+ import { workersAiRow } from './brain-probe.mjs';
26
31
  import { findChrome } from './chrome.mjs';
27
32
  import { whyFailed } from './net.mjs';
28
33
  import { isOurs } from './statusline.mjs';
@@ -98,7 +103,8 @@ export async function setupStatus({
98
103
  if (!bin || !existsSync(bin)) return { signedIn: null };
99
104
  const r = await exec(bin, ['whoami', '--json'], { cwd: root, timeout: 25_000, env: { ...env, WRANGLER_SEND_METRICS: 'false', CI: '1' } });
100
105
  if (r.code === 124) return { signedIn: null, why: 'Wrangler did not answer in time' };
101
- try { const d = JSON.parse(r.stdout.slice(Math.max(0, r.stdout.indexOf('{')))); return { signedIn: Boolean(d.loggedIn), accounts: Array.isArray(d.accounts) ? d.accounts.length : null }; } catch { return { signedIn: false }; }
106
+ // The accounts' ids stay in this process (the Workers AI row calls the studio's own account); never printed.
107
+ try { const d = JSON.parse(r.stdout.slice(Math.max(0, r.stdout.indexOf('{')))); return { signedIn: Boolean(d.loggedIn), accounts: Array.isArray(d.accounts) ? d.accounts.length : null, accountIds: Array.isArray(d.accounts) ? d.accounts.map((a) => a?.id).filter(Boolean) : [] }; } catch { return { signedIn: false }; }
102
108
  })(),
103
109
  (async () => {
104
110
  const git = await exec('git', ['--version']);
@@ -182,6 +188,15 @@ export async function setupStatus({
182
188
  });
183
189
  }
184
190
 
191
+ // Workers AI, only when a server's AI guides think with it: does the model they use answer on this account?
192
+ {
193
+ const local = root ? readLocal(root) : {};
194
+ const live = Boolean(local.deployedAt || local.connectedAt || (studio?.cloudflare?.created ?? []).some((c) => String(c).startsWith('worker:')));
195
+ const bin = root ? join(root, 'node_modules', '.bin', win ? 'wrangler.cmd' : 'wrangler') : null;
196
+ const row = root ? await workersAiRow({ root, studio, env, exec, fetchFn, who, bin, remote: live && Boolean(who.signedIn || env.CLOUDFLARE_API_TOKEN), cli }) : null;
197
+ if (row) rows.push(row);
198
+ }
199
+
185
200
  // Chrome, for the checks.
186
201
  {
187
202
  const found = chrome();
@@ -254,6 +269,7 @@ export async function setupStatus({
254
269
  { feature: 'Backup and pull requests', state: ready(by.github.state), needs: ['github'] },
255
270
  { feature: 'Songs and game scores', state: ready(by.elevenlabs.state), needs: ['elevenlabs'] },
256
271
  { feature: 'Painted art and generated video', state: ready(by.fal.state), needs: ['fal'] },
272
+ ...(by['workers-ai'] ? [{ feature: 'AI guides that think (Workers AI)', state: ready(by['workers-ai'].state), needs: ['workers-ai'] }] : []),
257
273
  ];
258
274
  const now = rows.filter((r) => ['missing', 'act'].includes(r.state) && r.fix);
259
275
  const meanwhile = rows.filter((r) => r.fix?.open && r.state !== 'ok' && (r.fix.who === 'person' || r.state === 'later'));
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "v": 1,
3
- "about": "Fingerprints of every studio template version (scripts/studio-template-history.mjs): homie-studio upgrade replaces a file or an AGENTS.md section only when it is exactly one of these.",
3
+ "about": "Fingerprints of every studio template version (scripts/studio-template-history.mjs): homie-studio upgrade replaces a file or an AGENTS.md section only when it is exactly one of these. A version's named entries are for a studio the template itself names (lib/upgrade.mjs NAMED_IN_TEMPLATE), as that studio reads them.",
4
4
  "versions": {
5
5
  "0.2.0": {
6
6
  "files": {
@@ -138,14 +138,23 @@
138
138
  "music/README.md": "b8a828de9311bd28",
139
139
  "videos/README.md": "70614388cbb203c7",
140
140
  "posts/README.md": "37841aae476bd540",
141
- "site/README.md": "0fd152a20a3eb55f",
141
+ "site/README.md": [
142
+ "0fd152a20a3eb55f",
143
+ "ca131cd0472464fb"
144
+ ],
142
145
  "site/src/worker.mjs": "ca3f1630f2420ad3"
143
146
  },
144
147
  "sections": {
145
148
  "AGENTS.md": {
146
149
  "(intro)": "214aa46eb3a895d5",
147
- "## Layout": "b5f801e112d163a9",
148
- "## Commands (all through the pinned CLI in node_modules)": "63833b7006efdef4",
150
+ "## Layout": [
151
+ "b5f801e112d163a9",
152
+ "dc8ce5f6276d369e"
153
+ ],
154
+ "## Commands (all through the pinned CLI in node_modules)": [
155
+ "2af7c804c1e81d86",
156
+ "63833b7006efdef4"
157
+ ],
149
158
  "## The site": "7d3525ba39b7f262",
150
159
  "## Making games": "c204473d9b468f49",
151
160
  "## Rules": "969318425c99f5a5",
@@ -252,24 +261,44 @@
252
261
  "files": {
253
262
  "CLAUDE.md": "ddf2b2a0376202f6",
254
263
  "README.md": "684b3cca8001673c",
255
- "games/README.md": "5e5e942baa7e0372",
264
+ "games/README.md": [
265
+ "5ad32fe80c61f117",
266
+ "5e5e942baa7e0372"
267
+ ],
256
268
  "music/README.md": "b8a828de9311bd28",
257
269
  "videos/README.md": "70614388cbb203c7",
258
270
  "posts/README.md": "37841aae476bd540",
259
271
  "site/README.md": "ca131cd0472464fb",
260
- "site/src/worker.mjs": "ca3f1630f2420ad3"
272
+ "site/src/worker.mjs": "ca3f1630f2420ad3",
273
+ "HANDOFF.md": "ce7a8fd91c6bbee2"
261
274
  },
262
275
  "sections": {
263
276
  "AGENTS.md": {
264
277
  "(intro)": "214aa46eb3a895d5",
265
278
  "## Layout": "f14d641159480e6c",
266
- "## Commands (all through the pinned CLI in node_modules)": "3a3d3bef7e8d1a50",
267
- "## The site": "7d3525ba39b7f262",
279
+ "## Commands (all through the pinned CLI in node_modules)": [
280
+ "3a3d3bef7e8d1a50",
281
+ "e579cd6ce3ce342a"
282
+ ],
283
+ "## The site": [
284
+ "7c1618431bae6324",
285
+ "7d3525ba39b7f262"
286
+ ],
268
287
  "## Making games": "63069b31c87a2aa2",
269
288
  "## The Game Codex and progress": "0738ef719cef3a4c",
270
289
  "## Running live games (the back office)": "f0566f07cff04113",
271
290
  "## Rules": "969318425c99f5a5",
272
- "## Beta": "9d02638207a26e73"
291
+ "## Beta": "9d02638207a26e73",
292
+ "## Continuing a build from the Claude app": "0d68081e924ca419"
293
+ }
294
+ },
295
+ "named": {
296
+ "Homie Arcade": {
297
+ "sections": {
298
+ "AGENTS.md": {
299
+ "## Commands (all through the pinned CLI in node_modules)": "befe520937e71984"
300
+ }
301
+ }
273
302
  }
274
303
  }
275
304
  },
@@ -298,6 +327,15 @@
298
327
  "## Rules": "969318425c99f5a5",
299
328
  "## Beta": "9d02638207a26e73"
300
329
  }
330
+ },
331
+ "named": {
332
+ "Homie Arcade": {
333
+ "sections": {
334
+ "AGENTS.md": {
335
+ "## Commands (all through the pinned CLI in node_modules)": "befe520937e71984"
336
+ }
337
+ }
338
+ }
301
339
  }
302
340
  },
303
341
  "0.14.1": {
@@ -325,6 +363,15 @@
325
363
  "## Rules": "969318425c99f5a5",
326
364
  "## Beta": "9d02638207a26e73"
327
365
  }
366
+ },
367
+ "named": {
368
+ "Homie Arcade": {
369
+ "sections": {
370
+ "AGENTS.md": {
371
+ "## Commands (all through the pinned CLI in node_modules)": "befe520937e71984"
372
+ }
373
+ }
374
+ }
328
375
  }
329
376
  },
330
377
  "0.14.2": {
@@ -352,6 +399,15 @@
352
399
  "## Rules": "969318425c99f5a5",
353
400
  "## Beta": "9d02638207a26e73"
354
401
  }
402
+ },
403
+ "named": {
404
+ "Homie Arcade": {
405
+ "sections": {
406
+ "AGENTS.md": {
407
+ "## Commands (all through the pinned CLI in node_modules)": "befe520937e71984"
408
+ }
409
+ }
410
+ }
355
411
  }
356
412
  },
357
413
  "0.14.3": {
@@ -379,6 +435,15 @@
379
435
  "## Rules": "969318425c99f5a5",
380
436
  "## Beta": "9d02638207a26e73"
381
437
  }
438
+ },
439
+ "named": {
440
+ "Homie Arcade": {
441
+ "sections": {
442
+ "AGENTS.md": {
443
+ "## Commands (all through the pinned CLI in node_modules)": "befe520937e71984"
444
+ }
445
+ }
446
+ }
382
447
  }
383
448
  },
384
449
  "0.14.4": {
@@ -406,6 +471,15 @@
406
471
  "## Rules": "969318425c99f5a5",
407
472
  "## Beta": "9d02638207a26e73"
408
473
  }
474
+ },
475
+ "named": {
476
+ "Homie Arcade": {
477
+ "sections": {
478
+ "AGENTS.md": {
479
+ "## Commands (all through the pinned CLI in node_modules)": "befe520937e71984"
480
+ }
481
+ }
482
+ }
409
483
  }
410
484
  },
411
485
  "0.15.0": {
@@ -433,6 +507,15 @@
433
507
  "## Rules": "969318425c99f5a5",
434
508
  "## Beta": "9d02638207a26e73"
435
509
  }
510
+ },
511
+ "named": {
512
+ "Homie Arcade": {
513
+ "sections": {
514
+ "AGENTS.md": {
515
+ "## Commands (all through the pinned CLI in node_modules)": "befe520937e71984"
516
+ }
517
+ }
518
+ }
436
519
  }
437
520
  },
438
521
  "0.16.0": {
@@ -461,6 +544,15 @@
461
544
  "## Rules": "969318425c99f5a5",
462
545
  "## Beta": "9d02638207a26e73"
463
546
  }
547
+ },
548
+ "named": {
549
+ "Homie Arcade": {
550
+ "sections": {
551
+ "AGENTS.md": {
552
+ "## Commands (all through the pinned CLI in node_modules)": "befe520937e71984"
553
+ }
554
+ }
555
+ }
464
556
  }
465
557
  },
466
558
  "0.16.1": {
@@ -489,6 +581,15 @@
489
581
  "## Rules": "969318425c99f5a5",
490
582
  "## Beta": "9d02638207a26e73"
491
583
  }
584
+ },
585
+ "named": {
586
+ "Homie Arcade": {
587
+ "sections": {
588
+ "AGENTS.md": {
589
+ "## Commands (all through the pinned CLI in node_modules)": "befe520937e71984"
590
+ }
591
+ }
592
+ }
492
593
  }
493
594
  },
494
595
  "0.17.0": {
@@ -517,6 +618,15 @@
517
618
  "## Rules": "969318425c99f5a5",
518
619
  "## Beta": "9d02638207a26e73"
519
620
  }
621
+ },
622
+ "named": {
623
+ "Homie Arcade": {
624
+ "sections": {
625
+ "AGENTS.md": {
626
+ "## Commands (all through the pinned CLI in node_modules)": "befe520937e71984"
627
+ }
628
+ }
629
+ }
520
630
  }
521
631
  },
522
632
  "0.18.0": {
@@ -545,6 +655,89 @@
545
655
  "## Rules": "969318425c99f5a5",
546
656
  "## Beta": "9d02638207a26e73"
547
657
  }
658
+ },
659
+ "named": {
660
+ "Homie Arcade": {
661
+ "sections": {
662
+ "AGENTS.md": {
663
+ "## Commands (all through the pinned CLI in node_modules)": "eedef844b7aaf7ad"
664
+ }
665
+ }
666
+ }
667
+ }
668
+ },
669
+ "0.18.1": {
670
+ "files": {
671
+ "CLAUDE.md": "ddf2b2a0376202f6",
672
+ "README.md": "684b3cca8001673c",
673
+ "HANDOFF.md": "ce7a8fd91c6bbee2",
674
+ "games/README.md": "5ad32fe80c61f117",
675
+ "music/README.md": "a3dc042dadd39df8",
676
+ "videos/README.md": "16babc3437d5545b",
677
+ "posts/README.md": "37841aae476bd540",
678
+ "site/README.md": "ca131cd0472464fb",
679
+ "site/src/worker.mjs": "ca3f1630f2420ad3"
680
+ },
681
+ "sections": {
682
+ "AGENTS.md": {
683
+ "(intro)": "214aa46eb3a895d5",
684
+ "## Layout": "eb1942414b8af7f8",
685
+ "## Commands (all through the pinned CLI in node_modules)": "9c51f189412e6a9f",
686
+ "## The site": "ab922bb6bde23293",
687
+ "## Making games": "a8a893eeada33bf5",
688
+ "## The Game Codex and progress": "0738ef719cef3a4c",
689
+ "## Running live games (the back office)": "f0566f07cff04113",
690
+ "## Servers and AI seats": "0a332b07e22d3fce",
691
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
692
+ "## Rules": "969318425c99f5a5",
693
+ "## Beta": "9d02638207a26e73"
694
+ }
695
+ },
696
+ "named": {
697
+ "Homie Arcade": {
698
+ "sections": {
699
+ "AGENTS.md": {
700
+ "## Commands (all through the pinned CLI in node_modules)": "eedef844b7aaf7ad"
701
+ }
702
+ }
703
+ }
704
+ }
705
+ },
706
+ "0.18.2": {
707
+ "files": {
708
+ "CLAUDE.md": "ddf2b2a0376202f6",
709
+ "README.md": "684b3cca8001673c",
710
+ "HANDOFF.md": "ce7a8fd91c6bbee2",
711
+ "games/README.md": "5ad32fe80c61f117",
712
+ "music/README.md": "a3dc042dadd39df8",
713
+ "videos/README.md": "16babc3437d5545b",
714
+ "posts/README.md": "37841aae476bd540",
715
+ "site/README.md": "ca131cd0472464fb",
716
+ "site/src/worker.mjs": "ca3f1630f2420ad3"
717
+ },
718
+ "sections": {
719
+ "AGENTS.md": {
720
+ "(intro)": "214aa46eb3a895d5",
721
+ "## Layout": "eb1942414b8af7f8",
722
+ "## Commands (all through the pinned CLI in node_modules)": "9c51f189412e6a9f",
723
+ "## The site": "ab922bb6bde23293",
724
+ "## Making games": "a8a893eeada33bf5",
725
+ "## The Game Codex and progress": "0738ef719cef3a4c",
726
+ "## Running live games (the back office)": "f0566f07cff04113",
727
+ "## Servers and AI seats": "0a332b07e22d3fce",
728
+ "## Continuing a build from the Claude app": "0d68081e924ca419",
729
+ "## Rules": "969318425c99f5a5",
730
+ "## Beta": "9d02638207a26e73"
731
+ }
732
+ },
733
+ "named": {
734
+ "Homie Arcade": {
735
+ "sections": {
736
+ "AGENTS.md": {
737
+ "## Commands (all through the pinned CLI in node_modules)": "eedef844b7aaf7ad"
738
+ }
739
+ }
740
+ }
548
741
  }
549
742
  }
550
743
  }