@homie-rocks/studio 0.51.8 → 0.53.0

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/apps/APPS.md +117 -0
  3. package/apps/apps.ts +68 -1
  4. package/bin/homie-studio.mjs +8 -2
  5. package/docs/shop-migration.md +9 -0
  6. package/docs/toolkit-batch.md +49 -0
  7. package/docs/toolkit-place-patterns.md +6 -0
  8. package/functions/FUNCTIONS.md +42 -1
  9. package/functions/functions.ts +4 -0
  10. package/guides/app/PLACES.md +146 -0
  11. package/guides/app/SKILL.md +3 -0
  12. package/guides/game/SKILL.md +3 -0
  13. package/guides/shop/SKILL.md +27 -12
  14. package/lib/app-needs.mjs +59 -15
  15. package/lib/app-ready.mjs +1 -1
  16. package/lib/build.mjs +8 -2
  17. package/lib/check.mjs +40 -16
  18. package/lib/cloudflare.mjs +2 -2
  19. package/lib/dev-restart.mjs +25 -0
  20. package/lib/dev.mjs +7 -1
  21. package/lib/fork.mjs +47 -0
  22. package/lib/functions-build.mjs +3 -0
  23. package/lib/jobs.mjs +1 -0
  24. package/lib/mcp-tools.mjs +3 -3
  25. package/lib/needs-headless.mjs +4 -3
  26. package/lib/scaffold.mjs +29 -1
  27. package/lib/shop-links.mjs +7 -7
  28. package/lib/shop-mcp.mjs +88 -14
  29. package/lib/stripe-connect.mjs +46 -6
  30. package/lib/template-history.json +84 -0
  31. package/lib/tools-cli.mjs +25 -3
  32. package/lib/upgrade-places.mjs +49 -0
  33. package/lib/upgrade.mjs +2 -1
  34. package/lib/worker-config.mjs +7 -3
  35. package/links/links.d.mts +5 -0
  36. package/links/links.d.ts +2 -0
  37. package/links/links.mjs +2 -0
  38. package/links/time.mjs +20 -0
  39. package/package.json +1 -1
  40. package/rules/rules.ts +1 -1
  41. package/shop/SHOP.md +27 -12
  42. package/shop/shop.ts +3 -3
  43. package/tools/tools.ts +13 -0
  44. package/worker/app-format.mjs +6 -3
  45. package/worker/app-records.mjs +21 -5
  46. package/worker/app-shell.mjs +28 -5
  47. package/worker/app-streams.mjs +57 -0
  48. package/worker/app-tools.mjs +44 -0
  49. package/worker/app-web.mjs +23 -0
  50. package/worker/booking-calendar.mjs +26 -0
  51. package/worker/booking-holds.mjs +100 -0
  52. package/worker/booking-options.mjs +23 -0
  53. package/worker/booking-resources.mjs +55 -0
  54. package/worker/build-id.mjs +1 -1
  55. package/worker/business-events.mjs +31 -0
  56. package/worker/customer-mcp.mjs +1 -1
  57. package/worker/event-resources.mjs +1 -1
  58. package/worker/functions.mjs +5 -0
  59. package/worker/index.mjs +54 -9
  60. package/worker/local-test.mjs +10 -0
  61. package/worker/mcp-events.mjs +3 -3
  62. package/worker/mcp-tools.mjs +23 -2
  63. package/worker/mcp.mjs +3 -3
  64. package/worker/office.mjs +27 -0
  65. package/worker/pages.mjs +4 -4
  66. package/worker/records-page.mjs +3 -3
  67. package/worker/shop-links.mjs +12 -0
  68. package/worker/shop.mjs +20 -5
  69. package/worker/version.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -17,6 +17,65 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
17
17
  `npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
18
18
  file) and what the upgrade would change, and changes nothing until you agree.
19
19
 
20
+ ## [0.53.0] - 2026-10-15
21
+
22
+ **Plugin 0.53.0**
23
+
24
+ Take bookings against real resources, keep private apps usable, collect deposits reliably and turn tested examples into your own place.
25
+
26
+ ### Added
27
+
28
+ - **Bookings that fit your place:** Reserve shared resources alongside booking capacity, allocate packages, and use seasonal calendars in your place's time zone. Shared phone controls let customers choose booking options and resources; common booking argument aliases work across tools.
29
+ - **Keep upcoming slots ready to sell:** Declared `booking.slots_need_payment_links` events let your studio respond when rolling slot inventory needs payment links.
30
+ - **Apps customers can install:** Public apps can offer a direct install page with a web manifest and optional service worker. Add your own icons and offline behavior; installation support depends on the browser.
31
+ - **A quicker start from an example:** Fork editable source with new studio and app identities and fresh deployment settings. A single concept flag marks previews with a TEST / CONCEPT banner and keeps them out of the directory and search indexing.
32
+ - **Turn-based games without unnecessary physics:** Participants can have private roles and timed phases without needing physics bodies. Private fields remain visible only to their owning player.
33
+ - **Simpler Stripe setup:** `shop observe` accepts incremental public responses from Stripe's official plugin and returns the next setup plan without a hand-built observations file. New Stripe sign-ins show pairing instructions in the job's progress before approval completes.
34
+
35
+ ### Fixed
36
+
37
+ - **Deposits tied to the right booking:** Supporter and deposit links keep quantity at one. Variable deposits retain the app reference and require the saved subtotal before fulfillment; booking and AI checkout also preserve the reference in the saved order.
38
+ - **Private apps that load safely:** Authorized private app frames can load their modules through expiring asset links while anonymous modules and records stay closed. Missing record IDs explain exactly where to supply the ID.
39
+ - **Checks that reflect each screen's role:** Wall and phone permissions remain separate, each need starts in a fresh room after the baseline, and saving check evidence no longer restarts development. Brief Cloudflare startup failures retry with a visible notice.
40
+ - **Smoother shop upgrades:** Existing billing portals are discovered when older verifications lack their IDs. Webhook setup fills missing account IDs after checking link ownership with the saved Stripe login; reused logins no longer show unnecessary pairing instructions.
41
+
42
+ ### Upgrade notes
43
+
44
+ - Apply `0030_booking_resources.sql` after the existing booking migrations to use resource-aware bookings.
45
+ - Concept mode labels a preview; it does not isolate payment credentials or data. Installed public app surfaces run as same-origin studio code; private app shells keep their sandbox.
46
+
47
+ ## [0.52.0] - 2026-10-15
48
+
49
+ **Plugin 0.52.0**
50
+
51
+ Run your place with trusted actions across screens and AI, live business records, shared booking capacity and stronger proofs that it works for your customers.
52
+
53
+ ### Added
54
+
55
+ - **One trusted action from any screen or AI:** App screens call the same studio tools as MCP through `createAppTools`, with strict input schemas, server role checks and versioned writes.
56
+ - **Screens that stay in step:** Live record subscriptions and presence use the existing Lobby without polling or a dummy game room. Supported app context supplies your public address and a local proof clock; isolated proofs stay separate.
57
+ - **Business events with useful history:** Record changes include the previous value. Trusted functions publish schema-checked business feeds with durable receipts that prevent duplicate events.
58
+ - **Bookings that share one capacity:** App holds and Stripe session checkout use the same capacity row, including group quantities, ownership, version checks and scheduled release. Confirmations distinguish unpaid reservations from verified payments.
59
+ - **Proofs for difficult situations:** Needs can force disabled clicks to check server refusals, race actions from separate browsers, restart local development and reload without WebGL.
60
+ - **Time at your place:** IANA time zones support local scheduling and explicitly reject nonexistent or ambiguous wall times around clock changes.
61
+
62
+ ### Fixed
63
+
64
+ - **Shops that survive upgrades:** Matching 0.51.6 Stripe plugin verifications remain valid; real shop, slot or site changes explain what needs verification. Unfinished setup still builds and deploys with checkout closed and a clear next step.
65
+ - **Room for your whole catalogue:** Verified public links ship as a JSON asset instead of a size-limited Worker text binding, including catalogues with more than 100 links. Missing assets keep checkout closed.
66
+ - **Faster Stripe setup and replanning:** Saved public observations avoid repeated discovery. Explicit refresh paginates expanded Payment Link lists, refreshes changed links and preserves verification freshness. Private webhook setup installs Stripe's official npm CLI automatically when missing, then continues to browser approval.
67
+
68
+ ### Changed
69
+
70
+ - **Guides grounded in six real builds:** App and game guides include place patterns, security alternatives and small examples that compile against the toolkit.
71
+ - **Cleaner upgrades:** Recognized polling workarounds and exact obsolete capability notes are retired; custom business logic remains available for review.
72
+
73
+ ### Upgrade notes
74
+
75
+ - Apply migrations `0027_app_live.sql`, `0028_business_events.sql` and `0029_booking_holds.sql` in order.
76
+ - Replace refresh timers with record subscriptions, and private origin, peer and clock reads with `createAppContext`.
77
+ - Shared local booking holds require Stripe session checkout. Keyless Payment Links cannot claim those holds and are explicitly refused.
78
+
20
79
  ## [0.51.8] - 2026-10-15
21
80
 
22
81
  **Plugin 0.51.8**
package/apps/APPS.md CHANGED
@@ -222,3 +222,120 @@ and pass a role when deliberately testing the customer experience.
222
222
  `check --shots` may capture outside the studio, but persistent needs evidence copies
223
223
  those pictures into `.studio/checks` and stores studio-relative paths. Keep that
224
224
  folder with the Codex evidence. Deploy can render it without outside-path warnings.
225
+
226
+ ## Screens and AI share tools
227
+
228
+ Import `createAppTools` from `@homie-rocks/studio/apps`, then call
229
+ `await tools.call('place_book', {slot, request})`. It runs the studio's `defineTool`
230
+ handler, with its strict input schema, audience checks, rate policy and versioned
231
+ `context.database`. The shell supplies identity; message fields cannot grant a
232
+ role. Only studio-defined tools are exposed. Priced tools still use MCP's payment
233
+ approval flow. Dispose the client on unmount. Use a stable request ID across
234
+ retries; a lost response does not mean the write failed. Keep rules in a shared
235
+ `domain.ts`, but only the server handler commits them.
236
+
237
+ ## Live records and frame context
238
+
239
+ `records.subscribe(() => void refresh())` returns an unsubscribe function. It
240
+ notifies on connection/reconnection and when the collection changes; `refresh`
241
+ calls `records.list()` to obtain currently authorized data. Do an initial read as
242
+ well. Coalesce overlapping refreshes in a busy screen. This uses the existing
243
+ Lobby, without a rules file or a dummy map. Notifications contain collection names,
244
+ not records or private IDs. A 15-second server heartbeat repairs missed wakes;
245
+ screens do not poll. Closing the page closes its stream. Apply `0027_app_live.sql`
246
+ on upgrade before starting the new Worker.
247
+
248
+ `createAppContext()` provides `ready`, `current`, `subscribe`, `now()` and `dispose`.
249
+ After `await context.ready`, `current` has `app`, `origin`, public `url`, and
250
+ `presence` (connected screens in this app/room and isolated proof scope, including
251
+ the current screen). Presence is screens, not unique people or paid customers.
252
+ The public address excludes role grants and booking credentials. Do not inspect
253
+ `HOMIE_NET` or cast room internals to read peers.
254
+
255
+ For a signed local proof session, the shell supports
256
+ `window.homieApp.setTestClock(epochMilliseconds)` and `null` to restore wall time;
257
+ frame code reads `context.now()`. Production shells refuse the setter. This is a
258
+ view clock, never authority for capacity, payment or server deadlines. Scheduled
259
+ needs supply their own explicit `scheduledTime`.
260
+
261
+ See `guides/app/PLACES.md` for the six-build recipe and small typed examples.
262
+
263
+ ## One capacity budget for holds and Stripe
264
+
265
+ A collection with `booking: {slots:'slots', item:'ticket'}` uses the same
266
+ `shop_bookings` row from hold through payment. A trusted tool can call:
267
+
268
+ ```ts
269
+ const held = await context.bookings.hold({
270
+ app: 'place', collection: 'bookings', slot: 'morning', quantity: 2,
271
+ id: input.request, expiresAt: Date.now() + 60_000 // choose the hold window on the server
272
+ });
273
+ ```
274
+
275
+ Require a signed-in caller, or pass a private, random 32–128 hex-character `token`
276
+ for a guest. Keep the request ID stable on retries; a retry returns the original expiry and never extends it. The returned `id` is
277
+ scoped by app/collection/proof; it is public identification, never authorization.
278
+ Only the owner (or holder of that private token) can `get`, `confirm` or `release`.
279
+ Confirm/release take `{id, version, token?}`. Another owner's ID is not permission.
280
+ The entire group quantity reserves atomically or fails; there is no partial group.
281
+ Slot reads report availability including held and confirmed quantities.
282
+
283
+ Use `shop.book('ticket', {collection:'bookings', slot:'morning', hold:held.id,
284
+ token}, 2)` to pay for that same row. Session checkout claims it once, in the order
285
+ transaction. A second checkout cannot take it; verified payment updates it.
286
+ `confirmed` means a capacity commitment; only `paid:true` means payment was
287
+ verified. Do not label a confirmation as a paid receipt.
288
+
289
+ A scheduled owner function calls
290
+ `context.bookings.releaseExpired({app:'place',collection:'bookings',now:event.data.scheduledTime})`.
291
+ Only unconfirmed local holds expire this way. After checkout claims a hold,
292
+ Stripe's verified payment/expiry/refund owns its lifecycle; a local timer cannot
293
+ release a possibly paid seat. Keyless Payment Links cannot safely claim local
294
+ holds: configure session checkout, or keep using their existing pending-to-paid
295
+ path. The toolkit refuses to mix active keyless links with new local holds.
296
+ Apply `0029_booking_holds.sql`; public projections never include actors or tokens.
297
+
298
+ ## Adversarial needs
299
+
300
+ Actions inside `concurrent` start together on separate browser contexts:
301
+
302
+ ```json
303
+ {"concurrent":[{"surface":"phone","selector":"#book","click":true},{"surface":"phone-two","selector":"#book","click":true}]}
304
+ ```
305
+
306
+ Assert exactly one saved booking afterwards. `{"surface":"staff","selector":"#close","forceClick":true}`
307
+ temporarily bypasses the disabled attribute, clicks, and restores it: assert the
308
+ server refusal and unchanged data. It is not a permission grant.
309
+ `{"surface":"phone","capabilities":{"webgl":false}}` installs a WebGL blocker
310
+ before reloading that surface, leaving ordinary controls functional. Set `true`
311
+ to restore WebGL on another reload. `{"surface":"phone","clock":1792800000000}`
312
+ sets the supported local frame clock.
313
+
314
+ `{"restart":true}` asks the authenticated local dev controller to restart its own
315
+ Wrangler child, keeps disk state and proof sessions, and waits for readiness.
316
+ It never kills a PID discovered by port. Start the current toolkit's dev server;
317
+ remote URLs and old servers without this controller refuse or time out. Put
318
+ restart/capability steps outside concurrent groups, then assert saved state.
319
+
320
+ ## Place time and upgrades
321
+
322
+ ```ts
323
+ import {placeTime} from '@homie-rocks/studio/links';
324
+ const startsAt = placeTime('2027-03-20', '09:30', 'America/Vancouver');
325
+ ```
326
+
327
+ Store the occurrence's local date/time and IANA zone in place config; derive epoch
328
+ milliseconds with the runtime's zone database. Never append `-07:00`. Invalid dates
329
+ and spring gaps are refused; repeated autumn times require explicit
330
+ `{disambiguation:'earlier'}` or `'later'`. Generate future occurrence dates from the
331
+ calendar rule, not a constant “next Saturday”. Update runtime zone data with the
332
+ runtime itself.
333
+
334
+ `upgrade --diff` includes recognized obsolete capability notes and standalone
335
+ 2–3-second named refresh timers over `createAppRecords` clients. `upgrade --apply`
336
+ replaces those timers with subscriptions and corrects exact stale notes without
337
+ marking old needs passed. Timer conversion uses the studio’s installed TypeScript
338
+ compiler; run `npm install` in the studio first if it is missing. Review the diff and rerun the affected needs. Custom
339
+ combined timer/jobs, hand-written routes and business-event mirrors need a semantic
340
+ migration to the APIs above; they are not automatically deleted. The owner's
341
+ words, historical evidence and unresolved integration work remain intact.
package/apps/apps.ts CHANGED
@@ -4,7 +4,9 @@ export interface AppRecord<T> { id: string; data: T; version: number; updatedAt:
4
4
  export function createAppRecords<T extends Record<string, unknown>>({ collection }: { collection: string }) {
5
5
  if (!/^[a-z][a-z0-9-]{0,39}$/.test(collection)) throw new Error('invalid collection');
6
6
  const pending = new Map<string, { resolve: (v: any) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>();
7
+ const listeners = new Set<() => void>();
7
8
  const receive = (e: MessageEvent) => {
9
+ if(e.source === parent && e.data?.type === 'homie-app-changed' && e.data.collections?.includes(collection)) { for(const listener of listeners) listener(); return; }
8
10
  if (e.source !== parent || e.data?.type !== 'homie-app-result') return;
9
11
  const p = pending.get(e.data.id); if (!p) return;
10
12
  pending.delete(e.data.id); clearTimeout(p.timer);
@@ -18,11 +20,76 @@ export function createAppRecords<T extends Record<string, unknown>>({ collection
18
20
  parent.postMessage({ type: 'homie-app', id, collection, method, record, data, version }, '*');
19
21
  });
20
22
  return {
23
+ subscribe(listener: () => void) { listeners.add(listener); parent.postMessage({type:'homie-app-subscribe',collections:[collection]}, '*'); return () => {listeners.delete(listener);}; },
21
24
  async list(): Promise<AppRecord<T>[]> { return (await ask('GET')).records; },
22
25
  async get(id: string): Promise<AppRecord<T> | null> { return (await ask('GET', id)).records[0] ?? null; },
23
26
  create: (data: T, id = randomId()) => ask('POST', id, data),
24
27
  update: (record: AppRecord<T>, data: T) => ask('PUT', record.id, data, record.version),
25
28
  remove: (record: AppRecord<T>) => ask('DELETE', record.id, undefined, record.version),
26
- dispose() { removeEventListener('message', receive); for (const p of pending.values()) { clearTimeout(p.timer); p.reject(new Error('disposed')); } pending.clear(); },
29
+ dispose() { listeners.clear(); removeEventListener('message', receive); for (const p of pending.values()) { clearTimeout(p.timer); p.reject(new Error('disposed')); } pending.clear(); },
27
30
  };
28
31
  }
32
+
33
+ /** Call the same trusted studio definition used by MCP. The shell supplies identity. */
34
+ export function createAppTools() {
35
+ const pending = new Map<string, {resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout>}>();
36
+ const receive = (e: MessageEvent) => {
37
+ if (e.source !== parent || e.data?.type !== 'homie-app-result') return;
38
+ const p = pending.get(e.data.id); if (!p) return;
39
+ pending.delete(e.data.id); clearTimeout(p.timer);
40
+ if (e.data.result?.ok) p.resolve(e.data.result.data); else p.reject(new Error(e.data.result?.error ?? 'tool failed'));
41
+ };
42
+ addEventListener('message', receive);
43
+ return {
44
+ call<T = unknown>(name: string, input: Record<string, unknown> = {}): Promise<T> {
45
+ return new Promise((resolve, reject) => {
46
+ const id = randomId();
47
+ const timer = setTimeout(() => { pending.delete(id); reject(new Error('app shell did not answer')); }, 15000);
48
+ pending.set(id, {resolve: value => resolve(value as T), reject, timer});
49
+ parent.postMessage({type:'homie-app-tool', id, name, input}, '*');
50
+ });
51
+ },
52
+ dispose() { removeEventListener('message', receive); for (const p of pending.values()) { clearTimeout(p.timer); p.reject(new Error('disposed')); } pending.clear(); }
53
+ };
54
+ }
55
+
56
+ export interface AppContext { app: string; origin: string; url: string; presence: number; now: number | null }
57
+ /** Public self address and live presence supplied by the authenticated shell. */
58
+ export function createAppContext() {
59
+ let value: AppContext | null = null;
60
+ const listeners = new Set<(context: AppContext) => void>();
61
+ let resolve!: (context: AppContext) => void;
62
+ const ready = new Promise<AppContext>(r => {resolve = r;});
63
+ const receive = (e: MessageEvent) => {
64
+ if(e.source !== parent || e.data?.type !== 'homie-app-context') return;
65
+ value = Object.freeze(e.data.context) as AppContext;
66
+ resolve(value); for(const listener of listeners) listener(value);
67
+ };
68
+ addEventListener('message', receive);
69
+ parent.postMessage({type:'homie-app-context'}, '*');
70
+ parent.postMessage({type:'homie-app-subscribe',collections:[]}, '*');
71
+ return {
72
+ ready,
73
+ get current() {return value;},
74
+ now: () => value?.now ?? Date.now(),
75
+ subscribe(listener: (context: AppContext) => void) {listeners.add(listener); if(value)listener(value); return () => {listeners.delete(listener);};},
76
+ dispose() {removeEventListener('message',receive);listeners.clear();}
77
+ };
78
+ }
79
+
80
+ /** Native accessible choices; submit value() to the same booking tool used by an AI. */
81
+ export function createBookingChoices({container,options=[],resources=[]}:{container:HTMLElement;options?:Array<{id:string;name:string}>;resources?:Array<{id:string;name:string}>}) {
82
+ const fields=new Map<string,HTMLSelectElement>();
83
+ const group=document.createElement('fieldset');
84
+ const legend=document.createElement('legend');legend.textContent='Choose an option';group.append(legend);
85
+ for(const [name,choices] of [['option',options],['resource',resources]] as const) {
86
+ if(!choices.length)continue;
87
+ const label=document.createElement('label');label.textContent=name==='option'?'Option':'Resource';
88
+ const select=document.createElement('select');select.name=name;
89
+ const empty=document.createElement('option');empty.value='';empty.textContent=name==='option'?'Choose an option':'Any available resource';select.append(empty);
90
+ for(const choice of choices){const option=document.createElement('option');option.value=choice.id;option.textContent=choice.name;select.append(option);}
91
+ label.append(select);group.append(label);fields.set(name,select);
92
+ }
93
+ container.append(group);
94
+ return {value:():{option?:string;resource?:string}=>Object.fromEntries([...fields].filter(([,select])=>select.value).map(([name,select])=>[name,select.value])),dispose:()=>group.remove()};
95
+ }
@@ -911,6 +911,7 @@ async function main() {
911
911
  }
912
912
  // The starter library and the optimiser work anywhere; the rest of style and assets inside a studio (lib/art-cli.mjs).
913
913
  if (cmd === 'assets' && ['find', 'optimise', 'optimize'].includes(sub)) return assetsCommand(null, sub, positional, flags, { log });
914
+ if(cmd==='fork')return (await import('../lib/fork.mjs')).forkStudio(positional[1],positional[2],{name:flags.get('name'),slug:flags.get('slug'),ids:flags.get('ids')?JSON.parse(flags.get('ids')):{}});
914
915
  const root = requireStudio();
915
916
  // Keep guidance visible to AI callers using --json as well as human output.
916
917
  if (['build', 'check', 'upgrade'].includes(cmd)) {
@@ -1111,18 +1112,23 @@ async function main() {
1111
1112
  if (sub === 'init') return shopInit(root, { supporter: flags.has('supporter'), giving: flags.has('giving'), currency: flags.get('currency') ?? null, price: flags.get('price') ?? 500, managed: flags.has('managed') });
1112
1113
  if(sub==='listen')return (await import('../lib/shop-listen.mjs')).shopListen(root,{profile:flags.get('profile'),port:flags.get('port')??8787,log});
1113
1114
  if (sub === 'check') return shopCheck(root);
1115
+ if(sub==='observe') {
1116
+ const {recordShopObservations}=await import('../lib/shop-mcp.mjs');
1117
+ const input=JSON.parse(readFileSync(0,'utf8'));
1118
+ return recordShopObservations(root,Array.isArray(input)?{observations:input}:input);
1119
+ }
1114
1120
  if (sub === 'plan' || sub === 'verify') {
1115
1121
  const {shopMcpPlan,saveMcpVerification}=await import('../lib/shop-mcp.mjs');
1116
1122
  const file=flags.get('have');
1117
1123
  const input=file?JSON.parse(readFileSync(file==='-'?0:file,'utf8')):{};
1118
1124
  return (sub==='verify'?saveMcpVerification:shopMcpPlan)(root,input);
1119
1125
  }
1120
- if (sub === 'webhook') return shopConnect(root,{webhookOnly:true,live:flags.has('live'),renew:flags.has('renew'),profile:flags.get('profile'),log:line=>process.stderr.write(`${line}\n`)});
1126
+ if (sub === 'webhook') return shopConnect(root,{onEvent:event=>process.stderr.write('HOMIE_EVENT '+JSON.stringify(event)+'\n'),webhookOnly:true,live:flags.has('live'),renew:flags.has('renew'),profile:flags.get('profile'),log:line=>process.stderr.write(`${line}\n`)});
1121
1127
  if (sub === 'connect' && flags.has('local')) {
1122
1128
  if(flags.has('live'))throw new Error('Local checkout requires test mode');
1123
1129
  return (await import('../lib/shop-local.mjs')).shopConnectLocal(root,{url:flags.get('url'),profile:flags.get('profile')});
1124
1130
  }
1125
- if (sub === 'connect') return shopConnect(root, { managed: flags.has('managed') ? true : null, live: flags.has('live'), manual: flags.has('manual'), fromClipboard: flags.has('from-clipboard'), renew: flags.has('renew'), log: (line) => process.stderr.write(`${line}\n`) });
1131
+ if (sub === 'connect') return shopConnect(root, { onEvent:event=>process.stderr.write('HOMIE_EVENT '+JSON.stringify(event)+'\n'), managed: flags.has('managed') ? true : null, live: flags.has('live'), manual: flags.has('manual'), fromClipboard: flags.has('from-clipboard'), renew: flags.has('renew'), log: (line) => process.stderr.write(`${line}\n`) });
1126
1132
  if (sub === 'disconnect') return shopDisconnect(root);
1127
1133
  if (sub === 'catalog') {
1128
1134
  const from = flags.get('have');
@@ -0,0 +1,9 @@
1
+ # Keyless shop setup and migration
2
+
3
+ Use `stripe_shop_plan` with Stripe's official plugin to perform the returned operations. Supply each new response as `{method,path,params,body}` in `observations`; earlier responses are saved automatically. Product and price discovery uses metadata searches. Payment Links use Stripe's paginated list API with expanded line items, then match studio, origin and revision metadata locally (Stripe has no Payment Link search endpoint). No creation is planned before discovery completes.
4
+
5
+ For CLI workflows, pipe an array of these responses, or `{observations:[...]}`, into `homie-studio shop observe`. It saves the responses and returns the next dependency batch. `shop plan` resumes without `--have`. Never include credentials or webhook creation responses.
6
+
7
+ Old verifications without a billing portal ID discover and reuse the existing portal before planning creation. Old verifications without an account ID adopt the official CLI's signed-in account only after reading and matching their saved Payment Links. A known different account is still refused. Verification freshness and configuration checks still apply.
8
+
9
+ `stripe_webhook` returns a job. Read `studio_job`: while browser approval is pending, `events` contains `{type:"stripe.pairing",code,url}`. Show that code and URL to the owner. A saved valid Stripe login emits no pairing event and needs no new approval. Secrets never enter the event.
@@ -0,0 +1,49 @@
1
+ # App references, private frames and checks
2
+
3
+ `shop.book(item, {collection, slot}, quantity, {reference})` accepts your app's booking reference. `studio_checkout` over MCP accepts `reference` alongside `lines`, `game` and optional `booking: {collection,slot}`. The saved order and `order.paid` event retain this value. It identifies your record; it is not proof of payment. Confirm the signed payment before fulfilling it.
4
+
5
+ HTTP record creation puts the ID in `/<app>/api/app/records/<collection>/<id>` and fields in `{data:{...}}`. The `app_records` MCP tool and `ctx.records` put `id` at the top level of their arguments, alongside `app` and `collection`. A missing HTTP ID now returns the exact URL format. IDs use 1–64 letters, digits, underscores or hyphens.
6
+
7
+ Private apps authorize the owner page first, then give its sandboxed frame a signed asset path valid for five minutes. Relative modules and chunks retain that path, without the owner's cookie. Asset capabilities cannot read records, call tools or authorize room sockets. Reload the owner page for a new path after expiry. Responses are private and uncached; unsigned private modules and records stay closed.
8
+
9
+ `check` gives the baseline action and every Codex need separate browser contexts and fresh rooms. Isolated record namespaces follow those rooms. `check --url https://studio.example` uses the saved owner MCP connection to mint 15-minute app-role sessions. These sessions cannot grant global owner access or access another app, and their record writes stay in the test namespace. Test slots never emit production payment-link work. With `--no-isolate`, records are explicitly shared. A need's `role` applies to its primary surface (`surface`, the first action’s surface, or the phone/matching role in observation-only needs); other surfaces retain their declared roles. Use an individual step's `role` for an explicit override. `--shots <directory>` captures passing and failing UI needs.
10
+
11
+ `dev` ignores changes to the authoring `CODEX.md` when detecting runtime edits, so saving needs evidence does not interrupt an active check. It reports when its requested port is busy and gives the actual URL. `check` without `--url` follows the running studio's registered dev URL. MCP connections retry a brief Cloudflare 1042 startup response up to three times and report each retry. Other errors return normally. Records-only apps do not inherit game-player cost estimates.
12
+
13
+ For turn-based games, a `player:true` entity can omit `body` and movement code. Declare its hidden role with `f.text(32, {visibility:'owner'})`; use `visibility:'server'` for secrets no client should receive. Keep hidden roles out of public shared fields and effects. The server projects snapshots for the receiving seat, including spectators. Use `room.start` and `room.on` with `world.after(ticks, event, data)` for phase deadlines; timers belong to the room and require no physics body. The normal server host remains required for secrecy.
14
+
15
+ Set one studio flag, `"concept":true`, and build for a concept preview. The site returns no directory manifest, disallows indexing and displays a fixed TEST / CONCEPT banner on HTML surfaces. Remove the flag and rebuild to leave concept mode. This labels a test site; it does not sandbox payment credentials or data.
16
+
17
+ A public records-only app can opt into a direct install surface:
18
+
19
+ ```json
20
+ {"web":{"manifest":{"name":"Guest app","short_name":"Guest","icons":[]},"serviceWorker":"sw.js"}}
21
+ ```
22
+
23
+ Place the service worker in the app's public files. `/app-id/install` serves the compiled app directly, with a manifest and optional service-worker registration. Records and app-tool helpers use the direct bridge. Add suitable icons and implement the offline behaviour in your service worker. The installed surface runs as same-origin studio code; signed-in/private app shells retain their sandbox. The manifest scope is the app's address, and installation support depends on the browser.
24
+
25
+ Fork editable example source in one command:
26
+
27
+ ```sh
28
+ homie-studio fork /path/to/example /path/to/new-place --name 'New Place' --slug new-place
29
+ ```
30
+
31
+ The command renames the studio, app/game IDs, record/slot IDs declared in JSON or JavaScript/TypeScript objects and matching source references. Use `--ids '{"old-id":"new-id"}'` for explicit overrides or long names. It excludes local state, credentials, dependencies, generated output and deployment identities, then generates a fresh Worker configuration. It refuses an existing destination and leaves the example unchanged. Install dependencies and run the returned check before publishing.
32
+
33
+ Supporter and deposit Payment Links have quantity fixed at one. For a variable deposit, declare `kind:"deposit", price:"choose"`, then call `shop.buy(id, 1, amount, {reference})`. Amounts use currency minor units. The link prefills the saved amount and carries the order ID, which maps back to the app reference. A signed payment with a different subtotal does not fulfill the order. Compute and validate the amount in the app's trusted booking operation and compare the payment event to its saved booking amount before confirmation; a buyer-supplied reference alone grants no authority.
34
+
35
+ Declare a function or AI event feed for `booking.slots_need_payment_links`. For example, in `studio.json`:
36
+
37
+ ```json
38
+ {"mcp":{"events":{"slot-links":{"event":"booking.slots_need_payment_links","audience":"owner"}}}}
39
+ ```
40
+
41
+ Creating or updating a booking's slot record without a matching capacity link inserts the event in the same database transaction. Its `slots` array contains `app`, booking `collection`, slot collection `slots`, `slot`, `item` and `capacity`. The owner's AI reads the full published slot inventory, calls `stripe_shop_plan`, performs its operations through Stripe's official plugin, records responses, verifies, then builds the updated public configuration. Never create webhooks through model-visible plugin calls. Unsubscribed events are not retained. Free items require no payment link.
42
+
43
+ Resource-aware bookings declare `booking: {slots:"slots", item:"ticket", resources:"resources"}`. Resource records hold `capacity`, optional `skills` arrays and `attributes` objects. Slots hold ISO `start`/`end`, candidate `resources`, and optional required `skills`/`attributes`. `ctx.bookings.hold` accepts `resource` (preferred resource), `option` and `addons`. An option can contain `components`, each naming candidate `resources`, optional `skills`/`attributes`, `units`, `offsetMinutes` and `durationMinutes`. Every component consumes capacity; overlapping allocations are checked atomically by the database. Release frees them. Editing an allocated resource or moving an allocated booking is refused: release and allocate the replacement deliberately. Resource checkout requires the allocated hold ID, so a raw checkout cannot bypass capacity. Paid exclusive holds still require session checkout; keyless inventory cannot share an exclusive unpaid hold.
44
+
45
+ Set the app's IANA `timeZone`, then use `ctx.bookings.calendar({app,startDate,endDate,time,days,capacity})` for an inclusive season of multi-day slots. `durationMinutes` handles shorter slots; `weekdays` uses Sunday=0 through Saturday=6. Persist the returned records through `ctx.records`. DST gaps and ambiguous wall times are refused; supply an explicit offset timestamp for such exceptional slots.
46
+
47
+ The public `studio_book` MCP tool uses the same hold operation. It accepts `time` as `slot`, `preferredResource`/`preferred_resource` as `resource`, and `addons`, `addOns` or `add-ons` as either an array of IDs or an object of quantities. Conflicting aliases are refused. Supply a stable request `id`, `expiresAt` within 15 minutes (or the booking declaration’s `holdMinutes`, up to one day), and a private random hex `token` for anonymous bookings. Keep that token private for later management.
48
+
49
+ The phone helper `createBookingChoices({container,options,resources})` from `@homie-rocks/studio/apps` renders accessible native selects. Submit its `value()` (`{option,resource}`) to the app's trusted booking tool. The phone and customer AI therefore use the same option/resource IDs and capacity rules. Call `dispose()` when the surface closes.
@@ -1,5 +1,11 @@
1
1
  # Building places: layout, authority and recovery
2
2
 
3
+ Start with [the six-build place recipe](../guides/app/PLACES.md) and
4
+ [the app contracts](../apps/APPS.md) for screen-to-tool calls, live records,
5
+ shared booking holds, named events, IANA time and adversarial needs. The small
6
+ CAS example below teaches authority; paid inventory uses `context.bookings` so
7
+ checkout and app capacity remain one system.
8
+
3
9
  ## TV join safe area
4
10
 
5
11
  On `/<id>/tv`, the shell sets `body.homie-join-visible` while the join card is present. It publishes CSS pixel values on the document root in both the shell and the sandboxed app: `--homie-join-x`, `--homie-join-y`, `--homie-join-width`, `--homie-join-height`, and `--homie-join-inset-top/right/bottom/left`. The insets include the card's distance from its configured screen edge. Inactive edges are zero; no card means all values are zero and the body class is absent. Measurements update on resize and iframe navigation. The card is at most 480 × 280 CSS pixels; its address wraps within that box.
@@ -26,7 +26,21 @@ export default defineFunction<{ order: string }>({
26
26
  ```
27
27
 
28
28
  Build discovers and type-checks `functions/*.ts`. Handlers have the studio tool
29
- context and run with the studio's authority. They can change records, send a
29
+ context and run with the studio's authority. Handlers can call `ctx.records` (or
30
+ `context.records` when the parameter is named `context`) to read, create, update
31
+ and delete declared app records. Supply `app`, `collection`, and `id` at the top
32
+ level; put the record fields inside `data`. Updates and deletes also require
33
+ the current `version`. These calls use the same validation and permissions as
34
+ the app record API.
35
+
36
+ ```ts
37
+ await context.records({
38
+ app: 'calendar', collection: 'notes', id: event.id,
39
+ operation: 'create', data: { message: 'Ready' },
40
+ });
41
+ ```
42
+
43
+ Handlers They can change records, send a
30
44
  room event through declared capabilities, call an external service, or implement
31
45
  another business workflow. Treat event data as data, not instructions.
32
46
 
@@ -383,3 +397,30 @@ A booking payment can be tested with:
383
397
  Create any records the handler reads first. First fire registers subscriptions
384
398
  before writing the event, so the first event is delivered too. No Stripe checkout
385
399
  or browser approval is needed for this explicitly synthetic local event.
400
+
401
+ ## Previous records and named business events
402
+
403
+ `record.changed` data includes `previous`: `null` for insert, the old data for
404
+ update/delete. `data` remains the new value for update and the deleted value for
405
+ delete. Both are captured by the same database transaction as the write. Import
406
+ `RecordChanged<T>` and `ScheduledEvent` from `@homie-rocks/studio/functions`;
407
+ scheduled time is `event.data.scheduledTime` (epoch milliseconds), not `event.at`.
408
+
409
+ Declare a feed in `studio.json` and its allowed payload:
410
+
411
+ ```json
412
+ {"mcp":{"events":{"arrivals":{"event":"place.arrived","audience":"owner","schema":{"type":"object","properties":{"booking":{"type":"string"}},"required":["booking"],"additionalProperties":false}}}}}
413
+ ```
414
+
415
+ A function declares `publish: ['arrivals']`, then calls
416
+ `await context.events.publish({feed:'arrivals', id:event.id, data:{booking:'...'}})`.
417
+ Use the source event ID, or a stable business request ID; repeats with the same
418
+ payload are no-ops, and changed payloads under the same ID are refused. Publication
419
+ and its deduplication receipt commit atomically. Receipts survive feed pruning.
420
+ Do not mirror every record or build a sleep-and-retry log. Keep contact details
421
+ out of operational events. Owner AIs use the existing feed cursor, MCP subscription
422
+ or declared webhook; functions never send email/SMS themselves.
423
+
424
+ Apply `0028_business_events.sql` on upgrade. Local proof publications carry their
425
+ scope and do not appear in production feed reads. Event feeds retain their declared
426
+ type; give different business feeds distinct types when their content differs.
@@ -8,6 +8,10 @@ export interface StudioFunction<Data = unknown> {
8
8
  /** Permit an explicit owner replay command to consume retained history. */
9
9
  replay?: boolean;
10
10
  events?: StudioTool['events'];
11
+ publish?: string[];
11
12
  handler(event: StudioEvent<Data>, context: ToolContext): unknown | Promise<unknown>;
12
13
  }
13
14
  export function defineFunction<Data = unknown>(definition: StudioFunction<Data>): StudioFunction<Data> { return definition; }
15
+
16
+ export interface RecordChanged<T = Record<string, unknown>> { app: string; collection: string; record: string; version: number; operation: "insert" | "update" | "delete"; data: T; previous: T | null }
17
+ export interface ScheduledEvent { function: string; cron: string; scheduledTime: number }
@@ -0,0 +1,146 @@
1
+ # Build a place people recognize
2
+
3
+ Use this with APPS.md and TOOLS.md. Six place builds exposed the same rule:
4
+ make the owner's dream visible, with one trusted operation behind every action.
5
+
6
+ 1. **Start with their paragraph.** Quote the owner's actual words in CODEX.md,
7
+ including sensory details and operating rules. Write one executable need per
8
+ clause. Describe Scene as data: “held = white ball; confirmed = gold; mine =
9
+ gold ring; empty capacity = empty pegs.” List phone, wall, signed-in staff,
10
+ customer AI and owner AI with their addresses and actions. Record what is
11
+ simulated, evidence viewports and who reviewed it. Local review is not independent.
12
+ 2. **Make records visible.** Give each meaningful record an object, each state a
13
+ material, and the visitor's object a clear marker. Arrivals pop and chime;
14
+ outcomes become bags, cups or flags. Empty capacity can be negative space.
15
+ 3. **Spend geometry where it reads.** Use one layout curve for paths, trees and
16
+ objects. Add a gradient sky shader and sun, fog matching the horizon, terrain
17
+ vertex colours by zone, and seeded low-poly local vegetation. Animate water
18
+ and a flag in vertex shaders; avoid recomputing a large mesh's normals every
19
+ frame. Ease role-specific cameras with `1 - Math.exp(-dt * 4)`. Cap pixel
20
+ ratio on phones, pause hidden frames, respect reduced motion. Generate a
21
+ filtered noise ambient bed after the first sound opt-in; its gain follows the
22
+ scene. Compose a share card from the live scene and actual outcome.
23
+ 4. **One set of rules.** Put validation and transitions in `domain.ts`; the view
24
+ may preview them, but a `defineTool` handler decides and saves. Screens call
25
+ `createAppTools().call`, AIs call the same definition over MCP. Use strict
26
+ schemas, bounded inputs, versioned writes with revalidation on conflict, and
27
+ a caller-generated request ID. A repeated ID with different data is refused.
28
+ Do not rebuild the operation with browser record writes or a custom route.
29
+ 5. **Plan the AI's whole visit.** Provide `place_info` (“Call this first”: rules,
30
+ availability, prices, notice), `place_book` (all choices in one call, bounded
31
+ enums from config, a private management credential and a `next` sentence),
32
+ and `place_booking` (owner-authorized lookup/change/cancel). Never expose
33
+ management credentials in public records. Owner tools read an outbox, mark
34
+ messages sent, consume event feeds, and repeat next month by a calendar rule.
35
+ Homie composes; the owner's AI sends through its own connector. A repeat
36
+ invites past visitors; it never silently enrols them.
37
+ 6. **Operate from one config.** Put the place's IANA zone, schedule rule, capacity,
38
+ prices, add-ons and notice in one config. Derive UI, tools and `shop.json` from
39
+ it during build. Do not maintain a second price list. Model holds → confirm →
40
+ scheduled release, with fixed spot IDs and version checks on reuse. Group
41
+ allocation must be atomic, never a loop that reports partial success. Use the
42
+ toolkit's booking capacity for paid inventory. HANDOVER.md says what runs
43
+ where, the monthly rhythm, how to connect an AI, what to do when the owner
44
+ leaves, and which integrations are still unproven.
45
+
46
+ ## Small working boundaries
47
+
48
+ The examples use the installed toolkit's exports. Declare `reserve` in `tools/`;
49
+ this small example reserves one whole slot for a signed-in account. Keep the
50
+ public collection a projection without account IDs or secrets. A production
51
+ paid slot uses the shop booking system, not a second ledger.
52
+
53
+ ```ts
54
+ import { defineTool } from '@homie-rocks/studio/tools';
55
+ export default defineTool<{ slot: string; request: string }>({
56
+ name: 'reserve', description: 'Reserve one slot; repeat the same request safely.',
57
+ audience: 'signed-in',
58
+ inputSchema: {
59
+ type: 'object', additionalProperties: false, required: ['slot', 'request'],
60
+ properties: {
61
+ slot: { type: 'string', enum: ['morning', 'afternoon'] },
62
+ request: { type: 'string', pattern: '^[a-f0-9]{32}$' }
63
+ }
64
+ },
65
+ async handler(input, ctx) {
66
+ const owner = ctx.caller.id;
67
+ const key = 'slot:' + input.slot;
68
+ const old = await ctx.database.getVersioned(key);
69
+ if (old) {
70
+ const saved = old.value as { owner: string; request: string };
71
+ if (saved.owner === owner && saved.request === input.request) return { booked: true };
72
+ return { booked: false, next: 'That slot is full. No place has been taken.' };
73
+ }
74
+ const result = await ctx.database.put(key, { owner, request: input.request }, { version: 0 });
75
+ return { booked: result.ok, next: result.ok ? 'Your place is reserved.' : 'That slot filled. Choose another.' };
76
+ }
77
+ });
78
+ ```
79
+
80
+ ```ts
81
+ import { createAppTools, createAppContext, createAppRecords } from '@homie-rocks/studio/apps';
82
+ const tools = createAppTools();
83
+ const context = createAppContext();
84
+ const self = await context.ready; // self.url is public; it contains no private access link
85
+ const records = createAppRecords<{ status: string }>({ collection: 'spots' });
86
+ const refresh = async () => {
87
+ const rows = await records.list();
88
+ document.querySelector('output')!.textContent = `${rows.length} places · ${self.url}`;
89
+ };
90
+ const stop = records.subscribe(() => { void refresh(); });
91
+ await refresh();
92
+ // In the visitor's click handler, preserve request across network retries:
93
+ const request = crypto.randomUUID().replaceAll('-', '');
94
+ await tools.call('reserve', { slot: 'morning', request });
95
+ // On unmount: stop(); records.dispose(); context.dispose(); tools.dispose();
96
+ ```
97
+
98
+ Build the controls first: an area menu is the primary navigation, and tapping a
99
+ 3D object selects the same area. Catch renderer creation failure and context loss;
100
+ keep every booking/control usable without WebGL. Refusals are human sentences
101
+ that say what happened. Prove refusals by bypassing disabled controls, race two
102
+ browsers for the last seat, reload and restart, and run with WebGL unavailable.
103
+ Document the exact commands and results; an empty assertion list is not proof.
104
+
105
+ ## Do not teach the mistakes
106
+
107
+ - No booking codes in public collections or anonymous edits of arbitrary bookings.
108
+ Bind tickets to their account/seat, or verify a secret only the booker holds.
109
+ - No public staff console. Use a signed-in role and check it on the server.
110
+ Label an intentionally public simulator in both UI and Codex.
111
+ - Never interpolate visitor text into `innerHTML`: a stored XSS reached a staff
112
+ tablet in the study. Use `textContent` for names, contacts, notices and outbox text.
113
+ - Never issue an “official receipt” for an unpaid donation. Record a pledge as a
114
+ pledge; count a paid gift only after a verified, deduplicated `order.paid` event,
115
+ with the gift's ID as its reference.
116
+ - No fixed UTC offsets or one hard-coded event date. Use IANA zones and recurring
117
+ calendar rules; carry the same occurrence into site copy and all screens.
118
+ - No sleep-and-retry event arrays or staff-device cron jobs. Use durable functions,
119
+ version checks, declared feeds and the server scheduler.
120
+ - No live-only assets. Commit every asset the site serves, with its provenance,
121
+ and verify a clean checkout can build it. Declare only packages actually used.
122
+ - No invented reviews, impact figures or verification. Label estimates and test
123
+ payments honestly; remove stale workaround claims when upgrading.
124
+
125
+ ## Booking toolkit and customer-AI arguments
126
+
127
+ Use declared booking slots and resources instead of duplicating capacity logic.
128
+ `studio_book` accepts `slot` or `time`, `resource`, `preferredResource` or
129
+ `preferred_resource`, and `addons`, `addOns` or `add-ons` as an array of IDs or an
130
+ object of quantities. Contradictory aliases are refused. Use the same option and
131
+ resource IDs in `createBookingChoices` on the phone. A booking may allocate
132
+ multiple resource components, with skills, attributes and capacity checked by
133
+ the server. Use the app's IANA `timeZone` and `ctx.bookings.calendar` for dated or
134
+ multi-day seasons. Public holds need a stable ID, bounded expiry and a private
135
+ management token for anonymous customers.
136
+
137
+ Pass the app's booking reference to `shop.book` or MCP `studio_checkout`. For
138
+ variable deposits use an amount-based link and validate the paid amount against
139
+ the saved booking. Declare `booking.slots_need_payment_links` so the owner's AI
140
+ can prepare links for newly rolled dates through Stripe's official plugin.
141
+ Use `shop observe` to record plugin responses, then verify and build.
142
+
143
+ The toolkit's `docs/toolkit-batch.md` documents resource declarations, install
144
+ surfaces, private frames, concept previews, example forks and isolated live-site
145
+ checks. Record IDs belong at the top level of `ctx.records`/`app_records`
146
+ arguments, alongside `app` and `collection`.
@@ -6,6 +6,9 @@ compatibility: Node 22 and Chrome; the studio's pinned toolkit and Wrangler.
6
6
 
7
7
  # Make an app
8
8
 
9
+ For place-based builds, read [the six-build patterns](../app/PLACES.md): owner words to needs, records as objects, a local scene and ambience, one trusted operation for screens and AI, graceful graphics, and adversarial proof. Apply the same authority and honest-evidence rules to game economies and tickets.
10
+
11
+
9
12
  For the installed version’s payment, records and local-check contracts, start with
10
13
  `node_modules/@homie-rocks/studio/apps/APPS.md`. Its “Real Stripe test checkout on
11
14
  this computer” section is the supported offline-hosting path: real Stripe test
@@ -25,6 +25,9 @@ cover, doors and platforms, use declared entity `collider` state and movement's
25
25
 
26
26
  # Make a game
27
27
 
28
+ For place-based builds, read [the six-build patterns](../app/PLACES.md): owner words to needs, records as objects, a local scene and ambience, one trusted operation for screens and AI, graceful graphics, and adversarial proof. Apply the same authority and honest-evidence rules to game economies and tickets.
29
+
30
+
28
31
  You are in a Homie studio when the folder (or one above it) has `studio.json`. If there
29
32
  is none, use the `studio-setup` skill first.
30
33