beehaven 0.8.22 → 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.
- package/dist/build-channel.js +2 -2
- package/dist/cli.js +70 -4
- package/dist/commands/agent-instructions.js +182 -0
- package/dist/commands/agent.js +5 -1
- package/dist/commands/canvas-image.js +57 -3
- package/dist/commands/daemon-cmd.js +32 -0
- package/dist/commands/workspace.js +16 -0
- package/dist/reserved-type.js +60 -0
- package/package.json +1 -1
- package/scripts/sync-skills.mjs +1 -0
- package/skills/README.md +20 -18
- package/skills/brand-identity/SKILL.md +248 -0
- package/skills/clearly-agent/SKILL.md +14 -4
- package/skills/clearly-canvas/SKILL.md +39 -2
- package/skills/clearly-canvas/authoring.md +16 -5
- package/skills/design-craft/SKILL.md +4 -0
- package/skills/index.json +10 -3
package/dist/build-channel.js
CHANGED
|
@@ -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 = '
|
|
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-
|
|
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
|
@@ -309,7 +309,8 @@ ${DEV_ONLY_HELP ? ` PROJECT — build your own Durable Object backend (HIVE_DE
|
|
|
309
309
|
app setup <name> Re-run a hive's setup script
|
|
310
310
|
install <pkg> Shorthand for "app install <pkg>"
|
|
311
311
|
skills install Teach every agent about Clearly —
|
|
312
|
-
writes the shipped SKILL.md files
|
|
312
|
+
writes the shipped SKILL.md files + the
|
|
313
|
+
AGENTS.md / GEMINI.md / CLAUDE.md block
|
|
313
314
|
skills <list|sync|remove> Manage skills paired with installed hives
|
|
314
315
|
|
|
315
316
|
Deep dive: beehaven help hives Mental model + install/connect/call loop
|
|
@@ -437,7 +438,9 @@ ${DEV_ONLY_HELP ? `
|
|
|
437
438
|
ACCOUNT
|
|
438
439
|
─────────────────────────────────────────────────────────────────────
|
|
439
440
|
|
|
440
|
-
login Sign in (browser) — primary flow
|
|
441
|
+
login Sign in (browser) — primary flow; also teaches
|
|
442
|
+
your agents (skills + AGENTS.md block;
|
|
443
|
+
--no-agent-setup to skip)
|
|
441
444
|
login --email <e> --password <p> Self-hosted email/password (set BEEHAVEN_API_URL)
|
|
442
445
|
logout Clear local credentials + keychain
|
|
443
446
|
signup Open sign-up page in browser
|
|
@@ -1299,7 +1302,25 @@ async function run() {
|
|
|
1299
1302
|
await shorthandCmd(rawVerb, process.argv.slice(3), {});
|
|
1300
1303
|
return;
|
|
1301
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
|
+
}
|
|
1302
1322
|
const { values, positionals } = parseArgs({
|
|
1323
|
+
args: argv,
|
|
1303
1324
|
allowPositionals: true,
|
|
1304
1325
|
options: {
|
|
1305
1326
|
project: { type: 'string' },
|
|
@@ -1492,6 +1513,7 @@ async function run() {
|
|
|
1492
1513
|
// Paired Claude Code skill install policy (see commands/skills.ts)
|
|
1493
1514
|
'with-skills': { type: 'boolean', default: false },
|
|
1494
1515
|
'no-skills': { type: 'boolean', default: false },
|
|
1516
|
+
'no-agent-setup': { type: 'boolean', default: false },
|
|
1495
1517
|
// ── mobile-install metadata for `beehaven teleport <build>.ipa --composition <id>`
|
|
1496
1518
|
// (see commands/workspace.ts — wantMobileInstall branch).
|
|
1497
1519
|
// Future: parse Info.plist / AndroidManifest.xml automatically
|
|
@@ -2945,11 +2967,24 @@ async function run() {
|
|
|
2945
2967
|
// omitted BEEHAVEN_HEADLESS=1, so the "restarted" daemon hit the
|
|
2946
2968
|
// parent-death watchdog and exited ~3s later — restart reported a PID and
|
|
2947
2969
|
// left you with nothing running. See spawnStandaloneDaemon's comment.
|
|
2948
|
-
const { spawnStandaloneDaemon } = await import('./commands/daemon-cmd.js');
|
|
2970
|
+
const { spawnStandaloneDaemon, waitForServingDaemon } = await import('./commands/daemon-cmd.js');
|
|
2949
2971
|
const { resolveRelayUrlForDaemon } = await import('./daemon.js');
|
|
2950
2972
|
const relayUrl = resolveRelayUrlForDaemon();
|
|
2951
2973
|
const pid = spawnStandaloneDaemon(values);
|
|
2952
|
-
|
|
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
|
+
}
|
|
2953
2988
|
console.log(` Relay: ${color.dim(relayUrl)}`);
|
|
2954
2989
|
break;
|
|
2955
2990
|
}
|
|
@@ -3303,6 +3338,31 @@ async function run() {
|
|
|
3303
3338
|
*/
|
|
3304
3339
|
console.log(` ${color.green('✓')} Connected — your new sign-in is live`);
|
|
3305
3340
|
console.log(` Background service ${wasRunning ? 'restarted' : 'started'} (PID ${pid})`);
|
|
3341
|
+
/**
|
|
3342
|
+
* ⚠⚠ TEACH THE AGENTS AS PART OF SIGNING IN (PPO-190). "Install the CLI, log in, and your
|
|
3343
|
+
* agents handle the rest" needs the agents to KNOW about the CLI — the skills plus the
|
|
3344
|
+
* instruction block in ~/.codex/AGENTS.md, ~/.gemini/GEMINI.md and a pointer in
|
|
3345
|
+
* ~/.claude/CLAUDE.md. That used to be a separate `beehaven skills install` the installer's
|
|
3346
|
+
* closing text asked for, and a step people skip is a step that did not happen.
|
|
3347
|
+
*
|
|
3348
|
+
* ⚠ Idempotent and non-fatal: unchanged files are not rewritten, symlinked files are left
|
|
3349
|
+
* alone, and a failure here never fails the sign-in. Opt out with `--no-agent-setup` or
|
|
3350
|
+
* BEEHAVEN_NO_AGENT_SETUP=1; undo with `beehaven skills uninstall`.
|
|
3351
|
+
*/
|
|
3352
|
+
if (!values['no-agent-setup'] && process.env.BEEHAVEN_NO_AGENT_SETUP !== '1') {
|
|
3353
|
+
try {
|
|
3354
|
+
const { installAgentSkills } = await import('./commands/agent-skills.js');
|
|
3355
|
+
const { installAgentInstructions } = await import('./commands/agent-instructions.js');
|
|
3356
|
+
const sk = installAgentSkills();
|
|
3357
|
+
const ins = installAgentInstructions();
|
|
3358
|
+
const changed = [...sk.steps, ...ins.steps].filter((x) => /=(written|updated)$/.test(x)).length;
|
|
3359
|
+
console.log(` ${color.green('✓')} Your agents know about Clearly${changed ? '' : color.dim(' (already set up)')}`);
|
|
3360
|
+
console.log(color.dim(' Skills + instructions for Claude Code, Codex, Gemini and pi · undo: beehaven skills uninstall'));
|
|
3361
|
+
}
|
|
3362
|
+
catch (err) {
|
|
3363
|
+
console.log(color.dim(` Agent setup skipped: ${err?.message ?? err} — run: beehaven skills install`));
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3306
3366
|
console.log('');
|
|
3307
3367
|
break;
|
|
3308
3368
|
}
|
|
@@ -3897,7 +3957,13 @@ complete -F _beehaven_completions beehaven`);
|
|
|
3897
3957
|
*/
|
|
3898
3958
|
if (sub === 'install' || sub === 'uninstall') {
|
|
3899
3959
|
const { installAgentSkills, removeAgentSkills, AGENT_SKILL_DIRS } = await import('./commands/agent-skills.js');
|
|
3960
|
+
const { installAgentInstructions, removeAgentInstructions } = await import('./commands/agent-instructions.js');
|
|
3900
3961
|
const r = sub === 'install' ? installAgentSkills() : removeAgentSkills();
|
|
3962
|
+
// ⚠ The instruction block travels with the skills (PPO-190) — a skill is only OFFERED;
|
|
3963
|
+
// the block is what makes an agent reach for `beehaven` at all.
|
|
3964
|
+
const ins = sub === 'install' ? installAgentInstructions() : removeAgentInstructions();
|
|
3965
|
+
r.steps.push(...ins.steps);
|
|
3966
|
+
r.warnings.push(...ins.warnings);
|
|
3901
3967
|
const wrote = r.steps.filter((x) => /=(written|updated)$/.test(x)).length;
|
|
3902
3968
|
const current = r.steps.filter((x) => x.endsWith('=current')).length;
|
|
3903
3969
|
const removed = r.steps.filter((x) => x.endsWith('=removed')).length;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-instructions — the "use the beehaven CLI" block in each agent's GLOBAL instruction file.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ THE MISSING HALF OF "install the CLI, log in, and your agents handle the rest" (PPO-190).
|
|
5
|
+
* `skills install` put SKILL.md files on disk, but a skill is only OFFERED to a model when its
|
|
6
|
+
* description matches. The instruction block is what makes an agent reach for `beehaven` at all
|
|
7
|
+
* when the user says "put this on my board". The Mac app has written it on every launch
|
|
8
|
+
* (`apps/electron/electron/agent-skills-installer.ts`); the CLI never did, so a CLI-only user's
|
|
9
|
+
* Codex and Gemini had skills they never used.
|
|
10
|
+
*
|
|
11
|
+
* ⚠⚠ BYTE-IDENTICAL TO THE MAC APP'S BLOCK, MARKER LINE INCLUDED. Both writers splice between the
|
|
12
|
+
* same BLOCK_START / BLOCK_END. If their text differed by one character, a machine with both would
|
|
13
|
+
* rewrite the file on every app launch and every CLI install, each undoing the other. One text
|
|
14
|
+
* means both converge and either uninstaller removes it. `agentInstructionParity.test.mjs` in the
|
|
15
|
+
* root repo pins the two copies together.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ CLAUDE GETS THE SHORT POINTER — `~/.claude/CLAUDE.md` is prepended to every Claude Code
|
|
18
|
+
* session in every repository, so it carries a pointer to the skills, not a manual. Same rule as
|
|
19
|
+
* the Mac app; see its note on `AGENT_INSTRUCTION_FILES`.
|
|
20
|
+
*
|
|
21
|
+
* ⚠ ONLY OUR BLOCK IS EVER TOUCHED. Text around it is preserved byte for byte; a file with no
|
|
22
|
+
* block gets one appended; removal deletes the block and nothing else.
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import os from 'os';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
const MANAGED_MARKER = 'managed-by: clearly-mac-app';
|
|
28
|
+
export const BLOCK_START = '<!-- clearly:beehaven-cli:start -->';
|
|
29
|
+
export const BLOCK_END = '<!-- clearly:beehaven-cli:end -->';
|
|
30
|
+
/** Resolved at call time so tests can point HOME at a temp dir. */
|
|
31
|
+
export function agentInstructionFiles(home = os.homedir()) {
|
|
32
|
+
return [
|
|
33
|
+
{ agent: 'codex', file: path.join(home, '.codex', 'AGENTS.md') },
|
|
34
|
+
{ agent: 'gemini', file: path.join(home, '.gemini', 'GEMINI.md') },
|
|
35
|
+
{ agent: 'claude', file: path.join(home, '.claude', 'CLAUDE.md') },
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
/** The managed block appended to AGENTS.md / GEMINI.md. Keep identical to the Mac app's. */
|
|
39
|
+
export function instructionBlock() {
|
|
40
|
+
return [
|
|
41
|
+
BLOCK_START,
|
|
42
|
+
`<!-- ${MANAGED_MARKER}. Edits inside this block are overwritten on app launch; edit outside it. -->`,
|
|
43
|
+
'',
|
|
44
|
+
'## Clearly — drive it with the `beehaven` CLI',
|
|
45
|
+
'',
|
|
46
|
+
'Clearly (clearly.sh) is a workspace — documents, projects, boards, tickets and a design',
|
|
47
|
+
'canvas. Reach it from the shell with the `beehaven` CLI, NOT an HTTP API you construct.',
|
|
48
|
+
'',
|
|
49
|
+
'⚠ SIGN IN FIRST. Every call is attributed to an agent identity, and without one the CLI',
|
|
50
|
+
'refuses. `login` MINTS the name if it does not exist yet, and binds this terminal:',
|
|
51
|
+
'',
|
|
52
|
+
'```sh',
|
|
53
|
+
'beehaven agent login <your-name> # required — mints + binds, once per session',
|
|
54
|
+
'beehaven connect home # target the personal workspace',
|
|
55
|
+
'beehaven actions # list every action available there',
|
|
56
|
+
'beehaven call <action> \'{...}\'',
|
|
57
|
+
'```',
|
|
58
|
+
'',
|
|
59
|
+
'Read the full skills before driving it — they carry the exact verbs and node shapes:',
|
|
60
|
+
'',
|
|
61
|
+
'- `~/.<agent>/skills/clearly-agent/SKILL.md` — **read this first**: identity, sessions, your brief',
|
|
62
|
+
'- `~/.<agent>/skills/beehaven-cli/SKILL.md` — connect / call / sql / logs, the daemon',
|
|
63
|
+
'- `~/.<agent>/skills/clearly-workspace/SKILL.md` — audit what a workspace holds and who made it',
|
|
64
|
+
'- `~/.<agent>/skills/clearly-canvas/SKILL.md` — build + perceive a canvas, comm blocks, code diffs',
|
|
65
|
+
'- `~/.<agent>/skills/design-craft/SKILL.md` — **load before any visual work**: grid, type, colour, the studio pass',
|
|
66
|
+
'- `~/.<agent>/skills/layout-systems/SKILL.md` — grids and systems across many artboards',
|
|
67
|
+
'- `~/.<agent>/skills/pair-on-canvas/SKILL.md` — drive a coding task from a spec on the canvas',
|
|
68
|
+
'- `~/.<agent>/skills/codebase-map/SKILL.md` — draw a repo’s architecture on the canvas',
|
|
69
|
+
'',
|
|
70
|
+
'If a command reports the daemon is unreachable, `beehaven login` re-auths and',
|
|
71
|
+
'`beehaven start --headless` starts a daemon when the Mac app is not running.',
|
|
72
|
+
BLOCK_END,
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
/** The short form, for `~/.claude/CLAUDE.md`. Keep identical to the Mac app's `claudeBlock()`. */
|
|
76
|
+
export function claudeBlock() {
|
|
77
|
+
return [
|
|
78
|
+
BLOCK_START,
|
|
79
|
+
`<!-- ${MANAGED_MARKER}. Edits inside this block are overwritten on app launch; edit outside it. -->`,
|
|
80
|
+
'',
|
|
81
|
+
'**Clearly workspace** — drive it from the shell with the `beehaven` CLI (documents, projects,',
|
|
82
|
+
'boards, tickets, canvas). Sign in once per session first: `beehaven agent login <name>` —',
|
|
83
|
+
'it mints the identity if new. Full detail is in the skills, `clearly-agent` first.',
|
|
84
|
+
BLOCK_END,
|
|
85
|
+
].join('\n');
|
|
86
|
+
}
|
|
87
|
+
/** Insert or replace our block, leaving everything around it untouched. Pure. */
|
|
88
|
+
export function upsertBlock(existing, block) {
|
|
89
|
+
const start = existing.indexOf(BLOCK_START);
|
|
90
|
+
const end = existing.indexOf(BLOCK_END);
|
|
91
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
92
|
+
return existing.slice(0, start) + block + existing.slice(end + BLOCK_END.length);
|
|
93
|
+
}
|
|
94
|
+
return existing.trim() ? `${existing.trimEnd()}\n\n${block}\n` : `${block}\n`;
|
|
95
|
+
}
|
|
96
|
+
/** Remove our block (and the blank line it was appended after). Pure. */
|
|
97
|
+
export function stripBlock(existing) {
|
|
98
|
+
const start = existing.indexOf(BLOCK_START);
|
|
99
|
+
const end = existing.indexOf(BLOCK_END);
|
|
100
|
+
if (start === -1 || end === -1 || end < start)
|
|
101
|
+
return existing;
|
|
102
|
+
const before = existing.slice(0, start).replace(/\n{1,2}$/, '\n');
|
|
103
|
+
const after = existing.slice(end + BLOCK_END.length).replace(/^\n/, '');
|
|
104
|
+
const out = (before + after);
|
|
105
|
+
return out.trim() ? out : '';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Write the block into every agent's global instruction file. Idempotent: an unchanged file is
|
|
109
|
+
* reported `=current` and not rewritten.
|
|
110
|
+
*
|
|
111
|
+
* ⚠ A SYMLINKED INSTRUCTION FILE IS LEFT ALONE. People keep dotfiles in a repo and link them in;
|
|
112
|
+
* writing through the link would silently edit their checkout. Same rule as the skills installer.
|
|
113
|
+
*/
|
|
114
|
+
export function installAgentInstructions(home = os.homedir()) {
|
|
115
|
+
const steps = [];
|
|
116
|
+
const warnings = [];
|
|
117
|
+
for (const { agent, file } of agentInstructionFiles(home)) {
|
|
118
|
+
try {
|
|
119
|
+
/**
|
|
120
|
+
* ⚠ ONLY FOR AGENTS THAT ARE INSTALLED. Creating `~/.gemini/GEMINI.md` for someone without
|
|
121
|
+
* Gemini invents an agent: the skills installer keys on that same directory existing, so
|
|
122
|
+
* the NEXT run would install skills for it too. Same rule as `installAgentSkills`.
|
|
123
|
+
*/
|
|
124
|
+
if (!fs.existsSync(path.dirname(file))) {
|
|
125
|
+
steps.push(`${agent}/instructions=agent-absent`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (isSymlink(file)) {
|
|
129
|
+
steps.push(`${agent}/instructions=symlink-skipped`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
let existing = '';
|
|
133
|
+
try {
|
|
134
|
+
existing = fs.readFileSync(file, 'utf8');
|
|
135
|
+
}
|
|
136
|
+
catch { /* first run */ }
|
|
137
|
+
const next = upsertBlock(existing, agent === 'claude' ? claudeBlock() : instructionBlock());
|
|
138
|
+
if (next === existing) {
|
|
139
|
+
steps.push(`${agent}/instructions=current`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
fs.writeFileSync(file, next, { mode: 0o644 });
|
|
143
|
+
steps.push(`${agent}/instructions=${existing ? 'updated' : 'written'}`);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
warnings.push(`${agent}/instructions: ${e?.message ?? String(e)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { steps, warnings };
|
|
150
|
+
}
|
|
151
|
+
/** Remove only our block. A file that becomes empty is deleted only if we created all of it. */
|
|
152
|
+
export function removeAgentInstructions(home = os.homedir()) {
|
|
153
|
+
const steps = [];
|
|
154
|
+
const warnings = [];
|
|
155
|
+
for (const { agent, file } of agentInstructionFiles(home)) {
|
|
156
|
+
try {
|
|
157
|
+
if (!fs.existsSync(file) || isSymlink(file))
|
|
158
|
+
continue;
|
|
159
|
+
const existing = fs.readFileSync(file, 'utf8');
|
|
160
|
+
const next = stripBlock(existing);
|
|
161
|
+
if (next === existing)
|
|
162
|
+
continue;
|
|
163
|
+
if (next)
|
|
164
|
+
fs.writeFileSync(file, next);
|
|
165
|
+
else
|
|
166
|
+
fs.unlinkSync(file);
|
|
167
|
+
steps.push(`${agent}/instructions=removed`);
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
warnings.push(`${agent}/instructions: ${e?.message ?? String(e)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { steps, warnings };
|
|
174
|
+
}
|
|
175
|
+
function isSymlink(file) {
|
|
176
|
+
try {
|
|
177
|
+
return fs.lstatSync(file).isSymbolicLink();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/dist/commands/agent.js
CHANGED
|
@@ -845,7 +845,11 @@ async function agentLogin(args, values) {
|
|
|
845
845
|
console.error(` ${yellow('⚠')} Signed in, but this workspace refused ${cyan(name)}: ${outcome.reason}`);
|
|
846
846
|
if (/revoked|not active|unknown agent|not admitted/i.test(`${outcome.scope} ${outcome.reason}`)) {
|
|
847
847
|
console.error(dim(` The agent is archived or not admitted here. Restore it in Settings → Agent access,`));
|
|
848
|
-
|
|
848
|
+
// ⚠ The workspace names the live identity in its refusal (PPO-190). The old advice —
|
|
849
|
+
// `beehaven call agent-list` — ran AS this refused identity and was refused in turn.
|
|
850
|
+
if (!/Log in as one:/.test(outcome.reason)) {
|
|
851
|
+
console.error(dim(` or log in as an active agent: beehaven agent login <name> (the Settings → Agent access page lists them).`));
|
|
852
|
+
}
|
|
849
853
|
}
|
|
850
854
|
console.error(dim(` Calls from this session run as ${name} and will be refused — they will NOT fall back to another agent.`));
|
|
851
855
|
process.exitCode = 1;
|
|
@@ -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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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':
|
|
@@ -860,6 +860,22 @@ export async function cmdCall(action, rawInput, values) {
|
|
|
860
860
|
input = { ...input, action: canonical, name: canonical };
|
|
861
861
|
}
|
|
862
862
|
}
|
|
863
|
+
// ⚠ `type` IS THE WIRE'S ACTION NAME (PPO-190): a top-level `type` would be overwritten by the
|
|
864
|
+
// daemon's `{ ...input, type: action }` and lost with `ok: true`. Move it to the verb's own field
|
|
865
|
+
// or refuse with the right spelling — never drop it in silence. See reserved-type.ts.
|
|
866
|
+
{
|
|
867
|
+
const { reconcileReservedType } = await import('../reserved-type.js');
|
|
868
|
+
const r = reconcileReservedType(action, input);
|
|
869
|
+
if (r.kind === 'refused') {
|
|
870
|
+
console.error(` ${r.error}`);
|
|
871
|
+
process.exit(1);
|
|
872
|
+
}
|
|
873
|
+
if (r.kind === 'moved') {
|
|
874
|
+
input = r.input;
|
|
875
|
+
if (!values.json)
|
|
876
|
+
console.error(` · ${r.note}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
863
879
|
// Confirm spend BEFORE resolving a building or touching the daemon — a cancelled call must
|
|
864
880
|
// cost nothing and behave identically whether the input came from argv, a file, or stdin.
|
|
865
881
|
// It runs after the parse because the gate is input-aware: `engine:'handroll'` and any call
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reserved-type — `type` is the WIRE'S action name, so a top-level `type` in a call's input is lost.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ SILENT DATA LOSS (PPO-190). The daemon sends `{ ...input, type: action }`. An agent that calls
|
|
5
|
+
* `ticket-create '{"title":"…","type":"bug"}'` gets `ok: true` and a ticket that is NOT a bug: its
|
|
6
|
+
* `type` was overwritten by `"ticket-create"` before the workspace ever saw it. AGENTS.md has
|
|
7
|
+
* carried "use `issueType`, not `type`" as a warning, which is a rule every new agent has to be
|
|
8
|
+
* told and every one of them breaks once.
|
|
9
|
+
*
|
|
10
|
+
* The verbs already accept a non-colliding spelling (`issueType`, `linkType`, `proposalType`,
|
|
11
|
+
* `kind`). This moves `type` onto that spelling when the verb has one, and refuses with the right
|
|
12
|
+
* spelling when it does not — either way, nothing is dropped in silence.
|
|
13
|
+
*/
|
|
14
|
+
/** Verb → the field the workspace reads instead of `type`. */
|
|
15
|
+
export const TYPE_ALIASES = {
|
|
16
|
+
// Each entry is a verb whose handler reads this field (verified against apps/cloudflare/src).
|
|
17
|
+
'ticket-create': 'issueType',
|
|
18
|
+
'ticket-update': 'issueType',
|
|
19
|
+
'ticket-link-create': 'linkType',
|
|
20
|
+
'proposal-create': 'proposalType',
|
|
21
|
+
'composition-create': 'kind',
|
|
22
|
+
'record-thought': 'kind',
|
|
23
|
+
'thought-record': 'kind',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Reconcile a top-level `type` with the wire's use of the name. Pure.
|
|
27
|
+
*
|
|
28
|
+
* ⚠ A `type` EQUAL TO THE ACTION is left alone — that is a caller spelling the action twice, which
|
|
29
|
+
* the wire already agrees with. Nested `type`s (canvas nodes, `updates.type`) never collide and are
|
|
30
|
+
* never inspected.
|
|
31
|
+
*/
|
|
32
|
+
export function reconcileReservedType(action, input) {
|
|
33
|
+
if (!input || typeof input !== 'object' || !Object.prototype.hasOwnProperty.call(input, 'type'))
|
|
34
|
+
return { kind: 'none' };
|
|
35
|
+
const value = input.type;
|
|
36
|
+
if (value === undefined || value === action)
|
|
37
|
+
return { kind: 'none' };
|
|
38
|
+
const field = TYPE_ALIASES[action];
|
|
39
|
+
if (field) {
|
|
40
|
+
const { type: _dropped, ...rest } = input;
|
|
41
|
+
if (rest[field] !== undefined && rest[field] !== value) {
|
|
42
|
+
return {
|
|
43
|
+
kind: 'refused',
|
|
44
|
+
error: `both "type" (${JSON.stringify(value)}) and "${field}" (${JSON.stringify(rest[field])}) were given — `
|
|
45
|
+
+ `"type" is the action name on the wire and would be dropped; pass only "${field}".`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
kind: 'moved',
|
|
50
|
+
field,
|
|
51
|
+
input: { ...rest, [field]: value },
|
|
52
|
+
note: `"type" is reserved for the action name on the wire — sent as "${field}": ${JSON.stringify(value)}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
kind: 'refused',
|
|
57
|
+
error: `"type" is reserved for the action name on the wire, so ${action} would never see ${JSON.stringify(value)}. `
|
|
58
|
+
+ `Use the verb's own field (see: beehaven describe ${action}).`,
|
|
59
|
+
};
|
|
60
|
+
}
|
package/package.json
CHANGED
package/scripts/sync-skills.mjs
CHANGED
|
@@ -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
|
-
#
|
|
1
|
+
# Skills shipped with the CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
8
|
-
with the CLI itself, not with any specific hive. Currently:
|
|
6
|
+
## Where each skill comes from
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
cp -R packages/cli/skills/claude-shopper ~/.claude/skills/beehaven-shopper
|
|
20
|
-
```
|
|
20
|
+
## Rules
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
@@ -211,7 +220,8 @@ ask the owner.
|
|
|
211
220
|
`beehaven call agent-list '{}'` shows the live roster; log in as one of those.
|
|
212
221
|
- ⚠ **`workspace-sql` takes `query`**, not `sql`. The wrong key is silently dropped and you get the
|
|
213
222
|
SCHEMA back — `ok: true`, a confident list of tables, not your answer.
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
-
|
|
217
|
-
|
|
223
|
+
- **`document-create` takes `markdown` or `html`.** `content`, `body` and `text` are accepted as
|
|
224
|
+
markdown aliases, but prefer `markdown`.
|
|
225
|
+
- **`ticket-create` / `ticket-update` take `issueType` and `body`.** The wire uses `type` as the action
|
|
226
|
+
name, so the CLI moves a top-level `type` to `issueType` for you and refuses it on verbs with no
|
|
227
|
+
such field. `description` is accepted as `body`.
|
|
@@ -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
|
-
|
|
121
|
-
|
|
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
|
|
63
|
-
|
|
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 `
|
|
128
|
-
omit both `--x`/`--y` to use canvas
|
|
129
|
-
the image's native dimensions
|
|
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": "
|
|
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": "
|
|
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": "
|
|
84
|
+
"sha": "9e94e53b90ded575"
|
|
78
85
|
}
|
|
79
86
|
]
|