makersclaw 0.1.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 (57) hide show
  1. package/README.md +68 -0
  2. package/bin/makersclaw.mjs +10 -0
  3. package/package.json +34 -0
  4. package/registry/blocks/board/README.md +130 -0
  5. package/registry/blocks/board/block.json +10 -0
  6. package/registry/blocks/board/board.jsx +435 -0
  7. package/registry/blocks/board/demo.jsx +50 -0
  8. package/registry/blocks/board/index.js +4 -0
  9. package/registry/blocks/job-panel/README.md +68 -0
  10. package/registry/blocks/job-panel/block.json +10 -0
  11. package/registry/blocks/job-panel/demo.jsx +75 -0
  12. package/registry/blocks/job-panel/index.js +3 -0
  13. package/registry/blocks/job-panel/job-panel.jsx +110 -0
  14. package/src/cli.mjs +301 -0
  15. package/src/fsutil.mjs +76 -0
  16. package/src/install.mjs +457 -0
  17. package/src/registry.mjs +191 -0
  18. package/templates/next/files/README.md +626 -0
  19. package/templates/next/files/app/auto-refresh.jsx +26 -0
  20. package/templates/next/files/app/error-relay.jsx +53 -0
  21. package/templates/next/files/app/error.jsx +44 -0
  22. package/templates/next/files/app/global-error.jsx +36 -0
  23. package/templates/next/files/app/globals.css +178 -0
  24. package/templates/next/files/app/layout.jsx +51 -0
  25. package/templates/next/files/app/not-found.jsx +23 -0
  26. package/templates/next/files/app/page.jsx +22 -0
  27. package/templates/next/files/app/s/[key]/page.jsx +47 -0
  28. package/templates/next/files/app/shell.jsx +64 -0
  29. package/templates/next/files/check/run.mjs +683 -0
  30. package/templates/next/files/components/parts/fixture.jsx +60 -0
  31. package/templates/next/files/components/parts/index.js +5 -0
  32. package/templates/next/files/components/parts/record-switcher.jsx +49 -0
  33. package/templates/next/files/components/ui/button.jsx +81 -0
  34. package/templates/next/files/components/ui/cx.js +4 -0
  35. package/templates/next/files/components/ui/depiction.jsx +66 -0
  36. package/templates/next/files/components/ui/field.jsx +126 -0
  37. package/templates/next/files/components/ui/icon.jsx +87 -0
  38. package/templates/next/files/components/ui/index.js +12 -0
  39. package/templates/next/files/components/ui/menu.jsx +123 -0
  40. package/templates/next/files/components/ui/notice.jsx +86 -0
  41. package/templates/next/files/components/ui/sheet.jsx +89 -0
  42. package/templates/next/files/components/ui/tabs.jsx +81 -0
  43. package/templates/next/files/lib/actions.js +187 -0
  44. package/templates/next/files/lib/claw.js +219 -0
  45. package/templates/next/files/lib/data.js +171 -0
  46. package/templates/next/files/lib/db.js +72 -0
  47. package/templates/next/files/lib/hooks.js +218 -0
  48. package/templates/next/files/lib/platform.js +59 -0
  49. package/templates/next/files/lib/sample.js +12 -0
  50. package/templates/next/files/middleware.js +42 -0
  51. package/templates/next/files/next.config.mjs +41 -0
  52. package/templates/next/files/package.json +19 -0
  53. package/templates/next/files/postcss.config.mjs +3 -0
  54. package/templates/next/files/schema.sql +22 -0
  55. package/templates/next/files/surfaces/README.md +32 -0
  56. package/templates/next/files/surfaces.js +53 -0
  57. package/templates/next/template.json +118 -0
@@ -0,0 +1,626 @@
1
+ # artifact template
2
+
3
+ The app every artifact starts from: Next.js 15 (app router, JSX, server
4
+ actions), Postgres via `DATABASE_URL`, Tailwind v4, and a kit of parts
5
+ (`components/ui`, `components/parts`). It carries the behaviour every
6
+ app shares, the parts every app is assembled from, and the workspace's
7
+ colours. You design the surfaces: what is on them, how they are laid
8
+ out, how dense, how they read — never what colour anything is.
9
+
10
+ ## 1. What you are inside
11
+
12
+ The app renders in an iframe inside an OS window on the founder's desk.
13
+ The window's title bar already carries the app's name, **Edit**,
14
+ **Pause / Resume**, **Run now** and the status. Under it, the window's
15
+ tab strip carries **App**, **Agent**, **Runs**, **Activity** and
16
+ **Settings**: the OS draws the last four (the chat with the app's
17
+ agent, each run and its report, every activity line, the app's settings).
18
+ You design the **App tab**: the shell and the domain surfaces. Never
19
+ repeat the name, the run controls, or the other tabs.
20
+
21
+ **The colours are the workspace's, not yours.** The founder picks a
22
+ theme once (System Settings → Appearance) and the desk and every app in
23
+ the workspace are painted in it. `app/layout.jsx` links
24
+ `_claw/theme.css`, which sets the palette's variables; `app/globals.css`
25
+ turns them into the tokens you use: `bg-background`, `bg-card`,
26
+ `text-foreground`, `text-muted-foreground`, `text-primary`,
27
+ `bg-primary text-primary-foreground`, `border-border`, `bg-accent`,
28
+ `text-destructive`, `text-success`, `text-warning`, `text-info`,
29
+ `ring-ring`, `rounded-sm|md|lg|xl`, `font-sans|mono|display`. Never a
30
+ hex, `rgb()`, `hsl()`, a raw Tailwind hue (`text-neutral-500`,
31
+ `bg-white`) or a `<style jsx>` block: `npm run check` refuses each, and
32
+ an app that carries its own colour stops matching the workspace the day
33
+ the founder changes theme. Light or dark is a property of the preset;
34
+ the tokens flip with it, so you write no `dark:` variants either.
35
+
36
+ One exception, with rules of its own (§3c): a **depiction** — a picture
37
+ of something that exists elsewhere, a tweet as X will show it, a
38
+ LinkedIn post in the feed — wears THAT place's colours, face and widths,
39
+ in `components/depictions/`, framed on a surface by the kit's
40
+ `<Depiction>`. Colour that is a row's value (`style={{ background:
41
+ row.hex }}`) is neither chrome nor depiction: it is data, and the check
42
+ never sees it.
43
+
44
+ ## 2. Files
45
+
46
+ Paths here are relative to this folder — the artifact's `app/`.
47
+ `artifact_files` paths are relative to the artifact folder, so prefix
48
+ `app/`: this file's `app/shell.jsx` is written as `app/app/shell.jsx`,
49
+ `surfaces/<key>.jsx` as `app/surfaces/<key>.jsx`.
50
+
51
+ Yours — write and rewrite freely:
52
+
53
+ ```
54
+ app/shell.jsx the inside of the window: nav, layout, type, density
55
+ app/DESIGN.md the design, written before the code (§3)
56
+ surfaces/<key>.jsx one file per domain surface
57
+ surfaces.js the manifest entries (import + component when built)
58
+ components/*.jsx anything shared between surfaces
59
+ components/depictions/ pictures of things that exist elsewhere, in their own colours (§3c)
60
+ ```
61
+
62
+ Keep — the contract; do not edit:
63
+
64
+ ```
65
+ app/layout.jsx <html data-theme>, the _claw/theme.css link, AutoRefresh, ErrorRelay, Shell
66
+ app/globals.css the workspace theme's tokens (§1) — no palette of your own goes here
67
+ app/page.jsx / → the home surface (or "No surfaces yet" while the manifest is empty)
68
+ app/s/[key]/page.jsx renders the surface with that key; planned ones say so
69
+ app/error.jsx, app/global-error.jsx, app/not-found.jsx, app/error-relay.jsx, app/auto-refresh.jsx
70
+ components/ui/ the kit: Button, Field, Input, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction… (§3b, §3c)
71
+ components/parts/ the parts: RecordSwitcher, useFixture + FixtureNotice (§3b)
72
+ check/pins.json the fixed files by hash; `npm run check` refuses one that differs
73
+ lib/* db, data, actions, claw, platform, hooks, sample (§5)
74
+ check/ `npm run check` (§6)
75
+ middleware.js product-token check, x-claw-surface and x-claw-theme headers
76
+ next.config.mjs basePath + Server Actions allowedOrigins, both from env (§4b)
77
+ postcss.config.mjs Tailwind v4
78
+ schema.sql public.artifact_activity, for reference
79
+ ```
80
+
81
+ ## 3. Design first
82
+
83
+ Before any code, write `app/DESIGN.md` — one screen of text:
84
+
85
+ - The founder's design rules from the workspace file `/company/design.md`,
86
+ quoted. Read it before every build; if it is absent, write it from this
87
+ first build.
88
+ - Which tokens carry which job on THIS app — what is `bg-card`, where
89
+ `text-primary` appears, what `text-destructive` is reserved for. Not a
90
+ palette: the colours are the workspace's (§1). Colour is a signal, so
91
+ status tones (success, warning, destructive, info) are for status only.
92
+ - Depictions: `<thing> (modes)` or `none` — which pictures of elsewhere
93
+ the app shows (§3c), and in which of the target's modes.
94
+ - Type: scale and weight within the workspace's faces (`font-sans`,
95
+ `font-mono`, `font-display`). Numbers tabular (`data-nums` on the element).
96
+ - Layout and nav: where the surfaces live, what the home surface shows in
97
+ the first second.
98
+ - Density, empty states, error states.
99
+ - What makes this app look like ITS domain and not another.
100
+
101
+ Then `app/shell.jsx`, then the surfaces in the order the founder uses
102
+ them. Every one is a domain surface — the things the app finds, makes,
103
+ sends or receives. There is no agent, runs, activity or settings surface
104
+ to design: Agent, Runs and Activity are tabs the OS draws on the window,
105
+ and the app's settings — model, prompt, skills, connections, schedule —
106
+ are the window's Settings tab, the same for every artifact. The keys
107
+ `agent`, `runs` and `activity` are reserved; a surface may not take one.
108
+ The starter `app/shell.jsx` is a placeholder; replace it first.
109
+
110
+ What the OS does **not** own is asking the agent for something. Those
111
+ tabs are where the founder goes to look back; the app is where they set
112
+ work going and watch it happen (`useJob`, §5). A verb on an object list —
113
+ "Generate a launch plan" beside "New launch plan" — is part of the App
114
+ tab, and so is the panel it opens while the work runs.
115
+
116
+ **The rule: no two apps in a workspace should be mistaken for each other
117
+ at a glance.** The design comes from the domain, not from a default. The
118
+ colours are the same across the workspace by design; what tells the apps
119
+ apart is the shape of each surface — which is your work.
120
+
121
+ ### 3b. Parts — assemble, never hand-roll
122
+
123
+ Every control in the app is a part from `components/ui`; the pieces that
124
+ touch this template's data are in `components/parts`. Each is the
125
+ workspace's look over the browser's own behaviour (a `<dialog>` for the
126
+ sheet, the Popover API for the menu, the native `<select>`, roving tabs)
127
+ — which is why they work on first contact and why a hand-rolled one
128
+ rarely does. `npm run check` refuses a raw `<select>`, `<dialog>`,
129
+ `<svg>`, `role="tab"`, `popover` or `window.confirm()` outside the kit.
130
+
131
+ ```jsx
132
+ import { Button, IconButton, Card, Field, Input, Textarea, Select, Switch, Checkbox,
133
+ Sheet, Confirm, Tabs, TabPanel, Menu, Notice, Badge, Kicker, Empty, Icon, cx }
134
+ from "../components/ui"; // "./ui" from a component
135
+ import { RecordSwitcher, useFixture, FixtureNotice, isSample } from "../components/parts";
136
+ ```
137
+
138
+ - `Button` — `variant` primary (one per surface) · secondary · quiet ·
139
+ danger · link; `size` md (44px) · sm; `icon="plus"`; `href` for a link.
140
+ `IconButton` for an icon alone (`label` required).
141
+ - `Field` wraps `Input`, `Textarea`, `Select`, `Switch`, `Checkbox` with
142
+ its label, hint and error. `Select` with empty `options` renders a
143
+ muted line, never a disabled dropdown.
144
+ - `Sheet` — the one overlay, a rail beside the work (`side="end"`), a
145
+ dialog (`center`) or a bottom sheet; on a narrow window every side
146
+ becomes a bottom sheet. `Confirm` is the "delete this?" case.
147
+ - `Tabs` + `TabPanel` — views of ONE thing (a record's notes | history).
148
+ Never for the app's surfaces; those are the shell's nav.
149
+ - `Menu` — the "…" of a row. Empty `items` render nothing: a menu is
150
+ earned by having actions.
151
+ - `Card`, `Notice` (tone neutral · info · success · warning · danger),
152
+ `Badge`, `Kicker` (the small line above a heading), `Empty` (a filter
153
+ or search with no matches — not the landing state, that is the fixture).
154
+ - `Icon name="…"` — one sprite, `ICON_NAMES` lists it. Missing one? Add
155
+ the glyph to `components/ui/icon.jsx`; never an `<svg>` in a surface.
156
+ - `RecordSwitcher` — pick a saved record or make a new one. With nothing
157
+ saved it shows only New. A disabled dropdown reading "No saved…" is the
158
+ failure this part exists to prevent.
159
+ - `useFixture(rows, SAMPLE)` → `{ rows, isFixture, clear }` +
160
+ `FixtureNotice` — the worked example (§3: it opens populated), shown
161
+ only while the real collection is empty and **never INSERTed**. The
162
+ workspace database is shared by every app; an invented row is a real
163
+ row somewhere else. Clearing writes nothing. Editing a fixture row
164
+ copies its values into a new draft and saves THAT.
165
+
166
+ A control with nothing to act on is not drawn.
167
+
168
+ **Blocks — the bigger pieces, added when a surface needs one.** A board
169
+ you drag cards across, the panel that follows a job the agent is doing:
170
+ behaviour that goes wrong when every app draws it by hand. No app has a
171
+ block until it is added — `makersclaw search <what the surface does>`
172
+ finds one, `makersclaw add <name>` lays it in `components/blocks/<name>/`
173
+ and prints its props and an example (in Makersclaw, the `template` tool
174
+ runs both). Import it from there:
175
+
176
+ ```jsx
177
+ import { Board } from "../components/blocks/board"; // "./blocks/board" from a component
178
+ ```
179
+
180
+ A block is fixed like the kit: compose it through its props — what a card
181
+ says, what a move saves — never edit its files (`npm run check` refuses
182
+ a change; `makersclaw add <name>` restores one). Search before you build
183
+ a board, a table, a timeline, a calendar, a canvas or a job panel by
184
+ hand. When no block fits, build the part from the kit in `components/`,
185
+ with tokens only — except a depiction, §3c.
186
+
187
+ ### 3c. Depictions — the one place colour is yours to copy
188
+
189
+ A depiction is a picture of something that exists elsewhere: a tweet as
190
+ X will show it, a LinkedIn post in the feed, the row an email makes in
191
+ Gmail's inbox, a Slack message, a receipt. Its whole job is *this is how
192
+ it will look there*, so it is painted in that place's colours, set in
193
+ that place's face, at that place's width — and it does not move when the
194
+ founder changes theme. The test, per element: **will the founder send,
195
+ post, paste or print THIS and see it there?** Then it is a depiction. Do
196
+ they type into it, save from it, switch records in it, or does it change
197
+ what is stored? Then it is chrome, and it is the workspace's. X's
198
+ composer is not a depiction; the founder is the one typing. Not a
199
+ depiction either: a chart, a code block, a status, a pin on a map, the
200
+ founder's own brand on your buttons, a colour a user picks for a tag, a
201
+ Slack mark on a "Post to Slack" button. Those stay tokens. A published
202
+ thing is an embed (`<iframe>` of the real post), not a depiction.
203
+
204
+ **The file.** `components/depictions/<thing>.jsx` — one thing as it looks
205
+ in one place, one default export, `"use client"` if it holds the target's
206
+ own toggle. It opens with a header comment: the target, the date you
207
+ traced it, its column widths in px, its named modes, then `Exact:`
208
+ (colours, sizes, radii, widths, where the text folds) and `Approximate:`
209
+ (the face when it is proprietary — X's Chirp, Google Sans — and the emoji
210
+ set). A depiction without that header is a guess, not a picture.
211
+
212
+ Inside it, everything is the target's and everything is px: `text-[15px]
213
+ p-[12px] rounded-[16px]`, `style={{ color: "#536471" }}`, its own `<svg>`
214
+ glyphs (the X mark, the verified badge — the kit's stroke icons do not
215
+ look like X's). Never `text-sm`, `p-4`, `rounded-lg` or `font-sans`: rem
216
+ is the workspace's base size, `--spacing` carries the founder's density,
217
+ `rounded-lg` is the preset's radius, and no wrapper can undo them — the
218
+ check refuses each. Never a token, never `dark:`: the target's own modes
219
+ (X: default, dim, lights out) are a `mode` prop and a table in the file,
220
+ and have nothing to do with the workspace being light or dark. The root
221
+ paints the target's page colour and its outer padding; the card sits
222
+ inside. A white tweet floating on the workspace's charcoal is not how it
223
+ will look there.
224
+
225
+ It takes data in and nothing else: text, name, handle, avatar url, counts,
226
+ `mode`. It imports `react`, `../ui/cx` and its siblings — never `../ui`,
227
+ `../parts` or `lib/`. No `<form>`, no `<input>`, no `<a>` (a link there is
228
+ a `<span>` that looks like one), no handler except the target's own
229
+ toggle written in place (`onClick={() => setOpen(true)}` for LinkedIn's
230
+ "…more"), no reads, no writes. When an automation will post the thing,
231
+ the depiction renders the same text the automation sends, so preview and
232
+ post cannot drift.
233
+
234
+ **The frame.** On the surface — or in the client component that holds the
235
+ draft, where a preview can follow typing (`components/<X>.jsx`, importing
236
+ `Depiction` from `"./ui"`) — always this shape:
237
+
238
+ ```jsx
239
+ import { Depiction, Tabs } from "../components/ui";
240
+ import XPost from "../components/depictions/x-post";
241
+
242
+ <Depiction of="x-post" scheme="dark" width={600} label="On X · Dim (face approximate)">
243
+ <XPost text={draft.text} name={me.name} handle={me.handle} avatar={me.avatar} mode="dim" />
244
+ </Depiction>
245
+ ```
246
+
247
+ Exactly one depiction inside the frame, self-closing. `of` names the
248
+ file; `scheme` is the target's light or dark (never the workspace's);
249
+ `width` is the target's real column, from the header comment — the frame
250
+ scales the picture to fit a narrow column and never reflows it, because
251
+ line breaks and folds are the preview. The caption is chrome. Two modes
252
+ side by side are two frames. The composer, the record switcher, the mode
253
+ and device toggles, Save — chrome, in tokens, outside. A depiction is a
254
+ default import, framed wherever it is rendered, and is never dropped
255
+ into the shell, the kit or `lib/`: rendered outside its frame it inherits
256
+ the workspace, and the check says so.
257
+
258
+ **One door, one bundle.** The folder is reached one way: a static
259
+ `import XPost from "…/depictions/x-post"` at the top of the file that
260
+ frames it. No `import()`, no `next/dynamic`, no re-export module, no
261
+ alias (`const P = XPost`), no picture passed along as a value. Through
262
+ the frame go data props only — a string, a number, a field of a row;
263
+ never an element, an object, an array, a function or a spread, whatever
264
+ the prop is called. The check reads every `.js`/`.jsx` the app has,
265
+ dot-files included; Tailwind reads exactly those files (`globals.css`
266
+ pins its sources), so a class name in a `.json` or built from pieces
267
+ never reaches the page.
268
+
269
+ ## 4. Server and client
270
+
271
+ One split, stated once:
272
+
273
+ ```
274
+ lib/data.js, lib/db.js server components and server actions only
275
+ lib/platform.js, lib/hooks.js "use client" files only
276
+ lib/actions.js either (they are server actions)
277
+ surfaces.js server files only (entries import components)
278
+ ```
279
+
280
+ Pass plain data across the boundary — strings, numbers, arrays, objects.
281
+ `plain(row)` already turns dates into ISO strings. A client component can
282
+ receive a server action as a prop, and nothing else that is a function.
283
+
284
+ A surface file that uses hooks or event handlers starts with `"use
285
+ client"` and cannot import `lib/data.js`. Split it: a server file loads
286
+ the rows and renders a client component with props.
287
+
288
+ ```jsx
289
+ // surfaces/investors.jsx (server)
290
+ import { loadRows } from "../lib/data";
291
+ import InvestorBoard from "../components/InvestorBoard";
292
+
293
+ export default async function Investors({ surface, searchParams }) {
294
+ const { rows, error } = await loadRows({ source: "public.investors", orderBy: "updated_at desc" });
295
+ return <InvestorBoard rows={rows} error={error} highlight={searchParams.row ?? null} />;
296
+ }
297
+ ```
298
+
299
+ ```jsx
300
+ // components/InvestorBoard.jsx (client)
301
+ "use client";
302
+ import { moveRow } from "../lib/actions";
303
+ import { Card, Notice } from "./ui";
304
+ import { FixtureNotice, useFixture } from "./parts";
305
+
306
+ const SAMPLE = [{ name: "Ada Ventures", stage: "warm" }, /* … */];
307
+
308
+ export default function InvestorBoard({ rows: real, error, highlight }) {
309
+ const { rows, isFixture, clear } = useFixture(real, SAMPLE);
310
+ /* … */
311
+ }
312
+ ```
313
+
314
+ Every surface component receives `surface` (`{ key, kind, label, source
315
+ }`) and `searchParams` (the URL query; `?row=<id>` is the convention for
316
+ "highlight this row" — an activity line opens its surface with it).
317
+
318
+ ### Linking — `next/link`, never a bare `<a href="/…">`
319
+
320
+ The app is served under a prefix (`/p/<label>` in local dev, nothing in
321
+ production) and `next.config.mjs` puts it in `basePath`. **`<Link>` and
322
+ `router.push()` apply that prefix. A plain `<a href>` does not** — the
323
+ browser resolves it against the ORIGIN, so the click leaves the app
324
+ entirely and loads the Makersclaw dashboard inside the app's own frame.
325
+
326
+ ```jsx
327
+ import Link from "next/link";
328
+ <Link href={`/s/${key}`}>Goals</Link> // ✅ becomes /p/<label>/s/goals
329
+ <a href={`/s/${key}`}>Goals</a> // ❌ goes to /s/goals on the dashboard
330
+ ```
331
+
332
+ Assets hide the mistake: Next writes those itself, so `_next/static/…`
333
+ comes out correctly prefixed while the nav is broken, and the page still
334
+ answers 200. Nothing in the build catches it — only a click does. A write
335
+ under `app/` that contains a root-relative `<a>` comes back with a
336
+ `warnings` entry saying so.
337
+
338
+ Same trap, same fix: `window.location` assignments and `<form
339
+ action="/…">`. A real external URL (`https://`, `mailto:`) is fine.
340
+
341
+
342
+ ### 4b. Writes are rejected — "Invalid Server Actions request"
343
+
344
+ Already handled. Do not fix it in `next.config.mjs`.
345
+
346
+ Next 15 refuses a Server Action whose `Origin` header disagrees with
347
+ `x-forwarded-host`, and in this stack they always disagree: the sandbox
348
+ ingress rewrites `x-forwarded-host` to its own vercel.run name while the
349
+ browser is on the product host. Untreated, the app builds, loads and
350
+ renders perfectly — and its **first save fails**, which reads like a dead
351
+ button rather than a proxy policy.
352
+
353
+ `next.config.mjs` already reads `CLAW_ALLOWED_ORIGINS`, which `product_run`
354
+ derives per environment (`src/lib/products.ts`, `productAllowedOrigins`):
355
+ the product's own FQDN in production, the dashboard's dev origin under a
356
+ path prefix.
357
+
358
+ **Never hardcode an origin here.** `allowedOrigins: ["localhost:5173"]`
359
+ passes every test you can run in development and refuses every write in
360
+ production. If a write is still rejected, the value is wrong at the
361
+ source — say so and stop; it is not an app-level fix.
362
+
363
+ ## 5. API reference
364
+
365
+ ### Reads — `lib/data.js` (server)
366
+
367
+ Every read resolves `{ ..., error }` and never throws. `error` is text
368
+ when the table is missing or the database unreachable; it is also
369
+ `console.error`ed, so the log tail carries it to the platform agent.
370
+ `source` is `"table"` or `"schema.table"`; names go through `ident()`.
371
+
372
+ ```js
373
+ loadRows({ source, idColumn = "id", orderBy, limit = 500, filter }) → { rows, error }
374
+ loadRow({ source, idColumn = "id", id }) → { row, error } // row null when none
375
+ countRows({ source, filter }) → { count, error }
376
+ countBy({ source, column, filter }) → { counts: { [value]: n }, error }
377
+ loadActivity({ limit = 100, surface, since }) → { rows, error } // public.artifact_activity, this artifact
378
+ ```
379
+
380
+ `filter` is `{ column: value }`: `null` → `IS NULL`, an array → `= ANY`,
381
+ else equality. `orderBy` is `"updated_at desc, id"`; default `idColumn
382
+ desc`. `limit` caps at 5000 (activity: 1000).
383
+
384
+ ```js
385
+ const { rows, error } = await loadRows({ source: "public.drafts", filter: { stage: ["draft", "approved"] } });
386
+ const { counts } = await countBy({ source: "public.investors", column: "stage" });
387
+ const { rows: log } = await loadActivity({ surface: "drafts", limit: 50 });
388
+ ```
389
+
390
+ Helpers: `plain(row)` (dates → ISO, buffers → base64), `orderClause(orderBy,
391
+ fallback)` (validated `ORDER BY` text), `markSample(sample, idColumn)`
392
+ (stable ids + `_sample: true`), `currentSurface(explicit)` (the surface
393
+ key of the request, from `x-claw-surface`, unless one is passed).
394
+
395
+ ### Writes — `lib/actions.js` (server actions, callable from either side)
396
+
397
+ Each quotes names with `ident()`, writes one activity row through
398
+ `logActivity`, calls `revalidatePath("/", "layout")`, and resolves
399
+ `{ ok }` or `{ ok: false, error }`. Never throws. `surface` defaults to
400
+ the request's surface (middleware sets it from `/s/<key>`).
401
+
402
+ ```js
403
+ updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data })
404
+ moveRow({ source, idColumn = "id", id, stateColumn, to, surface }) // action "moved"
405
+ insertRow({ source, idColumn = "id", values, surface, action = "added", detail }) → { ok, id }
406
+ deleteRow({ source, idColumn = "id", id, surface, detail }) // action "removed"
407
+ saveSetting({ mode = "table" | "json", source, keyColumn = "key", valueColumn = "value",
408
+ column, idColumn = "id", id, key, value, surface })
409
+ ```
410
+
411
+ `saveSetting` table mode updates the `(key, value)` row and inserts one
412
+ when missing — a plain `key text, value text` table is enough. JSON mode
413
+ sets `key` inside the jsonb `column` of row `id`.
414
+
415
+ ```js
416
+ "use client";
417
+ import { moveRow } from "../lib/actions";
418
+ const r = await moveRow({ source: "public.investors", id, stateColumn: "stage", to: "sent" });
419
+ if (!r.ok) setError(r.error);
420
+ ```
421
+
422
+ Never write raw SQL from a surface. A write that needs more than these
423
+ is a new function in `lib/actions.js` that also calls `logActivity`.
424
+
425
+ ### Platform — `lib/platform.js` (browser)
426
+
427
+ Typed wrappers over `lib/claw.js`, one per `_claw` route an app has a
428
+ reason to call. Every call resolves `{ ok, status, data }` or `{ ok:
429
+ false, status, error, unavailable }`; nothing throws. `unavailable: true`
430
+ (404 or no network) means the gateway has no such route yet: show a
431
+ quiet "not connected yet", not an error.
432
+
433
+ ```js
434
+ import { platform } from "../lib/platform"; // or the named functions
435
+ platform.me() GET _claw/me → { artifact: { id, name, vehicle, status, home }, user: { id, email }, blueprint }
436
+ platform.startJob(message, { sessionId, onEvent }) POST _claw/chat → streams session · delta · tool · done · error
437
+ platform.cancelJob(sessionId) POST _claw/cancel
438
+ platform.reportError({ message, stack, url, source }) POST _claw/events
439
+ ```
440
+
441
+ Named exports: `me, startJob, cancelJob, reportError, platform`. That is
442
+ the whole list. Note what is **not** there: the runs, the chat history,
443
+ Run now, Pause / Resume. Those are MONITORING and they are the OS's — the
444
+ window's tabs and title bar — so an app never calls `_claw/agent`,
445
+ `_claw/runs`, `_claw/run`, `_claw/pause` or `_claw/resume`. Starting work
446
+ is the opposite thing, and it belongs here (see `useJob`). The artifact's
447
+ status lives on `<html data-claw-status>` (`publishStatus` in
448
+ `lib/claw.js`); `useArtifact()` keeps it current.
449
+
450
+ ### Hooks — `lib/hooks.js` ("use client")
451
+
452
+ Three hooks: a generic fetch over one `_claw` route, the app's identity,
453
+ and a job. Each owns fetching, polling (while the tab is visible; once a
454
+ minute while `unavailable`), streaming and cleanup on unmount. A surface
455
+ renders what comes back and owns every pixel.
456
+
457
+ ```js
458
+ useClaw(path, { every, enabled = true }) → { data, error, loading, unavailable, reload }
459
+ useArtifact() → { artifact, user, blueprint, loading, error, unavailable, reload }
460
+ useJob() → { running, steps, text, progress, error, sessionId, finishedAt, start, cancel, reset }
461
+ ```
462
+
463
+ There is no chat, runs or activity hook: the OS draws those tabs.
464
+ Activity has no hook either — it is a Postgres table, read server-side
465
+ with `loadActivity` by any domain surface that wants a slice of it (a
466
+ row's history beside the row, say).
467
+
468
+ ### Commissioning work — `useJob()`
469
+
470
+ The founder asks the app's agent to make something, from inside the app,
471
+ and watches it happen. This is SOP §2.3 and the reason it exists is
472
+ blunt: with invocation only on the window's Agent tab, a founder starts
473
+ work and then stares at an app where nothing changes.
474
+
475
+ ```jsx
476
+ const job = useJob();
477
+ const products = useProducts(); // your own data hook
478
+
479
+ useEffect(() => { products.reload(); }, [job.progress]); // rows as they land
480
+
481
+ <button onClick={() => job.start("Draft a launch checklist for Rowhouse")}>
482
+ Generate a launch plan
483
+ </button>
484
+
485
+ {job.running || job.finishedAt ? (
486
+ <aside> {/* your design, not ours */}
487
+ {job.steps.map((s, i) => <li key={i} data-status={s.status}>{s.label}</li>)}
488
+ <p>{job.text}</p>
489
+ {job.running ? <button onClick={job.cancel}>Stop</button>
490
+ : <button onClick={job.reset}>Close</button>}
491
+ </aside>
492
+ ) : null}
493
+ ```
494
+
495
+ The panel is a block — `makersclaw add job-panel` — that keeps the four
496
+ rules below for you: `<JobPanel job={job} title="Drafting the launch
497
+ plan" doneTitle="Launch plan drafted" />`. Draw your own only when it
498
+ cannot say what the founder needs to see.
499
+
500
+ Four rules, and a build is wrong without them:
501
+
502
+ 1. **The verb sits on the object list, beside the manual one.** "Generate
503
+ a launch plan" next to "New launch plan", on the Launches surface.
504
+ Never a nav item named after the agent, and never the only way to
505
+ create that object — the manual form has to exist too.
506
+ 2. **The panel does not exist when no job is running.** Mount it from
507
+ `job.running || job.finishedAt`; `job.reset()` closes it. A panel
508
+ that is always there is the monitoring console (§2.7).
509
+ 3. **`job.text` is not the deliverable.** It is the agent's account of
510
+ what it did. The deliverable is ROWS — that is why `job.progress`
511
+ reloads your data hook, and why the panel closing leaves a fuller
512
+ list behind it.
513
+ 4. **Show `step.label`, never `step.name`.** `label` is the founder's
514
+ phrase ("Searching the web"); `name` is `web_search`.
515
+
516
+ `job.sessionId` is the thread. Keep it on the row the job produced and
517
+ pass it back as `job.start(msg, { sessionId })` to refine that one thing
518
+ where it lives — one conversation per object, never a global chat.
519
+
520
+ ### Activity — the contract
521
+
522
+ `public.artifact_activity` is how the founder sees the work. The OS's
523
+ **Activity tab** on the app window reads it, newest first — and so can
524
+ any domain surface that wants to (`loadActivity`). Every
525
+ `lib/actions.js` write inserts a row through `logActivity()` in
526
+ `lib/db.js`; the artifact's agent writes the same table with the `sql`
527
+ verb, `by = 'agent'` or `'run:<id>'`.
528
+
529
+ ```js
530
+ await logActivity({
531
+ action: "drafted", // found, drafted, sent, replied, skipped, edited, moved, added, removed…
532
+ surface: "drafts", // surface key → /s/drafts?row=<row_ref>
533
+ row_ref: String(row.id), // the id in that surface's source table
534
+ detail: "Drafted intro to Weekend Fund",
535
+ data: { to: "ryan@weekend.fund" },
536
+ by: "user:founder@x.com", // 'agent' | 'user:<email>' | 'run:<id>'
537
+ });
538
+ ```
539
+
540
+ Columns: `id, artifact_id, at, action, surface, row_ref, detail, data
541
+ (jsonb), by`. `loadActivity` returns them without `artifact_id` — every
542
+ row is this artifact's. The OS Activity tab shows the surface's label
543
+ on each line and does not link into the app yet; the convention for
544
+ anything that does (a domain surface's own history slice today, the tab
545
+ later) is `surface` + `row_ref` → `/s/<surface>?row=<row_ref>`, so stamp
546
+ both on every line and make the surface honour `?row=`.
547
+
548
+ ### Errors
549
+
550
+ `app/error.jsx` catches a surface that throws while rendering, shows the
551
+ message inside the shell, and posts it to `_claw/events`.
552
+ `app/global-error.jsx` does the same for the layout. `app/error-relay.jsx`
553
+ posts `window.onerror` and `unhandledrejection`. All three go through
554
+ `reportError({ message, stack, url, source })` in `lib/claw.js`, deduped
555
+ and capped per page load; `source` is `"preview"` while the artifact is a
556
+ draft and `"runtime"` once launched (`app/error-relay.jsx` keeps the
557
+ status on `<html data-claw-status>` from `_claw/me`; nothing in your
558
+ files has to). The platform agent reads them as console events. Call
559
+ `reportError` yourself only for a failure the boundaries cannot see.
560
+
561
+ ## 6. Acceptance
562
+
563
+ `npm run check` — before `surface_built`, and again before launch. One
564
+ line per rule, under twenty lines, exit 1 on a failure that names the
565
+ file and line and what to use instead. It builds for itself, into
566
+ `.next-check`, and only when a source changed since the last check —
567
+ never run `npm run build` first, and never build into `.next`: the dev
568
+ server product_run started owns that folder as another user, and a
569
+ second build there dies with `EACCES: .next/trace`.
570
+
571
+ ```
572
+ check: 5 files · 2 surfaces
573
+ ok colours no colour outside the workspace theme (1 depiction excepted)
574
+ ok controls every control is a kit part
575
+ ok examples fixtures stay in code (useFixture), nothing INSERTs one
576
+ ok manifest 2 surfaces: drafts, sent
577
+ ok icons every icon name exists
578
+ ok kit 33 fixed files match the template
579
+ ok depictions 1 picture of elsewhere, framed on 1 surface: x-post
580
+ ok smoke built in 41s · / → 307 · /s/drafts 200 · /s/sent 200 (0.6s)
581
+ all clear
582
+ ```
583
+
584
+ `kit` refuses a fixed file that differs from the template (they are
585
+ hashed in `check/pins.json`) and any file of yours under `components/ui`
586
+ or `components/parts`. `depictions` reads both directions: nothing of
587
+ the workspace inside a depiction (a token, `font-sans`, `dark:`, a rem
588
+ utility), nothing of the depiction outside its frame (a named import, a
589
+ picture in the shell or a `Card`, a dead file nobody renders).
590
+
591
+ The smoke test starts that build on a loopback port and loads every
592
+ surface; `--static` skips the build and the smoke. Then, by eye:
593
+
594
+ - Renders at 360px, 768px and 1280px without horizontal scroll.
595
+ - The fixture shows while the table is empty; a saved row replaces it.
596
+ - Error state shows the text from `{ error }`.
597
+ - Keyboard focus visible on every control.
598
+ - Numbers tabular (`data-nums`).
599
+ - Every write goes through `lib/actions.js`; no raw SQL from a surface.
600
+ - Every surface reachable from the nav.
601
+
602
+ ## 7. Run
603
+
604
+ 1. `makersclaw init` laid this into the artifact's `app/` (in Makersclaw,
605
+ `artifact_scaffold {dir}` runs it and makes `public.artifact_activity`
606
+ in the workspace database). `makersclaw.json` records the version and
607
+ the blocks; `makersclaw update` moves the fixed files and blocks to a
608
+ newer version and never touches your own files.
609
+ 2. `app_database {name, dir}` → `DATABASE_URL` in `.env`. The app's role
610
+ reads and writes every table in `public`.
611
+ 3. Make the domain tables with the `sql` verb (they land in `public`).
612
+ 4. `product_run` with this directory. While building, run it as a dev
613
+ server — `command "npx next dev -H 127.0.0.1"`, `build ""` — so each
614
+ edit shows in the founder's live preview without a rebuild.
615
+ `product_run` sets `CLAW_BASE_PATH`, `CLAW_PRODUCT`,
616
+ `CLAW_PRODUCT_TOKEN` and `CLAW_ARTIFACT_ID`.
617
+
618
+ Every `artifact_files` write under `app/` comes back with the console's
619
+ state; a 5xx carries the log tail. Fix it before moving on — the founder
620
+ is watching the preview.
621
+
622
+ Every page is `dynamic = "force-dynamic"`: nothing is prerendered, so a
623
+ build never needs the database. `app/auto-refresh.jsx` re-renders from
624
+ the server every 5 s while the tab is visible, so the agent's writes
625
+ appear. The founder reads "artifact", "app", "agent", "automation" —
626
+ nothing else.
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+
6
+ // Rows change under this page from outside it — the agent writes them
7
+ // while the founder watches. Re-render from the server every 5 s while the
8
+ // tab is visible; router.refresh() keeps form state and scroll, a reload
9
+ // would not. Nothing runs while the tab is hidden.
10
+ const EVERY_MS = 5000;
11
+
12
+ export default function AutoRefresh() {
13
+ const router = useRouter();
14
+ useEffect(() => {
15
+ const tick = () => {
16
+ if (document.visibilityState === "visible") router.refresh();
17
+ };
18
+ const id = setInterval(tick, EVERY_MS);
19
+ document.addEventListener("visibilitychange", tick);
20
+ return () => {
21
+ clearInterval(id);
22
+ document.removeEventListener("visibilitychange", tick);
23
+ };
24
+ }, [router]);
25
+ return null;
26
+ }