beehaven 0.8.24 → 0.8.29

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.
@@ -67,9 +67,8 @@ export function resolveRelaySelection(cfg) {
67
67
  * `https://localhost:3000` (canvas.ts's `--dev` branch). They are not interchangeable: the apex
68
68
  * 301s to www, so the apex spelling handed people a link with a redirect in it.
69
69
  *
70
- * ⚠ STAGING RETURNS THE PRODUCTION SITE, AND THAT IS CORRECT. The frontend is prod-only — the
71
- * `staging.clearly-sh.pages.dev` alias is retired (CLAUDE.md) — so the staging worker's app really
72
- * is `www.clearly.sh`. Inventing a `staging.clearly.sh` here would be the dead-link bug again.
70
+ * Staging uses the local HTTPS frontend (Clearly AGENTS.md, 2026-10-08). Its workspace IDs
71
+ * belong to staging and must never be linked into the production frontend.
73
72
  *
74
73
  * ⚠ `custom` RETURNS undefined, DELIBERATELY. A custom relay says nothing about where its frontend
75
74
  * is, and guessing production for it is how you hand someone a link into the wrong deployment.
@@ -78,10 +77,10 @@ export function resolveRelaySelection(cfg) {
78
77
  export function appOriginForEnv(envName) {
79
78
  switch (envName) {
80
79
  case 'prod':
81
- case 'staging':
82
80
  return 'https://www.clearly.sh';
83
81
  // `npm run dev` is `next dev --experimental-https`, so the local app is HTTPS, not HTTP.
84
82
  case 'dev':
83
+ case 'staging':
85
84
  return 'https://localhost:3000';
86
85
  default:
87
86
  return undefined;
@@ -1,148 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  // ============================================================================
3
3
  // Beehaven Office - Relay Setup (Login)
4
- // Browser-based OAuth login flow for Clearly account
4
+ // The daemon's interactive sign-in fallback, and logout.
5
5
  // ============================================================================
6
- import http from 'http';
7
- import crypto from 'crypto';
8
- import { createInterface } from 'readline';
9
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
6
+ import { existsSync, readFileSync, writeFileSync, chmodSync } from 'fs';
10
7
  import { homedir } from 'os';
11
8
  import { join } from 'path';
12
9
  import { spawnSync } from 'child_process';
13
10
  import { Relay, CLEARLY_RELAY_URL } from './relay.js';
14
- import { resolveRelaySelection } from './relay-presets.js';
15
11
  import { SESSION_KEYCHAIN_ACCOUNTS, KEYCHAIN_SERVICE } from './keychain.js';
16
- import { withUtm } from './utm.js';
17
12
  const CONFIG_DIR = join(homedir(), '.beehaven');
18
13
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
19
- const AUTH_BASE_URL = 'https://clearly.sh/auth/beehaven';
20
- const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
21
14
  /**
22
- * The raw config, for questions that must be answered BEFORE a token exists.
23
- * `Relay.loadConfig()` returns null without one, which is the state a first login is in.
15
+ * The daemon calls this when it starts interactively with no token, or when its session has
16
+ * expired (daemon.ts). It then re-reads the config, so all it needs is credentials on disk.
17
+ *
18
+ * ⚠⚠ AWO-224 — THIS USED TO BE A SECOND, DIFFERENT SIGN-IN. It opened
19
+ * `https://www.clearly.sh/auth/beehaven?callback=…`, waited for a single `token` and wrote its own
20
+ * config. The web app no longer serves that route (404 on prod and, via the apex redirect, the
21
+ * same 404), so the fallback opened a dead page and timed out after two minutes. It now runs the
22
+ * exact flow `beehaven login` runs (`browser-login.ts`: `/cli/auth`, the keychain idToken slot
23
+ * the daemon reads, both dial-target keys) and nothing else — it must NOT restart the daemon,
24
+ * because it runs inside it.
24
25
  */
25
- function readConfigFile() {
26
- if (!existsSync(CONFIG_FILE))
27
- return {};
28
- try {
29
- return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
30
- }
31
- catch {
32
- return {};
33
- }
34
- }
35
- function saveRelayConfig(token, endpoint) {
36
- mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
37
- let existing = {};
38
- if (existsSync(CONFIG_FILE)) {
39
- try {
40
- existing = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
41
- }
42
- catch (err) {
43
- console.warn(`[config] Failed to parse ${CONFIG_FILE}:`, err.message);
44
- }
45
- }
46
- existing.token = token;
47
- // ⚠ BOTH KEYS, as `beehaven env` writes them — `endpoint` is dialled, `relayUrl` is the alias
48
- // other surfaces read. Updating one leaves the other naming a different environment.
49
- existing.endpoint = endpoint;
50
- existing.relayUrl = endpoint;
51
- existing.tier = 'connected';
52
- writeFileSync(CONFIG_FILE, JSON.stringify(existing, null, 2));
53
- chmodSync(CONFIG_FILE, 0o600);
54
- }
55
- function prompt(question) {
56
- const rl = createInterface({ input: process.stdin, output: process.stdout });
57
- return new Promise((resolve) => {
58
- rl.question(question, (answer) => {
59
- rl.close();
60
- resolve(answer.trim());
61
- });
62
- });
63
- }
64
- /**
65
- * Start a local HTTP server on a random port to receive the OAuth callback.
66
- * Returns a promise for the token, plus helpers to get the port and close.
67
- */
68
- function startCallbackServer() {
69
- let resolveToken;
70
- let rejectToken;
71
- let serverRef;
72
- let settled = false;
73
- const oauthState = crypto.randomBytes(32).toString('hex');
74
- const promise = new Promise((resolve, reject) => {
75
- resolveToken = resolve;
76
- rejectToken = reject;
77
- serverRef = http.createServer((req, res) => {
78
- if (settled)
79
- return;
80
- const url = new URL(req.url, `http://localhost`);
81
- if (url.pathname === '/callback') {
82
- // Validate OAuth state parameter to prevent CSRF (skip if server doesn't echo it back yet)
83
- const returnedState = url.searchParams.get('state');
84
- if (returnedState && returnedState !== oauthState) {
85
- res.writeHead(403, { 'Content-Type': 'text/html' });
86
- res.end('<!DOCTYPE html><html><body><h1>Invalid state parameter</h1></body></html>');
87
- return;
88
- }
89
- const token = url.searchParams.get('token');
90
- if (token) {
91
- settled = true;
92
- res.writeHead(200, { 'Content-Type': 'text/html' });
93
- res.end([
94
- '<!DOCTYPE html><html><head>',
95
- '<meta charset="utf-8">',
96
- '<title>Beehaven - Logged In</title>',
97
- '<style>',
98
- 'body{font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#fef9ef}',
99
- '.card{text-align:center;padding:48px;border-radius:16px;background:white;box-shadow:0 2px 12px rgba(0,0,0,0.08)}',
100
- 'h1{margin:0 0 8px;font-size:24px}',
101
- 'p{margin:0;color:#666;font-size:15px}',
102
- '.bee{font-size:48px;margin-bottom:16px}',
103
- '</style>',
104
- '</head><body>',
105
- '<div class="card">',
106
- '<div class="bee">&#x1F41D;</div>',
107
- '<h1>Logged in!</h1>',
108
- '<p>You can close this tab and return to your terminal.</p>',
109
- '</div>',
110
- '</body></html>',
111
- ].join(''));
112
- serverRef.close();
113
- resolve(token);
114
- }
115
- else {
116
- res.writeHead(400, { 'Content-Type': 'text/html' });
117
- res.end('<!DOCTYPE html><html><body><h1>Missing token</h1></body></html>');
118
- }
119
- }
120
- else {
121
- res.writeHead(404);
122
- res.end();
123
- }
124
- });
125
- serverRef.listen(0, '127.0.0.1');
126
- // Timeout after 2 minutes
127
- const timer = setTimeout(() => {
128
- if (!settled) {
129
- settled = true;
130
- serverRef.close();
131
- reject(new Error('Login timed out after 2 minutes.'));
132
- }
133
- }, LOGIN_TIMEOUT_MS);
134
- serverRef.on('close', () => clearTimeout(timer));
135
- });
136
- return {
137
- promise,
138
- getPort: () => serverRef.address()?.port || 0,
139
- close: () => {
140
- settled = true;
141
- serverRef?.close();
142
- },
143
- state: oauthState,
144
- };
145
- }
146
26
  export async function login() {
147
27
  console.log('');
148
28
  console.log(' Clearly account required to use Beehaven.');
@@ -165,114 +45,19 @@ export async function login() {
165
45
  console.log(' Session expired. Please log in again.');
166
46
  console.log('');
167
47
  }
168
- // Start local callback server
169
- const callbackServer = startCallbackServer();
170
- // Small delay to let server bind
171
- await new Promise((r) => setTimeout(r, 100));
172
- const port = callbackServer.getPort();
173
- if (!port) {
174
- console.log(' Failed to start auth server.');
175
- callbackServer.close();
176
- return false;
177
- }
178
- const callbackUrl = `http://localhost:${port}/callback`;
179
- const authUrl = withUtm(`${AUTH_BASE_URL}?callback=${encodeURIComponent(callbackUrl)}&state=${callbackServer.state}`);
180
- // Try to open browser
181
- let browserOpened = false;
182
- try {
183
- const openModule = await import('open');
184
- await openModule.default(authUrl);
185
- browserOpened = true;
186
- }
187
- catch {
188
- browserOpened = false;
189
- }
190
- if (browserOpened) {
191
- console.log(' Opening browser to sign in with Google...');
192
- console.log('');
193
- console.log(' Waiting for login... (press Ctrl+C to cancel)');
194
- }
195
- else {
196
- // Fallback: show URL and offer manual token paste
197
- console.log(' Could not open browser. Sign in manually:');
198
- console.log('');
199
- console.log(` 1. Open: ${authUrl}`);
200
- console.log(' 2. Sign in with Google');
201
- console.log('');
202
- console.log(' Waiting for login callback...');
203
- console.log(' Or paste your relay token below:');
204
- console.log('');
205
- // Race: callback server vs manual token paste
206
- const manualToken = prompt(' Relay token: ').then((token) => {
207
- if (token) {
208
- callbackServer.close();
209
- return token;
210
- }
211
- return null;
212
- });
213
- try {
214
- const token = await Promise.race([
215
- callbackServer.promise,
216
- manualToken.then((t) => {
217
- if (t)
218
- return t;
219
- return new Promise(() => { });
220
- }),
221
- ]);
222
- if (token) {
223
- return await verifyAndSave(token);
224
- }
225
- }
226
- catch (err) {
227
- console.log(` ${err.message}`);
228
- return false;
229
- }
230
- }
231
- // Wait for callback
232
48
  try {
233
- const token = await callbackServer.promise;
234
- return await verifyAndSave(token);
235
- }
236
- catch (err) {
237
- console.log(` ${err.message}`);
238
- return false;
239
- }
240
- }
241
- async function verifyAndSave(token) {
242
- // Validate token format before sending to server
243
- if (!token || !/^[a-zA-Z0-9_\-.]{20,}$/.test(token)) {
244
- console.log(' Invalid token format.');
245
- return false;
246
- }
247
- console.log('');
248
- console.log(' Verifying...');
249
- const relay = new Relay();
250
- /**
251
- * ⚠⚠ THE ACTIVE RELAY, NOT A HARDCODED PROD CONSTANT. Both of these were `CLEARLY_RELAY_URL`, so
252
- * a staging or dev user who signed in here was VERIFIED against prod and then had `endpoint`
253
- * OVERWRITTEN to prod — silently moving them to another environment as a side effect of logging
254
- * in. `beehaven env staging` writes the target; a login must honour it, never redefine it.
255
- *
256
- * ⚠ Read from the file rather than `Relay.loadConfig()`: that returns null without a token, which
257
- * is exactly the state a first login is in. `resolveRelaySelection` defaults to prod when the
258
- * config names nothing, so the no-config case is unchanged.
259
- */
260
- const dialUrl = resolveRelaySelection(readConfigFile()).url;
261
- const profile = await relay.verifyToken(token, dialUrl);
262
- if (profile) {
263
- saveRelayConfig(token, dialUrl);
264
- if (profile.displayName) {
265
- console.log(` Logged in as ${profile.displayName} (${profile.subscriptionPlan})`);
266
- }
267
- else {
268
- console.log(' Logged in.');
269
- }
49
+ // ⚠ Imported lazily: the Mac app loads this file for `logout` alone and should not pull in
50
+ // the sign-in flow's dependencies to do it.
51
+ const { browserLogin } = await import('./browser-login.js');
52
+ const who = await browserLogin();
53
+ console.log(` Logged in as ${who.email ?? who.userId}.`);
270
54
  console.log(' Config saved to ~/.beehaven/config.json');
271
55
  console.log('');
272
56
  return true;
273
57
  }
274
- else {
275
- console.log(' Token verification failed. Please try again.');
58
+ catch (err) {
59
+ console.log(` ${err?.message ?? err}`);
60
+ console.log(' Run `beehaven login`, then start again.');
276
61
  console.log('');
277
62
  return false;
278
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beehaven",
3
- "version": "0.8.24",
3
+ "version": "0.8.29",
4
4
  "description": "Beehaven CLI — command your Clearly workspace, agents, and canvas from the terminal",
5
5
  "private": false,
6
6
  "type": "module",
@@ -25,6 +25,18 @@ beehaven cd home # back to the default workspace
25
25
  beehaven disconnect # end the session
26
26
  ```
27
27
 
28
+ Start discovery narrowly: `beehaven explain <action>` gives one schema; `beehaven actions
29
+ --search <term>` or `--category <name>` bounds the inventory. The default inventory is compact;
30
+ `--verbose` or `--format full --json` requests full detail. Save large JSON once and print a
31
+ small summary rather than repeating a call after output truncation.
32
+
33
+ For ticket text search use `ticket-list {text:"…",projectId?,fields?,limit?}` (`query` and `q`
34
+ are aliases). For a design setup, `design-start {title,projectId?,boardId?,idempotencyKey}` creates or
35
+ resumes the linked artifacts using the normal permissions. Preserve idempotencyKey across retries;
36
+ requestId is reserved by the transport.
37
+ For supported canvas mutations inspect actual `updated` IDs, `outcome`, ignored/rejected fields
38
+ and warnings. A navigation request reaching a socket is not a verified editor display.
39
+
28
40
  Once `cd`'d, **every** verb auto-routes to that target: `actions`, `call`, `sql`, `ls`, `cat`, `tree`, `grep`, `watch`, `logs`, `workspace`, `ask`. Switch by `cd`-ing again.
29
41
 
30
42
  ```bash
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: brand-identity
3
3
  description: >-
4
- Build a complete brand identity on a Clearly canvas the way a studio does —
4
+ Design the requested logo or brand identity on a Clearly canvas —
5
5
  brief, mark, wordmark, lockups and clear space, palette, type system,
6
6
  applications, and a spec board someone else can build from. Use when asked
7
7
  for a brand, a logo, an identity, a rebrand, a visual system, brand
@@ -13,10 +13,14 @@ description: >-
13
13
 
14
14
  # brand-identity — a system, not a picture of a logo
15
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.
16
+ Match the user's scope. For a logo concept, deliver the editable mark/wordmark, primary and
17
+ one-color use, a small-size check, and the requested application. A logo plus landing page does
18
+ not require merchandise, storefronts or a complete guideline deck. Add those when requested or
19
+ when a concrete use in the brief requires them.
20
+
21
+ For a complete identity request, the deliverable is a system someone else can build with:
22
+ symbol, wordmark, palette, typography, applications and guidelines. Sections 6–8 describe that
23
+ larger scope; they are optional for a narrow concept.
20
24
 
21
25
  Read **`design-craft`** first or alongside — grid, type scale, colour roles and the studio
22
26
  pass are assumed here.
@@ -93,7 +97,9 @@ good. If it does not work in flat black, the shape is wrong and the gradient is
93
97
  - ⚠ **A wordmark is always tracked tighter than body text.** At display size, `letterSpacing`
94
98
  −1 to −4. Then fix the specific pairs the font gets wrong (an uppercase A next to a V, a
95
99
  T next to a lowercase o).
96
- - **Lockups are a fixed set, not an invitation.** Ship exactly these and say so:
100
+ - **For a full identity, define this fixed set.** For a concept, deliver the requested lockup
101
+ plus the mark-only and one-color forms needed for its smallest use. Additional lockups are
102
+ optional unless the brief asks for them:
97
103
 
98
104
  ```
99
105
  primary mark + wordmark, horizontal ← the default
@@ -225,11 +231,11 @@ Run `design-craft`'s studio pass, plus these identity-specific ones:
225
231
  ```
226
232
  □ Rendered the mark at 24px and LOOKED at it
227
233
  □ Mark works in flat black, flat white, and knocked out
228
- □ Every lockup has stated clear space and minimum size
234
+ □ Every delivered lockup has stated clear space and minimum size
229
235
  □ Palette has a dark-ground variant and role sentences
230
236
  □ Type scale has NUMBERS, not just face names
231
- □ At least 4 real applications, at real proportions
232
- □ Misuse section exists
237
+ □ Requested applications at real proportions (4–6 for a full identity; the requested surface for a concept)
238
+ □ Misuse section exists for a full identity; a concise use note is enough for a logo concept
233
239
  □ Put the mark beside the competitor named in UNLIKE — still distinct?
234
240
  □ Can someone build a new asset from this board without asking a question?
235
241
  ```
@@ -75,6 +75,17 @@ the admitted agent in the workspace, and soul reads/updates report whether the d
75
75
  wearing the drawing or a preset. If the capability line is absent, it is disabled for that
76
76
  deployment; the existing companion and presets remain available.
77
77
 
78
+ ## End with a handoff
79
+
80
+ ```bash
81
+ beehaven agent handoff "what you did · what is next · anything blocked"
82
+ ```
83
+
84
+ The handoff is the note your next login, or the next agent you hand the work to, reads first,
85
+ under **Last time you were here**. Write it for a colleague who has none of your context: what changed,
86
+ where it is (ticket keys, document titles), what is next and what is blocked. It ends the work session
87
+ and keeps your identity bound. Without one, the next brief can show only the session's label and counts.
88
+
78
89
  ⚠ **Do not call `agent-login` immediately after `beehaven agent login`.** The RPC is an alias of
79
90
  `agent-session-start`; doing both creates two sessions. Call it directly only when deliberately starting
80
91
  or resuming an additional workspace session.
@@ -118,8 +129,8 @@ item queued. Every comment/description write returns `mentions`, `agentsQueued`
118
129
  `beehaven comment <ref>` takes any id, key or exact title and the surface's anchor:
119
130
  `--quote` (document passage) · `--slide 3` (deck, required) · `--cell B4` / `--cell "Tab!B4"` (sheet) ·
120
131
  `--column "In review"` (board) · `--pin <id>` or `--x/--y` (canvas) · `--reply <id>`. An anchor a
121
- surface cannot carry is **refused**, never dropped. Over MCP: `clearly_write { type:"comment",
122
- target, content, fields }`, `clearly_read { type:"comment", target }`, and `target:"@me"` for mentions.
132
+ surface cannot carry is **refused**, never dropped. Over MCP: `clearly_comment { target, content,
133
+ fields }`, `clearly_read { type:"comment", target }`, and `target:"@me"` for mentions.
123
134
 
124
135
  ## Inbox — people and agents message you, and you message them
125
136
 
@@ -140,7 +151,7 @@ beehaven inbox who # every handle you can put in <
140
151
 
141
152
  Over MCP, `type: "message"` on the same tools: `clearly_read { type: "message", target: "@inbox" }`
142
153
  lists your threads (`box`, `query`), `target: "@people"` lists who you can message, and a thread id
143
- opens it (marks read). `clearly_write { type: "message", fields: { to: ["ada"] }, content }` starts a
154
+ opens it (marks read). `clearly_comment { type: "message", fields: { to: ["ada"] }, content }` starts a
144
155
  thread; with `target: <threadId>` it replies. `clearly_edit { changes: { read | starred | muted } }`
145
156
  changes your view; `clearly_delete` archives (`restore: true` brings it back).
146
157
 
@@ -179,6 +190,19 @@ task changes a product's UI, the composition comes FIRST: build the screen and i
179
190
  `canvas.review` it, link it to the ticket (`work-link {ticketId, kind:"design", refKind:"composition",
180
191
  refId}`), then implement. `clearly-canvas` §0 has the details.
181
192
 
193
+ No brand yet (`brand-tokens` answers `brand-not-found`) for a real company? Ask the person for their
194
+ website rather than inventing a palette, then:
195
+
196
+ 1. `beehaven call brand-import '{"url":"theirsite.com","dryRun":true}'` — colours by role, faces, logos,
197
+ voice and shape, each with its reason in `provenance`. Tell the person what you read.
198
+ 2. Ask what to call the brand (`suggestedName` is what the site calls itself — a suggestion, not a default).
199
+ 3. `beehaven call brand-import '{"url":"theirsite.com","name":"<their answer>"}'` saves it (the read is
200
+ reused; the first brand becomes the workspace default). Without `name` nothing is saved:
201
+ `brand-name-required`.
202
+
203
+ `brandId` + `apply:["colors","type"]` refreshes an existing brand as a new version instead. Licensed faces
204
+ come back set in the closest free one — `faces` says which.
205
+
182
206
  ## When a write is refused
183
207
 
184
208
  `agent-denied-create` / `-update` / `-delete` mean your **capability matrix** said no. It covers the
@@ -22,6 +22,10 @@ stored. A rendered image you have inspected is the evidence for visual quality.
22
22
  For a small correction, keep the existing direction and make the smallest useful edit.
23
23
  For a new composition or substantial redesign, use the workflow below.
24
24
 
25
+ Keep the requested scope. A landing concept with a logo needs its page layouts and a usable
26
+ logo; add application boards or a full guideline system only when the brief calls for them.
27
+ Read only relevant references. Save large tool responses once and print compact summaries.
28
+
25
29
  ## The studio bar — what "done" means, every time
26
30
 
27
31
  Load **`design-craft`** before any new visual work: grid, type scale, colour roles,
@@ -33,8 +37,9 @@ A composition is done when every requested artboard meets all four conditions:
33
37
  1. **`canvas.review` verdict is `clean` or `look`.** `fix` means a probable defect
34
38
  remains: text colliding or covered, contrast under WCAG (4.5:1 body, 3:1 display),
35
39
  type below the artboard's legible floor, missing media, text overflowing its box,
36
- placeholder copy. Fix it. When it is deliberate, pass the check name or layer in
37
- `ignore` and say why in the handoff.
40
+ placeholder copy. Fix it. For an intentional finding, use
41
+ `accept:[{check,ids:[node IDs],reason}]`. Accepted findings stay visible and return `look`.
42
+ Legacy `ignore` can suppress multiple checks and is not an unconditional pass.
38
43
  2. **Every `look` finding was looked at**: type-size and palette sprawl, near-miss
39
44
  alignment, copy against the trim, text over imagery. Act on it or say why not.
40
45
  3. **You inspected the rendered PNG** against design-craft's studio pass, covering what no
@@ -54,8 +59,9 @@ build from them — redrawing a button the library already has is how designs dr
54
59
  product they describe.
55
60
 
56
61
  ```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
62
+ beehaven call brand-tokens '{"compact":true}' # roles without repeated CSS/prose
63
+ beehaven call brand-import '{"url":"theirsite.com","dryRun":true}' # no brand yet? read it off their website
64
+ beehaven call component-list '{"query":"<kind>","compact":true}' # IDs, dimensions and prop names
59
65
  beehaven call component-instantiate '{"compositionId":"<id>","instances":[
60
66
  {"id":"<component>","x":96,"y":200,"variant":"Light"},
61
67
  {"id":"<component>","x":96,"y":280,"variant":"Light","props":{"label":"Save changes"}}]}'
@@ -68,8 +74,9 @@ beehaven call component-instantiate '{"compositionId":"<id>","instances":[
68
74
  `component-create` (or `{ fromCompositionId, nodeId }` to save one you drew).
69
75
  - Component node coordinates are relative to the component ROOT, even under an inner
70
76
  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.
77
+ - For Clearly product UI, use its brand kit. For a separate client, use that client's kit
78
+ and supplied assets. A requested new client identity is scoped to that composition;
79
+ it does not change the workspace's default brand.
73
80
 
74
81
  **UX design before implementation.** When the work is a change to a product's UI, the
75
82
  composition is the design the code will answer to: show the real states (empty, loading,
@@ -107,6 +114,33 @@ beehaven call canvas-perceive '{"compositionId":"<id>","format":"json"}'
107
114
  beehaven call canvas-catalog '{"query":"create-node"}'
108
115
  ```
109
116
 
117
+ A canvas is planned like a git repository: its own plan board, a WORK TICKET for the request, and
118
+ COMMITS — one ticket per coherent layer or section of a larger change.
119
+ `design-start {title, brandId?}` makes a PRIVATE DRAFT canvas, bound to the brand, with its private
120
+ plan board and the work ticket, and returns `{ compositionId, ticketId, ticketKey, boardId, brand }`.
121
+ Same title again returns the same draft. Nothing is shared until it is filed:
122
+ `design-file {compositionId, projectId | projectTitle}` moves the canvas — plan board and every
123
+ ticket with it — into the project the person chose and sends the work ticket to in-review. Ask which
124
+ project unless they named one; a close-but-different title answers `similar-projects` with
125
+ candidates. Repeat-safe.
126
+ Plan a larger change before drawing, the way you would split a big code change into commits:
127
+ `design-plan {compositionId, commits: ["Grid, type and colour styles", "Nav", "Hero", …]}` (drafts
128
+ and filed canvases alike) adds ordered commit tickets under the work ticket. `sections: true` uses
129
+ the landing-page set (set up frames and styles, nav bar, hero, highlights, offer, story, call to
130
+ action, footer, mobile, states); `phases: true` and a `brief` add the studio method (brief, research,
131
+ structure, direction, design, review) — load the `design-process` skill. Order commits so each
132
+ builds on the last and can be reviewed alone. Build ONE commit per canvas-act batch that ends with
133
+ `canvas.review` of the artboard and carries
134
+ `"ticket":{"id":"<key>","status":"done","resolution":"<what changed>"}` — it lands only if that
135
+ review is clean or look; `result.ticket` says whether it did, and the ticket logs the canvas
136
+ revision and the layers the commit changed. Auto-layout, gradients, shadows and SVG placeholders:
137
+ see "Technique" in [authoring.md](authoring.md).
138
+
139
+ `composition-board-get {compositionId}` reads the plan back. `design-start` with a project, ticket or
140
+ `phases` still sets up planned work directly (kept for one release).
141
+ For an existing task, reuse its ticket/project. Check one unknown action with
142
+ `beehaven explain <action>` or a filtered catalog; do not download the entire registry.
143
+
110
144
  Use the composition supplied by the user. If none is named, inspect recent titles and
111
145
  context; create a clearly titled composition for new work instead of modifying an
112
146
  arbitrary recent canvas. File shared work into its project.
@@ -188,6 +222,10 @@ For a single page, establish the complete hierarchy before polishing small detai
188
222
  Use a batch for each coherent build or revision, with descriptive unique layer names.
189
223
  Parent content to its artboard. Inspect every operation result and rollback status.
190
224
  Save your builder and returned IDs so revisions are targeted and reproducible.
225
+ Check `ignoredFields`, `rejectedFields`, `warnings`, `outcome` and `canvasChanges` as well as
226
+ `ok/count`. `matched` is not `updated`. An unchanged or partly ignored request does not prove
227
+ the intended edit happened. Read `composition-detail {includeScene:true}` from its top-level
228
+ `nodes`; never treat a missing response field as an empty scene. Keep executed payloads immutable.
191
229
 
192
230
  ⚠ **Batch, never loop.** One `canvas-act` batch of hundreds of steps is one scene read,
193
231
  persisted every 50 steps, and the workspace stays responsive for everyone in it while it
@@ -195,6 +233,15 @@ runs. The same steps sent as hundreds of separate calls each re-read the whole s
195
233
  call fails with a dropped connection, read the scene (`composition-detail
196
234
  {id, includeScene:true}`) before retrying: the work may have landed.
197
235
 
236
+ ⚠ **One batch per canvas at a time.** A reply that times out ("outcome unknown") or drops does
237
+ not stop the batch — it is still running on the workspace. Resending at once used to start a
238
+ second, slow, unprotected batch and could half-apply both (CA-43). The workspace now queues a
239
+ second writer behind the first and answers `code: "canvas-busy"` (with the running batch's
240
+ progress) if it waits too long — nothing from that call was applied. Either way: read the scene,
241
+ then send only what is missing. A ~1,100-step build takes about 40 s; the CLI waits 180 s by
242
+ default and `--timeout <ms>` (or `BEEHAVEN_RPC_TIMEOUT_MS`) raises it. Never place component
243
+ instances after a build whose reply you did not see.
244
+
198
245
  Keep review notes and status outside the delivered artwork. Use
199
246
  [communications.md](communications.md) only when posting those blocks is useful.
200
247
 
@@ -216,6 +263,12 @@ It invokes `canvas.review` on one persisted snapshot and returns:
216
263
  type and palette sprawl, near-miss edges, trim crowding, text over imagery;
217
264
  - the placeholder/overflow audit, measured text fit, and a preview PNG.
218
265
 
266
+ For several artboards, write their IDs/names as a JSON array and use:
267
+ `node <skill-directory>/review.mjs --many <composition-id> <ids.json> <output-directory>`.
268
+ This uses `canvas.review-many`: one scene read, separate findings per artboard and compact
269
+ preview handles. Review only artboards changed since their verified snapshot. The helper versions
270
+ existing output paths so before/after evidence is retained.
271
+
219
272
  The helper prints the verdict and every finding. Work until the verdict is `clean` or
220
273
  `look`, as described in the studio bar above. If the installed worker lacks `canvas.review`,
221
274
  use a real frame ID with `--perceive` for a preview-only check and run the text measurement
@@ -223,9 +276,16 @@ and audit separately. A failed or missing render means visual verification is in
223
276
 
224
277
  ```sh
225
278
  # A deliberate finding — say why in the handoff:
226
- beehaven call canvas-act '{"compositionId":"<id>","action":"canvas.review","args":{"ids":["<frame>"],"ignore":["text-over-image"]}}'
279
+ beehaven call canvas-act '{"compositionId":"<id>","action":"canvas.review","args":{"ids":["<frame>"],"accept":[{"check":"text-over-image","ids":["<text-id>","<image-id>"],"reason":"Intentional type over this photograph; rendered contrast was inspected."}]}}'
227
280
  ```
228
281
 
282
+ Asset storage is a reference-aware pipeline: an SVG may have `svgRef` instead of inline markup.
283
+ Do not shrink or recreate it just because the inline field is absent. Replace it with
284
+ `canvas.update-nodes {ids, patch:{svg:<complete markup>}}`; references are managed by the action.
285
+ Use `canvas.fonts` before choosing an unfamiliar family/weight. Fallback metrics are not exact.
286
+ Use `canvas.editor-state {requireLive:true,includePixels:true}` to verify the responding editor.
287
+ An agent camera and a navigation delivery count do not prove the person's view was framed.
288
+
229
289
  Inspect the actual image and identify concrete defects, not generic praise:
230
290
 
231
291
  | Lens | What to judge |