makersclaw 0.6.0 → 0.8.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 (152) hide show
  1. package/README.md +56 -9
  2. package/package.json +1 -1
  3. package/r/annotate.json +4 -4
  4. package/r/approval-request.json +3 -3
  5. package/r/audio-player.json +3 -3
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +4 -4
  8. package/r/calendar-view.json +4 -4
  9. package/r/cart.json +3 -3
  10. package/r/chart.json +6 -6
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +4 -4
  13. package/r/comment-box.json +4 -4
  14. package/r/contact-section.json +3 -3
  15. package/r/core.json +10 -4
  16. package/r/csv-import.json +4 -4
  17. package/r/data-table.json +3 -3
  18. package/r/diagram.json +3 -3
  19. package/r/doc-editor.json +4 -4
  20. package/r/doc-page.json +4 -4
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +3 -3
  24. package/r/flow-canvas.json +4 -4
  25. package/r/footer.json +3 -3
  26. package/r/gantt.json +4 -4
  27. package/r/job-panel.json +3 -3
  28. package/r/json-editor.json +4 -4
  29. package/r/map.json +5 -5
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +4 -4
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +4 -4
  35. package/r/page-builder.json +6 -6
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +6 -6
  39. package/r/product-detail.json +3 -3
  40. package/r/product-grid.json +4 -4
  41. package/r/record-form.json +3 -3
  42. package/r/record-notes.json +5 -5
  43. package/r/registry.json +126 -121
  44. package/r/search-dialog.json +3 -3
  45. package/r/settings-list.json +3 -3
  46. package/r/setup-checklist.json +3 -3
  47. package/r/share-dialog.json +3 -3
  48. package/r/sidebar.json +3 -3
  49. package/r/signature-pad.json +3 -3
  50. package/r/sketch-canvas.json +5 -5
  51. package/r/spreadsheet.json +3 -3
  52. package/r/stat-cards.json +4 -4
  53. package/r/task-list.json +4 -4
  54. package/r/task-model.json +3 -3
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +3 -3
  57. package/r/whiteboard.json +4 -4
  58. package/registry/blocks/annotate/README.md +24 -12
  59. package/registry/blocks/approval-request/README.md +15 -8
  60. package/registry/blocks/audio-player/README.md +17 -10
  61. package/registry/blocks/board/README.md +13 -9
  62. package/registry/blocks/board/board.jsx +1 -1
  63. package/registry/blocks/calendar-view/README.md +28 -4
  64. package/registry/blocks/cart/README.md +14 -10
  65. package/registry/blocks/chart/README.md +14 -7
  66. package/registry/blocks/chart/cartesian.jsx +1 -1
  67. package/registry/blocks/chart/core.jsx +33 -23
  68. package/registry/blocks/chart/pie.jsx +1 -1
  69. package/registry/blocks/chat/README.md +47 -14
  70. package/registry/blocks/chat/demo.jsx +2 -2
  71. package/registry/blocks/code-editor/README.md +4 -0
  72. package/registry/blocks/code-editor/code-editor.jsx +1 -1
  73. package/registry/blocks/comment-box/README.md +20 -13
  74. package/registry/blocks/contact-section/README.md +7 -6
  75. package/registry/blocks/csv-import/README.md +34 -12
  76. package/registry/blocks/data-table/README.md +41 -8
  77. package/registry/blocks/diagram/diagram.jsx +1 -1
  78. package/registry/blocks/doc-editor/README.md +18 -6
  79. package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
  80. package/registry/blocks/doc-page/README.md +16 -6
  81. package/registry/blocks/file-upload/README.md +7 -1
  82. package/registry/blocks/flow-canvas/README.md +30 -12
  83. package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
  84. package/registry/blocks/footer/README.md +7 -4
  85. package/registry/blocks/gantt/README.md +18 -12
  86. package/registry/blocks/job-panel/README.md +1 -1
  87. package/registry/blocks/json-editor/README.md +16 -4
  88. package/registry/blocks/map/README.md +19 -3
  89. package/registry/blocks/map/map-view.jsx +1 -1
  90. package/registry/blocks/map/maplibre.js +1 -1
  91. package/registry/blocks/mind-map/README.md +20 -7
  92. package/registry/blocks/org-chart/README.md +14 -9
  93. package/registry/blocks/page-builder/README.md +14 -7
  94. package/registry/blocks/page-builder/demo.jsx +3 -3
  95. package/registry/blocks/page-builder/page-builder.jsx +1 -1
  96. package/registry/blocks/page-builder/page-view.jsx +1 -1
  97. package/registry/blocks/page-builder/page.js +1 -1
  98. package/registry/blocks/pdf-viewer/README.md +26 -21
  99. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
  100. package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
  101. package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
  102. package/registry/blocks/product-detail/README.md +13 -7
  103. package/registry/blocks/product-detail/demo.jsx +1 -1
  104. package/registry/blocks/product-grid/README.md +12 -10
  105. package/registry/blocks/record-form/README.md +26 -9
  106. package/registry/blocks/record-notes/README.md +42 -30
  107. package/registry/blocks/search-dialog/README.md +38 -7
  108. package/registry/blocks/settings-list/README.md +29 -17
  109. package/registry/blocks/setup-checklist/README.md +12 -14
  110. package/registry/blocks/share-dialog/README.md +1 -1
  111. package/registry/blocks/sidebar/README.md +23 -13
  112. package/registry/blocks/signature-pad/README.md +15 -4
  113. package/registry/blocks/sketch-canvas/README.md +25 -10
  114. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
  115. package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
  116. package/registry/blocks/spreadsheet/README.md +17 -8
  117. package/registry/blocks/stat-cards/README.md +17 -8
  118. package/registry/blocks/task-list/README.md +14 -11
  119. package/registry/blocks/task-model/README.md +3 -3
  120. package/registry/blocks/transaction-list/README.md +15 -7
  121. package/registry/blocks/whiteboard/README.md +34 -22
  122. package/src/cli.mjs +33 -6
  123. package/src/install.mjs +285 -26
  124. package/src/registry.mjs +109 -1
  125. package/templates/next/browser/app/auto-refresh.jsx +67 -0
  126. package/templates/next/browser/app/layout.jsx +94 -0
  127. package/templates/next/browser/app/page.jsx +60 -0
  128. package/templates/next/browser/app/s/[key]/page.jsx +95 -0
  129. package/templates/next/browser/lib/actions.js +16 -0
  130. package/templates/next/browser/lib/data.js +43 -0
  131. package/templates/next/files/README.md +1100 -89
  132. package/templates/next/files/app/frame.js +128 -4
  133. package/templates/next/files/app/shell.jsx +54 -37
  134. package/templates/next/files/check/run.mjs +1514 -45
  135. package/templates/next/files/components/ui/index.js +1 -0
  136. package/templates/next/files/components/ui/item.jsx +2 -2
  137. package/templates/next/files/components/ui/layout.jsx +307 -0
  138. package/templates/next/files/components/ui/notice.jsx +1 -1
  139. package/templates/next/files/components/ui/separator.jsx +1 -1
  140. package/templates/next/files/lib/actions.js +170 -51
  141. package/templates/next/files/lib/brain-server.js +96 -0
  142. package/templates/next/files/lib/brain.js +413 -0
  143. package/templates/next/files/lib/claw.js +15 -1
  144. package/templates/next/files/lib/data.js +119 -29
  145. package/templates/next/files/lib/db.js +97 -17
  146. package/templates/next/files/lib/door.js +680 -0
  147. package/templates/next/files/lib/hooks.js +71 -2
  148. package/templates/next/files/lib/platform.js +93 -4
  149. package/templates/next/files/next.config.mjs +36 -2
  150. package/templates/next/files/surfaces.js +3 -2
  151. package/templates/next/template.json +18 -3
  152. package/templates/next/files/schema.sql +0 -22
@@ -1,10 +1,11 @@
1
1
  # artifact template
2
2
 
3
3
  The app every artifact starts from: Next.js 16 (app router, JSX, server
4
- actions, Turbopack), Postgres via `DATABASE_URL`, Tailwind v4, and a kit
5
- of parts (`components/ui`, `components/parts`). It carries the behaviour
6
- every app shares, the parts every app is assembled from, and the
7
- workspace's colours. You design the surfaces: what is on them, how they
4
+ actions, Turbopack), Postgres via `DATABASE_URL` or, in a browser app,
5
+ the platform's data door (§2), Tailwind v4, and a kit of parts
6
+ (`components/ui`, `components/parts`). It carries the behaviour every
7
+ app shares, the parts every app is assembled from, and the workspace's
8
+ colours. You design the surfaces: what is on them, how they
8
9
  are laid out, how dense, how they read — never what colour anything is.
9
10
 
10
11
  ## 1. What you are inside
@@ -48,15 +49,23 @@ Paths here are relative to this folder — the artifact's `app/`.
48
49
  `app/`: this file's `app/shell.jsx` is written as `app/app/shell.jsx`,
49
50
  `surfaces/<key>.jsx` as `app/surfaces/<key>.jsx`.
50
51
 
52
+ An app is one of two modes, and `makersclaw.json`'s `mode` says which.
53
+ A **server app** runs on a Next server of its own (`"server"`, or no
54
+ key). A **browser app** runs in the member's browser: its pages are
55
+ files built once, and every read and write goes through the platform's
56
+ data door (`"browser"`). People add, edit and delete in it as in any
57
+ app. Both lay the files below; a browser app swaps six of them and
58
+ leaves out four (Per mode, below). Which to build: §4.
59
+
51
60
  Yours — write and rewrite freely:
52
61
 
53
62
  ```
54
- app/shell.jsx the inside of the window: nav, layout, type, density
63
+ app/shell.jsx the inside of the window: nav, layout, type, density — an AppLayout (§3b, Layouts)
55
64
  app/DESIGN.md the design, written before the code (§3)
56
65
  surfaces/<key>.jsx one file per domain surface
57
66
  surfaces.js the manifest entries (import + component when built)
58
67
  components/*.jsx anything shared between surfaces
59
- components/*-actions.js your own server actions, for a write lib/actions.js cannot make (§5)
68
+ components/*-actions.js a server app's own server actions, for a write lib/actions.js cannot make (§5)
60
69
  components/depictions/ pictures of things that exist elsewhere, in their own colours (§3c)
61
70
  ```
62
71
 
@@ -69,17 +78,59 @@ app/page.jsx / → the home surface (or "No surfaces yet" while the ma
69
78
  app/s/[key]/page.jsx renders the surface with that key; planned ones say so
70
79
  app/error.jsx, app/global-error.jsx, app/not-found.jsx, app/error-relay.jsx, app/auto-refresh.jsx,
71
80
  app/escape-relay.jsx, app/frame.js, app/claw-alive/route.js the app inside its window: refresh while shown (a surface opts out, §7), Escape from the desk
72
- components/ui/ the kit: Button, Field, Input, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction… (§3b, §3c)
81
+ components/ui/ the kit: AppLayout, DocumentLayout, Button, Field, Input, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction… (§3b, §3c)
73
82
  components/parts/ the parts: RecordSwitcher, LiveEmpty + LiveWork, useFixture + FixtureNotice (§3a, §3b)
74
83
  check/pins.json the fixed files by hash; `npm run check` refuses one that differs
75
- lib/* db, data, actions, claw, platform, hooks, sample (§5)
76
84
  check/ `npm run check` (§6)
77
85
  proxy.js product-token check, x-claw-surface and x-claw-theme headers (Next 16's name for middleware.js)
78
- next.config.mjs basePath + Server Actions allowedOrigins (and the dev server's allowedDevOrigins), from env (§4b)
86
+ next.config.mjs basePath + Server Actions allowedOrigins (and the dev server's allowedDevOrigins), from env (§4b);
87
+ a browser app's export (output "export"), from makersclaw.json's mode
79
88
  postcss.config.mjs Tailwind v4
80
- schema.sql public.artifact_activity, for reference
81
89
  ```
82
90
 
91
+ And `lib/`, fixed too (§5):
92
+
93
+ ```
94
+ lib/data.js the reads of your own tables (Reads)
95
+ lib/actions.js the writes of your own tables: the one-row writes and runAction (Writes)
96
+ lib/db.js the database pool: query(), logActivity() — a server app's, for its server files
97
+ lib/door.js your own tables from the browser, through the data door; the hooks (Door)
98
+ lib/brain.js the company brain from the browser, in both modes; its hooks (Brain)
99
+ lib/brain-server.js the brain's reads for a server component — a server app's
100
+ lib/claw.js the _claw API the gateway answers, from the browser
101
+ lib/platform.js typed wrappers over lib/claw.js (Platform)
102
+ lib/hooks.js the React hooks over them: useJob, useLlm, useArtifact… (Hooks)
103
+ lib/sample.js markSample, for sample rows (§3a)
104
+ ```
105
+
106
+ ### Per mode
107
+
108
+ A browser app lays its own copy of six fixed files (the template's
109
+ `templates/next/browser/`). Each is fixed and pinned like the rest, and
110
+ keeps the server copy's names, so a surface imports what it always did:
111
+
112
+ ```
113
+ app/layout.jsx built once: the theme set by a script in <head> before the page paints; examples decided at build (§3a)
114
+ app/page.jsx / → a page that forwards to the home surface, keeping the query
115
+ app/s/[key]/page.jsx one file per surface, whose surface renders in the browser inside <Suspense>
116
+ app/auto-refresh.jsx asks the data door what changed, and every hook on the page reads again (§7)
117
+ lib/data.js lib/door.js's reads and hooks, under the usual name
118
+ lib/actions.js lib/door.js's writes, under the usual name
119
+ ```
120
+
121
+ And it has none of these, since nothing of it runs on a server:
122
+
123
+ ```
124
+ proxy.js the platform serves the files; there is no request of the app's to check
125
+ app/claw-alive/route.js no route handler runs; the auto-refresh asks the door instead
126
+ lib/db.js no database login in the app: the door holds it, and knows who is asking
127
+ lib/brain-server.js the brain's reads run in the browser, through lib/brain.js
128
+ ```
129
+
130
+ `npx makersclaw mode <browser|server>` is the only switch: it lays that
131
+ mode's fixed files, removes the other's, and says what your own code
132
+ must change (§4).
133
+
83
134
  ## 3. Design first
84
135
 
85
136
  Before any code, write `app/DESIGN.md` — one screen of text:
@@ -96,7 +147,12 @@ Before any code, write `app/DESIGN.md` — one screen of text:
96
147
  - Type: scale and weight within the workspace's faces (`font-sans`,
97
148
  `font-mono`, `font-display`). Numbers tabular (`data-nums` on the element).
98
149
  - Layout and nav: where the surfaces live, what the home surface shows in
99
- the first second.
150
+ the first second. Who scrolls: the shell is an `AppLayout` (§3b,
151
+ Layouts), the one owner of the window's height; say for each surface
152
+ whether its content scrolls (a list, an article) or fills (a canvas, a
153
+ grid that scrolls itself), whether it has an inspector and how wide,
154
+ and what sits in a footer that must stay in view. Decide it before
155
+ the shell, not after a long page has pushed a button out of reach.
100
156
  - Density, empty states (§3a: what each surface says while its table is
101
157
  empty), error states.
102
158
  - What makes this app look like ITS domain and not another.
@@ -108,7 +164,8 @@ to design: Agent, Runs and Activity are tabs the OS draws on the window,
108
164
  and the app's settings — model, prompt, skills, connections, schedule —
109
165
  are the window's Settings tab, the same for every artifact. The keys
110
166
  `agent`, `runs` and `activity` are reserved; a surface may not take one.
111
- The starter `app/shell.jsx` is a placeholder; replace it first.
167
+ The starter `app/shell.jsx` is a placeholder; replace it first, and
168
+ keep it an `AppLayout`.
112
169
 
113
170
  What the OS does **not** own is asking the agent for something. Those
114
171
  tabs are where the founder goes to look back; the app is where they set
@@ -168,7 +225,9 @@ shadcn project. There a surface may still open on a worked example —
168
225
  `useFixture(rows, SAMPLE)`, §3b — so its author sees it filled.
169
226
  `app/layout.jsx` decides per request: examples are off whenever
170
227
  `CLAW_ARTIFACT_ID` is set (`product_run` sets it for every workspace app)
171
- and on otherwise; `CLAW_EXAMPLES=1` forces them on. Inside a workspace
228
+ and on otherwise; `CLAW_EXAMPLES=1` forces them on. A browser app's
229
+ layout decides the same once, at build: the platform builds every browser
230
+ app with its `CLAW_ARTIFACT_ID`. Inside a workspace
172
231
  `useFixture` hands back the real rows — none — so the same surface falls
173
232
  through to `LiveEmpty`. Up to 0.3.x a workspace app opened on its example
174
233
  until its table had a row; since 0.4.0 it opens empty.
@@ -203,12 +262,64 @@ import { Button, IconButton, ButtonGroup, ButtonGroupSeparator, Toggle, ToggleGr
203
262
  Table, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption,
204
263
  Spinner, Skeleton, Progress, Separator, Kbd, KbdGroup, AspectRatio,
205
264
  Stat, StatGroup, Delta, StatusDot, Legend, Meter, Distribution, Swatch, Steps,
206
- toast, Toaster, Depiction, Icon, ICON_NAMES, usePopoverPosition, panelClass, cx }
265
+ toast, Toaster, Depiction, Icon, ICON_NAMES, usePopoverPosition, panelClass, cx,
266
+ AppLayout, DocumentLayout }
207
267
  from "../components/ui"; // "./ui" from a component
208
268
  import { RecordSwitcher, LiveEmpty, LiveWork, liveWords, useFixture, FixtureNotice, isSample,
209
269
  Examples, useExamples } from "../components/parts";
210
270
  ```
211
271
 
272
+ - **Layouts** — who owns the window's height, and who scrolls. Every
273
+ design-first build of 2026-10-07 shipped a shell of `min-h-dvh` that
274
+ grew with its content: no pane scrolled on its own, and a footer or a
275
+ button after a long article sat past the end of the window, where
276
+ nothing could reach it. So the kit owns the window, once, and `npm
277
+ run check` refuses anything else that sizes by it (`viewport`, §6).
278
+
279
+ `AppLayout` is the app frame and the one viewport owner: the
280
+ outermost one is exactly the window's height; one inside another
281
+ fills the slot it is in instead, so there is never a second owner.
282
+ The shell is one. Slots and props:
283
+ `header` (the top bar, in view; it draws its own bottom border) ·
284
+ `navigation` (a rail from 768px, `navigationWidth` px, 176 by default;
285
+ below 768px a Sheet behind the menu button, titled `navigationLabel`;
286
+ a press on a link closes it) · `children`, the content, with
287
+ `contentMode` `scroll` (the default: its region scrolls) or `fill` (a
288
+ bounded column whose child scrolls itself: a canvas, a grid, a
289
+ spreadsheet), `contentLabel`, `contentClassName` (spacing and token
290
+ backgrounds only) and `scrollKey` (pass the route: when it changes the
291
+ content scrolls back to its top and the drawer closes) · `inspector`
292
+ (beside the content from `inspectorAt` md · lg · xl, lg by default;
293
+ `inspectorWidth` px, 360 by default, 240 to 560; `inspectorLabel`) ·
294
+ `footer` (under the content, in view) · `height` viewport · parent.
295
+ Below `inspectorAt` there is no room for two columns: in `scroll` mode
296
+ the content and the inspector share one scroll, the inspector after
297
+ the content, so everything is reachable and nothing hides behind a
298
+ button; in `fill` mode the inspector is a bounded pane under the
299
+ content (at most 45% of the height) that scrolls on its own. A
300
+ surface that needs an inspector or a fill of its own returns an
301
+ `AppLayout` as its outermost element; inside the shell it fills the
302
+ content edge to edge (the content's padding drops for it):
303
+ ```jsx
304
+ <AppLayout
305
+ header={<EditorBar draft={draft} />}
306
+ inspector={<ReviewPanel notes={notes} />}
307
+ inspectorWidth={440}
308
+ footer={<SaveState saved={saved} />}
309
+ contentClassName="p-6"
310
+ contentLabel="Draft"
311
+ >
312
+ <DraftText draft={draft} />
313
+ </AppLayout>
314
+ ```
315
+ `DocumentLayout` is a page that flows, for print and export: no
316
+ height of its own, it sits in the content and the content scrolls it.
317
+ `toolbar` (held at the top, never printed), `width` prose · page (A4)
318
+ · letter · full, `paper` (a sheet on screen, plain in print), `align`
319
+ start · center, `label`. In print the frame lets go (no bound, no
320
+ navigation, header, inspector or footer) and the document flows onto
321
+ pages. Inside either, bound a box by its parent (`h-full`, `flex-1
322
+ min-h-0`) or by a size (`max-h-96`, `ScrollArea`), never by the window.
212
323
  - `Button` — `variant` primary (one per surface, in the app's accent —
213
324
  say it: a Button with no `variant` is secondary) · secondary · quiet ·
214
325
  danger · link (inline text: no padding or height, wraps and truncates
@@ -494,7 +605,7 @@ default import, framed wherever it is rendered, and is never dropped
494
605
  into the shell, the kit or `lib/`: rendered outside its frame it inherits
495
606
  the workspace, and the check says so.
496
607
 
497
- **One door, one bundle.** The folder is reached one way: a static
608
+ **One door, one bundle.** The folder is reached one way: a plain
498
609
  `import XPost from "…/depictions/x-post"` at the top of the file that
499
610
  frames it. No `import()`, no `next/dynamic`, no re-export module, no
500
611
  alias (`const P = XPost`), no picture passed along as a value. Through
@@ -507,15 +618,83 @@ never reaches the page.
507
618
 
508
619
  ## 4. Server and client
509
620
 
621
+ ### Which mode
622
+
623
+ **An app is a browser app unless it needs server code.** Server code
624
+ means one of three things:
625
+
626
+ - a route handler: a proxy to a service with a secret, a render to PDF;
627
+ - an upload larger than the data door takes;
628
+ - SQL a member types (`sql-console`).
629
+
630
+ Nothing else needs a server. A browser app reads and writes its own
631
+ tables and the company brain through the door, runs a write of more
632
+ than one row as an action (§5), and has no process of its own to start,
633
+ stop or crash. `makersclaw.json`'s `mode` says which this app is
634
+ (`"browser"` is a browser app; §2). An app moves with `npx makersclaw
635
+ mode server` (or `browser`), and its own code moves in the same build:
636
+ the command prints what must change.
637
+
638
+ ### A browser app
639
+
640
+ Its pages are built once, with no request, into files the platform
641
+ serves. Everything below runs in the member's browser:
642
+
643
+ - **Every surface is a client component.** Its file starts with `"use
644
+ client"`. It reads through the hooks: `useRows`, `useRow`, `useCount`,
645
+ `useCountBy` and `useActivity` from `lib/data.js`, and the brain's
646
+ `useRecords`, `useRecord`, `useRecordCounts` and `useBrainSearch` from
647
+ `lib/brain.js`. It hands rows to blocks once they have loaded, and
648
+ wires the blocks' callbacks to `lib/actions.js` (§5, Door, has a
649
+ whole surface).
650
+ - **It receives `surface` alone.** No `searchParams`: the page has no
651
+ request. `?row=`, `?list=` and any other view state come from
652
+ `useSearchParams()` (`next/navigation`).
653
+ - **No `process.env` in its code but `CLAW_BASE_PATH`, `CLAW_PRODUCT`
654
+ and `NEXT_PUBLIC_*`.** A prerendered page prints what it reads into a
655
+ file anyone who opens the app can fetch. A secret stays with the
656
+ platform; the build never sees `.env`.
657
+ - No `"use server"`, no `route.js`, no `proxy.js`, no import of
658
+ `next/headers`, `next/cache`, `next/server`, `pg` or `lib/db`, no
659
+ route segment config (`dynamic`, `revalidate`…). There is no server to
660
+ run them, and `npm run check` names each one (§6, `browser`).
661
+
662
+ ```jsx
663
+ // surfaces/investors.jsx (a browser app)
664
+ "use client";
665
+ import { useSearchParams } from "next/navigation";
666
+ import { useRows } from "../lib/data";
667
+ import InvestorBoard from "../components/InvestorBoard";
668
+
669
+ export default function Investors() {
670
+ const highlight = useSearchParams().get("row");
671
+ const { rows, error, loading } = useRows({ source: "investors", orderBy: "updated_at desc" });
672
+ if (loading) return null; // nothing until the first answer: useFixture must not take a read in flight for an empty table
673
+ return <InvestorBoard rows={rows} error={error} highlight={highlight} />;
674
+ }
675
+ ```
676
+
677
+ `components/InvestorBoard.jsx` is the same file in both modes (below):
678
+ it calls `moveRow` from `lib/actions.js`, which in a browser app goes
679
+ through the door.
680
+
681
+ ### A server app
682
+
510
683
  One split, stated once:
511
684
 
512
685
  ```
513
686
  lib/data.js, lib/db.js server components and server actions only
514
687
  lib/platform.js, lib/hooks.js "use client" files only
515
688
  lib/actions.js either (they are server actions)
689
+ lib/brain.js "use client" files only, in a server app too (§5, Brain)
690
+ lib/door.js "use client" files only: the own tables from the browser (§5, Door)
691
+ lib/brain-server.js server components only: the brain's reads
516
692
  surfaces.js server files only (entries import components)
517
693
  ```
518
694
 
695
+ In a browser app every `lib/` file but `sample.js` is for `"use client"`
696
+ files, and `surfaces.js` is the pages'.
697
+
519
698
  Pass plain data across the boundary — strings, numbers, arrays, objects.
520
699
  `plain(row)` already turns a timestamp into its ISO string. A `date`
521
700
  column is its day, `"2026-09-28"`: text, the same day for every reader,
@@ -537,7 +716,7 @@ import { loadRows } from "../lib/data";
537
716
  import InvestorBoard from "../components/InvestorBoard";
538
717
 
539
718
  export default async function Investors({ surface, searchParams }) {
540
- const { rows, error } = await loadRows({ source: "public.investors", orderBy: "updated_at desc" });
719
+ const { rows, error } = await loadRows({ source: "investors", orderBy: "updated_at desc" });
541
720
  return <InvestorBoard rows={rows} error={error} highlight={searchParams.row ?? null} />;
542
721
  }
543
722
  ```
@@ -559,7 +738,11 @@ export default function InvestorBoard({ rows: real, error, highlight }) {
559
738
 
560
739
  Every surface component receives `surface` (`{ key, kind, label, source
561
740
  }`) and `searchParams` (the URL query; `?row=<id>` is the convention for
562
- "highlight this row" — an activity line opens its surface with it).
741
+ "highlight this row" — an activity line opens its surface with it). A
742
+ browser app's surface receives `surface` alone and reads the query with
743
+ `useSearchParams()` (above).
744
+ `source` is the table's bare name, `"investors"`, with no schema in
745
+ front: the app's tables are in its own schema (§5, Your tables).
563
746
 
564
747
  ### Views by link
565
748
 
@@ -572,10 +755,12 @@ part of the contract, the same way `?row=` is:
572
755
  - `?list=<value>` shows one list. An app that keeps separate lists in
573
756
  shared tables declares which column names them (`lists: { table,
574
757
  column }`), and every surface over that table reads `list` and filters
575
- by that column — `loadRows({ source, filter: searchParams.list ? {
576
- [column]: searchParams.list } : undefined })`. With no `list` it shows
577
- every list: a filter of `{ [column]: undefined }` matches no row at
578
- all. It is the one key every app with lists honours.
758
+ by that column — `loadRows({ source, filter: { [column]:
759
+ searchParams.list } })`; in a browser app, `useRows({ source, filter:
760
+ { [column]: useSearchParams().get("list") ?? undefined } })`. With no
761
+ `list` it shows every list: a column
762
+ whose condition is `undefined` is no condition (up to 0.6.x it matched
763
+ no row). It is the one key every app with lists honours.
579
764
  - A job's view may declare a filter (`week=this&list={list}`; `{list}`
580
765
  and `{since}` are filled in per request). Its keys are query params
581
766
  the surface ALREADY reads: declare one only for a param the surface
@@ -608,6 +793,11 @@ action="/…">`. A real external URL (`https://`, `mailto:`) is fine.
608
793
 
609
794
  ### 4b. Writes are rejected — "Invalid Server Actions request"
610
795
 
796
+ A server app only. A browser app has no Server Actions: its writes go
797
+ to the data door on the page's own origin, and nothing here applies to
798
+ them. Its preview still runs on `next dev`, so the dev server's
799
+ `allowedDevOrigins` below holds for it too.
800
+
611
801
  Already handled. Do not fix it in `next.config.mjs`.
612
802
 
613
803
  Next refuses a Server Action whose `Origin` header disagrees with
@@ -631,53 +821,205 @@ source — say so and stop; it is not an app-level fix.
631
821
 
632
822
  ## 5. API reference
633
823
 
824
+ ### Your tables — one schema, named bare
825
+
826
+ The app's tables live in its own schema, and no other app reads them.
827
+ In Makersclaw that is `app_<name>`, and `makersclaw.json` says
828
+ `"tables": "own"`. Besides its own tables, the app's database role
829
+ reaches only what the platform lays for it under `claw.` and
830
+ `claw_app.` (the activity door, `claw_app.activity`, below).
831
+
832
+ **Name every table bare: `deals`, with no schema in front — neither
833
+ `public.` nor `app_<name>.`.** The role's search path sends a bare name
834
+ to the app's schema. A schema written by hand breaks the app: its role
835
+ reaches nothing in `public`, and a second copy of the app lives in a
836
+ schema of another name. Bare holds everywhere a table is named: a
837
+ surface's `source`, every `lib/data.js` and `lib/actions.js` call, and
838
+ SQL of your own (`update deals set …`). `npm run check` refuses a
839
+ qualified name (§6, `tables`).
840
+
841
+ One schema per app, not one per copy of its code. A change build's
842
+ preview reads and writes the live app's tables, so rows saved while
843
+ testing are real rows. In a change to a live app, add what the change
844
+ needs (new tables, new columns that allow null or have a default);
845
+ remove or rename in a build after it ships.
846
+
847
+ **The company's things are not your tables.** A customer, a person, a
848
+ product — anything another app or a person would look up — is a record
849
+ in the company brain, read and written through `lib/brain.js` (Brain,
850
+ below). A row of yours points at a record by a `uuid` column named for
851
+ its kind (`contact_id`), declared in `links`. Never copy a record's
852
+ fields into a table of yours: the copy goes stale the day someone edits
853
+ the record.
854
+
855
+ `makersclaw init --tables own|public` records where the tables live;
856
+ Makersclaw passes it. It never changes afterwards: an app's tables move
857
+ from `public` to their own schema only with its workspace. An app laid
858
+ before 0.7.0 has no `tables` key: its tables are in `public`, and a
859
+ bare name reaches them there too. So write names bare in either.
860
+
634
861
  ### Reads — `lib/data.js` (server)
635
862
 
863
+ In a browser app `lib/data.js` is the door's (Door, below): these same
864
+ names, shapes and filters, called from `"use client"` files, with a hook
865
+ for each.
866
+
636
867
  Every read resolves `{ ..., error }` and never throws. `error` is text
637
868
  when the table is missing or the database unreachable; it is also
638
869
  `console.error`ed, so the log tail carries it to the platform agent.
639
- `source` is `"table"` or `"schema.table"`; names go through `ident()`.
870
+ `source` is the table's bare name (`"drafts"`); names go through
871
+ `ident()`. It still takes `"schema.table"`, for an app from before
872
+ 0.7.0 whose code names `public.<table>`.
640
873
 
641
874
  ```js
642
875
  loadRows({ source, idColumn = "id", orderBy, limit = 500, filter }) → { rows, error }
643
876
  loadRow({ source, idColumn = "id", id }) → { row, error } // row null when none
644
877
  countRows({ source, filter }) → { count, error }
645
878
  countBy({ source, column, filter }) → { counts: { [value]: n }, error }
646
- loadActivity({ limit = 100, surface, since }) → { rows, error } // public.artifact_activity, this artifact
879
+ loadActivity({ limit = 100, surface, since }) → { rows, error } // claw_app.activity: this app's lines
880
+ ```
881
+
882
+ `filter` is an AND of `{ column: condition }`, at most 20: the
883
+ platform data door's grammar, so a filter means the same in a server
884
+ app and a browser one.
885
+
886
+ ```
887
+ value equals (text, a number, true or false, a Date)
888
+ [value, …] one of, at most 1,000
889
+ null empty
890
+ { not: value | null } anything else, empty rows included; or not empty
891
+ { gt | gte | lt | lte: v } a bound; one lower and one upper may combine
892
+ { has: value } an array column (text[], int[]…) holds it
893
+ { prefix: "text" } starts with, case ignored
647
894
  ```
648
895
 
649
- `filter` is `{ column: value }`: `null` → `IS NULL`, an array → `= ANY`,
650
- else equality. `orderBy` is `"updated_at desc, id"`; default `idColumn
651
- desc`. `limit` caps at 5000 (activity: 1000).
896
+ A column whose condition is `undefined` is left out. Anything else is
897
+ refused, and the read returns why as its `error`. `orderBy` is
898
+ `"updated_at desc, id"`; default `idColumn desc`. `limit` caps at 5000
899
+ (activity: 1000).
652
900
 
653
901
  ```js
654
- const { rows, error } = await loadRows({ source: "public.drafts", filter: { stage: ["draft", "approved"] } });
655
- const { counts } = await countBy({ source: "public.investors", column: "stage" });
902
+ const { rows, error } = await loadRows({ source: "drafts", filter: { stage: ["draft", "approved"] } });
903
+ const { rows: due } = await loadRows({ source: "tasks", filter: { done: false, due: { lte: today }, owner: { not: null } } });
904
+ const { counts } = await countBy({ source: "investors", column: "stage" });
656
905
  const { rows: log } = await loadActivity({ surface: "drafts", limit: 50 });
657
906
  ```
658
907
 
659
908
  Helpers: `plain(row)` (timestamps → ISO, buffers → base64; a `date` is
660
909
  already its `"YYYY-MM-DD"`), `orderClause(orderBy,
661
- fallback)` (validated `ORDER BY` text), `markSample(sample, idColumn)`
910
+ fallback)` (validated `ORDER BY` text), `whereClause(filter, params)`
911
+ (the filter above as `WHERE` text), `markSample(sample, idColumn)`
662
912
  (stable ids + `_sample: true`), `currentSurface(explicit)` (the surface
663
913
  key of the request, from `x-claw-surface`, unless one is passed).
664
914
 
665
915
  ### Writes — `lib/actions.js` (server actions, callable from either side)
666
916
 
667
- Each quotes names with `ident()`, writes one activity row through
668
- `logActivity`, calls `revalidatePath("/", "layout")`, and resolves
669
- `{ ok }` or `{ ok: false, error }`. Never throws. `surface` defaults to
670
- the request's surface (proxy.js sets it from `/s/<key>`).
917
+ In a browser app `lib/actions.js` is the door's (Door, below): these
918
+ same names, shapes and codes, as plain functions of the browser. The
919
+ door runs each write and writes its activity line.
920
+
921
+ In a server app each quotes names with `ident()`, runs in its own
922
+ transaction, writes one activity row through `logActivity` after it
923
+ commits, calls `revalidatePath("/", "layout")`, and resolves `{ ok }`
924
+ or `{ ok: false, error, code }`. Never throws. `code` is the data
925
+ door's (Door, below), so a surface branches on it the same way in both
926
+ modes: `not_found` (no such row), `no_such_table`, `no_such_action`,
927
+ `action_refused`, `conflict`, `bad_request`, `too_slow`, `try_again`,
928
+ else `failed`. `surface` defaults to the request's surface key
929
+ (the `<key>` of `/s/<key>`, which proxy.js reads).
671
930
 
672
931
  ```js
673
- updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data })
932
+ updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data, log })
674
933
  moveRow({ source, idColumn = "id", id, stateColumn, to, surface }) // action "moved"
675
934
  insertRow({ source, idColumn = "id", values, surface, action = "added", detail }) → { ok, id }
676
935
  deleteRow({ source, idColumn = "id", id, surface, detail }) // action "removed"
677
936
  saveSetting({ mode = "table" | "json", source, keyColumn = "key", valueColumn = "value",
678
937
  column, idColumn = "id", id, key, value, surface })
938
+ runAction({ name, args, surface, action, detail, rowRef, log }) → { ok, result }
939
+ ```
940
+
941
+ `log: false` (`updateRow` and `runAction` only) writes no activity line:
942
+ for a save that is typing, not a decision, like an autosave every few
943
+ keystrokes. An insert and a delete always log. Name such a write "no
944
+ activity line" in the brief (`references/actions.md`), so the agent logs
945
+ none for it either. The door takes the same `log: false` in a browser
946
+ app.
947
+
948
+ **Who is asking.** Each write's transaction first sets two settings,
949
+ local to it: `claw.member_id` and `claw.member_email`, the member the
950
+ platform named on the request (`''` when none: a local run, a job).
951
+ The platform's data door sets the same two for a browser app, and the
952
+ agent's `sql` for the member it acts for. A default, a trigger or an
953
+ action reads them:
954
+
955
+ ```sql
956
+ nullif(current_setting('claw.member_email', true), '')
679
957
  ```
680
958
 
959
+ A column the page must not choose (`created_by`, `approved_by`) is set
960
+ by a `before insert` or `before update` trigger that reads it, never
961
+ sent from the page: a default alone keeps whatever the page sends.
962
+ Made with `sql`, once per table:
963
+
964
+ ```sql
965
+ create or replace function deals_created_by() returns trigger
966
+ language plpgsql as $$
967
+ begin
968
+ new.created_by := nullif(current_setting('claw.member_email', true), '');
969
+ return new;
970
+ end $$;
971
+ create trigger deals_created_by before insert on deals
972
+ for each row execute function deals_created_by();
973
+ ```
974
+
975
+ It is a label, not a permission: nothing checks it, and the app's own
976
+ agent can set it too.
977
+
978
+ **Actions.** A write of more than one row, or one that decides
979
+ something, is an action: a function in the app's own schema, `(args
980
+ jsonb) returns jsonb`, made with the `sql` verb. The screen calls it
981
+ with `runAction`, which runs `select <name>($1::jsonb)` with `args` as
982
+ JSON (`{}` when none); the app's agent runs the same function with
983
+ `sql`, so the two can never drift. Its name is one bare, lower-case
984
+ name (`close_cycle`): the data door calls only a function of the app's
985
+ own schema, so a name with a schema or capitals (`CloseCycle`) is
986
+ `no_such_action` in both modes. It refuses with `raise exception '<sentence>'`, which
987
+ comes back as `error`, code `action_refused`. It never writes
988
+ `claw_app.activity`: `runAction` logs the call (`action` defaults to the
989
+ function's name, `row_ref` is `rowRef` or the result's `id`), and the
990
+ agent logs its own.
991
+
992
+ ```sql
993
+ create or replace function close_cycle(args jsonb) returns jsonb
994
+ language plpgsql as $$
995
+ declare
996
+ c uuid := (args->>'cycle')::uuid;
997
+ begin
998
+ update cycles set status = 'closed', closed_at = now() where id = c and status = 'open';
999
+ if not found then raise exception 'that cycle is already closed'; end if;
1000
+ update goals set status = 'carried' where cycle_id = c and status = 'open';
1001
+ return jsonb_build_object('id', c);
1002
+ end $$;
1003
+ ```
1004
+
1005
+ ```js
1006
+ const r = await runAction({ name: "close_cycle", args: { cycle: id }, action: "closed", detail: "Q3" });
1007
+ if (!r.ok) setError(r.error); // "that cycle is already closed"
1008
+ ```
1009
+
1010
+ **Name every action in the agent's brief, `references/actions.md`,
1011
+ with its `select`**: `` `close_cycle` `` and `select
1012
+ close_cycle($1::jsonb)`, its params and what it refuses. The agent
1013
+ runs it from there, and `npm run check` refuses an action the screens
1014
+ run that the brief does not name (§6, `agent`).
1015
+
1016
+ In a change to a live app, never change or drop a function the live app
1017
+ runs, a helper one included: the preview writes the live schema, so the
1018
+ live screens would run the new body at once, and a roll back would run
1019
+ it too. Write a new one (`close_cycle_2`), call it from the new code and
1020
+ name it in `references/actions.md`. Drop the old one once neither the
1021
+ live build nor the one kept for roll back calls it.
1022
+
681
1023
  `saveSetting` table mode updates the `(key, value)` row and inserts one
682
1024
  when missing — a plain `key text, value text` table is enough. JSON mode
683
1025
  sets `key` inside the jsonb `column` of row `id`.
@@ -692,19 +1034,288 @@ the column's default: `updated_at timestamptz not null default now()`.
692
1034
  ```js
693
1035
  "use client";
694
1036
  import { moveRow } from "../lib/actions";
695
- const r = await moveRow({ source: "public.investors", id, stateColumn: "stage", to: "sent" });
1037
+ const r = await moveRow({ source: "investors", id, stateColumn: "stage", to: "sent" });
696
1038
  if (!r.ok) setError(r.error);
697
1039
  ```
698
1040
 
699
1041
  Never write raw SQL from a surface. `lib/*.js` is fixed (`npm run check`
700
1042
  refuses an edit, and `makersclaw update` lays the file again), so a write
701
- these cannot make goes in a server-action file of the app's own:
702
- `"use server"` on its first line, beside the components that call it
703
- (`components/deal-actions.js`). There it composes the actions above, or
704
- runs its UPDATE or DELETE through `query()` from `lib/db.js`, records it
705
- with `logActivity()` and ends with `revalidatePath("/", "layout")`, as
706
- `lib/actions.js` does. A new row still goes through `insertRow`: the
707
- check refuses a raw `INSERT INTO` anywhere outside `lib/`.
1043
+ these cannot make is an action (above), called with `runAction`. In a
1044
+ server app only, a server-action file of the app's own (`"use server"` on its first line,
1045
+ beside the components that call it: `components/deal-actions.js`) may
1046
+ compose the writes above. A new row still goes through `insertRow`:
1047
+ the check refuses a raw `INSERT INTO` anywhere outside `lib/`, and, in
1048
+ an app on its own tables, any SQL that writes in the app's code (§6,
1049
+ `agent`).
1050
+
1051
+ ### Door — `lib/door.js` (browser, both modes)
1052
+
1053
+ The app's own tables, read and written from the browser through the
1054
+ platform's data door, `_claw/data`. The gateway answers it under the
1055
+ app's path, as the member whose cookie comes with the request. Each
1056
+ write runs on the app's own database login, in one transaction that
1057
+ knows who is asking (`claw.member_email`, above), and the door writes
1058
+ its activity line.
1059
+
1060
+ A browser app (`"mode": "browser"` in `makersclaw.json`) has no server of
1061
+ its own. Its `lib/data.js` and `lib/actions.js` are this file under
1062
+ their usual names, so a surface imports as it always did:
1063
+ `import { useRows } from "../lib/data"`, `import { moveRow } from
1064
+ "../lib/actions"`. In a server app a `"use client"` component may take
1065
+ the hooks from here.
1066
+
1067
+ ```js
1068
+ // Reads: lib/data.js's names and shapes
1069
+ loadRows({ source, idColumn = "id", orderBy, limit = 500, filter, offset, cursor }) → { rows, error, next }
1070
+ loadRow({ source, idColumn = "id", id }) → { row, error } // row null when none, or no id
1071
+ countRows({ source, filter }) → { count, error }
1072
+ countBy({ source, column, filter }) → { counts, error }
1073
+ loadActivity({ limit = 100, surface, since }) → { rows, error }
1074
+
1075
+ // Writes: lib/actions.js's names and shapes
1076
+ insertRow({ source, idColumn, values, surface, action, detail }) → { ok, id }
1077
+ updateRow({ source, idColumn, id, patch, surface, action, detail, log }) → { ok }
1078
+ moveRow({ source, idColumn, id, stateColumn, to, surface }) → { ok }
1079
+ deleteRow({ source, idColumn, id, surface, detail }) → { ok }
1080
+ saveSetting({ mode, source, keyColumn, valueColumn, column, idColumn, id, key, value, surface })
1081
+ runAction({ name, args, surface, action, detail, rowRef, log }) → { ok, result }
1082
+
1083
+ // Hooks: a read on mount, and again on dataChanged()
1084
+ useRows(opts) useRow(opts) useCount(opts) useCountBy(opts) useActivity(opts)
1085
+ → { …the read's values, error, loading, reload }
1086
+ dataChanged() every hook on the page reads again
1087
+ whenLanded() resolves once those reads have landed
1088
+ ```
1089
+
1090
+ - `filter` is the grammar above (Reads), sent as it is. `orderBy` is
1091
+ `"updated_at desc"`. `cursor` is the last answer's `next`, which the
1092
+ door gives only when `orderBy` names one column; `offset` is at most
1093
+ 10,000.
1094
+ - Every function resolves and never throws. A read that fails keeps its
1095
+ empty values: `{ rows: [], error, code }`. A write that fails is
1096
+ `{ ok: false, error, code }`, never `undefined`. Branch on `code`;
1097
+ show `error`.
1098
+ - No door answers (a copy on its own, an older Makersclaw):
1099
+ `unavailable: true`, code `unavailable`. Show a quiet "not connected
1100
+ yet".
1101
+ - The codes a page meets: `bad_request` (a column, filter or order the
1102
+ table does not have), `no_such_table`, `no_such_action`, `not_found`,
1103
+ `read_only` (a view: write the table it reads), `action_refused` (an
1104
+ action's own `raise exception`: show its sentence), `conflict` (a
1105
+ unique or foreign key), `too_slow`, `try_again`, `rate_limited`.
1106
+ - Writes go one at a time, in the order they were called, as Server
1107
+ Actions did. A slow save never lands after the newer one.
1108
+ - A write that answers ok calls `dataChanged()`: every hook on the page
1109
+ reads again, the brain's too. When the next step needs the new rows
1110
+ (open the row just made), `await whenLanded()` first.
1111
+ - `loading` is true until a hook's first answer, and again when its
1112
+ options change. A re-read keeps the last rows, and `loading` false,
1113
+ until the new ones land; each answer is a new array. A failed re-read
1114
+ keeps the rows and sets `error`.
1115
+ - A hook compares its options by value on every render: a new object
1116
+ with the same values reads nothing. A server component read once per
1117
+ request; a hook's own answer is a render. So a time in a bound
1118
+ (`filter`, the brain's `where`, `since`; a Date or ISO text) is
1119
+ compared to the minute: `useActivity({ since: new Date(Date.now() - 7
1120
+ * 864e5) })` reads once, and again only after the minute has moved
1121
+ on, keeping its rows meanwhile. The read sends the time as given.
1122
+ Anything else worked out on each render (a `Date.now()` number) is new
1123
+ options every time, and reads every time: work it out once
1124
+ (`useMemo`, `useState`).
1125
+ - `useRow` with no `id` asks nothing: `row` is null.
1126
+ - `surface` defaults to the page's surface key (the `<key>` of
1127
+ `/s/<key>` in its path). `data` on `updateRow` is taken and never
1128
+ sent: the door never puts a value in a line. `log: false` as in
1129
+ Writes.
1130
+ - The door writes the activity line. Nothing in the browser logs.
1131
+
1132
+ ```jsx
1133
+ // surfaces/deals.jsx (a browser app)
1134
+ "use client";
1135
+ import { useSearchParams } from "next/navigation";
1136
+ import { useRows, whenLanded } from "../lib/data";
1137
+ import { insertRow, moveRow } from "../lib/actions";
1138
+ import { Notice } from "../components/ui";
1139
+ import { Board } from "../components/blocks/board";
1140
+
1141
+ export default function Deals() {
1142
+ const opened = useSearchParams().get("row");
1143
+ const { rows, error, loading } = useRows({ source: "deals", orderBy: "updated_at desc" });
1144
+ if (error) return <Notice tone="danger">{error}</Notice>;
1145
+ return (
1146
+ <Board
1147
+ rows={loading ? undefined : rows}
1148
+ groupBy="stage"
1149
+ columns={["lead", "call", "won"]}
1150
+ onMove={(d, to) => moveRow({ source: "deals", id: d.id, stateColumn: "stage", to })}
1151
+ onCreate={async (name, stage) => {
1152
+ const r = await insertRow({ source: "deals", values: { name, stage } });
1153
+ if (r.ok) await whenLanded();
1154
+ return r;
1155
+ }}
1156
+ />
1157
+ );
1158
+ }
1159
+ ```
1160
+
1161
+ ### Brain — `lib/brain.js` (browser, both modes)
1162
+
1163
+ The company brain is the workspace's shared records: contacts,
1164
+ companies, deals, whatever kinds the workspace keeps. Each record sits
1165
+ in an area (Everyone, Finance…). Every app sees the same Jane Doe.
1166
+
1167
+ An app reaches the brain only through the data door, `_claw/data`. The
1168
+ gateway answers it under the app's path, so it works while the app's
1169
+ machine sleeps. It hands the app only the kinds `makersclaw.json`
1170
+ declares, and only the records the member may read. Never SQL: the
1171
+ app's database login holds none of the brain.
1172
+
1173
+ **What the app declares: `brain` in `makersclaw.json`.** Write it
1174
+ before the first call. The platform grants it when the app ships; a
1175
+ ship that widens it waits for the member's Yes.
1176
+
1177
+ ```json
1178
+ "brain": {
1179
+ "kinds": [
1180
+ { "kind": "contact", "label": "Contact", "plural": "Contacts",
1181
+ "fields": [{ "name": "name", "type": "text" }, { "name": "email", "type": "email" }],
1182
+ "key": ["email"], "filters": [] }
1183
+ ],
1184
+ "reads": ["note"],
1185
+ "writes": ["contact"],
1186
+ "links": [{ "table": "tasks", "column": "contact_id", "kind": "contact", "on_erase": "null" }]
1187
+ }
1188
+ ```
1189
+
1190
+ - `kinds`: the full def of every kind the app reads or writes, since
1191
+ the workspace may not have it yet. `note`, the brain's own kind, needs
1192
+ none.
1193
+ - `reads` and `writes`: kinds by name. Writing implies reading. The door
1194
+ answers any other kind `not_declared`.
1195
+ - `links`: a `uuid` column of one of the app's own tables (named bare,
1196
+ `"tasks"`) that holds a record's id, and what erasing that record does
1197
+ to the row (`null` or `delete`). Own tables only (`"tables": "own"`).
1198
+
1199
+ `npm run check` checks the block's shape and that every call names a
1200
+ kind it declares (§6). The rest (field types, keys, each link against
1201
+ your tables) the platform checks when the app ships.
1202
+
1203
+ Call these from `"use client"` files, in a server app too. The grant
1204
+ cookie is who is asking, and it makes a write the member's press:
1205
+ confirmed, and able to overwrite a confirmed value, erase or merge. A
1206
+ write from the app's server would be app code acting: unconfirmed, and
1207
+ skipped over a confirmed value. So there is no Server Action for the
1208
+ brain.
1209
+
1210
+ ```js
1211
+ import { listRecords, writeRecord } from "../lib/brain";
1212
+
1213
+ // Reads
1214
+ kinds() → { kinds, brain, error }
1215
+ listRecords({ kind, where, order, limit, cursor, fields, prefix, state })
1216
+ → { records, next, error }
1217
+ getRecord({ id, links = false, state }) → { record, links, error }
1218
+ countRecords({ kind, by, where }) → { count, by, error }
1219
+ searchBrain({ q, kinds, limit, meaning }) → { results, meaning, error }
1220
+ recordHistory({ id, limit, cursor }) → { changes, next, error }
1221
+ brainChanges({ since, kinds }) → { changes, next, error }
1222
+ brainLists({ kind }) → { lists, error }
1223
+ listMembers({ list, limit, cursor }) → { list, records, next, error }
1224
+
1225
+ // Writes
1226
+ writeRecord({ kind, id, fields, area, from, ifVersion, owner, surface, detail })
1227
+ → { ok, id, created, changed, version, skipped }
1228
+ writeRecords({ writes }) → { ok, results } // at most 100, all or none
1229
+ confirmFields({ id, fields }) → { ok, confirmed, version }
1230
+ eraseRecord({ id, reason }) → { ok, erased } // gone for every app, no undo
1231
+ mergeRecords({ keep, drop }) → { ok, id, merged, filled, version }
1232
+ linkRecords({ from, to, type }) → { ok, created }
1233
+ unlinkRecords({ from, to, type }) → { ok, removed }
1234
+ createList({ name, kind, area }) → { ok, id }
1235
+ changeList({ list, add, remove, position }) → { ok, added, removed }
1236
+
1237
+ // Hooks: a read on mount, and again on dataChanged() (lib/door.js's rules)
1238
+ useRecords(args) useRecord(args) // no id: record null, nothing asked
1239
+ useRecordCounts(args) useBrainSearch(args) // a blank q asks nothing
1240
+ → { …the read's values, error, loading, reload }
1241
+ ```
1242
+
1243
+ Every function resolves and never throws:
1244
+
1245
+ - A read resolves the door's JSON with `error: null`.
1246
+ - A write resolves the door's JSON.
1247
+ - A refusal is `{ ok: false, error, code }`. A read keeps its empty
1248
+ values (`records: []`, `next: null`), so the surface renders it like
1249
+ no rows. Branch on `code`; show `error`.
1250
+ - A 404 with no code, or no network, is
1251
+ `{ ok: false, unavailable: true }`: a claw with no door yet. Show a
1252
+ quiet "not connected yet".
1253
+
1254
+ Arguments are camelCase (`ifVersion`). Results are the door's keys as
1255
+ they come (`label_field`, `updated_at`).
1256
+
1257
+ A record is `{ id, kind, area, label, fields, state, unconfirmed, owner,
1258
+ source, created_at, updated_at, version }`. `fields` holds the values;
1259
+ `state` says, field by field, who wrote it and whether a person
1260
+ confirmed it.
1261
+
1262
+ - `where` is an AND of conditions on the kind's fields:
1263
+ `{ stage: "lead" }` equals, `{ stage: ["lead", "won"] }` one of,
1264
+ `{ phone: null }` empty, `{ not: v }`, `{ gt | gte | lt | lte: v }`,
1265
+ `{ has: v }` (a choices field), `{ prefix: "ja" }`. Also `area`,
1266
+ `owner_id`, `unconfirmed`, `updated_at`. A kind's declared `filters` are fast;
1267
+ another field scans the kind and may answer `too_slow`.
1268
+ - `order` is `-updated_at` (the default), `updated_at`, `created_at`,
1269
+ `-created_at`, `label`, `-label`, `<field>` or `-<field>`.
1270
+ - `limit` is 50 by default, at most 200. `cursor` is the last answer's
1271
+ `next`; `next` is null at the end.
1272
+ - `state: false` leaves each field's state out. `fields: ["name"]`
1273
+ returns only those.
1274
+ - `searchBrain` looks by words and by meaning. `meaning: false` is words
1275
+ only: nothing is embedded and nothing is charged. Use it for a picker
1276
+ that searches as the member types. A blank `q` asks nothing.
1277
+ - `brainChanges` is what changed in the member's areas and the app's
1278
+ kinds since `since`, the last answer's `next`.
1279
+ - `ifVersion` refuses a write with `changed` when someone saved the
1280
+ record after the page read it.
1281
+ - `surface` defaults to the page's surface key (the `<key>` of
1282
+ `/s/<key>` in its path: `contacts`, not `/s/contacts`); `detail` is
1283
+ one sentence.
1284
+
1285
+ The door writes the activity line for every write it takes (Activity,
1286
+ below). Never call `logActivity` for a brain write. Writes go one at a
1287
+ time, in the same turn as `lib/door.js`'s. One that answers ok calls
1288
+ `dataChanged()`, so every hook on the page reads again (these four and
1289
+ the own tables'); a server app may also `router.refresh()`.
1290
+
1291
+ The codes a page meets: `not_declared` (the app did not declare that
1292
+ kind, or not for writing), `not_granted` (the workspace has not granted
1293
+ the app's areas yet), `area_not_granted`, `unknown_kind`, `not_found`,
1294
+ `bad_field`, `bad_request`, `changed`, `hidden_match` (a record with
1295
+ that key is in an area the member cannot read), `key_taken`,
1296
+ `not_a_person`, `rate_limited`, `too_slow`, `not_signed_in`.
1297
+
1298
+ **Server reads — `lib/brain-server.js`.** The same nine reads, same
1299
+ arguments and results, for a server component in a server app (a
1300
+ browser app has no such file: its brain reads are the hooks above). They go
1301
+ with the door grant the product proxy stamps on each member's request
1302
+ (`x-claw-door`), to `CLAW_DOOR_URL`. It has no writes. With no grant (a
1303
+ render outside a member's request, or a machine whose proxy does not
1304
+ stamp it yet) each read answers
1305
+ `{ ok: false, unavailable: true, code: "no_door" }`: read in the browser
1306
+ instead.
1307
+
1308
+ ```js
1309
+ // surfaces/contacts.jsx (server)
1310
+ import { listRecords } from "../lib/brain-server";
1311
+ import ContactList from "../components/ContactList";
1312
+
1313
+ export default async function Contacts({ searchParams }) {
1314
+ const first = await listRecords({ kind: "contact", order: "label" });
1315
+ // no_door: ContactList reads with lib/brain.js in the browser.
1316
+ return <ContactList first={first.code === "no_door" ? null : first} highlight={searchParams.row ?? null} />;
1317
+ }
1318
+ ```
708
1319
 
709
1320
  ### Platform — `lib/platform.js` (browser)
710
1321
 
@@ -721,6 +1332,8 @@ import { platform } from "../lib/platform"; // or the named functions
721
1332
  platform.me() GET _claw/me → { artifact: { id, name, vehicle, status, home }, user: { id, email }, blueprint, desk_origin }
722
1333
  platform.ask({ text, row, how, job, list, surface, sessionId, open })
723
1334
  POST _claw/ask → { ok, request, desk_origin }, and the column opens on it
1335
+ platform.llm(input, { system, schema, maxTokens, tier, task, signal })
1336
+ POST _claw/llm → { ok: true, text } | { ok: true, data } | { ok: false, error, code } (Calling a model)
724
1337
  platform.requests() GET _claw/requests → { requests, run, desk_origin }
725
1338
  platform.requests({ id }) GET _claw/requests?id=<id> → { request }
726
1339
  platform.stopRequest(id) POST _claw/requests/<id>/stop → { ok, request }
@@ -744,13 +1357,16 @@ didnt_finish. `line` is the one line every screen shows for it
744
1357
  show it as it is, never a status word of your own. `app_session_id` is
745
1358
  set only on your own requests.
746
1359
 
747
- Named exports: `me, ask, requests, stopRequest, openRequest,
1360
+ Named exports: `me, ask, llm, requests, stopRequest, openRequest,
748
1361
  surfaceOfPath, startJob, cancelJob, reportError, platform`. That is the
749
1362
  whole list. Note what is **not** there: the runs, the chat history, Run
750
1363
  now, Pause / Resume. Those are MONITORING and they are the OS's — the
751
1364
  window's tabs and title bar — so an app never calls `_claw/agent`,
752
1365
  `_claw/runs`, `_claw/run`, `_claw/pause` or `_claw/resume`. Asking is the
753
1366
  opposite thing, and it belongs here (`ask()` and `useJob`, below).
1367
+ `llm()` is neither: one call to a model, answered in the page (Calling a
1368
+ model, below). It is the one wrapper that resolves the platform's answer
1369
+ as it is, `{ ok: true, text }` and not `{ ok: true, status, data }`.
754
1370
  `requests` is not monitoring either: it is this app's asks in flight, so
755
1371
  an empty surface can show the work coming (§3a). `startJob` and
756
1372
  `cancelJob` are the 0.3.x door to the same work, kept so `useJob` can
@@ -763,8 +1379,8 @@ that exact origin — never `"*"` — and only while the app is framed.
763
1379
 
764
1380
  ### Hooks — `lib/hooks.js` ("use client")
765
1381
 
766
- Four hooks: a generic fetch over one `_claw` route, the app's identity,
767
- a job, and the app's requests. Each owns fetching, polling (while the
1382
+ Five hooks: a generic fetch over one `_claw` route, the app's identity,
1383
+ a job, the app's requests, and a model call. Each owns fetching, polling (while the
768
1384
  tab is visible; once a minute while `unavailable`), streaming and cleanup
769
1385
  on unmount. A surface renders what comes back and owns every pixel.
770
1386
 
@@ -775,6 +1391,7 @@ useJob() → { running, status, queued, que
775
1391
  steps, text, progress, error, sessionId, requestId, request, finishedAt,
776
1392
  start, cancel, reset, open }
777
1393
  useRequests({ surface, job, every = 5000 }) → { requests, live, run, loading, error, unavailable, reload, open, stop }
1394
+ useLlm() → { run, loading, result, error, unavailable } (Calling a model)
778
1395
  jobFromRequest(request) → what useJob shows for one request (pure; for tests)
779
1396
  ```
780
1397
 
@@ -791,10 +1408,10 @@ while the App tab is on screen (behind the window's other tabs it asks
791
1408
  nothing; coming back looks at once); a start through `useJob` on the
792
1409
  same page makes it look at once. Hand it to `LiveEmpty` (§3a).
793
1410
 
794
- There is no chat, runs or activity hook: the OS draws those tabs.
795
- Activity has no hook either — it is a Postgres table, read server-side
796
- with `loadActivity` by any domain surface that wants a slice of it (a
797
- row's history beside the row, say).
1411
+ There is no chat or runs hook: the OS draws those tabs. A domain
1412
+ surface that wants a slice of the activity (a row's history beside the
1413
+ row, say) reads it with `loadActivity`: on the server in a server app,
1414
+ or with `useActivity` (`lib/door.js`, §5 Door) from the browser.
798
1415
 
799
1416
  ### Commissioning work — `useJob()`
800
1417
 
@@ -951,14 +1568,212 @@ if (!r.ok) setError(r.error); // r.code: needs_answer, queue_fu
951
1568
  It resolves once the platform took the ask (`{ ok: true, data: {
952
1569
  request, desk_origin } }`) or refused it; it never waits for the work.
953
1570
 
1571
+ ### Calling a model — `llm()`
1572
+
1573
+ `llm()` is for one-shot text work on what is on screen: sort these
1574
+ mails, draft this reply, translate this note, summarise this thread,
1575
+ pull the date out of this line. The answer comes back in the page, and
1576
+ the screen does what it likes with it.
1577
+
1578
+ **`llm()` for one-shot text work on what the screen holds; `ask()` for
1579
+ work that needs tools, the workspace, or more than one step.** Finding
1580
+ leads, reading the Wiki, writing rows for forty threads, anything that
1581
+ should show in the agent column and the Requests list: that is `ask()`.
1582
+ The app's agent does it, with its tools. `llm()` has none: it sees the
1583
+ words you hand it and nothing else.
1584
+
1585
+ ```js
1586
+ "use client";
1587
+ import { updateRow } from "../lib/actions";
1588
+ import { llm } from "../lib/platform";
1589
+
1590
+ // Text.
1591
+ const r = await llm(thread.body, {
1592
+ system: "Draft a short, warm reply from the founder. Plain text, no greeting.",
1593
+ task: "draft-reply",
1594
+ });
1595
+ if (r.ok) setDraft(r.text); // r.error says why not
1596
+
1597
+ // Data: a schema in, an answer checked against it out.
1598
+ const SORT = {
1599
+ type: "object",
1600
+ required: ["label", "urgent"],
1601
+ properties: { label: { enum: ["sales", "support", "billing", "other"] }, urgent: { type: "boolean" } },
1602
+ };
1603
+ const s = await llm(`From: ${mail.from}\nSubject: ${mail.subject}\n\n${mail.body}`, { system: "Sort this mail.", schema: SORT, task: "sort-mail" });
1604
+ if (s.ok) await updateRow({ source: "mail", id: mail.id, patch: { label: s.data.label, urgent: s.data.urgent } });
1605
+ ```
1606
+
1607
+ `data` is the model's JSON as it came: **a key your schema does not name
1608
+ is kept**. Read the keys you asked for, as above, and do not spread
1609
+ `data` into a row.
1610
+
1611
+ `llm(input, opts)` takes:
1612
+
1613
+ - `input` — what to work on: a string, or a conversation, `[{ role:
1614
+ "user" | "assistant", content }]`: at most 100 messages, none empty,
1615
+ the last one the user's (the one to answer). Required. An empty one
1616
+ asks nothing of the platform and resolves `bad_input`.
1617
+ - `system` — how to go about it, at most 8,000 characters.
1618
+ - `schema` — a JSON Schema for the answer, an object, at most 16 KB
1619
+ once written out. The answer then comes back as `data`, checked
1620
+ against it, not as `text`. If the model's reply misses, the platform
1621
+ sends it back once with what was wrong; a second miss is
1622
+ `schema_mismatch`. The platform runs your schema on the model's words,
1623
+ so what it may hold is bounded (below).
1624
+ - `maxTokens` — the answer's ceiling, a whole number, 1 or more. 1,024
1625
+ unless you say; over 4,096 is cut to 4,096. (It travels as
1626
+ `max_tokens`.)
1627
+ - `tier` — `"fast"` (the default) or `"smart"` for the harder job. An
1628
+ app never names a model. `smart` costs more and is slower: a long
1629
+ answer from it can run into the 60 second limit, so keep `maxTokens`
1630
+ modest there.
1631
+ - `task` — a short label for the usage line, `a-z`, `0-9` and `-`, at
1632
+ most 40 characters (`"sort-mail"`).
1633
+ - `signal` — an `AbortSignal`. An abort resolves `{ ok: false, aborted:
1634
+ true }`.
1635
+
1636
+ **What a schema may hold.** Write it out as a tree, small. The call
1637
+ refuses a schema with:
1638
+
1639
+ - `pattern` or `patternProperties` (`bad_input`): a regular expression
1640
+ run on a model's words can hang the platform. Use `enum`, `format` or
1641
+ length bounds.
1642
+ - `$ref`, `$defs` or `definitions` (`bad_input`; `$dynamicRef` and
1643
+ `$recursiveRef` too): each reference is checked in full where it is
1644
+ used, so a few can mean a tree of any size. Write the shape out where
1645
+ it is used.
1646
+ - nesting deeper than 20 levels, a list inside a list counted
1647
+ (`bad_input`).
1648
+ - more than 1,000 parts (`too_large`): every object in it, every `enum`
1649
+ value and every type listed in a `type` array. A real answer's schema
1650
+ is a few dozen.
1651
+ - a schema it cannot read, such as a `type` it does not know
1652
+ (`bad_input`).
1653
+
1654
+ A reply with so many values that checking it against the schema would
1655
+ cost too much is a miss like any other (`schema_mismatch`, and the
1656
+ model is told why): ask for fewer values, or a smaller schema. Make the
1657
+ root an object (`type: "object"`): providers' structured answers mostly
1658
+ want one, and when one refuses the format the platform asks again in
1659
+ words alone, which is less sure. Everything else in JSON Schema is let
1660
+ through.
1661
+
1662
+ It resolves, and never throws:
1663
+
1664
+ - `{ ok: true, text }` — no schema.
1665
+ - `{ ok: true, data }` — with a schema.
1666
+ - `{ ok: false, error, code, status }` — a refusal, as the platform
1667
+ sent it. Branch on `code`. **Show `error` as it is for the codes that
1668
+ speak to the member, and a plain line of your own for the rest.** The
1669
+ rest speak to you, the developer ("schema uses "pattern""), and a
1670
+ member should not read them.
1671
+
1672
+ Words for the member, shown as they are:
1673
+
1674
+ - `no_credits` (402): the workspace's wallet is empty, the workspace
1675
+ is suspended, or the Free plan's daily limit is used up.
1676
+ `member_cap` (402): the member's monthly spend cap is used up.
1677
+ `app_cap` (402): this app's weekly cap is used up. Say so and stop;
1678
+ do not retry.
1679
+ - `rate_limited` (429): 30 calls a minute per member. Wait a moment,
1680
+ then again.
1681
+ - `model_failed` (502), `try_again` (503) and `failed` (500): the
1682
+ model, the membership check or the platform could not answer just
1683
+ now. Try again.
1684
+ - `timeout` (504): a call is held to 60 seconds, a schema retry
1685
+ included, and one the clock cuts is still charged. Ask for less (a
1686
+ lower `maxTokens`, a shorter input) rather than the same again.
1687
+ - `no_member_identity` (401) and `not_a_member` (403): the page is not
1688
+ running in a member's own session (`no_member_identity` says to open
1689
+ the app from the desk again), or the member was removed from the
1690
+ workspace.
1691
+
1692
+ Words for you, not shown:
1693
+
1694
+ - `bad_input` (400): the call itself is wrong; fix the code.
1695
+ - `too_large` (413): the body is over 128 KB, `system` over 8,000
1696
+ characters, `schema` over 16 KB or 1,000 parts, or `input` over 100
1697
+ messages. Send less.
1698
+ - `schema_mismatch` (422): the model could not fit the schema, twice;
1699
+ `error` names where it missed. Loosen the schema or ask for less.
1700
+ - `cross_origin` (403): not the app's own page (a typed URL, a form
1701
+ post).
1702
+ - `not_configured` (503): the platform has no model to give.
1703
+ - `{ ok: false, unavailable: true }` — the platform has no such door (a
1704
+ claw from before it) or there is no network. Render the quiet "not
1705
+ connected yet" of every `_claw` door, or leave the button out.
1706
+
1707
+ `useLlm()` is the same call from a component: `{ run, loading, result,
1708
+ error, unavailable }`. `run(input, opts)` takes the same arguments and
1709
+ resolves the same answer; `result` is that answer when it was ok, `error`
1710
+ the refusal's words, and `unavailable` is true on a claw with no door (or
1711
+ no network), when `error` stays null: that is the quiet "not connected
1712
+ yet", not an alert, as for `useClaw`. A new run aborts the one before it,
1713
+ so a slow answer never lands over a newer one, and leaving the page
1714
+ aborts the call in flight. `error` is the sentence whatever the code, so
1715
+ a screen that must tell the money (`no_credits`, `member_cap`, `app_cap`)
1716
+ from the rest reads `code` from what `run()` resolves.
1717
+
1718
+ ```jsx
1719
+ "use client";
1720
+ import { Button, Notice } from "../components/ui";
1721
+ import { useLlm } from "../lib/hooks";
1722
+
1723
+ export default function Translate({ note }) {
1724
+ const { run, loading, result, error, unavailable } = useLlm();
1725
+ if (unavailable) return <p className="text-sm text-muted-foreground">Translation is not connected yet.</p>;
1726
+ return (
1727
+ <div className="grid gap-2">
1728
+ <Button loading={loading} onClick={() => run(`Translate to French:\n${note.text}`, { task: "translate" })}>
1729
+ Translate
1730
+ </Button>
1731
+ {error ? <Notice tone="danger">{error}</Notice> : result ? <p className="text-sm">{result.text}</p> : null}
1732
+ </div>
1733
+ );
1734
+ }
1735
+ ```
1736
+
1737
+ What to keep in mind:
1738
+
1739
+ - **Browser only, in both modes.** The call rides the member's session:
1740
+ `llm()` and `useLlm()` run in a `"use client"` file. A server
1741
+ component or a server action has no member to charge and cannot call
1742
+ it.
1743
+ - **It spends the workspace's credits**, like any inference, and counts
1744
+ against the member's monthly cap and the app's weekly cap. A schema
1745
+ call the model misses is asked again once, and both are charged; so is
1746
+ a call the 60 second clock cuts. Call it on a press, never on every
1747
+ render or every keystroke, and never once per row of a long list: that
1748
+ is a batch, and a batch is `ask()`'s.
1749
+ - **The answer is words, not markup.** Render `text` as text. Never put a
1750
+ model's answer in `dangerouslySetInnerHTML`.
1751
+ - **The answer is not saved.** What matters goes into a row
1752
+ (`updateRow`, `insertRow`) before the page closes.
1753
+ - **No streaming.** The answer arrives whole.
1754
+ - **Nothing appears on the desk.** No request, no thread, no run: the
1755
+ agent column and the Requests list are `ask()`'s. The call is metered
1756
+ to the member who made it; the usage line names the app, the tier and
1757
+ the `task` label. Claw does not log the request or the reply.
1758
+ - **Never by its path.** A string naming `_claw/llm` fails
1759
+ `npm run check` (§6, `llm`): the two functions are the contract, and
1760
+ they handle what a bare `fetch` would not (a claw without the door, a
1761
+ dropped network, an abort).
1762
+
954
1763
  ### Activity — the contract
955
1764
 
956
- `public.artifact_activity` is how the founder sees the work. The OS's
957
- **Activity tab** on the app window reads it, newest first — and so can
958
- any domain surface that wants to (`loadActivity`). Every
959
- `lib/actions.js` write inserts a row through `logActivity()` in
960
- `lib/db.js`; the artifact's agent writes the same table with the `sql`
961
- verb, `by = 'agent'` or `'run:<id>'`.
1765
+ `claw_app.activity` is how the founder sees the work. It is the
1766
+ platform's view of the workspace's activity table, and it holds only
1767
+ this app's lines. The OS's **Activity tab** on the app window reads
1768
+ them, newest first — and so can any domain surface that wants to
1769
+ (`loadActivity`). In a server app every `lib/actions.js` write inserts a
1770
+ line through `logActivity()` in `lib/db.js` (but a `log: false` patch or
1771
+ action). In a browser app the door writes it, for every write it takes
1772
+ (`lib/door.js`), with the same `log: false`. The app's agent writes the
1773
+ same view
1774
+ with the `sql` verb, `by = 'agent'` or `'run:<id>'`. A brain write's
1775
+ line is the door's: it writes one for every write it takes (`lib/brain.js`
1776
+ never logs, and neither does the page).
962
1777
 
963
1778
  ```js
964
1779
  await logActivity({
@@ -971,13 +1786,22 @@ await logActivity({
971
1786
  });
972
1787
  ```
973
1788
 
974
- Columns: `id, artifact_id, at, action, surface, row_ref, detail, data
975
- (jsonb), by`. `loadActivity` returns them without `artifact_id` — every
976
- row is this artifact's. The OS Activity tab shows the surface's label
977
- on each line and does not link into the app yet; the convention for
978
- anything that does (a domain surface's own history slice today, the tab
979
- later) is `surface` + `row_ref` → `/s/<surface>?row=<row_ref>`, so stamp
980
- both on every line and make the surface honour `?row=`.
1789
+ The view fills in `artifact_id` (the app whose role writes the line)
1790
+ and `id`, so an insert names neither; one that names another app's id
1791
+ is refused. Columns: `id, artifact_id, at, action, surface, row_ref,
1792
+ detail, data (jsonb), by`. `loadActivity` returns them without
1793
+ `artifact_id` — every line is this app's. The OS Activity tab shows the
1794
+ surface's label on each line and does not link into the app yet; the
1795
+ convention for anything that does (a domain surface's own history slice
1796
+ today, the tab later) is `surface` + `row_ref` → `/s/<surface>?row=<row_ref>`,
1797
+ so stamp both on every line and make the surface honour `?row=`.
1798
+
1799
+ Up to 0.6.x the lines went into the `artifact_activity` table in
1800
+ `public`, named with `CLAW_ARTIFACT_ID`. An app on its own schema can
1801
+ reach nothing in `public`, so since 0.7.0 both functions go through the
1802
+ view; the lines are the same. Outside Makersclaw there is no view:
1803
+ `logActivity` warns and returns false, and `loadActivity` returns the
1804
+ database's `error`.
981
1805
 
982
1806
  ### Errors
983
1807
 
@@ -1009,74 +1833,261 @@ ok controls every control is a kit part
1009
1833
  ok examples examples show only outside a workspace (useFixture); a workspace app opens empty
1010
1834
  ok manifest 2 surfaces: drafts, sent
1011
1835
  ok icons every icon name exists
1012
- ok kit 70 fixed files match the template
1836
+ ok kit 74 fixed files match the template
1013
1837
  ok depictions 1 picture of elsewhere, framed on 1 surface: x-post
1838
+ ok agent no SQL write in app code; the agent's brief names every action with its call (1), every table written (2) and every server action that writes (0)
1839
+ ok tables its own schema (makersclaw.json): every table named bare, and each bare name one of the 2 its schema makes (../install/schema.sql); claw. and claw_app. are the platform's
1840
+ ok brain no brain block in makersclaw.json; no lib/brain.js call names a kind
1841
+ ok viewport nothing in the app sizes itself by the window
1842
+ ok llm no screen names _claw/llm: a model is called through llm() or useLlm() (README §5)
1014
1843
  ok smoke built in 41s · / → 307 · /s/drafts 200 · /s/sent 200 (0.6s)
1015
1844
  all clear
1016
1845
  ```
1017
1846
 
1847
+ A browser app's check has one more line, and its smoke serves files:
1848
+
1849
+ ```
1850
+ ok browser files and a door: 2 client surfaces; no server code, route, proxy or segment config; env only by the names a page may print
1851
+ ok smoke built in 38s (copy at ~/.claw-check/app) · export 2.1 MB in 64 files · / → 200 to /s/drafts · /s/drafts 200 · /s/sent 200 (0.1s)
1852
+ ```
1853
+
1018
1854
  `kit` refuses a fixed file that differs from the template (they are
1019
1855
  hashed in `check/pins.json`) and any file of yours under `components/ui`
1020
- or `components/parts`. `depictions` reads both directions: nothing of
1856
+ or `components/parts`. `viewport` refuses anything in your code that
1857
+ sizes itself by the window: `min-h-dvh`, `min-h-screen`, `h-screen`,
1858
+ `h-dvh` (and their `svh`, `lvh` and `max-h-` kin), and any `vh` unit —
1859
+ `h-[80vh]`, `max-h-[60vh]`, `h-[calc(100vh-4rem)]`, `style={{ height:
1860
+ "100vh" }}`. The window has one owner, the kit's `AppLayout` (§3b,
1861
+ Layouts); a box inside it is bounded by its parent (`h-full`, `flex-1
1862
+ min-h-0`) or by a size (`max-h-96`). `depictions` reads both directions: nothing of
1021
1863
  the workspace inside a depiction (a token, `font-sans`, `dark:`, a rem
1022
1864
  utility), nothing of the depiction outside its frame (a named import, a
1023
1865
  picture in the shell or a `Card`, a dead file nobody renders).
1024
1866
 
1867
+ `agent` holds the app's agent to what the screens do. Its brief beside
1868
+ the app (`../AGENTS.md`, `../references/*.md`) names, in backticks, every
1869
+ server action that writes. In an app on its own tables (`"tables":
1870
+ "own"`) the screens write through two doors only, so it also refuses:
1871
+
1872
+ - SQL that writes in the app's code: `insert into`, `update … set` or
1873
+ `delete from`, in any case, in a string of any `.js`, `.jsx` or `.mjs`
1874
+ but the fixed `lib/`, where it reads as a statement (a `where`, a
1875
+ `values`, a `set col =`, a `;` after it; handed to `query()`; or the
1876
+ whole string). Both modes, brief or none. A one-row write is
1877
+ `insertRow`, `updateRow`, `moveRow`, `deleteRow` or `saveSetting`;
1878
+ anything else is an action (§5). Prose in JSX text and a label in an
1879
+ attribute (`title="Delete from board"`) are not SQL; the same words
1880
+ alone in a string of code are, so word such a label otherwise.
1881
+ - The data door by its path: a string naming `_claw/data`,
1882
+ `data/actions/` or `data/tables/`, or `claw("data/…")`. The screens
1883
+ reach the door through its functions, which this rule and the ship
1884
+ can read.
1885
+ - An action the brief does not name with its call. For `runAction({
1886
+ name: "close_cycle" })` the brief says `` `close_cycle` `` and `select
1887
+ close_cycle(`, so the agent runs the same function:
1888
+ ```
1889
+ - `close_cycle`: the Close cycle button. Run `select close_cycle($1::jsonb)`
1890
+ with params `[{"cycle": "<id>"}]` (sql execute). It carries the open goals
1891
+ over and refuses a closed cycle. Then log `closed` on `cycles`.
1892
+ ```
1893
+ - A table a one-row write names (`source: "deals"`) that the brief does
1894
+ not name in backticks, with the one statement that does the same.
1895
+ - Brief or none: a name the code decides at run time (`runAction({ name:
1896
+ kind })`: name it in the code, so the brief can), `runAction("x")`
1897
+ (it takes one object) and a name that is not one bare, lower-case name.
1898
+
1899
+ A name or a source is read where it is written: a literal, or a `const`
1900
+ string of the same file that the call sees; a surface's `source:
1901
+ surface.source` is its entry's in `surfaces.js`. A call counts however
1902
+ the file reaches the function: imported, renamed, through `import * as`,
1903
+ with `?.(`, or through an app file that re-exports it. A one-row write
1904
+ whose table the code decides at run time (a prop) is not checked, and
1905
+ the ok line says how many. `node check/run.mjs
1906
+ --actions` prints what the screens run and write, `{"actions": […],
1907
+ "tables": […]}`, and exits 0: the platform's ship checks that every
1908
+ action is a function in the app's schema before it builds. An app with
1909
+ no `tables` key keeps the first sentence of this rule only.
1910
+
1911
+ `tables` reads every table name in the app's code when `makersclaw.json`
1912
+ says `"tables": "own"`: a surface's `source`, the `source` of every
1913
+ `lib/actions.js` and `lib/data.js` call (a `const` string of the same
1914
+ file counts, where the call sees it), and the name after `from`, `join`,
1915
+ `into` and `update` in SQL (not a column's `from`: `extract(epoch from
1916
+ t.at)`, `is distinct from`). Your tables are named bare (`deals`): the app's own schema answers
1917
+ them, and a second copy of the app lives in another. It refuses
1918
+ `public.<t>` (an app on its own schema reaches nothing there),
1919
+ `app_<x>.<t>` (a schema by name, even this app's), and `brain.<t>` or
1920
+ `brain_api.<t>` (the company brain goes through `lib/brain.js`, §5).
1921
+ `claw.` and `claw_app.` are the platform's (`claw_app.activity`, the
1922
+ activity view) and pass. An app laid before 0.7.0 has no `tables` key:
1923
+ its tables are in `public`, and this rule leaves its names alone.
1924
+ `"tables": "moved"` is refused: that is the platform's word for an old
1925
+ app whose tables its workspace moved, never a manifest's.
1926
+
1927
+ And each bare name must be a table or view the app's own schema makes,
1928
+ so none leans on something else. The list is the first found of:
1929
+ `CLAW_APP_TABLES` (the platform passes it when it ships the app, read
1930
+ from the live schema), `../install/schema.sql` (a play folder's), and
1931
+ `../../schema.sql` (a store package's), from their `create table` and
1932
+ `create view`. The names read: a surface's `source` in `surfaces.js`
1933
+ (a literal or a const), the `source` of a `lib/data.js`, `lib/door.js`
1934
+ or `lib/actions.js` call by its name (`loadRows`, `useRows`,
1935
+ `insertRow`…, either side of a ternary), and in SQL — strings joined
1936
+ with `+` read as one, handed to `query()` or starting as a statement —
1937
+ the name after `from` (and after its commas), `join`, `insert into`,
1938
+ `update … set`, a delete's or merge's `using`, and `table`, `truncate`
1939
+ or `copy`. A CTE, a function (`from unnest(…)`), a SQL string or
1940
+ comment and `pg_*` are not tables. Not read: a call through an import
1941
+ alias, options held in a variable (`loadRows(opts)`), SQL a helper takes
1942
+ that does not start as a statement, and SQL built at run time. With no
1943
+ list here, the ok line says "no table list here". The same SQL is read
1944
+ for `public.`, `app_<x>.` and `brain.` names that only it shows: after a
1945
+ comma, in a `using`, with spaces around the dot.
1946
+
1947
+ `brain` reads the `brain` block of `makersclaw.json` (§5) and every
1948
+ call of `lib/brain.js` or `lib/brain-server.js` whose kind is written in
1949
+ the code (`kind: "contact"`, or a `const` string the call sees).
1950
+ The block's `kinds`, `reads`, `writes` and `links` are arrays, and each
1951
+ kind has `kind`, `label`, `plural` and `fields`. A read's kind
1952
+ (`listRecords`, `countRecords`, `brainLists`; the `kinds` of
1953
+ `searchBrain` and `brainChanges`) is in `reads` or `writes`. A write's
1954
+ (`writeRecord`, each entry of `writeRecords`, `createList`) is in
1955
+ `writes`. A kind that only `kinds` defines is refused too: the door
1956
+ hands the app only what `reads` and `writes` name. A kind held in a
1957
+ variable is not read here; the door refuses it on the member's screen.
1958
+
1959
+ `llm` reads every app, in both modes: no string in the code names
1960
+ `_claw/llm`, and none handed to `claw()`, `clawUrl()` or `clawStream()`
1961
+ starts `llm`. A model is called with `llm()` or `useLlm()` (§5, Calling a
1962
+ model), which handle a claw without the door, a dropped network and an
1963
+ abort; a `fetch` of the path handles none of them. The path is read as
1964
+ a browser reads it (`claw("//llm")`, `claw("./llm")` and `"/_claw/./llm"`
1965
+ are the door too). A path built from pieces or held in a variable is not
1966
+ read here, as for rule 8. A longer name (`_claw/llm-notes`) is another
1967
+ route, and a comment or text in JSX is not a string of code.
1968
+
1969
+ `browser` reads a browser app only (`"mode": "browser"`): its pages are
1970
+ files built once, and everything they read or write goes through the
1971
+ platform's data door. So it refuses, file and line, a `"use server"`
1972
+ module or function; an import of `next/headers`, `next/cache`,
1973
+ `next/server`, `pg`, `lib/db` or `lib/brain-server`; a `route.js` under
1974
+ `app/`; a `proxy.js` or `middleware.js` at the root (under export Next
1975
+ ignores it); an export of `dynamic`, `revalidate`, `fetchCache`,
1976
+ `runtime` or `dynamicParams`; a surface whose first line is not `"use
1977
+ client"`; and a `process.env` name other than `CLAW_BASE_PATH`,
1978
+ `CLAW_PRODUCT`, `NEXT_PUBLIC_*` and `NODE_ENV` — a prerendered page
1979
+ prints what it reads into a file anyone who opens the app can fetch.
1980
+
1025
1981
  The smoke test starts that build on a loopback port and loads every
1026
- surface; `--static` skips the build and the smoke. Then, by eye:
1982
+ surface; `--no-build` skips the build and the smoke. A browser app's smoke
1983
+ builds in a copy of the app (`~/.claw-check/<app>`), never in it, with
1984
+ no `.env`; serves the export by the platform's rules; and checks that
1985
+ `/` forwards to a surface and every `/s/<key>` answers 200 with no error
1986
+ page. That proves every page prerenders, not that a surface renders
1987
+ with data: the rows come in the browser. Then, by eye:
1027
1988
 
1028
1989
  - Renders at 360px, 768px and 1280px without horizontal scroll.
1990
+ - The page itself never scrolls: the shell is the window's height
1991
+ (`AppLayout`), and each pane scrolls inside it. With the longest
1992
+ content a surface can hold (a 30-page article, a 40-message thread,
1993
+ a tall inspector), the last line and every button after it are
1994
+ reachable, at 375px and at 1280px; the header and the footer stay in
1995
+ view.
1029
1996
  - In a workspace, an empty table shows `LiveEmpty`: the work in flight,
1030
1997
  else what would be here and the manual way to add it — never an
1031
1998
  example (§3a). A saved row replaces it.
1032
1999
  - Error state shows the text from `{ error }`.
1033
2000
  - Keyboard focus visible on every control.
1034
2001
  - Numbers tabular (`data-nums`).
1035
- - Every write is a server action — `lib/actions.js`, or the app's own
1036
- `"use server"` file (§5) — never raw SQL in a surface.
2002
+ - Every write goes through `lib/actions.js` (a one-row write or
2003
+ `runAction`), or in a server app the app's own `"use server"` file
2004
+ (§5) — never raw SQL in a surface.
1037
2005
  - Every surface reachable from the nav.
1038
2006
 
1039
2007
  ## 7. Run
1040
2008
 
1041
2009
  1. `makersclaw init` laid this into the artifact's `app/` (in Makersclaw,
1042
- `artifact_scaffold {dir}` runs it and makes `public.artifact_activity`
1043
- in the workspace database). `makersclaw.json` records the version and
1044
- the blocks; `makersclaw update` moves the fixed files and blocks to a
1045
- newer version and never touches your own files. `pnpm-lock.yaml` is
1046
- cut from the lock this version ships (by init, add and update), so
1047
- every app on the version installs the same packages;
1048
- `makersclaw install` installs from it. Never edit it by hand.
1049
- 2. `app_database {name, dir}` → `DATABASE_URL` in `.env`. The app's role
1050
- reads and writes every table in `public`.
1051
- 3. Make the domain tables with the `sql` verb (they land in `public`).
2010
+ `artifact_scaffold {dir}` runs it). `makersclaw.json` records the
2011
+ version, the blocks, where the tables live (`"tables"`, from
2012
+ `init --tables`; §5, Your tables) and the mode (`"mode"`, from `init
2013
+ --mode`; §2). `makersclaw update` moves the fixed files and blocks
2014
+ to a newer version; it never touches your own files, and keeps every
2015
+ other key of `makersclaw.json` (`tables`, `mode`, `brain`). Only
2016
+ `makersclaw mode` changes the mode (§4). `pnpm-lock.yaml` is cut from the lock this version ships
2017
+ (by init, add and update), so every app on the version installs the
2018
+ same packages; `makersclaw install` installs from it. Never edit it
2019
+ by hand.
2020
+ 2. `app_database {name, dir}` gives the app its own schema and role.
2021
+ A server app gets `DATABASE_URL` in its environment; a browser app
2022
+ gets none, since its pages reach the tables through the door and its
2023
+ agent through `sql`. The role reads and writes the app's own tables,
2024
+ by bare name. The platform lays the activity door,
2025
+ `claw_app.activity`, with the database (§5, Activity); the app makes
2026
+ no activity table of its own.
2027
+ 3. Make the domain tables with the `sql` verb, bare
2028
+ (`create table if not exists deals (…)`): they land in the app's own
2029
+ schema. An app's first tables are often there before its first
2030
+ build, made by the chat that created it (`artifact_manage
2031
+ install_tables`): read them with `sql schema` first. An app from
2032
+ before 0.7.0 makes them in `public`, with the same bare names.
1052
2033
  4. `product_run` with this directory. While building, run it as a dev
1053
2034
  server — `command "npx next dev -H 127.0.0.1"`, `build ""` — so each
1054
2035
  edit shows in the founder's live preview without a rebuild.
1055
2036
  `product_run` sets `CLAW_BASE_PATH`, `CLAW_PRODUCT`,
1056
2037
  `CLAW_PRODUCT_TOKEN` and `CLAW_ARTIFACT_ID` — the last is what turns
1057
- examples off (§3a).
2038
+ examples off (§3a). A browser app previews the same way, on `next
2039
+ dev`; its surfaces read through the door there too.
2040
+ 5. `npm run check` (§6) before `surface_built` and before launch. For a
2041
+ browser app its smoke builds the export as the platform will: in a
2042
+ copy of the app (`~/.claw-check/<app>`), never in it; with no `.env`;
2043
+ served by the platform's rules. It proves every page prerenders and
2044
+ `/` forwards to a surface, not that a surface renders with data: look
2045
+ at the preview for that.
1058
2046
 
1059
2047
  Every `artifact_files` write under `app/` comes back with the console's
1060
2048
  state; a 5xx carries the log tail. Fix it before moving on — the founder
1061
2049
  is watching the preview.
1062
2050
 
1063
- Every page is `dynamic = "force-dynamic"`: nothing is prerendered, so a
1064
- build never needs the database. `app/auto-refresh.jsx` re-renders the
1065
- page from the server while it is shown, so the agent's writes appear:
1066
- one refresh at a time, 5 s after the last one landed plus what that one
1067
- took (a minute at most), and only while the App tab is on screen in a
1068
- visible browser tab (hidden, it asks nothing; back, it refreshes at
1069
- once), nobody is typing (never while a sheet is open, a field they typed
1070
- in holds focus, or within 10 s of a keystroke), and the app answers
1071
- `/claw-alive` (a fixed route) with a 204.
2051
+ In a server app every page is `dynamic = "force-dynamic"`: nothing is
2052
+ prerendered, so a build never needs the database.
2053
+
2054
+ A browser app's pages are the opposite: `next build` prerenders each once
2055
+ (`output: "export"`), `/` as a page that forwards to the home surface
2056
+ (a script keeping the query, and a meta refresh) and
2057
+ `/s/<key>` as one file per surface, whose surface renders in the browser
2058
+ inside `<Suspense>`. Its theme is set by a script in `<head>` before the
2059
+ page paints: `?theme=`, then the `claw_theme` cookie, then dark. The
2060
+ platform builds it with `CLAW_BASE_PATH`, `CLAW_PRODUCT` and
2061
+ `CLAW_ARTIFACT_ID` and nothing else of the app's environment, and
2062
+ serves the files it leaves; no code of the app's runs on the workspace's
2063
+ machine.
2064
+
2065
+ `app/auto-refresh.jsx` re-renders the page from the server while it is
2066
+ shown, so the agent's writes appear: one refresh at a time, 5 s after
2067
+ the last one landed plus what that one took (a minute at most), and only
2068
+ while the App tab is on screen in a visible browser tab (hidden, it asks
2069
+ nothing; back, it refreshes at once), nobody is typing (never while a
2070
+ sheet is open, a field they typed in holds focus, or within 10 s of a
2071
+ keystroke), and the app answers `/claw-alive` (a fixed route) with a
2072
+ 204.
2073
+
2074
+ A browser app's `app/auto-refresh.jsx` keeps every one of those rules,
2075
+ but a tick does not re-render anything from a server. It asks the data
2076
+ door what changed (`_claw/data/changes`: the brain's changes in the
2077
+ app's kinds, and a stamp of the app's own tables), and only when
2078
+ something did, every hook on the page (`useRows` and the rest, the
2079
+ brain's too) reads again. A brain change shows on the next tick. A
2080
+ write the agent or a job makes to the app's own tables shows within
2081
+ about 11 s: the stamp comes from Postgres's counters, which lag.
1072
2082
 
1073
2083
  **A surface that must not be refreshed opts out with
1074
2084
  `data-claw-refresh="off"` on its root element**: one whose rows only its
1075
2085
  own writes change, or a table too big to send every few seconds. While
1076
2086
  any element on the page carries it, nothing is refreshed and nothing is
1077
2087
  probed; the surface's own writes still re-render it (`lib/actions.js`
1078
- revalidates), and so does every navigation. Set it from state to pause
1079
- only while it matters: `data-claw-refresh={importing ? "off" : undefined}`.
2088
+ revalidates; in a browser app every write reads the hooks again), and so
2089
+ does every navigation. Set it from state to pause only while it matters:
2090
+ `data-claw-refresh={importing ? "off" : undefined}`.
1080
2091
 
1081
2092
  **A page left open stops refreshing after 30 minutes with no sign of
1082
2093
  anyone** in the app — a key, the pointer, a touch, a wheel, focus. The