@clearlysh/beehaven 0.8.23 → 0.8.24

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.
@@ -53,9 +53,9 @@ export const PUBLIC_BUILD = true;
53
53
  * ⚠ REWRITTEN IN `dist/` AT BUILD TIME, like `PUBLIC_BUILD`. `unstamped` is what a source checkout
54
54
  * carries and what `tsx`/`vitest` see, and it is honest: nothing has stamped them.
55
55
  */
56
- export const BUILD_SHA = '8585349e92e9-dirty';
56
+ export const BUILD_SHA = '150d0d2c336a';
57
57
  /** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
58
- export const BUILD_TIME = '2026-10-02T00:47Z';
58
+ export const BUILD_TIME = '2026-10-02T08:34Z';
59
59
  /**
60
60
  * The build identity as one short string, for `--version`.
61
61
  *
package/dist/cli.js CHANGED
@@ -1302,7 +1302,25 @@ async function run() {
1302
1302
  await shorthandCmd(rawVerb, process.argv.slice(3), {});
1303
1303
  return;
1304
1304
  }
1305
+ /**
1306
+ * ⚠ `--x -3000` IS A NEGATIVE COORDINATE, NOT A FLAG. node's parseArgs reads any value starting
1307
+ * with a dash as a missing argument and dies with "Option '--x' argument is ambiguous" — which is
1308
+ * how `beehaven canvas add-image … --x -3000` failed (CDS-25). Join a negative NUMBER onto the
1309
+ * coordinate/size flag before it; nothing else is rewritten.
1310
+ */
1311
+ const NUMERIC_FLAGS = new Set(['--x', '--y', '--width', '--height', '--w', '--h', '--dx', '--dy']);
1312
+ const argv = [];
1313
+ for (let i = 2; i < process.argv.length; i++) {
1314
+ const a = process.argv[i], b = process.argv[i + 1];
1315
+ if (NUMERIC_FLAGS.has(a) && typeof b === 'string' && /^-\d+(\.\d+)?$/.test(b)) {
1316
+ argv.push(`${a}=${b}`);
1317
+ i++;
1318
+ continue;
1319
+ }
1320
+ argv.push(a);
1321
+ }
1305
1322
  const { values, positionals } = parseArgs({
1323
+ args: argv,
1306
1324
  allowPositionals: true,
1307
1325
  options: {
1308
1326
  project: { type: 'string' },
@@ -2949,11 +2967,24 @@ async function run() {
2949
2967
  // omitted BEEHAVEN_HEADLESS=1, so the "restarted" daemon hit the
2950
2968
  // parent-death watchdog and exited ~3s later — restart reported a PID and
2951
2969
  // left you with nothing running. See spawnStandaloneDaemon's comment.
2952
- const { spawnStandaloneDaemon } = await import('./commands/daemon-cmd.js');
2970
+ const { spawnStandaloneDaemon, waitForServingDaemon } = await import('./commands/daemon-cmd.js');
2953
2971
  const { resolveRelayUrlForDaemon } = await import('./daemon.js');
2954
2972
  const relayUrl = resolveRelayUrlForDaemon();
2955
2973
  const pid = spawnStandaloneDaemon(values);
2956
- console.log(` Daemon restarted (PID ${pid}).`);
2974
+ // ⚠ Report the daemon that is ACTUALLY serving — see waitForServingDaemon.
2975
+ const served = await waitForServingDaemon(pid);
2976
+ if (served.ready && served.spawnedAlive) {
2977
+ console.log(` Daemon restarted (PID ${pid}). Ready — socket answering.`);
2978
+ }
2979
+ else if (served.ready) {
2980
+ console.log(` Another supervisor (the Clearly app) restarted the daemon first — PID ${served.livePids.join(', ')} is serving. Ready.`);
2981
+ }
2982
+ else {
2983
+ console.error(served.livePids.length
2984
+ ? ` Daemon process is up (PID ${served.livePids.join(', ')}) but its socket is not answering yet. 'beehaven status' shows state.`
2985
+ : ` The daemon did not stay up (spawned PID ${pid} exited). Run 'beehaven start --headless', or reopen the Clearly app.`);
2986
+ process.exitCode = 1;
2987
+ }
2957
2988
  console.log(` Relay: ${color.dim(relayUrl)}`);
2958
2989
  break;
2959
2990
  }
@@ -25,6 +25,50 @@ function coordinateFlag(raw, label) {
25
25
  throw new Error(`${label} must be a number`);
26
26
  return value;
27
27
  }
28
+ /**
29
+ * ⚠ NATIVE SIZE IS READ HERE, FROM THE FILE'S OWN HEADER. The docs promise "omit both size flags to
30
+ * use the image's native dimensions", but nothing measured the image, so the server's placeholder
31
+ * box won: a 1032×320 PNG landed as a 400×400 node with its aspect squashed (CDS-26). PNG, GIF,
32
+ * JPEG and WebP carry their size in the first bytes; AVIF (ISO-BMFF) falls back to the server.
33
+ */
34
+ export function imageSize(buf, mime) {
35
+ try {
36
+ if (mime === 'image/png' && buf.length >= 24 && buf.toString('ascii', 12, 16) === 'IHDR') {
37
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
38
+ }
39
+ if (mime === 'image/gif' && buf.length >= 10)
40
+ return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
41
+ if (mime === 'image/webp' && buf.length >= 30 && buf.toString('ascii', 8, 12) === 'WEBP') {
42
+ const kind = buf.toString('ascii', 12, 16);
43
+ if (kind === 'VP8X')
44
+ return { width: 1 + buf.readUIntLE(24, 3), height: 1 + buf.readUIntLE(27, 3) };
45
+ if (kind === 'VP8L') {
46
+ const b = buf.readUInt32LE(21);
47
+ return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };
48
+ }
49
+ if (kind === 'VP8 ')
50
+ return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
51
+ }
52
+ if (mime === 'image/jpeg' && buf[0] === 0xff && buf[1] === 0xd8) {
53
+ let i = 2;
54
+ while (i + 9 < buf.length) {
55
+ if (buf[i] !== 0xff) {
56
+ i++;
57
+ continue;
58
+ }
59
+ const marker = buf[i + 1];
60
+ const len = buf.readUInt16BE(i + 2);
61
+ // SOF0–SOF15 except DHT(C4), JPG(C8), DAC(CC) carry the frame size.
62
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
63
+ return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
64
+ }
65
+ i += 2 + len;
66
+ }
67
+ }
68
+ }
69
+ catch { /* fall back to the server's default */ }
70
+ return null;
71
+ }
28
72
  function absoluteRelayUrl(url) {
29
73
  if (!url.startsWith('/'))
30
74
  return url;
@@ -54,12 +98,19 @@ export async function cmdCanvasAddImage(positionals, values) {
54
98
  throw new Error(`Image must be 1 byte to 100 MB (${stat.size} bytes given)`);
55
99
  const x = coordinateFlag(values.x, '--x');
56
100
  const y = coordinateFlag(values.y, '--y');
57
- const width = positiveFlag(values.width, '--width');
58
- const height = positiveFlag(values.height, '--height');
101
+ let width = positiveFlag(values.width, '--width');
102
+ let height = positiveFlag(values.height, '--height');
59
103
  if ((x === undefined) !== (y === undefined))
60
104
  throw new Error('Pass --x and --y together');
61
105
  if ((width === undefined) !== (height === undefined))
62
106
  throw new Error('Pass --width and --height together');
107
+ if (width === undefined) {
108
+ const native = imageSize(readFileSync(path), mimeType);
109
+ if (native && native.width > 0 && native.height > 0) {
110
+ width = native.width;
111
+ height = native.height;
112
+ }
113
+ }
63
114
  // Refuse an unknown target before putting bytes in storage for a canvas that cannot use them.
64
115
  const target = await rpcOnce('composition-detail', { id: compositionId }, 30_000);
65
116
  if (target?.error || target?.composition?.id !== compositionId) {
@@ -93,7 +144,10 @@ export async function cmdCanvasAddImage(positionals, values) {
93
144
  if (placed?.ok !== true || !placed?.nodeId && !placed?.blockId) {
94
145
  throw new Error(`Image uploaded but could not be placed: ${placed?.error ?? 'unknown error'}. Asset key: ${key}`);
95
146
  }
96
- const result = { ok: true, compositionId, nodeId: placed.nodeId ?? placed.blockId, imageFileId: recorded?.id, name };
147
+ // ⚠ `src` IS RETURNED — reusing an upload (a brand logo, a second placement) needed a scene read
148
+ // to find the URL the node stores (CDS-27).
149
+ const src = String(placed?.src ?? placed?.url ?? placed?.node?.src ?? absoluteRelayUrl(String(intent.publicUrl)));
150
+ const result = { ok: true, compositionId, nodeId: placed.nodeId ?? placed.blockId, imageFileId: recorded?.id, name, src, ...(width !== undefined ? { width, height } : {}) };
97
151
  if (values.json)
98
152
  console.log(JSON.stringify(result, null, 2));
99
153
  else
@@ -138,6 +138,38 @@ export async function startHeadlessDaemon(values) {
138
138
  ? ` Ready — socket answering.`
139
139
  : ` Started, but not answering yet after ${Math.round(DAEMON_READY_TIMEOUT_MS / 1000)}s. 'beehaven status' shows state.`);
140
140
  }
141
+ /**
142
+ * ⚠⚠ WHO IS ACTUALLY SERVING AFTER A RESPAWN. `restart` used to print the PID it spawned and stop
143
+ * there. On a Mac with the Clearly app open, the tray supervisor respawns ITS daemon the moment the
144
+ * old one dies — so the CLI's spawn lost the single-instance race ("Refusing to start — daemon
145
+ * already running"), exited, and the user was told "Daemon restarted (PID 5706)" about a process
146
+ * that no longer existed (observed 2026-10-01; the next call said "Cannot connect to daemon").
147
+ * This waits for a usable socket and reports the PID that is really live.
148
+ */
149
+ export async function waitForServingDaemon(spawnedPid) {
150
+ const { isDaemonRunning } = await import('../daemon-client.js');
151
+ const alive = (pid) => { if (!pid)
152
+ return false; try {
153
+ process.kill(pid, 0);
154
+ return true;
155
+ }
156
+ catch {
157
+ return false;
158
+ } };
159
+ const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
160
+ let ready = false;
161
+ while (Date.now() < deadline) {
162
+ await new Promise((r) => setTimeout(r, DAEMON_POLL_MS));
163
+ if (await isDaemonRunning()) {
164
+ ready = true;
165
+ break;
166
+ }
167
+ // The spawn died and nobody else took over: no point waiting out the clock.
168
+ if (!alive(spawnedPid) && findLiveDaemons().length === 0)
169
+ break;
170
+ }
171
+ return { ready, spawnedAlive: alive(spawnedPid), livePids: findLiveDaemons() };
172
+ }
141
173
  export async function cmdDaemon(sub, values) {
142
174
  switch (sub) {
143
175
  case 'start':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clearlysh/beehaven",
3
- "version": "0.8.23",
3
+ "version": "0.8.24",
4
4
  "description": "Beehaven CLI — command your Clearly workspace, agents, and canvas from the terminal",
5
5
  "private": false,
6
6
  "type": "module",
@@ -35,6 +35,7 @@ const SOURCES = {
35
35
  'agent-doodle-space':'apps/mcp-server/plugin/skills/agent-doodle-space/SKILL.md',
36
36
  'design-craft': 'apps/mcp-server/plugin/skills/design-craft/SKILL.md',
37
37
  'layout-systems': 'apps/mcp-server/plugin/skills/layout-systems/SKILL.md',
38
+ 'brand-identity': 'apps/mcp-server/plugin/skills/brand-identity/SKILL.md',
38
39
  'pair-on-canvas': 'apps/mcp-server/plugin/skills/pair-on-canvas/SKILL.md',
39
40
  'codebase-map': 'apps/mcp-server/plugin/skills/codebase-map/SKILL.md',
40
41
  'clearly-workspace': 'apps/mcp-server/plugin/skills/clearly-workspace/SKILL.md',
package/skills/README.md CHANGED
@@ -1,24 +1,26 @@
1
- # CLI-paired Claude Code skills
1
+ # Skills shipped with the CLI
2
2
 
3
- Claude Code skills that document built-in `beehaven` CLI verbs.
4
- Distinct from hive-paired skills (which live with each hive in
5
- `clearly-hives/<hive>/skills/`).
3
+ The `beehaven` tarball carries the agent skills a CLI-only user needs — the same set the Clearly
4
+ Mac app installs — so `curl | sh` teaches an agent Clearly without the app.
6
5
 
7
- A skill belongs here when it documents functionality that ships
8
- with the CLI itself, not with any specific hive. Currently:
6
+ ## Where each skill comes from
9
7
 
10
- - **`claude-shopper/`** — documents `beehaven shop find/at/search/get/checkout`
11
- (the shop@0 protocol surface). Not paired with any hive — the shop
12
- catalog is part of the CLI's Yellow Pages integration.
8
+ | Skill | Source of truth (edit HERE, never the copy) |
9
+ |---|---|
10
+ | `beehaven-cli` | `clearly-hives/beehaven-cli/skills/SKILL.md` |
11
+ | `clearly-agent`, `clearly-canvas`, `clearly-workspace`, `design-craft`, `layout-systems`, `brand-identity`, `pair-on-canvas`, `codebase-map`, `agent-home`, `agent-doodle-space` | `apps/mcp-server/plugin/skills/<name>/` in the monorepo |
12
+ | `claude-shopper` | this folder — hand-written, documents `beehaven shop` (the shop@0 protocol) |
13
13
 
14
- These aren't installed by `beehaven install` (that's for hive-paired
15
- skills via the `agentDocs.claudeCode` manifest field). If a user wants
16
- the claude-shopper skill, they install it manually:
14
+ `scripts/sync-skills.mjs` copies every monorepo source into `skills/<name>/` (`npm run sync:skills`,
15
+ and automatically at `prepack`) and writes `skills/index.json`. Its `SOURCES` table mirrors the
16
+ Mac app's `electron-builder*.yml` extraResources and `SHIPPED_SKILLS`
17
+ (`apps/electron/electron/agent-skills-installer.ts`); `apps/electron/scripts/verify-mcp-parity.mjs`
18
+ fails when the two sets disagree.
17
19
 
18
- ```bash
19
- cp -R packages/cli/skills/claude-shopper ~/.claude/skills/beehaven-shopper
20
- ```
20
+ ## Rules
21
21
 
22
- Or eventually via a `beehaven skills install-bundled <name>` verb (not
23
- yet built). See `beehaven_sdk/docs/LOCAL_HIVES.md` §11.5 for the
24
- hive-paired skill flow this complements.
22
+ - Edit the source, then run `npm run sync:skills`. A change made to a copy here is overwritten.
23
+ - Copies are generated, so they are listed in `.gitignore` (a second committed copy is a second copy
24
+ to drift). ⚠ `agent-home`, `agent-doodle-space`, `clearly-agent` and `clearly-workspace` are still
25
+ tracked from before that rule; treat them as generated all the same.
26
+ - Only `README.md` and `claude-shopper/` are hand-written here.
@@ -0,0 +1,248 @@
1
+ ---
2
+ name: brand-identity
3
+ description: >-
4
+ Build a complete brand identity on a Clearly canvas the way a studio does —
5
+ brief, mark, wordmark, lockups and clear space, palette, type system,
6
+ applications, and a spec board someone else can build from. Use when asked
7
+ for a brand, a logo, an identity, a rebrand, a visual system, brand
8
+ guidelines, or "design a company/product/app called X". Load design-craft
9
+ alongside it for the underlying grid, type and colour craft. Triggers:
10
+ "design a brand", "make me a logo", "brand identity", "visual identity",
11
+ "rebrand", "brand guidelines", "logo and colours", "identity system".
12
+ ---
13
+
14
+ # brand-identity — a system, not a picture of a logo
15
+
16
+ ⚠⚠ **THE DELIVERABLE IS A SYSTEM SOMEONE ELSE CAN BUILD WITH.** A single mark on a white
17
+ artboard is not an identity; it is a drawing. Pentagram's identity work ships a symbol, a
18
+ wordmark, a graphic system, a palette, a type system and the guidelines that let an in-house
19
+ team keep it consistent. That is the shape to copy.
20
+
21
+ Read **`design-craft`** first or alongside — grid, type scale, colour roles and the studio
22
+ pass are assumed here.
23
+
24
+ ---
25
+
26
+ ## 1. The brief — five lines, before anything is drawn
27
+
28
+ You may ask the user, or state your reading and proceed. You may not skip it.
29
+
30
+ ```
31
+ WHO the organisation, in one clause
32
+ FOR the audience, specifically — not "everyone"
33
+ UNLIKE the nearest competitor, and what must NOT be borrowed from them
34
+ FEELS three adjectives, and one the brand is deliberately NOT
35
+ LIVES where it actually appears: app icon? shopfront? 30 pages of PDF? a t-shirt?
36
+ ```
37
+
38
+ ⚠⚠ **`LIVES` DECIDES THE MARK MORE THAN `FEELS` DOES.** A brand that lives at 24px in a
39
+ browser tab cannot have a mark with four internal details. A brand that lives on a building
40
+ can. Ask where it is smallest and design for that first.
41
+
42
+ ⚠ **`UNLIKE` is what stops generic output.** Without it, every fintech gets a blue geometric
43
+ sans and every studio gets a black wordmark in Helvetica. Name the cliché and refuse it out loud.
44
+
45
+ ---
46
+
47
+ ## 2. The mark — construct it, never freehand it
48
+
49
+ ⚠⚠ **EVERY CURVE AND ANGLE SHOULD BE DERIVABLE.** A mark built from a stated construction —
50
+ a circle of radius R, a 30° angle, a stroke of R/6 — survives scaling, redrawing and
51
+ handover. A mark of hand-placed béziers does not, and it shows.
52
+
53
+ **The construction methods that actually work on a canvas:**
54
+
55
+ | method | how | good for |
56
+ |---|---|---|
57
+ | **Geometric** | circles/squares/triangles on a shared radius; every measure a fraction of R | tech, finance, infrastructure |
58
+ | **Derived letterform** | take the initial, cut or extend one stroke | anything with a strong name |
59
+ | **Monoline** | one stroke weight everywhere, round caps and joins | modern, friendly, screen-first |
60
+ | **Counter-form** | the shape is the NEGATIVE space between forms | clever marks that survive one colour |
61
+ | **Modular** | one unit repeated/rotated on a grid | systems, networks, collectives |
62
+
63
+ **Build it as an `svg` node with a stated viewBox and derived numbers:**
64
+
65
+ ```jsonc
66
+ {"action":"canvas.create-node","args":{"name":"mark","type":"svg","x":120,"y":120,"w":240,"h":240,
67
+ "svg":"<svg viewBox='0 0 240 240' xmlns='http://www.w3.org/2000/svg'>…</svg>"}}
68
+ ```
69
+
70
+ ⚠ **`<text>` inside an `svg` node does NOT render headlessly** — no system fonts are loaded.
71
+ Wordmarks are real text nodes, or outlined paths. Never live `<text>` in the mark.
72
+
73
+ **Test the mark against these before going further. A mark that fails any of them is not done:**
74
+
75
+ ```
76
+ □ 24px Still readable as a distinct shape? (Render it at 24 and LOOK.)
77
+ □ 1 colour Works in solid black? Solid white on dark?
78
+ □ Knockout Works as a hole in a shape — no fill, no gradient carrying it?
79
+ □ Square Sits comfortably in an app-icon square with its own padding?
80
+ □ Rotated Is it accidentally something else at 90°/180°?
81
+ □ Unlike Put it next to the competitor named in the brief. Distinct?
82
+ ```
83
+
84
+ ⚠⚠ **DESIGN IT IN ONE COLOUR FIRST.** A gradient can make a weak shape look temporarily
85
+ good. If it does not work in flat black, the shape is wrong and the gradient is hiding it.
86
+
87
+ ---
88
+
89
+ ## 3. Wordmark and lockups
90
+
91
+ - **Set the wordmark in real type, then adjust** — do not draw letters from scratch unless
92
+ the brief asks for a custom face.
93
+ - ⚠ **A wordmark is always tracked tighter than body text.** At display size, `letterSpacing`
94
+ −1 to −4. Then fix the specific pairs the font gets wrong (an uppercase A next to a V, a
95
+ T next to a lowercase o).
96
+ - **Lockups are a fixed set, not an invitation.** Ship exactly these and say so:
97
+
98
+ ```
99
+ primary mark + wordmark, horizontal ← the default
100
+ stacked mark above wordmark ← square-ish slots
101
+ mark-only the symbol alone ← ≥ 24px, when context supplies the name
102
+ reversed the primary on dark
103
+ mono one colour, no exceptions ← print, engraving, embroidery
104
+ ```
105
+
106
+ - **Clear space** = a fraction of the mark, never an absolute px. **¼ of the mark's height**
107
+ on all four sides is a defensible default. Express it in terms of the mark so it scales.
108
+ - **Minimum size** — state one, per lockup, and test it: mark-only 24px digital / 10mm print;
109
+ the full lockup needs roughly 2–3× that before the wordmark closes up.
110
+
111
+ ---
112
+
113
+ ## 4. Palette
114
+
115
+ Follow `design-craft`'s role list — ground, ink, ink-muted, accent, 2–3 support. For an
116
+ identity, add:
117
+
118
+ - ⚠⚠ **ONE colour is the brand.** People remember a single colour. A five-colour identity is
119
+ remembered as no colour. Pick the one, then support it.
120
+ - **State a dark-ground variant of every colour.** The same accent needs ~10% more lightness
121
+ on near-black to read the same. An identity without this ships broken dark mode.
122
+ - **Give each colour a role sentence**, not just a hex: *"Accent. The one thing that shouts.
123
+ CTAs, the mark, one word in a headline. Never a background."* A palette without roles gets
124
+ used at random.
125
+ - **Print values matter if `LIVES` includes print** — state CMYK or Pantone intent, and know
126
+ that a bright screen accent may be unreachable in CMYK. Say so rather than shipping a
127
+ surprise.
128
+
129
+ ---
130
+
131
+ ## 5. Type system
132
+
133
+ ```
134
+ DISPLAY headlines, the mark's neighbour one face, one or two weights
135
+ TEXT body, UI, long form must have real weights + italics
136
+ MONO code, data, captions only if the brand needs it
137
+ ```
138
+
139
+ - **One face in two weights beats two faces.** If you use two, make them different in CLASS.
140
+ - ⚠ **Check the face has what the brand needs**: does it have a decent 500? Real italics or
141
+ faked obliques? The numerals you want (lining vs oldstyle, tabular for data)? A missing
142
+ weight is discovered three months in, on a deadline.
143
+ - **Publish the scale**, not just the names — the ratio, the base, and the resulting sizes
144
+ (see `design-craft` §2). A type "system" without sizes is a font recommendation.
145
+
146
+ ---
147
+
148
+ ## 6. Applications — this is what makes it believable
149
+
150
+ For an imagery moodboard, create a small series of purpose-made images with a shared
151
+ material, lighting and colour logic, then place each as its own editable image node.
152
+ Keep the official Clearly rabbit wordmark as a separate canonical asset; do not ask an
153
+ image model to redraw it. The CLI path for local raster files is
154
+ `beehaven canvas add-image <file> --composition <id>`; see
155
+ [`clearly-canvas/authoring.md`](../clearly-canvas/authoring.md#local-images-from-the-cli)
156
+ for the upload, nesting and MCP contracts. Inspect a rendered artboard after placing
157
+ the images: a stored `src` alone does not prove the intended image is visible.
158
+
159
+ ⚠⚠ **A mark on a white square convinces nobody. The SAME mark on six real surfaces does.**
160
+ This is the single highest-value part of the deliverable and the part agents most often skip.
161
+
162
+ Pick 4–6 that match the brief's `LIVES`:
163
+
164
+ ```
165
+ app icon 512×512 business card 1050×600 social avatar + header
166
+ website hero poster / out-of-home packaging face
167
+ email signature t-shirt / tote signage / door vinyl
168
+ presentation title invoice / letterhead UI screen with the system applied
169
+ ```
170
+
171
+ Build each as a real frame at real proportions on the canvas, in a row, with a caption under
172
+ each. **Do not fake them as flat rectangles labelled "app icon"** — draw the rounded square,
173
+ put the mark in it at the right optical size, show it at 512 and again at 48.
174
+
175
+ ---
176
+
177
+ ## 7. The spec board — hand-off, on the canvas
178
+
179
+ A titled section, below the applications, that another designer could build from:
180
+
181
+ ```
182
+ 01 MARK construction diagram — the circles/angles/ratios, drawn, with the numbers
183
+ 02 CLEAR SPACE the ¼-height rule, drawn as a dashed frame with the measure labelled
184
+ 03 LOCKUPS all five, at the same optical size, labelled
185
+ 04 MISUSE 4–6 tiles: stretched, recoloured, rotated, on a busy photo, effects,
186
+ too small — each with a ✗. This section prevents more damage than any other.
187
+ 05 PALETTE swatches + hex + ROLE SENTENCE + dark-ground variant
188
+ 06 TYPE the faces, the scale with real numbers, a specimen paragraph
189
+ 07 GRID margins, columns, gutter, baseline — the numbers from design-craft §1
190
+ 08 VOICE 3 lines: how it speaks, one thing it never says
191
+ ```
192
+
193
+ ⚠ **Section 04 (misuse) is not filler.** Guidelines are read once and then broken; the misuse
194
+ tiles are what people actually remember.
195
+
196
+ ---
197
+
198
+ ## 8. Canvas layout for the whole delivery
199
+
200
+ Lay it out left to right as a narrative, one row per stage, generous gutters (200px+ between
201
+ artboards) so each reads as its own object:
202
+
203
+ ```
204
+ Row 1 COVER 1080×1350 the mark, big, on the brand ground
205
+ Row 2 THE MARK construction · lockups · clear space
206
+ Row 3 PALETTE + TYPE full-width panels
207
+ Row 4 APPLICATIONS 4–6 real-proportion artboards in a row
208
+ Row 5 SPEC BOARD the eight sections above, as titled panels
209
+ ```
210
+
211
+ - **Put a caption above every artboard** (`19px/700`, accent, `letterSpacing: 2`) so the
212
+ board reads as a deck rather than a pile.
213
+ - **Set the composition background** to a dark neutral (`composition-patch {backgroundColor}`)
214
+ so the artboards read as objects on a desk. ⚠ That patch reaches an open tab now, but if
215
+ the ground is load-bearing, lay a real rect and `canvas.order {op:"back"}` it.
216
+ - ⚠ **Build in ONE batch** and check `count` against the number of ops you sent — a batch
217
+ stops at the first failure and lands everything before it.
218
+
219
+ ---
220
+
221
+ ## 9. Verify like a studio
222
+
223
+ Run `design-craft`'s studio pass, plus these identity-specific ones:
224
+
225
+ ```
226
+ □ Rendered the mark at 24px and LOOKED at it
227
+ □ Mark works in flat black, flat white, and knocked out
228
+ □ Every lockup has stated clear space and minimum size
229
+ □ Palette has a dark-ground variant and role sentences
230
+ □ Type scale has NUMBERS, not just face names
231
+ □ At least 4 real applications, at real proportions
232
+ □ Misuse section exists
233
+ □ Put the mark beside the competitor named in UNLIKE — still distinct?
234
+ □ Can someone build a new asset from this board without asking a question?
235
+ ```
236
+
237
+ ⚠⚠ **THE LAST ONE IS THE REAL TEST.** If any answer would be "ask the designer", the system
238
+ is incomplete — and the whole point of an identity is that it works when the designer has left.
239
+
240
+ ---
241
+
242
+ ## Sources
243
+
244
+ Pentagram brand-identity practice (symbol, wordmark, graphic system, palette, type system,
245
+ guidelines for in-house teams) · Michael Bierut, *How to* (brief → rationale → trade-offs) ·
246
+ Paula Scher's Public Theater identity — a system built almost entirely from typography, which
247
+ survived because the plays changed constantly and the identity was designed to absorb it ·
248
+ Massimo Vignelli, *The Vignelli Canon*.
@@ -170,6 +170,15 @@ teammate or another agent needs your work, move it into a project you both belon
170
170
  ⚠ Creating something does **not** put it in your owner's Recents — that is deliberate. They find it
171
171
  in Drafts. Opening it is what makes it recent *for them*.
172
172
 
173
+ ## Designing UI? Use the workspace's brand and components — and design before you build
174
+
175
+ A workspace stores its brand (`brand-tokens`) and its component library (`component-list`) as data.
176
+ Anything visual you make — a canvas, a deck, a mockup of a screen you are about to implement —
177
+ starts from those, placed with `component-instantiate {compositionId, instances:[…]}`. When the
178
+ task changes a product's UI, the composition comes FIRST: build the screen and its states there,
179
+ `canvas.review` it, link it to the ticket (`work-link {ticketId, kind:"design", refKind:"composition",
180
+ refId}`), then implement. `clearly-canvas` §0 has the details.
181
+
173
182
  ## When a write is refused
174
183
 
175
184
  `agent-denied-create` / `-update` / `-delete` mean your **capability matrix** said no. It covers the
@@ -47,6 +47,36 @@ A new composition almost always needs at least one revision after its first rend
47
47
  the pass finds nothing, look again at the thumbnail. If there is still nothing, say what
48
48
  you checked. Never make a change only to show that a review happened.
49
49
 
50
+ ## 0. Start from the workspace's brand and component library
51
+
52
+ A workspace keeps its brand and its components as data. Read them before you draw, and
53
+ build from them — redrawing a button the library already has is how designs drift from the
54
+ product they describe.
55
+
56
+ ```sh
57
+ beehaven call brand-tokens '{}' # colours (light + dark), type roles, radius, logos, voice
58
+ beehaven call component-list '{"query":"<brand or kind>"}' # parts, with props, variants and the code they mirror
59
+ beehaven call component-instantiate '{"compositionId":"<id>","instances":[
60
+ {"id":"<component>","x":96,"y":200,"variant":"Light"},
61
+ {"id":"<component>","x":96,"y":280,"variant":"Light","props":{"label":"Save changes"}}]}'
62
+ ```
63
+
64
+ - `instances` places many parts with ONE scene read and ONE write; never loop single
65
+ placements. `mode:"dark"` resolves brand colours against the brand's dark palette.
66
+ - Placed instances are ordinary editable nodes whose root remembers its master
67
+ (`libraryComponentId`). Edit freely; a genuinely new part goes back into the library with
68
+ `component-create` (or `{ fromCompositionId, nodeId }` to save one you drew).
69
+ - Component node coordinates are relative to the component ROOT, even under an inner
70
+ frame — the opposite of `canvas.create-node`, where x/y follow `parentId`.
71
+ - If the workspace has a brand kit, it IS the system for the brief: use its roles, faces
72
+ and voice rather than inventing a palette. Change the brand itself only when asked.
73
+
74
+ **UX design before implementation.** When the work is a change to a product's UI, the
75
+ composition is the design the code will answer to: show the real states (empty, loading,
76
+ error, long content, narrow width, dark theme), review it, and link it to the ticket with
77
+ `work-link {"ticketId":"…","kind":"design","refKind":"composition","refId":"<id>"}`
78
+ before implementation starts.
79
+
50
80
  ## 1. Establish the brief and inspect the material
51
81
 
52
82
  Read the supplied canvas, copy, brand and references before drawing. Resolve:
@@ -117,8 +147,9 @@ multiple colours and dense pages can all be appropriate. Choose deliberately.
117
147
 
118
148
  ## 3. Define a small design system and code the composition
119
149
 
120
- Write a local JS/Python builder when the artifact has repeated elements or several
121
- artboards. Use named tokens, helpers, arrays and layout arithmetic as you would in
150
+ When §0 found a brand kit and components, they are the system: map the brief onto their
151
+ roles, then add only what the brief genuinely needs. Write a local JS/Python builder when
152
+ the artifact has repeated elements or several artboards. Use named tokens, helpers, arrays and layout arithmetic as you would in
122
153
  HTML/CSS. The CLI can submit the complete generated payload. You do not need to
123
154
  hand-author hundreds of coordinates in a chat message.
124
155
 
@@ -158,6 +189,12 @@ Use a batch for each coherent build or revision, with descriptive unique layer n
158
189
  Parent content to its artboard. Inspect every operation result and rollback status.
159
190
  Save your builder and returned IDs so revisions are targeted and reproducible.
160
191
 
192
+ ⚠ **Batch, never loop.** One `canvas-act` batch of hundreds of steps is one scene read,
193
+ persisted every 50 steps, and the workspace stays responsive for everyone in it while it
194
+ runs. The same steps sent as hundreds of separate calls each re-read the whole scene. If a
195
+ call fails with a dropped connection, read the scene (`composition-detail
196
+ {id, includeScene:true}`) before retrying: the work may have landed.
197
+
161
198
  Keep review notes and status outside the delivered artwork. Use
162
199
  [communications.md](communications.md) only when posting those blocks is useful.
163
200
 
@@ -57,10 +57,19 @@ Create normalizes hex colours; raw stored scenes use RGBA arrays in 0..1.
57
57
 
58
58
  - `frame.create` and `canvas.create-node` with `parentId` take coordinates **relative
59
59
  to that parent**. Create the frame first. Names can resolve earlier nodes in a batch.
60
+ A frame nested in a frame (a dark band inside an artboard) needs a worker from
61
+ 2026-10-02 or later; on an older one, create it at world coordinates and attach it with
62
+ `canvas.update-nodes {ids:[id], patch:{parentId}}` (membership only, position kept).
63
+ To move a node INTO a frame and place it in one patch, pass `parentId` with frame-relative x/y.
64
+ - Component templates (`component-create`) are different: every node's x/y is relative
65
+ to the component ROOT, even under an inner frame.
60
66
  - Persisted nodes and perception bounds use **world coordinates**. Raw scene replacement
61
67
  through `composition-patch`/MCP must use world coordinates, even with `parentId`.
62
- - `canvas.update-nodes` geometry is world geometry. For group moves/resizes use the
63
- catalog's frame/arrangement/scale actions; do not assume a patch reflows children.
68
+ - `canvas.update-nodes` geometry is world geometry — EXCEPT when the same patch also sets
69
+ `parentId`: then x/y are relative to that frame, exactly like `canvas.create-node`. A
70
+ membership-only patch (`parentId` without x/y) keeps the node's world position. For group
71
+ moves/resizes use the catalog's frame/arrangement/scale actions; do not assume a patch
72
+ reflows children.
64
73
  - Build backgrounds before foregrounds. Use `canvas.order` to change stacking.
65
74
  - Name nodes uniquely; use returned IDs where names collide. Parent content to its
66
75
  frame so movement, selection, clipping and reviews can treat it as one artifact.
@@ -124,9 +133,11 @@ beehaven canvas add-image ./photo.png --composition <id> \
124
133
  --x 120 --y 160 --width 600 --height 400 --json
125
134
  ```
126
135
 
127
- The command reports `nodeId` and `imageFileId`. Coordinates are world coordinates;
128
- omit both `--x`/`--y` to use canvas auto-placement, and omit both size flags to use
129
- the image's native dimensions. Check `beehaven canvas` help on the installed CLI:
136
+ The command reports `nodeId`, `imageFileId` and `src` (the stored URL — reuse it, e.g. as a
137
+ brand logo). Coordinates are world coordinates; omit both `--x`/`--y` to use canvas
138
+ auto-placement, and omit both size flags to use the image's native dimensions (read from
139
+ the file header for PNG, JPEG, GIF and WebP). A negative coordinate works as `--x -3000`
140
+ on CLI 0.8.24+; older CLIs need `--x=-3000`. Check `beehaven canvas` help on the installed CLI:
130
141
  older versions do not have this command. On one of those, use the documented
131
142
  `get-upload-url` → binary PUT → `file-uploaded` sequence, then
132
143
  `canvas-act {action:"canvas.create-node",args:{type:"image",src:<absolute URL>,x,y,w,h}}`.
@@ -132,6 +132,10 @@ headline estimated at 867px measured **990**.
132
132
 
133
133
  ## 3. Colour — a system, not a mood board
134
134
 
135
+ ⚠ **A workspace brand kit already IS this role list.** `beehaven call brand-tokens '{}'`
136
+ returns ground, ink, muted, accent and support roles (light and dark) with their contrast
137
+ already measured. Use those values; the list below is for when there is no kit.
138
+
135
139
  **Build the palette from a role list, not from colours you like:**
136
140
 
137
141
  ```
package/skills/index.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "project-plans.md",
18
18
  "review.mjs"
19
19
  ],
20
- "sha": "21faa5f8d14beefe"
20
+ "sha": "9b34f0b8326257d0"
21
21
  },
22
22
  {
23
23
  "name": "agent-home",
@@ -38,7 +38,7 @@
38
38
  "files": [
39
39
  "SKILL.md"
40
40
  ],
41
- "sha": "7761aa240847d20c"
41
+ "sha": "260c3a6967b08a8b"
42
42
  },
43
43
  {
44
44
  "name": "layout-systems",
@@ -47,6 +47,13 @@
47
47
  ],
48
48
  "sha": "8de89b7b7c86f81f"
49
49
  },
50
+ {
51
+ "name": "brand-identity",
52
+ "files": [
53
+ "SKILL.md"
54
+ ],
55
+ "sha": "50587f531bfab1c6"
56
+ },
50
57
  {
51
58
  "name": "pair-on-canvas",
52
59
  "files": [
@@ -74,6 +81,6 @@
74
81
  "files": [
75
82
  "SKILL.md"
76
83
  ],
77
- "sha": "ad938cb0070fcab0"
84
+ "sha": "9e94e53b90ded575"
78
85
  }
79
86
  ]