@clearlysh/beehaven 0.8.27 → 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.
@@ -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 = 'db2b3eaeb90b';
56
+ export const BUILD_SHA = 'd733c0004e58-dirty';
57
57
  /** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
58
- export const BUILD_TIME = '2026-10-07T02:00Z';
58
+ export const BUILD_TIME = '2026-10-10T21:21Z';
59
59
  /**
60
60
  * The build identity as one short string, for `--version`.
61
61
  *
package/dist/clearly.js CHANGED
@@ -47,11 +47,12 @@ const c = {
47
47
  red: (s) => `\x1b[31m${s}\x1b[0m`,
48
48
  };
49
49
  /**
50
- * ⚠ THE AGENT TURN IS THE SLOW PART AND THE CLI RELAY CAPS AT 180s. `RESPOND_TIMEOUT_MS` on the
51
- * server is the same 180s, so a turn that outlives this was going to be killed anyway — matching
52
- * them means a timeout here always means the same thing it means there.
50
+ * ⚠ THE AGENT TURN IS THE SLOW PART. The server's `RESPOND_TIMEOUT_MS` is 300s (clearly CDS-90, was
51
+ * 180s); this waits that plus the report's tail, so a turn that finishes on time is never abandoned
52
+ * by its caller and a timeout here still means the turn itself ran out. The relay-forward window
53
+ * follows the `_timeout` sent with the call.
53
54
  */
54
- const TURN_MS = 180_000;
55
+ export const TURN_MS = 330_000;
55
56
  /**
56
57
  * ⚠ STDERR IS CAPTURED, NOT INHERITED. A failing probe is normal here — `HEAD~30` does not exist in
57
58
  * a repo with two commits — and with inherited stderr git printed
package/dist/cli.js CHANGED
@@ -3917,7 +3917,7 @@ complete -F _beehaven_completions beehaven`);
3917
3917
  console.log(` ${r.url}`);
3918
3918
  // "reached" not "navigated": the frame goes to every connected surface but only the
3919
3919
  // VISIBLE one acts on it, so a count of sockets is not a count of things that moved.
3920
- console.log(color.dim(` ${r.delivered > 0 ? `on screen (reached ${r.delivered} connected surface(s))` : 'no tab connected'}`));
3920
+ console.log(color.dim(` ${r.delivered > 0 ? `navigation requested (${r.delivered} connected surface(s)); editor display is not yet verified` : 'no navigation recipient connected'}`));
3921
3921
  // Only reach for the local browser when asked, or when nothing was
3922
3922
  // delivered — opening a window under someone who already has the tab
3923
3923
  // focused is worse than doing nothing.
@@ -26,7 +26,10 @@ import { resolveActiveAgent } from '../agent-state.js';
26
26
  import { isAgentRuntime } from '../agent-scope.js';
27
27
  import { getTarget } from '../cli-state.js';
28
28
  import { attachShellCwd, absorbShellCwd, shellTargetKey } from '../shell-session.js';
29
- export function getTimeout(values) {
29
+ /** Workspace-agent turns wait for the server's turn ceiling (300s) plus the report's tail. */
30
+ export const AGENT_TURN_TIMEOUT_MS = 330_000;
31
+ const AGENT_TURN_ACTIONS = new Set(['agent-prompt', 'agent-turn']);
32
+ export function getTimeout(values, action) {
30
33
  // Precedence: explicit --timeout flag → BEEHAVEN_RPC_TIMEOUT_MS env → default.
31
34
  // The default is generous because real jobs (sticker-sheet gen, renders,
32
35
  // long agent turns) routinely run well past the old 30s and shouldn't need a
@@ -37,7 +40,8 @@ export function getTimeout(values) {
37
40
  const env = process.env.BEEHAVEN_RPC_TIMEOUT_MS;
38
41
  if (env && Number.isFinite(Number(env)) && Number(env) > 0)
39
42
  return parseInt(env, 10);
40
- return 180_000;
43
+ // An agent turn may run to the server's 300s ceiling (clearly CDS-90); waiting less abandons it.
44
+ return action && AGENT_TURN_ACTIONS.has(action) ? AGENT_TURN_TIMEOUT_MS : 180_000;
41
45
  }
42
46
  /**
43
47
  * True when `msg` says `action` isn't available on the target DO.
@@ -618,11 +622,24 @@ export async function cmdActions(values, positional) {
618
622
  grouped.set(cat, []);
619
623
  grouped.get(cat).push(action);
620
624
  }
625
+ if (!values.verbose && !values.category && !values.search) {
626
+ for (const [category, entries] of grouped) {
627
+ console.log(` ${category.padEnd(18)} ${String(entries.length).padStart(3)} actions · ${entries.slice(0, 3).map(a => a.name).join(', ')}${entries.length > 3 ? ', …' : ''}`);
628
+ }
629
+ console.log('');
630
+ console.log(' Find an action: beehaven actions --search <term>');
631
+ console.log(' List a category: beehaven actions --category <name>');
632
+ console.log(' Exact schema: beehaven explain <action>');
633
+ console.log(' Complete inventory: beehaven actions --verbose or --format full --json');
634
+ return;
635
+ }
621
636
  for (const [category, catActions] of grouped) {
622
637
  const catLabel = category.charAt(0).toUpperCase() + category.slice(1);
623
638
  console.log(` \x1b[36m${catLabel}\x1b[0m (${catActions.length})`);
624
639
  for (const a of catActions) {
625
- const desc = a.description ? ` \x1b[90m${a.description}\x1b[0m` : '';
640
+ const description = String(a.description || '');
641
+ const display = values.verbose || description.length <= 160 ? description : `${description.slice(0, 157)}…`;
642
+ const desc = display ? ` \x1b[90m${display}\x1b[0m` : '';
626
643
  const aliasLabel = Array.isArray(a.aliases) && a.aliases.length > 0
627
644
  ? ` \x1b[2m(also: ${a.aliases.join(', ')})\x1b[0m`
628
645
  : '';
@@ -1088,7 +1105,7 @@ export async function cmdCall(action, rawInput, values) {
1088
1105
  console.error(`\x1b[2m→ ${target} ${action}${who}\x1b[0m`);
1089
1106
  }
1090
1107
  try {
1091
- const raw = await rpcOnce(resolved.action, resolved.input, getTimeout(values));
1108
+ const raw = await rpcOnce(resolved.action, resolved.input, getTimeout(values, action));
1092
1109
  // Unwrap proxy envelope: admin:*-rpc wraps results in { type: 'ack', result: ... }
1093
1110
  const result = raw?.type === 'ack' && raw?.result !== undefined
1094
1111
  ? raw.result
@@ -3090,7 +3107,7 @@ export async function cmdAgent(prompt, values) {
3090
3107
  : {}),
3091
3108
  },
3092
3109
  // Omni runs tools; the CLI default is far too short for a real turn.
3093
- getTimeout(values) || 180_000);
3110
+ getTimeout(values, 'agent-turn'));
3094
3111
  /**
3095
3112
  * Print the REPLY, not the envelope. The whole response is still available
3096
3113
  * with `--json` — but a verb whose entire purpose is "ask a question and read
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clearlysh/beehaven",
3
- "version": "0.8.27",
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
  ```
@@ -190,6 +190,19 @@ task changes a product's UI, the composition comes FIRST: build the screen and i
190
190
  `canvas.review` it, link it to the ticket (`work-link {ticketId, kind:"design", refKind:"composition",
191
191
  refId}`), then implement. `clearly-canvas` §0 has the details.
192
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
+
193
206
  ## When a write is refused
194
207
 
195
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
@@ -225,6 +263,12 @@ It invokes `canvas.review` on one persisted snapshot and returns:
225
263
  type and palette sprawl, near-miss edges, trim crowding, text over imagery;
226
264
  - the placeholder/overflow audit, measured text fit, and a preview PNG.
227
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
+
228
272
  The helper prints the verdict and every finding. Work until the verdict is `clean` or
229
273
  `look`, as described in the studio bar above. If the installed worker lacks `canvas.review`,
230
274
  use a real frame ID with `--perceive` for a preview-only check and run the text measurement
@@ -232,9 +276,16 @@ and audit separately. A failed or missing render means visual verification is in
232
276
 
233
277
  ```sh
234
278
  # A deliberate finding — say why in the handoff:
235
- 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."}]}}'
236
280
  ```
237
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
+
238
289
  Inspect the actual image and identify concrete defects, not generic praise:
239
290
 
240
291
  | Lens | What to judge |
@@ -49,6 +49,12 @@ do not blindly resend only skipped operations. Mixed live/headless actions may h
49
49
  different rollback behavior. Use deterministic layer names and returned IDs to avoid
50
50
  duplicate content during retries.
51
51
 
52
+ Also check ignored/rejected fields, warnings, mutation outcome and actual changed IDs. A matched
53
+ node or `ok:true` receipt alone is not proof of an update. The bundled `assertReceipt` helper can
54
+ reject partial drops; `requireChange:true` rejects an unexpected no-op. A deliberate replay may
55
+ be unchanged and should reuse its previous evidence instead of recreating nodes.
56
+ Keep executed payloads and returned-ID manifests separate from regenerated plans.
57
+
52
58
  ## Node shape and coordinates
53
59
 
54
60
  Fields are flat: `{name,type,x,y,w,h,text,size,family,fontWeight,fill,...}`.
@@ -71,6 +77,13 @@ Create normalizes hex colours; raw stored scenes use RGBA arrays in 0..1.
71
77
  moves/resizes use the catalog's frame/arrangement/scale actions; do not assume a patch
72
78
  reflows children.
73
79
  - Build backgrounds before foregrounds. Use `canvas.order` to change stacking.
80
+
81
+ For create/update geometry, `coordinateSpace:"world"` or `"parent"` makes the intent explicit.
82
+ Default behavior remains compatible with the rules above. Receipts include normalized world
83
+ bounds and parent offsets where available. Template coordinates remain relative to their root.
84
+ An SVG's bytes may be stored in `svgRef` once it exceeds the inline limit; this is valid artwork,
85
+ not a missing node. Replacing `svg` clears and rebuilds its reference automatically. Never patch
86
+ `svgRef` manually or guess its storage URL. Image `fit` is explicitly `contain` or `cover`.
74
87
  - Name nodes uniquely; use returned IDs where names collide. Parent content to its
75
88
  frame so movement, selection, clipping and reviews can treat it as one artifact.
76
89
  - Give an intended stroke both `stroke` and `strokeWidth`. Zero explicitly removes it.
@@ -106,6 +119,16 @@ and tokens when relevant. Constraints reflow children on frame resize; they do n
106
119
  a static canvas into a responsive website. Design explicit mobile artboards when the
107
120
  deliverable requires them, with content order and hierarchy reconsidered for the width.
108
121
 
122
+ ## Technique
123
+
124
+ The same rules the in-app assistant works to (canvas-catalog returns them as `technique`).
125
+
126
+ - AUTO-LAYOUT: rows and stacks (nav links, button groups, card rows, form fields) are a frame whose children you create in reading order, then canvas.auto-layout { id, direction, gap, padding, align, justify, hug }. hug:false keeps the frame size, which a full-width bar needs with justify:"space-between"; set a child to layoutSizingH/V "fill" | "hug" | "fixed" with canvas.update-nodes.
127
+ - GRADIENTS: gradient { kind, angle, stops: [{ offset, color }] } replaces the solid fill. angle is the direction the colour travels: 0 = left to right, 90 = top to bottom, 45 = top-left to bottom-right (not CSS degrees). Use two or three stops from the brand palette, and keep text off a busy gradient or check its contrast in canvas.review.
128
+ - SHADOWS AND BLUR: effects on any node. A soft card shadow is { type: "drop-shadow", x: 0, y: 4, blur: 16, spread: 0, color: "#0000001F" }; frosted glass is { type: "background-blur", blur: 16 }. Never fake a shadow with an extra rectangle.
129
+ - SVG PLACEHOLDERS: until real imagery exists, draw it as an svg node at its final size in a muted tone from the brand palette, never a flat accent block, e.g. for a 400×240 slot svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 240"><rect width="400" height="240" rx="12" fill="#EDE6DF"/><circle cx="170" cy="96" r="14" fill="#CDBFB2"/><path d="M150 168l40-48 30 34 22-20 40 34z" fill="#CDBFB2"/></svg>' — <svg viewBox="0 0 W H"> always matches the node w and h. Put a label in its own text node and name the svg "<section> image placeholder". Hand-write only simple marks like this (placeholders, dividers, UI icons); logos, illustrations and finished artwork come from svg-make.
130
+ - SECTIONS: one frame per page section, named exactly as its ticket says, inside the page artboard. Build a section in one canvas-act batch that ends with canvas.review of the artboard and carries ticket: { id: "<its key>", status: "done", resolution: "<what changed>" }. The ticket moves only when that review is clean or look; result.ticket says whether it did. Then start the next section.
131
+
109
132
  Use `arrow.create {from,to,routing,label}` for connected diagrams, so arrows remain bound
110
133
  when nodes move. Use native shapes and SVG for custom vector details. Keep labels as
111
134
  native text so they remain editable and measurable.
@@ -1,20 +1,69 @@
1
1
  #!/usr/bin/env node
2
2
  /** Save review pixels outside the model context. No shell interpolation, no workspace mutations. */
3
3
  import { execFileSync } from 'node:child_process';
4
- import { mkdirSync, writeFileSync } from 'node:fs';
4
+ import { createHash } from 'node:crypto';
5
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
5
6
  import { dirname, resolve } from 'node:path';
6
7
  import { fileURLToPath } from 'node:url';
7
8
 
8
- export function rpc(action, payload) {
9
+ export function assertReceipt(result, { requireChange = false } = {}) {
10
+ if (result?.ok === false || result?.error || result?.results?.some(r => r.ok === false)) {
11
+ throw new Error(result.error || result.results?.find(r => !r.ok)?.error || 'Canvas request failed');
12
+ }
13
+ for (const row of result?.results ?? [result]) {
14
+ const value = row.value ?? row;
15
+ if (value.ignoredFields?.length || value.rejectedFields?.length) {
16
+ throw new Error(`Fields were not applied: ${[...(value.ignoredFields ?? []), ...(value.rejectedFields ?? [])].join(', ')}. Some work may have landed; inspect the saved scene before retrying.`);
17
+ }
18
+ if (requireChange) {
19
+ const delta = value.canvasChanges;
20
+ const changed = ['changed', 'partial'].includes(value.outcome) || (Array.isArray(delta) ? delta.length > 0 : delta && typeof delta === 'object'
21
+ ? Number(delta.added || 0) + Number(delta.changed || 0) + Number(delta.removed || 0) > 0
22
+ : value.placed === true || /\.create(-node)?$/.test(String(row.action || '')) && !!value.id && !value.duplicate);
23
+ if (!changed) throw new Error('No nodes changed, or the receipt does not prove a change. Inspect the saved scene; do not treat a match as an update.');
24
+ }
25
+ }
26
+ return result;
27
+ }
28
+
29
+ export function rpc(action, payload, options) {
9
30
  const stdout = execFileSync('beehaven', ['call', action, '-'], {
10
31
  input: JSON.stringify(payload), encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
11
32
  stdio: ['pipe', 'pipe', 'pipe'], timeout: 120_000,
12
33
  });
13
34
  const result = JSON.parse(stdout);
14
- if (result.ok === false || result.error || result.results?.some((r) => r.ok === false)) {
15
- throw new Error(result.error || result.results.find((r) => !r.ok)?.error || 'Canvas request failed');
35
+ return assertReceipt(result, options);
36
+ }
37
+
38
+ export function sceneNodes(result) {
39
+ if (!Array.isArray(result?.nodes)) throw new Error('Expected composition-detail includeScene:true to return top-level nodes. Refusing to interpret a missing field as an empty canvas.');
40
+ return result.nodes;
41
+ }
42
+
43
+ /** Resume creates only. Existing layers are preserved, including the person's later edits. */
44
+ export function resumeCanvasPlan(plan, snapshot, idManifest = {}) {
45
+ const nodes = sceneNodes(snapshot), byId = new Map(nodes.map(n => [n.id, n]));
46
+ const names = new Map();
47
+ for (const n of nodes) { const list = names.get(n.name) ?? []; list.push(n); names.set(n.name, list); }
48
+ const roots = new Map(), batch = [], skipped = [];
49
+ for (const step of plan.batch ?? []) {
50
+ const args = { ...(step.args ?? {}) };
51
+ if (!['frame.create', 'canvas.create-node'].includes(step.action)) { skipped.push({ action: step.action, reason: 'Resume preserves existing mutations; revise explicitly.' }); continue; }
52
+ const type = step.action === 'frame.create' ? 'frame' : args.type;
53
+ const pinned = args.id || idManifest[args.name];
54
+ const matches = names.get(args.name) ?? [];
55
+ if (!pinned && matches.length > 1) throw new Error(`Ambiguous layer name ${args.name}; use its recorded ID.`);
56
+ const existing = pinned ? byId.get(pinned) : matches[0];
57
+ if (existing) {
58
+ if (existing.type !== type) throw new Error(`Layer ${args.name} has type ${existing.type}, expected ${type}. No automatic replacement.`);
59
+ roots.set(args.name, existing.id); skipped.push({ id: existing.id, name: args.name, reason: 'Already exists; preserved.' }); continue;
60
+ }
61
+ args.id = pinned || `plan-${createHash('sha256').update(`${plan.compositionId}:${args.name}`).digest('hex').slice(0, 24)}`;
62
+ if (args.parentId && roots.has(args.parentId)) args.parentId = roots.get(args.parentId);
63
+ roots.set(args.name, args.id);
64
+ batch.push({ ...step, args });
16
65
  }
17
- return result;
66
+ return { compositionId: plan.compositionId, expectedCanvasNodesRev: snapshot.canvasNodesRev, batch, skipped };
18
67
  }
19
68
 
20
69
  export function saveReview(report, prefix) {
@@ -24,7 +73,21 @@ export function saveReview(report, prefix) {
24
73
  }
25
74
  const bytes = Buffer.from(preview.dataUrl.slice('data:image/png;base64,'.length), 'base64');
26
75
  if (!bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) throw new Error('Invalid PNG response');
27
- const base = resolve(prefix);
76
+ if (bytes.length < 45 || bytes.readUInt32BE(8) !== 13 || bytes.subarray(12,16).toString() !== 'IHDR'
77
+ || !bytes.readUInt32BE(16) || !bytes.readUInt32BE(20)) throw new Error('Invalid PNG response: missing image header/body');
78
+ let offset = 8, imageData = false, end = false;
79
+ while (offset + 12 <= bytes.length) {
80
+ const length = bytes.readUInt32BE(offset), tag = bytes.subarray(offset+4,offset+8).toString();
81
+ if (offset + length + 12 > bytes.length) throw new Error('Invalid PNG response: truncated image chunk');
82
+ if (tag === 'IDAT' && length > 0) imageData = true;
83
+ offset += length + 12;
84
+ if (tag === 'IEND') { end = length === 0; break; }
85
+ }
86
+ if (!imageData || !end) throw new Error('Invalid PNG response: missing image data/end');
87
+ let base = resolve(prefix);
88
+ const original = base;
89
+ let version = 0;
90
+ while (existsSync(`${base}.png`) || existsSync(`${base}.json`)) base = `${original}-r${report.canvasNodesRev ?? 'unknown'}-${++version}`;
28
91
  mkdirSync(dirname(base), { recursive: true });
29
92
  const { dataUrl, url, ...metadata } = preview;
30
93
  const saved = { ...report, preview: { ...metadata, path: `${base}.png` } };
@@ -34,16 +97,47 @@ export function saveReview(report, prefix) {
34
97
  return { png: `${base}.png`, report: `${base}.json`,
35
98
  // `fix` = a probable defect remains: fix it, or pass its check/id in `ignore` with a reason.
36
99
  // `look` = only things worth a second look. `clean` = nothing mechanical found — NOT "good".
37
- verdict: report.verdict ?? 'unknown', blocking: report.blocking,
100
+ verdict: report.verdict ?? 'unknown', acceptanceStatus: report.acceptanceStatus,
101
+ acceptedFindings: report.acceptedFindings ?? [], blocking: report.blocking,
38
102
  craft: craft.map((f) => `${f.severity} ${f.check}: ${f.detail}`),
103
+ audit: (report.audit?.violations ?? []).map(v => ({check:v.kind,layer:v.name || v.id,detail:v.detail || v.message || v.reason || `${v.kind} on ${v.name || v.id}`})),
104
+ scopeWarnings: report.scopeWarnings ?? [],
105
+ assets: (report.assets ?? []).filter(a => a.status !== 'ready'),
106
+ typography: (report.typography?.text ?? []).filter(t => t.issue || t.horizontalOverflow > 1 || !t.autoHeight && t.verticalOverflow > 1 || t.exact === false)
107
+ .map(t => ({layer:t.name || t.id,issue:t.issue,horizontalOverflow:t.horizontalOverflow,verticalOverflow:t.verticalOverflow,
108
+ requestedFont:t.font?.requestedFamily,requestedWeight:t.font?.requestedWeight,resolvedFile:t.font?.file,exact:t.exact})),
39
109
  nodes: report.scope?.nodeCount, findings: report.audit?.total, textChecked: report.typography?.checked,
40
110
  textOmitted: report.typography?.omitted, visualReviewRequired: true,
41
111
  note: report.verdict === 'fix'
42
- ? 'Fix every warn (or justify it with ignore), then review again. Then open the PNG and critique it.'
112
+ ? 'Fix every warn (or accept an exact finding with a reason), then review changed artboards. Then open the PNG and critique it.'
43
113
  : 'Open the PNG with your image-viewing tool, critique it against the studio bar, revise and render again.' };
44
114
  }
45
115
 
46
- export function main(argv = process.argv.slice(2)) {
116
+ export async function saveReviewPixels(report, prefix, fetchImage = fetch) {
117
+ if (report.preview?.dataUrl) return saveReview(report, prefix);
118
+ const url = report.preview?.url;
119
+ if (!url || !/^https?:\/\//.test(url)) throw new Error(report.previewError || 'No downloadable preview. Visual verification is incomplete.');
120
+ const response = await fetchImage(url);
121
+ if (!response.ok) throw new Error(`Preview download failed (HTTP ${response.status}); request a fresh review.`);
122
+ const bytes = Buffer.from(await response.arrayBuffer());
123
+ return saveReview({ ...report, preview: { ...report.preview, dataUrl: `data:image/png;base64,${bytes.toString('base64')}` } }, prefix);
124
+ }
125
+
126
+ export async function main(argv = process.argv.slice(2)) {
127
+ if (argv.includes('--many')) {
128
+ const args = argv.filter(a => a !== '--many');
129
+ if (args.length !== 3) throw new Error('Usage: node review.mjs --many <compositionId> <artboard-ids.json> <output-directory>');
130
+ const [compositionId, manifest, directory] = args;
131
+ const ids = JSON.parse(readFileSync(manifest, 'utf8'));
132
+ if (!Array.isArray(ids) || !ids.length) throw new Error('The manifest must be a non-empty artboard ID/name array.');
133
+ const response = rpc('canvas-act', { compositionId, action: 'canvas.review-many', args: { ids, preview: 'files' } });
134
+ const reports = response.results?.[0]?.value?.reviews;
135
+ if (!Array.isArray(reports) || reports.length !== ids.length) throw new Error('Incomplete review-many result; do not claim all artboards passed.');
136
+ const saved = [];
137
+ for (let i = 0; i < reports.length; i++) saved.push(await saveReviewPixels(reports[i], resolve(directory, `${String(i + 1).padStart(2, '0')}-${String(ids[i]).replace(/[^a-z0-9_-]/gi, '-')}`)));
138
+ process.stdout.write(JSON.stringify(saved, null, 2) + '\n');
139
+ return;
140
+ }
47
141
  const legacy = argv.includes('--perceive');
48
142
  const args = argv.filter((a) => a !== '--perceive');
49
143
  if (args.length !== 3) throw new Error('Usage: node review.mjs <compositionId> <frame-id-or-name> <output-prefix> [--perceive]\n--perceive: older workers, preview only; pass a frame ID.');
@@ -60,9 +154,9 @@ export function main(argv = process.argv.slice(2)) {
60
154
  report = response.results?.[0]?.value;
61
155
  if (!report?.scope) throw new Error('canvas.review unavailable. Check canvas-catalog; use --perceive for an explicitly preview-only check on an older worker.');
62
156
  }
63
- process.stdout.write(JSON.stringify(saveReview(report, prefix), null, 2) + '\n');
157
+ process.stdout.write(JSON.stringify(await saveReviewPixels(report, prefix), null, 2) + '\n');
64
158
  }
65
159
 
66
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
67
- try { main(); } catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }
160
+ if (process.argv[1] && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
161
+ main().catch(error => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; });
68
162
  }
@@ -298,6 +298,11 @@ fewer elements, tighter type, one accent*. Every ornament added is a step away f
298
298
 
299
299
  ## Sources
300
300
 
301
+ The full shelf — about fifty books across grid, type, print production, colour, graphic form,
302
+ brand identity, information and interface design, each reduced to one rule — is in
303
+ [references.md](references.md). In Research pick two or three whose lens fits the brief, and name
304
+ the one behind each direction in its rationale.
305
+
301
306
  Josef Müller-Brockmann, *Grid Systems in Graphic Design* (1981) · Massimo Vignelli, *The
302
307
  Vignelli Canon* (semantics / syntactics / pragmatics; "Design is one") · Michael Bierut,
303
308
  *How to* (Pentagram; brief → rationale → trade-offs) · Paula Scher / Pentagram, The Public
@@ -0,0 +1,95 @@
1
+ # The design shelf
2
+
3
+ <!-- Generated from apps/cloudflare/src/agent/design-references.ts (the design-references and brand-references house skills).
4
+ Edit the list there, then run: WRITE_DESIGN_REFERENCES=1 npx vitest run src/__tests__/verify-design-references.test.ts -->
5
+
6
+ Books a studio designer works from, each reduced to one rule you can act on. The in-app
7
+ assistant gets the same list as the `design-references` and `brand-references` workspace skills.
8
+
9
+ ## How to use it
10
+
11
+ - In Research, pick two or three references whose lens fits the brief: a print piece or poster → grid, type, print; an identity → brand; a chart or report → information; a screen or flow → interface. Write what each one changes in this design.
12
+ - In Direction, name the reference behind each direction's idea in one line ("a Müller-Brockmann modular grid, type as the image after Scher").
13
+ - In Review, critique against the same rules you chose.
14
+ - These are paraphrased principles, not quotations. Never invent a quote or page number; cite author and title only.
15
+
16
+ ## Method — deciding before drawing
17
+
18
+ - Massimo Vignelli, *The Vignelli Canon* (2010): Answer semantics (what it means, to whom), syntactics (its visual grammar) and pragmatics (will they get it) before placing a node; few typefaces, a grid, discipline.
19
+ - Michael Bierut, *How to* (2015): Present brief → idea → trade-offs; the idea comes out of the problem, and the rationale is part of the deliverable.
20
+ - Paul Rand, *Thoughts on Design* (1947): Form and content are one: simplify until the idea is legible, and allow wit — play is a way of making it memorable.
21
+ - Bruno Munari, *Design as Art* (1966): Solve the problem with the simplest means; decoration that solves nothing is noise.
22
+ - Karl Gerstner, *Designing Programmes* (1964): Design the rules, not one layout: define the variables (size, position, colour) and generate the set from them.
23
+ - Dieter Rams, *Less but Better* (1995): As little design as possible: every element must earn its place by use; remove what does not.
24
+ - Alan Fletcher, *The Art of Looking Sideways* (2001): Look for the idea sideways — a visual pun or found connection beats a literal illustration of the brief.
25
+
26
+ ## Grid and layout
27
+
28
+ - Josef Müller-Brockmann, *Grid Systems in Graphic Design* (1981): Construct margins, columns, gutter and baseline first; every element lands on the grid; break it once, on purpose.
29
+ - Timothy Samara, *Making and Breaking the Grid* (2002): Pick the grid for the content (manuscript, column, modular, hierarchical); a deliberate break needs a grid to break.
30
+ - Kimberly Elam, *Geometry of Design* (2001): Place the focal point on a proportional line (golden section, root rectangles, thirds), not at dead centre.
31
+ - Beth Tondreau, *Layout Essentials* (2009): Grids by job: a single column for reading, multi-column for mixed content, modular for catalogues and dashboards.
32
+
33
+ ## Typography
34
+
35
+ - Robert Bringhurst, *The Elements of Typographic Style* (1992): Measure 45–75 characters, leading to suit the measure, honour the content: type exists to be read.
36
+ - Ellen Lupton, *Thinking with Type* (2004): Build hierarchy by changing one variable at a time — size, weight, colour or space — and track capitals open.
37
+ - Jan Tschichold, *The New Typography* (1928): Asymmetric, functional layout: hierarchy by size, weight and position, never ornament.
38
+ - Emil Ruder, *Typographie* (1967): White space is active form: the counter, the gap and the margin are composed like the type itself.
39
+ - Jost Hochuli, *Detail in Typography* (1987): Letter, word and line spacing decide readability: tighten display type, keep word spaces even, open the leading for long lines.
40
+ - Erik Spiekermann and E. M. Ginger, *Stop Stealing Sheep & Find Out How Type Works* (1993): A typeface is a voice: choose it for the message and audience, then set it with care before anything else.
41
+ - Ellen Lupton, *Type on Screen* (2014): On screen, type is motion and interaction too: sizes, scales and states are a system, not one setting.
42
+
43
+ ## Print and editorial production
44
+
45
+ - Jan Tschichold, *The Form of the Book*: Classical page proportions: inner margin smallest, foot margin largest; the text block sits high and toward the spine.
46
+ - Richard Hendel, *On Book Design* (1998): A book design serves the reading; decide trim, margins, type and running heads together, and stay invisible.
47
+ - Andrew Haslam, *Book Design* (2006): Plan the whole object — format, grid, pagination, binding — before designing a spread.
48
+ - Jan V. White, *Editing by Design* (1974): Editorial layout starts from what the story says; headline, picture and caption are one unit.
49
+ - International Paper, *Pocket Pal*: Production rules: CMYK for press, 300 dpi images at final size, 3 mm (0.125 in) bleed, a safe area inside the trim.
50
+ - Alan Pipes, *Production for Graphic Designers*: Specify for the press: paper stock, ink coverage, rich black for large areas only, overprint and trap small type.
51
+
52
+ ## Colour
53
+
54
+ - Josef Albers, *Interaction of Color* (1963): Colour is relative: judge it beside its neighbours on the real ground, never as a swatch alone.
55
+ - Johannes Itten, *The Art of Color* (1961): Choose the one contrast that leads (light–dark, warm–cool, complementary, saturation, extent) and let the rest support it.
56
+
57
+ ## Graphic form and visual perception
58
+
59
+ - Armin Hofmann, *Graphic Design Manual* (1965): Build with elementary contrasts — point, line, plane, black and white, large and small — before colour.
60
+ - Ellen Lupton and Jennifer Cole Phillips, *Graphic Design: The New Basics* (2008): Point, line, plane, rhythm, scale, texture, figure/ground and framing are variables; change one deliberately.
61
+ - Wucius Wong, *Principles of Two-Dimensional Design* (1972): Repetition, gradation and radiation give a field structure; an anomaly in that structure is the focal point.
62
+ - Rudolf Arnheim, *Art and Visual Perception* (1954): Visual weight balances on the frame's hidden axes; size, value, isolation and position all add weight.
63
+ - Robin Williams, *The Non-Designer's Design Book* (1994): Contrast, repetition, alignment, proximity: if two things differ, make them clearly different.
64
+ - Alex W. White, *The Elements of Graphic Design* (2002): Treat space as an element: emptiness gives the content its shape and its importance.
65
+ - Kenya Hara, *Designing Design* (2007): Emptiness invites the viewer in: leave room for the reader's imagination instead of filling the frame.
66
+ - Paula Scher, *Make It Bigger* (2002): Type can be the image: scale and crop a word until it carries the identity on its own.
67
+ - Philip B. Meggs, *A History of Graphic Design* (1983): Know the lineage of a style so a reference is chosen, not a pastiche of the first thing found.
68
+ - Adrian Frutiger, *Signs and Symbols*: A sign reads by its simplest form; test a mark small, at a distance and in one colour.
69
+
70
+ ## Brand and identity
71
+
72
+ - Alina Wheeler, *Designing Brand Identity* (2003): Identity is a process — research, strategy, design, touchpoints, asset management; design the touchpoints, not just the logo.
73
+ - Marty Neumeier, *The Brand Gap* (2003): A brand is the customer's gut feeling; close the gap between strategy and the look by testing what people actually perceive.
74
+ - Marty Neumeier, *Zag* (2006): When everyone zigs, zag: state what makes this the only one, then design to that difference.
75
+ - Wally Olins, *On Brand* (2003): A brand shows in product, environment, communication and behaviour; keep all four saying the same thing.
76
+ - Byron Sharp, *How Brands Grow* (2010): Growth comes from being easy to notice and buy: protect distinctive assets and stay consistent.
77
+ - Jenni Romaniuk, *Building Distinctive Brand Assets* (2018): An asset (colour, shape, character) works once it is famous and unique to the brand; use it everywhere, change it rarely.
78
+ - Tom Geismar, Ivan Chermayeff and Sagi Haviv, *Identify* (2011): A mark must be appropriate, distinctive and simple; it earns meaning through use over time.
79
+ - David Airey, *Logo Design Love* (2009): Test a logo at 16 px, in one colour and in reverse; if it needs detail or colour to survive, simplify it.
80
+ - Danne & Blackburn, *NASA Graphics Standards Manual* (1975): A standards manual fixes the mark, clear space, colours, type, grid and applications so anyone can apply the system.
81
+ - Unimark International (Vignelli, Noorda), *New York City Transit Authority Graphics Standards Manual* (1970): A system scales by rules: one typeface, fixed modules, colour coding and consistent placement.
82
+
83
+ ## Information design
84
+
85
+ - Edward R. Tufte, *The Visual Display of Quantitative Information* (1983): Maximise data-ink: remove chart junk, label directly, show the comparison; use small multiples for change.
86
+ - Edward R. Tufte, *Envisioning Information* (1990): Layer and separate: quiet the grid and background so the data reads first.
87
+ - Otto Neurath, *International Picture Language* (1936): Isotype: count with repeated identical pictograms, never by enlarging one.
88
+
89
+ ## Interface and product design
90
+
91
+ - Don Norman, *The Design of Everyday Things* (1988): Make the action visible: signifiers, feedback, mapping and constraints; a control must say how it is used.
92
+ - Steve Krug, *Don't Make Me Think* (2000): People scan, they do not read: obvious hierarchy, conventions, one clear next action per page.
93
+ - Adam Wathan and Steve Schoger, *Refactoring UI* (2018): Hierarchy by weight and colour before size, de-emphasise secondary text, use a spacing and shadow scale.
94
+ - Jenifer Tidwell, *Designing Interfaces* (2005): Use the known pattern for the job (navigation, forms, lists, dashboards) before inventing one.
95
+ - William Lidwell, Kritina Holden and Jill Butler, *Universal Principles of Design* (2003): Check the design against named principles (Fitts, Hick, proximity, progressive disclosure) and say which one a choice serves.
package/skills/index.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "SKILL.md"
6
6
  ],
7
- "sha": "2d33d2ecfb166fa6"
7
+ "sha": "6233057bd1a44b51"
8
8
  },
9
9
  {
10
10
  "name": "clearly-canvas",
@@ -17,7 +17,7 @@
17
17
  "project-plans.md",
18
18
  "review.mjs"
19
19
  ],
20
- "sha": "8b1890ca7588af6c"
20
+ "sha": "7d79406b37f611a8"
21
21
  },
22
22
  {
23
23
  "name": "agent-home",
@@ -36,9 +36,10 @@
36
36
  {
37
37
  "name": "design-craft",
38
38
  "files": [
39
- "SKILL.md"
39
+ "SKILL.md",
40
+ "references.md"
40
41
  ],
41
- "sha": "260c3a6967b08a8b"
42
+ "sha": "bf2f4fac6f57ea46"
42
43
  },
43
44
  {
44
45
  "name": "layout-systems",
@@ -52,7 +53,7 @@
52
53
  "files": [
53
54
  "SKILL.md"
54
55
  ],
55
- "sha": "50587f531bfab1c6"
56
+ "sha": "cd891499599ccb7b"
56
57
  },
57
58
  {
58
59
  "name": "pair-on-canvas",
@@ -81,6 +82,6 @@
81
82
  "files": [
82
83
  "SKILL.md"
83
84
  ],
84
- "sha": "c16e1b5d08dbd669"
85
+ "sha": "b3f47988cb9235b9"
85
86
  }
86
87
  ]