beehaven 0.8.35 → 0.8.37
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/agent-sessions.js +9 -6
- package/dist/agents-md-text.js +21 -1
- package/dist/browser-login.js +94 -0
- package/dist/build-channel.js +2 -2
- package/dist/call-brief.js +157 -0
- package/dist/claude-bridge.js +4 -1
- package/dist/cli.js +100 -50
- package/dist/commands/agent-instructions.js +38 -30
- package/dist/commands/agents-md.js +11 -5
- package/dist/commands/help.js +39 -5
- package/dist/commands/mcp-token.js +113 -0
- package/dist/commands/workspace.js +31 -2
- package/dist/daemon-client.js +7 -2
- package/dist/ipc-client.js +4 -1
- package/dist/ipc-server.js +4 -1
- package/dist/reconnect-backoff.js +78 -0
- package/dist/relay.js +42 -27
- package/dist/utf8-stream.js +23 -0
- package/package.json +1 -1
- package/scripts/sync-skills.mjs +1 -0
- package/skills/clearly-artifacts/SKILL.md +143 -0
- package/skills/clearly-canvas/SKILL.md +16 -8
- package/skills/clearly-canvas/authoring.md +40 -17
- package/skills/clearly-canvas/review.mjs +25 -4
- package/skills/index.json +8 -1
package/dist/relay.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join, dirname, resolve, sep } from 'path';
|
|
|
10
10
|
import { execSync, execFileSync } from 'child_process';
|
|
11
11
|
import WebSocket from 'ws';
|
|
12
12
|
import { logger } from './logger.js';
|
|
13
|
+
import { ReconnectBackoff } from './reconnect-backoff.js';
|
|
13
14
|
import { TerminalBlockParser } from './terminal/block-parser.js';
|
|
14
15
|
import { writeIntegration, cleanupIntegration } from './terminal/shell-integration.js';
|
|
15
16
|
import { transformPath } from './daemon-path-transform.js';
|
|
@@ -73,11 +74,16 @@ export class Relay {
|
|
|
73
74
|
// ── WebSocket ─────────────────────────────────────────────────────────
|
|
74
75
|
ws = null;
|
|
75
76
|
wsConnecting = false;
|
|
76
|
-
|
|
77
|
+
/** ⚠ PPO-532: resets only after a connection stays open 60 s, not when one opens — see
|
|
78
|
+
* reconnect-backoff.ts. `reconnectAttempts` (read by the daemon's status.json) reads it. */
|
|
79
|
+
backoff = new ReconnectBackoff();
|
|
80
|
+
get reconnectAttempts() {
|
|
81
|
+
return this.backoff.attempts;
|
|
82
|
+
}
|
|
77
83
|
reconnectTimer = null;
|
|
78
84
|
/** Watchdog timer + the moment we first noticed we were disconnected. If the
|
|
79
85
|
* relay stays down past the threshold, the watchdog forces a HARD reconnect
|
|
80
|
-
* (terminate any zombie socket +
|
|
86
|
+
* (terminate any zombie socket + redial now; PPO-532 keeps the backoff). This is the
|
|
81
87
|
* self-heal a HEADLESS / orphaned daemon needs — it has no Mac-app tray
|
|
82
88
|
* parent to restart it, so a wedged WS (e.g. a half-open socket after system
|
|
83
89
|
* sleep) would otherwise leave it "stuck reconnecting" forever. */
|
|
@@ -388,6 +394,8 @@ export class Relay {
|
|
|
388
394
|
return;
|
|
389
395
|
// Re-enable auto-reconnect in case a previous stop() set it.
|
|
390
396
|
this.stopped = false;
|
|
397
|
+
// An explicit start (daemon boot, sign-in) is not a retry: no inherited backoff.
|
|
398
|
+
this.backoff.reset();
|
|
391
399
|
// Validate endpoint against whitelist
|
|
392
400
|
try {
|
|
393
401
|
const endpointHost = new URL(this.config.endpoint.replace('wss://', 'https://').replace('ws://', 'http://')).hostname;
|
|
@@ -534,6 +542,8 @@ export class Relay {
|
|
|
534
542
|
params.set('deviceId', String(this.deviceId).slice(0, 64));
|
|
535
543
|
const url = `${this.wsBase()}/my/workspace/ws?${params.toString()}`;
|
|
536
544
|
const ws = new WebSocket(url);
|
|
545
|
+
// This socket's healthy clock (PPO-532); null until it opens.
|
|
546
|
+
let openedAt = null;
|
|
537
547
|
// Wait for open or first error/close before resolving
|
|
538
548
|
await new Promise((resolve) => {
|
|
539
549
|
const done = () => {
|
|
@@ -543,7 +553,10 @@ export class Relay {
|
|
|
543
553
|
this.ws = ws;
|
|
544
554
|
this.wsConnecting = false;
|
|
545
555
|
this.connected = true;
|
|
546
|
-
|
|
556
|
+
// ⚠ PPO-532: opening is not recovering. A workspace that runs out of memory while
|
|
557
|
+
// hydrating accepts the socket and drops it seconds later; resetting here made every
|
|
558
|
+
// retry the first (~1 s), a reconnect loop. The backoff resets after 60 s open.
|
|
559
|
+
openedAt = this.backoff.opened();
|
|
547
560
|
console.log(`[relay] WebSocket connected to ${this.config.endpoint}`);
|
|
548
561
|
// Notify subscribers that the WebSocket is open so they
|
|
549
562
|
// can pull fresh state. Wrapped because the callback
|
|
@@ -764,12 +777,17 @@ export class Relay {
|
|
|
764
777
|
this.heartbeatInterval = null;
|
|
765
778
|
this.wsPingInterval = null;
|
|
766
779
|
this.wsWakeDetector = null;
|
|
767
|
-
// Reject pending requests
|
|
768
|
-
|
|
780
|
+
// Reject pending requests.
|
|
781
|
+
// ⚠ CAN2-175 — SAY THE OUTCOME IS UNKNOWN, as the timeout does. A dropped socket loses the REPLY,
|
|
782
|
+
// not the request: a canvas batch carries on and lands on the workspace. A bare "WebSocket closed"
|
|
783
|
+
// read as "nothing happened", the caller resent at once, and could not tell which state was real.
|
|
784
|
+
for (const [requestId, pending] of this.pendingRequests) {
|
|
769
785
|
clearTimeout(pending.timer);
|
|
770
|
-
pending.reject(new Error(
|
|
786
|
+
pending.reject(new Error(`WebSocket closed (code ${code ?? '?'}) before the reply (action=${pending.action ?? '?'}, requestId=${requestId}); outcome unknown: `
|
|
787
|
+
+ 'the request may still be running on the workspace or may already have landed. Read its state before resending.'));
|
|
771
788
|
}
|
|
772
789
|
this.pendingRequests.clear();
|
|
790
|
+
this.backoff.closed(openedAt);
|
|
773
791
|
// Always schedule reconnect while we're still configured and not
|
|
774
792
|
// stopped. Previous guard `if (wasConnected)` broke the chain: if
|
|
775
793
|
// a reconnect attempt failed to open, the new ws's close handler
|
|
@@ -1084,12 +1102,12 @@ export class Relay {
|
|
|
1084
1102
|
return reject(new Error('WebSocket not connected'));
|
|
1085
1103
|
}
|
|
1086
1104
|
const requestId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
1105
|
+
const action = msg.type === 'agent-invoke' ? msg.action : msg.type;
|
|
1087
1106
|
const timer = setTimeout(() => {
|
|
1088
1107
|
this.pendingRequests.delete(requestId);
|
|
1089
|
-
const action = msg.type === 'agent-invoke' ? msg.action : msg.type;
|
|
1090
1108
|
reject(new Error(`Request timeout after ${timeout}ms (action=${action}, requestId=${requestId}); outcome unknown`));
|
|
1091
1109
|
}, timeout);
|
|
1092
|
-
this.pendingRequests.set(requestId, { resolve, reject, timer });
|
|
1110
|
+
this.pendingRequests.set(requestId, { resolve, reject, timer, action });
|
|
1093
1111
|
// Send both rid (legacy) and requestId (workspace actions) for compatibility
|
|
1094
1112
|
if (!this.wsSend({ ...msg, rid: requestId, requestId })) {
|
|
1095
1113
|
clearTimeout(timer);
|
|
@@ -1100,28 +1118,21 @@ export class Relay {
|
|
|
1100
1118
|
}
|
|
1101
1119
|
// ── Reconnection ──────────────────────────────────────────────────────
|
|
1102
1120
|
/**
|
|
1103
|
-
* Reconnect with exponential backoff + jitter.
|
|
1121
|
+
* Reconnect with exponential backoff + FULL jitter (PPO-532, reconnect-backoff.ts).
|
|
1104
1122
|
*
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1108
|
-
* keeps
|
|
1123
|
+
* Delay: uniform over [0, min(30s, 1s·2^attempt)). The ±25% jitter this replaced kept the
|
|
1124
|
+
* daemon socket and its agent sockets — which drop together — reconnecting together. The
|
|
1125
|
+
* attempt count resets only once a connection has stayed open 60 s, so a socket that opens
|
|
1126
|
+
* and dies (an out-of-memory workspace) keeps backing off instead of retrying every second.
|
|
1109
1127
|
*
|
|
1110
|
-
* Single-flight: a reconnect already scheduled is a no-op. The timer
|
|
1111
|
-
*
|
|
1112
|
-
* stop().
|
|
1128
|
+
* Single-flight: a reconnect already scheduled is a no-op. The timer is cleared by stop()
|
|
1129
|
+
* and forceReconnect().
|
|
1113
1130
|
*/
|
|
1114
1131
|
scheduleReconnect() {
|
|
1115
1132
|
if (this.reconnectTimer)
|
|
1116
1133
|
return;
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
// floor at the exponential value so we never reconnect FASTER
|
|
1120
|
-
// than the backoff intends, only slower.
|
|
1121
|
-
const jitter = Math.random() * 0.25 * baseDelay;
|
|
1122
|
-
const delay = Math.round(baseDelay + jitter);
|
|
1123
|
-
this.reconnectAttempts++;
|
|
1124
|
-
console.log(`[relay] Reconnecting in ${(delay / 1000).toFixed(1)}s (attempt ${this.reconnectAttempts})`);
|
|
1134
|
+
const delay = this.backoff.next();
|
|
1135
|
+
console.log(`[relay] Reconnecting in ${(delay / 1000).toFixed(1)}s (attempt ${this.backoff.attempts})`);
|
|
1125
1136
|
this.reconnectTimer = setTimeout(() => {
|
|
1126
1137
|
this.reconnectTimer = null;
|
|
1127
1138
|
if (this.useWebSocket) {
|
|
@@ -1151,6 +1162,10 @@ export class Relay {
|
|
|
1151
1162
|
this.disconnectedSince = Date.now();
|
|
1152
1163
|
return;
|
|
1153
1164
|
}
|
|
1165
|
+
// ⚠ PPO-532: a scheduled retry means the backoff chain is alive (≤30 s away). Forcing
|
|
1166
|
+
// past it turned a workspace that keeps dropping new sockets back into rapid redials.
|
|
1167
|
+
if (this.reconnectTimer)
|
|
1168
|
+
return;
|
|
1154
1169
|
if (Date.now() - this.disconnectedSince > maxDownMs) {
|
|
1155
1170
|
console.warn(`[relay] Watchdog: relay down ${Math.round((Date.now() - this.disconnectedSince) / 1000)}s — forcing hard reconnect`);
|
|
1156
1171
|
this.disconnectedSince = Date.now();
|
|
@@ -1159,7 +1174,8 @@ export class Relay {
|
|
|
1159
1174
|
}, 30_000);
|
|
1160
1175
|
}
|
|
1161
1176
|
/** Hard reconnect: drop any (possibly zombie) socket, clear pending reconnect
|
|
1162
|
-
* state,
|
|
1177
|
+
* state, then redial at once. The attempt count is kept (PPO-532): if this dial
|
|
1178
|
+
* fails too, the next retry continues the backoff instead of restarting at 1 s. */
|
|
1163
1179
|
forceReconnect() {
|
|
1164
1180
|
try {
|
|
1165
1181
|
this.ws?.terminate();
|
|
@@ -1172,7 +1188,6 @@ export class Relay {
|
|
|
1172
1188
|
clearTimeout(this.reconnectTimer);
|
|
1173
1189
|
this.reconnectTimer = null;
|
|
1174
1190
|
}
|
|
1175
|
-
this.reconnectAttempts = 0;
|
|
1176
1191
|
if (this.useWebSocket)
|
|
1177
1192
|
this.connectWs();
|
|
1178
1193
|
else
|
|
@@ -1182,7 +1197,7 @@ export class Relay {
|
|
|
1182
1197
|
const ok = await this.sendHeartbeatHttp();
|
|
1183
1198
|
if (ok) {
|
|
1184
1199
|
this.connected = true;
|
|
1185
|
-
this.
|
|
1200
|
+
this.backoff.reset(); // legacy HTTP has no socket to watch: a good heartbeat is recovery
|
|
1186
1201
|
console.log(`[relay] Reconnected (tier: ${this.tier})`);
|
|
1187
1202
|
if (this.heartbeatInterval)
|
|
1188
1203
|
clearInterval(this.heartbeatInterval);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utf8-stream — decode a socket's bytes ONCE PER CHARACTER, not once per chunk (CA-63).
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ WHY THIS EXISTS (staging, 2026-10-23). A 402-step `beehaven call canvas-act @build.json` (90,714
|
|
5
|
+
* bytes, the file holding the correct C2 B7) stored ONE text node's middle dot as "��" — the
|
|
6
|
+
* same node, the same character, on every resend — while dozens of identical dots elsewhere in the batch
|
|
7
|
+
* were stored intact. Every IPC reader did `buffer += chunk.toString()`. A local socket hands the reader
|
|
8
|
+
* the stream in pieces (8 KB on macOS, 64 KB on Linux), so any multi-byte character that straddles a
|
|
9
|
+
* piece boundary was decoded as two halves, each half an invalid sequence, each a U+FFFD. Which character
|
|
10
|
+
* lands on a boundary depends only on the bytes before it, so the same payload broke the same character
|
|
11
|
+
* every time. The CLI → daemon direction corrupted what was STORED; the daemon → CLI direction corrupted
|
|
12
|
+
* what a reader was SHOWN (a long scene read could carry U+FFFD the canvas does not have).
|
|
13
|
+
*
|
|
14
|
+
* A `StringDecoder` holds an incomplete trailing sequence until its remaining bytes arrive, so the text is
|
|
15
|
+
* exactly what one decode of the whole stream would give. One decoder per stream (it carries state), and
|
|
16
|
+
* never shared between two sockets.
|
|
17
|
+
*/
|
|
18
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
19
|
+
/** A stateful UTF-8 decoder for ONE byte stream. A string chunk (a socket with an encoding set) passes through. */
|
|
20
|
+
export function utf8Stream() {
|
|
21
|
+
const decoder = new StringDecoder('utf8');
|
|
22
|
+
return (chunk) => (typeof chunk === 'string' ? chunk : decoder.write(chunk));
|
|
23
|
+
}
|
package/package.json
CHANGED
package/scripts/sync-skills.mjs
CHANGED
|
@@ -46,6 +46,7 @@ const SOURCES = {
|
|
|
46
46
|
'clearly-docs': 'apps/mcp-server/plugin/skills/clearly-docs/SKILL.md',
|
|
47
47
|
'clearly-init': 'apps/mcp-server/plugin/skills/clearly-init/SKILL.md',
|
|
48
48
|
'clearly-workflows': 'apps/mcp-server/plugin/skills/clearly-workflows/SKILL.md',
|
|
49
|
+
'clearly-artifacts': 'apps/mcp-server/plugin/skills/clearly-artifacts/SKILL.md',
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
let missing = 0;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: clearly-artifacts
|
|
3
|
+
description: >-
|
|
4
|
+
Needs a terminal with the beehaven CLI (Claude Code, Codex, Gemini CLI); in a chat with only the Clearly connector, say so: the connector cannot make a running page.
|
|
5
|
+
Put a self-contained interactive HTML page into a Clearly workspace, the way a
|
|
6
|
+
Claude artifact works: an explainer, an animation, a small tool, a calculator, a
|
|
7
|
+
chart that runs. Covers which of the three HTML homes runs scripts (a SITE does,
|
|
8
|
+
a canvas ARTIFACT card does but cannot be clicked, a NOTE never does), the exact
|
|
9
|
+
CLI calls, the sandbox limits, updating, filing in a project, how a person opens
|
|
10
|
+
it, and QA. Use when asked to publish, host or add an HTML page, demo or animation
|
|
11
|
+
to the workspace. Triggers: "make an artifact", "publish this HTML", "add this page /
|
|
12
|
+
animation / explainer to the workspace", "host this demo", "interactive page",
|
|
13
|
+
"put this HTML on the canvas", "why doesn't my page's script run".
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Clearly artifacts — a running HTML page in the workspace
|
|
17
|
+
|
|
18
|
+
Sign in first (`clearly-agent`). `beehaven env` must name the environment you mean; staging and
|
|
19
|
+
production hold different workspaces.
|
|
20
|
+
|
|
21
|
+
## 1. Pick the home — only two of the three run code
|
|
22
|
+
|
|
23
|
+
| Home | Make it with | Scripts and CSS animation | Clickable | Search, history | Opens at |
|
|
24
|
+
|---|---|---|---|---|---|
|
|
25
|
+
| **Site** | `compose-upload-init` → `file-teleport-inline` → `compose-upload-finalize` | run | yes | not in `document-search`; no version history; a re-upload overwrites | Recents or project card → `/compositions/detail?id=<id>` |
|
|
26
|
+
| **Canvas artifact** | `canvas-act` `canvas.add-artifact` | run | **no**: the card never takes pointer events, so it pans like a picture | no; it is part of the canvas and is deleted with it | the canvas, `/c/<compositionId>` |
|
|
27
|
+
| **Note** | `document-create {html}`; `page-write` (in-app chat only) | **never**: it opens in the document editor, which drops `<script>`, `<style>` and inline `<svg>` | n/a | yes, both | `/documents/<id>` |
|
|
28
|
+
|
|
29
|
+
- An interactive page someone opens, clicks through or comes back to is a **site**. This is the
|
|
30
|
+
Claude-artifact equivalent and the default.
|
|
31
|
+
- A page that only needs to be **seen** beside other cards (a looping animation, a live chart in a
|
|
32
|
+
review board) is a **canvas artifact**. Buttons inside it cannot be pressed.
|
|
33
|
+
- Prose the team will read, search and edit is a **note**. Never put a script in a note: it
|
|
34
|
+
is stored, and then nothing ever runs it. Editing the note in the app rewrites its body.
|
|
35
|
+
|
|
36
|
+
No home gives a public link that runs. Every share link (`/r/…`) is served with
|
|
37
|
+
`script-src 'none'`, and a shared composition renders as a static picture. Say so when someone asks to
|
|
38
|
+
publish an interactive page outside the workspace.
|
|
39
|
+
|
|
40
|
+
## 2. Write it self-contained
|
|
41
|
+
|
|
42
|
+
Both running homes inject the page with `srcdoc` into `<iframe sandbox="allow-scripts">`, with no
|
|
43
|
+
`allow-same-origin`. The page gets an opaque origin (`window.origin === "null"`).
|
|
44
|
+
|
|
45
|
+
- **One file.** `<!doctype html>`, `<meta name="viewport" content="width=device-width, initial-scale=1">`,
|
|
46
|
+
CSS in `<style>`, JS in `<script>`, art as inline SVG, images and fonts as `data:` URIs,
|
|
47
|
+
system font stacks. Make no network requests and load no CDN scripts.
|
|
48
|
+
- **The sandbox blocks:** `localStorage`, `sessionStorage`, cookies and IndexedDB (accessing them
|
|
49
|
+
throws), `alert`/`confirm`/`prompt`, form submission, popups, `window.open` and top-level
|
|
50
|
+
navigation. Keep state in memory, and wrap any storage access in `try`.
|
|
51
|
+
- **A site's other files are rarely reached.** The preview rewrites only `src="…"`/`href="…"`
|
|
52
|
+
in the entry HTML that name a build file exactly (up to 60 files). It does not rewrite `url()`
|
|
53
|
+
inside CSS, `import`, or `fetch` of a sibling file. Inline them.
|
|
54
|
+
- **Size:** a canvas artifact is at most 512 KB. A site file is at most 6 MB inline. Over the CLI
|
|
55
|
+
WebSocket, keep a single `beehaven call` under about 900 KB, or use `beehaven compose push`,
|
|
56
|
+
which switches to a presigned upload.
|
|
57
|
+
- **Motion:** honour `@media (prefers-reduced-motion: reduce)`. A canvas card lays the page out
|
|
58
|
+
at `designWidth` (default 1280) and grows to the document's full height. A `100vh` layout
|
|
59
|
+
makes it grow again on every fit, so size sections in px or by content.
|
|
60
|
+
|
|
61
|
+
## 3. Make it (CLI)
|
|
62
|
+
|
|
63
|
+
Build JSON payloads with a script and pass them as `@file`. Do not hand-escape HTML into a shell
|
|
64
|
+
argument.
|
|
65
|
+
|
|
66
|
+
**Site, from one file** (one `file-teleport-inline` per file; `entryPoint` and each path are
|
|
67
|
+
relative to `build/`):
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
beehaven call compose-upload-init '{"name":"MCP tokens explained","sourceType":"static","entryPoint":"index.html","projectId":"<projectId>"}'
|
|
71
|
+
# → { "id": "cmp_…", "created": true }
|
|
72
|
+
node -e 'const fs=require("fs"),b=fs.readFileSync("page.html");fs.writeFileSync("up.json",JSON.stringify({key:"compositions/<id>/build/index.html",composition:true,name:"index.html",mimeType:"text/html",size:b.length,data:b.toString("base64")}))'
|
|
73
|
+
beehaven call file-teleport-inline @up.json
|
|
74
|
+
beehaven call compose-upload-finalize '{"compositionId":"<id>","totalBytes":<bytes>,"entryPoint":"index.html"}'
|
|
75
|
+
beehaven call composition-fs '{"compositionId":"<id>","action":"list"}' # one row per file, sizes match
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Both parts of the upload are load-bearing. A `key` outside `compositions/<id>/build/`, or a
|
|
79
|
+
missing `composition:true`, still answers ok, and the site then renders nothing.
|
|
80
|
+
|
|
81
|
+
**Site, from a folder** that holds only the page and its assets:
|
|
82
|
+
`cd <folder> && beehaven compose create --name "<title>" && beehaven compose push`. Every file in
|
|
83
|
+
the folder is uploaded. A Vite, Next, Astro or SvelteKit `package.json` there runs its build first.
|
|
84
|
+
To push to an existing site, run `beehaven compose push --composition <id>` from the folder; there
|
|
85
|
+
is no `--dir` flag. `compose create` cannot file the site, so file it afterwards (§4). Ignore the
|
|
86
|
+
URL that `push` prints, because it does not open the site.
|
|
87
|
+
|
|
88
|
+
**Canvas artifact** (create or reuse the canvas with `composition-create {title, projectId}`):
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
# payload: {"compositionId":"<canvasId>","action":"canvas.add-artifact",
|
|
92
|
+
# "args":{"html":"<!doctype html>…","title":"…","name":"explainer","x":0,"y":0,"w":1000,"designWidth":1000}}
|
|
93
|
+
beehaven call canvas-act @artifact.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`w` is the card's width on the board; `designWidth` is the viewport the page lays out at. Use 390
|
|
97
|
+
for a phone design. `canvas.place {kind:"site", id}` puts an existing site on a canvas as an
|
|
98
|
+
artifact card. It copies the entry HTML at that moment: later re-uploads do not reach the card,
|
|
99
|
+
and the site's other files do not load.
|
|
100
|
+
|
|
101
|
+
**Connector only (MCP):** no tool makes a site or an artifact. `clearly_file_upload` cannot choose
|
|
102
|
+
a site path, and `clearly_create` stores a document body as markdown. `clearly_canvas_place
|
|
103
|
+
{target, kind:"site", id}` can place a site that already exists. Otherwise, tell the person the
|
|
104
|
+
running page needs the CLI.
|
|
105
|
+
|
|
106
|
+
## 4. Update, file, link
|
|
107
|
+
|
|
108
|
+
- **Update a site:** upload to the same `key`, then `compose-upload-finalize` again. Only someone
|
|
109
|
+
who may manage the site (its creator) can overwrite it. A file you stop shipping stays in the
|
|
110
|
+
build. Agents are refused `composition-fs … "action":"delete"`, so ask the person to remove it,
|
|
111
|
+
or stop referencing it. There is no history, so keep the source in the repository or a note.
|
|
112
|
+
- **Update a canvas artifact:** there is no edit verb. Add the new version at the old card's
|
|
113
|
+
`x`/`y`/`w`, then remove the old card with `canvas.delete {ids:[…]}`.
|
|
114
|
+
- **File:** `projectId` on `compose-upload-init` (to file later, call it again with
|
|
115
|
+
`{"compositionId","projectId"}`). For a canvas, `composition-create {projectId}`. For a note,
|
|
116
|
+
`document-create {projectId}` or `document-update {id, projectId}`. Unfiled agent work is
|
|
117
|
+
private to its author.
|
|
118
|
+
- **Link it to the ticket:** `beehaven call work-link '{"ticketId":"…","kind":"design","refKind":"composition","refId":"<id>"}'`.
|
|
119
|
+
|
|
120
|
+
## 5. Hand it over
|
|
121
|
+
|
|
122
|
+
Give the person the place to click, not just an id.
|
|
123
|
+
|
|
124
|
+
- A site: "Recents (or the project) → the card", which opens `/compositions/detail?id=<id>`.
|
|
125
|
+
Do not hand over `/c/<siteId>`. That is what `beehaven open`, ticket links and workspace-ref
|
|
126
|
+
chips build for a site, and it opens an empty canvas instead of the page.
|
|
127
|
+
- A canvas artifact: `/c/<canvasId>`. Double-clicking the card opens its stored file in a new tab,
|
|
128
|
+
which shows source text or an access error, never the running page.
|
|
129
|
+
|
|
130
|
+
## 6. QA — open it, read the console, check mobile
|
|
131
|
+
|
|
132
|
+
1. **Render it in the real frame model** before uploading. Use Playwright: an
|
|
133
|
+
`<iframe sandbox="allow-scripts" srcdoc>` at 1280 and 390 wide. Confirm there are no console
|
|
134
|
+
errors and no requests from the frame, that `scrollWidth === clientWidth`, that the animation
|
|
135
|
+
advances by itself, and that its controls respond.
|
|
136
|
+
2. **Read the stored copy back:** check that `composition-fs list` shows the size you sent.
|
|
137
|
+
3. **See the server render it.** For a canvas artifact, run
|
|
138
|
+
`canvas-act {"compositionId":"<canvasId>","action":"canvas.review","args":{"ids":["<artifactId>"]}}`.
|
|
139
|
+
It returns a filmstrip of four frames across about 5.4 s; inspect the PNG. For a site, place it on a scratch canvas
|
|
140
|
+
(`canvas.place {kind:"site", id}`), review that card the same way, and then archive the scratch
|
|
141
|
+
canvas (`composition-set-archived {id, archived:true}`).
|
|
142
|
+
4. Report what you checked and what you could not check. Opening the signed-in page itself needs a
|
|
143
|
+
person's session.
|
|
@@ -32,7 +32,7 @@ For a new composition or substantial redesign, use the workflow below.
|
|
|
32
32
|
|
|
33
33
|
Keep the requested scope. A landing concept with a logo needs its page layouts and a usable
|
|
34
34
|
logo; add application boards or a full guideline system only when the brief calls for them.
|
|
35
|
-
Read only relevant references. Save large tool responses once and print compact summaries.
|
|
35
|
+
Read only relevant references. Save large tool responses once (`beehaven call … --out file.json`) and print compact summaries.
|
|
36
36
|
|
|
37
37
|
## The studio bar — what "done" means, every time
|
|
38
38
|
|
|
@@ -46,7 +46,8 @@ A composition is done when every requested artboard meets all five conditions:
|
|
|
46
46
|
remains: text colliding or covered, contrast under WCAG (4.5:1 body, 3:1 display),
|
|
47
47
|
type below the artboard's legible floor, missing media, text overflowing its box,
|
|
48
48
|
placeholder copy. Fix it. For an intentional finding, use
|
|
49
|
-
`accept:[{check,ids:[node IDs],reason}]
|
|
49
|
+
`accept:[{check,ids:[node IDs],reason}]`; one rule may name every layer with the same
|
|
50
|
+
deliberate finding. Accepted findings stay visible and return `look`.
|
|
50
51
|
Legacy `ignore` can suppress multiple checks and is not an unconditional pass.
|
|
51
52
|
2. **Every `look` finding was looked at**: type-size and palette sprawl, near-miss
|
|
52
53
|
alignment, copy against the trim, text over imagery. Act on it or say why not.
|
|
@@ -117,8 +118,9 @@ Read `clearly-agent` before your first workspace call. Pin the intended workspac
|
|
|
117
118
|
sign in once. Do not switch a user's explicit workspace to `home`.
|
|
118
119
|
|
|
119
120
|
Your surface: the shell. The `beehaven` examples in this skill are literal commands to
|
|
120
|
-
run; they are not shapes to translate into an MCP canvas-action tool.
|
|
121
|
-
|
|
121
|
+
run; they are not shapes to translate into an MCP canvas-action tool. The Clearly connector
|
|
122
|
+
has its own canvas tools for agents without a shell (batched per kind of step, see
|
|
123
|
+
authoring.md); in a shell the CLI's mixed `canvas-act` batch takes fewer calls.
|
|
122
124
|
|
|
123
125
|
```sh
|
|
124
126
|
beehaven env
|
|
@@ -156,7 +158,7 @@ builds on the last and can be reviewed alone. Build ONE commit per canvas-act ba
|
|
|
156
158
|
`canvas.review` of the artboard and carries
|
|
157
159
|
`"ticket":{"id":"<key>","status":"done","resolution":"<what changed>"}` — it lands only if that
|
|
158
160
|
review is clean or look; `result.ticket` says whether it did, and the ticket logs the canvas
|
|
159
|
-
revision
|
|
161
|
+
revision, the layers the commit changed and a link to its frame. Auto-layout, gradients, shadows, drawing and placeholders:
|
|
160
162
|
see "Technique" in [authoring.md](authoring.md).
|
|
161
163
|
|
|
162
164
|
`composition-board-get {compositionId}` reads the plan back. `design-start` with a project, ticket or
|
|
@@ -279,7 +281,7 @@ a detail view catches collisions, wrapping, cropping and legibility.
|
|
|
279
281
|
The bundled helper saves pixels directly to disk so base64 does not fill your context:
|
|
280
282
|
|
|
281
283
|
```sh
|
|
282
|
-
node <skill-directory>/review.mjs <composition-id> <frame-id-or-unique-name>
|
|
284
|
+
node <skill-directory>/review.mjs <composition-id> <frame-id-or-unique-name> canvas-review/01
|
|
283
285
|
# Open the returned PNG using your image-viewing tool; read the JSON findings.
|
|
284
286
|
```
|
|
285
287
|
|
|
@@ -300,9 +302,15 @@ The helper prints the verdict and every finding. Work until the verdict is `clea
|
|
|
300
302
|
use a real frame ID with `--perceive` for a preview-only check and run the text measurement
|
|
301
303
|
and audit separately. A failed or missing render means visual verification is incomplete.
|
|
302
304
|
|
|
305
|
+
A deliberate finding (say why in the handoff): write the findings you are keeping to a file and
|
|
306
|
+
pass it to the same helper. They stay visible in the report and return `look`.
|
|
307
|
+
|
|
308
|
+
```json
|
|
309
|
+
[{"check":"text-over-image","ids":["<text-id>","<image-id>"],"reason":"Intentional type over this photograph; rendered contrast was inspected."}]
|
|
310
|
+
```
|
|
311
|
+
|
|
303
312
|
```sh
|
|
304
|
-
|
|
305
|
-
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."}]}}'
|
|
313
|
+
node <skill-directory>/review.mjs <composition-id> <frame> canvas-review/02 --accept accepted.json
|
|
306
314
|
```
|
|
307
315
|
|
|
308
316
|
Asset storage is a reference-aware pipeline: an SVG may have `svgRef` instead of inline markup.
|
|
@@ -38,15 +38,34 @@ add('canvas.update-nodes', { ids: ['opening-title', 'opening-deck'], patch: { la
|
|
|
38
38
|
add('canvas.auto-layout', { id: 'hero-copy', direction: 'v', gap: 24, sizingH: 'fixed', sizingV: 'hug' });
|
|
39
39
|
add('canvas.auto-layout', { id: '01-opening', direction: 'v', gap: 64, padding: grid.margin, sizingH: 'fixed', sizingV: 'hug' });
|
|
40
40
|
// Continue with the brief's central visual, supporting evidence and footer.
|
|
41
|
-
writeFileSync('
|
|
41
|
+
writeFileSync('canvas-build.json', JSON.stringify({ compositionId, batch }));
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
```sh
|
|
45
45
|
beehaven call composition-create '{"title":"Project — concept","projectId":"<project-id>"}'
|
|
46
46
|
node build.mjs <returned-id>
|
|
47
|
-
beehaven call canvas-act
|
|
47
|
+
beehaven call canvas-act @canvas-build.json --brief
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
`--brief` prints a short receipt instead of the full JSON:
|
|
51
|
+
- step counts by kind;
|
|
52
|
+
- layers added, changed and removed;
|
|
53
|
+
- every failed step with its error;
|
|
54
|
+
- rollbacks and skips;
|
|
55
|
+
- lint to fix and review verdicts.
|
|
56
|
+
|
|
57
|
+
The exit code stays the same. Use it for routine batches instead of piping the JSON through a
|
|
58
|
+
summariser of your own. Drop it when you need a step's full value.
|
|
59
|
+
|
|
60
|
+
Keep builders, payloads and results in your **working directory**:
|
|
61
|
+
- Write files with your file-writing tool, not shell heredocs or `>` redirects into `/tmp`.
|
|
62
|
+
- Run each `beehaven` command on its own rather than chained with `&&`, `;`, `| tee` or `| head`.
|
|
63
|
+
- Save a large result with `beehaven call <action> '…' --out scene.json` (add `--brief` for the
|
|
64
|
+
receipt), not `> file`: the whole JSON goes to the file and only a line reaches the console.
|
|
65
|
+
|
|
66
|
+
Agent hosts that gate shell commands (Claude Code, Codex) refuse heredocs containing JSON and
|
|
67
|
+
compound pipelines outright. One recorded run lost four tool calls that way (MCP2-20).
|
|
68
|
+
|
|
50
69
|
Check `ok`, each `results[].ok`, `count`, `skipped` and `rolledBack`. A failed headless
|
|
51
70
|
batch may roll back its scene writes. Re-read the scene before deciding what to retry;
|
|
52
71
|
do not blindly resend only skipped operations. Mixed live/headless actions may have
|
|
@@ -155,6 +174,7 @@ The same rules the in-app assistant works to (canvas-catalog returns them as `te
|
|
|
155
174
|
- COMPUTED GEOMETRY: shapes from math (radial petals, spirals, grids) are computed and drawn in ONE call: canvas-act { compositionId, script } runs JavaScript with geo.petal/circle/arc/star/polygon/spiral/smooth/rotate/radial/grid/bounds (degrees, 0 = 3 o'clock, clockwise) and a read-only scene list, then applies the { action, args } steps it returns. Never copy numbers from one tool result into another call. A new top-level artboard goes beside or below what scene already holds (or leave out both x and y), never at 0,0 over an existing one.
|
|
156
175
|
- SHEETS: several concepts or variants go on canvas.sheet { name, title, subtitle, cards or count, cols, cardW, cardH }: a live wrap grid of card frames that own their captions, so rows never overlap. Draw into a card with parentId = its id.
|
|
157
176
|
- MEASURE: answer spacing, overlap and alignment questions with canvas.measure { frame } or { ids } and quote its numbers, never from memory or a preview.
|
|
177
|
+
- REFERENCES: canvas.link {id:<exact frame/item id>} returns a stable URL and Markdown for tickets, documents, comments and inbox refs. Use the returned link instead of a screenshot or a guessed canvas URL. It preserves the parent canvas permissions; rename/move retains the layer id.
|
|
158
178
|
- PLACEHOLDERS: until real imagery exists, draw a native placeholder at the final size in a muted brand tone, never a flat accent block: a rect with the slot radius plus a simple motif (an ellipse sun, a vector.create mountain), grouped with canvas.group and named "<section> image placeholder", with any label in its own text node.
|
|
159
179
|
- 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.
|
|
160
180
|
|
|
@@ -215,15 +235,13 @@ the backing shows through as dark wedges at the corners.
|
|
|
215
235
|
|
|
216
236
|
### Images from MCP
|
|
217
237
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
`
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
existing image's `src`, `fit` or geometry, but cannot create a node. For a local file,
|
|
226
|
-
use the CLI upload path first; an MCP-only client needs a URL it can already access.
|
|
238
|
+
A connector-only agent places an image already reachable by an absolute URL (or a
|
|
239
|
+
`data:` URI) as an `image` node in `clearly_canvas_add_nodes`:
|
|
240
|
+
`{id,type:"image",name,src,x,y,w,h,fit:"contain",parentId?}`. Use PNG or JPEG: the headless
|
|
241
|
+
review cannot draw WebP yet and does not say so (CAN2-188), so a WebP image looks blank in the
|
|
242
|
+
review PNG while the editor shows it. `clearly_canvas_update_nodes`
|
|
243
|
+
changes an existing image's `src`, `fit` or geometry. For a local file in a shell, use the
|
|
244
|
+
CLI upload path first.
|
|
227
245
|
|
|
228
246
|
Current headless rendering can resolve images, including workspace media. A blank image
|
|
229
247
|
can indicate fetch, decoding or budget failure; check its source and inspect a live view.
|
|
@@ -236,8 +254,8 @@ design. Verify the returned artifact in a browser at realistic viewport sizes.
|
|
|
236
254
|
## Review and scoped perception
|
|
237
255
|
|
|
238
256
|
```sh
|
|
239
|
-
beehaven call canvas-act '{"compositionId":"<id>","action":"canvas.review","args":{"ids":["<frame-id>"]}}'
|
|
240
|
-
beehaven call canvas-perceive '{"compositionId":"<id>","includePixels":true,"fitNodeIds":["<frame-id>"],"pixelScale":1}' >
|
|
257
|
+
beehaven call canvas-act '{"compositionId":"<id>","action":"canvas.review","args":{"ids":["<frame-id>"]}}' --brief
|
|
258
|
+
beehaven call canvas-perceive '{"compositionId":"<id>","includePixels":true,"fitNodeIds":["<frame-id>"],"pixelScale":1}' > perceive.json
|
|
241
259
|
```
|
|
242
260
|
|
|
243
261
|
Use the bundled `review.mjs` to decode the PNG and keep base64 out of the conversation.
|
|
@@ -268,7 +286,12 @@ MCP's artifact tools are split by operation:
|
|
|
268
286
|
node array — pass `expectedRev` from the read.
|
|
269
287
|
- `clearly_edit` patches existing nodes with `changes:{ids,patch}`.
|
|
270
288
|
|
|
271
|
-
|
|
272
|
-
`
|
|
273
|
-
|
|
274
|
-
|
|
289
|
+
The connector also has native canvas tools for agents without a shell, batched per kind of
|
|
290
|
+
step rather than mixed (MCP2-22): `clearly_canvas_add_nodes` builds a section per call (up to
|
|
291
|
+
300 frames, shapes, text, pen paths and groups of its own new layers),
|
|
292
|
+
`clearly_canvas_update_nodes {edits:[{ids,patch}]}` revises many layers at once,
|
|
293
|
+
`clearly_canvas_measure_text {texts:[…]}` measures several lines, and
|
|
294
|
+
`clearly_canvas_review` returns the saved artboard as an MCP image to inspect. There is no
|
|
295
|
+
`clearly_canvas_act`: a mixed batch is the CLI's `canvas-act`. In a shell, use the CLI; it
|
|
296
|
+
is fewer calls for the same scene. Do not claim the design was visually checked from scene
|
|
297
|
+
JSON alone.
|
|
@@ -123,14 +123,35 @@ export async function saveReviewPixels(report, prefix, fetchImage = fetch) {
|
|
|
123
123
|
return saveReview({ ...report, preview: { ...report.preview, dataUrl: `data:image/png;base64,${bytes.toString('base64')}` } }, prefix);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* `--accept <findings.json>`: findings you have looked at and are keeping, each
|
|
128
|
+
* `{ check, ids: [node IDs], reason }`. They stay visible in the report and return `look`.
|
|
129
|
+
* Without this, a deliberate overlap (steam rising out of a bowl) could only be accepted by
|
|
130
|
+
* hand-writing the canvas.review call — agents did exactly that (MCP2-20).
|
|
131
|
+
*/
|
|
132
|
+
export function takeAccept(argv) {
|
|
133
|
+
const i = argv.indexOf('--accept');
|
|
134
|
+
if (i < 0) return { argv, accept: undefined };
|
|
135
|
+
const file = argv[i + 1];
|
|
136
|
+
if (!file || file.startsWith('--')) throw new Error('--accept needs a JSON file: [{"check":"text-over-image","ids":["…"],"reason":"…"}]');
|
|
137
|
+
const accept = JSON.parse(readFileSync(file, 'utf8'));
|
|
138
|
+
if (!Array.isArray(accept) || !accept.every(a => a && typeof a.check === 'string' && Array.isArray(a.ids) && a.ids.length && typeof a.reason === 'string' && a.reason.trim())) {
|
|
139
|
+
throw new Error('--accept file must be an array of { check, ids: [node IDs], reason } with a non-empty reason.');
|
|
140
|
+
}
|
|
141
|
+
return { argv: argv.filter((_, j) => j !== i && j !== i + 1), accept };
|
|
142
|
+
}
|
|
143
|
+
|
|
126
144
|
export async function main(argv = process.argv.slice(2)) {
|
|
145
|
+
const taken = takeAccept(argv);
|
|
146
|
+
argv = taken.argv;
|
|
147
|
+
const accept = taken.accept ? { accept: taken.accept } : {};
|
|
127
148
|
if (argv.includes('--many')) {
|
|
128
149
|
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>');
|
|
150
|
+
if (args.length !== 3) throw new Error('Usage: node review.mjs --many <compositionId> <artboard-ids.json> <output-directory> [--accept findings.json]');
|
|
130
151
|
const [compositionId, manifest, directory] = args;
|
|
131
152
|
const ids = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
132
153
|
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' } });
|
|
154
|
+
const response = rpc('canvas-act', { compositionId, action: 'canvas.review-many', args: { ids, preview: 'files', ...accept } });
|
|
134
155
|
const reports = response.results?.[0]?.value?.reviews;
|
|
135
156
|
if (!Array.isArray(reports) || reports.length !== ids.length) throw new Error('Incomplete review-many result; do not claim all artboards passed.');
|
|
136
157
|
const saved = [];
|
|
@@ -140,7 +161,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
140
161
|
}
|
|
141
162
|
const legacy = argv.includes('--perceive');
|
|
142
163
|
const args = argv.filter((a) => a !== '--perceive');
|
|
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.');
|
|
164
|
+
if (args.length !== 3) throw new Error('Usage: node review.mjs <compositionId> <frame-id-or-name> <output-prefix> [--accept findings.json] [--perceive]\n--perceive: older workers, preview only; pass a frame ID.');
|
|
144
165
|
const [compositionId, id, prefix] = args;
|
|
145
166
|
let report;
|
|
146
167
|
if (legacy) {
|
|
@@ -150,7 +171,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
150
171
|
report = { preview: response.room?.pixels, visualReviewRequired: true,
|
|
151
172
|
limitations: ['Preview-only compatibility mode. Text metrics and scoped audit were not run.'] };
|
|
152
173
|
} else {
|
|
153
|
-
const response = rpc('canvas-act', { compositionId, action: 'canvas.review', args: { ids: [id], pixelScale: 1 } });
|
|
174
|
+
const response = rpc('canvas-act', { compositionId, action: 'canvas.review', args: { ids: [id], pixelScale: 1, ...accept } });
|
|
154
175
|
report = response.results?.[0]?.value;
|
|
155
176
|
if (!report?.scope) throw new Error('canvas.review unavailable. Check canvas-catalog; use --perceive for an explicitly preview-only check on an older worker.');
|
|
156
177
|
}
|
package/skills/index.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"project-plans.md",
|
|
18
18
|
"review.mjs"
|
|
19
19
|
],
|
|
20
|
-
"sha": "
|
|
20
|
+
"sha": "4b478740d8ef22c2"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "agent-home",
|
|
@@ -118,5 +118,12 @@
|
|
|
118
118
|
"SKILL.md"
|
|
119
119
|
],
|
|
120
120
|
"sha": "b9622f21173bab0d"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"name": "clearly-artifacts",
|
|
124
|
+
"files": [
|
|
125
|
+
"SKILL.md"
|
|
126
|
+
],
|
|
127
|
+
"sha": "14434d4904da91a3"
|
|
121
128
|
}
|
|
122
129
|
]
|